# 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` 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`