Carte : filtre par categorie, editeur de point, et corrections users/quotas
Regroupe la configuration de carte en deux blocs (Carte / Points d'interet), ajoute le filtre par categorie et l'editeur de point geographique, et passe les ecrans Users et les jauges de quota par AppLocalizations (FR/EN/NL). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2fbee208e7
commit
0054122829
@ -15,13 +15,8 @@ class QuotaBarsWidget extends StatefulWidget {
|
||||
|
||||
class _QuotaBarsWidgetState extends State<QuotaBarsWidget> {
|
||||
InstanceQuotaDTO? _quota;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _fetchQuota());
|
||||
}
|
||||
bool _loading = false;
|
||||
String? _loadedInstanceId;
|
||||
|
||||
Future<void> _fetchQuota() async {
|
||||
final managerContext = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
|
||||
@ -72,6 +67,17 @@ 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,
|
||||
|
||||
@ -0,0 +1,151 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -45,10 +45,34 @@ 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(
|
||||
@ -265,12 +289,34 @@ 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: MapCanvas(
|
||||
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(
|
||||
key: ValueKey(selected?.id ?? 'none'),
|
||||
geometry: selected?.geometry,
|
||||
color: selected?.polyColor,
|
||||
geometry: pending?.geometry ?? selected?.geometry,
|
||||
color: pending?.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]`.
|
||||
@ -282,17 +328,17 @@ class _GeoPointEditorState extends State<GeoPointEditor> {
|
||||
: _label(l, selected, widget.visiblePoints.indexOf(selected)),
|
||||
ghosts: [
|
||||
for (final point in widget.visiblePoints)
|
||||
if (point.id != selected?.id && point.geometry != null)
|
||||
if (point.id != selected?.id &&
|
||||
(_pending[point.id]?.geometry ?? point.geometry) != null)
|
||||
MapCanvasGhost(
|
||||
geometry: point.geometry!, color: point.polyColor),
|
||||
geometry: _pending[point.id]?.geometry ?? point.geometry!,
|
||||
color: _pending[point.id]?.color ?? point.polyColor),
|
||||
],
|
||||
onChanged: (geometry, color) {
|
||||
if (selected == null) return;
|
||||
selected.geometry = geometry;
|
||||
selected.polyColor = color;
|
||||
widget.onPointChanged(selected);
|
||||
setState(() =>
|
||||
_pending[selected.id!] = _PendingGeometry(geometry, color));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -322,6 +368,54 @@ 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
|
||||
|
||||
@ -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/Components/multi_select_container.dart';
|
||||
import 'package:manager_app/Screens/Configurations/Section/SubSection/Map/category_filter.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() {
|
||||
return Wrap(
|
||||
spacing: kSpace5,
|
||||
runSpacing: kSpace4,
|
||||
crossAxisAlignment: WrapCrossAlignment.end,
|
||||
final categories = mapDTO.categories ?? [];
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 240,
|
||||
@ -238,45 +238,25 @@ class _MapConfigState extends State<MapConfig> {
|
||||
onChanged: (String value) => searchNotifier.value = value,
|
||||
),
|
||||
),
|
||||
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();
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -353,28 +333,23 @@ class _MapConfigState extends State<MapConfig> {
|
||||
}
|
||||
|
||||
Widget _buildMapHeader(Size size, String mapProviderIn) {
|
||||
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.stretch,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: kSpace6,
|
||||
runSpacing: kSpace5,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
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: AppLocalizations.of(context)!.serviceLabel,
|
||||
label: l.serviceLabel,
|
||||
color: Colors.black,
|
||||
initialValue: mapProviderIn,
|
||||
inputValues: map_providers,
|
||||
onChanged: (String value) {
|
||||
setState(() {
|
||||
switch (value) {
|
||||
case "Google":
|
||||
mapDTO.mapProvider = MapProvider.Google;
|
||||
@ -384,26 +359,64 @@ class _MapConfigState extends State<MapConfig> {
|
||||
break;
|
||||
}
|
||||
widget.onChanged(mapDTO);
|
||||
});
|
||||
}),
|
||||
GeolocInputContainer(
|
||||
label: AppLocalizations.of(context)!.centerPointLabel,
|
||||
initialValue: mapDTO.centerLatitude != null &&
|
||||
mapDTO.centerLongitude != null
|
||||
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.centerLongitude = localisation.longitude.toString();
|
||||
mapDTO.centerLatitude = localisation.latitude.toString();
|
||||
}
|
||||
widget.onChanged(mapDTO);
|
||||
},
|
||||
isSmall: true),
|
||||
ResourceInputContainer(
|
||||
label: AppLocalizations.of(context)!.iconLabel,
|
||||
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,
|
||||
@ -417,29 +430,35 @@ class _MapConfigState extends State<MapConfig> {
|
||||
}
|
||||
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: kSpace6),
|
||||
Wrap(
|
||||
spacing: kSpace6,
|
||||
runSpacing: kSpace5,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
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: AppLocalizations.of(context)!.listViewLabel,
|
||||
label: l.listViewOption,
|
||||
isChecked: mapDTO.isListViewEnabled ?? false,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -448,63 +467,81 @@ class _MapConfigState extends State<MapConfig> {
|
||||
});
|
||||
},
|
||||
),
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
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,
|
||||
decoration: BoxDecoration(
|
||||
color: kSurface,
|
||||
borderRadius: BorderRadius.circular(kRadiusCard),
|
||||
border: Border.all(color: kLine),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.9,
|
||||
color: kInk3,
|
||||
),
|
||||
),
|
||||
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),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -143,6 +143,26 @@ 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) {
|
||||
@ -167,6 +187,7 @@ class _MenuConfigState extends State<MenuConfig> {
|
||||
onReorder: _onReorder,
|
||||
onAdd: _createSubSection,
|
||||
onTap: _openSubSection,
|
||||
onToggleVisibility: _toggleVisibility,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,6 +58,12 @@ 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(
|
||||
@ -170,6 +176,8 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: kPagePadding,
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(_formRevision),
|
||||
child: EditorColumns(
|
||||
main: [
|
||||
_cardIdentity(appContext, l),
|
||||
@ -181,6 +189,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -354,10 +363,18 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
||||
.sectionApi!
|
||||
.sectionGetDetail(sectionDTO.id!);
|
||||
var nullableSection = SectionDTO.fromJson(rawData);
|
||||
if (nullableSection != null) {
|
||||
managerAppContext.selectedSection = nullableSection!;
|
||||
if (nullableSection == null) return;
|
||||
|
||||
managerAppContext.selectedSection = nullableSection;
|
||||
appContext.setContext(managerAppContext);
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
sectionDetailDTO = null;
|
||||
lastLoadedSectionId = null;
|
||||
_sectionFuture = Future.value(rawData);
|
||||
_formRevision++;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> delete(AppContext appContext) async {
|
||||
|
||||
@ -38,6 +38,15 @@ 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);
|
||||
@ -107,6 +116,8 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: kPagePadding,
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(_formRevision),
|
||||
child: EditorColumns(
|
||||
main: [
|
||||
_cardGeneral(config, l),
|
||||
@ -118,6 +129,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -277,9 +289,12 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
||||
return SectionReorderList(
|
||||
sectionsIn: sections!,
|
||||
configurationId: config.id!,
|
||||
onChangedOrder: (List<SectionDTO> sectionsOut) async {
|
||||
// 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) {
|
||||
sections = sectionsOut;
|
||||
await managerCtx.clientAPI!.sectionApi!.sectionUpdateOrder(sections!);
|
||||
_orderDirty = true;
|
||||
},
|
||||
askReload: () => setState(() {
|
||||
_sectionsFuture = null;
|
||||
@ -327,6 +342,15 @@ 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 {
|
||||
@ -346,6 +370,12 @@ 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);
|
||||
|
||||
@ -17,6 +17,7 @@ class SectionCard extends StatelessWidget {
|
||||
required this.onTap,
|
||||
this.dragHandle,
|
||||
this.isDropTarget = false,
|
||||
this.onToggleVisibility,
|
||||
}) : super(key: key);
|
||||
|
||||
final SectionDTO section;
|
||||
@ -25,6 +26,9 @@ 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)!;
|
||||
@ -101,6 +105,20 @@ 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!),
|
||||
],
|
||||
@ -150,6 +168,41 @@ 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
|
||||
@ -163,12 +216,14 @@ 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();
|
||||
@ -212,6 +267,9 @@ 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]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -79,10 +79,37 @@ 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,
|
||||
@ -102,6 +129,9 @@ 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(() {
|
||||
|
||||
@ -88,12 +88,29 @@ 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 été 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
|
||||
@ -103,14 +120,7 @@ class _MainScreenState extends State<MainScreen> {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.instance.id == widget.instance.id) return;
|
||||
|
||||
_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);
|
||||
}
|
||||
_applyInstance(widget.instance);
|
||||
|
||||
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
|
||||
}
|
||||
@ -275,7 +285,17 @@ 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'
|
||||
@ -543,6 +563,16 @@ 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);
|
||||
@ -608,10 +638,16 @@ class _MainScreenState extends State<MainScreen> {
|
||||
child: ValueListenableBuilder<int?>(
|
||||
valueListenable: currentPosition,
|
||||
builder: (context, value, _) {
|
||||
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
|
||||
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.
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: selectedElement,
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(instance.id),
|
||||
child: selectedElement!,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
|
||||
@ -5,6 +5,7 @@ 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';
|
||||
|
||||
@ -59,11 +60,14 @@ 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');
|
||||
@ -71,14 +75,16 @@ 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 (response.statusCode != 200 && mounted) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(l.userCreateError(utf8.decode(response.bodyBytes))),
|
||||
backgroundColor: kError,
|
||||
));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
showNotification(kError, kWhite,
|
||||
l.userCreateError(utf8.decode(response.bodyBytes)), context, null);
|
||||
return;
|
||||
}
|
||||
|
||||
showNotification(kSuccess, kWhite, l.userCreatedSuccess, context, null);
|
||||
await _loadUsers(ctx);
|
||||
}
|
||||
|
||||
@ -90,13 +96,36 @@ class _UsersScreenState extends State<UsersScreen> {
|
||||
'lastName': lastName,
|
||||
'role': roleValue,
|
||||
};
|
||||
await ctx.clientAPI!.apiApi!.invokeAPI(
|
||||
final response = 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 _loadUsers(ctx);
|
||||
}
|
||||
|
||||
@ -206,15 +235,17 @@ class _UsersScreenState extends State<UsersScreen> {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
// `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(
|
||||
title: Text(l.deleteUserTitle),
|
||||
content: Text(l.deleteUserConfirm(user['email'] as String? ?? '')),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(l.cancel)),
|
||||
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(l.cancel)),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(dialogContext);
|
||||
await _deleteUser(ctx, user['id'] as String);
|
||||
},
|
||||
child: Text(l.delete, style: const TextStyle(color: Colors.white)),
|
||||
|
||||
@ -581,13 +581,19 @@
|
||||
"searchLabel": "Search:",
|
||||
"geopointsLoadError": "Error loading geographic points",
|
||||
"geopointDeleteConfirm": "Are you sure you want to delete this geographic point?",
|
||||
"serviceLabel": "Service:",
|
||||
"centerPointLabel": "Center point:",
|
||||
"iconLabel": "Icon:",
|
||||
"listViewLabel": "List view:",
|
||||
"typeLabel": "Type:",
|
||||
"zoomLabel": "Zoom:",
|
||||
"categoriesLabel": "Categories:",
|
||||
"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",
|
||||
"startDateLabel": "Start date",
|
||||
"notDefined": "Not defined",
|
||||
"endDateLabel": "End date",
|
||||
@ -834,6 +840,25 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@ -1341,6 +1366,11 @@
|
||||
}
|
||||
},
|
||||
"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)",
|
||||
|
||||
@ -581,13 +581,19 @@
|
||||
"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 :",
|
||||
"centerPointLabel": "Point de centrage :",
|
||||
"iconLabel": "Icône :",
|
||||
"listViewLabel": "Vue liste :",
|
||||
"typeLabel": "Type :",
|
||||
"zoomLabel": "Zoom :",
|
||||
"categoriesLabel": "Catégories :",
|
||||
"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",
|
||||
"startDateLabel": "Date de début",
|
||||
"notDefined": "Non définie",
|
||||
"endDateLabel": "Date de fin",
|
||||
@ -834,6 +840,25 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@ -1341,6 +1366,11 @@
|
||||
}
|
||||
},
|
||||
"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)",
|
||||
|
||||
@ -2527,43 +2527,79 @@ 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 :'**
|
||||
/// **'Icône par défaut'**
|
||||
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 :'**
|
||||
/// **'Type de carte'**
|
||||
String get typeLabel;
|
||||
|
||||
/// No description provided for @zoomLabel.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Zoom :'**
|
||||
/// **'Zoom initial'**
|
||||
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.
|
||||
@ -3694,6 +3730,36 @@ 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:
|
||||
@ -5632,6 +5698,36 @@ 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:
|
||||
|
||||
@ -1349,25 +1349,43 @@ 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 centerPointLabel => 'Center point:';
|
||||
String get geopointConfirmPosition => 'Confirm position';
|
||||
|
||||
@override
|
||||
String get iconLabel => 'Icon:';
|
||||
String get clearFilter => 'Show all';
|
||||
|
||||
@override
|
||||
String get listViewLabel => 'List view:';
|
||||
String get mapGroupMap => 'Map';
|
||||
|
||||
@override
|
||||
String get typeLabel => 'Type:';
|
||||
String get mapGroupPoints => 'Points of interest';
|
||||
|
||||
@override
|
||||
String get zoomLabel => 'Zoom:';
|
||||
String get displayLabel => 'Display';
|
||||
|
||||
@override
|
||||
String get categoriesLabel => 'Categories:';
|
||||
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';
|
||||
|
||||
@override
|
||||
String get startDateLabel => 'Start date';
|
||||
@ -1960,6 +1978,26 @@ 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';
|
||||
@ -3048,6 +3086,22 @@ 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';
|
||||
|
||||
|
||||
@ -1381,25 +1381,43 @@ 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 centerPointLabel => 'Point de centrage :';
|
||||
String get geopointConfirmPosition => 'Valider la position';
|
||||
|
||||
@override
|
||||
String get iconLabel => 'Icône :';
|
||||
String get clearFilter => 'Tout afficher';
|
||||
|
||||
@override
|
||||
String get listViewLabel => 'Vue liste :';
|
||||
String get mapGroupMap => 'Carte';
|
||||
|
||||
@override
|
||||
String get typeLabel => 'Type :';
|
||||
String get mapGroupPoints => 'Points d\'intérêt';
|
||||
|
||||
@override
|
||||
String get zoomLabel => 'Zoom :';
|
||||
String get displayLabel => 'Affichage';
|
||||
|
||||
@override
|
||||
String get categoriesLabel => 'Catégories :';
|
||||
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';
|
||||
|
||||
@override
|
||||
String get startDateLabel => 'Date de début';
|
||||
@ -2000,6 +2018,26 @@ 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';
|
||||
@ -3099,6 +3137,22 @@ 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';
|
||||
|
||||
|
||||
@ -1363,25 +1363,43 @@ 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 centerPointLabel => 'Middelpunt:';
|
||||
String get geopointConfirmPosition => 'Positie bevestigen';
|
||||
|
||||
@override
|
||||
String get iconLabel => 'Pictogram:';
|
||||
String get clearFilter => 'Alles tonen';
|
||||
|
||||
@override
|
||||
String get listViewLabel => 'Lijstweergave:';
|
||||
String get mapGroupMap => 'Kaart';
|
||||
|
||||
@override
|
||||
String get typeLabel => 'Type:';
|
||||
String get mapGroupPoints => 'Interessepunten';
|
||||
|
||||
@override
|
||||
String get zoomLabel => 'Zoom:';
|
||||
String get displayLabel => 'Weergave';
|
||||
|
||||
@override
|
||||
String get categoriesLabel => 'Categorieën:';
|
||||
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';
|
||||
|
||||
@override
|
||||
String get startDateLabel => 'Startdatum';
|
||||
@ -1981,6 +1999,26 @@ 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';
|
||||
@ -3075,6 +3113,22 @@ 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';
|
||||
|
||||
|
||||
@ -581,13 +581,19 @@
|
||||
"searchLabel": "Zoeken:",
|
||||
"geopointsLoadError": "Fout bij het laden van geografische punten",
|
||||
"geopointDeleteConfirm": "Weet u zeker dat u dit geografische punt wilt verwijderen?",
|
||||
"serviceLabel": "Service:",
|
||||
"centerPointLabel": "Middelpunt:",
|
||||
"iconLabel": "Pictogram:",
|
||||
"listViewLabel": "Lijstweergave:",
|
||||
"typeLabel": "Type:",
|
||||
"zoomLabel": "Zoom:",
|
||||
"categoriesLabel": "Categorieën:",
|
||||
"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",
|
||||
"startDateLabel": "Startdatum",
|
||||
"notDefined": "Niet gedefinieerd",
|
||||
"endDateLabel": "Einddatum",
|
||||
@ -834,6 +840,25 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@ -1341,6 +1366,11 @@
|
||||
}
|
||||
},
|
||||
"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)",
|
||||
|
||||
@ -125,7 +125,12 @@ 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,
|
||||
);
|
||||
|
||||
@ -1306,4 +1306,57 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user