Un lycéen a une seule ligne dans parcours-lyceens, mise à jour progressivement à chaque étape du questionnaire via partialUpdate(). C'est l'inverse du parcours éclaireur qui utilise plusieurs lignes.

Étapes du questionnaire (ordre réel)

Définies dans front/src/pages/publicspace/questionnaire/lyceen/form-lyceen.ts.

  1. Niveau (niveau) — Seconde / Première / Terminale. Seconde sort du questionnaire (pas de suite).
  2. Collège ou Seconde — écran d'information uniquement, aucune sauvegarde.
  3. Filière (filiere) — Bac général / techno / pro. Visible à partir de Première.
  4. Spés Première (spes_premiere) — 3 spécialités. Uniquement Première + Bac Général.
  5. Spés Terminale (spes_terminale) — 2 spécialités. Uniquement Terminale + Bac Général.
  6. Abandon de spé (spe_abandonnee_premiere) — Terminale + Bac Général, si spes_terminale déjà rempli.
  7. Option Terminale (options_terminale) — Terminale + Bac Général + une spé abandonnée. Maths expertes et Maths complémentaires sont mutuellement exclusives.
  8. Domaines (domains) — Première/Terminale, tous bacs.
  9. Matières (matieres) — ne garde que les matières notées.
  10. Moyenne Première (moyennePremiere) — uniquement Terminale Bac Pro.
  11. Matières Terminale (matieres_terminale) — uniquement Terminale Bac Général.
  12. Environnement (environnement) — regroupe 4 champs plats (lycée/fac/école/CFA) en un tableau.
  13. Encadrement — boursier, budget, projet d'études sup, accompagnement souhaité.
  14. Lycée (dernière étape) — département, établissement.

Bac Pro et Bac Général se répartissent sur des champs quasi disjoints : le Bac Pro saute toutes les étapes spés/options de Terminale générale au profit de la moyenne de Première.

Fonctionnement technique du formulaire

Le wizard est le composant générique components/questionnaire/Questionnaire.vue (partagé avec le parcours éclaireur), piloté par le tableau questionnaire exporté par form-lyceen.ts. Le routing matche route.params.stepName sur step.name ; une étape disparaît de la navigation dès que toutes ses questions ont show(model) === false (filtre émergent, pas de saut explicite codé en dur).

  • partial-update est fire-and-forget. Sur « Suivant » comme « Précédent », onStepSave (→ partialUpdate) part sans être awaité avant le router.push : une erreur réseau est juste loguée en console, l'utilisateur est déjà sur l'écran suivant sans le savoir.
  • Le POST final ne réutilise pas le modèle tel quel. filterModel() (dans QuestionnaireLyceen.vue) ré-exécute le parseDataToSave de toutes les étapes sur le modèle complet pour reconstruire le payload envoyé à postFormulaireLyceen, pas seulement celui de la dernière étape.
  • loadData: true ne recharge pas les réponses précédentes. Il sert uniquement à détecter un « rerun » (résultat déjà calculé) pour afficher l'icône de sortie vers /etudes — reprendre un parcours abandonné en cours ne pré-remplit aucun champ malgré le nom de la prop.
  • Validation 100 % client, étape par étape. currentStepIsValid ne vérifie que les questions visibles de l'étape courante (required + rules), pas le modèle entier ; c'est ce qui active/désactive le bouton « Suivant »/« Terminer ». Un champ required vide bloque le bouton mais n'affiche aucun message (le tooltip reste vide) — seule une rules() en échec alimente le message affiché, et dans ce cas rules() est réexécutée une seconde fois pour construire ce message (pas de cache du résultat).

Pièges

  • partial-update ne crée jamais de ligne. Si le lycéen n'a pas encore de parcours-lyceens (compte tout neuf), l'appel intermédiaire ne fait rien et retourne silencieusement — seule la soumission finale (POST /parcours-lyceen/formulaire) crée la première ligne. Contrairement à ce que suggère « mise à jour progressive », un lycéen qui abandonne avant la fin n'a rien de sauvegardé.
  • Étape « Lycée » : noms de champs incohérents. Elle envoie lycee/lyceeNom, alors que l'entité a etablissement/etablissementNom. Sans conséquence en usage normal (cette étape ne déclenche un partial-update qu'en cliquant « Précédent », pas en la validant), mais si ça arrive, l'établissement choisi n'est alors pas persisté en intermédiaire.
  • abandon_terminale (front) → spe_abandonnee_premiere (colonne). Piège si on grep un seul des deux noms.
  • moyenne (jsonb) n'est jamais rempli par le questionnaire — uniquement par la réponse de l'algorithme externe, à la soumission finale.
  • Champs obsolètes toujours présents : boostAlternanceApprentissage, boostConnaisPersonne, boostPartir ne sont plus collectés depuis l'INSP-662 mais restent dans l'entité (marqués obsolètes en commentaire).
  • Aucune validation serveur sur /parcours-lyceen/partial-update et /parcours-lyceen/formulaire. Les deux routes typent @Body() body: any (parcours-lyceen.controller.ts) : le ValidationPipe global strict (whitelist/forbidNonWhitelisted dans main.ts) ne s'applique jamais, Nest sautant la validation dès que le paramètre n'a pas de type DTO reflété. Le seul DTO existant (ParcoursLyceenUpdateDto) est branché sur une autre route (patch user), pas sur celles-ci. Colonnes entité toutes nullable, sans enum ni contrainte DB. La validation client documentée ci-dessus est donc la seule barrière ; un payload posté directement (hors formulaire) est accepté tel quel, et /formulaire avale même ses erreurs silencieusement (catch (error) {} vide).

Les structures des champs JSONB sont typées dans parcours-lyceen.entity.ts.