Update fix for parcours, event bloc call apis + update layout for game (tabs)

This commit is contained in:
Thomas Fransolet 2026-03-04 16:42:45 +01:00
parent ded4620bf2
commit e05e5234c8
9 changed files with 574 additions and 280 deletions

View File

@ -3,6 +3,10 @@ import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'showNewOrUpdateProgrammeBlock.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 { class EventConfig extends StatefulWidget {
final SectionEventDTO initialValue; final SectionEventDTO initialValue;
@ -25,6 +29,22 @@ class _EventConfigState extends State<EventConfig> {
void initState() { void initState() {
super.initState(); super.initState();
eventDTO = widget.initialValue; eventDTO = widget.initialValue;
WidgetsBinding.instance.addPostFrameCallback((_) => _loadProgrammeBlocks());
}
Future<void> _loadProgrammeBlocks() async {
if (eventDTO.id == null || !mounted) return;
final appContext = Provider.of<AppContext>(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 @override
@ -41,11 +61,11 @@ class _EventConfigState extends State<EventConfig> {
title: Text("Date de début"), title: Text("Date de début"),
subtitle: Text(eventDTO.startDate != null subtitle: Text(eventDTO.startDate != null
? DateFormat('dd/MM/yyyy HH:mm') ? DateFormat('dd/MM/yyyy HH:mm')
.format(eventDTO.startDate!) .format(eventDTO.startDate!.toLocal())
: "Non définie"), : "Non définie"),
trailing: Icon(Icons.calendar_today), trailing: Icon(Icons.calendar_today),
onTap: () async { onTap: () async {
DateTime initialDate = eventDTO.startDate ?? DateTime.now(); DateTime initialDate = eventDTO.startDate?.toLocal() ?? DateTime.now();
if (initialDate.isBefore(DateTime(2000))) { if (initialDate.isBefore(DateTime(2000))) {
initialDate = DateTime.now(); initialDate = DateTime.now();
} }
@ -71,7 +91,7 @@ class _EventConfigState extends State<EventConfig> {
TimeOfDay? time = await showTimePicker( TimeOfDay? time = await showTimePicker(
context: context, context: context,
initialTime: TimeOfDay.fromDateTime( initialTime: TimeOfDay.fromDateTime(
eventDTO.startDate ?? DateTime.now()), eventDTO.startDate?.toLocal() ?? DateTime.now()),
builder: (context, child) { builder: (context, child) {
return Theme( return Theme(
data: Theme.of(context).copyWith( data: Theme.of(context).copyWith(
@ -100,11 +120,11 @@ class _EventConfigState extends State<EventConfig> {
child: ListTile( child: ListTile(
title: Text("Date de fin"), title: Text("Date de fin"),
subtitle: Text(eventDTO.endDate != null 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"), : "Non définie"),
trailing: Icon(Icons.calendar_today), trailing: Icon(Icons.calendar_today),
onTap: () async { onTap: () async {
DateTime initialDate = eventDTO.endDate ?? DateTime initialDate = eventDTO.endDate?.toLocal() ??
DateTime.now().add(Duration(days: 1)); DateTime.now().add(Duration(days: 1));
if (initialDate.isBefore(DateTime(2000))) { if (initialDate.isBefore(DateTime(2000))) {
initialDate = DateTime.now().add(Duration(days: 1)); initialDate = DateTime.now().add(Duration(days: 1));
@ -130,7 +150,7 @@ class _EventConfigState extends State<EventConfig> {
if (picked != null) { if (picked != null) {
TimeOfDay? time = await showTimePicker( TimeOfDay? time = await showTimePicker(
context: context, context: context,
initialTime: TimeOfDay.fromDateTime(eventDTO.endDate ?? initialTime: TimeOfDay.fromDateTime(eventDTO.endDate?.toLocal() ??
DateTime.now().add(Duration(days: 1))), DateTime.now().add(Duration(days: 1))),
builder: (context, child) { builder: (context, child) {
return Theme( return Theme(
@ -171,17 +191,54 @@ class _EventConfigState extends State<EventConfig> {
icon: Icon(Icons.add), icon: Icon(Icons.add),
label: Text("Ajouter un bloc"), label: Text("Ajouter un bloc"),
onPressed: () { onPressed: () {
final appContext =
Provider.of<AppContext>(context, listen: false);
showNewOrUpdateProgrammeBlock( showNewOrUpdateProgrammeBlock(
context, context,
null, null,
(newBlock) { (newBlock) async {
setState(() { try {
eventDTO.programme = [ // The API expects ProgrammeBlockDTO, while eventDTO.programme is List<ProgrammeBlock>
...(eventDTO.programme ?? []), // Usually they are structural equivalents in these generated APIs, but let's be safe.
newBlock final programmeBlockDTO =
]; ProgrammeBlockDTO.fromJson(newBlock.toJson());
widget.onChanged(eventDTO); 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<EventConfig> {
IconButton( IconButton(
icon: Icon(Icons.edit, color: kPrimaryColor), icon: Icon(Icons.edit, color: kPrimaryColor),
onPressed: () { onPressed: () {
final appContext = Provider.of<AppContext>(
context,
listen: false);
showNewOrUpdateProgrammeBlock( showNewOrUpdateProgrammeBlock(
context, context,
block, block,
(updatedBlock) { (updatedBlock) async {
setState(() { try {
eventDTO.programme![index] = updatedBlock; final programmeBlockDTO =
widget.onChanged(eventDTO); 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( IconButton(
icon: Icon(Icons.delete, color: kError), icon: Icon(Icons.delete, color: kError),
onPressed: () { onPressed: () async {
setState(() { final appContext = Provider.of<AppContext>(
eventDTO.programme!.removeAt(index); context,
widget.onChanged(eventDTO); 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);
}
}, },
), ),
], ],

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; 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/multi_string_input_and_resource_container.dart';
import 'package:manager_app/Components/number_input_container.dart'; import 'package:manager_app/Components/number_input_container.dart';
import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/resource_input_container.dart';
@ -32,181 +32,204 @@ class _GameConfigState extends State<GameConfig> {
gameDTO = widget.initialValue; gameDTO = widget.initialValue;
gameDTO.rows = gameDTO.rows ?? 3; gameDTO.rows = gameDTO.rows ?? 3;
gameDTO.cols = gameDTO.cols ?? 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(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( int initialIndex = gameDTO.gameType?.value ?? 0;
children: [
Padding( return DefaultTabController(
padding: const EdgeInsets.symmetric(vertical: 16.0), key: ValueKey("${gameDTO.id}_$initialIndex"),
child: Row( length: 3,
mainAxisAlignment: MainAxisAlignment.center, 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: [ children: [
Text("Type de Jeu : ", NumberInputContainer(
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), label: "Lignes :",
DropdownButton<GameTypes>( initialValue: gameDTO.rows ?? 3,
value: gameDTO.gameType, color: kPrimaryColor,
items: [ isSmall: true,
DropdownMenuItem( onChanged: (String value) {
value: GameTypes.number0, child: Text("Puzzle")),
DropdownMenuItem(
value: GameTypes.number1, child: Text("Puzzle Glissant")),
DropdownMenuItem(
value: GameTypes.number2, child: Text("Escape Game")),
],
onChanged: (val) {
setState(() { 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); 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);
}
},
),
),
],
),
],
),
], ],
], ),
); );
} }
} }

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:manager_app/Components/geometry_input_container.dart'; import 'package:manager_app/Components/geometry_input_container.dart';
import 'package:manager_app/Components/dropDown_input_container_categories.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, void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO,
Function getResult, AppContext appContext, BuildContext context) { Function getResult, AppContext appContext, BuildContext context) {
GeoPointDTO geoPointDTO = GeoPointDTO(); GeoPointDTO geoPointDTO = GeoPointDTO();
if (inputGeoPointDTO != null) { if (inputGeoPointDTO != null) {
geoPointDTO = inputGeoPointDTO; geoPointDTO =
GeoPointDTO.fromJson(jsonDecode(jsonEncode(inputGeoPointDTO)))!;
} else { } else {
geoPointDTO.title = <TranslationDTO>[]; geoPointDTO.title = <TranslationDTO>[];
geoPointDTO.description = <TranslationDTO>[]; geoPointDTO.description = <TranslationDTO>[];

View File

@ -2,6 +2,10 @@ import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.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 { class ParcoursConfig extends StatefulWidget {
final List<GuidedPathDTO> initialValue; final List<GuidedPathDTO> initialValue;
@ -31,6 +35,24 @@ class _ParcoursConfigState extends State<ParcoursConfig> {
super.initState(); super.initState();
paths = List.from(widget.initialValue); paths = List.from(widget.initialValue);
paths.sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0)); paths.sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0));
WidgetsBinding.instance.addPostFrameCallback((_) => _loadFromApi());
}
Future<void> _loadFromApi() async {
final appContext = Provider.of<AppContext>(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 @override
@ -48,18 +70,48 @@ class _ParcoursConfigState extends State<ParcoursConfig> {
icon: Icon(Icons.add), icon: Icon(Icons.add),
label: Text("Ajouter un parcours"), label: Text("Ajouter un parcours"),
onPressed: () { onPressed: () {
final appContext =
Provider.of<AppContext>(context, listen: false);
showNewOrUpdateGuidedPath( showNewOrUpdateGuidedPath(
context, context,
null, null,
widget.parentId, widget.parentId,
widget.isEvent, widget.isEvent,
widget.isEscapeMode, widget.isEscapeMode,
(newPath) { (newPath) async {
setState(() { try {
newPath.order = paths.length; newPath.order = paths.length;
paths.add(newPath); newPath.instanceId = (appContext.getContext() as ManagerAppContext).instanceId;
widget.onChanged(paths); 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<ParcoursConfig> {
: ReorderableListView.builder( : ReorderableListView.builder(
buildDefaultDragHandles: false, buildDefaultDragHandles: false,
itemCount: paths.length, itemCount: paths.length,
onReorder: (oldIndex, newIndex) { onReorder: (oldIndex, newIndex) async {
setState(() { if (newIndex > oldIndex) newIndex -= 1;
if (newIndex > oldIndex) newIndex -= 1; final item = paths.removeAt(oldIndex);
final item = paths.removeAt(oldIndex); paths.insert(newIndex, item);
paths.insert(newIndex, item); for (int i = 0; i < paths.length; i++) {
for (int i = 0; i < paths.length; i++) { paths[i].order = i;
paths[i].order = i; }
}
widget.onChanged(paths); setState(() {});
}); widget.onChanged(paths);
final appContext =
Provider.of<AppContext>(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) { itemBuilder: (context, index) {
final path = paths[index]; final path = paths[index];
@ -112,31 +183,85 @@ class _ParcoursConfigState extends State<ParcoursConfig> {
IconButton( IconButton(
icon: Icon(Icons.edit, color: kPrimaryColor), icon: Icon(Icons.edit, color: kPrimaryColor),
onPressed: () { onPressed: () {
final appContext = Provider.of<AppContext>(
context,
listen: false);
showNewOrUpdateGuidedPath( showNewOrUpdateGuidedPath(
context, context,
path, path,
widget.parentId, widget.parentId,
widget.isEvent, widget.isEvent,
widget.isEscapeMode, widget.isEscapeMode,
(updatedPath) { (updatedPath) async {
setState(() { try {
paths[index] = updatedPath; final result =
widget.onChanged(paths); 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( IconButton(
icon: Icon(Icons.delete, color: kError), icon: Icon(Icons.delete, color: kError),
onPressed: () { onPressed: () async {
setState(() { final appContext = Provider.of<AppContext>(
paths.removeAt(index); context,
for (int i = 0; i < paths.length; i++) { listen: false);
paths[i].order = i; 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( ReorderableDragStartListener(

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
@ -15,7 +16,7 @@ void showNewOrUpdateGuidedPath(
Function(GuidedPathDTO) onSave, Function(GuidedPathDTO) onSave,
) { ) {
GuidedPathDTO workingPath = path != null GuidedPathDTO workingPath = path != null
? GuidedPathDTO.fromJson(path.toJson())! ? GuidedPathDTO.fromJson(jsonDecode(jsonEncode(path)))!
: GuidedPathDTO( : GuidedPathDTO(
title: [], title: [],
description: [], description: [],
@ -193,8 +194,10 @@ void showNewOrUpdateGuidedPath(
"Étape $index" "Étape $index"
: "Étape $index", : "Étape $index",
), ),
subtitle: Text( subtitle: isEscapeMode
"${step.quizQuestions?.length ?? 0} question(s)"), ? Text(
"${step.quizQuestions?.length ?? 0} question(s)")
: null,
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -254,6 +257,16 @@ void showNewOrUpdateGuidedPath(
child: RoundedButton( child: RoundedButton(
text: "Sauvegarder", text: "Sauvegarder",
press: () { 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); onSave(workingPath);
Navigator.pop(context); Navigator.pop(context);
}, },

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:manager_app/Components/confirmation_dialog.dart'; import 'package:manager_app/Components/confirmation_dialog.dart';
@ -14,8 +15,9 @@ void showNewOrUpdateGuidedStep(
bool isEscapeMode, bool isEscapeMode,
Function(GuidedStepDTO) onSave, Function(GuidedStepDTO) onSave,
) { ) {
// Use jsonEncode/jsonDecode for a robust deep copy that handles nested DTOs correctly
GuidedStepDTO workingStep = step != null GuidedStepDTO workingStep = step != null
? GuidedStepDTO.fromJson(step.toJson())! ? GuidedStepDTO.fromJson(jsonDecode(jsonEncode(step)))!
: GuidedStepDTO( : GuidedStepDTO(
title: [], title: [],
description: [], description: [],
@ -23,17 +25,6 @@ void showNewOrUpdateGuidedStep(
order: 0, 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( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@ -100,16 +91,14 @@ void showNewOrUpdateGuidedStep(
], ],
), ),
Divider(height: 24), Divider(height: 24),
// Géométrie conversion JSON entre les deux types GeoDTO // Géométrie Directement avec GeometryDTO
GeometryInputContainer( GeometryInputContainer(
label: "Emplacement de l'étape :", label: "Emplacement de l'étape :",
initialGeometry: initialGeometry: workingStep.geometry,
_toGeometryDTO(workingStep.geometry),
initialColor: null, initialColor: null,
onSave: (geometry, color) { onSave: (geometry, color) {
setState(() { setState(() {
workingStep.geometry = workingStep.geometry = geometry;
_toEventGeometry(geometry);
}); });
}, },
), ),
@ -244,6 +233,11 @@ void showNewOrUpdateGuidedStep(
child: RoundedButton( child: RoundedButton(
text: "Sauvegarder", text: "Sauvegarder",
press: () { 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); onSave(workingStep);
Navigator.pop(context); Navigator.pop(context);
}, },

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
@ -34,33 +35,27 @@ void showNewOrUpdateQuizQuestion(
bool isEscapeMode, bool isEscapeMode,
Function(QuizQuestion) onSave, Function(QuizQuestion) onSave,
) { ) {
// QuizQuestion.label is List<TranslationAndResourceDTO> convert for display // Use JSON cloning for a robust deep copy
QuizQuestion? clonedQuestion = question != null
? QuizQuestion.fromJson(jsonDecode(jsonEncode(question)))
: null;
List<TranslationDTO> workingLabel = _toTranslationList( List<TranslationDTO> workingLabel = _toTranslationList(
question != null && question.label.isNotEmpty clonedQuestion != null && clonedQuestion.label.isNotEmpty
? question.label ? clonedQuestion.label
: [TranslationAndResourceDTO(language: 'FR', value: '')]); : [TranslationAndResourceDTO(language: 'FR', value: '')]);
List<ResponseDTO> workingResponses = List<ResponseDTO> workingResponses = clonedQuestion?.responses ?? [];
question != null && question.responses.isNotEmpty
? question.responses
.map((r) => ResponseDTO.fromJson(r.toJson())!)
.toList()
: [];
QuizQuestion workingQuestion = QuizQuestion( QuizQuestion workingQuestion = clonedQuestion ??
id: question?.id ?? 0, QuizQuestion(
label: _fromTranslationList(workingLabel), // kept in sync below id: 0,
responses: workingResponses, label: _fromTranslationList(workingLabel),
validationQuestionType: responses: workingResponses,
question?.validationQuestionType ?? QuestionType.number0, validationQuestionType: QuestionType.number0,
puzzleImageId: question?.puzzleImageId, order: question?.order ?? 0,
puzzleImage: question?.puzzleImage, guidedStepId: question?.guidedStepId ?? stepId,
puzzleRows: question?.puzzleRows, );
puzzleCols: question?.puzzleCols,
isSlidingPuzzle: question?.isSlidingPuzzle,
order: question?.order ?? 0,
guidedStepId: question?.guidedStepId ?? stepId,
);
showDialog( showDialog(
context: context, context: context,

View File

@ -35,11 +35,16 @@ class Client {
SectionQuizApi? _sectionQuizApi; SectionQuizApi? _sectionQuizApi;
SectionQuizApi? get sectionQuizApi => _sectionQuizApi; SectionQuizApi? get sectionQuizApi => _sectionQuizApi;
SectionAgendaApi? _sectionAgendaApi;
SectionAgendaApi? get sectionAgendaApi => _sectionAgendaApi;
SectionEventApi? _sectionEventApi;
SectionEventApi? get sectionEventApi => _sectionEventApi;
Client(String path) { Client(String path) {
_apiClient = ApiClient( _apiClient = ApiClient(basePath: path);
basePath: path); //basePath: "https://192.168.31.140");
//basePath: "https://192.168.31.140"); //basePath: "https://localhost:44339");
//basePath: "https://localhost:44339");
_apiClient!.addDefaultHeader("Access-Control_Allow_Origin", "*"); _apiClient!.addDefaultHeader("Access-Control_Allow_Origin", "*");
_authenticationApi = AuthenticationApi(_apiClient); _authenticationApi = AuthenticationApi(_apiClient);
_instanceApi = InstanceApi(_apiClient); _instanceApi = InstanceApi(_apiClient);
@ -51,5 +56,7 @@ class Client {
_deviceApi = DeviceApi(_apiClient); _deviceApi = DeviceApi(_apiClient);
_sectionMapApi = SectionMapApi(_apiClient); _sectionMapApi = SectionMapApi(_apiClient);
_sectionQuizApi = SectionQuizApi(_apiClient); _sectionQuizApi = SectionQuizApi(_apiClient);
_sectionAgendaApi = SectionAgendaApi(_apiClient);
_sectionEventApi = SectionEventApi(_apiClient);
} }
} }

View File

@ -23,15 +23,15 @@ class GameTypes {
int toJson() => value; int toJson() => value;
static const number0 = GameTypes._(0); static const Puzzle = GameTypes._(0);
static const number1 = GameTypes._(1); static const SlidingPuzzle = GameTypes._(1);
static const number2 = GameTypes._(2); static const Escape = GameTypes._(2);
/// List of all possible values in this [enum][GameTypes]. /// List of all possible values in this [enum][GameTypes].
static const values = <GameTypes>[ static const values = <GameTypes>[
number0, Puzzle,
number1, SlidingPuzzle,
number2, Escape,
]; ];
static GameTypes? fromJson(dynamic value) => static GameTypes? fromJson(dynamic value) =>
@ -74,17 +74,32 @@ class GameTypesTypeTransformer {
/// and users are still using an old app with the old code. /// and users are still using an old app with the old code.
GameTypes? decode(dynamic data, {bool allowNull = true}) { GameTypes? decode(dynamic data, {bool allowNull = true}) {
if (data != null) { if (data != null) {
switch (data) { if (data.runtimeType == String) {
case 0: switch (data.toString()) {
return GameTypes.number0; case r'Puzzle':
case 1: return GameTypes.Puzzle;
return GameTypes.number1; case r'SlidingPuzzle':
case 2: return GameTypes.SlidingPuzzle;
return GameTypes.number2; case r'Escape':
default: return GameTypes.Escape;
if (!allowNull) { default:
throw ArgumentError('Unknown enum value to decode: $data'); 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; return null;