Lot E : PDF en média d'étape, et QuestionType nommé

PDF en média d'étape — la liste de types du sélecteur de médias devient un
paramètre (`kSliderContentResourceTypes` par défaut) et l'étape de parcours
passe `kGuidedStepResourceTypes`, soit les types du slider plus Pdf.

QuestionType — le client généré nomme ses valeurs number0/1/2, ce qui
poussait le front à comparer des entiers bruts (`?.value == 2 ? 'Puzzle'`).
Alias nommés d'après l'enum serveur ajoutés à la main dans
question_type.dart (simple / multipleChoice / puzzle), `values` inchangé :
ce sont les mêmes trois valeurs. Les 7 usages number* du dialogue de
question sont migrés avec.

Fournisseur de carte (W1) — décision : visitapp-web reste sur Leaflet. Le
champ ne peut pas être masqué pour le web, il est porté par la section et
une même configuration est servie au mobile comme au web ; il est donc
conservé et documenté dans l'interface comme réglage mobile/tablette.

flutter build web , analyse du dossier sans erreur ni warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-11 15:31:07 +02:00
parent ac4927bdef
commit 5066b6c5e3
13 changed files with 97 additions and 14 deletions

View File

@ -497,6 +497,18 @@ class _AgendaConfigState extends State<AgendaConfig> {
}); });
}, },
), ),
if (agendaDTO.agendaMapProvider != null)
// Voir map_config.dart : réglage mobile/tablette, le web garde Leaflet.
Padding(
padding: const EdgeInsets.only(top: 4, bottom: 8),
child: Text(
AppLocalizations.of(context)!.mapProviderMobileOnlyNote,
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
),
if (agendaDTO.isOnlineAgenda == true) if (agendaDTO.isOnlineAgenda == true)
MultiStringInputContainer( MultiStringInputContainer(
label: AppLocalizations.of(context)!.jsonFilesLabel, label: AppLocalizations.of(context)!.jsonFilesLabel,

View File

@ -642,6 +642,21 @@ class _MapConfigState extends State<MapConfig> {
isSmall: true), isSmall: true),
], ],
), ),
const SizedBox(height: 8),
// Décision du 2026-08-11 (lot E, W1) : visitapp-web reste sur Leaflet.
// Le champ ne peut pas être masqué pour le web il est porté par la
// section, et une même configuration est servie au mobile comme au web.
// On le dit donc au client au lieu de le laisser croire le contraire.
Align(
alignment: Alignment.centerLeft,
child: Text(
AppLocalizations.of(context)!.mapProviderMobileOnlyNote,
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,

View File

@ -18,6 +18,15 @@ import 'package:manager_app/l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'showNewOrUpdateQuizQuestion.dart'; import 'showNewOrUpdateQuizQuestion.dart';
/// Médias acceptés sur une étape de parcours. Le PDF s'ajoute aux types du
/// slider : une étape peut porter un plan, une partition, un fac-similé.
/// Le rendu visiteur correspondant vit dans `CachedCustomResource`
/// (mymuseum-visitapp) et `ResourceViewer.tsx` (visitapp-web).
const kGuidedStepResourceTypes = <ResourceType>[
...kSliderContentResourceTypes,
ResourceType.Pdf,
];
void showNewOrUpdateGuidedStep( void showNewOrUpdateGuidedStep(
BuildContext context, BuildContext context,
GuidedStepDTO? step, GuidedStepDTO? step,
@ -248,7 +257,10 @@ void showNewOrUpdateGuidedStep(
IconButton( IconButton(
icon: Icon(Icons.add_circle_outline, color: kSuccess), icon: Icon(Icons.add_circle_outline, color: kSuccess),
onPressed: () async { onPressed: () async {
final result = await showNewOrUpdateContentSlider(null, appCtx, context, true, false); final result = await showNewOrUpdateContentSlider(
null, appCtx, context, true, false,
resourceTypes: kGuidedStepResourceTypes,
);
if (result != null) { if (result != null) {
setState(() { setState(() {
result.order = workingStep.contents!.length; result.order = workingStep.contents!.length;
@ -438,7 +450,7 @@ void showNewOrUpdateGuidedStep(
textStyle: TextStyle(fontSize: 14), textStyle: TextStyle(fontSize: 14),
), ),
subtitle: Text( subtitle: Text(
"Type: ${question.validationQuestionType?.value == 2 ? 'Puzzle' : question.validationQuestionType?.value == 1 ? 'QCM' : 'Texte'}"), "Type: ${question.validationQuestionType == QuestionType.puzzle ? 'Puzzle' : question.validationQuestionType == QuestionType.multipleChoice ? 'QCM' : 'Texte'}"),
); );
}, },
actions: [ actions: [

View File

@ -54,7 +54,7 @@ void showNewOrUpdateQuizQuestion(
id: 0, id: 0,
label: _fromTranslationList(workingLabel), label: _fromTranslationList(workingLabel),
responses: workingResponses, responses: workingResponses,
validationQuestionType: QuestionType.number0, validationQuestionType: QuestionType.simple,
order: question?.order ?? 0, order: question?.order ?? 0,
guidedStepId: question?.guidedStepId ?? stepId, guidedStepId: question?.guidedStepId ?? stepId,
); );
@ -163,13 +163,13 @@ void showNewOrUpdateQuizQuestion(
value: workingQuestion.validationQuestionType, value: workingQuestion.validationQuestionType,
items: [ items: [
DropdownMenuItem( DropdownMenuItem(
value: QuestionType.number0, value: QuestionType.simple,
child: Text("Simple (texte attendu)")), child: Text("Simple (texte attendu)")),
DropdownMenuItem( DropdownMenuItem(
value: QuestionType.number1, value: QuestionType.multipleChoice,
child: Text("Choix multiples (QCM)")), child: Text("Choix multiples (QCM)")),
DropdownMenuItem( DropdownMenuItem(
value: QuestionType.number2, value: QuestionType.puzzle,
child: Text("Puzzle")), child: Text("Puzzle")),
], ],
onChanged: (val) => setState(() { onChanged: (val) => setState(() {
@ -182,7 +182,7 @@ void showNewOrUpdateQuizQuestion(
// Type 0 : Simple texte // Type 0 : Simple texte
// ========================================= // =========================================
if (workingQuestion.validationQuestionType == if (workingQuestion.validationQuestionType ==
QuestionType.number0) ...[ QuestionType.simple) ...[
Divider(height: 24), Divider(height: 24),
Text(AppLocalizations.of(context)!.expectedAnswerLabel, Text(AppLocalizations.of(context)!.expectedAnswerLabel,
style: TextStyle(fontWeight: FontWeight.bold)), style: TextStyle(fontWeight: FontWeight.bold)),
@ -245,7 +245,7 @@ void showNewOrUpdateQuizQuestion(
// Type 1 : QCM // Type 1 : QCM
// ========================================= // =========================================
if (workingQuestion.validationQuestionType == if (workingQuestion.validationQuestionType ==
QuestionType.number1) ...[ QuestionType.multipleChoice) ...[
Divider(height: 24), Divider(height: 24),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -349,7 +349,7 @@ void showNewOrUpdateQuizQuestion(
// Type 2 : Puzzle // Type 2 : Puzzle
// ========================================= // =========================================
if (workingQuestion.validationQuestionType == if (workingQuestion.validationQuestionType ==
QuestionType.number2) ...[ QuestionType.puzzle) ...[
Divider(height: 24), Divider(height: 24),
Text("Configuration du Puzzle", Text("Configuration du Puzzle",
style: TextStyle(fontWeight: FontWeight.bold)), style: TextStyle(fontWeight: FontWeight.bold)),

View File

@ -8,7 +8,22 @@ import 'package:manager_app/constants.dart';
import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/l10n/app_localizations.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
Future<ContentDTO?> showNewOrUpdateContentSlider(ContentDTO? inputContentDTO, AppContext appContext, BuildContext context, bool showTitle, bool showDescription) async { /// Types de ressource proposés par défaut pour un contenu de slider.
const kSliderContentResourceTypes = <ResourceType>[
ResourceType.Image,
ResourceType.ImageUrl,
ResourceType.Video,
ResourceType.VideoUrl,
ResourceType.Audio,
];
Future<ContentDTO?> showNewOrUpdateContentSlider(
ContentDTO? inputContentDTO,
AppContext appContext,
BuildContext context,
bool showTitle,
bool showDescription,
{List<ResourceType> resourceTypes = kSliderContentResourceTypes}) async {
ContentDTO contentDTO = new ContentDTO(); ContentDTO contentDTO = new ContentDTO();
if (inputContentDTO != null) { if (inputContentDTO != null) {
@ -64,7 +79,7 @@ Future<ContentDTO?> showNewOrUpdateContentSlider(ContentDTO? inputContentDTO, Ap
initialValue: contentDTO.resourceId, initialValue: contentDTO.resourceId,
color: kPrimaryColor, color: kPrimaryColor,
fontSize: 20, fontSize: 20,
inResourceTypes: [ResourceType.Image, ResourceType.ImageUrl, ResourceType.Video, ResourceType.VideoUrl, ResourceType.Audio], inResourceTypes: resourceTypes,
onChanged: (ResourceDTO resource) { onChanged: (ResourceDTO resource) {
if(resource.id == null) { if(resource.id == null) {
contentDTO.resourceId = null; contentDTO.resourceId = null;

View File

@ -684,5 +684,6 @@
"annotationsLabel": "Annotations", "annotationsLabel": "Annotations",
"discardPathChangesConfirm": "This path has unsaved changes, along with its steps and their questions. Discard these changes?", "discardPathChangesConfirm": "This path has unsaved changes, along with its steps and their questions. Discard these changes?",
"discardStepChangesConfirm": "This step has unsaved changes, along with its questions. Discard these changes?", "discardStepChangesConfirm": "This step has unsaved changes, along with its questions. Discard these changes?",
"discardQuestionChangesConfirm": "This question has unsaved changes, along with its answers. Discard these changes?" "discardQuestionChangesConfirm": "This question has unsaved changes, along with its answers. Discard these changes?",
"mapProviderMobileOnlyNote": "The map provider and type apply to the mobile and tablet apps. The web app always renders its own map."
} }

View File

@ -684,5 +684,6 @@
"annotationsLabel": "Annotations", "annotationsLabel": "Annotations",
"discardPathChangesConfirm": "Ce parcours contient des modifications non enregistrées, ainsi que ses étapes et leurs questions. Abandonner ces modifications ?", "discardPathChangesConfirm": "Ce parcours contient des modifications non enregistrées, ainsi que ses étapes et leurs questions. Abandonner ces modifications ?",
"discardStepChangesConfirm": "Cette étape contient des modifications non enregistrées, ainsi que ses questions. Abandonner ces modifications ?", "discardStepChangesConfirm": "Cette étape contient des modifications non enregistrées, ainsi que ses questions. Abandonner ces modifications ?",
"discardQuestionChangesConfirm": "Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?" "discardQuestionChangesConfirm": "Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?",
"mapProviderMobileOnlyNote": "Le fournisseur et le type de carte s'appliquent aux applications mobile et tablette. L'application web affiche toujours sa propre carte."
} }

View File

@ -3315,6 +3315,12 @@ abstract class AppLocalizations {
/// In fr, this message translates to: /// In fr, this message translates to:
/// **'Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?'** /// **'Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?'**
String get discardQuestionChangesConfirm; String get discardQuestionChangesConfirm;
/// No description provided for @mapProviderMobileOnlyNote.
///
/// In fr, this message translates to:
/// **'Le fournisseur et le type de carte s\'appliquent aux applications mobile et tablette. L\'application web affiche toujours sa propre carte.'**
String get mapProviderMobileOnlyNote;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@ -1756,4 +1756,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get discardQuestionChangesConfirm => String get discardQuestionChangesConfirm =>
'This question has unsaved changes, along with its answers. Discard these changes?'; 'This question has unsaved changes, along with its answers. Discard these changes?';
@override
String get mapProviderMobileOnlyNote =>
'The map provider and type apply to the mobile and tablet apps. The web app always renders its own map.';
} }

View File

@ -1794,4 +1794,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get discardQuestionChangesConfirm => String get discardQuestionChangesConfirm =>
'Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?'; 'Cette question contient des modifications non enregistrées, ainsi que ses réponses. Abandonner ces modifications ?';
@override
String get mapProviderMobileOnlyNote =>
'Le fournisseur et le type de carte s\'appliquent aux applications mobile et tablette. L\'application web affiche toujours sa propre carte.';
} }

View File

@ -1773,4 +1773,8 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get discardQuestionChangesConfirm => String get discardQuestionChangesConfirm =>
'Deze vraag heeft niet-opgeslagen wijzigingen, samen met de antwoorden. Deze wijzigingen weggooien?'; 'Deze vraag heeft niet-opgeslagen wijzigingen, samen met de antwoorden. Deze wijzigingen weggooien?';
@override
String get mapProviderMobileOnlyNote =>
'De kaartprovider en het kaarttype gelden voor de mobiele en tablet-apps. De web-app toont altijd zijn eigen kaart.';
} }

View File

@ -684,5 +684,6 @@
"annotationsLabel": "Annotaties", "annotationsLabel": "Annotaties",
"discardPathChangesConfirm": "Deze route heeft niet-opgeslagen wijzigingen, samen met de stappen en hun vragen. Deze wijzigingen weggooien?", "discardPathChangesConfirm": "Deze route heeft niet-opgeslagen wijzigingen, samen met de stappen en hun vragen. Deze wijzigingen weggooien?",
"discardStepChangesConfirm": "Deze stap heeft niet-opgeslagen wijzigingen, samen met de vragen. Deze wijzigingen weggooien?", "discardStepChangesConfirm": "Deze stap heeft niet-opgeslagen wijzigingen, samen met de vragen. Deze wijzigingen weggooien?",
"discardQuestionChangesConfirm": "Deze vraag heeft niet-opgeslagen wijzigingen, samen met de antwoorden. Deze wijzigingen weggooien?" "discardQuestionChangesConfirm": "Deze vraag heeft niet-opgeslagen wijzigingen, samen met de antwoorden. Deze wijzigingen weggooien?",
"mapProviderMobileOnlyNote": "De kaartprovider en het kaarttype gelden voor de mobiele en tablet-apps. De web-app toont altijd zijn eigen kaart."
} }

View File

@ -27,6 +27,14 @@ class QuestionType {
static const number1 = QuestionType._(1); static const number1 = QuestionType._(1);
static const number2 = QuestionType._(2); static const number2 = QuestionType._(2);
// Ajouts manuels : le générateur nomme les valeurs number0/1/2, ce qui poussait
// le front à comparer des entiers bruts. Ces alias portent les noms de l'enum
// serveur (`QuizQuestion.QuestionType` : Simple, MultipleChoice, Puzzle).
// `values` n'est pas modifié — ce sont les mêmes trois valeurs, pas de nouvelles.
static const simple = number0;
static const multipleChoice = number1;
static const puzzle = number2;
/// List of all possible values in this [enum][QuestionType]. /// List of all possible values in this [enum][QuestionType].
static const values = <QuestionType>[ static const values = <QuestionType>[
number0, number0,