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/constants.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/Components/rounded_button.dart'; import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/check_input_container.dart'; import 'package:manager_app/Components/number_stepper_field.dart'; import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/reorderable_custom_list.dart'; import 'package:manager_app/Components/section_card.dart'; import 'showNewOrUpdateGuidedStep.dart'; void showNewOrUpdateGuidedPath( BuildContext context, GuidedPathDTO? path, String parentId, bool isEvent, FutureOr Function(GuidedPathDTO) onSave, ) { GuidedPathDTO workingPath = path != null ? GuidedPathDTO.fromJson(jsonDecode(jsonEncode(path)))! : GuidedPathDTO( title: [], description: [], steps: [], order: 0, ); bool isSaving = false; int stepsRevision = 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.82; // contentWidth = dialogWidth minus the 24px padding on each side final double contentWidth = dialogWidth - 48; final double halfWidth = (contentWidth - 20) / 2; return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( width: dialogWidth, constraints: BoxConstraints(maxHeight: screenHeight * 0.88), padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // -- Titre du dialog -- Text( path == null ? "Nouveau Parcours" : "Modifier le Parcours", style: TextStyle( color: kPrimaryColor, fontSize: 20, fontWeight: FontWeight.bold), ), SizedBox(height: 16), // -- Corps scrollable -- Flexible( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image du parcours ResourceInputContainer( label: "Image :", color: kPrimaryColor, initialValue: workingPath.imageResourceId, onChanged: (resourceDTO) => setState( () => workingPath.imageResourceId = resourceDTO.id), ), SizedBox(height: 16), // Titre + Description côte à côte Row( children: [ SizedBox( width: halfWidth, child: MultiStringInputContainer( label: "Titre :", modalLabel: "Titre du parcours", initialValue: workingPath.title ?? [], onGetResult: (val) => setState(() => workingPath.title = val), maxLines: 1, isTitle: true, isHTML: true, showPreview: true, ), ), SizedBox(width: 20), SizedBox( width: halfWidth, child: MultiStringInputContainer( label: "Description :", modalLabel: "Description du parcours", initialValue: workingPath.description ?? [], onGetResult: (val) => setState( () => workingPath.description = val), maxLines: 1, isTitle: false, isHTML: true, showPreview: true, ), ), ], ), SizedBox(height: 16), // Options SectionCard( icon: Icons.settings, title: "Options du parcours", child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CheckInputContainer( label: AppLocalizations.of(context)!.linearLabel, subtitle: "Les étapes doivent être suivies dans l'ordre.", isChecked: workingPath.isLinear ?? false, onChanged: (val) => setState( () => workingPath.isLinear = val), ), CheckInputContainer( label: AppLocalizations.of(context)!.requiredSuccessLabel, subtitle: "Le quiz de chaque étape doit être validé pour débloquer la suivante.", isChecked: workingPath.requireSuccessToAdvance ?? false, onChanged: (val) => setState(() => workingPath .requireSuccessToAdvance = val), ), CheckInputContainer( label: "Cacher les suivantes :", subtitle: "Seule l'étape en cours est visible sur la carte.", isChecked: workingPath.hideNextStepsUntilComplete ?? false, onChanged: (val) => setState(() => workingPath .hideNextStepsUntilComplete = val), ), SizedBox(height: 8), NumberStepperField( label: "Durée estimée", value: workingPath.estimatedDurationMinutes, min: 0, max: 600, unit: "min", onChanged: (val) => setState(() => workingPath.estimatedDurationMinutes = val.toInt()), ), ], ), ), SizedBox(height: 16), // Mode jeu SectionCard( icon: Icons.sports_esports, title: "Mode jeu", subtitle: "Escape game ou chasse au trésor", child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CheckInputContainer( label: "Mode jeu (escape game / chasse au trésor)", subtitle: "Active les messages de jeu ci-dessous et le vocabulaire escape game sur les étapes.", isChecked: workingPath.isGameMode ?? false, onChanged: (val) => setState(() => workingPath.isGameMode = val), ), if (workingPath.isGameMode == true) ...[ SizedBox(height: 8), Row( children: [ Expanded( child: MultiStringInputContainer( label: "Message de début :", modalLabel: "Message d'introduction du jeu", color: kPrimaryColor, initialValue: (workingPath.gameMessageDebut ?? []) .map((t) => TranslationDTO(language: t.language, value: t.value)) .toList(), onGetResult: (val) { setState(() { workingPath.gameMessageDebut = val.map((t) { final prev = (workingPath.gameMessageDebut ?? []).firstWhere( (e) => e.language == t.language, orElse: () => TranslationAndResourceDTO(), ); return TranslationAndResourceDTO( language: t.language, value: t.value, resourceId: prev.resourceId, resource: prev.resource, ); }).toList(); }); }, maxLines: 2, isTitle: false, isHTML: true, showPreview: true, ), ), SizedBox(width: 20), Expanded( child: MultiStringInputContainer( label: "Message de fin :", modalLabel: "Message de félicitations du jeu", color: kPrimaryColor, initialValue: (workingPath.gameMessageFin ?? []) .map((t) => TranslationDTO(language: t.language, value: t.value)) .toList(), onGetResult: (val) { setState(() { workingPath.gameMessageFin = val.map((t) { final prev = (workingPath.gameMessageFin ?? []).firstWhere( (e) => e.language == t.language, orElse: () => TranslationAndResourceDTO(), ); return TranslationAndResourceDTO( language: t.language, value: t.value, resourceId: prev.resourceId, resource: prev.resource, ); }).toList(); }); }, maxLines: 2, isTitle: false, isHTML: true, showPreview: true, ), ), ], ), ], ], ), ), SizedBox(height: 16), // Étapes SectionCard( icon: Icons.list_alt, title: AppLocalizations.of(context)!.pathStepsLabel, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Align( alignment: Alignment.centerRight, child: IconButton( icon: Icon(Icons.add_circle_outline, color: kSuccess), onPressed: () { showNewOrUpdateGuidedStep( context, null, workingPath.id ?? "temp", workingPath.isGameMode ?? false, (newStep) async { setState(() { newStep.order = workingPath.steps?.length ?? 0; workingPath.steps = [ ...(workingPath.steps ?? []), newStep ]; stepsRevision++; }); }, ); }, ), ), if (workingPath.steps?.isEmpty ?? true) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( AppLocalizations.of(context)!.noStepConfigured, style: TextStyle( fontStyle: FontStyle.italic, color: Colors.grey[600]), ), ) else ReorderableCustomList( key: ValueKey(stepsRevision), items: workingPath.steps!, shrinkWrap: true, onChanged: (updatedList) { setState(() { for (var i = 0; i < updatedList.length; i++) { updatedList[i].order = i; } workingPath.steps = List.from(updatedList); }); }, itemBuilder: (context, index, step) { return ListTile( leading: CircleAvatar(child: Text("${index + 1}")), title: HtmlWidget( step.title != null && step.title!.isNotEmpty ? step.title! .firstWhere( (t) => t.language == 'FR', orElse: () => step.title![0]) .value ?? "${AppLocalizations.of(context)!.stepFallback} $index" : "${AppLocalizations.of(context)!.stepFallback} $index", ), subtitle: workingPath.isGameMode ?? false ? Text( "${step.quizQuestions?.length ?? 0} question(s)") : null, ); }, actions: [ (context, index, step) => IconButton( icon: Icon(Icons.edit, color: kPrimaryColor), onPressed: () { showNewOrUpdateGuidedStep( context, step, workingPath.id ?? "temp", workingPath.isGameMode ?? false, (updatedStep) async { setState(() { updatedStep.order = step.order; workingPath.steps![index] = updatedStep; stepsRevision++; }); }, ); }, ), (context, index, step) => IconButton( icon: Icon(Icons.delete, color: kError), onPressed: () { setState(() { workingPath.steps!.removeAt(index); stepsRevision++; }); }, ), ], ), ], ), ), ], ), ), ), SizedBox(height: 16), // -- Boutons -- 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 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; } try { await onSave(workingPath); if (context.mounted) Navigator.pop(context); } catch (e) { setState(() => isSaving = false); } }, color: kPrimaryColor, fontSize: 15, horizontal: 24, ), ), ], ), ], ), ), ); }, ); }, ); }