256 lines
15 KiB
Markdown
256 lines
15 KiB
Markdown
# 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 false→true)
|
||
- [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`
|