Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VxSQeGQYUvPmSEoGdnidA
178 lines
8.4 KiB
Markdown
178 lines
8.4 KiB
Markdown
# 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<FormField> Fields
|
|
List<TranslationAndResourceDTO> IntroText jsonb
|
|
List<TranslationAndResourceDTO> 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.
|