383 lines
13 KiB
Dart
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();
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|