import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/Components/confirmation_dialog.dart'; import 'package:manager_app/Components/geometry_input_container.dart'; import 'package:manager_app/Components/reorderable_custom_list.dart'; import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/rounded_button.dart'; import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Slider/listView_card_image.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Slider/new_update_image_slider.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'showNewOrUpdateQuizQuestion.dart'; void showNewOrUpdateGuidedStep( BuildContext context, GuidedStepDTO? step, String pathId, bool isEscapeMode, FutureOr Function(GuidedStepDTO) onSave, ) { // Use jsonEncode/jsonDecode for a robust deep copy that handles nested DTOs correctly GuidedStepDTO workingStep = step != null ? GuidedStepDTO.fromJson(jsonDecode(jsonEncode(step)))! : GuidedStepDTO( title: [], description: [], quizQuestions: [], audioIds: [], contents: [], order: 0, ); workingStep.audioIds = List.from(workingStep.audioIds ?? []); workingStep.contents = List.from(workingStep.contents ?? []); workingStep.quizQuestions = List.from(workingStep.quizQuestions ?? []); bool isSaving = false; int questionsRevision = 0; showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return StatefulBuilder( builder: (context, setState) { final double screenWidth = MediaQuery.of(context).size.width; final double screenHeight = MediaQuery.of(context).size.height; final double dialogWidth = screenWidth * 0.75; final double contentWidth = dialogWidth - 48; final double halfWidth = (contentWidth - 20) / 2; final appCtx = Provider.of(context, listen: false); return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( width: dialogWidth, constraints: BoxConstraints(maxHeight: screenHeight * 0.85), padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( step == null ? AppLocalizations.of(context)!.newStepTitle : AppLocalizations.of(context)!.editStepTitle, style: TextStyle( color: kPrimaryColor, fontSize: 20, fontWeight: FontWeight.bold), ), SizedBox(height: 16), Flexible( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Titre + Description côte à côte Row( children: [ SizedBox( width: halfWidth, child: MultiStringInputContainer( label: "Titre :", modalLabel: AppLocalizations.of(context)!.stepTitleLabel, initialValue: workingStep.title ?? [], onGetResult: (val) => setState(() => workingStep.title = val), maxLines: 1, isTitle: true, isHTML: true, ), ), SizedBox(width: 20), SizedBox( width: halfWidth, child: MultiStringInputContainer( label: "Description :", modalLabel: AppLocalizations.of(context)!.stepDescriptionLabel, initialValue: workingStep.description ?? [], onGetResult: (val) => setState( () => workingStep.description = val), maxLines: 1, isTitle: false, isHTML: true, ), ), ], ), Divider(height: 24), // Géométrie — Directement avec GeometryDTO GeometryInputContainer( label: AppLocalizations.of(context)!.stepLocationLabel, initialGeometry: workingStep.geometry, initialColor: null, onSave: (geometry, color) { setState(() { workingStep.geometry = geometry; }); }, ), SizedBox(height: 8), SwitchListTile( dense: true, contentPadding: EdgeInsets.zero, title: Text("Déclenchement géolocalisé"), value: workingStep.isGeoTriggered ?? false, onChanged: (val) => setState(() => workingStep.isGeoTriggered = val), activeThumbColor: kPrimaryColor, ), if (workingStep.isGeoTriggered == true) ...[ SizedBox(height: 8), SizedBox( width: halfWidth, child: StringInputContainer( label: "Rayon (mètres) :", initialValue: workingStep.zoneRadiusMeters?.toString() ?? "", onChanged: (val) => setState(() => workingStep.zoneRadiusMeters = double.tryParse(val)), ), ), ], Divider(height: 24), // Comportement de l'étape Text("Comportement", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), SizedBox(height: 8), Row( children: [ Expanded( child: SwitchListTile( dense: true, title: Text(AppLocalizations.of(context)!.initiallyHiddenLabel), value: workingStep.isHiddenInitially ?? false, onChanged: (val) => setState(() => workingStep.isHiddenInitially = val), activeThumbColor: kPrimaryColor, ), ), Expanded( child: SwitchListTile( dense: true, title: Text(AppLocalizations.of(context)!.lockedLabel), value: workingStep.isStepLocked ?? false, onChanged: (val) => setState(() => workingStep.isStepLocked = val), activeThumbColor: kPrimaryColor, ), ), ], ), Divider(height: 24), Text("Contenu riche", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), SizedBox(height: 8), // Audio MultiStringInputContainer( label: "Audio :", resourceTypes: [ResourceType.Audio], modalLabel: "Audio du guide", initialValue: workingStep.audioIds!, onGetResult: (val) => setState(() => workingStep.audioIds = val.isEmpty ? null : val), maxLines: 1, isTitle: false, isHTML: false, ), SizedBox(height: 12), // Images Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Images :", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), IconButton( icon: Icon(Icons.add_circle_outline, color: kSuccess), onPressed: () async { final result = await showNewOrUpdateContentSlider(null, appCtx, context, true, false); if (result != null) { setState(() { result.order = workingStep.contents!.length; workingStep.contents = [...workingStep.contents!, result]; }); } }, ), ], ), if (workingStep.contents!.isEmpty) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( "Aucune image — cliquez + pour en ajouter", style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey[600], fontSize: 13), ), ) else SizedBox( height: 250, child: ReorderableListView( scrollDirection: Axis.horizontal, onReorderItem: (oldIndex, newIndex) { setState(() { final item = workingStep.contents!.removeAt(oldIndex); workingStep.contents!.insert(newIndex, item); for (var i = 0; i < workingStep.contents!.length; i++) { workingStep.contents![i].order = i; } }); }, children: List.generate(workingStep.contents!.length, (i) => ListViewCardContent( workingStep.contents!, i, Key('content_$i'), appCtx, (updated) => setState(() => workingStep.contents = List.from(updated)), true, false, )), ), ), // Questions — uniquement en mode Escape Game if (isEscapeMode) ...[ Divider(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocalizations.of(context)!.questionsChallengesLabel, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15)), IconButton( icon: Icon(Icons.add_circle_outline, color: kSuccess), onPressed: () { showNewOrUpdateQuizQuestion( context, null, workingStep.id ?? "temp", isEscapeMode, (newQuestion) { setState(() { newQuestion.order = workingStep.quizQuestions?.length ?? 0; workingStep.quizQuestions = [ ...(workingStep.quizQuestions ?? []), newQuestion ]; questionsRevision++; }); }, ); }, ), ], ), SizedBox( width: halfWidth, child: SwitchListTile( dense: true, contentPadding: EdgeInsets.zero, title: Text("Timer"), value: workingStep.isStepTimer ?? false, onChanged: (val) => setState(() => workingStep.isStepTimer = val), activeThumbColor: kPrimaryColor, ), ), if (workingStep.isStepTimer == true) ...[ SizedBox(height: 8), Row( children: [ SizedBox( width: halfWidth, child: StringInputContainer( label: AppLocalizations.of(context)!.durationSecondsLabel, initialValue: workingStep.timerSeconds?.toString() ?? "", onChanged: (val) => setState(() => workingStep.timerSeconds = int.tryParse(val)), ), ), SizedBox(width: 20), SizedBox( width: halfWidth, child: MultiStringInputContainer( label: "Message d'expiration :", modalLabel: "Message d'expiration du timer", initialValue: workingStep.timerExpiredMessage ?? [], onGetResult: (val) => setState(() => workingStep.timerExpiredMessage = val.isEmpty ? null : val), maxLines: 2, isTitle: false, isHTML: true, ), ), ], ), SizedBox(height: 8), ], if (workingStep.quizQuestions == null || workingStep.quizQuestions!.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( AppLocalizations.of(context)!.noQuestionsConfigured, style: TextStyle( fontStyle: FontStyle.italic, color: Colors.grey[600]), ), ) else ReorderableCustomList( key: ValueKey(questionsRevision), items: workingStep.quizQuestions!, shrinkWrap: true, onChanged: (updatedList) { setState(() { for (var i = 0; i < updatedList.length; i++) { updatedList[i].order = i; } workingStep.quizQuestions = List.from(updatedList); }); }, itemBuilder: (context, qIndex, question) { return ListTile( dense: true, title: HtmlWidget( question.label.isNotEmpty ? question.label .firstWhere( (t) => t.language == 'FR', orElse: () => question.label[0]) .value ?? "Question $qIndex" : "Question $qIndex", textStyle: TextStyle(fontSize: 14), ), subtitle: Text( "Type: ${question.validationQuestionType?.value == 2 ? 'Puzzle' : question.validationQuestionType?.value == 1 ? 'QCM' : 'Texte'}"), ); }, actions: [ (context, qIndex, question) => IconButton( icon: Icon(Icons.edit, size: 18, color: kPrimaryColor), onPressed: () { showNewOrUpdateQuizQuestion( context, question, workingStep.id ?? "temp", isEscapeMode, (updatedQuestion) { setState(() { updatedQuestion.order = question.order; workingStep.quizQuestions![ qIndex] = updatedQuestion; questionsRevision++; }); }, ); }, ), (context, qIndex, question) => IconButton( icon: Icon(Icons.delete, size: 18, color: kError), onPressed: () { showConfirmationDialog( "Supprimer cette question ?", () {}, () => setState(() { workingStep.quizQuestions! .removeAt(qIndex); questionsRevision++; }), context, ); }, ), ], ), ], ], ), ), ), SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ SizedBox( height: 46, child: RoundedButton( text: "Annuler", press: () => Navigator.pop(context), color: kSecond, fontSize: 15, horizontal: 24, ), ), SizedBox(width: 12), SizedBox( height: 46, child: RoundedButton( text: isSaving ? "Sauvegarde..." : "Sauvegarder", icon: isSaving ? Icons.hourglass_empty : null, press: () async { if (isSaving) return; setState(() => isSaving = true); // Initialise les booleans null → false workingStep.isHiddenInitially ??= false; workingStep.isStepTimer ??= false; workingStep.isStepLocked ??= false; workingStep.isGeoTriggered ??= false; if (workingStep.isGeoTriggered != true) { workingStep.zoneRadiusMeters = null; } if (!isEscapeMode) { workingStep.isStepTimer = false; workingStep.timerSeconds = null; workingStep.timerExpiredMessage = null; } try { await onSave(workingStep); if (context.mounted) Navigator.pop(context); } catch (e) { setState(() => isSaving = false); } }, color: kPrimaryColor, fontSize: 15, horizontal: 24, ), ), ], ), ], ), ), ); }, ); }, ); }