Compare commits

..

No commits in common. "842f69ebc97a0160dcc8cc5dffee26b144684fba" and "2fbee208e75b882cd4b53be1976f761efe441dba" have entirely different histories.

25 changed files with 295 additions and 1222 deletions

View File

@ -11,14 +11,10 @@ FROM ghcr.io/cirruslabs/flutter:3.44.0 AS build
# docker build --build-arg API_BASE_URL=https://api.mymuseum.be -t ... .
ARG API_BASE_URL=http://localhost:5000
# Empreinte du commit, affichee sur l'ecran de login (voir kGitSha).
# docker build --build-arg GIT_SHA=$(git rev-parse --short HEAD) ...
ARG GIT_SHA=dev
WORKDIR /app
COPY . .
RUN flutter pub get
RUN flutter build web --release --dart-define=API_BASE_URL=$API_BASE_URL --dart-define=GIT_SHA=$GIT_SHA
RUN flutter build web --release --dart-define=API_BASE_URL=$API_BASE_URL
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf

View File

@ -15,8 +15,13 @@ class QuotaBarsWidget extends StatefulWidget {
class _QuotaBarsWidgetState extends State<QuotaBarsWidget> {
InstanceQuotaDTO? _quota;
bool _loading = false;
String? _loadedInstanceId;
bool _loading = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _fetchQuota());
}
Future<void> _fetchQuota() async {
final managerContext = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
@ -67,17 +72,6 @@ class _QuotaBarsWidgetState extends State<QuotaBarsWidget> {
final isAssistant = managerContext.instanceDTO?.isAssistant ?? false;
final l10n = AppLocalizations.of(context)!;
// Premier rendu, mais aussi changement d'instance par un SuperAdmin : sans
// ce test, les barres resteraient sur le stockage de l'instance précédente.
final instanceId = managerContext.instanceId;
if (instanceId != _loadedInstanceId) {
_loadedInstanceId = instanceId;
_loading = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _fetchQuota();
});
}
if (_loading) {
return const SizedBox(
height: 8,

View File

@ -1,151 +0,0 @@
import 'package:flutter/material.dart';
import 'package:html/parser.dart' show parse;
import 'package:manager_app/constants.dart';
import 'package:manager_app/l10n/app_localizations.dart';
import 'package:manager_api_new/api.dart';
/// Filtre par catégorie de la liste des points.
///
/// Il passait par `MultiSelectContainer` : un `ListView` horizontal dans un
/// `SingleChildScrollView` horizontal, largeur figée à 25 % de l'écran. Au-delà
/// de deux catégories les suivantes étaient hors champ, sans aucun indice.
/// Ici les puces reviennent à la ligne et toutes sont visibles.
class CategoryFilter extends StatelessWidget {
const CategoryFilter({
Key? key,
required this.categories,
required this.selectedIds,
required this.onChanged,
}) : super(key: key);
final List<CategorieDTO> categories;
final List<int> selectedIds;
final ValueChanged<List<int>> onChanged;
static String labelOf(CategorieDTO categorie) {
final labels = categorie.label ?? [];
if (labels.isEmpty) return "";
final raw = labels
.firstWhere((t) => t.language == 'FR', orElse: () => labels.first)
.value ??
"";
return parse(raw).documentElement?.text.trim() ?? raw;
}
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(l.categoriesLabel, style: kLabelField),
if (selectedIds.isNotEmpty) ...[
const SizedBox(width: kSpace3),
_ClearButton(onPressed: () => onChanged([])),
],
],
),
const SizedBox(height: kSpace2),
Wrap(
spacing: kSpace2,
runSpacing: kSpace2,
children: [
for (final categorie in categories)
_CategoryChip(
label: labelOf(categorie),
isSelected: selectedIds.contains(categorie.id),
onTap: () {
final next = List<int>.from(selectedIds);
next.contains(categorie.id)
? next.remove(categorie.id)
: next.add(categorie.id!);
onChanged(next);
},
),
],
),
],
);
}
}
class _CategoryChip extends StatelessWidget {
const _CategoryChip({
required this.label,
required this.isSelected,
required this.onTap,
});
final String label;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(kRadiusInput),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 120),
padding: const EdgeInsets.symmetric(
horizontal: kSpace3, vertical: kSpace2),
decoration: BoxDecoration(
color: isSelected ? kPrimaryColor : kSurface,
border: Border.all(color: isSelected ? kPrimaryColor : kLine),
borderRadius: BorderRadius.circular(kRadiusInput),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isSelected) ...[
const Icon(Icons.check, size: 13, color: kWhite),
const SizedBox(width: kSpace2),
],
Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? kWhite : kInk,
),
),
],
),
),
),
);
}
}
class _ClearButton extends StatelessWidget {
const _ClearButton({required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(kRadiusInput),
onTap: onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: kSpace2),
child: Text(
AppLocalizations.of(context)!.clearFilter,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: kPrimaryColor),
),
),
),
);
}
}

View File

@ -45,34 +45,10 @@ class GeoPointEditor extends StatefulWidget {
State<GeoPointEditor> createState() => _GeoPointEditorState();
}
/// Une géométrie dessinée mais pas encore validée.
class _PendingGeometry {
const _PendingGeometry(this.geometry, this.color);
final GeometryDTO geometry;
final String color;
}
class _GeoPointEditorState extends State<GeoPointEditor> {
int? selectedId;
bool isMapExpanded = false;
/// Un déplacement sur la carte partait au serveur au relâchement de la
/// souris : le moindre glissement involontaire était écrit, et « Annuler »
/// de l'en-tête ne le défaisait pas. Il attend maintenant sa validation.
/// Gardé par point pour qu'un aller-retour dans la liste ne le perde pas.
final Map<int, _PendingGeometry> _pending = {};
void _commit(GeoPointDTO point) {
final pending = _pending[point.id];
if (pending == null) return;
setState(() {
point.geometry = pending.geometry;
point.polyColor = pending.color;
_pending.remove(point.id);
});
widget.onPointChanged(point);
}
GeoPointDTO? get _selected {
if (widget.visiblePoints.isEmpty) return null;
return widget.visiblePoints.firstWhere(
@ -289,34 +265,12 @@ class _GeoPointEditorState extends State<GeoPointEditor> {
Widget _buildCanvas(AppLocalizations l, GeoPointDTO? selected,
{required double height}) {
final pending = selected == null ? null : _pending[selected.id];
return Padding(
padding: const EdgeInsets.all(kSpace4),
child: Stack(
children: [
_buildMapCanvas(l, selected, pending, height),
if (pending != null)
Positioned(
right: kSpace4,
bottom: kSpace8,
child: _PendingGeometryActions(
onConfirm: () => _commit(selected!),
onDiscard: () =>
setState(() => _pending.remove(selected!.id)),
),
),
],
),
);
}
Widget _buildMapCanvas(AppLocalizations l, GeoPointDTO? selected,
_PendingGeometry? pending, double height) {
return MapCanvas(
child: MapCanvas(
key: ValueKey(selected?.id ?? 'none'),
geometry: pending?.geometry ?? selected?.geometry,
color: pending?.color ?? selected?.polyColor,
geometry: selected?.geometry,
color: selected?.polyColor,
height: height,
// Les points d'une carte sont stockés en GeoJSON — voir
// `visitapp-web/src/lib/geo.ts`, qui les lit en `[lng, lat]`.
@ -328,18 +282,18 @@ class _GeoPointEditorState extends State<GeoPointEditor> {
: _label(l, selected, widget.visiblePoints.indexOf(selected)),
ghosts: [
for (final point in widget.visiblePoints)
if (point.id != selected?.id &&
(_pending[point.id]?.geometry ?? point.geometry) != null)
if (point.id != selected?.id && point.geometry != null)
MapCanvasGhost(
geometry: _pending[point.id]?.geometry ?? point.geometry!,
color: _pending[point.id]?.color ?? point.polyColor),
geometry: point.geometry!, color: point.polyColor),
],
onChanged: (geometry, color) {
if (selected == null) return;
setState(() =>
_pending[selected.id!] = _PendingGeometry(geometry, color));
selected.geometry = geometry;
selected.polyColor = color;
widget.onPointChanged(selected);
},
);
),
);
}
Widget _buildForm(AppLocalizations l, GeoPointDTO? selected) {
@ -368,54 +322,6 @@ class _GeoPointEditorState extends State<GeoPointEditor> {
}
}
/// Posé sur la carte quand une position a bougé sans être écrite.
class _PendingGeometryActions extends StatelessWidget {
const _PendingGeometryActions({
required this.onConfirm,
required this.onDiscard,
});
final VoidCallback onConfirm;
final VoidCallback onDiscard;
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
return Material(
elevation: 3,
borderRadius: BorderRadius.circular(kRadiusInput),
color: kSurface,
child: Padding(
padding: const EdgeInsets.all(kSpace2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: onDiscard,
child: Text(l.cancel,
style: const TextStyle(fontSize: 12, color: kInk3)),
),
const SizedBox(width: kSpace1),
FilledButton.icon(
onPressed: onConfirm,
icon: const Icon(Icons.check, size: 15),
label: Text(l.geopointConfirmPosition,
style: const TextStyle(fontSize: 12)),
style: FilledButton.styleFrom(
backgroundColor: kPrimaryColor,
foregroundColor: kWhite,
padding: const EdgeInsets.symmetric(
horizontal: kSpace4, vertical: kSpace2),
),
),
],
),
),
);
}
}
/// Les champs d'un point, repris de `showNewOrUpdateGeoPoint`.
///
/// Un point porte neuf champs traduits. Les quatre du haut sont presque

View File

@ -9,7 +9,7 @@ import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Map/category_input_container.dart';
import 'package:manager_app/Components/dropDown_input_container.dart';
import 'package:manager_app/Components/resource_input_container.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Map/category_filter.dart';
import 'package:manager_app/Components/multi_select_container.dart';
import 'package:manager_app/Components/single_select_container.dart';
import 'package:manager_app/Components/check_input_container.dart';
import 'package:manager_app/Components/slider_input_container.dart';
@ -226,10 +226,10 @@ class _MapConfigState extends State<MapConfig> {
}
Widget _buildPointsToolbar() {
final categories = mapDTO.categories ?? [];
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
return Wrap(
spacing: kSpace5,
runSpacing: kSpace4,
crossAxisAlignment: WrapCrossAlignment.end,
children: [
SizedBox(
width: 240,
@ -238,25 +238,45 @@ class _MapConfigState extends State<MapConfig> {
onChanged: (String value) => searchNotifier.value = value,
),
),
if (categories.isNotEmpty) ...[
const SizedBox(width: kSpace6),
// Prend la largeur restante pour que les puces reviennent à la ligne
// au lieu de sortir du cadre.
Expanded(
child: ValueListenableBuilder<List<int>?>(
valueListenable: selectedCategoriesNotifier,
builder: (context, selected, _) => CategoryFilter(
categories: categories,
selectedIds: selected ?? const [],
onChanged: (ids) => selectedCategoriesNotifier.value = ids,
),
if (mapDTO.categories != null && mapDTO.categories!.isNotEmpty)
SizedBox(
width: 320,
child: MultiSelectContainer(
label: AppLocalizations.of(context)!.categoriesLabel,
color: kSecond,
initialValue: mapDTO.categories!
.where((cat) =>
selectedCategoriesNotifier.value!.contains(cat.id))
.map((categorie) => _categoryLabel(categorie))
.toList(),
isMultiple: true,
isHTMLLabel: true,
values: mapDTO.categories!
.map((categorie) => _categoryLabel(categorie))
.toList(),
onChanged: (value) {
var tempOutput = new List<String>.from(value);
selectedCategoriesNotifier.value = mapDTO.categories!
.where((c) => tempOutput.contains(_categoryLabel(c)))
.map((cat) => cat.id!)
.toList();
},
),
),
],
],
);
}
String _categoryLabel(CategorieDTO categorie) {
final labels = categorie.label ?? [];
if (labels.isEmpty) return "";
return labels
.firstWhere((element) => element.language == 'FR',
orElse: () => labels.first)
.value ??
"";
}
/// Un point vit côté serveur : chaque champ validé et chaque déplacement sur
/// la carte part tout de suite.
Future<void> _savePoint(GeoPointDTO point) async {
@ -333,215 +353,158 @@ class _MapConfigState extends State<MapConfig> {
}
Widget _buildMapHeader(Size size, String mapProviderIn) {
final l = AppLocalizations.of(context)!;
// Décision du 2026-08-11 (lot E, W1) : visitapp-web reste sur Leaflet. Le
// champ ne peut pas être masqué pour le web il est porté par la section,
// et une même configuration est servie au mobile comme au web. La note vit
// sous le champ qu'elle qualifie, au lieu de flotter en pleine largeur.
final service = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
SingleSelectContainer(
label: l.serviceLabel,
color: Colors.black,
initialValue: mapProviderIn,
inputValues: map_providers,
onChanged: (String value) {
setState(() {
switch (value) {
case "Google":
mapDTO.mapProvider = MapProvider.Google;
break;
case "MapBox":
mapDTO.mapProvider = MapProvider.MapBox;
break;
}
widget.onChanged(mapDTO);
});
}),
const SizedBox(height: kSpace2),
Text(l.mapProviderMobileOnlyNote, style: kTextHint),
],
);
final typeField = mapDTO.mapProvider == MapProvider.Google
? DropDownInputContainer(
label: l.typeLabel,
values: map_types,
initialValue: mapType,
onChange: (String? value) {
mapDTO.mapType = MapTypeApp.fromJson(value);
widget.onChanged(mapDTO);
},
)
: DropDownInputContainer(
label: l.typeLabel,
values: map_types_mapBox,
initialValue: mapTypeMapBox,
onChange: (String? value) {
mapDTO.mapTypeMapbox = MapTypeMapBox.fromJson(value);
widget.onChanged(mapDTO);
},
);
final centerPoint = GeolocInputContainer(
label: l.centerPointLabel,
initialValue:
mapDTO.centerLatitude != null && mapDTO.centerLongitude != null
? LatLong(double.parse(mapDTO.centerLatitude!),
double.parse(mapDTO.centerLongitude!))
: null,
color: kPrimaryColor,
onChanged: (LatLong? localisation) {
if (localisation != null) {
mapDTO.centerLongitude = localisation.longitude.toString();
mapDTO.centerLatitude = localisation.latitude.toString();
}
widget.onChanged(mapDTO);
},
isSmall: true);
final zoom = SliderInputContainer(
label: l.zoomLabel,
initialValue: mapDTO.zoom != null ? mapDTO.zoom!.toDouble() : 18,
color: kPrimaryColor,
min: 0,
max: 30,
onChanged: (double value) {
mapDTO.zoom = value.toInt();
widget.onChanged(mapDTO);
},
);
final icon = ResourceInputContainer(
label: l.iconLabel,
initialValue: mapDTO.iconResourceId,
color: kPrimaryColor,
imageFit: BoxFit.contain,
onChanged: (ResourceDTO resource) {
if (resource.id == null) {
mapDTO.iconSource = null;
mapDTO.iconResourceId = null;
} else {
mapDTO.iconResourceId = resource.id;
mapDTO.iconSource = resource.url;
}
widget.onChanged(mapDTO);
},
isSmall: true);
final categories = CategoryInputContainer(
label: l.categoriesLabel,
initialValue: mapDTO.categories ?? [],
color: kPrimaryColor,
onChanged: (List<CategorieDTO>? value) {
if (value == null) return;
mapDTO.categories = value;
mapDTO.points?.forEach((p) {
if (p.categorieId != null &&
!mapDTO.categories!
.map((c) => c.id)
.any((e) => e != null && e == p.categorieId)) {
p.categorieId = null;
}
});
widget.onChanged(mapDTO);
},
);
final display = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(l.displayLabel, style: kLabelField),
const SizedBox(height: kSpace2),
CheckInputContainer(
label: l.listViewOption,
isChecked: mapDTO.isListViewEnabled ?? false,
onChanged: (value) {
setState(() {
mapDTO.isListViewEnabled = value;
widget.onChanged(mapDTO);
});
},
),
],
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_FieldGroup(
title: l.mapGroupMap,
fields: [service, typeField, centerPoint, zoom],
),
const SizedBox(height: kSpace4),
_FieldGroup(
title: l.mapGroupPoints,
fields: [icon, categories, display],
),
const SizedBox(height: kSpace5),
],
);
}
}
/// Un bloc de réglages : un intertitre, puis les champs répartis en colonnes de
/// ~280 px (1, 2 ou 3 selon la place). Ils se posaient dans un `Wrap` libre,
/// donc chacun prenait sa largeur naturelle et rien ne s'alignait d'une ligne
/// à l'autre.
class _FieldGroup extends StatelessWidget {
const _FieldGroup({required this.title, required this.fields});
final String title;
final List<Widget> fields;
static const double _minColumnWidth = 280;
@override
Widget build(BuildContext context) {
return Container(
padding: kCardPadding,
margin: const EdgeInsets.only(bottom: kSpace5),
decoration: BoxDecoration(
color: kSurface,
borderRadius: BorderRadius.circular(kRadiusCard),
border: Border.all(color: kLine),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
title.toUpperCase(),
style: const TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w600,
letterSpacing: 0.9,
color: kInk3,
Wrap(
spacing: kSpace6,
runSpacing: kSpace5,
crossAxisAlignment: WrapCrossAlignment.start,
children: [
SingleSelectContainer(
label: AppLocalizations.of(context)!.serviceLabel,
color: Colors.black,
initialValue: mapProviderIn,
inputValues: map_providers,
onChanged: (String value) {
switch (value) {
case "Google":
mapDTO.mapProvider = MapProvider.Google;
break;
case "MapBox":
mapDTO.mapProvider = MapProvider.MapBox;
break;
}
widget.onChanged(mapDTO);
}),
GeolocInputContainer(
label: AppLocalizations.of(context)!.centerPointLabel,
initialValue: mapDTO.centerLatitude != null &&
mapDTO.centerLongitude != null
? LatLong(double.parse(mapDTO.centerLatitude!),
double.parse(mapDTO.centerLongitude!))
: null,
color: kPrimaryColor,
onChanged: (LatLong? localisation) {
if (localisation != null) {
mapDTO.centerLongitude =
localisation.longitude.toString();
mapDTO.centerLatitude = localisation.latitude.toString();
}
widget.onChanged(mapDTO);
},
isSmall: true),
ResourceInputContainer(
label: AppLocalizations.of(context)!.iconLabel,
initialValue: mapDTO.iconResourceId,
color: kPrimaryColor,
imageFit: BoxFit.contain,
onChanged: (ResourceDTO resource) {
if (resource.id == null) {
mapDTO.iconSource = null;
mapDTO.iconResourceId = null;
} else {
mapDTO.iconResourceId = resource.id;
mapDTO.iconSource = resource.url;
}
widget.onChanged(mapDTO);
},
isSmall: true),
],
),
const SizedBox(height: kSpace3),
// Décision du 2026-08-11 (lot E, W1) : visitapp-web reste sur Leaflet.
// Le champ ne peut pas être masqué pour le web il est porté par la
// section, et une même configuration est servie au mobile comme au web.
// On le dit donc au client au lieu de le laisser croire le contraire.
Align(
alignment: Alignment.centerLeft,
child: Text(
AppLocalizations.of(context)!.mapProviderMobileOnlyNote,
style: kTextHint,
),
),
const SizedBox(height: kSpace4),
LayoutBuilder(
builder: (context, constraints) {
const spacing = kSpace6;
final columns = ((constraints.maxWidth + spacing) /
(_minColumnWidth + spacing))
.floor()
.clamp(1, 3);
final width =
(constraints.maxWidth - spacing * (columns - 1)) / columns;
return Wrap(
spacing: spacing,
runSpacing: kSpace6,
crossAxisAlignment: WrapCrossAlignment.start,
children: [
for (final field in fields)
SizedBox(width: width, child: field),
],
);
},
const SizedBox(height: kSpace6),
Wrap(
spacing: kSpace6,
runSpacing: kSpace5,
crossAxisAlignment: WrapCrossAlignment.start,
children: [
CheckInputContainer(
label: AppLocalizations.of(context)!.listViewLabel,
isChecked: mapDTO.isListViewEnabled ?? false,
onChanged: (value) {
setState(() {
mapDTO.isListViewEnabled = value;
widget.onChanged(mapDTO);
});
},
),
if (mapDTO.mapProvider == MapProvider.Google)
DropDownInputContainer(
label: AppLocalizations.of(context)!.typeLabel,
values: map_types,
initialValue: mapType,
onChange: (String? value) {
mapDTO.mapType = MapTypeApp.fromJson(value);
widget.onChanged(mapDTO);
},
),
if (mapDTO.mapProvider == MapProvider.MapBox)
DropDownInputContainer(
label: AppLocalizations.of(context)!.typeLabel,
values: map_types_mapBox,
initialValue: mapTypeMapBox,
onChange: (String? value) {
mapDTO.mapTypeMapbox = MapTypeMapBox.fromJson(value);
widget.onChanged(mapDTO);
},
),
SliderInputContainer(
label: AppLocalizations.of(context)!.zoomLabel,
initialValue:
mapDTO.zoom != null ? mapDTO.zoom!.toDouble() : 18,
color: kPrimaryColor,
min: 0,
max: 30,
onChanged: (double value) {
mapDTO.zoom = value.toInt();
widget.onChanged(mapDTO);
},
),
SizedBox(
width: 220,
child: CategoryInputContainer(
label: AppLocalizations.of(context)!.categoriesLabel,
initialValue:
mapDTO.categories != null ? mapDTO.categories! : [],
color: kPrimaryColor,
onChanged: (List<CategorieDTO>? value) {
if (value != null) {
mapDTO.categories = value;
if (mapDTO.points != null) {
mapDTO.points!.forEach((p) {
if (p.categorieId != null &&
!mapDTO.categories!.map((c) => c.id).any(
(e) => e != null && e == p.categorieId)) {
p.categorieId = null;
}
});
}
widget.onChanged(mapDTO);
}
},
),
)
],
),
],
),

View File

@ -143,26 +143,6 @@ class _MenuConfigState extends State<MenuConfig> {
}
}
Future<void> _toggleVisibility(SectionDTO section) async {
final l = AppLocalizations.of(context)!;
final newValue = section.isActive == false;
setState(() => section.isActive = newValue);
try {
await _api().sectionSetVisibility(section.id!, newValue);
showNotification(
kSuccess,
kWhite,
newValue ? l.sectionShownSuccess : l.sectionHiddenSuccess,
context,
null);
} catch (e) {
setState(() => section.isActive = !newValue);
showNotification(kError, kWhite, l.sectionVisibilityError, context, null);
}
}
/// Ouvre l'écran de la sous-section. Il lit `selectedSubSectionRawData`, donc
/// on lui passe le JSON brut correspondant, pas seulement le DTO.
void _openSubSection(SectionDTO section, int index) {
@ -187,7 +167,6 @@ class _MenuConfigState extends State<MenuConfig> {
onReorder: _onReorder,
onAdd: _createSubSection,
onTap: _openSubSection,
onToggleVisibility: _toggleVisibility,
);
}
}

View File

@ -58,12 +58,6 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
final GlobalKey globalKey = GlobalKey();
late Future<Object?> _sectionFuture;
// Les champs sont des TextFormField non contrôlés : leur `initialValue` n'est
// lu qu'à la création de l'élément. Recharger le DTO ne suffit donc pas à
// remettre le formulaire à l'état serveur — il faut forcer une nouvelle
// sous-arborescence, d'où ce compteur.
int _formRevision = 0;
Future<Object?> _loadSection() {
final appContext = Provider.of<AppContext>(context, listen: false);
return getSection(
@ -176,17 +170,14 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
Expanded(
child: SingleChildScrollView(
padding: kPagePadding,
child: KeyedSubtree(
key: ValueKey(_formRevision),
child: EditorColumns(
main: [
_cardIdentity(appContext, l),
_cardTypeConfig(rawSectionData, appContext),
],
rail: [
_cardQR(appContext, l),
],
),
child: EditorColumns(
main: [
_cardIdentity(appContext, l),
_cardTypeConfig(rawSectionData, appContext),
],
rail: [
_cardQR(appContext, l),
],
),
),
),
@ -363,18 +354,10 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
.sectionApi!
.sectionGetDetail(sectionDTO.id!);
var nullableSection = SectionDTO.fromJson(rawData);
if (nullableSection == null) return;
managerAppContext.selectedSection = nullableSection;
appContext.setContext(managerAppContext);
if (!mounted) return;
setState(() {
sectionDetailDTO = null;
lastLoadedSectionId = null;
_sectionFuture = Future.value(rawData);
_formRevision++;
});
if (nullableSection != null) {
managerAppContext.selectedSection = nullableSection!;
appContext.setContext(managerAppContext);
}
}
Future<void> delete(AppContext appContext) async {

View File

@ -38,15 +38,6 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
Future<ConfigurationDTO?>? _configFuture;
Future<List<SectionDTO>?>? _sectionsFuture;
// Les champs sont des TextFormField non contrôlés : leur `initialValue` n'est
// lu qu'à la création de l'élément. Changer de DTO ne suffit donc pas à
// remettre le formulaire à l'état serveur — il faut forcer une nouvelle
// sous-arborescence, d'où ce compteur.
int _formRevision = 0;
/// L'ordre des sections a été touché mais pas encore écrit.
bool _orderDirty = false;
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
@ -116,17 +107,14 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
Expanded(
child: SingleChildScrollView(
padding: kPagePadding,
child: KeyedSubtree(
key: ValueKey(_formRevision),
child: EditorColumns(
main: [
_cardGeneral(config, l),
_cardSections(config, appContext),
],
rail: [
_cardImages(config, l),
],
),
child: EditorColumns(
main: [
_cardGeneral(config, l),
_cardSections(config, appContext),
],
rail: [
_cardImages(config, l),
],
),
),
),
@ -289,12 +277,9 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
return SectionReorderList(
sectionsIn: sections!,
configurationId: config.id!,
// Le nouvel ordre partait au serveur au relâchement de la souris,
// donc « Annuler » n'avait plus rien à annuler. Il attend
// maintenant « Enregistrer », comme les autres champs de l'écran.
onChangedOrder: (List<SectionDTO> sectionsOut) {
onChangedOrder: (List<SectionDTO> sectionsOut) async {
sections = sectionsOut;
_orderDirty = true;
await managerCtx.clientAPI!.sectionApi!.sectionUpdateOrder(sections!);
},
askReload: () => setState(() {
_sectionsFuture = null;
@ -342,15 +327,6 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
ConfigurationDTO? configuration = await managerAppContext.clientAPI!.configurationApi!.configurationGetDetail(config.id!);
managerAppContext.selectedConfiguration = configuration;
appContext.setContext(managerAppContext);
if (!mounted) return;
setState(() {
_configFuture = Future.value(configuration);
_sectionsFuture = null;
sections = null;
_orderDirty = false;
_formRevision++;
});
}
Future<void> delete(ConfigurationDTO config, AppContext appContext) async {
@ -370,12 +346,6 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
Future<void> save(ConfigurationDTO config, AppContext appContext) async {
ManagerAppContext managerAppContext = appContext.getContext();
if (_orderDirty && sections != null) {
await managerAppContext.clientAPI!.sectionApi!.sectionUpdateOrder(sections!);
_orderDirty = false;
}
ConfigurationDTO? configuration = await managerAppContext.clientAPI!.configurationApi!.configurationUpdate(config);
managerAppContext.selectedConfiguration = configuration;
appContext.setContext(managerAppContext);

View File

@ -17,7 +17,6 @@ class SectionCard extends StatelessWidget {
required this.onTap,
this.dragHandle,
this.isDropTarget = false,
this.onToggleVisibility,
}) : super(key: key);
final SectionDTO section;
@ -26,9 +25,6 @@ class SectionCard extends StatelessWidget {
final Widget? dragHandle;
final bool isDropTarget;
/// Null pour un utilisateur qui ne peut pas éditer : la tuile perd le bouton.
final VoidCallback? onToggleVisibility;
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
@ -105,20 +101,6 @@ class SectionCard extends StatelessWidget {
fontSize: 10, color: kInk3)),
),
),
if (onToggleVisibility != null)
Positioned(
bottom: 2,
right: 2,
child: _OverlayIconButton(
icon: isHidden
? Icons.visibility_off
: Icons.visibility,
tooltip: isHidden
? l.sectionShowAction
: l.sectionHideAction,
onPressed: onToggleVisibility!,
),
),
if (dragHandle != null)
Positioned(top: 2, right: 2, child: dragHandle!),
],
@ -168,41 +150,6 @@ class SectionCard extends StatelessWidget {
}
}
/// Bouton posé sur l'image de la tuile : l'ombre porte le contraste, parce que
/// l'image derrière peut être de n'importe quelle couleur.
class _OverlayIconButton extends StatelessWidget {
const _OverlayIconButton({
required this.icon,
required this.tooltip,
required this.onPressed,
});
final IconData icon;
final String tooltip;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(kRadiusInput),
onTap: onPressed,
child: Padding(
padding: const EdgeInsets.all(kSpace1),
child: Icon(icon,
size: 17,
color: kWhite,
shadows: const [Shadow(color: kBlack, blurRadius: 3)]),
),
),
),
);
}
}
/// La grille de sections, réordonnable, terminée par la tuile « Ajouter ».
///
/// Partagée par les sections d'une configuration et par les sous-sections d'un
@ -216,14 +163,12 @@ class SectionGrid extends StatefulWidget {
required this.onReorder,
required this.onTap,
required this.onAdd,
this.onToggleVisibility,
}) : super(key: key);
final List<SectionDTO> sections;
final void Function(int oldIndex, int newIndex) onReorder;
final void Function(SectionDTO section, int index) onTap;
final VoidCallback onAdd;
final void Function(SectionDTO section)? onToggleVisibility;
@override
State<SectionGrid> createState() => _SectionGridState();
@ -267,9 +212,6 @@ class _SectionGridState extends State<SectionGrid> {
isDropTarget: dropTargetIndex == index,
dragHandle: SectionDragHandle(index: index, width: width),
onTap: () => widget.onTap(widget.sections[index], index),
onToggleVisibility: widget.onToggleVisibility == null
? null
: () => widget.onToggleVisibility!(widget.sections[index]),
),
),
),

View File

@ -79,37 +79,10 @@ class _SectionReorderListState extends State<SectionReorderList> {
}
}
/// Retirer une section de l'app sans la supprimer. Le champ existe depuis
/// toujours côté serveur et les apps visiteur le respectent déjà il n'avait
/// simplement aucun geste dans le manager.
Future<void> _toggleVisibility(
AppContext appContext, SectionDTO section) async {
final l = AppLocalizations.of(context)!;
final newValue = section.isActive == false;
setState(() => section.isActive = newValue);
try {
ManagerAppContext managerAppContext = appContext.getContext();
await managerAppContext.clientAPI!.sectionApi!
.sectionSetVisibility(section.id!, newValue);
showNotification(
kSuccess,
kWhite,
newValue ? l.sectionShownSuccess : l.sectionHiddenSuccess,
context,
null);
} catch (e) {
setState(() => section.isActive = !newValue);
showNotification(kError, kWhite, l.sectionVisibilityError, context, null);
}
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
final l = AppLocalizations.of(context)!;
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -129,9 +102,6 @@ class _SectionReorderListState extends State<SectionReorderList> {
sections: sections,
onReorder: _onReorder,
onAdd: () => _createSection(appContext),
onToggleVisibility: canEdit
? (section) => _toggleVisibility(appContext, section)
: null,
onTap: (section, index) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {

View File

@ -88,29 +88,12 @@ class _MainScreenState extends State<MainScreen> {
void initState() {
super.initState();
_buildMenu(widget.instance);
_renderedInstanceId = widget.instance.id;
currentPosition.value ??= _defaultPosition(widget.instance);
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
}
/// L'instance sur laquelle le menu et l'écran courant ont é construits.
String? _renderedInstanceId;
/// Reconstruit le menu et recale la position quand on a changé d'instance.
void _applyInstance(InstanceDTO instance) {
_buildMenu(instance);
_renderedInstanceId = instance.id;
final positions = menu.sections!
.expand((s) => [s.menuId, ...s.subMenu.map((sub) => sub.menuId)])
.toSet();
if (!positions.contains(currentPosition.value)) {
currentPosition.value = _defaultPosition(instance);
}
}
/// Le changement d'instance passe par la même route : Flutter réutilise cet
/// State et `initState` ne rejoue pas. Sans cela le menu reste celui de
/// l'instance précédente, et la position courante peut désigner une
@ -120,7 +103,14 @@ class _MainScreenState extends State<MainScreen> {
super.didUpdateWidget(oldWidget);
if (oldWidget.instance.id == widget.instance.id) return;
_applyInstance(widget.instance);
_buildMenu(widget.instance);
final positions = menu.sections!
.expand((s) => [s.menuId, ...s.subMenu.map((sub) => sub.menuId)])
.toSet();
if (!positions.contains(currentPosition.value)) {
currentPosition.value = _defaultPosition(widget.instance);
}
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
}
@ -285,17 +275,7 @@ class _MainScreenState extends State<MainScreen> {
setState(() {
managerCtx.instanceId = newInstance.id;
managerCtx.instanceDTO = newInstance;
// Une configuration ou une section restée
// sélectionnée appartient à l'instance qu'on
// vient de quitter : l'écran de détail la
// réaffichait telle quelle.
managerCtx.selectedConfiguration = null;
managerCtx.selectedSection = null;
managerCtx.selectedSubSection = null;
managerCtx.selectedSubSectionRawData = null;
});
Provider.of<AppContext>(context, listen: false)
.setContext(managerCtx);
final view = newInstance.isMobile! ? 'mobile'
: newInstance.isTablet! ? 'kiosk'
: newInstance.isWeb! ? 'web'
@ -563,16 +543,6 @@ class _MainScreenState extends State<MainScreen> {
final appContext = Provider.of<AppContext>(context);
ManagerAppContext managerAppContext = appContext.getContext();
// `widget.instance` est figée à la construction de la route. Or changer
// d'instance en restant sur la même vue ne change pas l'URL, donc GoRouter
// ne rejoue pas son builder et `didUpdateWidget` ne se déclenche jamais :
// l'écran continuait d'afficher l'instance précédente jusqu'à ce qu'on
// change d'onglet. C'est le contexte qui fait foi, pas le widget.
final instance = managerAppContext.instanceDTO ?? widget.instance;
if (instance.id != _renderedInstanceId) {
_applyInstance(instance);
}
// Synchronise les items de menu sensibles au rôle à chaque rebuild
final role = managerAppContext.role;
final hasAdminItems = menu.sections!.any((s) => s.menuId == 8);
@ -638,16 +608,10 @@ class _MainScreenState extends State<MainScreen> {
child: ValueListenableBuilder<int?>(
valueListenable: currentPosition,
builder: (context, value, _) {
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, instance);
// La clé porte l'instance : sans elle les écrans en dessous
// gardent leur State, donc leurs futures déjà résolues sur
// l'instance précédente.
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
return Padding(
padding: const EdgeInsets.all(8.0),
child: KeyedSubtree(
key: ValueKey(instance.id),
child: selectedElement!,
),
child: selectedElement,
);
}
),

View File

@ -5,7 +5,6 @@ import 'package:manager_app/l10n/app_localizations.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/Components/common_loader.dart';
import 'package:manager_app/Components/message_notification.dart';
import 'package:manager_app/constants.dart';
import 'package:provider/provider.dart';
@ -60,14 +59,11 @@ class _UsersScreenState extends State<UsersScreen> {
String firstName, String lastName, int roleValue) async {
// No password sent: the backend generates an invitation token and emails
// a "set your password" link to the new user instead.
// `instanceId` est obligatoire côté serveur : il rejetait la création avec
// « InstanceId is null » tant qu'on ne l'envoyait pas.
final body = {
'email': email,
'firstName': firstName,
'lastName': lastName,
'role': roleValue,
'instanceId': ctx.instanceId,
};
final response = await ctx.clientAPI!.apiApi!.invokeAPI(
'/api/User', 'POST', [], body, {}, {}, 'application/json');
@ -75,16 +71,14 @@ class _UsersScreenState extends State<UsersScreen> {
// `invokeAPI` ne lève pas sur un code d'erreur : sans ce test, un e-mail déjà
// utilisé (409) ou un rôle refusé (403) laissaient la liste inchangée sans
// qu'aucun message n'explique pourquoi.
if (!mounted) return;
final l = AppLocalizations.of(context)!;
if (response.statusCode != 200) {
showNotification(kError, kWhite,
l.userCreateError(utf8.decode(response.bodyBytes)), context, null);
if (response.statusCode != 200 && mounted) {
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(l.userCreateError(utf8.decode(response.bodyBytes))),
backgroundColor: kError,
));
return;
}
showNotification(kSuccess, kWhite, l.userCreatedSuccess, context, null);
await _loadUsers(ctx);
}
@ -96,36 +90,13 @@ class _UsersScreenState extends State<UsersScreen> {
'lastName': lastName,
'role': roleValue,
};
final response = await ctx.clientAPI!.apiApi!.invokeAPI(
await ctx.clientAPI!.apiApi!.invokeAPI(
'/api/User', 'PUT', [], body, {}, {}, 'application/json');
if (!mounted) return;
final l = AppLocalizations.of(context)!;
if (response.statusCode != 200) {
showNotification(kError, kWhite,
l.userUpdateError(utf8.decode(response.bodyBytes)), context, null);
return;
}
showNotification(kSuccess, kWhite, l.userUpdatedSuccess, context, null);
await _loadUsers(ctx);
}
Future<void> _deleteUser(ManagerAppContext ctx, String id) async {
final l = AppLocalizations.of(context)!;
try {
await ctx.clientAPI!.userApi!.userDeleteUser(id);
if (mounted) {
showNotification(kSuccess, kWhite, l.userDeletedSuccess, context, null);
}
} catch (e) {
if (mounted) {
showNotification(
kError, kWhite, l.userDeleteError(e.toString()), context, null);
}
return;
}
await ctx.clientAPI!.userApi!.userDeleteUser(id);
await _loadUsers(ctx);
}
@ -235,17 +206,15 @@ class _UsersScreenState extends State<UsersScreen> {
final l = AppLocalizations.of(context)!;
showDialog(
context: context,
// `context` est celui de l'écran, pas du dialogue : le popper ferme la page
// sous GoRouter au lieu de la boîte de dialogue.
builder: (dialogContext) => AlertDialog(
builder: (_) => AlertDialog(
title: Text(l.deleteUserTitle),
content: Text(l.deleteUserConfirm(user['email'] as String? ?? '')),
actions: [
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(l.cancel)),
TextButton(onPressed: () => Navigator.pop(context), child: Text(l.cancel)),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () async {
Navigator.pop(dialogContext);
Navigator.pop(context);
await _deleteUser(ctx, user['id'] as String);
},
child: Text(l.delete, style: const TextStyle(color: Colors.white)),

View File

@ -42,7 +42,6 @@ class _LoginScreenState extends State<LoginScreen> {
String? instanceId;
String? pinCode;
Storage localStorage = window.localStorage;
late final Future<String> appVersion = getAppVersion();
void authenticateTRY(AppContext appContext, bool fromClick) async {
clientAPI = Client(this.host!);
@ -291,7 +290,7 @@ class _LoginScreenState extends State<LoginScreen> {
textAlign: TextAlign.center,
),
FutureBuilder(
future: appVersion,
future: getAppVersion(),
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.data != null) {
@ -390,6 +389,6 @@ class _LoginScreenState extends State<LoginScreen> {
Future<String> getAppVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
return 'v${packageInfo.version} \u00b7 $kGitSha';
return packageInfo.version;
}
}

View File

@ -23,13 +23,6 @@ const kSuccess = Color(0xFF8bc34a);
const kApiBaseUrl = String.fromEnvironment('API_BASE_URL',
defaultValue: 'http://localhost:5000');
// Empreinte du build, injectee elle aussi au build :
// --dart-define=GIT_SHA=$(git rev-parse --short HEAD)
// Elle est affichee sous le titre de l'ecran de login et sert a relier un
// bundle deploye au commit exact qui l'a produit : le numero de version du
// pubspec ne suffisait pas, il ne bougeait pas d'un deploiement a l'autre.
const kGitSha = String.fromEnvironment('GIT_SHA', defaultValue: 'dev');
// Responsive
const kBreakpointMobile = 850.0;

View File

@ -581,19 +581,13 @@
"searchLabel": "Search:",
"geopointsLoadError": "Error loading geographic points",
"geopointDeleteConfirm": "Are you sure you want to delete this geographic point?",
"serviceLabel": "Service",
"geopointConfirmPosition": "Confirm position",
"clearFilter": "Show all",
"mapGroupMap": "Map",
"mapGroupPoints": "Points of interest",
"displayLabel": "Display",
"listViewOption": "Offer the list view",
"centerPointLabel": "Center point",
"iconLabel": "Default icon",
"listViewLabel": "List view",
"typeLabel": "Map type",
"zoomLabel": "Initial zoom",
"categoriesLabel": "Categories",
"serviceLabel": "Service:",
"centerPointLabel": "Center point:",
"iconLabel": "Icon:",
"listViewLabel": "List view:",
"typeLabel": "Type:",
"zoomLabel": "Zoom:",
"categoriesLabel": "Categories:",
"startDateLabel": "Start date",
"notDefined": "Not defined",
"endDateLabel": "End date",
@ -840,25 +834,6 @@
}
},
"usersQuotaHint": "Delete a user to invite another one.",
"userCreatedSuccess": "User created, the invitation has been emailed",
"userUpdatedSuccess": "User updated",
"userDeletedSuccess": "User deleted",
"userUpdateError": "Update failed: {message}",
"@userUpdateError": {
"placeholders": {
"message": {
"type": "String"
}
}
},
"userDeleteError": "Deletion failed: {message}",
"@userDeleteError": {
"placeholders": {
"message": {
"type": "String"
}
}
},
"userCreateError": "Creation failed: {message}",
"@userCreateError": {
"placeholders": {
@ -1366,11 +1341,6 @@
}
},
"sectionHiddenBadge": "Hidden",
"sectionHideAction": "Hide in the visitor app",
"sectionShowAction": "Show in the visitor app",
"sectionHiddenSuccess": "Section hidden",
"sectionShownSuccess": "Section shown",
"sectionVisibilityError": "Could not change the section visibility",
"addSectionTile": "Add a section",
"resourceTypeImage": "Image",
"resourceTypeImageUrl": "Image (URL)",

View File

@ -581,19 +581,13 @@
"searchLabel": "Recherche :",
"geopointsLoadError": "Une erreur est survenue lors de la récupération des points géographiques",
"geopointDeleteConfirm": "Êtes-vous sûr de vouloir supprimer ce point géographique ?",
"serviceLabel": "Service",
"geopointConfirmPosition": "Valider la position",
"clearFilter": "Tout afficher",
"mapGroupMap": "Carte",
"mapGroupPoints": "Points d'intérêt",
"displayLabel": "Affichage",
"listViewOption": "Proposer la vue liste",
"centerPointLabel": "Point de centrage",
"iconLabel": "Icône par défaut",
"listViewLabel": "Vue liste",
"typeLabel": "Type de carte",
"zoomLabel": "Zoom initial",
"categoriesLabel": "Catégories",
"serviceLabel": "Service :",
"centerPointLabel": "Point de centrage :",
"iconLabel": "Icône :",
"listViewLabel": "Vue liste :",
"typeLabel": "Type :",
"zoomLabel": "Zoom :",
"categoriesLabel": "Catégories :",
"startDateLabel": "Date de début",
"notDefined": "Non définie",
"endDateLabel": "Date de fin",
@ -840,25 +834,6 @@
}
},
"usersQuotaHint": "Supprimez un utilisateur pour en inviter un autre.",
"userCreatedSuccess": "Utilisateur créé, l'invitation est partie par e-mail",
"userUpdatedSuccess": "Utilisateur mis à jour",
"userDeletedSuccess": "Utilisateur supprimé",
"userUpdateError": "La mise à jour a échoué : {message}",
"@userUpdateError": {
"placeholders": {
"message": {
"type": "String"
}
}
},
"userDeleteError": "La suppression a échoué : {message}",
"@userDeleteError": {
"placeholders": {
"message": {
"type": "String"
}
}
},
"userCreateError": "La création a échoué : {message}",
"@userCreateError": {
"placeholders": {
@ -1366,11 +1341,6 @@
}
},
"sectionHiddenBadge": "Masquée",
"sectionHideAction": "Masquer dans l'app visiteur",
"sectionShowAction": "Afficher dans l'app visiteur",
"sectionHiddenSuccess": "Section masquée",
"sectionShownSuccess": "Section affichée",
"sectionVisibilityError": "Impossible de changer la visibilité de la section",
"addSectionTile": "Ajouter une section",
"resourceTypeImage": "Image",
"resourceTypeImageUrl": "Image (URL)",

View File

@ -2527,79 +2527,43 @@ abstract class AppLocalizations {
/// No description provided for @serviceLabel.
///
/// In fr, this message translates to:
/// **'Service'**
/// **'Service :'**
String get serviceLabel;
/// No description provided for @geopointConfirmPosition.
///
/// In fr, this message translates to:
/// **'Valider la position'**
String get geopointConfirmPosition;
/// No description provided for @clearFilter.
///
/// In fr, this message translates to:
/// **'Tout afficher'**
String get clearFilter;
/// No description provided for @mapGroupMap.
///
/// In fr, this message translates to:
/// **'Carte'**
String get mapGroupMap;
/// No description provided for @mapGroupPoints.
///
/// In fr, this message translates to:
/// **'Points d\'intérêt'**
String get mapGroupPoints;
/// No description provided for @displayLabel.
///
/// In fr, this message translates to:
/// **'Affichage'**
String get displayLabel;
/// No description provided for @listViewOption.
///
/// In fr, this message translates to:
/// **'Proposer la vue liste'**
String get listViewOption;
/// No description provided for @centerPointLabel.
///
/// In fr, this message translates to:
/// **'Point de centrage'**
/// **'Point de centrage :'**
String get centerPointLabel;
/// No description provided for @iconLabel.
///
/// In fr, this message translates to:
/// **'Icône par défaut'**
/// **'Icône :'**
String get iconLabel;
/// No description provided for @listViewLabel.
///
/// In fr, this message translates to:
/// **'Vue liste'**
/// **'Vue liste :'**
String get listViewLabel;
/// No description provided for @typeLabel.
///
/// In fr, this message translates to:
/// **'Type de carte'**
/// **'Type :'**
String get typeLabel;
/// No description provided for @zoomLabel.
///
/// In fr, this message translates to:
/// **'Zoom initial'**
/// **'Zoom :'**
String get zoomLabel;
/// No description provided for @categoriesLabel.
///
/// In fr, this message translates to:
/// **'Catégories'**
/// **'Catégories :'**
String get categoriesLabel;
/// No description provided for @startDateLabel.
@ -3730,36 +3694,6 @@ abstract class AppLocalizations {
/// **'Supprimez un utilisateur pour en inviter un autre.'**
String get usersQuotaHint;
/// No description provided for @userCreatedSuccess.
///
/// In fr, this message translates to:
/// **'Utilisateur créé, l\'invitation est partie par e-mail'**
String get userCreatedSuccess;
/// No description provided for @userUpdatedSuccess.
///
/// In fr, this message translates to:
/// **'Utilisateur mis à jour'**
String get userUpdatedSuccess;
/// No description provided for @userDeletedSuccess.
///
/// In fr, this message translates to:
/// **'Utilisateur supprimé'**
String get userDeletedSuccess;
/// No description provided for @userUpdateError.
///
/// In fr, this message translates to:
/// **'La mise à jour a échoué : {message}'**
String userUpdateError(String message);
/// No description provided for @userDeleteError.
///
/// In fr, this message translates to:
/// **'La suppression a échoué : {message}'**
String userDeleteError(String message);
/// No description provided for @userCreateError.
///
/// In fr, this message translates to:
@ -5698,36 +5632,6 @@ abstract class AppLocalizations {
/// **'Masquée'**
String get sectionHiddenBadge;
/// No description provided for @sectionHideAction.
///
/// In fr, this message translates to:
/// **'Masquer dans l\'app visiteur'**
String get sectionHideAction;
/// No description provided for @sectionShowAction.
///
/// In fr, this message translates to:
/// **'Afficher dans l\'app visiteur'**
String get sectionShowAction;
/// No description provided for @sectionHiddenSuccess.
///
/// In fr, this message translates to:
/// **'Section masquée'**
String get sectionHiddenSuccess;
/// No description provided for @sectionShownSuccess.
///
/// In fr, this message translates to:
/// **'Section affichée'**
String get sectionShownSuccess;
/// No description provided for @sectionVisibilityError.
///
/// In fr, this message translates to:
/// **'Impossible de changer la visibilité de la section'**
String get sectionVisibilityError;
/// No description provided for @addSectionTile.
///
/// In fr, this message translates to:

View File

@ -1349,43 +1349,25 @@ class AppLocalizationsEn extends AppLocalizations {
'Are you sure you want to delete this geographic point?';
@override
String get serviceLabel => 'Service';
String get serviceLabel => 'Service:';
@override
String get geopointConfirmPosition => 'Confirm position';
String get centerPointLabel => 'Center point:';
@override
String get clearFilter => 'Show all';
String get iconLabel => 'Icon:';
@override
String get mapGroupMap => 'Map';
String get listViewLabel => 'List view:';
@override
String get mapGroupPoints => 'Points of interest';
String get typeLabel => 'Type:';
@override
String get displayLabel => 'Display';
String get zoomLabel => 'Zoom:';
@override
String get listViewOption => 'Offer the list view';
@override
String get centerPointLabel => 'Center point';
@override
String get iconLabel => 'Default icon';
@override
String get listViewLabel => 'List view';
@override
String get typeLabel => 'Map type';
@override
String get zoomLabel => 'Initial zoom';
@override
String get categoriesLabel => 'Categories';
String get categoriesLabel => 'Categories:';
@override
String get startDateLabel => 'Start date';
@ -1978,26 +1960,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get usersQuotaHint => 'Delete a user to invite another one.';
@override
String get userCreatedSuccess =>
'User created, the invitation has been emailed';
@override
String get userUpdatedSuccess => 'User updated';
@override
String get userDeletedSuccess => 'User deleted';
@override
String userUpdateError(String message) {
return 'Update failed: $message';
}
@override
String userDeleteError(String message) {
return 'Deletion failed: $message';
}
@override
String userCreateError(String message) {
return 'Creation failed: $message';
@ -3086,22 +3048,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sectionHiddenBadge => 'Hidden';
@override
String get sectionHideAction => 'Hide in the visitor app';
@override
String get sectionShowAction => 'Show in the visitor app';
@override
String get sectionHiddenSuccess => 'Section hidden';
@override
String get sectionShownSuccess => 'Section shown';
@override
String get sectionVisibilityError =>
'Could not change the section visibility';
@override
String get addSectionTile => 'Add a section';

View File

@ -1381,43 +1381,25 @@ class AppLocalizationsFr extends AppLocalizations {
'Êtes-vous sûr de vouloir supprimer ce point géographique ?';
@override
String get serviceLabel => 'Service';
String get serviceLabel => 'Service :';
@override
String get geopointConfirmPosition => 'Valider la position';
String get centerPointLabel => 'Point de centrage :';
@override
String get clearFilter => 'Tout afficher';
String get iconLabel => 'Icône :';
@override
String get mapGroupMap => 'Carte';
String get listViewLabel => 'Vue liste :';
@override
String get mapGroupPoints => 'Points d\'intérêt';
String get typeLabel => 'Type :';
@override
String get displayLabel => 'Affichage';
String get zoomLabel => 'Zoom :';
@override
String get listViewOption => 'Proposer la vue liste';
@override
String get centerPointLabel => 'Point de centrage';
@override
String get iconLabel => 'Icône par défaut';
@override
String get listViewLabel => 'Vue liste';
@override
String get typeLabel => 'Type de carte';
@override
String get zoomLabel => 'Zoom initial';
@override
String get categoriesLabel => 'Catégories';
String get categoriesLabel => 'Catégories :';
@override
String get startDateLabel => 'Date de début';
@ -2018,26 +2000,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get usersQuotaHint =>
'Supprimez un utilisateur pour en inviter un autre.';
@override
String get userCreatedSuccess =>
'Utilisateur créé, l\'invitation est partie par e-mail';
@override
String get userUpdatedSuccess => 'Utilisateur mis à jour';
@override
String get userDeletedSuccess => 'Utilisateur supprimé';
@override
String userUpdateError(String message) {
return 'La mise à jour a échoué : $message';
}
@override
String userDeleteError(String message) {
return 'La suppression a échoué : $message';
}
@override
String userCreateError(String message) {
return 'La création a échoué : $message';
@ -3137,22 +3099,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get sectionHiddenBadge => 'Masquée';
@override
String get sectionHideAction => 'Masquer dans l\'app visiteur';
@override
String get sectionShowAction => 'Afficher dans l\'app visiteur';
@override
String get sectionHiddenSuccess => 'Section masquée';
@override
String get sectionShownSuccess => 'Section affichée';
@override
String get sectionVisibilityError =>
'Impossible de changer la visibilité de la section';
@override
String get addSectionTile => 'Ajouter une section';

View File

@ -1363,43 +1363,25 @@ class AppLocalizationsNl extends AppLocalizations {
'Weet u zeker dat u dit geografische punt wilt verwijderen?';
@override
String get serviceLabel => 'Service';
String get serviceLabel => 'Service:';
@override
String get geopointConfirmPosition => 'Positie bevestigen';
String get centerPointLabel => 'Middelpunt:';
@override
String get clearFilter => 'Alles tonen';
String get iconLabel => 'Pictogram:';
@override
String get mapGroupMap => 'Kaart';
String get listViewLabel => 'Lijstweergave:';
@override
String get mapGroupPoints => 'Interessepunten';
String get typeLabel => 'Type:';
@override
String get displayLabel => 'Weergave';
String get zoomLabel => 'Zoom:';
@override
String get listViewOption => 'Lijstweergave aanbieden';
@override
String get centerPointLabel => 'Middelpunt';
@override
String get iconLabel => 'Standaardpictogram';
@override
String get listViewLabel => 'Lijstweergave';
@override
String get typeLabel => 'Kaarttype';
@override
String get zoomLabel => 'Beginzoom';
@override
String get categoriesLabel => 'Categorieën';
String get categoriesLabel => 'Categorieën:';
@override
String get startDateLabel => 'Startdatum';
@ -1999,26 +1981,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get usersQuotaHint =>
'Verwijder een gebruiker om er een andere uit te nodigen.';
@override
String get userCreatedSuccess =>
'Gebruiker aangemaakt, de uitnodiging is per e-mail verstuurd';
@override
String get userUpdatedSuccess => 'Gebruiker bijgewerkt';
@override
String get userDeletedSuccess => 'Gebruiker verwijderd';
@override
String userUpdateError(String message) {
return 'Bijwerken mislukt: $message';
}
@override
String userDeleteError(String message) {
return 'Verwijderen mislukt: $message';
}
@override
String userCreateError(String message) {
return 'Aanmaken mislukt: $message';
@ -3113,22 +3075,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get sectionHiddenBadge => 'Verborgen';
@override
String get sectionHideAction => 'Verbergen in de bezoekersapp';
@override
String get sectionShowAction => 'Tonen in de bezoekersapp';
@override
String get sectionHiddenSuccess => 'Sectie verborgen';
@override
String get sectionShownSuccess => 'Sectie zichtbaar';
@override
String get sectionVisibilityError =>
'Kan de zichtbaarheid van de sectie niet wijzigen';
@override
String get addSectionTile => 'Een sectie toevoegen';

View File

@ -581,19 +581,13 @@
"searchLabel": "Zoeken:",
"geopointsLoadError": "Fout bij het laden van geografische punten",
"geopointDeleteConfirm": "Weet u zeker dat u dit geografische punt wilt verwijderen?",
"serviceLabel": "Service",
"geopointConfirmPosition": "Positie bevestigen",
"clearFilter": "Alles tonen",
"mapGroupMap": "Kaart",
"mapGroupPoints": "Interessepunten",
"displayLabel": "Weergave",
"listViewOption": "Lijstweergave aanbieden",
"centerPointLabel": "Middelpunt",
"iconLabel": "Standaardpictogram",
"listViewLabel": "Lijstweergave",
"typeLabel": "Kaarttype",
"zoomLabel": "Beginzoom",
"categoriesLabel": "Categorieën",
"serviceLabel": "Service:",
"centerPointLabel": "Middelpunt:",
"iconLabel": "Pictogram:",
"listViewLabel": "Lijstweergave:",
"typeLabel": "Type:",
"zoomLabel": "Zoom:",
"categoriesLabel": "Categorieën:",
"startDateLabel": "Startdatum",
"notDefined": "Niet gedefinieerd",
"endDateLabel": "Einddatum",
@ -840,25 +834,6 @@
}
},
"usersQuotaHint": "Verwijder een gebruiker om er een andere uit te nodigen.",
"userCreatedSuccess": "Gebruiker aangemaakt, de uitnodiging is per e-mail verstuurd",
"userUpdatedSuccess": "Gebruiker bijgewerkt",
"userDeletedSuccess": "Gebruiker verwijderd",
"userUpdateError": "Bijwerken mislukt: {message}",
"@userUpdateError": {
"placeholders": {
"message": {
"type": "String"
}
}
},
"userDeleteError": "Verwijderen mislukt: {message}",
"@userDeleteError": {
"placeholders": {
"message": {
"type": "String"
}
}
},
"userCreateError": "Aanmaken mislukt: {message}",
"@userCreateError": {
"placeholders": {
@ -1366,11 +1341,6 @@
}
},
"sectionHiddenBadge": "Verborgen",
"sectionHideAction": "Verbergen in de bezoekersapp",
"sectionShowAction": "Tonen in de bezoekersapp",
"sectionHiddenSuccess": "Sectie verborgen",
"sectionShownSuccess": "Sectie zichtbaar",
"sectionVisibilityError": "Kan de zichtbaarheid van de sectie niet wijzigen",
"addSectionTile": "Een sectie toevoegen",
"resourceTypeImage": "Afbeelding",
"resourceTypeImageUrl": "Afbeelding (URL)",

View File

@ -125,12 +125,7 @@ Future<void> main() async {
path: '/main/:view',
builder: (context, state) {
final view = state.pathParameters['view'];
// La clé porte l'instance : sans elle, un SuperAdmin
// qui change d'instance en restant sur la même vue
// gardait le State de tous les écrans en dessous, donc
// les données de l'instance précédente.
return MainScreen(
key: ValueKey('${managerAppContext.instanceId}/$view'),
instance: managerAppContext.instanceDTO!,
view: view,
);

View File

@ -1306,57 +1306,4 @@ class SectionApi {
}
return null;
}
/// Performs an HTTP 'PUT /api/Section/{id}/visibility' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [bool] isActive (required):
Future<Response> sectionSetVisibilityWithHttpInfo(
String id,
bool isActive,
) async {
// ignore: prefer_const_declarations
final path = r'/api/Section/{id}/visibility'.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'isActive', isActive));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [bool] isActive (required):
Future<void> sectionSetVisibility(
String id,
bool isActive,
) async {
final response = await sectionSetVisibilityWithHttpInfo(
id,
isActive,
);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
}

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 3.1.3+11
version: 3.0.0+10
environment:
sdk: ">=3.1.0 <4.0.0"

View File

@ -14,7 +14,6 @@
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter application.">
@ -113,6 +112,9 @@
loadMainDartJs();
}
</script>
<script type="text/javascript">
window.flutterWebRenderer = "html";
</script>
<script type="module">
// Import the functions you need from the SDKs you need
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";