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

15 KiB
Raw Permalink Blame History

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

  • Types (GuidedPathDTO, GuidedStepDTO, champ guidedPaths sur MapDTO)
  • API client getGuidedPaths(sectionMapId)
  • 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)
  • Bouton "Parcours" dans la top bar de MapSection (visible si paths > 0, change en "Étape N/M" quand parcours actif)
  • Bottom sheet PathListSheet listant les parcours (titre + nb étapes + flag linéaire)
  • Polyline pointillée + step markers numérotés stylés (CSS pulse sur sélection)
  • Bottom sheet StepDetail (image header + titre + description + badge zone + nav précédent/suivant)
  • Bouton "Quitter le parcours" en bottom-left quand un parcours est actif
  • Auto-fit des bounds de la carte sur les étapes du parcours sélectionné
  • 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

  • src/lib/geo.ts : haversineMeters() + formatDistance()
  • src/hooks/useGeolocation.ts : hook watchPosition avec états idle | requesting | granted | denied | unavailable
  • Hook activé seulement quand un parcours est actif (économie de batterie)
  • 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)

  • 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
  • Marqueur utilisateur : point bleu pulsant (mim-user), position = geo.lat/lng, z-index 3000, non-interactif
  • 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)
  • hideNextStepsUntilComplete géré : visibleSteps slice à currentStep + 1, polyline et markers respectent
  • Conditions canAdvance dans StepDetail :
    • !requireSuccess || radius === 0 || inZone || geo refusé/indispo
    • Bouton "Marquer terminée" / "Terminer le parcours" disabled sinon, avec tooltip explicatif
  • 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
  • Badge "✓ Étape terminée" affiché si l'étape consultée est déjà complétée
  • Modal de fin EndOfPathModal : bravo + boutons "Retour à la carte" / "Recommencer"

Fait (suite — 3e itération, finale Phase 2)

  • StepTimer (map/StepTimer.tsx) : barre décroissante + MM:SS + couleurs vert/orange/rouge (≥50/≥25/<25%), callback onExpired
  • StepQuiz (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%
  • Toast (map/Toast.tsx) : toast vert qui apparaît en haut, auto-dismiss 3.5s, animation slide-in
  • 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)
  • Conditions canAdvance composées :
    quizOk = !hasQuiz || quizPassed || !requireSuccess
    zoneOk = radius === 0 || inZone || !requireSuccess || geoUnavailable
    lockOk = !isLocked || !requireSuccess || quizPassed || inZone || geoUnavailable
    canAdvance = quizOk && zoneOk && lockOk
    
  • Lock dur : isStepLocked impose au moins une preuve (quiz passé OU dans la zone) pour valider quand requireSuccess
  • isHiddenInitially : filtré dans visibleSteps — uniquement affichées si complétée ou devenue courante
  • Retry permission GPS : bouton "Localisation refusée — réessayer" dans le StepDetail, qui incrémente geoEnabledKey → re-mount du watcher
  • Hook useGeolocation étendu pour accepter un refreshKey qui force le re-watch

Phase 2.bis — Game Escape (FAIT)

  • GameDTO.guidedPaths ajouté aux types
  • 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é)
  • Server component pré-fetch les paths pour les sections Game de type Escape
  • 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
  • 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