visitapp-web/guided-path-implementation.md
2026-05-04 14:23:50 +02:00

256 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GuidedPath — Implémentation visitapp-web
Suivi d'implémentation du parcours guidé pour `visitapp-web`. Chaque phase peut être interrompue/reprise indépendamment.
---
## ✅ À tester (checklist complète)
### Pré-requis
- [ ] Avoir au moins une `SectionMap` avec quelques GeoPoints + au moins un parcours (idéalement plusieurs étapes)
- [ ] Avoir au moins un `SectionGame` de type `Escape` avec un parcours
- [ ] Tester depuis un device avec GPS (mobile ou navigateur desktop avec localisation activée)
- [ ] Tester en HTTPS (la Geolocation API est bloquée en HTTP sauf sur `localhost`)
### Mode Carte (SectionMap)
**Affichage de base**
- [ ] Sur une SectionMap **sans** parcours : aucun bouton "Parcours" visible
- [ ] Sur une SectionMap **avec** parcours : bouton **"Parcours (N)"** dans la top bar
- [ ] Tap sur le bouton → bottom sheet liste les parcours avec titre + nb étapes + badge "linéaire" si applicable
**Sélection d'un parcours**
- [ ] Tap sur un parcours dans la liste → la carte se recadre (flyToBounds) sur les étapes
- [ ] Polyline pointillée colorée relie les étapes dans l'ordre
- [ ] Markers étapes numérotés (1, 2, 3…) visibles
- [ ] POI passent en opacity réduite (0.45)
- [ ] Bouton change en **"Étape 1/N"**
- [ ] Bouton **"Quitter le parcours"** apparaît en bottom-left
**États visuels des markers étapes**
- [ ] Étape **courante** : couleur primaire avec animation pulse
- [ ] Étape **complétée** : vert avec ✓
- [ ] Étape **future** : gris clair
- [ ] Étape avec `isStepLocked` : gris avec 🔒
- [ ] Tap sur un marker step → bottom sheet de détail s'ouvre
**Marqueur utilisateur GPS**
- [ ] Premier accès au parcours → demande de permission GPS du navigateur
- [ ] Permission accordée → point bleu pulsant apparaît à votre position
- [ ] Le point se déplace en live si vous bougez
**StepDetail (bottom sheet d'une étape)**
- [ ] Header avec image de l'étape + badge "Étape N/M"
- [ ] Titre + description (HTML rendu)
- [ ] Si `zoneRadiusMeters > 0` : badge "À X m de la zone" qui passe vert "Vous êtes dans la zone" quand vous approchez
- [ ] Si GPS refusé : bouton "Localisation refusée — réessayer" (clic → re-demande la permission)
- [ ] Boutons Précédent/Suivant **uniquement** en mode non-linéaire
**Validation et progression**
- [ ] Bouton **"Marquer terminée"** sur l'étape courante
- [ ] Si `requireSuccessToAdvance` + zone non atteinte → bouton désactivé
- [ ] Validation → étape passe en complétée + ouverture auto de la suivante
- [ ] Dernière étape validée → modal **"Bravo !"** avec "Recommencer" / "Retour à la carte"
- [ ] Étape déjà complétée → badge "✓ Étape terminée" affiché à la place du bouton
**Quiz par étape**
- [ ] Si `step.quizQuestions` non vide → bloc "Défi" avec QCM
- [ ] Sélection des réponses → bouton "Valider mes réponses" s'active quand toutes répondues
- [ ] Validation → feedback vert (correct) / rouge (mauvaise sélection) sur les choix
- [ ] Si tout correct (100%) → badge "✓ Défi réussi"
- [ ] Si pas 100% → bouton "Réessayer" (réinitialise le quiz)
- [ ] Si `requireSuccessToAdvance` et quiz non passé → bouton "Marquer terminée" désactivé
**Timer par étape**
- [ ] Si `step.isStepTimer && step.timerSeconds > 0` → barre décompte affichée
- [ ] Couleur barre : vert > 50%, orange 25-50%, rouge < 25%
- [ ] Format temps : MM:SS
- [ ] À expiration message `timerExpiredMessage` affiché en rouge (si défini)
**Toast d'entrée en zone**
- [ ] Première fois que vous entrez dans la zone de l'étape courante toast vert "Zone atteinte : « titre »"
- [ ] Le toast disparaît après ~3.5s
- [ ] Si vous quittez puis revenez dans la zone : pas de re-déclenchement (1× par étape)
**Modes de parcours**
- [ ] `isLinear = true` : pas de boutons Précédent/Suivant, progression strictement par validation
- [ ] `isLinear = false` : navigation libre Précédent/Suivant entre étapes
- [ ] `hideNextStepsUntilComplete = true` : seules les étapes complétées + courante visibles sur la carte
- [ ] `isHiddenInitially` sur une étape : invisible tant qu'elle n'est pas devenue courante ou complétée
**Lock**
- [ ] `isStepLocked` + `requireSuccessToAdvance` message "🔒 Étape verrouillée validez la condition"
- [ ] Validation (quiz passé OU dans la zone) message "🔒 Étape déverrouillée"
### Mode Game Escape (SectionGame type Escape)
- [ ] Sur un Game type Escape **sans** parcours : stub "Parcours guidé bientôt disponible"
- [ ] Avec parcours : `EscapeProgression` rendu (vue verticale, pas de carte)
- [ ] Si plusieurs parcours : sélecteur initial avec liste de cartes
- [ ] Si un seul parcours : auto-sélectionné
- [ ] Compteur "N/M étapes" affiché en haut + bouton "Changer de parcours" (si plusieurs)
- [ ] Étapes empilées verticalement (carte par étape)
- [ ] Étape courante : bordure couleur primaire, étapes complétées vertes, futures grisées (opacity 0.6)
- [ ] Quiz / Timer / Toast / GPS retry / lock fonctionnent identiquement au mode carte
- [ ] Modal de fin avec "Recommencer" / "Retour"
### Bugs connus à valider
- [ ] **Endpoint Game Escape** : on utilise `/api/SectionMap/{id}/GuidedPath` pour les SectionGame aussi (hypothèse polymorphe). Si ça renvoie 404, le backend doit exposer `/api/SectionGame/{id}/GuidedPath` ou inclure `guidedPaths` dans le DTO `/api/Section/configuration/{id}/detail`
---
## Contexte
- **Pas un type de Section standalone** : GuidedPath est une entité attachée à `SectionMap`, `SectionGame` (type Escape), ou `SectionEvent`.
- **Endpoint backend** : `GET /api/SectionMap/{sectionMapId}/GuidedPath` retourne les parcours avec `steps + quizQuestions + triggerGeoPoint` eager-loadés.
- **Référence Flutter** :
- `mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_path_map_progression_page.dart` (mode carte, ~770 lignes)
- `mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_path_content_progression_page.dart` (mode contenu, ~450 lignes)
- `mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_path_list_sheet.dart` (sélecteur)
- `mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_step_timer.dart` (widget timer)
---
## Phase 1 — Découverte et affichage statique sur la carte
**Objectif** : permettre au visiteur de voir qu'il y a des parcours, choisir un parcours, et voir ses étapes tracées sur la carte (sans GPS, sans quiz, sans validation).
### Fichiers touchés
- `src/lib/api/types.ts` : ajout `GuidedPathDTO`, `GuidedStepDTO`, champ `guidedPaths?` sur `MapDTO`
- `src/lib/api/client.ts` : nouvelle fonction `getGuidedPaths(sectionMapId, apiKey)`
- `src/components/sections/MapSection.tsx` :
- fetch des parcours au mount (si `map.points` existe)
- bouton flottant "Parcours" en haut-droite (si parcours dispo)
- bottom sheet listant les parcours (titre + nb étapes)
- état `selectedPathId` qui filtre l'affichage carte
- `src/components/sections/map/LeafletMap.tsx` :
- prop optionnelle `pathSteps?: { lat, lng, order, title }[]`
- rendu polyline reliant les étapes
- markers étapes numérotés (style différent des points POI)
### États UX à couvrir
- Aucun parcours dispo bouton caché
- Parcours dispo, aucun sélectionné bouton "Parcours (N)" + ouverture liste au tap
- Parcours sélectionné polyline + steps numérotés sur la carte + bouton "Quitter le parcours"
- Tap sur step bottom sheet de l'étape (titre, image, description, badge ordre)
### Hors scope Phase 1
- GPS tracking
- Quiz par étape
- Timer
- Lock / progression réelle
- Page de progression dédiée (full-screen)
### État
- [x] Types (`GuidedPathDTO`, `GuidedStepDTO`, champ `guidedPaths` sur `MapDTO`)
- [x] API client `getGuidedPaths(sectionMapId)`
- [x] Pré-fetch server-side dans `app/[slug]/[configId]/sections/[sectionId]/page.tsx` (les paths sont injectés dans `section.map.guidedPaths` avant le render client)
- [x] Bouton "Parcours" dans la top bar de `MapSection` (visible si paths > 0, change en "Étape N/M" quand parcours actif)
- [x] Bottom sheet `PathListSheet` listant les parcours (titre + nb étapes + flag linéaire)
- [x] Polyline pointillée + step markers numérotés stylés (CSS pulse sur sélection)
- [x] Bottom sheet `StepDetail` (image header + titre + description + badge zone + nav précédent/suivant)
- [x] Bouton "Quitter le parcours" en bottom-left quand un parcours est actif
- [x] Auto-fit des bounds de la carte sur les étapes du parcours sélectionné
- [x] POI dim (opacity 0.45) quand un parcours est actif pour mettre les étapes en avant
---
## Phase 2 — Progression complète (en cours)
**Objectif** : reproduire `GuidedPathMapProgressionPage` du Flutter.
### Fait
- [x] `src/lib/geo.ts` : `haversineMeters()` + `formatDistance()`
- [x] `src/hooks/useGeolocation.ts` : hook `watchPosition` avec états `idle | requesting | granted | denied | unavailable`
- [x] Hook activé seulement quand un parcours est actif (économie de batterie)
- [x] StepDetail affiche en live :
- Distance jusqu'à la zone (formatée m/km)
- Badge vert "Vous êtes dans la zone" si `distance <= zoneRadiusMeters`
- États dégradés : "Localisation en cours…", "Localisation refusée", "GPS indisponible"
### Fait (suite — 2e itération)
- [x] **Marqueurs étapes différenciés** dans `LeafletMap` :
- Étape courante : couleur primaire pulsante (animation `mim-step-current`), z-index 2000
- Étape complétée : vert (#16a34a) avec ✓
- Étape locked : gris avec 🔒
- Étape future : gris clair
- [x] **Marqueur utilisateur** : point bleu pulsant (`mim-user`), position = `geo.lat/lng`, z-index 3000, non-interactif
- [x] **Progression in-memory** dans `MapSection` :
- `completedSteps: Set<string>` reset à chaque changement de parcours
- `currentStepId` = première étape non complétée dans l'ordre
- `markStepComplete()` : ajoute aux completed + avance auto vers la suivante (ou ouvre la modal de fin si dernière)
- [x] **`hideNextStepsUntilComplete`** géré : `visibleSteps` slice à `currentStep + 1`, polyline et markers respectent
- [x] **Conditions `canAdvance`** dans `StepDetail` :
- `!requireSuccess || radius === 0 || inZone || geo refusé/indispo`
- Bouton "Marquer terminée" / "Terminer le parcours" disabled sinon, avec tooltip explicatif
- [x] **Mode non-linéaire** : nav Précédent/Suivant n'apparaît que si `!path.isLinear`. En linéaire, l'avancement est strictement par validation
- [x] **Badge "✓ Étape terminée"** affiché si l'étape consultée est déjà complétée
- [x] **Modal de fin** `EndOfPathModal` : bravo + boutons "Retour à la carte" / "Recommencer"
### Fait (suite — 3e itération, finale Phase 2)
- [x] **`StepTimer`** ([map/StepTimer.tsx](visitapp-web/src/components/sections/map/StepTimer.tsx)) : barre décroissante + MM:SS + couleurs vert/orange/rouge (≥50/≥25/<25%), callback `onExpired`
- [x] **`StepQuiz`** ([map/StepQuiz.tsx](visitapp-web/src/components/sections/map/StepQuiz.tsx)) : QCM par question (réponses ordonnées), bouton Valider si toutes répondues, feedback vert/rouge sur les bonnes/mauvaises réponses après validation, bouton Réessayer, callback `(passed, score)`. Passe = 100%
- [x] **`Toast`** ([map/Toast.tsx](visitapp-web/src/components/sections/map/Toast.tsx)) : toast vert qui apparaît en haut, auto-dismiss 3.5s, animation slide-in
- [x] **Quiz / Timer / Toast intégrés dans `StepDetail`** :
- Quiz affiché si `step.quizQuestions` non vide ET étape courante non complétée
- Timer affiché si `step.isStepTimer && step.timerSeconds > 0` ET étape courante
- Toast déclenché à la première entrée dans la zone d'une étape (transition falsetrue)
- [x] **Conditions `canAdvance` composées** :
```
quizOk = !hasQuiz || quizPassed || !requireSuccess
zoneOk = radius === 0 || inZone || !requireSuccess || geoUnavailable
lockOk = !isLocked || !requireSuccess || quizPassed || inZone || geoUnavailable
canAdvance = quizOk && zoneOk && lockOk
```
- [x] **Lock dur** : `isStepLocked` impose au moins une preuve (quiz passé OU dans la zone) pour valider quand `requireSuccess`
- [x] **`isHiddenInitially`** : filtré dans `visibleSteps` — uniquement affichées si complétée ou devenue courante
- [x] **Retry permission GPS** : bouton "Localisation refusée — réessayer" dans le StepDetail, qui incrémente `geoEnabledKey` → re-mount du watcher
- [x] **Hook `useGeolocation`** étendu pour accepter un `refreshKey` qui force le re-watch
### Phase 2.bis — Game Escape (FAIT)
- [x] `GameDTO.guidedPaths` ajouté aux types
- [x] `getGuidedPathsForGame()` dans le client API (utilise le même endpoint `/api/SectionMap/{id}/GuidedPath` car la table `GuidedPath` est polymorphe via parent ID — ajustable si le backend expose un endpoint dédié)
- [x] Server component pré-fetch les paths pour les sections Game de type Escape
- [x] [`game/EscapeProgression.tsx`](visitapp-web/src/components/sections/game/EscapeProgression.tsx) — vue verticale sans carte, avec :
- Sélecteur de parcours si plusieurs (auto-sélection si un seul)
- Liste verticale des étapes (cartes empilées)
- Étape courante mise en valeur avec bordure primaire, étapes complétées en vert, étapes futures grisées
- Réutilise `StepTimer`, `StepQuiz`, `Toast`, `MessageDialog`
- Compteur "N/M étapes" en haut + bouton "Changer de parcours"
- Modal de fin avec Recommencer / Retour
- [x] `GameSection` branche `EscapeProgression` quand `gameType === 'Escape'` ET `guidedPaths` non vide, sinon fallback `EscapeStub`
---
## État global
**Tout le périmètre GuidedPath spécifié est implémenté** côté visitapp-web :
- Mode carte (Map section) : sélection parcours, polyline + steps numérotés, marqueurs différenciés (courant/complété/locked/future), marqueur user GPS, progression in-memory, conditions composées, quiz/timer/lock/toast/retry GPS, modal de fin
- Mode contenu (Game type Escape) : même logique sans la carte, vue empilée verticale
**Points d'attention pour validation backend** :
- L'endpoint `/api/SectionMap/{id}/GuidedPath` est utilisé pour Map ET Game Escape (hypothèse : table polymorphe via parent ID). À tester avec une section Game Escape réelle ; sinon le backend doit exposer `/api/SectionGame/{id}/GuidedPath` ou inclure `guidedPaths` directement dans le DTO retourné par `/api/Section/configuration/{id}/detail`
- [ ] **Modal de fin de parcours** : à la dernière étape validée, afficher un dialog "Bravo !" + bouton retour
- [ ] **Permission GPS** : si `geo.status === 'denied'`, afficher un overlay explicatif avec bouton "Réessayer" qui re-mount le hook (nécessite un `key` ou un `setEnabled(false); setEnabled(true)`)
### Phase 2.bis — Mode Game Escape
- [ ] Étendre `GameDTO` web avec `guidedPaths?: GuidedPathDTO[]`
- [ ] Endpoint backend équivalent : à vérifier (probablement `GET /api/SectionGame/{id}/GuidedPath` ou inclus dans le DTO)
- [ ] Page `GuidedPathContentProgressionPage` (sans carte, vue verticale)
- [ ] Brancher depuis `GameSection` quand `gameType === 'Escape'` (remplacer le stub actuel `EscapeStub`)
### Phase 2.bis — Mode Game Escape
- Page `GuidedPathContentProgressionPage` (sans carte, vue verticale)
- Brancher depuis `GameSection` quand `gameType === 'Escape'`
- Nécessite ajout `guidedPaths?` sur `GameDTO`