From e05e5234c825fac773d600900912e66bd210d66e Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Wed, 4 Mar 2026 16:42:45 +0100 Subject: [PATCH] Update fix for parcours, event bloc call apis + update layout for game (tabs) --- .../SubSection/Event/event_config.dart | 169 +++++++-- .../Section/SubSection/Game/game_config.dart | 347 ++++++++++-------- .../Map/showNewOrUpdateGeoPoint.dart | 5 +- .../SubSection/Parcours/parcours_config.dart | 179 +++++++-- .../Parcours/showNewOrUpdateGuidedPath.dart | 19 +- .../Parcours/showNewOrUpdateGuidedStep.dart | 28 +- .../Parcours/showNewOrUpdateQuizQuestion.dart | 41 +-- lib/client.dart | 17 +- manager_api_new/lib/model/game_types.dart | 49 ++- 9 files changed, 574 insertions(+), 280 deletions(-) diff --git a/lib/Screens/Configurations/Section/SubSection/Event/event_config.dart b/lib/Screens/Configurations/Section/SubSection/Event/event_config.dart index c945d40..fcacb8a 100644 --- a/lib/Screens/Configurations/Section/SubSection/Event/event_config.dart +++ b/lib/Screens/Configurations/Section/SubSection/Event/event_config.dart @@ -3,6 +3,10 @@ import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; import 'package:intl/intl.dart'; import 'showNewOrUpdateProgrammeBlock.dart'; +import 'package:provider/provider.dart'; +import 'package:manager_app/app_context.dart'; +import 'package:manager_app/Models/managerContext.dart'; +import 'package:manager_app/Components/message_notification.dart'; class EventConfig extends StatefulWidget { final SectionEventDTO initialValue; @@ -25,6 +29,22 @@ class _EventConfigState extends State { void initState() { super.initState(); eventDTO = widget.initialValue; + WidgetsBinding.instance.addPostFrameCallback((_) => _loadProgrammeBlocks()); + } + + Future _loadProgrammeBlocks() async { + if (eventDTO.id == null || !mounted) return; + final appContext = Provider.of(context, listen: false); + final api = (appContext.getContext() as ManagerAppContext).clientAPI!.sectionEventApi!; + try { + final blocks = await api.sectionEventGetAllProgrammeBlockFromSection(eventDTO.id!); + if (blocks == null || !mounted) return; + setState(() { + eventDTO.programme = blocks; + }); + } catch (e) { + // Silently keep initial value on error + } } @override @@ -41,11 +61,11 @@ class _EventConfigState extends State { title: Text("Date de début"), subtitle: Text(eventDTO.startDate != null ? DateFormat('dd/MM/yyyy HH:mm') - .format(eventDTO.startDate!) + .format(eventDTO.startDate!.toLocal()) : "Non définie"), trailing: Icon(Icons.calendar_today), onTap: () async { - DateTime initialDate = eventDTO.startDate ?? DateTime.now(); + DateTime initialDate = eventDTO.startDate?.toLocal() ?? DateTime.now(); if (initialDate.isBefore(DateTime(2000))) { initialDate = DateTime.now(); } @@ -71,7 +91,7 @@ class _EventConfigState extends State { TimeOfDay? time = await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime( - eventDTO.startDate ?? DateTime.now()), + eventDTO.startDate?.toLocal() ?? DateTime.now()), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( @@ -100,11 +120,11 @@ class _EventConfigState extends State { child: ListTile( title: Text("Date de fin"), subtitle: Text(eventDTO.endDate != null - ? DateFormat('dd/MM/yyyy HH:mm').format(eventDTO.endDate!) + ? DateFormat('dd/MM/yyyy HH:mm').format(eventDTO.endDate!.toLocal()) : "Non définie"), trailing: Icon(Icons.calendar_today), onTap: () async { - DateTime initialDate = eventDTO.endDate ?? + DateTime initialDate = eventDTO.endDate?.toLocal() ?? DateTime.now().add(Duration(days: 1)); if (initialDate.isBefore(DateTime(2000))) { initialDate = DateTime.now().add(Duration(days: 1)); @@ -130,7 +150,7 @@ class _EventConfigState extends State { if (picked != null) { TimeOfDay? time = await showTimePicker( context: context, - initialTime: TimeOfDay.fromDateTime(eventDTO.endDate ?? + initialTime: TimeOfDay.fromDateTime(eventDTO.endDate?.toLocal() ?? DateTime.now().add(Duration(days: 1))), builder: (context, child) { return Theme( @@ -171,17 +191,54 @@ class _EventConfigState extends State { icon: Icon(Icons.add), label: Text("Ajouter un bloc"), onPressed: () { + final appContext = + Provider.of(context, listen: false); showNewOrUpdateProgrammeBlock( context, null, - (newBlock) { - setState(() { - eventDTO.programme = [ - ...(eventDTO.programme ?? []), - newBlock - ]; - widget.onChanged(eventDTO); - }); + (newBlock) async { + try { + // The API expects ProgrammeBlockDTO, while eventDTO.programme is List + // Usually they are structural equivalents in these generated APIs, but let's be safe. + final programmeBlockDTO = + ProgrammeBlockDTO.fromJson(newBlock.toJson()); + if (programmeBlockDTO == null) return; + + final createdBlockDTO = + await (appContext.getContext() as ManagerAppContext) + .clientAPI! + .sectionEventApi! + .sectionEventCreateProgrammeBlock( + eventDTO.id!, programmeBlockDTO); + + if (createdBlockDTO != null) { + // Convert back if necessary + final createdBlock = + ProgrammeBlock.fromJson(createdBlockDTO.toJson()); + if (createdBlock != null) { + setState(() { + eventDTO.programme = [ + ...(eventDTO.programme ?? []), + createdBlock + ]; + widget.onChanged(eventDTO); + }); + showNotification( + kSuccess, + kWhite, + 'Bloc de programme créé avec succès', + context, + null); + } + } + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la création du bloc', + context, + null); + } }, ); }, @@ -219,25 +276,89 @@ class _EventConfigState extends State { IconButton( icon: Icon(Icons.edit, color: kPrimaryColor), onPressed: () { + final appContext = Provider.of( + context, + listen: false); showNewOrUpdateProgrammeBlock( context, block, - (updatedBlock) { - setState(() { - eventDTO.programme![index] = updatedBlock; - widget.onChanged(eventDTO); - }); + (updatedBlock) async { + try { + final programmeBlockDTO = + ProgrammeBlockDTO.fromJson( + updatedBlock.toJson()); + if (programmeBlockDTO == null) return; + + final resultDTO = + await (appContext.getContext() + as ManagerAppContext) + .clientAPI! + .sectionEventApi! + .sectionEventUpdateProgrammeBlock( + programmeBlockDTO); + + if (resultDTO != null) { + final result = ProgrammeBlock.fromJson( + resultDTO.toJson()); + if (result != null) { + setState(() { + eventDTO.programme![index] = result; + widget.onChanged(eventDTO); + }); + showNotification( + kSuccess, + kWhite, + 'Bloc mis à jour avec succès', + context, + null); + } + } + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la mise à jour du bloc', + context, + null); + } }, ); }, ), IconButton( icon: Icon(Icons.delete, color: kError), - onPressed: () { - setState(() { - eventDTO.programme!.removeAt(index); - widget.onChanged(eventDTO); - }); + onPressed: () async { + final appContext = Provider.of( + context, + listen: false); + try { + if (block.id != null) { + await (appContext.getContext() + as ManagerAppContext) + .clientAPI! + .sectionEventApi! + .sectionEventDeleteProgrammeBlock( + block.id!); + } + + setState(() { + eventDTO.programme!.removeAt(index); + widget.onChanged(eventDTO); + }); + showNotification( + kSuccess, + kWhite, + 'Bloc supprimé avec succès', + context, + null); + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la suppression du bloc', + context, + null); + } }, ), ], diff --git a/lib/Screens/Configurations/Section/SubSection/Game/game_config.dart b/lib/Screens/Configurations/Section/SubSection/Game/game_config.dart index 7f1b51e..c48c4af 100644 --- a/lib/Screens/Configurations/Section/SubSection/Game/game_config.dart +++ b/lib/Screens/Configurations/Section/SubSection/Game/game_config.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:manager_app/Components/message_notification.dart'; + import 'package:manager_app/Components/multi_string_input_and_resource_container.dart'; import 'package:manager_app/Components/number_input_container.dart'; import 'package:manager_app/Components/resource_input_container.dart'; @@ -32,181 +32,204 @@ class _GameConfigState extends State { gameDTO = widget.initialValue; gameDTO.rows = gameDTO.rows ?? 3; gameDTO.cols = gameDTO.cols ?? 3; - gameDTO.gameType = gameDTO.gameType ?? GameTypes.number0; + gameDTO.gameType = gameDTO.gameType ?? GameTypes.Puzzle; + gameDTO.messageDebut = gameDTO.messageDebut ?? []; + gameDTO.messageFin = gameDTO.messageFin ?? []; + gameDTO.guidedPaths = gameDTO.guidedPaths ?? []; super.initState(); } + @override + void didUpdateWidget(GameConfig oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialValue.id != oldWidget.initialValue.id) { + setState(() { + gameDTO = widget.initialValue; + gameDTO.rows = gameDTO.rows ?? 3; + gameDTO.cols = gameDTO.cols ?? 3; + gameDTO.gameType = gameDTO.gameType ?? GameTypes.Puzzle; + }); + } + } + + @override + void dispose() { + super.dispose(); + } + @override Widget build(BuildContext context) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + int initialIndex = gameDTO.gameType?.value ?? 0; + + return DefaultTabController( + key: ValueKey("${gameDTO.id}_$initialIndex"), + length: 3, + initialIndex: initialIndex, + child: Builder(builder: (context) { + final TabController controller = DefaultTabController.of(context); + + // Attach listener to sync gameType when tab changes + controller.addListener(() { + if (!controller.indexIsChanging) { + GameTypes newType = GameTypes.values[controller.index]; + if (gameDTO.gameType != newType) { + setState(() { + gameDTO.gameType = newType; + }); + widget.onChanged(gameDTO); + } + } + }); + + return Column( + children: [ + TabBar( + labelColor: kPrimaryColor, + unselectedLabelColor: Colors.grey, + indicatorColor: kPrimaryColor, + tabs: [ + Tab(icon: Icon(Icons.extension), text: "Puzzle"), + Tab(icon: Icon(Icons.grid_on), text: "Puzzle Glissant"), + Tab(icon: Icon(Icons.door_front_door), text: "Escape Game"), + ], + ), + Expanded( + child: Container( + height: 500, + child: TabBarView( + children: [ + _buildPuzzleConfig(), + _buildPuzzleConfig(), + ParcoursConfig( + initialValue: gameDTO.guidedPaths ?? [], + parentId: gameDTO.id!, + isEvent: false, + isEscapeMode: true, + onChanged: (paths) { + setState(() { + gameDTO.guidedPaths = paths; + widget.onChanged(gameDTO); + }); + }, + ), + ], + ), + ), + ), + ], + ); + }), + ); + } + + Widget _buildPuzzleConfig() { + return SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ResourceInputContainer( + label: "Image du puzzle :", + initialValue: gameDTO.puzzleImageId ?? '', + color: kPrimaryColor, + onChanged: (ResourceDTO resourceDTO) { + setState(() { + if (resourceDTO.id == null) { + gameDTO.puzzleImageId = null; + gameDTO.puzzleImage = null; + } else { + gameDTO.puzzleImageId = resourceDTO.id; + gameDTO.puzzleImage = resourceDTO; + } + widget.onChanged(gameDTO); + }); + }, + ), + Flexible( + child: MultiStringInputAndResourceContainer( + label: "Message départ :", + modalLabel: "Message départ", + fontSize: 20, + color: kPrimaryColor, + initialValue: gameDTO.messageDebut ?? [], + resourceTypes: [ + ResourceType.Image, + ResourceType.ImageUrl, + ResourceType.VideoUrl, + ResourceType.Video, + ResourceType.Audio + ], + onGetResult: (value) { + setState(() { + gameDTO.messageDebut = value; + widget.onChanged(gameDTO); + }); + }, + maxLines: 1, + isTitle: false, + ), + ), + Flexible( + child: MultiStringInputAndResourceContainer( + label: "Message fin :", + modalLabel: "Message fin", + fontSize: 20, + color: kPrimaryColor, + initialValue: gameDTO.messageFin ?? [], + resourceTypes: [ + ResourceType.Image, + ResourceType.ImageUrl, + ResourceType.VideoUrl, + ResourceType.Video, + ResourceType.Audio + ], + onGetResult: (value) { + setState(() { + gameDTO.messageFin = value; + widget.onChanged(gameDTO); + }); + }, + maxLines: 1, + isTitle: false, + ), + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Text("Type de Jeu : ", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), - DropdownButton( - value: gameDTO.gameType, - items: [ - DropdownMenuItem( - value: GameTypes.number0, child: Text("Puzzle")), - DropdownMenuItem( - value: GameTypes.number1, child: Text("Puzzle Glissant")), - DropdownMenuItem( - value: GameTypes.number2, child: Text("Escape Game")), - ], - onChanged: (val) { + NumberInputContainer( + label: "Lignes :", + initialValue: gameDTO.rows ?? 3, + color: kPrimaryColor, + isSmall: true, + onChanged: (String value) { setState(() { - gameDTO.gameType = val; + gameDTO.rows = int.tryParse(value) ?? 3; + widget.onChanged(gameDTO); + }); + }, + ), + NumberInputContainer( + label: "Colonnes :", + initialValue: gameDTO.cols ?? 3, + color: kPrimaryColor, + isSmall: true, + onChanged: (String value) { + setState(() { + gameDTO.cols = int.tryParse(value) ?? 3; widget.onChanged(gameDTO); }); }, ), ], ), - ), - if (gameDTO.gameType == GameTypes.number2) ...[ - // Escape Mode: Parcours Config - Expanded( - child: ParcoursConfig( - initialValue: gameDTO.guidedPaths ?? [], - parentId: gameDTO.id!, - isEvent: false, - isEscapeMode: true, - onChanged: (paths) { - setState(() { - gameDTO.guidedPaths = paths; - widget.onChanged(gameDTO); - }); - }, - ), - ), - ] else ...[ - // Regular Puzzle Mode - Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ResourceInputContainer( - label: "Image du puzzle :", - initialValue: gameDTO.puzzleImageId ?? '', - onChanged: (ResourceDTO resourceDTO) { - setState(() { - if (resourceDTO.id == null) { - gameDTO.puzzleImageId = null; - gameDTO.puzzleImage = null; - } else { - gameDTO.puzzleImageId = resourceDTO.id; - gameDTO.puzzleImage = resourceDTO; - } - widget.onChanged(gameDTO); - }); - }, - ), - Container( - height: 100, - child: MultiStringInputAndResourceContainer( - label: "Message départ :", - modalLabel: "Message départ", - fontSize: 20, - color: kPrimaryColor, - initialValue: gameDTO.messageDebut ?? [], - resourceTypes: [ - ResourceType.Image, - ResourceType.ImageUrl, - ResourceType.VideoUrl, - ResourceType.Video, - ResourceType.Audio - ], - onGetResult: (value) { - setState(() { - gameDTO.messageDebut = value; - widget.onChanged(gameDTO); - }); - }, - maxLines: 1, - isTitle: false, - ), - ), - Container( - height: 100, - child: MultiStringInputAndResourceContainer( - label: "Message fin :", - modalLabel: "Message fin", - fontSize: 20, - color: kPrimaryColor, - initialValue: gameDTO.messageFin ?? [], - resourceTypes: [ - ResourceType.Image, - ResourceType.ImageUrl, - ResourceType.VideoUrl, - ResourceType.Video, - ResourceType.Audio - ], - onGetResult: (value) { - setState(() { - gameDTO.messageFin = value; - widget.onChanged(gameDTO); - }); - }, - maxLines: 1, - isTitle: false, - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Container( - height: 100, - child: NumberInputContainer( - label: "Nombre de lignes :", - initialValue: gameDTO.rows ?? 3, - isSmall: true, - maxLength: 2, - onChanged: (value) { - try { - gameDTO.rows = int.parse(value); - setState(() { - widget.onChanged(gameDTO); - }); - } catch (e) { - showNotification(Colors.orange, kWhite, - 'Cela doit être un chiffre', context, null); - } - }, - ), - ), - Container( - height: 100, - child: NumberInputContainer( - label: "Nombre de colonnes :", - initialValue: gameDTO.cols ?? 3, - isSmall: true, - maxLength: 2, - onChanged: (value) { - try { - gameDTO.cols = int.parse(value); - setState(() { - widget.onChanged(gameDTO); - }); - } catch (e) { - showNotification(Colors.orange, kWhite, - 'Cela doit être un chiffre', context, null); - } - }, - ), - ), - ], - ), - ], - ), ], - ], + ), ); } } diff --git a/lib/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart b/lib/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart index 396cbf4..3c600b2 100644 --- a/lib/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart +++ b/lib/Screens/Configurations/Section/SubSection/Map/showNewOrUpdateGeoPoint.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:manager_app/Components/geometry_input_container.dart'; import 'package:manager_app/Components/dropDown_input_container_categories.dart'; @@ -13,9 +14,9 @@ import 'package:manager_api_new/api.dart'; void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO, Function getResult, AppContext appContext, BuildContext context) { GeoPointDTO geoPointDTO = GeoPointDTO(); - if (inputGeoPointDTO != null) { - geoPointDTO = inputGeoPointDTO; + geoPointDTO = + GeoPointDTO.fromJson(jsonDecode(jsonEncode(inputGeoPointDTO)))!; } else { geoPointDTO.title = []; geoPointDTO.description = []; diff --git a/lib/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart b/lib/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart index f0c27f2..bfeee7a 100644 --- a/lib/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart +++ b/lib/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart'; +import 'package:provider/provider.dart'; +import 'package:manager_app/app_context.dart'; +import 'package:manager_app/Models/managerContext.dart'; +import 'package:manager_app/Components/message_notification.dart'; class ParcoursConfig extends StatefulWidget { final List initialValue; @@ -31,6 +35,24 @@ class _ParcoursConfigState extends State { super.initState(); paths = List.from(widget.initialValue); paths.sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0)); + WidgetsBinding.instance.addPostFrameCallback((_) => _loadFromApi()); + } + + Future _loadFromApi() async { + final appContext = Provider.of(context, listen: false); + final api = (appContext.getContext() as ManagerAppContext).clientAPI!.sectionMapApi!; + try { + // Backend already includes steps + quiz questions via .Include() + final fetchedPaths = await api.sectionMapGetAllGuidedPathFromSection(widget.parentId); + if (fetchedPaths == null || !mounted) return; + + fetchedPaths.sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0)); + setState(() { + paths = fetchedPaths; + }); + } catch (e) { + // Silently keep initial value on error + } } @override @@ -48,18 +70,48 @@ class _ParcoursConfigState extends State { icon: Icon(Icons.add), label: Text("Ajouter un parcours"), onPressed: () { + final appContext = + Provider.of(context, listen: false); showNewOrUpdateGuidedPath( context, null, widget.parentId, widget.isEvent, widget.isEscapeMode, - (newPath) { - setState(() { + (newPath) async { + try { newPath.order = paths.length; - paths.add(newPath); - widget.onChanged(paths); - }); + newPath.instanceId = (appContext.getContext() as ManagerAppContext).instanceId; + if (widget.isEscapeMode) { + newPath.sectionGameId = widget.parentId; + } else if (widget.isEvent) { + newPath.sectionEventId = widget.parentId; + } else { + newPath.sectionMapId = widget.parentId; + } + final createdPath = + await (appContext.getContext() as ManagerAppContext) + .clientAPI! + .sectionMapApi! + .sectionMapCreateGuidedPath( + widget.parentId, newPath); + + if (createdPath != null) { + setState(() { + paths.add(createdPath); + widget.onChanged(paths); + }); + showNotification(kSuccess, kWhite, + 'Parcours créé avec succès', context, null); + } + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la création du parcours', + context, + null); + } }, ); }, @@ -77,16 +129,35 @@ class _ParcoursConfigState extends State { : ReorderableListView.builder( buildDefaultDragHandles: false, itemCount: paths.length, - onReorder: (oldIndex, newIndex) { - setState(() { - if (newIndex > oldIndex) newIndex -= 1; - final item = paths.removeAt(oldIndex); - paths.insert(newIndex, item); - for (int i = 0; i < paths.length; i++) { - paths[i].order = i; - } - widget.onChanged(paths); - }); + onReorder: (oldIndex, newIndex) async { + if (newIndex > oldIndex) newIndex -= 1; + final item = paths.removeAt(oldIndex); + paths.insert(newIndex, item); + for (int i = 0; i < paths.length; i++) { + paths[i].order = i; + } + + setState(() {}); + widget.onChanged(paths); + + final appContext = + Provider.of(context, listen: false); + final api = (appContext.getContext() as ManagerAppContext) + .clientAPI! + .sectionMapApi!; + + try { + // Update all affected paths orders + await Future.wait( + paths.map((p) => api.sectionMapUpdateGuidedPath(p))); + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la mise à jour de l\'ordre', + context, + null); + } }, itemBuilder: (context, index) { final path = paths[index]; @@ -112,31 +183,85 @@ class _ParcoursConfigState extends State { IconButton( icon: Icon(Icons.edit, color: kPrimaryColor), onPressed: () { + final appContext = Provider.of( + context, + listen: false); showNewOrUpdateGuidedPath( context, path, widget.parentId, widget.isEvent, widget.isEscapeMode, - (updatedPath) { - setState(() { - paths[index] = updatedPath; - widget.onChanged(paths); - }); + (updatedPath) async { + try { + final result = + await (appContext.getContext() + as ManagerAppContext) + .clientAPI! + .sectionMapApi! + .sectionMapUpdateGuidedPath( + updatedPath); + + if (result != null) { + setState(() { + paths[index] = result; + widget.onChanged(paths); + }); + showNotification( + kSuccess, + kWhite, + 'Parcours mis à jour avec succès', + context, + null); + } + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la mise à jour du parcours', + context, + null); + } }, ); }, ), IconButton( icon: Icon(Icons.delete, color: kError), - onPressed: () { - setState(() { - paths.removeAt(index); - for (int i = 0; i < paths.length; i++) { - paths[i].order = i; + onPressed: () async { + final appContext = Provider.of( + context, + listen: false); + try { + if (path.id != null) { + await (appContext.getContext() + as ManagerAppContext) + .clientAPI! + .sectionMapApi! + .sectionMapDeleteGuidedPath(path.id!); } - widget.onChanged(paths); - }); + + setState(() { + paths.removeAt(index); + for (int i = 0; i < paths.length; i++) { + paths[i].order = i; + } + widget.onChanged(paths); + }); + showNotification( + kSuccess, + kWhite, + 'Parcours supprimé avec succès', + context, + null); + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la suppression du parcours', + context, + null); + } }, ), ReorderableDragStartListener( diff --git a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart index b3bd7a4..1f0645f 100644 --- a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart +++ b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; @@ -15,7 +16,7 @@ void showNewOrUpdateGuidedPath( Function(GuidedPathDTO) onSave, ) { GuidedPathDTO workingPath = path != null - ? GuidedPathDTO.fromJson(path.toJson())! + ? GuidedPathDTO.fromJson(jsonDecode(jsonEncode(path)))! : GuidedPathDTO( title: [], description: [], @@ -193,8 +194,10 @@ void showNewOrUpdateGuidedPath( "Étape $index" : "Étape $index", ), - subtitle: Text( - "${step.quizQuestions?.length ?? 0} question(s)"), + subtitle: isEscapeMode + ? Text( + "${step.quizQuestions?.length ?? 0} question(s)") + : null, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -254,6 +257,16 @@ void showNewOrUpdateGuidedPath( child: RoundedButton( text: "Sauvegarder", press: () { + // Initialise les booleans null → false + workingPath.isLinear ??= false; + workingPath.requireSuccessToAdvance ??= false; + workingPath.hideNextStepsUntilComplete ??= false; + // Initialise les booleans nuls dans chaque étape + for (final s in workingPath.steps ?? []) { + s.isHiddenInitially ??= false; + s.isStepTimer ??= false; + s.isStepLocked ??= false; + } onSave(workingPath); Navigator.pop(context); }, diff --git a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedStep.dart b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedStep.dart index d2bc19a..52e60d1 100644 --- a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedStep.dart +++ b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedStep.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/Components/confirmation_dialog.dart'; @@ -14,8 +15,9 @@ void showNewOrUpdateGuidedStep( bool isEscapeMode, Function(GuidedStepDTO) onSave, ) { + // Use jsonEncode/jsonDecode for a robust deep copy that handles nested DTOs correctly GuidedStepDTO workingStep = step != null - ? GuidedStepDTO.fromJson(step.toJson())! + ? GuidedStepDTO.fromJson(jsonDecode(jsonEncode(step)))! : GuidedStepDTO( title: [], description: [], @@ -23,17 +25,6 @@ void showNewOrUpdateGuidedStep( order: 0, ); - // Convert EventAddressDTOGeometry to GeometryDTO via JSON for the geometry picker - GeometryDTO? _toGeometryDTO(EventAddressDTOGeometry? geo) { - if (geo == null) return null; - return GeometryDTO.fromJson(geo.toJson()); - } - - EventAddressDTOGeometry? _toEventGeometry(GeometryDTO? geo) { - if (geo == null) return null; - return EventAddressDTOGeometry.fromJson(geo.toJson()); - } - showDialog( context: context, builder: (BuildContext context) { @@ -100,16 +91,14 @@ void showNewOrUpdateGuidedStep( ], ), Divider(height: 24), - // Géométrie — conversion JSON entre les deux types GeoDTO + // Géométrie — Directement avec GeometryDTO GeometryInputContainer( label: "Emplacement de l'étape :", - initialGeometry: - _toGeometryDTO(workingStep.geometry), + initialGeometry: workingStep.geometry, initialColor: null, onSave: (geometry, color) { setState(() { - workingStep.geometry = - _toEventGeometry(geometry); + workingStep.geometry = geometry; }); }, ), @@ -244,6 +233,11 @@ void showNewOrUpdateGuidedStep( child: RoundedButton( text: "Sauvegarder", press: () { + // Initialise les booleans null → false + // pour éviter l'erreur backend "Error converting null to Boolean" + workingStep.isHiddenInitially ??= false; + workingStep.isStepTimer ??= false; + workingStep.isStepLocked ??= false; onSave(workingStep); Navigator.pop(context); }, diff --git a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart index 7ec5dee..729c4bc 100644 --- a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart +++ b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; @@ -34,33 +35,27 @@ void showNewOrUpdateQuizQuestion( bool isEscapeMode, Function(QuizQuestion) onSave, ) { - // QuizQuestion.label is List — convert for display + // Use JSON cloning for a robust deep copy + QuizQuestion? clonedQuestion = question != null + ? QuizQuestion.fromJson(jsonDecode(jsonEncode(question))) + : null; + List workingLabel = _toTranslationList( - question != null && question.label.isNotEmpty - ? question.label + clonedQuestion != null && clonedQuestion.label.isNotEmpty + ? clonedQuestion.label : [TranslationAndResourceDTO(language: 'FR', value: '')]); - List workingResponses = - question != null && question.responses.isNotEmpty - ? question.responses - .map((r) => ResponseDTO.fromJson(r.toJson())!) - .toList() - : []; + List workingResponses = clonedQuestion?.responses ?? []; - QuizQuestion workingQuestion = QuizQuestion( - id: question?.id ?? 0, - label: _fromTranslationList(workingLabel), // kept in sync below - responses: workingResponses, - validationQuestionType: - question?.validationQuestionType ?? QuestionType.number0, - puzzleImageId: question?.puzzleImageId, - puzzleImage: question?.puzzleImage, - puzzleRows: question?.puzzleRows, - puzzleCols: question?.puzzleCols, - isSlidingPuzzle: question?.isSlidingPuzzle, - order: question?.order ?? 0, - guidedStepId: question?.guidedStepId ?? stepId, - ); + QuizQuestion workingQuestion = clonedQuestion ?? + QuizQuestion( + id: 0, + label: _fromTranslationList(workingLabel), + responses: workingResponses, + validationQuestionType: QuestionType.number0, + order: question?.order ?? 0, + guidedStepId: question?.guidedStepId ?? stepId, + ); showDialog( context: context, diff --git a/lib/client.dart b/lib/client.dart index be94358..68480eb 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -35,11 +35,16 @@ class Client { SectionQuizApi? _sectionQuizApi; SectionQuizApi? get sectionQuizApi => _sectionQuizApi; + SectionAgendaApi? _sectionAgendaApi; + SectionAgendaApi? get sectionAgendaApi => _sectionAgendaApi; + + SectionEventApi? _sectionEventApi; + SectionEventApi? get sectionEventApi => _sectionEventApi; + Client(String path) { - _apiClient = ApiClient( - basePath: path); - //basePath: "https://192.168.31.140"); - //basePath: "https://localhost:44339"); + _apiClient = ApiClient(basePath: path); + //basePath: "https://192.168.31.140"); + //basePath: "https://localhost:44339"); _apiClient!.addDefaultHeader("Access-Control_Allow_Origin", "*"); _authenticationApi = AuthenticationApi(_apiClient); _instanceApi = InstanceApi(_apiClient); @@ -51,5 +56,7 @@ class Client { _deviceApi = DeviceApi(_apiClient); _sectionMapApi = SectionMapApi(_apiClient); _sectionQuizApi = SectionQuizApi(_apiClient); + _sectionAgendaApi = SectionAgendaApi(_apiClient); + _sectionEventApi = SectionEventApi(_apiClient); } -} \ No newline at end of file +} diff --git a/manager_api_new/lib/model/game_types.dart b/manager_api_new/lib/model/game_types.dart index 87169f4..c3b41b6 100644 --- a/manager_api_new/lib/model/game_types.dart +++ b/manager_api_new/lib/model/game_types.dart @@ -23,15 +23,15 @@ class GameTypes { int toJson() => value; - static const number0 = GameTypes._(0); - static const number1 = GameTypes._(1); - static const number2 = GameTypes._(2); + static const Puzzle = GameTypes._(0); + static const SlidingPuzzle = GameTypes._(1); + static const Escape = GameTypes._(2); /// List of all possible values in this [enum][GameTypes]. static const values = [ - number0, - number1, - number2, + Puzzle, + SlidingPuzzle, + Escape, ]; static GameTypes? fromJson(dynamic value) => @@ -74,17 +74,32 @@ class GameTypesTypeTransformer { /// and users are still using an old app with the old code. GameTypes? decode(dynamic data, {bool allowNull = true}) { if (data != null) { - switch (data) { - case 0: - return GameTypes.number0; - case 1: - return GameTypes.number1; - case 2: - return GameTypes.number2; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } + if (data.runtimeType == String) { + switch (data.toString()) { + case r'Puzzle': + return GameTypes.Puzzle; + case r'SlidingPuzzle': + return GameTypes.SlidingPuzzle; + case r'Escape': + return GameTypes.Escape; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } else if (data is int) { + switch (data) { + case 0: + return GameTypes.Puzzle; + case 1: + return GameTypes.SlidingPuzzle; + case 2: + return GameTypes.Escape; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } } } return null;