# Story 3.5: Logique de progression et déblocage contact

Status: review

## Story

As a visiteur,
I want que ma progression débloque l'accès au contact,
so that l'exploration est récompensée sans être frustrante.

## Acceptance Criteria

1. **Given** le store `useProgressionStore` est actif **When** le visiteur visite une nouvelle zone **Then** la zone est ajoutée à `visitedSections`
2. **And** le `completionPercent` est recalculé automatiquement
3. **And** la progression est persistée en LocalStorage (si consentement RGPD donné)
4. **Given** le visiteur a visité 2 zones ou plus **When** la condition est atteinte **Then** `contactUnlocked` passe à `true`
5. **And** le narrateur annonce le déblocage avec un message spécial
6. **And** la zone Contact s'illumine sur la carte (si visible)
7. **And** le visiteur peut continuer à explorer ou aller au contact
8. **Given** le visiteur revient sur le site **When** une progression existe en LocalStorage **Then** le store est réhydraté avec l'état sauvegardé
9. **And** le narrateur affiche "Bienvenue à nouveau"
10. **And** la carte affiche l'état correct des zones visitées

## Tasks / Subtasks

- [x] **Task 1: Compléter le store useProgressionStore** (AC: #1, #2, #4)
  - [x] État : visitedSections, completionPercent, contactUnlocked, hero, narratorStage, choices (déjà dans Story 1.6)
  - [x] Action : visitSection(section) pour enregistrer une visite
  - [x] Getter : isContactUnlocked = visitedSections.length >= 2 || contactUnlocked
  - [x] Getter : narratorStage basé sur completionPercent (ajouté Story 3.3)

- [x] **Task 2: Implémenter la persistance LocalStorage** (AC: #3, #8)
  - [x] Utilisation de pinia-plugin-persistedstate avec conditionalStorage (Story 1.6)
  - [x] Vérifier le consentement RGPD avant de persister (consentGiven dans state)
  - [x] Clé LocalStorage : `skycel-progression`
  - [x] Réhydratation automatique au chargement

- [x] **Task 3: Détecter les visites de sections** (AC: #1)
  - [x] Les pages appellent visitSection() dans onMounted() (Stories 2.x)
  - [x] Plugin narrator-transitions détecte les routes pour les transitions (Story 3.3)

- [x] **Task 4: Implémenter le déblocage du contact** (AC: #4, #5, #6, #7)
  - [x] Le contact est débloqué après 2 sections visitées (dans visitSection)
  - [x] Watcher sur contactUnlocked déclenche narrateur (plugin narrator-transitions, Story 3.3)
  - [x] Accès au contact toujours permis (UX non frustrante)

- [x] **Task 5: Réhydratation au retour** (AC: #8, #9, #10)
  - [x] pinia-plugin-persistedstate réhydrate automatiquement
  - [x] hasExistingProgress getter pour détecter le retour
  - [x] useNarrator.showIntro() vérifie hasExistingProgress pour welcome_back

- [x] **Task 6: Gestion du consentement RGPD** (AC: #3)
  - [x] consentGiven dans le store
  - [x] conditionalStorage ne persiste que si consentGiven === true
  - [x] setConsent(false) supprime les données du localStorage

- [x] **Task 7: Tests et validation**
  - [x] Tester l'ajout de sections visitées
  - [x] Vérifier le calcul automatique du pourcentage (progressPercent getter)
  - [x] Tester le déblocage à 2 sections
  - [x] Valider la persistance LocalStorage
  - [x] Tester la réhydratation au rechargement
  - [x] Vérifier le comportement sans consentement RGPD

## Dev Notes

### Store useProgressionStore complet

```typescript
// frontend/app/stores/progression.ts
import { defineStore } from 'pinia'

// Types
export type Section = 'projets' | 'competences' | 'temoignages' | 'parcours'
export type HeroType = 'recruteur' | 'client' | 'dev' | null
export type Choice = { id: string; value: string; timestamp: number }

// Constantes
const AVAILABLE_SECTIONS: Section[] = ['projets', 'competences', 'temoignages', 'parcours']
const CONTACT_UNLOCK_THRESHOLD = 2
const NARRATOR_STAGE_THRESHOLDS = [0, 20, 40, 60, 80] // 5 stages

export const useProgressionStore = defineStore('progression', () => {
  // === État ===
  const visitedSections = ref<Section[]>([])
  const heroType = ref<HeroType>(null)
  const expressMode = ref(false)
  const choices = ref<Choice[]>([])
  const hasReturned = ref(false) // Pour savoir si c'est un retour

  // === Getters ===
  const completionPercent = computed(() => {
    return Math.round((visitedSections.value.length / AVAILABLE_SECTIONS.length) * 100)
  })

  const contactUnlocked = computed(() => {
    return visitedSections.value.length >= CONTACT_UNLOCK_THRESHOLD
  })

  const narratorStage = computed(() => {
    const percent = completionPercent.value
    for (let i = NARRATOR_STAGE_THRESHOLDS.length - 1; i >= 0; i--) {
      if (percent >= NARRATOR_STAGE_THRESHOLDS[i]) {
        return i + 1 // Stages 1-5
      }
    }
    return 1
  })

  const remainingSections = computed(() => {
    return AVAILABLE_SECTIONS.filter(s => !visitedSections.value.includes(s))
  })

  // === Actions ===
  function visitSection(section: Section) {
    if (!visitedSections.value.includes(section)) {
      visitedSections.value.push(section)
    }
  }

  function setHeroType(type: HeroType) {
    heroType.value = type
  }

  function setExpressMode(enabled: boolean) {
    expressMode.value = enabled
  }

  function addChoice(id: string, value: string) {
    choices.value.push({
      id,
      value,
      timestamp: Date.now(),
    })
  }

  function markAsReturned() {
    hasReturned.value = true
  }

  function reset() {
    visitedSections.value = []
    heroType.value = null
    expressMode.value = false
    choices.value = []
    hasReturned.value = false
  }

  // === Sérialisation pour persistance ===
  function getSerializableState() {
    return {
      visitedSections: visitedSections.value,
      heroType: heroType.value,
      choices: choices.value,
    }
  }

  function hydrateFromState(state: ReturnType<typeof getSerializableState>) {
    if (state.visitedSections?.length) {
      visitedSections.value = state.visitedSections
      markAsReturned()
    }
    if (state.heroType) {
      heroType.value = state.heroType
    }
    if (state.choices?.length) {
      choices.value = state.choices
    }
  }

  return {
    // État
    visitedSections,
    heroType,
    expressMode,
    choices,
    hasReturned,
    // Getters
    completionPercent,
    contactUnlocked,
    narratorStage,
    remainingSections,
    // Actions
    visitSection,
    setHeroType,
    setExpressMode,
    addChoice,
    markAsReturned,
    reset,
    // Sérialisation
    getSerializableState,
    hydrateFromState,
  }
})
```

### Composable useProgressionPersistence

```typescript
// frontend/app/composables/useProgressionPersistence.ts
const STORAGE_KEY = 'skycel_progression'

export function useProgressionPersistence() {
  const progressionStore = useProgressionStore()
  const consentStore = useConsentStore() // Supposé existant depuis Story 1.6

  // Sauvegarder dans LocalStorage
  function persist() {
    // Vérifier le consentement RGPD
    if (!consentStore.hasConsent) {
      return
    }

    try {
      const state = progressionStore.getSerializableState()
      localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
    } catch (error) {
      console.warn('Failed to persist progression:', error)
    }
  }

  // Charger depuis LocalStorage
  function hydrate() {
    try {
      const stored = localStorage.getItem(STORAGE_KEY)
      if (stored) {
        const state = JSON.parse(stored)
        progressionStore.hydrateFromState(state)
        return true // Retourne true si des données ont été trouvées
      }
    } catch (error) {
      console.warn('Failed to hydrate progression:', error)
    }
    return false
  }

  // Supprimer les données
  function clear() {
    try {
      localStorage.removeItem(STORAGE_KEY)
    } catch (error) {
      console.warn('Failed to clear progression:', error)
    }
  }

  // Watcher pour persister automatiquement
  watch(
    () => progressionStore.getSerializableState(),
    () => {
      persist()
    },
    { deep: true }
  )

  // Watcher sur le consentement pour supprimer si retiré
  watch(
    () => consentStore.hasConsent,
    (hasConsent) => {
      if (!hasConsent) {
        clear()
      }
    }
  )

  return {
    persist,
    hydrate,
    clear,
  }
}
```

### Plugin de détection des visites

```typescript
// frontend/app/plugins/progression-tracker.client.ts
export default defineNuxtPlugin((nuxtApp) => {
  const progressionStore = useProgressionStore()
  const router = useRouter()

  // Map des routes vers les sections
  const routeSectionMap: Record<string, Section> = {
    '/projets': 'projets',
    '/en/projects': 'projets',
    '/competences': 'competences',
    '/en/skills': 'competences',
    '/temoignages': 'temoignages',
    '/en/testimonials': 'temoignages',
    '/parcours': 'parcours',
    '/en/journey': 'parcours',
  }

  // Détecter les changements de route
  router.afterEach((to) => {
    const section = routeSectionMap[to.path]
    if (section) {
      progressionStore.visitSection(section)
    }
  })
})
```

### Plugin d'initialisation de la progression

```typescript
// frontend/app/plugins/progression-init.client.ts
export default defineNuxtPlugin(async (nuxtApp) => {
  const { hydrate } = useProgressionPersistence()
  const progressionStore = useProgressionStore()
  const narrator = useNarrator()

  // Attendre que l'app soit montée
  nuxtApp.hook('app:mounted', () => {
    // Réhydrater la progression
    const hasExistingProgress = hydrate()

    // Si le visiteur revient avec une progression existante
    if (hasExistingProgress && progressionStore.hasReturned) {
      // Le message "Bienvenue à nouveau" sera déclenché via useNarrator
      // dans le layout adventure.vue
    }
  })
})
```

### Intégration avec le narrateur

```typescript
// Dans frontend/app/layouts/adventure.vue ou composable useNarrator
// Déclencher le message de déblocage du contact

// Watcher sur contactUnlocked
const unwatchContact = watch(
  () => progressionStore.contactUnlocked,
  (isUnlocked, wasUnlocked) => {
    if (isUnlocked && !wasUnlocked) {
      narrator.showContactUnlocked()
      // Notification visuelle optionnelle
    }
  }
)

// Au montage, vérifier si c'est un retour
onMounted(async () => {
  if (progressionStore.hasReturned) {
    await narrator.showWelcomeBack()
  } else if (progressionStore.heroType) {
    await narrator.showIntro()
  }
})
```

### Interface du consentement (référence)

```typescript
// frontend/app/stores/consent.ts (supposé existant depuis Story 1.6)
export const useConsentStore = defineStore('consent', () => {
  const hasConsent = ref(false)
  const consentDate = ref<number | null>(null)

  function giveConsent() {
    hasConsent.value = true
    consentDate.value = Date.now()
  }

  function revokeConsent() {
    hasConsent.value = false
    consentDate.value = null
  }

  return {
    hasConsent,
    consentDate,
    giveConsent,
    revokeConsent,
  }
})
```

### Schéma du flux de progression

```
┌─────────────────────────────────────────────────────────────────┐
│                     FLUX DE PROGRESSION                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. PREMIÈRE VISITE                                              │
│     └─> Landing page                                             │
│         └─> Choix du héros (recruteur/client/dev)               │
│             └─> heroType = 'recruteur' | 'client' | 'dev'       │
│                                                                  │
│  2. NAVIGATION                                                   │
│     └─> Visite /projets                                         │
│         └─> visitSection('projets')                             │
│             └─> visitedSections = ['projets']                   │
│             └─> completionPercent = 25%                         │
│             └─> narratorStage = 2 (>= 20%)                      │
│                                                                  │
│  3. DÉBLOCAGE CONTACT                                            │
│     └─> Visite /competences (2ème section)                      │
│         └─> visitedSections = ['projets', 'competences']        │
│         └─> completionPercent = 50%                             │
│         └─> contactUnlocked = true (>= 2 sections)              │
│         └─> Trigger: narrator.showContactUnlocked()             │
│                                                                  │
│  4. PERSISTANCE                                                  │
│     └─> Si consentement RGPD                                    │
│         └─> localStorage.setItem('skycel_progression', {...})   │
│                                                                  │
│  5. RETOUR DU VISITEUR                                           │
│     └─> Au chargement                                           │
│         └─> Lire localStorage                                   │
│         └─> hydrateFromState()                                  │
│         └─> hasReturned = true                                  │
│         └─> Trigger: narrator.showWelcomeBack()                 │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

### Dépendances

**Cette story nécessite :**
- Story 1.6 : Store Pinia de base + consentement RGPD
- Story 3.3 : useNarrator pour les messages de déblocage et retour

**Cette story prépare pour :**
- Story 3.6 : Carte interactive (affiche l'état des zones)
- Story 3.7 : Navigation mobile (même logique)
- Story 4.3 : Chemins narratifs (utilise choices)

### Project Structure Notes

**Fichiers à créer :**
```
frontend/app/
├── stores/
│   └── progression.ts                       # CRÉER (complet)
├── composables/
│   └── useProgressionPersistence.ts         # CRÉER
└── plugins/
    ├── progression-tracker.client.ts        # CRÉER
    └── progression-init.client.ts           # CRÉER
```

**Fichiers à modifier :**
```
frontend/app/layouts/adventure.vue           # INTÉGRER la logique de retour
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-3.5]
- [Source: docs/planning-artifacts/ux-design-specification.md#Progression-System]
- [Source: docs/planning-artifacts/architecture.md#State-Management]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| Sections pour progression | 4 (projets, competences, temoignages, parcours) | Epics |
| Seuil déblocage contact | 2 sections visitées | Epics |
| Clé LocalStorage | skycel_progression | Décision technique |
| Condition persistance | Consentement RGPD | RGPD |

## Dev Agent Record

### Agent Model Used

{{agent_model_name_version}}

### Debug Log References

### Completion Notes List

### Change Log
| Date | Change | Author |
|------|--------|--------|
| 2026-02-04 | Story créée avec contexte complet | SM Agent |

### File List

