383 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:manager_app/Components/collection_editor.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/multi_string_input_html_modal.dart';
import 'package:manager_app/Components/resource_input_container.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Quizz/quizz_answer_list.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_app/l10n/app_localizations.dart';
import 'package:manager_api_new/api.dart';
import 'dart:convert';
import 'package:provider/provider.dart';
/// Types de ressource acceptés par l'intitulé d'une question et par les
/// messages de score.
const _kQuizResourceTypes = <ResourceType>[
ResourceType.Image,
ResourceType.ImageUrl,
ResourceType.Video,
ResourceType.VideoUrl,
ResourceType.Audio,
];
class QuizzConfig extends StatefulWidget {
final String? color;
final String? label;
final QuizDTO initialValue;
final ValueChanged<QuizDTO> onChanged;
const QuizzConfig({
Key? key,
this.color,
this.label,
required this.initialValue,
required this.onChanged,
}) : super(key: key);
@override
_QuizzConfigState createState() => _QuizzConfigState();
}
class _QuizzConfigState extends State<QuizzConfig> {
late QuizDTO quizzDTO;
List<QuestionDTO> questions = [];
@override
void initState() {
super.initState();
quizzDTO = widget.initialValue;
WidgetsBinding.instance.addPostFrameCallback((_) => _loadFromApi());
}
SectionQuizApi _api() {
final appContext = Provider.of<AppContext>(context, listen: false);
return (appContext.getContext() as ManagerAppContext)
.clientAPI!
.sectionQuizApi!;
}
Future<void> _loadFromApi() async {
if (quizzDTO.id == null || !mounted) return;
try {
final fetched =
await _api().sectionQuizGetAllQuizQuestionFromSection(quizzDTO.id!);
if (fetched == null || !mounted) return;
fetched.sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0));
setState(() {
questions = List.from(fetched);
quizzDTO.questions = questions;
});
} catch (e) {
// La liste initiale reste affichée en cas d'échec.
}
}
/// Une question vit côté serveur : chaque champ validé part tout de suite.
Future<void> _saveQuestion(QuestionDTO question) async {
final l = AppLocalizations.of(context)!;
try {
await _api().sectionQuizUpdate(question);
} catch (e) {
showNotification(kError, kWhite, l.questionUpdateError, context, null);
}
}
QuestionDTO _emptyQuestion(List<String> languages) {
final question = QuestionDTO();
question.label = <TranslationAndResourceDTO>[];
question.responses = <ResponseDTO>[];
for (final language in languages) {
question.label!
.add(TranslationAndResourceDTO(language: language, value: ""));
}
return question;
}
String _questionLabel(AppLocalizations l, QuestionDTO question, int index) {
final labels = question.label ?? [];
if (labels.isEmpty) return l.untitledQuestion(index + 1);
final value = labels
.firstWhere((t) => t.language == 'FR', orElse: () => labels.first)
.value ??
"";
final plain = value.replaceAll(RegExp(r'<[^>]*>'), ' ').trim();
return plain.isEmpty ? l.untitledQuestion(index + 1) : plain;
}
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final appContext = Provider.of<AppContext>(context);
final languages = (appContext.getContext() as ManagerAppContext)
.selectedConfiguration!
.languages!;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildScoreMessages(l, appContext),
const SizedBox(height: kSpace6),
Row(
children: [
Expanded(child: Text(l.questionsLabel, style: kLabelField)),
Text(l.dragToReorder, style: kTextHint),
],
),
const SizedBox(height: kSpace2),
CollectionEditor<QuestionDTO>(
items: questions,
addLabel: l.questionLabel,
itemLabel: (question, index) => _questionLabel(l, question, index),
createItem: () => _emptyQuestion(languages),
setOrder: (question, order) => question.order = order,
onChanged: (_) {
quizzDTO.questions = questions;
widget.onChanged(quizzDTO);
},
remote: RemoteCollection<QuestionDTO>(
create: () async {
try {
final created = await _api()
.sectionQuizCreate(quizzDTO.id!, _emptyQuestion(languages));
if (created != null && mounted) {
showNotification(kSuccess, kWhite, l.questionCreatedSuccess,
context, null);
}
return created;
} catch (e) {
showNotification(
kError, kWhite, l.questionCreateError, context, null);
return null;
}
},
delete: (question) async {
try {
await _api().sectionQuizDelete(question.id!);
showNotification(
kSuccess, kWhite, l.questionDeletedSuccess, context, null);
return true;
} catch (e) {
showNotification(
kError, kWhite, l.questionDeleteError, context, null);
return false;
}
},
reorder: (ordered) async {
try {
await Future.wait(
ordered.map((question) => _api().sectionQuizUpdate(question)));
return true;
} catch (e) {
showNotification(kError, kWhite, l.questionOrderUpdateError,
context, null);
return false;
}
},
),
detailBuilder: (question, index) => _QuestionFields(
key: ValueKey(question.id ?? identityHashCode(question)),
question: question,
onChanged: () => _saveQuestion(question),
),
),
],
);
}
Widget _buildScoreMessages(AppLocalizations l, AppContext appContext) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(l.quizScoreMessagesLabel, style: kLabelField),
const SizedBox(height: kSpace2),
Wrap(
spacing: kSpace3,
runSpacing: kSpace3,
children: [
_scoreButton(l.quizBadScore, () => updateScoreQuizMessage(
context, appContext, quizzDTO.badLevel, l.quizBadScoreMsg, 0)),
_scoreButton(l.quizMediumScore, () => updateScoreQuizMessage(
context, appContext, quizzDTO.mediumLevel, l.quizMediumScoreMsg, 1)),
_scoreButton(l.quizGoodScore, () => updateScoreQuizMessage(
context, appContext, quizzDTO.goodLevel, l.quizGoodScoreMsg, 2)),
_scoreButton(l.quizExcellentScore, () => updateScoreQuizMessage(
context, appContext, quizzDTO.greatLevel, l.quizExcellentScoreMsg, 3)),
],
),
],
);
}
Widget _scoreButton(String label, VoidCallback onPressed) {
return OutlinedButton.icon(
onPressed: onPressed,
icon: const Icon(Icons.message_outlined, size: 15, color: kPrimaryColor),
label: Text(label,
style: const TextStyle(
fontSize: 13, fontWeight: FontWeight.w600, color: kPrimaryColor)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: kLine),
padding: const EdgeInsets.symmetric(
horizontal: kSpace4, vertical: kSpace3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(kRadiusPill)),
),
);
}
updateScoreQuizMessage(BuildContext context, AppContext appContext, List<TranslationAndResourceDTO>? inLevelDTO, String text, int levelToUpdate) {
List<TranslationAndResourceDTO> levelDTO = <TranslationAndResourceDTO>[];
if (inLevelDTO != null) {
levelDTO = inLevelDTO;
} else {
ManagerAppContext managerAppContext = appContext.getContext();
managerAppContext.selectedConfiguration!.languages!.forEach((element) {
var translationMessageDTO = new TranslationAndResourceDTO();
translationMessageDTO.language = element;
translationMessageDTO.value = "";
levelDTO.add(translationMessageDTO);
});
}
List<TranslationAndResourceDTO> newValues = <TranslationAndResourceDTO>[];
List<TranslationAndResourceDTO> initials = levelDTO;
appContext.getContext().selectedConfiguration!.languages!.forEach((value) {
if(initials.map((iv) => iv.language).contains(value)) {
newValues.add(TranslationAndResourceDTO.fromJson(jsonDecode(jsonEncode(initials.firstWhere((element) => element.language == value)))!)!);
} else {
newValues.add(TranslationAndResourceDTO(language: value, value: ""));
}
});
showMultiStringInputAndResourceHTML(text, text, true, initials, newValues, (value) {
if(value != null && value.isNotEmpty) {
levelDTO = value;
setState(() {
switch(levelToUpdate) {
case 0:
quizzDTO.badLevel = levelDTO;
break;
case 1:
quizzDTO.mediumLevel = levelDTO;
break;
case 2:
quizzDTO.goodLevel = levelDTO;
break;
case 3:
quizzDTO.greatLevel = levelDTO;
break;
}
widget.onChanged(quizzDTO);
});
}
}, 1, _kQuizResourceTypes, context);
}
}
/// Les champs d'une question, tels qu'ils étaient dans la modale — image de
/// fond, intitulé traduit, réponses — mais édités dans le panneau de droite.
///
/// Chaque champ enregistre quand il est validé : la question est un objet
/// serveur, pas une entrée du DTO de la section.
class _QuestionFields extends StatefulWidget {
const _QuestionFields({
Key? key,
required this.question,
required this.onChanged,
}) : super(key: key);
final QuestionDTO question;
final VoidCallback onChanged;
@override
State<_QuestionFields> createState() => _QuestionFieldsState();
}
class _QuestionFieldsState extends State<_QuestionFields> {
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context)!;
final question = widget.question;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
LayoutBuilder(
builder: (context, constraints) {
final image = ResourceInputContainer(
label: l.backgroundImageLabel,
initialValue: question.imageBackgroundResourceId,
isSmall: true,
onChanged: (ResourceDTO resource) {
setState(() {
if (resource.id == null) {
question.imageBackgroundResourceId = null;
question.imageBackgroundResourceUrl = null;
question.imageBackgroundResourceType = null;
} else {
question.imageBackgroundResourceId = resource.id;
question.imageBackgroundResourceUrl = resource.url;
question.imageBackgroundResourceType = resource.type;
}
});
widget.onChanged();
},
);
final label = MultiStringInputAndResourceContainer(
label: l.questionInputLabel,
modalLabel: l.questionLabel,
resourceTypes: _kQuizResourceTypes,
initialValue: question.label ?? [],
onGetResult: (value) {
setState(() => question.label = value);
widget.onChanged();
},
maxLines: 1,
isTitle: true,
);
if (constraints.maxWidth > 520) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
image,
const SizedBox(width: kSpace5),
Expanded(child: label),
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [image, const SizedBox(height: kSpace5), label],
);
},
),
const SizedBox(height: kSpace5),
Text(l.answersLabel, style: kLabelField),
const SizedBox(height: kSpace2),
Container(
decoration: BoxDecoration(
border: Border.all(color: kLine),
borderRadius: BorderRadius.circular(kRadiusCard),
),
clipBehavior: Clip.antiAlias,
child: QuizzResponseList(
responses: question.responses ?? [],
onChanged: (List<ResponseDTO> responsesOutput) {
question.responses = responsesOutput;
widget.onChanged();
},
),
),
],
);
}
}