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.Image, ResourceType.ImageUrl, ResourceType.Video, ResourceType.VideoUrl, ResourceType.Audio, ]; class QuizzConfig extends StatefulWidget { final String? color; final String? label; final QuizDTO initialValue; final ValueChanged 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 { late QuizDTO quizzDTO; List questions = []; @override void initState() { super.initState(); quizzDTO = widget.initialValue; WidgetsBinding.instance.addPostFrameCallback((_) => _loadFromApi()); } SectionQuizApi _api() { final appContext = Provider.of(context, listen: false); return (appContext.getContext() as ManagerAppContext) .clientAPI! .sectionQuizApi!; } Future _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 _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 languages) { final question = QuestionDTO(); question.label = []; question.responses = []; 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(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( 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( 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? inLevelDTO, String text, int levelToUpdate) { List levelDTO = []; 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 newValues = []; List 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 responsesOutput) { question.responses = responsesOutput; widget.onChanged(); }, ), ), ], ); } }