# SectionForm — formulaires visiteurs > Chantier **V2**, reporté le 2026-08-07, **analysé dans le code le 2026-09-15**. > Spec d'origine : `todo-features.md` § SectionForm · carte kanban `cards/5-planifie/240-…` > Ce document ne rouvre pas la décision « V2 » : il dit **comment** on le fait quand on le fait. --- ## Pourquoi ce type de section Tout le flux produit va aujourd'hui du lieu vers le visiteur. Ce qui remonte est **dérivé** : `VisitEvent` (télémétrie anonyme) et `VisitorQuestion` (ce qui a été demandé au guide IA). Aucun mécanisme ne permet à un lieu de **poser une question**. Use cases visés, dans l'ordre de valeur commerciale : enquête de satisfaction (les musées publics en produisent pour leurs rapports de subsides), livre d'or, sondage d'exposition, inscription à un atelier. --- ## Décisions actées | Décision | Conséquence | |---|---| | **Réponses anonymes**, sans compte ni identification | Pas de champ « nom », pas de liaison à une personne. Ça allège massivement le volet RGPD — on reste sur le régime déjà décrit au §8 des CGU | | **Les trois fronts visiteur**, kiosk compris | `mymuseum-visitapp`, `visitapp-web`, **`tablet-app`** — une borne en fin de parcours est le meilleur emplacement pour une enquête de satisfaction | | **Table de réponses dédiée**, pas `VisitEvent.Metadata` | Rétention propre, export, suppression ciblée, requêtes par champ | | **Les réponses ne sont jamais indexées** pour le RAG | Seuls les libellés des questions le sont | | Pas d'upload de fichier en V1 | Les règles Firebase Storage de prod sont encore ouvertes en écriture — mauvais moment pour ajouter une porte d'upload public | --- ## Ce qui sert de socle (déjà écrit) La moitié de la plomberie existe. Trois précédents à reprendre plutôt qu'à réinventer : 1. **`StatsController.TrackEvent`** (`Controllers/StatsController.cs`) — le **seul** endpoint `[AllowAnonymous]` où un visiteur écrit en base. C'est exactement le contrat de soumission d'un formulaire : modèle à copier, y compris la validation défensive. 2. **`SectionQuiz` + `QuizQuestion`** (`Data/SubSection/`) — structurellement un formulaire : liste ordonnée de questions traduites en `jsonb`, options en `jsonb`, un `QuestionType`. `SectionForm` c'est le même squelette **moins** la bonne réponse, **plus** le stockage. 3. **`VisitorQuestionPurgeService`** (`Services/`) — le job de purge par rétention existe et tourne : `FormSubmission` s'y branche sans rien inventer. Et le coût d'ajout d'un type de section est balisé : `Scene3D`, le dernier ajouté, a touché 11 fichiers backend et 8 dans `manager-app`. La partie réellement neuve ici, c'est **l'écran de réponses**, qui n'a aucun équivalent. --- ## Modèle de données ``` DTOs/SectionType.cs Form ← ajouté EN FIN d'enum (persisté en int : cf. le commentaire de Scene3D) Data/SubSection/SectionForm.cs : Section List Fields List IntroText jsonb List ConfirmationText jsonb DateTime? ClosingDate int RetentionDays Data/SubSection/FormField.cs Label jsonb traduit (même forme que QuizQuestion.Label) Order, FieldType, IsRequired Options jsonb (pour single_choice / multiple_choice) Data/FormSubmission.cs Id, InstanceId, SectionId, ConfigurationId SessionId, Language, Answers jsonb, Timestamp [Index(InstanceId)] [Index(SectionId)] ``` `FormSubmission` vit à la racine de `Data/`, avec `VisitEvent` et `VisitorQuestion` — c'est de la donnée de visite, pas du contenu éditorial. ### Types de champs — V1 volontairement courte `text` (court), `long_text`, `single_choice`, `multiple_choice`, `rating` (1-5). Pas d'email, pas de date, pas d'upload. Les quatre premiers couvrent l'enquête de satisfaction et le sondage, qui sont les deux vrais cas. --- ## Backend - `DTOs/SubSection/FormDTO.cs` + branchement dans **`Services/SectionFactory.cs`** (le `switch` de désérialisation **et** le `switch` de construction). - `Controllers/SectionFormController.cs`, sur le modèle de `SectionScene3DController` : - CRUD admin, authentifié ; - `POST /api/SectionForm/submit` en **`[AllowAnonymous]`** ; - `GET /api/SectionForm/{id}/submissions` (admin) — liste + agrégats ; - `GET /api/SectionForm/{id}/export` — CSV ; - `DELETE /api/SectionForm/{id}/submissions` — purge manuelle. - `GetEmbeddableText` : **les libellés de questions oui, les réponses jamais.** Le raisonnement est déjà écrit dans `SectionQuiz` (« le guide les réciterait au premier visiteur qui demande ») ; ici ce serait pire — le guide recracherait les avis d'autres visiteurs, y compris désobligeants, à un visiteur suivant. - **L'endpoint de soumission est ouvert** : rate-limit par `SessionId`/IP (la politique `UseRateLimiter()` existe déjà depuis le chantier API Keys), respect de `ClosingDate`, et cap de soumissions par session. --- ## manager-app Fichiers à toucher, identifiés sur le précédent `Scene3D` : | Fichier | Nature | |---|---| | `lib/client.dart` + `manager_api_new/` | client généré, **édité à la main** | | `lib/Screens/Configurations/new_section_popup.dart` | entrée « Formulaire » | | `lib/Components/fetch_section_icon.dart` | icône du type | | `lib/Screens/Configurations/Section/section_detail_screen.dart` | routage vers l'éditeur | | `lib/Screens/Configurations/Section/SubSection/Form/form_config.dart` | **neuf** — constructeur de formulaire | | `lib/Screens/Configurations/Section/SubSection/Form/form_submissions.dart` | **neuf** — onglet réponses | | `lib/l10n/app_{fr,en,nl}.arb` | i18n obligatoire, aucun littéral dans un widget | **Où vivent les réponses** : dans un **onglet de l'écran de la section**, pas dans `Screens/Statistics/`. L'admin qui ouvre son formulaire veut ses réponses sous les yeux ; `Statistics` répond à une autre question (la fréquentation). Contenu de l'onglet : en-tête d'agrégats (nombre de réponses, moyenne des `rating`, répartition des choix en barres horizontales — la charte de `stats-screen-plan.md` s'applique), puis la liste des textes libres, puis l'export CSV. --- ## Fronts visiteur | Repo | Fichier | Note | |---|---|---| | `mymuseum-visitapp` | `lib/Screens/Sections/Form/form_page.dart` | à côté de `Quiz/quizz_page.dart` | | `visitapp-web` | `src/components/sections/FormSection.tsx` | **obligatoire** — le plan Essentiel est web-only. Lire le Flutter d'abord, c'est la référence de comportement | | `tablet-app` | `lib/Screens/Form/form_view.dart` | à côté de `Screens/Quizz/quizz_view.dart` | ### Le point d'attention propre au kiosk Sur une borne fixe, le visiteur suivant hérite de l'écran du précédent. Même anonyme, une saisie libre à moitié écrite qui reste affichée est un défaut visible. Il faut donc, sur `tablet-app` uniquement : **reset du formulaire après soumission** et **reset sur inactivité**, sur le même timer que celui qui ramène déjà la borne à l'accueil. --- ## RGPD — allégé, pas nul L'anonymat retire le gros du sujet : pas de personne identifiée, donc pas de droit d'accès à exercer par visiteur. Restent deux points, tous deux déjà traités ailleurs dans le produit : - **La saisie libre peut contenir des données personnelles spontanées.** C'est exactement ce que le §8 des CGU écrit déjà pour le champ de question du guide IA — la même clause couvre ce cas, il suffit de l'étendre au formulaire lors de la relecture juridique déjà planifiée (carte `195-faire-valider-les-cgu-par-un-juriste`). - **Rétention** : `RetentionDays` paramétrable + purge automatique via le service existant. --- ## Hors périmètre V1 du chantier Logique conditionnelle entre champs, pages multiples, notification email à chaque réponse, upload de fichier, réponses liées à un visiteur identifié. Le risque de ce chantier est de glisser vers « on refait Google Forms » — or la valeur ici n'est pas la richesse de l'éditeur, c'est d'être **déclenchable dans le parcours de visite** (fin d'un parcours guidé, beacon de sortie, borne de fin). --- ## Positionnement commercial À réserver aux plans **Pro et supérieurs** : la collecte de retours a une valeur propre, et le plan Essentiel est déjà le plan « vitrine ». À arbitrer avec la grille de `myinfomate-landing`, qui reste la source de vérité des prix.