Update fix for parcours, event bloc call apis + update layout for game (tabs)
This commit is contained in:
parent
ded4620bf2
commit
e05e5234c8
@ -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<EventConfig> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
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
|
||||
@ -41,11 +61,11 @@ class _EventConfigState extends State<EventConfig> {
|
||||
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<EventConfig> {
|
||||
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<EventConfig> {
|
||||
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<EventConfig> {
|
||||
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<EventConfig> {
|
||||
icon: Icon(Icons.add),
|
||||
label: Text("Ajouter un bloc"),
|
||||
onPressed: () {
|
||||
final appContext =
|
||||
Provider.of<AppContext>(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<ProgrammeBlock>
|
||||
// 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<EventConfig> {
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit, color: kPrimaryColor),
|
||||
onPressed: () {
|
||||
final appContext = Provider.of<AppContext>(
|
||||
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<AppContext>(
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@ -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<GameConfig> {
|
||||
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<GameTypes>(
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = <TranslationDTO>[];
|
||||
geoPointDTO.description = <TranslationDTO>[];
|
||||
|
||||
@ -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<GuidedPathDTO> initialValue;
|
||||
@ -31,6 +35,24 @@ class _ParcoursConfigState extends State<ParcoursConfig> {
|
||||
super.initState();
|
||||
paths = List.from(widget.initialValue);
|
||||
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
|
||||
@ -48,18 +70,48 @@ class _ParcoursConfigState extends State<ParcoursConfig> {
|
||||
icon: Icon(Icons.add),
|
||||
label: Text("Ajouter un parcours"),
|
||||
onPressed: () {
|
||||
final appContext =
|
||||
Provider.of<AppContext>(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<ParcoursConfig> {
|
||||
: 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<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) {
|
||||
final path = paths[index];
|
||||
@ -112,31 +183,85 @@ class _ParcoursConfigState extends State<ParcoursConfig> {
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit, color: kPrimaryColor),
|
||||
onPressed: () {
|
||||
final appContext = Provider.of<AppContext>(
|
||||
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<AppContext>(
|
||||
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(
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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<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(
|
||||
question != null && question.label.isNotEmpty
|
||||
? question.label
|
||||
clonedQuestion != null && clonedQuestion.label.isNotEmpty
|
||||
? clonedQuestion.label
|
||||
: [TranslationAndResourceDTO(language: 'FR', value: '')]);
|
||||
|
||||
List<ResponseDTO> workingResponses =
|
||||
question != null && question.responses.isNotEmpty
|
||||
? question.responses
|
||||
.map((r) => ResponseDTO.fromJson(r.toJson())!)
|
||||
.toList()
|
||||
: [];
|
||||
List<ResponseDTO> 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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 = <GameTypes>[
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user