Thomas Fransolet a54952e5f4 MISC
2026-07-10 16:54:26 +02:00

410 lines
20 KiB
Dart

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/string_input_container.dart';
import 'package:manager_app/Components/resource_input_container.dart';
import 'package:manager_app/Components/reorderable_custom_list.dart';
import 'showNewOrUpdateGuidedStep.dart';
void showNewOrUpdateGuidedPath(
BuildContext context,
GuidedPathDTO? path,
String parentId,
bool isEvent,
FutureOr<void> 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;
final double thirdWidth = (contentWidth - 40) / 3;
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,
),
),
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,
),
),
],
),
Divider(height: 24),
// Options
Row(
children: [
SizedBox(
width: thirdWidth,
child: CheckInputContainer(
label: AppLocalizations.of(context)!.linearLabel,
isChecked: workingPath.isLinear ?? false,
onChanged: (val) => setState(
() => workingPath.isLinear = val),
),
),
SizedBox(width: 20),
SizedBox(
width: thirdWidth,
child: CheckInputContainer(
label: AppLocalizations.of(context)!.requiredSuccessLabel,
isChecked:
workingPath.requireSuccessToAdvance ??
false,
onChanged: (val) => setState(() => workingPath
.requireSuccessToAdvance = val),
),
),
SizedBox(width: 20),
SizedBox(
width: thirdWidth,
child: CheckInputContainer(
label: "Cacher les suivantes :",
isChecked:
workingPath.hideNextStepsUntilComplete ??
false,
onChanged: (val) => setState(() => workingPath
.hideNextStepsUntilComplete = val),
),
),
],
),
SizedBox(height: 8),
SizedBox(
width: thirdWidth,
child: StringInputContainer(
label: "Durée estimée (min) :",
initialValue: workingPath.estimatedDurationMinutes?.toString() ?? "",
onChanged: (val) => setState(() =>
workingPath.estimatedDurationMinutes = int.tryParse(val)),
),
),
Divider(height: 24),
// Mode jeu
CheckInputContainer(
label: "Mode jeu (escape game / chasse au trésor)",
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,
),
),
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,
),
),
],
),
],
Divider(height: 24),
// Étapes
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(AppLocalizations.of(context)!.pathStepsLabel,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15)),
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<GuidedStepDTO>(
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,
),
),
],
),
],
),
),
);
},
);
},
);
}