manager-app/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart

389 lines
18 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_app/Components/rounded_button.dart';
import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:manager_app/Components/resource_input_container.dart';
import 'package:manager_app/Components/number_input_container.dart';
import 'package:manager_app/Components/check_input_container.dart';
// Conversions between TranslationDTO and TranslationAndResourceDTO
// (ResponseDTO.label uses TranslationAndResourceDTO, but we use MultiStringInputContainer
// which requires TranslationDTO — the resource field is not needed for quiz responses)
List<TranslationDTO> _toTranslationList(
List<TranslationAndResourceDTO>? list) =>
(list ?? [])
.map((t) => TranslationDTO(language: t.language, value: t.value))
.toList();
List<TranslationAndResourceDTO> _fromTranslationList(
List<TranslationDTO> list) =>
list
.map((t) =>
TranslationAndResourceDTO(language: t.language, value: t.value))
.toList();
// Creates an empty ResponseDTO; labels are populated by MultiStringInputContainer
ResponseDTO _emptyResponse({bool isGood = false, int order = 0}) =>
ResponseDTO(label: [], isGood: isGood, order: order);
void showNewOrUpdateQuizQuestion(
BuildContext context,
QuizQuestion? question,
String stepId,
bool isEscapeMode,
Function(QuizQuestion) onSave,
) {
// Use JSON cloning for a robust deep copy
QuizQuestion? clonedQuestion = question != null
? QuizQuestion.fromJson(jsonDecode(jsonEncode(question)))
: null;
List<TranslationDTO> workingLabel = _toTranslationList(
clonedQuestion != null && clonedQuestion.label.isNotEmpty
? clonedQuestion.label
: [TranslationAndResourceDTO(language: 'FR', value: '')]);
List<ResponseDTO> workingResponses = clonedQuestion?.responses ?? [];
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,
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.65;
final double contentWidth = dialogWidth - 48;
final double halfWidth = (contentWidth - 20) / 2;
void ensureSimpleResponse() {
if (workingQuestion.responses.isEmpty) {
workingQuestion.responses
.add(_emptyResponse(isGood: true, order: 0));
}
workingQuestion.responses[0].isGood = true;
}
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(
question == null
? "Nouvelle Question"
: "Modifier la Question",
style: TextStyle(
color: kPrimaryColor,
fontSize: 20,
fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
Flexible(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- Intitulé (multi-langue via MultiStringInputContainer) ---
MultiStringInputContainer(
label: "Question posée :",
modalLabel: "Intitulé de la question",
initialValue: workingLabel,
onGetResult: (val) => setState(() {
workingLabel = val;
workingQuestion.label = _fromTranslationList(val);
}),
maxLines: 3,
isTitle: false,
isHTML: true,
),
SizedBox(height: 16),
// --- Type ---
Text("Type de validation :",
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
DropdownButton<QuestionType>(
value: workingQuestion.validationQuestionType,
items: [
DropdownMenuItem(
value: QuestionType.number0,
child: Text("Simple (texte attendu)")),
DropdownMenuItem(
value: QuestionType.number1,
child: Text("Choix multiples (QCM)")),
DropdownMenuItem(
value: QuestionType.number2,
child: Text("Puzzle")),
],
onChanged: (val) => setState(() {
workingQuestion.validationQuestionType = val;
workingQuestion.responses = [];
}),
),
// =========================================
// Type 0 : Simple texte
// =========================================
if (workingQuestion.validationQuestionType ==
QuestionType.number0) ...[
Divider(height: 24),
Text("Réponse attendue :",
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Builder(builder: (_) {
ensureSimpleResponse();
final List<TranslationDTO> respLabel =
_toTranslationList(
workingQuestion.responses[0].label);
return MultiStringInputContainer(
label: "",
modalLabel: "Réponse attendue",
initialValue: respLabel,
onGetResult: (val) => setState(() {
workingQuestion.responses[0].label =
_fromTranslationList(val);
workingQuestion.responses[0].isGood = true;
}),
maxLines: 1,
isTitle: true,
isHTML: true,
);
}),
SizedBox(height: 4),
Text(
"La validation se fait par comparaison (insensible à la casse).",
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
],
// =========================================
// Type 1 : QCM
// =========================================
if (workingQuestion.validationQuestionType ==
QuestionType.number1) ...[
Divider(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Réponses possibles :",
style:
TextStyle(fontWeight: FontWeight.bold)),
TextButton.icon(
icon: Icon(Icons.add_circle_outline,
color: kSuccess),
label: Text("Ajouter",
style: TextStyle(color: kSuccess)),
onPressed: () => setState(() =>
workingQuestion.responses.add(
_emptyResponse(
order: workingQuestion
.responses.length))),
),
],
),
if (workingQuestion.responses.isEmpty)
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: Text(
"Aucune réponse définie. Ajoutez-en au moins une.",
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
)
else
Column(
children: List.generate(
workingQuestion.responses.length, (i) {
final resp = workingQuestion.responses[i];
final List<TranslationDTO> respLabel =
_toTranslationList(resp.label);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
// Checkbox bonne réponse
Tooltip(
message: resp.isGood == true
? "Bonne réponse ✓"
: "Mauvaise réponse",
child: Checkbox(
value: resp.isGood ?? false,
activeColor: kSuccess,
onChanged: (val) => setState(
() => resp.isGood = val),
),
),
// Traductions (via MultiStringInputContainer)
Expanded(
child: MultiStringInputContainer(
label: "Réponse ${i + 1} :",
modalLabel: "Réponse ${i + 1}",
initialValue: respLabel,
onGetResult: (val) => setState(
() => resp.label =
_fromTranslationList(
val)),
maxLines: 1,
isTitle: true,
isHTML: true,
),
),
// Supprimer
IconButton(
icon: Icon(Icons.delete_outline,
color: kError, size: 20),
onPressed: () => setState(() =>
workingQuestion.responses
.removeAt(i)),
),
],
),
),
);
}),
),
SizedBox(height: 4),
Text(
"✓ = bonne réponse. Plusieurs peuvent être correctes.",
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
],
// =========================================
// Type 2 : Puzzle
// =========================================
if (workingQuestion.validationQuestionType ==
QuestionType.number2) ...[
Divider(height: 24),
Text("Configuration du Puzzle",
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 12),
ResourceInputContainer(
label: "Image du puzzle :",
initialValue: workingQuestion.puzzleImageId,
onChanged: (res) => setState(() {
workingQuestion.puzzleImageId = res.id;
workingQuestion.puzzleImage =
Resource.fromJson(res.toJson());
}),
),
SizedBox(height: 12),
Row(
children: [
SizedBox(
width: halfWidth,
child: NumberInputContainer(
label: "Lignes :",
initialValue:
workingQuestion.puzzleRows ?? 3,
onChanged: (val) => setState(() =>
workingQuestion.puzzleRows =
int.tryParse(val) ?? 3),
isSmall: true,
),
),
SizedBox(width: 20),
SizedBox(
width: halfWidth,
child: NumberInputContainer(
label: "Colonnes :",
initialValue:
workingQuestion.puzzleCols ?? 3,
onChanged: (val) => setState(() =>
workingQuestion.puzzleCols =
int.tryParse(val) ?? 3),
isSmall: true,
),
),
],
),
SizedBox(height: 8),
CheckInputContainer(
label: "Puzzle glissant (Sliding) :",
isChecked:
workingQuestion.isSlidingPuzzle ?? false,
onChanged: (val) => setState(
() => workingQuestion.isSlidingPuzzle = val),
),
],
],
),
),
),
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: "Sauvegarder",
press: () {
for (int i = 0;
i < workingQuestion.responses.length;
i++) {
workingQuestion.responses[i].order = i;
}
onSave(workingQuestion);
Navigator.pop(context);
},
color: kPrimaryColor,
fontSize: 15,
horizontal: 24,
),
),
],
),
],
),
),
);
},
);
},
);
}