Compare commits
2 Commits
2fbee208e7
...
842f69ebc9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
842f69ebc9 | ||
|
|
0054122829 |
@ -11,10 +11,14 @@ FROM ghcr.io/cirruslabs/flutter:3.44.0 AS build
|
|||||||
# docker build --build-arg API_BASE_URL=https://api.mymuseum.be -t ... .
|
# docker build --build-arg API_BASE_URL=https://api.mymuseum.be -t ... .
|
||||||
ARG API_BASE_URL=http://localhost:5000
|
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
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN flutter pub get
|
RUN flutter pub get
|
||||||
RUN flutter build web --release --dart-define=API_BASE_URL=$API_BASE_URL
|
RUN flutter build web --release --dart-define=API_BASE_URL=$API_BASE_URL --dart-define=GIT_SHA=$GIT_SHA
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|||||||
@ -15,13 +15,8 @@ class QuotaBarsWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _QuotaBarsWidgetState extends State<QuotaBarsWidget> {
|
class _QuotaBarsWidgetState extends State<QuotaBarsWidget> {
|
||||||
InstanceQuotaDTO? _quota;
|
InstanceQuotaDTO? _quota;
|
||||||
bool _loading = true;
|
bool _loading = false;
|
||||||
|
String? _loadedInstanceId;
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _fetchQuota());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchQuota() async {
|
Future<void> _fetchQuota() async {
|
||||||
final managerContext = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
|
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 isAssistant = managerContext.instanceDTO?.isAssistant ?? false;
|
||||||
final l10n = AppLocalizations.of(context)!;
|
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) {
|
if (_loading) {
|
||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
height: 8,
|
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();
|
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> {
|
class _GeoPointEditorState extends State<GeoPointEditor> {
|
||||||
int? selectedId;
|
int? selectedId;
|
||||||
bool isMapExpanded = false;
|
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 {
|
GeoPointDTO? get _selected {
|
||||||
if (widget.visiblePoints.isEmpty) return null;
|
if (widget.visiblePoints.isEmpty) return null;
|
||||||
return widget.visiblePoints.firstWhere(
|
return widget.visiblePoints.firstWhere(
|
||||||
@ -265,12 +289,34 @@ class _GeoPointEditorState extends State<GeoPointEditor> {
|
|||||||
|
|
||||||
Widget _buildCanvas(AppLocalizations l, GeoPointDTO? selected,
|
Widget _buildCanvas(AppLocalizations l, GeoPointDTO? selected,
|
||||||
{required double height}) {
|
{required double height}) {
|
||||||
|
final pending = selected == null ? null : _pending[selected.id];
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(kSpace4),
|
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'),
|
key: ValueKey(selected?.id ?? 'none'),
|
||||||
geometry: selected?.geometry,
|
geometry: pending?.geometry ?? selected?.geometry,
|
||||||
color: selected?.polyColor,
|
color: pending?.color ?? selected?.polyColor,
|
||||||
height: height,
|
height: height,
|
||||||
// Les points d'une carte sont stockés en GeoJSON — voir
|
// Les points d'une carte sont stockés en GeoJSON — voir
|
||||||
// `visitapp-web/src/lib/geo.ts`, qui les lit en `[lng, lat]`.
|
// `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)),
|
: _label(l, selected, widget.visiblePoints.indexOf(selected)),
|
||||||
ghosts: [
|
ghosts: [
|
||||||
for (final point in widget.visiblePoints)
|
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(
|
MapCanvasGhost(
|
||||||
geometry: point.geometry!, color: point.polyColor),
|
geometry: _pending[point.id]?.geometry ?? point.geometry!,
|
||||||
|
color: _pending[point.id]?.color ?? point.polyColor),
|
||||||
],
|
],
|
||||||
onChanged: (geometry, color) {
|
onChanged: (geometry, color) {
|
||||||
if (selected == null) return;
|
if (selected == null) return;
|
||||||
selected.geometry = geometry;
|
setState(() =>
|
||||||
selected.polyColor = color;
|
_pending[selected.id!] = _PendingGeometry(geometry, color));
|
||||||
widget.onPointChanged(selected);
|
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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`.
|
/// Les champs d'un point, repris de `showNewOrUpdateGeoPoint`.
|
||||||
///
|
///
|
||||||
/// ⚠️ Un point porte neuf champs traduits. Les quatre du haut sont presque
|
/// ⚠️ 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/Screens/Configurations/Section/SubSection/Map/category_input_container.dart';
|
||||||
import 'package:manager_app/Components/dropDown_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/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/single_select_container.dart';
|
||||||
import 'package:manager_app/Components/check_input_container.dart';
|
import 'package:manager_app/Components/check_input_container.dart';
|
||||||
import 'package:manager_app/Components/slider_input_container.dart';
|
import 'package:manager_app/Components/slider_input_container.dart';
|
||||||
@ -226,10 +226,10 @@ class _MapConfigState extends State<MapConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPointsToolbar() {
|
Widget _buildPointsToolbar() {
|
||||||
return Wrap(
|
final categories = mapDTO.categories ?? [];
|
||||||
spacing: kSpace5,
|
|
||||||
runSpacing: kSpace4,
|
return Row(
|
||||||
crossAxisAlignment: WrapCrossAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 240,
|
width: 240,
|
||||||
@ -238,45 +238,25 @@ class _MapConfigState extends State<MapConfig> {
|
|||||||
onChanged: (String value) => searchNotifier.value = value,
|
onChanged: (String value) => searchNotifier.value = value,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (mapDTO.categories != null && mapDTO.categories!.isNotEmpty)
|
if (categories.isNotEmpty) ...[
|
||||||
SizedBox(
|
const SizedBox(width: kSpace6),
|
||||||
width: 320,
|
// Prend la largeur restante pour que les puces reviennent à la ligne
|
||||||
child: MultiSelectContainer(
|
// au lieu de sortir du cadre.
|
||||||
label: AppLocalizations.of(context)!.categoriesLabel,
|
Expanded(
|
||||||
color: kSecond,
|
child: ValueListenableBuilder<List<int>?>(
|
||||||
initialValue: mapDTO.categories!
|
valueListenable: selectedCategoriesNotifier,
|
||||||
.where((cat) =>
|
builder: (context, selected, _) => CategoryFilter(
|
||||||
selectedCategoriesNotifier.value!.contains(cat.id))
|
categories: categories,
|
||||||
.map((categorie) => _categoryLabel(categorie))
|
selectedIds: selected ?? const [],
|
||||||
.toList(),
|
onChanged: (ids) => selectedCategoriesNotifier.value = ids,
|
||||||
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
|
/// Un point vit côté serveur : chaque champ validé et chaque déplacement sur
|
||||||
/// la carte part tout de suite.
|
/// la carte part tout de suite.
|
||||||
Future<void> _savePoint(GeoPointDTO point) async {
|
Future<void> _savePoint(GeoPointDTO point) async {
|
||||||
@ -353,28 +333,23 @@ class _MapConfigState extends State<MapConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMapHeader(Size size, String mapProviderIn) {
|
Widget _buildMapHeader(Size size, String mapProviderIn) {
|
||||||
return Container(
|
final l = AppLocalizations.of(context)!;
|
||||||
padding: kCardPadding,
|
|
||||||
margin: const EdgeInsets.only(bottom: kSpace5),
|
// Décision du 2026-08-11 (lot E, W1) : visitapp-web reste sur Leaflet. Le
|
||||||
decoration: BoxDecoration(
|
// champ ne peut pas être masqué pour le web — il est porté par la section,
|
||||||
color: kSurface,
|
// et une même configuration est servie au mobile comme au web. La note vit
|
||||||
borderRadius: BorderRadius.circular(kRadiusCard),
|
// sous le champ qu'elle qualifie, au lieu de flotter en pleine largeur.
|
||||||
border: Border.all(color: kLine),
|
final service = Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Wrap(
|
|
||||||
spacing: kSpace6,
|
|
||||||
runSpacing: kSpace5,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
SingleSelectContainer(
|
SingleSelectContainer(
|
||||||
label: AppLocalizations.of(context)!.serviceLabel,
|
label: l.serviceLabel,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
initialValue: mapProviderIn,
|
initialValue: mapProviderIn,
|
||||||
inputValues: map_providers,
|
inputValues: map_providers,
|
||||||
onChanged: (String value) {
|
onChanged: (String value) {
|
||||||
|
setState(() {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "Google":
|
case "Google":
|
||||||
mapDTO.mapProvider = MapProvider.Google;
|
mapDTO.mapProvider = MapProvider.Google;
|
||||||
@ -384,26 +359,64 @@ class _MapConfigState extends State<MapConfig> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
widget.onChanged(mapDTO);
|
widget.onChanged(mapDTO);
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
GeolocInputContainer(
|
const SizedBox(height: kSpace2),
|
||||||
label: AppLocalizations.of(context)!.centerPointLabel,
|
Text(l.mapProviderMobileOnlyNote, style: kTextHint),
|
||||||
initialValue: mapDTO.centerLatitude != null &&
|
],
|
||||||
mapDTO.centerLongitude != null
|
);
|
||||||
|
|
||||||
|
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!),
|
? LatLong(double.parse(mapDTO.centerLatitude!),
|
||||||
double.parse(mapDTO.centerLongitude!))
|
double.parse(mapDTO.centerLongitude!))
|
||||||
: null,
|
: null,
|
||||||
color: kPrimaryColor,
|
color: kPrimaryColor,
|
||||||
onChanged: (LatLong? localisation) {
|
onChanged: (LatLong? localisation) {
|
||||||
if (localisation != null) {
|
if (localisation != null) {
|
||||||
mapDTO.centerLongitude =
|
mapDTO.centerLongitude = localisation.longitude.toString();
|
||||||
localisation.longitude.toString();
|
|
||||||
mapDTO.centerLatitude = localisation.latitude.toString();
|
mapDTO.centerLatitude = localisation.latitude.toString();
|
||||||
}
|
}
|
||||||
widget.onChanged(mapDTO);
|
widget.onChanged(mapDTO);
|
||||||
},
|
},
|
||||||
isSmall: true),
|
isSmall: true);
|
||||||
ResourceInputContainer(
|
|
||||||
label: AppLocalizations.of(context)!.iconLabel,
|
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,
|
initialValue: mapDTO.iconResourceId,
|
||||||
color: kPrimaryColor,
|
color: kPrimaryColor,
|
||||||
imageFit: BoxFit.contain,
|
imageFit: BoxFit.contain,
|
||||||
@ -417,29 +430,35 @@ class _MapConfigState extends State<MapConfig> {
|
|||||||
}
|
}
|
||||||
widget.onChanged(mapDTO);
|
widget.onChanged(mapDTO);
|
||||||
},
|
},
|
||||||
isSmall: true),
|
isSmall: true);
|
||||||
],
|
|
||||||
),
|
final categories = CategoryInputContainer(
|
||||||
const SizedBox(height: kSpace3),
|
label: l.categoriesLabel,
|
||||||
// Décision du 2026-08-11 (lot E, W1) : visitapp-web reste sur Leaflet.
|
initialValue: mapDTO.categories ?? [],
|
||||||
// Le champ ne peut pas être masqué pour le web — il est porté par la
|
color: kPrimaryColor,
|
||||||
// section, et une même configuration est servie au mobile comme au web.
|
onChanged: (List<CategorieDTO>? value) {
|
||||||
// On le dit donc au client au lieu de le laisser croire le contraire.
|
if (value == null) return;
|
||||||
Align(
|
mapDTO.categories = value;
|
||||||
alignment: Alignment.centerLeft,
|
mapDTO.points?.forEach((p) {
|
||||||
child: Text(
|
if (p.categorieId != null &&
|
||||||
AppLocalizations.of(context)!.mapProviderMobileOnlyNote,
|
!mapDTO.categories!
|
||||||
style: kTextHint,
|
.map((c) => c.id)
|
||||||
),
|
.any((e) => e != null && e == p.categorieId)) {
|
||||||
),
|
p.categorieId = null;
|
||||||
const SizedBox(height: kSpace6),
|
}
|
||||||
Wrap(
|
});
|
||||||
spacing: kSpace6,
|
widget.onChanged(mapDTO);
|
||||||
runSpacing: kSpace5,
|
},
|
||||||
crossAxisAlignment: WrapCrossAlignment.start,
|
);
|
||||||
|
|
||||||
|
final display = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
Text(l.displayLabel, style: kLabelField),
|
||||||
|
const SizedBox(height: kSpace2),
|
||||||
CheckInputContainer(
|
CheckInputContainer(
|
||||||
label: AppLocalizations.of(context)!.listViewLabel,
|
label: l.listViewOption,
|
||||||
isChecked: mapDTO.isListViewEnabled ?? false,
|
isChecked: mapDTO.isListViewEnabled ?? false,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
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
|
/// Ouvre l'écran de la sous-section. Il lit `selectedSubSectionRawData`, donc
|
||||||
/// on lui passe le JSON brut correspondant, pas seulement le DTO.
|
/// on lui passe le JSON brut correspondant, pas seulement le DTO.
|
||||||
void _openSubSection(SectionDTO section, int index) {
|
void _openSubSection(SectionDTO section, int index) {
|
||||||
@ -167,6 +187,7 @@ class _MenuConfigState extends State<MenuConfig> {
|
|||||||
onReorder: _onReorder,
|
onReorder: _onReorder,
|
||||||
onAdd: _createSubSection,
|
onAdd: _createSubSection,
|
||||||
onTap: _openSubSection,
|
onTap: _openSubSection,
|
||||||
|
onToggleVisibility: _toggleVisibility,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,12 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
|||||||
final GlobalKey globalKey = GlobalKey();
|
final GlobalKey globalKey = GlobalKey();
|
||||||
late Future<Object?> _sectionFuture;
|
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() {
|
Future<Object?> _loadSection() {
|
||||||
final appContext = Provider.of<AppContext>(context, listen: false);
|
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||||
return getSection(
|
return getSection(
|
||||||
@ -170,6 +176,8 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: kPagePadding,
|
padding: kPagePadding,
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: ValueKey(_formRevision),
|
||||||
child: EditorColumns(
|
child: EditorColumns(
|
||||||
main: [
|
main: [
|
||||||
_cardIdentity(appContext, l),
|
_cardIdentity(appContext, l),
|
||||||
@ -181,6 +189,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -354,10 +363,18 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
|
|||||||
.sectionApi!
|
.sectionApi!
|
||||||
.sectionGetDetail(sectionDTO.id!);
|
.sectionGetDetail(sectionDTO.id!);
|
||||||
var nullableSection = SectionDTO.fromJson(rawData);
|
var nullableSection = SectionDTO.fromJson(rawData);
|
||||||
if (nullableSection != null) {
|
if (nullableSection == null) return;
|
||||||
managerAppContext.selectedSection = nullableSection!;
|
|
||||||
|
managerAppContext.selectedSection = nullableSection;
|
||||||
appContext.setContext(managerAppContext);
|
appContext.setContext(managerAppContext);
|
||||||
}
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
sectionDetailDTO = null;
|
||||||
|
lastLoadedSectionId = null;
|
||||||
|
_sectionFuture = Future.value(rawData);
|
||||||
|
_formRevision++;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete(AppContext appContext) async {
|
Future<void> delete(AppContext appContext) async {
|
||||||
|
|||||||
@ -38,6 +38,15 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
|||||||
Future<ConfigurationDTO?>? _configFuture;
|
Future<ConfigurationDTO?>? _configFuture;
|
||||||
Future<List<SectionDTO>?>? _sectionsFuture;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appContext = Provider.of<AppContext>(context);
|
final appContext = Provider.of<AppContext>(context);
|
||||||
@ -107,6 +116,8 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: kPagePadding,
|
padding: kPagePadding,
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: ValueKey(_formRevision),
|
||||||
child: EditorColumns(
|
child: EditorColumns(
|
||||||
main: [
|
main: [
|
||||||
_cardGeneral(config, l),
|
_cardGeneral(config, l),
|
||||||
@ -118,6 +129,7 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -277,9 +289,12 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
|||||||
return SectionReorderList(
|
return SectionReorderList(
|
||||||
sectionsIn: sections!,
|
sectionsIn: sections!,
|
||||||
configurationId: config.id!,
|
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;
|
sections = sectionsOut;
|
||||||
await managerCtx.clientAPI!.sectionApi!.sectionUpdateOrder(sections!);
|
_orderDirty = true;
|
||||||
},
|
},
|
||||||
askReload: () => setState(() {
|
askReload: () => setState(() {
|
||||||
_sectionsFuture = null;
|
_sectionsFuture = null;
|
||||||
@ -327,6 +342,15 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
|
|||||||
ConfigurationDTO? configuration = await managerAppContext.clientAPI!.configurationApi!.configurationGetDetail(config.id!);
|
ConfigurationDTO? configuration = await managerAppContext.clientAPI!.configurationApi!.configurationGetDetail(config.id!);
|
||||||
managerAppContext.selectedConfiguration = configuration;
|
managerAppContext.selectedConfiguration = configuration;
|
||||||
appContext.setContext(managerAppContext);
|
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 {
|
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 {
|
Future<void> save(ConfigurationDTO config, AppContext appContext) async {
|
||||||
ManagerAppContext managerAppContext = appContext.getContext();
|
ManagerAppContext managerAppContext = appContext.getContext();
|
||||||
|
|
||||||
|
if (_orderDirty && sections != null) {
|
||||||
|
await managerAppContext.clientAPI!.sectionApi!.sectionUpdateOrder(sections!);
|
||||||
|
_orderDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
ConfigurationDTO? configuration = await managerAppContext.clientAPI!.configurationApi!.configurationUpdate(config);
|
ConfigurationDTO? configuration = await managerAppContext.clientAPI!.configurationApi!.configurationUpdate(config);
|
||||||
managerAppContext.selectedConfiguration = configuration;
|
managerAppContext.selectedConfiguration = configuration;
|
||||||
appContext.setContext(managerAppContext);
|
appContext.setContext(managerAppContext);
|
||||||
|
|||||||
@ -17,6 +17,7 @@ class SectionCard extends StatelessWidget {
|
|||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.dragHandle,
|
this.dragHandle,
|
||||||
this.isDropTarget = false,
|
this.isDropTarget = false,
|
||||||
|
this.onToggleVisibility,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final SectionDTO section;
|
final SectionDTO section;
|
||||||
@ -25,6 +26,9 @@ class SectionCard extends StatelessWidget {
|
|||||||
final Widget? dragHandle;
|
final Widget? dragHandle;
|
||||||
final bool isDropTarget;
|
final bool isDropTarget;
|
||||||
|
|
||||||
|
/// Null pour un utilisateur qui ne peut pas éditer : la tuile perd le bouton.
|
||||||
|
final VoidCallback? onToggleVisibility;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
@ -101,6 +105,20 @@ class SectionCard extends StatelessWidget {
|
|||||||
fontSize: 10, color: kInk3)),
|
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)
|
if (dragHandle != null)
|
||||||
Positioned(top: 2, right: 2, child: dragHandle!),
|
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 ».
|
/// 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
|
/// 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.onReorder,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.onAdd,
|
required this.onAdd,
|
||||||
|
this.onToggleVisibility,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final List<SectionDTO> sections;
|
final List<SectionDTO> sections;
|
||||||
final void Function(int oldIndex, int newIndex) onReorder;
|
final void Function(int oldIndex, int newIndex) onReorder;
|
||||||
final void Function(SectionDTO section, int index) onTap;
|
final void Function(SectionDTO section, int index) onTap;
|
||||||
final VoidCallback onAdd;
|
final VoidCallback onAdd;
|
||||||
|
final void Function(SectionDTO section)? onToggleVisibility;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SectionGrid> createState() => _SectionGridState();
|
State<SectionGrid> createState() => _SectionGridState();
|
||||||
@ -212,6 +267,9 @@ class _SectionGridState extends State<SectionGrid> {
|
|||||||
isDropTarget: dropTargetIndex == index,
|
isDropTarget: dropTargetIndex == index,
|
||||||
dragHandle: SectionDragHandle(index: index, width: width),
|
dragHandle: SectionDragHandle(index: index, width: width),
|
||||||
onTap: () => widget.onTap(widget.sections[index], index),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appContext = Provider.of<AppContext>(context);
|
final appContext = Provider.of<AppContext>(context);
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -102,6 +129,9 @@ class _SectionReorderListState extends State<SectionReorderList> {
|
|||||||
sections: sections,
|
sections: sections,
|
||||||
onReorder: _onReorder,
|
onReorder: _onReorder,
|
||||||
onAdd: () => _createSection(appContext),
|
onAdd: () => _createSection(appContext),
|
||||||
|
onToggleVisibility: canEdit
|
||||||
|
? (section) => _toggleVisibility(appContext, section)
|
||||||
|
: null,
|
||||||
onTap: (section, index) {
|
onTap: (section, index) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@ -88,12 +88,29 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_buildMenu(widget.instance);
|
_buildMenu(widget.instance);
|
||||||
|
_renderedInstanceId = widget.instance.id;
|
||||||
|
|
||||||
currentPosition.value ??= _defaultPosition(widget.instance);
|
currentPosition.value ??= _defaultPosition(widget.instance);
|
||||||
|
|
||||||
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, 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
|
/// 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
|
/// 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
|
/// 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);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.instance.id == widget.instance.id) return;
|
if (oldWidget.instance.id == widget.instance.id) return;
|
||||||
|
|
||||||
_buildMenu(widget.instance);
|
_applyInstance(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);
|
selectedElement = initElementToShow(context, widget.view, currentPosition.value!, menu, widget.instance);
|
||||||
}
|
}
|
||||||
@ -275,7 +285,17 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
managerCtx.instanceId = newInstance.id;
|
managerCtx.instanceId = newInstance.id;
|
||||||
managerCtx.instanceDTO = newInstance;
|
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'
|
final view = newInstance.isMobile! ? 'mobile'
|
||||||
: newInstance.isTablet! ? 'kiosk'
|
: newInstance.isTablet! ? 'kiosk'
|
||||||
: newInstance.isWeb! ? 'web'
|
: newInstance.isWeb! ? 'web'
|
||||||
@ -543,6 +563,16 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
final appContext = Provider.of<AppContext>(context);
|
final appContext = Provider.of<AppContext>(context);
|
||||||
ManagerAppContext managerAppContext = appContext.getContext();
|
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
|
// Synchronise les items de menu sensibles au rôle à chaque rebuild
|
||||||
final role = managerAppContext.role;
|
final role = managerAppContext.role;
|
||||||
final hasAdminItems = menu.sections!.any((s) => s.menuId == 8);
|
final hasAdminItems = menu.sections!.any((s) => s.menuId == 8);
|
||||||
@ -608,10 +638,16 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
child: ValueListenableBuilder<int?>(
|
child: ValueListenableBuilder<int?>(
|
||||||
valueListenable: currentPosition,
|
valueListenable: currentPosition,
|
||||||
builder: (context, value, _) {
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
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/Models/managerContext.dart';
|
||||||
import 'package:manager_app/app_context.dart';
|
import 'package:manager_app/app_context.dart';
|
||||||
import 'package:manager_app/Components/common_loader.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:manager_app/constants.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@ -59,11 +60,14 @@ class _UsersScreenState extends State<UsersScreen> {
|
|||||||
String firstName, String lastName, int roleValue) async {
|
String firstName, String lastName, int roleValue) async {
|
||||||
// No password sent: the backend generates an invitation token and emails
|
// No password sent: the backend generates an invitation token and emails
|
||||||
// a "set your password" link to the new user instead.
|
// 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 = {
|
final body = {
|
||||||
'email': email,
|
'email': email,
|
||||||
'firstName': firstName,
|
'firstName': firstName,
|
||||||
'lastName': lastName,
|
'lastName': lastName,
|
||||||
'role': roleValue,
|
'role': roleValue,
|
||||||
|
'instanceId': ctx.instanceId,
|
||||||
};
|
};
|
||||||
final response = await ctx.clientAPI!.apiApi!.invokeAPI(
|
final response = await ctx.clientAPI!.apiApi!.invokeAPI(
|
||||||
'/api/User', 'POST', [], body, {}, {}, 'application/json');
|
'/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à
|
// `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
|
// utilisé (409) ou un rôle refusé (403) laissaient la liste inchangée sans
|
||||||
// qu'aucun message n'explique pourquoi.
|
// qu'aucun message n'explique pourquoi.
|
||||||
if (response.statusCode != 200 && mounted) {
|
if (!mounted) return;
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Text(l.userCreateError(utf8.decode(response.bodyBytes))),
|
if (response.statusCode != 200) {
|
||||||
backgroundColor: kError,
|
showNotification(kError, kWhite,
|
||||||
));
|
l.userCreateError(utf8.decode(response.bodyBytes)), context, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showNotification(kSuccess, kWhite, l.userCreatedSuccess, context, null);
|
||||||
await _loadUsers(ctx);
|
await _loadUsers(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,13 +96,36 @@ class _UsersScreenState extends State<UsersScreen> {
|
|||||||
'lastName': lastName,
|
'lastName': lastName,
|
||||||
'role': roleValue,
|
'role': roleValue,
|
||||||
};
|
};
|
||||||
await ctx.clientAPI!.apiApi!.invokeAPI(
|
final response = await ctx.clientAPI!.apiApi!.invokeAPI(
|
||||||
'/api/User', 'PUT', [], body, {}, {}, 'application/json');
|
'/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);
|
await _loadUsers(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteUser(ManagerAppContext ctx, String id) async {
|
Future<void> _deleteUser(ManagerAppContext ctx, String id) async {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
|
try {
|
||||||
await ctx.clientAPI!.userApi!.userDeleteUser(id);
|
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);
|
await _loadUsers(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,15 +235,17 @@ class _UsersScreenState extends State<UsersScreen> {
|
|||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
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),
|
title: Text(l.deleteUserTitle),
|
||||||
content: Text(l.deleteUserConfirm(user['email'] as String? ?? '')),
|
content: Text(l.deleteUserConfirm(user['email'] as String? ?? '')),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(l.cancel)),
|
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(l.cancel)),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(dialogContext);
|
||||||
await _deleteUser(ctx, user['id'] as String);
|
await _deleteUser(ctx, user['id'] as String);
|
||||||
},
|
},
|
||||||
child: Text(l.delete, style: const TextStyle(color: Colors.white)),
|
child: Text(l.delete, style: const TextStyle(color: Colors.white)),
|
||||||
|
|||||||
@ -42,6 +42,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
String? instanceId;
|
String? instanceId;
|
||||||
String? pinCode;
|
String? pinCode;
|
||||||
Storage localStorage = window.localStorage;
|
Storage localStorage = window.localStorage;
|
||||||
|
late final Future<String> appVersion = getAppVersion();
|
||||||
|
|
||||||
void authenticateTRY(AppContext appContext, bool fromClick) async {
|
void authenticateTRY(AppContext appContext, bool fromClick) async {
|
||||||
clientAPI = Client(this.host!);
|
clientAPI = Client(this.host!);
|
||||||
@ -290,7 +291,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: getAppVersion(),
|
future: appVersion,
|
||||||
builder: (context, AsyncSnapshot<String> snapshot) {
|
builder: (context, AsyncSnapshot<String> snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done &&
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
snapshot.data != null) {
|
snapshot.data != null) {
|
||||||
@ -389,6 +390,6 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
|
|
||||||
Future<String> getAppVersion() async {
|
Future<String> getAppVersion() async {
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
return packageInfo.version;
|
return 'v${packageInfo.version} \u00b7 $kGitSha';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,13 @@ const kSuccess = Color(0xFF8bc34a);
|
|||||||
const kApiBaseUrl = String.fromEnvironment('API_BASE_URL',
|
const kApiBaseUrl = String.fromEnvironment('API_BASE_URL',
|
||||||
defaultValue: 'http://localhost:5000');
|
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
|
// Responsive
|
||||||
const kBreakpointMobile = 850.0;
|
const kBreakpointMobile = 850.0;
|
||||||
|
|
||||||
|
|||||||
@ -581,13 +581,19 @@
|
|||||||
"searchLabel": "Search:",
|
"searchLabel": "Search:",
|
||||||
"geopointsLoadError": "Error loading geographic points",
|
"geopointsLoadError": "Error loading geographic points",
|
||||||
"geopointDeleteConfirm": "Are you sure you want to delete this geographic point?",
|
"geopointDeleteConfirm": "Are you sure you want to delete this geographic point?",
|
||||||
"serviceLabel": "Service:",
|
"serviceLabel": "Service",
|
||||||
"centerPointLabel": "Center point:",
|
"geopointConfirmPosition": "Confirm position",
|
||||||
"iconLabel": "Icon:",
|
"clearFilter": "Show all",
|
||||||
"listViewLabel": "List view:",
|
"mapGroupMap": "Map",
|
||||||
"typeLabel": "Type:",
|
"mapGroupPoints": "Points of interest",
|
||||||
"zoomLabel": "Zoom:",
|
"displayLabel": "Display",
|
||||||
"categoriesLabel": "Categories:",
|
"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",
|
"startDateLabel": "Start date",
|
||||||
"notDefined": "Not defined",
|
"notDefined": "Not defined",
|
||||||
"endDateLabel": "End date",
|
"endDateLabel": "End date",
|
||||||
@ -834,6 +840,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"usersQuotaHint": "Delete a user to invite another one.",
|
"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": "Creation failed: {message}",
|
||||||
"@userCreateError": {
|
"@userCreateError": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -1341,6 +1366,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sectionHiddenBadge": "Hidden",
|
"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",
|
"addSectionTile": "Add a section",
|
||||||
"resourceTypeImage": "Image",
|
"resourceTypeImage": "Image",
|
||||||
"resourceTypeImageUrl": "Image (URL)",
|
"resourceTypeImageUrl": "Image (URL)",
|
||||||
|
|||||||
@ -581,13 +581,19 @@
|
|||||||
"searchLabel": "Recherche :",
|
"searchLabel": "Recherche :",
|
||||||
"geopointsLoadError": "Une erreur est survenue lors de la récupération des points géographiques",
|
"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 ?",
|
"geopointDeleteConfirm": "Êtes-vous sûr de vouloir supprimer ce point géographique ?",
|
||||||
"serviceLabel": "Service :",
|
"serviceLabel": "Service",
|
||||||
"centerPointLabel": "Point de centrage :",
|
"geopointConfirmPosition": "Valider la position",
|
||||||
"iconLabel": "Icône :",
|
"clearFilter": "Tout afficher",
|
||||||
"listViewLabel": "Vue liste :",
|
"mapGroupMap": "Carte",
|
||||||
"typeLabel": "Type :",
|
"mapGroupPoints": "Points d'intérêt",
|
||||||
"zoomLabel": "Zoom :",
|
"displayLabel": "Affichage",
|
||||||
"categoriesLabel": "Catégories :",
|
"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",
|
"startDateLabel": "Date de début",
|
||||||
"notDefined": "Non définie",
|
"notDefined": "Non définie",
|
||||||
"endDateLabel": "Date de fin",
|
"endDateLabel": "Date de fin",
|
||||||
@ -834,6 +840,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"usersQuotaHint": "Supprimez un utilisateur pour en inviter un autre.",
|
"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": "La création a échoué : {message}",
|
||||||
"@userCreateError": {
|
"@userCreateError": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -1341,6 +1366,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sectionHiddenBadge": "Masquée",
|
"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",
|
"addSectionTile": "Ajouter une section",
|
||||||
"resourceTypeImage": "Image",
|
"resourceTypeImage": "Image",
|
||||||
"resourceTypeImageUrl": "Image (URL)",
|
"resourceTypeImageUrl": "Image (URL)",
|
||||||
|
|||||||
@ -2527,43 +2527,79 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @serviceLabel.
|
/// No description provided for @serviceLabel.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
/// **'Service :'**
|
/// **'Service'**
|
||||||
String get serviceLabel;
|
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.
|
/// No description provided for @centerPointLabel.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
/// **'Point de centrage :'**
|
/// **'Point de centrage'**
|
||||||
String get centerPointLabel;
|
String get centerPointLabel;
|
||||||
|
|
||||||
/// No description provided for @iconLabel.
|
/// No description provided for @iconLabel.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
/// **'Icône :'**
|
/// **'Icône par défaut'**
|
||||||
String get iconLabel;
|
String get iconLabel;
|
||||||
|
|
||||||
/// No description provided for @listViewLabel.
|
/// No description provided for @listViewLabel.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
/// **'Vue liste :'**
|
/// **'Vue liste'**
|
||||||
String get listViewLabel;
|
String get listViewLabel;
|
||||||
|
|
||||||
/// No description provided for @typeLabel.
|
/// No description provided for @typeLabel.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
/// **'Type :'**
|
/// **'Type de carte'**
|
||||||
String get typeLabel;
|
String get typeLabel;
|
||||||
|
|
||||||
/// No description provided for @zoomLabel.
|
/// No description provided for @zoomLabel.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
/// **'Zoom :'**
|
/// **'Zoom initial'**
|
||||||
String get zoomLabel;
|
String get zoomLabel;
|
||||||
|
|
||||||
/// No description provided for @categoriesLabel.
|
/// No description provided for @categoriesLabel.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
/// **'Catégories :'**
|
/// **'Catégories'**
|
||||||
String get categoriesLabel;
|
String get categoriesLabel;
|
||||||
|
|
||||||
/// No description provided for @startDateLabel.
|
/// No description provided for @startDateLabel.
|
||||||
@ -3694,6 +3730,36 @@ abstract class AppLocalizations {
|
|||||||
/// **'Supprimez un utilisateur pour en inviter un autre.'**
|
/// **'Supprimez un utilisateur pour en inviter un autre.'**
|
||||||
String get usersQuotaHint;
|
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.
|
/// No description provided for @userCreateError.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
@ -5632,6 +5698,36 @@ abstract class AppLocalizations {
|
|||||||
/// **'Masquée'**
|
/// **'Masquée'**
|
||||||
String get sectionHiddenBadge;
|
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.
|
/// No description provided for @addSectionTile.
|
||||||
///
|
///
|
||||||
/// In fr, this message translates to:
|
/// In fr, this message translates to:
|
||||||
|
|||||||
@ -1349,25 +1349,43 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
'Are you sure you want to delete this geographic point?';
|
'Are you sure you want to delete this geographic point?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get serviceLabel => 'Service:';
|
String get serviceLabel => 'Service';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get centerPointLabel => 'Center point:';
|
String get geopointConfirmPosition => 'Confirm position';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get iconLabel => 'Icon:';
|
String get clearFilter => 'Show all';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listViewLabel => 'List view:';
|
String get mapGroupMap => 'Map';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeLabel => 'Type:';
|
String get mapGroupPoints => 'Points of interest';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get zoomLabel => 'Zoom:';
|
String get displayLabel => 'Display';
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
String get startDateLabel => 'Start date';
|
String get startDateLabel => 'Start date';
|
||||||
@ -1960,6 +1978,26 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get usersQuotaHint => 'Delete a user to invite another one.';
|
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
|
@override
|
||||||
String userCreateError(String message) {
|
String userCreateError(String message) {
|
||||||
return 'Creation failed: $message';
|
return 'Creation failed: $message';
|
||||||
@ -3048,6 +3086,22 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sectionHiddenBadge => 'Hidden';
|
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
|
@override
|
||||||
String get addSectionTile => 'Add a section';
|
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 ?';
|
'Êtes-vous sûr de vouloir supprimer ce point géographique ?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get serviceLabel => 'Service :';
|
String get serviceLabel => 'Service';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get centerPointLabel => 'Point de centrage :';
|
String get geopointConfirmPosition => 'Valider la position';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get iconLabel => 'Icône :';
|
String get clearFilter => 'Tout afficher';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listViewLabel => 'Vue liste :';
|
String get mapGroupMap => 'Carte';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeLabel => 'Type :';
|
String get mapGroupPoints => 'Points d\'intérêt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get zoomLabel => 'Zoom :';
|
String get displayLabel => 'Affichage';
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
String get startDateLabel => 'Date de début';
|
String get startDateLabel => 'Date de début';
|
||||||
@ -2000,6 +2018,26 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get usersQuotaHint =>
|
String get usersQuotaHint =>
|
||||||
'Supprimez un utilisateur pour en inviter un autre.';
|
'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
|
@override
|
||||||
String userCreateError(String message) {
|
String userCreateError(String message) {
|
||||||
return 'La création a échoué : $message';
|
return 'La création a échoué : $message';
|
||||||
@ -3099,6 +3137,22 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sectionHiddenBadge => 'Masquée';
|
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
|
@override
|
||||||
String get addSectionTile => 'Ajouter une section';
|
String get addSectionTile => 'Ajouter une section';
|
||||||
|
|
||||||
|
|||||||
@ -1363,25 +1363,43 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
'Weet u zeker dat u dit geografische punt wilt verwijderen?';
|
'Weet u zeker dat u dit geografische punt wilt verwijderen?';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get serviceLabel => 'Service:';
|
String get serviceLabel => 'Service';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get centerPointLabel => 'Middelpunt:';
|
String get geopointConfirmPosition => 'Positie bevestigen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get iconLabel => 'Pictogram:';
|
String get clearFilter => 'Alles tonen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listViewLabel => 'Lijstweergave:';
|
String get mapGroupMap => 'Kaart';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get typeLabel => 'Type:';
|
String get mapGroupPoints => 'Interessepunten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get zoomLabel => 'Zoom:';
|
String get displayLabel => 'Weergave';
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
String get startDateLabel => 'Startdatum';
|
String get startDateLabel => 'Startdatum';
|
||||||
@ -1981,6 +1999,26 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get usersQuotaHint =>
|
String get usersQuotaHint =>
|
||||||
'Verwijder een gebruiker om er een andere uit te nodigen.';
|
'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
|
@override
|
||||||
String userCreateError(String message) {
|
String userCreateError(String message) {
|
||||||
return 'Aanmaken mislukt: $message';
|
return 'Aanmaken mislukt: $message';
|
||||||
@ -3075,6 +3113,22 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sectionHiddenBadge => 'Verborgen';
|
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
|
@override
|
||||||
String get addSectionTile => 'Een sectie toevoegen';
|
String get addSectionTile => 'Een sectie toevoegen';
|
||||||
|
|
||||||
|
|||||||
@ -581,13 +581,19 @@
|
|||||||
"searchLabel": "Zoeken:",
|
"searchLabel": "Zoeken:",
|
||||||
"geopointsLoadError": "Fout bij het laden van geografische punten",
|
"geopointsLoadError": "Fout bij het laden van geografische punten",
|
||||||
"geopointDeleteConfirm": "Weet u zeker dat u dit geografische punt wilt verwijderen?",
|
"geopointDeleteConfirm": "Weet u zeker dat u dit geografische punt wilt verwijderen?",
|
||||||
"serviceLabel": "Service:",
|
"serviceLabel": "Service",
|
||||||
"centerPointLabel": "Middelpunt:",
|
"geopointConfirmPosition": "Positie bevestigen",
|
||||||
"iconLabel": "Pictogram:",
|
"clearFilter": "Alles tonen",
|
||||||
"listViewLabel": "Lijstweergave:",
|
"mapGroupMap": "Kaart",
|
||||||
"typeLabel": "Type:",
|
"mapGroupPoints": "Interessepunten",
|
||||||
"zoomLabel": "Zoom:",
|
"displayLabel": "Weergave",
|
||||||
"categoriesLabel": "Categorieën:",
|
"listViewOption": "Lijstweergave aanbieden",
|
||||||
|
"centerPointLabel": "Middelpunt",
|
||||||
|
"iconLabel": "Standaardpictogram",
|
||||||
|
"listViewLabel": "Lijstweergave",
|
||||||
|
"typeLabel": "Kaarttype",
|
||||||
|
"zoomLabel": "Beginzoom",
|
||||||
|
"categoriesLabel": "Categorieën",
|
||||||
"startDateLabel": "Startdatum",
|
"startDateLabel": "Startdatum",
|
||||||
"notDefined": "Niet gedefinieerd",
|
"notDefined": "Niet gedefinieerd",
|
||||||
"endDateLabel": "Einddatum",
|
"endDateLabel": "Einddatum",
|
||||||
@ -834,6 +840,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"usersQuotaHint": "Verwijder een gebruiker om er een andere uit te nodigen.",
|
"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": "Aanmaken mislukt: {message}",
|
||||||
"@userCreateError": {
|
"@userCreateError": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -1341,6 +1366,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sectionHiddenBadge": "Verborgen",
|
"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",
|
"addSectionTile": "Een sectie toevoegen",
|
||||||
"resourceTypeImage": "Afbeelding",
|
"resourceTypeImage": "Afbeelding",
|
||||||
"resourceTypeImageUrl": "Afbeelding (URL)",
|
"resourceTypeImageUrl": "Afbeelding (URL)",
|
||||||
|
|||||||
@ -125,7 +125,12 @@ Future<void> main() async {
|
|||||||
path: '/main/:view',
|
path: '/main/:view',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final view = state.pathParameters['view'];
|
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(
|
return MainScreen(
|
||||||
|
key: ValueKey('${managerAppContext.instanceId}/$view'),
|
||||||
instance: managerAppContext.instanceDTO!,
|
instance: managerAppContext.instanceDTO!,
|
||||||
view: view,
|
view: view,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1306,4 +1306,57 @@ class SectionApi {
|
|||||||
}
|
}
|
||||||
return null;
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 3.0.0+10
|
version: 3.1.3+11
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.1.0 <4.0.0"
|
sdk: ">=3.1.0 <4.0.0"
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<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 content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter application.">
|
<meta name="description" content="A new Flutter application.">
|
||||||
|
|
||||||
@ -112,9 +113,6 @@
|
|||||||
loadMainDartJs();
|
loadMainDartJs();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript">
|
|
||||||
window.flutterWebRenderer = "html";
|
|
||||||
</script>
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
// Import the functions you need from the SDKs you need
|
// Import the functions you need from the SDKs you need
|
||||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";
|
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user