Aller au contenu

🏗️ Architecture Frontend Vue.js - Gryly

📋 Table des matières

  1. Vue d'ensemble
  2. Architecture générale
  3. Structure des composants
  4. Gestion d'état
  5. Services et API
  6. Routing et navigation
  7. Sécurité et authentification
  8. Internationalisation
  9. Tests et qualité
  10. Déploiement

🎯 Vue d'ensemble

Gryly est une application web moderne construite avec Vue.js 3, utilisant la Composition API et un architecture modulaire basée sur les principes SOLID.

🎨 Technologies principales

  • Vue.js 3 - Framework principal avec Composition API
  • Vue Router 4 - Gestion des routes
  • Pinia - Gestion d'état
  • Axios - Client HTTP
  • VueUse (@vueuse/core) - Composables utilitaires Vue 3 (events, observer, interactions)
  • Tailwind CSS - Framework CSS utilitaire (extensions Teadle alignées maquette espace école maquette-demandes-ecole.html : corail 50900, mushroom 50900 + 250, grey 50900, ainsi que blue / green / orange / red en échelles complètes (tokens tokens.css, sans override dupliqué) pour badges, avatars et états ; toggle pour l’état activé des interrupteurs — rgb(13 148 136) ; dirtyBar pour la bande champ modifié). Pas de couleurs legacy primary / secondary Vue.js ; onglets inactifs paramètres : text-grey-600. Police Inter chargée depuis Google Fonts (index.html, poids 400–700, display=swap) ; theme.extend.fontFamily.sans = Inter + stack Tailwind par défaut. StaffLayout.vue : coquille staff bg-mushroom-100, sidebar border-mushroom-250, logo « T » et badge compteur bg-corail-500, item actif bg-corail-50 text-corail-600, en-tête fluide min-h-14 py-2 sm:min-h-16 (UX-359, hamburger 44 px hors du bloc titre, titre truncate) / border-mushroom-250 / typo text-xl + sous-titre hidden sm:block text-xs text-grey-500. Messages dans la sidebar : lien actif vers /staff/messages (même style que Tableau de bord / Mes demandes). assets/main.css : @layer base avec cursor: pointer sur button:enabled et [role='button']:not([aria-disabled='true']) (aligné maquette) ; en tête de fichier, blocs WCAG (UX-046) : prefers-reduced-motion, @media (hover: none) ciblé (.hover:shadow-md, .hover:-translate-y-0.5), pointer-events: none sur SVG dans button / a / [role='button'], html { scroll-padding-top: 4rem; scrollbar-gutter: stable }.
  • lucide-vue-next - Icônes (import par composant, tree-shaking)
  • Vite - Build tool et dev server
  • ESLint + Prettier - Qualité du code

🏛️ Architecture générale {#architecture-generale}

graph TB
    subgraph "Frontend Vue.js"
        A[App.vue] --> B[Router]
        B --> C[Layouts]
        C --> D[Views]
        D --> E[Components]

        F[Stores/Pinia] --> G[Services]
        G --> H[API Layer]
        H --> I[Backend API]

        J[Utils] --> K[Logger]
        J --> L[Validators]
        J --> M[Helpers]

        N[Assets] --> O[Images]
        N --> P[Styles]
        N --> Q[Icons]
    end

    subgraph "External Services"
        I --> R[Symfony Backend]
        I --> S[OAuth Providers]
        I --> T[File Storage]
    end

    style A fill:#42b883
    style F fill:#ff6b6b
    style G fill:#4ecdc4
    style H fill:#45b7d1

📁 Structure des dossiers

frontend/
├── src/
│   ├── components/          # Composants réutilisables
│   │   ├── messages/        # Inbox messagerie (MessagesInbox, BellDropdown, BannerAlert, ToastNotification — alertes inbox, ConversationList, ConversationItem, ConversationDetail, NotificationList, NotificationItem, NotificationDetail, MessageBubble, modales Accept/Refuse/Cancel/Relaunch, ComboboxFilter) ; deep link `?tab=notifications&notification=uuid` ; StaffLayout : BellDropdown en-tête hors `/staff/messages` ; NotificationBell (trainer, hors `/messages`) aligné visuellement sur BellDropdown
│   │   ├── (shared)/        # Composants partagés
│   │   ├── Auth/           # Composants d'authentification
│   │   ├── Dashboard/      # Composants de tableau de bord
│   │   ├── Onboarding/     # Composants d'onboarding
│   │   ├── Settings/       # Hub paramètres formateur (en-tête commun, etc.)
│   │   ├── Trainer/        # `TrainerCalendarConnectorHub.vue` — hub cartes connecteurs calendrier (OAuth, iCal, sync) : prop `flow` `organization` | `personal` ; orga consomme un contexte `provide`/`inject` (`orgCalendarConnectorInjectionKey.js`) rempli par `ConfigureOrganizationView.vue` ; perso via `TrainerPersonalCalendarConnector.vue` (wrapper mince). `SidebarSyncDot.vue` — indicateur sidebar DISC-019 (états syncing/error, tooltip, navigation vers Organisations)
│   │   └── (forms)/        # Composants de formulaires
│   ├── views/              # Pages/Vues
│   │   ├── (unauthenticated)/ # Pages publiques
│   │   ├── trainer/        # Pages formateur
│   │   └── organization/   # Pages organisation
│   ├── layouts/            # Layouts de pages
│   ├── stores/             # Stores Pinia
│   ├── services/           # Services métier
│   │   ├── (auth)/         # Services d'authentification
│   │   ├── (core)/         # Services principaux
│   │   └── (onboarding)/   # Services d'onboarding
│   ├── api/                # Couche API
│   ├── composables/        # Logique réutilisable (ex. `useTimeFormat` — dates messagerie ; `useBlockSaveFeedback`, `useTrainerSettingsHeader`, `useTrainerSettingsScrollSpy`, `useAvatarColor` — mapping hex API `OrganizationColor` → classes avatar ; `getColorForId(id)` palette déterministe par id, ex. contacts org. ; `useFocusTrap` — piège Tab dans un `role="dialog"` + restauration du focus ; `useModalFocusReturn` (FEAT-171) — retour focus au déclencheur après fermeture `BaseModal` controlled (`open`/`close`, `restoreDelay` 200 ms, WCAG 2.4.3) ; `useErrorMessage` (FEAT-175) — mapping Axios / codes métier → messages FR (`mapError`, `getAuthError`, `getMagicLinkError`) ; consommé par `api/auth.js` (`handleAuthError`) ; `useModal` / `useMultipleModals` — Échap et pile de modales ; `useCrossPageHighlight` — FEAT-081 : deep-links `?highlight=` / `?status=` sur « Mes demandes » formateur & école, scroll + classe `.is-highlighted`, `usePrefersReducedMotion` ; `useSyncPolling` — suivi `GET /process/{uuid}` mutualisé (factory `createSyncPoller` multi-instance + composable auto-cleanup single-instance, backoff 1s/2s/5s + pause Page Visibility) ; `useToast` — stack singleton max 3 toasts globaux)
│   ├── utils/              # Utilitaires
│   ├── assets/             # Ressources statiques
│   ├── i18n/               # Internationalisation
│   └── router/             # Configuration des routes
├── public/                 # Fichiers publics (servis à la racine par Vite, ex. `public/billing/teadle_logo_min.png` → `/billing/teadle_logo_min.png`)
├── tests/                  # Tests
└── docs/                   # Documentation

🧩 Structure des composants

🎯 Hiérarchie des composants

graph TD
    A[App.vue] --> B[Layouts]
    B --> C[TrainerLayout]
    B --> D[OnboardingLayout]

    C --> E[Views/Trainer]
    D --> F[Views/Onboarding]

    E --> G[DashboardView]
    E --> H[PlanningView]

    F --> I[TrainerOnboardingView]
    F --> J[OrganizationOnboardingView]

    G --> K[Components/Dashboard]
    H --> L[Components/Calendar]
    I --> M[Components/Onboarding]

    K --> N[WeekSchedule]
    K --> O[Heatmap]
    K --> P[Actions]

    M --> Q[OnboardingChecklistItem]
    M --> R[OnboardingCelebrationModal]

    style A fill:#42b883
    style B fill:#ff6b6b
    style K fill:#4ecdc4
    style M fill:#45b7d1

TrainerLayout déclenche GET /notification/list (cloche / bandeaux) uniquement sur les routes du menu latéral, avec throttle 5 min et horodatage en localStorage — voir constants/trainerMenuNotificationRoutes.js et .doc/frontend/WORKFLOWS.md. Sidebar formateur (ordre maquette DISC-023 + DES-1 DISC-027, FEAT-136) : Tableau de bord → Démarrer → Mon planning → Mes demandes → Organisations (SidebarSyncDot) → Messages (badge unread) → Mes factures ; après séparateur : Mes documents + Mon parcours / CV (trainer-parcours-cv) → Paramètres. Mon planning est un lien simple vers /trainer/planning (plus de sous-menu latéral). Partages et Disponibilités sont accessibles depuis le hub Paramètres (/trainer/settings) : tuiles → /trainer/settings/planning/shares (ShareCalendarView.vue) et /trainer/settings/planning/availability (AvailabilityView.vue, onglets horaires | feries | absences | agenda ; anciennes URLs /trainer/planning/share et /trainer/planning/availability redirigent). Sur desktop, le header global du layout est masqué (isPlanningSettingsChromeRoute) : chaque vue embarque sa coque maquette DISC-023 (titre + cloche, lien retour Paramètres, onglets ou liste PlanningShareEntityCard.vue). Horaires hebdo : cartes jour + pause déjeuner (utils/availabilityWeekDays.js, MockToggle.vue). Partages : modales maquette via PlanningShareModal.vue (sélecteur lien/email, options en boutons segmentés, « Plus d’options » repliable), PlanningShareRevokeModal.vue (confirmation centrée). Le header de Mon planning expose les actions maquette (configurer disponibilités, compteur partages, partager) via PlanningHeaderActions.vue. DISC-019 : l’entrée Organisations embarque SidebarSyncDot (mobile + desktop non replié), et ToastContainer est monté globalement dans le layout. Au mount : syncStore.bootstrap() (reprise des sync actives après F5). Au unmount : syncStore.stopAll() (cleanup pollers). Coquille scroll : le bandeau global est en shrink-0 (évite la compression flex au recalcul du layout) ; le contenu défile dans un div overflow-y-auto exposé par provide('trainerMainScrollEl') ; le Dashboard (KpiStrip.vue) injecte cette ref pour ancrer les KPI dans ce conteneur (scroll relatif) plutôt que scrollIntoView seul sur le document, ce qui évite des effets de bord sur le header. Racine des shells à défilement interne (UX-358) : cascade h-screen h-svh (TrainerLayout, StaffLayout, OnboardingLayout, .page-fullscreen) — h-svh (petit viewport, iOS Safari / Chrome Android) prime là où il est supporté, h-screen reste le repli ; jamais h-screen seul, jamais h-dvh sur un shell (les drawers BaseModal restent en h-dvh). En-tête de page mobile (UX-359) : hauteur fluide min-h-14 py-2 sm:min-h-16 (sm:py-1 formateur pour rester à 64 px avec text-2xl) ; hamburger lg:hidden shrink-0 min-h-11 min-w-11 hors du bloc titre ; titre truncate + :title ; sous-titre hidden sm:block ; lien retour min-h-11 sous lg. Disponibilités / Partages : titre porté par l’en-tête du layout sous lg (en-tête de vue hidden lg:flex). Actions d’en-tête injectées (UX-361, pattern layout-page-header-actions) : CTA primaires sous 640 px = icône seule (hidden sm:inline sur le libellé) + aria-label explicite, cible min-h-11 min-w-11 lg:min-h-0 lg:min-w-0, justify-center, px-3 sm:px-4 ; gap-3 lg:gap-2 entre cibles adjacentes (Messages) ; badge informatif masqué sous sm (OrganizationConfigureHeaderBadge). Composants : ComplianceHeaderActions, IntervenantsHeaderActions, MessagesInboxHeaderActions, InvoicesListHeaderActions (split + chevron 44 px sous lg), OrganizationsListHeaderActions. Drawer navigation mobile (UX-364, parité StaffLayout / UX-254) : BaseModal type="drawer" position="left" (h-dvh w-72 max-w-[80vw]), bouton « Fermer le menu » 44 px, piège de focus / Échap / verrou défilement (Reka DialogRoot) ; fermé par défaut, fermeture à chaque navigation et au passage ≥ lg.

🔧 Composants principaux

Composants d'authentification

  • Footer.vue (FEAT-172) — pied de page : variant full (défaut, logo + copyright + CTA LinkedIn brand-color) | utility (pre-auth DISC-024 : lien discret text-grey-700 / hover:text-blue-500, sans logo ni copyright). Tests : Footer.spec.js.
  • OAuthButtons.vue - Boutons OAuth (LinkedIn, Google, Microsoft) ; prop variant: legacy (défaut, vues signup) | ds (SigninView DISC-024) ; emits provider-selected, error (erreur interne affichée uniquement en legacy)
  • SigninView.vue (FEAT-178) — page /signin : Magic Link primary + password fallback + OAuth ; state machine 10 états ; modal reset inline via BaseModal + useModalFocusReturn (suppression PasswordResetModal.vue)
  • OAuthRedirect.vue - Gestion des redirections OAuth

Composants partagés (shared)

  • DashboardCardSkeleton.vue – Skeleton loaders pour états de chargement (@brayamvalero/vue3-skeleton, palette mushroom). Variants : revenue, chart, list, stats, default. Props bare, compact. Respecte prefers-reduced-motion (WCAG 2.2) via usePrefersReducedMotion. Utilisé : Dashboard, InvoicesView, CreateInvoiceView, CreateCRAView, Incomes.
  • Animations décoratives / skeletons inline (UX-077) : préfixe Tailwind motion-safe: sur les classes d’animation CSS (motion-safe:animate-pulse, réf. SkeletonLoader.vue et pattern DS feedback-skeleton-loader) ; animations pilotées en JS → usePrefersReducedMotion. Filet global prefers-reduced-motion dans assets/main.css conservé en défense-en-profondeur (commentaire de convention). Spinners animate-spin : hors scope UX-077 (filet global uniquement).
  • Focus visible global (FEAT-173) : assets/main.css — tokens :root { --focus-ring-color: #c00c0c; … } ; @layer base *:focus-visible (outline corail-700, !important pour primer sur focus:outline-none legacy). Composants DS login (BaseButton, BaseInput tone corail, BaseSelect corail, MagicLinkStatus, toggle password) : rings corail-500 retirés (outline global suffit). .compact-control : idem. Override .is-highlighted:focus-visible. Cleanup massif focus:ring-corail-500 dans vues métier → issue FIX future ; scroll-padding-top: 4rem déjà sur html (WCAG 2.4.12).
  • ConfirmDangerModal.vue — Confirmation destructive (suppression) : Teleport vers body, icône corbeille (Trash2, lucide) sur fond bg-red-50, titre + message, Annuler (bordure) / Supprimer (bg-red-500). v-model + événements confirm / cancel. Remplace window.confirm pour ce cas d’usage. Exemple : suppression d’un modèle de document dans BillingSettingsView.vue.
  • HelpTooltip.vue — Infobulle d’aide contextuelle : bouton icône CircleHelp (lucide-vue-next), bulle sombre au survol et au focus. Props : text (obligatoire), placement (top | bottom, défaut top). Accessibilité : role="tooltip", aria-describedby / aria-label="Aide" sur le déclencheur. Usage : aide métier ponctuelle (pas pour remplacer le toast de confirmation d’autosave en bas à droite sur les paramètres).
  • MiniLoader.vue - Spinner compact pour chargements secondaires
  • UrgencyBadge.vue (FEAT-052 / DISC-012) — badge urgence demandes de réservation : props secondsBeforeExpiration, responseDeadline ; trois niveaux visuels (< 2 h rouge + Clock, < 48 h orange + AlertTriangle, sinon texte gris « Avant le … ») ; rafraîchissement client toutes les 60 s ; tabular-nums. Utilisé dans components/Trainer/RequestCard.vue (page formateur « Mes demandes ») et views/staff/RequestsView.vue (colonne expiration, lots en attente).
  • ToastNotification.vue (FEAT-052) — toast feedback type Sonner : fixed bottom-4 right-4, role="status" / aria-live="polite", auto-dismiss configurable (défaut 4 s), fermeture manuelle ; props message, visible, icon (check-circle, x-circle, send, info, bell), iconClass. Branché sur views/trainer/RequestsView.vue (accept / refus / erreurs) et views/staff/RequestsView.vue (relance, annulation, copie email, erreur d’envoi message).
  • CommandPalette.vue (FEAT-058 / DISC-012) — palette de commandes Ctrl+K / Cmd+K : Teleportbody, overlay z-[80], recherche filtrante, flèches + Entrée, ARIA combobox / listbox / options ; props items (sections { title, options[] } avec id, label, icon Lucide, kbd, subtitle, route…), événements select / close. Intégrée à views/trainer/RequestsView.vue (12 commandes, 3 sections) et views/staff/RequestsView.vue (9 commandes, 2 sections + listener global keydown avec garde champs comme le formateur).
  • ConversationDrawer.vue (components/shared/, FEAT-056, UX-046) — panneau latéral 500px (conversation staff ↔ formateur sur un batch) : fil de messages, expansion « voir les messages précédents », textarea Entrée = envoyer ; useFocusTrap pour le focus dans le dialog + restauration ; Échap → close. Props open, request, messages, loading, userInitials, userName, fonctions de badge statut ; événements close, send.

Composants de formulaires ((forms)/)

  • PhoneInput.vue - Téléphone international
  • ProgressiveEmailInput.vue - Email avec validation progressive (message après blur, puis correction en direct), bordures d’état, librairie validator (isEmail) centralisée dans src/utils/email.js (isValidEmail). Props utiles : variant (default = focus rouge formateur, blue = onboarding), external-error (erreur imposée au submit), show-all-errors, slot suffix (icônes). Événements : blur-valid, input-valid. Utilisé notamment par ContactSidebar.vue, StaffInvitationConnectedView.vue.
  • Rôles contact organisation (FEAT-248) : GET /trainer/organization/contact/rolescontactAPI.getContactRoles() (tableau de ROLE_*) ; cache module-level useContactRoles + traduction i18n front constants/contactRoles.js. Consommé par ContactSidebar.vue (permissions en v-for, retry si échec) et ConfigureOrganizationView.vue (chips, filtres incl. Conformité).

Composants de tableau de bord

  • WeekSchedule.vue - Planning hebdomadaire
  • components/Trainer/planning/CalendarWeekGrid.vue (FEAT-132) — grille semaine DS (pattern container-calendar-week-grid) : en-têtes 7 jours (today corail, weekend hatch), time-gutter 56px, lignes h-14, off-hours/pause, now-indicator (pulse, setInterval 60s), slot scoped #day (day, dayIndex, hourHeight, hours), defineExpose({ HOUR_HEIGHT_PX: 56 }). Branché par PlanningView.vue (FEAT-135) via slot #day + CalendarEventBlock positionné par eventStyle() (algo lanes R14).
  • components/Trainer/planning/CalendarEventBlock.vue (FEAT-133) — bloc événement vue Semaine (position absolue via :style parent) : R3 past 50 %, R4 indispo hatch, R5 couleur org border-left, R6 conflit + AlertTriangle, R10 formation si durée ≥ 2 h ; emits click / hover / clear-hover.
  • components/Trainer/planning/CalendarMonthGrid.vue (FEAT-133) — grille mois 7×N : pills (max 2) + « +N autres », today/hors-mois ; emits event-click / day-click.
  • components/Trainer/planning/CalendarDayAgenda.vue (FEAT-133) — liste agenda vue Jour : rows horaire corail + détails + chevron ; emptyMessage ; emit event-click.
  • components/Trainer/planning/CalendarFilterBar.vue (FEAT-134) — filtres calendriers : BaseCheckbox + accent dynamique, pastille couleur, compteur, « + Ajouter un calendrier » ; emits toggle-calendar / add-calendar.
  • components/Trainer/planning/PlanningMiniCalendar.vue (FEAT-134) — mini-calendrier view-based : :ref-date, :view (jour | semaine | mois), feuilletage indép., bande sélection, @select-day ; remplace l’ancien contrat range-start / anchor-date.
  • components/Trainer/planning/CalendarLegend.vue (FEAT-134) — légende 4 statuts (dots + hatch indispo), items configurable.
  • components/Trainer/planning/CalendarToolbar.vue (FEAT-135 / UX-363) — toolbar persistante : nav période, segmented switcher ; props opt-in showDateButton + dateSheetOpen, emit open-date (bouton « Choisir une date » sous lg).
  • components/Trainer/planning/CalendarFilterBar.vue (UX-363) — chips calendriers : rangee defilante overflow-x-auto snap-x sous lg, retour a la ligne a partir de lg ; p-1 -m-1 pour anneaux de focus non rognes.
  • components/Trainer/planning/PlanningMiniCalendar.vue (UX-363) — prop opt-in touchTargets (chevrons 44 px dans la feuille de date).
  • views/trainer/PlanningView.vue (FEAT-135 + FEAT-137 + FEAT-141 + UX-363) — rewrite Phase 1 DISC-023 ; sous lg : colonne laterale masquee (hidden lg:flex), bandeau perso dans le flux, feuille BaseModal (mini-calendrier + legende statuts), rangee d'actions mobile sur une ligne ; lien « Configurer mes disponibilités » stylise (plus de bouton imbrique dans lien).
  • utils/planningEmptyPeriod.js (FEAT-135) — libellé état vide selon vue DISC-023 (jour | semaine | mois, plus les types FullCalendar).
  • PlanningCalendar.vuesupprimé en FEAT-162 (cleanup DISC-023) après migration complète des vues planning vers CalendarWeekGrid / CalendarMonthGrid / CalendarDayAgenda.
  • views/(unauthenticated)/Planning/SharedPlanningView.vue (FEAT-153 + FEAT-154) — vue publique planning en layout min-h-screen flex flex-col (header DS + zone scroll flex-1 overflow-y-auto) migrée sur CalendarWeekGrid pour le pattern drag-to-create : drag layer pointer events (touch-action: none local), ghost persistant valid/invalid, alternative clavier (tabindex + Enter/Space), bouton mobile 44x44, aria-live="polite" pendant drag, ouverture wizard pré-rempli via handleBookingRequest.
  • views/(unauthenticated)/Planning/SharedPlanningView.vue (FEAT-155 / UX-360) — extension dual-zone draft : bouton header secondaire Ma demande (badge dot ring-2 ring-white) qui redirige vers le bandeau de reprise, bandeau primaire (bg-mushroom-50) avec CTA Continuer ma demande / Annuler, sticky sentinel (useStickyBannerSentinel avec root: pageScrollRef) pour passage inline/fixed ; mode collé : top = hauteur mesurée de l'en-tête (useElementBounding) ; remontée via pageScrollRef.scrollTo ; confirmation remplacement/annulation via modal destructif, et conservation d’instance wizard via v-show (reset explicite par resetWizardData).
  • views/(unauthenticated)/Planning/SharedPlanningView.vue (FEAT-156) — info-bar compacte + popover détails (onClickOutside + fermeture Escape via useMultipleModals), sidebar lg+ avec mini-map mensuelle et légende, et hachures cellule-par-cellule branchées via CalendarWeekGrid:isCellAvailable en lisant availability.rules.
  • views/(unauthenticated)/Planning/SharedPlanningView.vue (FEAT-157) — section inline #tracking (plus de redirection externe) avec machine 4 états idle/link-sent/verifying/results, ancrage header Mes reservations vers scroll local, et gestion ?token= au mount avec nettoyage URL (router.replace sans token/email).
  • views/(unauthenticated)/Planning/SharedPlanningView.vue (alignement maquette maquette-planning-public-ecole (2).html) — harmonisation visuelle info-bar/tracking vers le schéma blue-* (callout principal, popover actions, step link-sent), ajout de la toolbar période (prev/next/today) + switcher Jour/Semaine/Mois, rendu multi-vues (CalendarWeekGrid / CalendarMonthGrid / CalendarDayAgenda) et ajout de la carte sidebar A propos.
  • views/staff/IntervenantCalendarView.vue (FEAT-160) — vue staff interne modernisée DS (bg-mushroom-100, actions BaseButton, erreurs BaseAlert, icônes Lucide), sans PlanningCalendar legacy. Rendu multi-vues day/week/month via CalendarDayAgenda, CalendarWeekGrid (+ CalendarEventBlock), CalendarMonthGrid (monthCells), avec mapping planning API -> format DISC-023 (dateKey, hStart, hEnd, past, type) et bascule mois→jour au clic cellule.
  • views/staff/IntervenantsCalendarsView.vue (FEAT-161) — vue staff multi-intervenants refondue DS (palette grey/mushroom/corail, BaseButton/BaseInput/BaseCheckbox/BaseAlert, icônes Lucide), migration complète de PlanningCalendar vers CalendarWeekGrid / CalendarMonthGrid / CalendarDayAgenda, toolbar unifiée (nav période + switcher Jour/Semaine/Mois, pattern maquette), listbox intervenants accessible (combobox + roving tabindex + focus option), mapping agrégé events DISC-023 (type: 'indispo'), merge availability UNION, couleurs déterministes (AVAILABLE_COLORS + hash).
  • views/staff/IntervenantsView.vue (FEAT-233 + FEAT-231) — hub « Mes intervenants » : useStaffComplianceStore.mapIntervenantRow dérive presence / departed / paused depuis l’API ; onglets BaseTabs Actifs / Anciens (role="tabpanel") ; bandeau de départ BaseAlert + modale BaseModal (wording R14) ; badges « Indisponible » / « Parti le … » ; sous-titre RGPD pour les partis.
  • components/ui/BaseInput.vue (FEAT-165 / FEAT-161) — defineExpose({ focus }) pour focus programmatique depuis les vues staff (recherche intervenants).
  • views/staff/IntervenantCalendarView.vue (FEAT-160 finalisation) — toolbar unifiée (navigation période + switcher Jour/Semaine/Mois dans un bloc bg-white rounded-xl, pattern maquette-planning-public-ecole), chevrons ChevronLeft/ChevronRight, rechargement silencieux 5 mois via _reloadPlanningForVisibleRange (clé lastPlanningRangeKey mise à jour).
  • views/(unauthenticated)/Planning/SharedPlanningView.vue (FEAT-162) — suppression du fallback legacy BookingSidebar/showBookingModal : le flux de réservation passe uniquement par BookingWizard, ce qui retire la dette technique historique sans impact fonctionnel.
  • components/BookingSidebar.vuesupprimé en FEAT-162 (composant legacy non consommé).
  • Dépendances @fullcalendar/* (core, daygrid, interaction, timegrid, vue3) — retirées en FEAT-162 du frontend (package.json + lockfile).
  • components/BookingWizard.vue (FEAT-158 + FEAT-159) — refonte visuelle DS en drawer (BaseModal type="drawer" position="right") avec header/footer en slots, progression 5 étapes public / 3 étapes staff (wizardSteps, totalSteps, currentStepLabel), migration des champs vers BaseInput/BaseSelect/BaseCheckbox/BaseAlert/BaseButton et icônes Lucide, conservation de la logique métier (recherche, alternatives, validations email+téléphone E.164, idempotency). FEAT-159 ajoute le mode wizardFromDrag en étape 2 : card contextuelle éditable (editDate/editStartTime/editEndTime), validations inline (passé / ordre / durée min / overlap), emits update:slot (sync WYSIWYG du ghost côté SharedPlanningView) et pause-for-calendar-edit (retour grille FEAT-155). Exposition parent enrichie : defineExpose({ clearLocalStorage, resetWizardData }) pour le flow pause/annulation sans remount. Ajustements maquette 2026-05-28 : étape 2 alignée en style compact (palette verte, format date/heure ven. 29 mai 2026 + 12:30), étape 3 alignée sur la structure HTML de référence (text-sm/text-xs, titre Sujet et formation, aide message), étape 4 reprise en version compacte (coordonnées + encarts info/RGPD), et étape 5 refondue en sections numérotées (Synthèse, Sujet et contact, Créneaux demandés) avec blocs délai/RGPD conformes à la maquette. Finalisation stricte FEAT-158 : suppression des éléments legacy (SearchableSelect, PhoneInput, <svg> inline, classes bg-red-*), remplacement par DS/Lucide uniquement, radios rythme migrés en cartes-boutons (aria-pressed) et modal de succès migrée vers BaseModal.
  • components/SlotSelectionModal.vue (fix interaction 2026-05-28) — migration de la modale de sélection de créneau vers BaseModal DS (au lieu d’un overlay manuel fixed) pour restaurer les interactions imbriquées depuis BookingWizard : clic sur créneau, scroll du contenu, fermeture via bouton/overlay/Escape, et focus management cohérent avec la stack de dialogs Reka UI.
  • composables/useDragToCreate.ts (FEAT-154) — state machine drag 4 états (idlependingdraggingreleased) avec seuil adaptatif (mouse/touch), throttle requestAnimationFrame, guard hasPointerCapture() avant setPointerCapture(), gestion pointercancel iOS Safari, exposé drag (ghost, ariaAnnounce) + onPointerDown + resetDrag.
  • composables/useStickyBannerSentinel.ts (FEAT-155 / UX-360) — wrapper useIntersectionObserver (VueUse) pour gérer le passage inline/sticky du bandeau draft via sentinelle ; option root (conteneur .page-fullscreen-scroll) pour bascule exacte sans marge en dur ; défaut viewport + rootMargin: -65px inchangé pour autres consommateurs futurs.
  • composables/useMagicLinkTracking.js (FEAT-157) — composable JS (JSDoc) pour le flux magic-link : envoi email (requestTrackingLink), vérification token (verifyTrackingTokenperformToken + getTrackingReservations) et mapping défensif batches -> liste reservations via mapBatchesToFlatReservations.
  • components/Trainer/planning/PlanningMiniCalendar.vue (FEAT-156) — extension backward-compatible : props optionnelles showTodayButton, chevronOrientation (vertical/horizontal) et liveLabel (aria-live sur le label mois) pour réutilisation dans la vue publique sans duplication.
  • components/Trainer/planning/CalendarWeekGrid.vue (FEAT-156) — extension backward-compatible : prop optionnelle isCellAvailable(dayIdx, hour) ; si fournie, applique les hachures au niveau cellule, sinon conserve le comportement historique (weekend par colonne).
  • components/SharedKeyboardModal.vue (FEAT-154) — alternative WCAG 2.5.7 pour création de créneau sans drag : BaseModal slots #header/#footer, BaseInput date/heure (step=1800), validation métier injectée (validate), erreurs inline via BaseAlert, submit vers SharedPlanningView.
  • components/SharedConfirmModal.vue (FEAT-155) — modal de confirmation destructive (replace/cancel draft) basé sur BaseModal (role="alertdialog"), layout centré (icône/titre/body/actions), bouton safe à gauche, bouton destructive à droite.
  • Heatmap.vue - Carte de chaleur des activités
  • Actions.vue - Actions rapides
  • Incomes.vue - Affichage des revenus
  • Opportunities.vue - Opportunités disponibles
  • ActiveSchools.vue - Écoles actives
  • MySchools.vue (FEAT-078 / FEAT-098 / FIX-106) - Bloc « Mes écoles » du dashboard formateur : tableau desktop + cartes mobile, toggle période (quarter/year/12_months), deeplink facturation (/trainer/invoices?organization={uuid}), états loading/error/empty/data, accessibilité (caption sr-only, th scope, labels sr-only trend/statut). FEAT-098 : prop isSample, badge item Exemple, emit sample-click et blocage navigation facturation sur entrées sample. FIX-106 : empty state migré vers EmptyState DS avec CTA action + onboardingLink « Comment attirer des organisations ? » vers trainer-getting-started.
  • KpiStrip.vue (FEAT-077 / FEAT-098) - Barre KPI compacte en tête dashboard avec scroll interne vers les cartes détaillées (kpi-prev, kpi-heures, kpi-charge, kpi-interventions). FEAT-098 : prop isSample (pas de variation visuelle, utilisée pour homogénéité contrat parent).
  • MyDay.vue (FEAT-077 / FEAT-098 / FIX-106) - BLOC 1 « Ma journée » : toggle today/week, listes interventions, états loading/error/empty/new, émission open-intervention. FEAT-098 : badge Exemple par intervention sample. FIX-106 : état vide "new user" migré vers EmptyState DS avec onboardingLink « Voir le guide de demarrage ».
  • ToProcess.vue (FEAT-077 / FEAT-098 / FIX-106) - BLOC 2 « À traiter » : badge delta +X nouvelles, quick action d’acceptation (modale de confirmation), lien drill-down vers /trainer/requests?highlight={id}. FEAT-098 : badge Exemple par demande sample et emit sample-click (pas d’ouverture de la modale d’acceptation réelle). FIX-106 : état vide "new user" migré vers EmptyState DS avec onboardingLink « Comment demarrer ? ».
  • MyIndicators.vue + KpiCard.vue (FEAT-077 / FEAT-098 / FIX-106 / UX-362) - BLOC 3 : 4 KPI avec count-up, tooltips, donut SVG heures et breakdowns écoles. Grille grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 items-start ; détail déplié dans chaque carte via slot #detail + BaseCollapse (pattern kpi-card-expandable) : déclencheur min-h-11 lg:min-h-0, aria-expanded + aria-controls, région aria-labelledby = titre de la carte ; auto-collapse (un seul ouvert). FEAT-098 : mention en-tête Données d'exemple via prop isSample. FIX-106 : fallback sans summary migré vers EmptyState DS avec onboardingLink « Voir le guide de demarrage ».
  • RevenueEvolutionChart.vue (FEAT-077) - Section évolution CA avec fetch autonome (6_months/12_months/year), tracé SVG réactif et footer stats (moyenne/meilleur mois).
  • MyStatistics.vue (FEAT-077 / FEAT-098 / FIX-106) - Section statistiques collapsée (clients actifs, concentration CA, taux horaire effectif, évolution annuelle). FEAT-098 : mention en-tête Données d'exemple via prop isSample. FIX-106 : fallback sans summary migré vers EmptyState DS avec onboardingLink « Voir le guide de demarrage ».
  • InterventionDrawer.vue + ToastNotification.vue (FEAT-077) - drawer intervention (focus trap + Escape + retour focus) et toast succès d’acceptation (timer bar 3s).
  • SampleDataBanner.vue (FEAT-097) - bandeau orange informatif dashboard, visible si sample data actif (hors pre-avert J-2), avec lien trainer-getting-started et action "Supprimer les exemples" (appelle onboardingStore.dismissSampleData()).
  • PreAvertBanner.vue (FEAT-097) - bandeau orange d'alerte J-2 (role="alert"), visible si sample actif et expiresAt <= now + 2 jours, CTA "Partager mon profil" vers trainer-getting-started.
  • FirstRealRequestBanner.vue (FEAT-097) - bandeau vert maquette (« 🎉 Votre première vraie demande… »), visible si une ligne recent-requests a isFirst: true (backend) et flag localStorage absent (teadle_disc016_first_request_seen), CTA vers trainer-requests.
  • SampleModal.vue (FEAT-099) - modale guardrail sample data (wrapper BaseModal) ouverte apres sample-click pour expliquer le mode exemple et proposer la redirection vers trainer-getting-started; fermeture via overlay, Escape ou CTA secondaire.
  • EmptyState.vue (FEAT-099) - composant DS dashboard enrichi avec prop optionnelle onboardingLink ({ label, to? }) pour rendre un router-link contextuel vers le guide de demarrage, sans casser l'API historique (icon, message, actionLabel, emit action).
  • DashboardView.vue (FEAT-098 / FEAT-099) - orchestration sample mode : isSampleMode + mappings effectiveSummary, effectiveInterventionsToday/Week, effectiveRecentRequests, effectiveSchools depuis onboardingStore.sampleDataPayload; passage isSample aux composants dashboard et ouverture/fermeture de SampleModal sur les evenements sample-click.

Composants d'onboarding

  • PublicLayout.vue (FEAT-170) — pages pre-auth (login, signup, tracking) : min-h-svh flex flex-col bg-mushroom-50, centrage via my-auto + max-w-md (pas justify-center parent), slots default + #footer. Remplace progressivement .page-fullscreen (legacy main.css). Tests : PublicLayout.spec.js.
  • OnboardingLayout.vue - Coquille onboarding avec scroll interne : wrapper h-screen h-svh flex flex-col (UX-358) + zone de contenu flex-1 overflow-y-auto pour garantir le défilement des étapes (legacy .page-fullscreen avec overflow-hidden sur le conteneur).
  • SidebarGettingStartedItem.vue (FEAT-090 / DISC-016) - Entrée menu Démarrer (icône fusée, badge X/5 corail ou coche verte si terminé) entre « Tableau de bord » et « Mon planning » quand useOnboardingStore().showInSidebar est vrai ; lien nommé trainer-getting-started/trainer/getting-started. TrainerLayout : si formateur et progress === null, hydrateProgressFromAuth() ; timer 5 min pour fetchProgress() (GET /me).
  • GettingStartedView.vue (FEAT-091 / FEAT-092 / FEAT-094 / FEAT-095 / FEAT-096) — page /trainer/getting-started : au montage, fetchProgress() systématique (GET /me → progression onboarding) ; root bg-mushroom-100. Bandeau TrainerLayout : « 🚀 Démarrer ». Checklist 5 étapes (OnboardingChecklistItem ×5 + séparateur « Recommandé »), ordre core puis IMPORT_CALENDAR / DEFINE_PRICING ; slot #cta step SHARE_LINK pour FEAT-093 ; CTA « Voir mes demandes » si isComplete. FEAT-094 : modale unique « Votre profil est prêt ! » (OnboardingCelebrationModal, Reka UI via BaseModal) à la transition 5/5 ou au chargement si déjà complet et flag localStorage teadle_onboarding_celebration_shown absent ; fermeture pose le flag. FEAT-095 : le bouton « Masquer ce guide » devient un flux différé Gmail-style (pendingDismiss) avec BaseToast inline (4 s, pausable, action « Annuler ») ; le commit backend (onboardingStore.dismiss) ne part qu’à l’expiration du toast. FEAT-096 : section everboarding AllerPlusLoinSection affichée dès completedCount >= 3 (3 cartes actives + 1 carte disabled « Bientot » + lien aide externe).
  • OnboardingChecklistItem.vue (FEAT-092) — accordéon step : cercles 3 états (vert / corail + ring / gris), badge Recommandé (steps 4–5), durée ou Complété, BaseCollapse pour le panneau, CTA router-link ou slot.
  • components/ui/BaseCollapse.vue (FEAT-102 minimal / FEAT-092) — repli hauteur grid 0fr/1fr, inert si fermé, role="region".
  • components/ui/BaseToast.vue (FEAT-103 / FEAT-095) — toast DS : variant (success / error / warning / info), position inline | bottom-right, durationMs (0 = pas d’auto-fermeture), role="status" / aria-live="polite", fermeture manuelle (closable) ; support pausable (pause/reprise timer au hover et au focus), slot default (contenu) + slot action (ex. bouton undo), barre de progression role="progressbar" mise à jour en continu.
  • components/ui/SyncToast.vue + ToastContainer.vue (FEAT-111 / DISC-019) — wrapper métier des toasts de synchronisation agenda : SyncToast consomme BaseToast (success 5s pausable, error persistant 0ms avec CTA router-link), ToastContainer téléporte en bas-droite (Teleport vers body, max 3 via useToast, stack vertical).
  • components/ui/BaseModal.vue (FEAT-085 / FEAT-094 / FEAT-163) — wrapper reka-ui (DialogRoot / DialogPortal / DialogOverlay / DialogContent) étendu avec slots structurés #header / default / #footer, variants type (modal | drawer) + position (center | right | left), role (dialog | alertdialog) et closeOnOverlayClick (défaut true). Backward compat critique : si aucun #header/#footer, le body conserve p-8 historique ; sinon le body devient scrollable sans padding forcé. Animations pilotées par marqueurs sémantiques (base-modal-modal, base-modal-drawer-right, base-modal-drawer-left) + fallback prefers-reduced-motion.
  • components/ui/BaseToggle.vue (FEAT-127) — interrupteur on/off DS : v-model booléen, size sm | lg (défaut lg), ariaLabel, disabled, class ; CVA toggleTrackVariants / toggleKnobVariants dans lib/variants.js + cn() (lib/utils.js) ; aria-pressed (pas role="switch") ; track vert / mushroom + knob translate-x. MockToggle.vue (planning) reste en prod jusqu’à FEAT-138 / FEAT-139.
  • components/ui/BaseCheckbox.vue (FEAT-128 / FEAT-167) — case à cocher DS avec 2 modes : sans label (input seul, backward compat) ou avec label/slot #label (markup riche RGPD). Support required, error inline (role="alert" + aria-invalid), id auto (useId), v-model booléen ou tableau (value en mode groupe), accentColor inline ; forwarding attrs/listeners via inheritAttrs: false + v-bind="$attrs" sur l’input.
  • components/ui/BaseTabs.vue (FEAT-129) — navigation role="tablist" / role="tab" : v-model = id actif, tabs (id, label, icon?, count?), badge si count != null ; CVA tabVariants / tabCountVariants ; clavier APG (flèches cycliques, Home/End, roving tabindex). Tabpanels (id="tabpanel-<id>") gérés par le consommateur.
  • components/ui/BaseSelect.vue (FEAT-130 / FEAT-166) — champ DS en 2 modes : mode="select" (Reka SelectRoot) et mode="autocomplete" (Reka ComboboxRoot). API enrichie alignée BaseInput : label, required, hint, error, id auto (useId), ariaLabel, class sur racine (space-y-1), forwarding attrs/listeners avec inheritAttrs: false + v-bind="$attrs" vers SelectTrigger/ComboboxInput. Autocomplete : v-model:query, filterFn customisable, allowCreate, slot #empty-action (payload { query }) + emit create.
  • components/ui/BaseAlert.vue (FEAT-131 + FEAT-179) — callout role="note" | status | alert : variantes info | success | warning | error (CVA alertVariants + alertIconColors / alertTextColors), icône Lucide par défaut (AlertCircle pour error) ou :icon, slot #title + défaut, dismissible + @close, prop ariaLive optionnelle avec auto-dérivation (alertassertive, statuspolite), override class. Tests : BaseAlert.spec.js.
  • components/ui/BaseButton.vue (FEAT-082) — bouton natif : variant primary (corail-500) | secondary (border mushroom-250), size md (h-10 rounded-full), prop class via cn/tailwind-merge ; utilisé par PlanningView header (FEAT-137).
  • components/ui/MagicLinkStatus.vue (FEAT-169) — suivi Magic Link 4 états : state sent | verifying | expired | error ; props email, expiresAt (ISO → expiresInHuman), errorMessage ; emits resend, change-email ; slots #sent-extra, #expired-extra ; ARIA role/aria-live (status/polite vs alert/assertive) ; CVA magicLink*Variants ; CTAs BaseButton. Tests : MagicLinkStatus.spec.js.
  • components/ui/BaseInput.vue (FEAT-084 / FEAT-165 / FEAT-168 / FEAT-180) — champ DS enrichi : v-model, type, id auto (useId) ou explicite, label, required, hint, error, ariaLabel, name, autocomplete, inputmode (WHATWG pass-through — clavier mobile, ex. email), step|min|max, slots #prefix/#suffix, forwarding d’attributs/listeners natifs via inheritAttrs: false + v-bind="$attrs" sur l’<input>. showPasswordToggle (défaut false) : bouton eye/eye-off absolu dans .input-wrap (passwordToggleButtonVariants, pr-12, aria-label / aria-pressed FR) si type="password". defineExpose({ focus }) pour autofocus parent (ex. modale reset DISC-024). class sur racine ; inputFieldVariants + tone corail|blue ; aria-describedby priorise error sur hint. Tests : BaseInput.spec.js.
  • Getting started — SHARE_LINK (FEAT-093) : pas d’appel GET /trainer/planning/share ; statut d’étape dérivé de completedSteps (GET /me via fetchProgress). Si SHARE_LINK absent : CTA vers trainer-planning-share, « Je l’ai déjà partagé » → completeStep('SHARE_LINK') ; BaseToast sur erreur de validation uniquement.
  • OnboardingProgressBar.vue (FEAT-091, réécriture DISC-016) — barre segmentée (completedCount / totalCount, défaut 5), hints contextuels (3/5, 4/5, complet + shimmer sur les segments à 100 %). Ancienne version route-based (3 étapes) retirée (composant orphelin).
  • OnboardingCelebrationModal.vue (FEAT-094) — modale 5/5 : checkmark SVG animé (200 ms, respect prefers-reduced-motion), titre / sous-titre Reka DialogTitle / DialogDescription, CTAs demandes + fermer ; consomme BaseModal.
  • AllerPlusLoinSection.vue (FEAT-096, FEAT-219) — section everboarding progressive : grille responsive 2x2, liens internes vers facturation (trainer-billing?tab=personnalisation), messages (trainer-messages), dashboard (trainer-dashboard) et parcours CV (trainer-parcours-cv, badge « Opérationnel » statique) + footer aide externe.
  • BackButton.vue - Bouton de retour
  • DatePicker.vue - Sélecteur de date

Vues facturation (formateur)

  • CompanyView.vue - Paramètres entreprise : infos bancaires + section Facturation (TVA / unité / délai par défaut, numérotation, annexes ; pas de champ tarif horaire ici — il est sur BillingSettingsView /trainer/settings/billing)
  • CreateInvoiceView.vue - Création de facture : préremplissage depuis la config entreprise, bloc « Paramètres appliqués » (lecture seule), case TVA non applicable (article 293 B du CGI) si 0 % sur toutes les lignes ; query ?organizationUuid= (FEAT-272 / R-AV2) pour pré-sélectionner le client depuis l’éditeur d’avoir
  • CreditNoteEditorView.vue - Édition / finalisation d’un avoir (FEAT-272) : route /trainer/invoices/:invoiceUuid/credit-notes/:uuid, calquée sur la facture pour lignes, produits et garde de saisie
  • BillingSettingsView.vue - Onglet Personnalisation : table des modèles (BillingTemplate) branchée API (GET/POST/PUT/DELETE + duplicate + set-default), filtre Tous / Factures & Avoirs / CRA, confirmation suppression, badge Teadle pour modèles système ; modale « Nouveau modèle » (nom + type INVOICE ou ACTIVITY_REPORT) avant POST /trainer/billing/templates puis redirection vers l’éditeur.
  • DocumentTemplateEditorView.vue - Éditeur fullscreen /trainer/template-editor/:uuid (ancienne route redirigée) : API templates. documentType issu du modèle (INVOICE vs ACTIVITY_REPORT) : accordéons et payload CRA (colonnes date/formation/classe/durée/notes/source + table.columnLabels, récap modules/sessions, signature, pied confidentialité/texte libre) vs facture (émetteur, client, contenu, colonnes facture, paiement, pénalités). Badge type lecture seule (en-tête + panneau config). Toggle Modèle par défaut (useBillingSettingsAppearance) inchangé. Preview : body { name, documentType, settings } ; aperçu HTML CRA (tableau avec Source iCal/manuel, récap sans couleur accent, signature bas de page) généré côté backend (billing_template_preview.html.twig + PreviewBillingTemplateCommand).

Configurateur organisation (formateur)

  • TrainerCalendarOAuthCallbackView.vue — route /trainer/calendar/oauth/:provider/callback (enfant de TrainerLayout, JWT requis) : après redirection Google/Outlook, lit code / state / error dans la query, appelle POST …/calendar/list (calendarAPI.oauthCalendarList), affiche le choix d’agenda (radio), puis POST …/callback (calendarAPI.oauthCalendarCallback) avec jetons + calendarId + state ; réponse ProcessResponse ; redirection vers trainer-organizations-configure avec ?tab=connecteurs&syncProcess=<uuid> pour le suivi dans ConfigureOrganizationView (comme après ajout iCal).
  • Re-sync calendrier (ConfigureOrganizationView) : état « connecté » si icalUrl, hasOAuthCredentials ou providerType === ical_file ; liste connecteurs (GET /trainer/calendar/connectors/config : entrées = stratégies backend, sans libellés ; ordre + noms FR + OAuth « stub » dans calendarConnectorProviders / useTrainerCalendarConnectorsConfig ; une carte iCal pour lien ou fichier — entrée ical_file filtrée ; à l’affichage ical_file est normalisé vers la carte ical ; allowFileUpload pour ical, hyperplanning, ypareo (URL ou fichier .ics exclusifs)) chargée avant le GET configurateur (await loadConnectorProvidersFromApi) pour afficher le bon picto/nom ; providerType normalisé en minuscules pour matcher l’API hub. Query optionnelle ?calendarConfiguratorUuid= ou ?configuratorUuid= (deep-link) surcharge l’UUID renvoyé par l’organisation. Bouton Re-synchroniserPOST /trainer/calendar/configurator/{uuid}/sync (calendarAPI.sync). Bloc « Lien iCal » masqué si pas d’URL ; texte Source pour OAuth / fichier. Retour OAuth ?syncProcess= : initConnectorState ne remplace pas l’état syncing tant que isSyncing ou le polling process est actif (évite bannière erreur + succès) ; pendant ce suivi, le skeleton connecteurs est masqué comme en re-sync.
  • ConfigureOrganizationView.vue - Configuration des associations formation/matière (calendrier). Layout : bandeau global TrainerLayout (comme trainer/settings/*) : titre « Nom · Configuration » via organizationsHeaderState + useTrainerOrganizationsHeader, sous-titre « Configurez votre organisation partenaire », badge « Organisation associée » (OrganizationConfigureHeaderBadge.vue + setHeaderActions), cloche header = BellDropdown du layout (pattern DS 2026). Racine vue flex-1 flex flex-col overflow-hidden + fond bg-mushroom-100 ; lien retour « Organisations » sous le header global, onglets horizontaux (pattern APG : role="tablist" / tabpanel, flèches, badges check ou compteur sur filteredAssignments). Contenu scrollable flex-1 overflow-y-auto p-6 bg-mushroom-100 ; cartes principales bg-white rounded-xl border-mushroom-200 (informations, connecteurs, zone sensible) ; onglet Tarifs en pleine largeur, les autres en max-w-3xl mx-auto. Deep link (FEAT-035) : query ?tab=informations|connecteurs|formations|tarifs|contacts|events ; les anciens ?section=general|ical|training|pricing|contacts restent mappés pour compatibilité. L’onglet actif est recalculé depuis la route via un watch avec immediate sur query.tab, query.section et params.uuid (pas de garde isTabEnabled à ce stade — initializeData() est async). Depuis OrganizationsView, liens nommés trainer-organizations-configure + query.tab (ex. contacts, connecteurs, formations). Déverrouillage : Connecteurs et Contacts dès association ; Formations et Tarifs après sync iCal (calendarConfiguratorUuid). Tokens Tailwind : mushroom-300, grey-400 (voir tailwind.config.js). Bandes de suggestion (auto-assigné / suggestion disponible) : scores de confiance individuels (Formation : X% | Matière : Y%) via getConfidenceDisplay(assignment) et suggestedPrograms[].matchScoring / suggestedModules[].matchScoring ; repli sur assignment.confidence si besoin. Tab Informations (FEAT-042, FEAT-048) : aligné maquette maquette-configuration-organisationune seule carte blanche (v-show="!loading") avec en-tête conditionnel (titres / sous-titres ajout vs édition comme l’état orgState === 'not-associated' de la maquette) ; sans association (FEAT-048), segments Écoles & Universités / Entreprises & Centres (py-2.5 comme la maquette), recherche organizationAPI.searchTrainerOrganizations (debounce 800 ms, min. 3 caractères), association (router.replace + assignTrainerOrganization), création (?create=true + createTrainerOrganization) ; l’ancienne page AddOrganizationView est retirée et /trainer/organizations/add redirige vers trainer-organizations-configure. Une fois associé, champs dans .input-wrap + indicateur .dirty (bloc <style> non scoped dans la vue, réutilisable ailleurs) ; snapshot origForm après populateForm / sauvegarde / association ; informationsDirty (non-propriétaire : campus seul) ; boutons Annuler + Enregistrer toujours visibles, désactivés si pristine ; isFieldDisabledForNonOwner sur tous les champs sauf campus ; bannière bleue non-propriétaire ; skeleton pulse dans l’onglet si chargement sur Informations (spinner global uniquement pour les autres onglets) ; erreur chargement + Réessayer dans le panneau ; watch(informationsDirty, { immediate: true })CustomEvent set-unsaved (section: 'informations') pour FEAT-047. Tab Connecteurs (FEAT-043) : hub six providers (Google, Outlook, Apple, Hyperplanning, Ypareo, iCal générique), aide contextuelle par provider, OAuth via calendarAPI.oauthCalendarAuthorize(provider, organizationUuid) (réponse 200 + authorizationUrl → redirection navigateur ; 501 si fournisseur non prêt → toast + ouverture du fallback URL), synchro initiale / fichier .ics avec providerType sur addIcalLink / uploadIcalFile, re-sync et changement d’URL via sync ou nouvel addIcalLink, barre de progression globale (style « ribbing ») + six étapes alignées sur les états du Process, état providerType / icalUrl issu du configurateur (initConnectorState après getCalendarConfigurator), modale déconnexion (réinitialisation UI locale — pas d’endpoint suppression calendrier en V1). Icônes : views/trainer/components/ConnectorProviderIcon.vue. Tab Formations (FEAT-044) : liste groupée par préfixe de titre avant le premier - (groupedAssignments, getGroupKey), pills « Toutes » + années (selectedSchoolYear null = toutes ; défaut année courante au chargement config), filtre avec épinglage des fiches dirty hors période ; barre d’enregistrement si formationsDirty et onglet Formations actif : entre le tablist et contentScroll, style maquette (bg-white border-b border-mushroom-250 px-6 py-2.5 flex items-center justify-between shrink-0 shadow-sm, gap-2.5, boutons py-1.5) ; le scroll de la liste Formations est uniquement dans contentScroll (flex-1 overflow-y-auto), la vue remplit la colonne via TrainerLayout (router-view rendu dans <component class="flex min-h-0 flex-1 flex-col"> sous TrainerSettingsSubnav shrink-0) (snapshots _orig* + checkItemDirty, cancelFormations, événement set-unsaved section formations) ; champs en input-wrap + classe dirty ; Formation / Matière : SelectWithCreate dans .input-wrap.input-wrap--dropdown (assets/main.css, overflow: visible pour ne pas clipper le panneau) et carte cours en relative z-0 focus-within:z-20 pour empiler la liste au-dessus de la carte suivante ; grille des quatre champs grid-cols-1 sm:grid-cols-4 ; interrupteur actif/inactif (bouton role="switch") ; modale « Valider le groupe » (openValidateGroupModal / confirmValidateGroup, détection niveau extractLevel) ; bannières transfert de période en tête de liste (transferBannerList) ; empty state + skeleton ; échec save (assignPmo) : toast fixe bas d’écran (Teleportbody, raiseFormationsSaveError / clearFormationsSaveError, fermeture manuelle + auto 12 s, sans error global) ; bannière verte lorsque toutes les clés actives sont associées. Tab Tarifs (FEAT-045) : aligné maquette maquette-configuration-organisation (4).html (tab Tarifs) ; pleine largeur ; skeleton / empty / table (matière, période, tarif/h input-wrap + dirty, badges) ; pills py-1.5, selectedSchoolYear partagé avec Formations ; bandeau prix défaut (€ /h) alimenté par defaultPrice (GET /trainer/calendar/configurator/{uuid}) + lien trainer-billing-settings (/trainer/settings/billing) ; alerte si !selectedProgram || !selectedModule dans le filtre ; champ prix désactivé tant que formation ou matière manquante ; barre Annuler + Enregistrer uniquement si tarifDirty (transition opacity) ; saveTarifs / toast / set-unsaved / calendarAPI.setPrices sans reload après succès. Tab Contacts (FEAT-046) : skeleton (chargement page global), empty, erreur contactsLoadError + réessai loadContacts, en-tête + recherche (mushroom-50 / focus corail), pills, cartes rounded-xl, avatars getColorForId(uuid) + getInitials (useAvatarColor), TransitionGroup sur les cartes, toast invitation + modale 403 (Teleport body) ; CRUD dans ContactSidebar.vue. Tab Événements : à droite de Contacts ; actif dès association (comme Contacts) ; OrganizationEventsPanel charge GET /trainer/organization/{uuid}/events uniquement au focus d’onglet (pas à l’init page) ; tableau type Factures (sans facettes) Date / Horaires / Intitulé / Provenance (Connecteur | Demande) ; skeleton vue3-skeleton ; drawer lecture seule OrganizationEventDrawer (lien planning, lien demande si requestId). FEAT-047 : _dirtySections (Set réassigné pour la réactivité) alimenté par CustomEvent set-unsaved (listener dès le setup, retrait au onUnmounted) ; garde sur switchTab / navigateTab (_doSwitchTab, pendingTab) avec modale « Modifications non enregistrées » (Teleportbody, max-w-sm, clic overlay = Rester) ; abandon via confirmLeaveTab (cancelInformations / cancelFormations / cancelTarifs par section). Modale suppression d’association : layout A centré, Teleportbody, v-show + transition opacité, pas de fermeture au clic overlay ; useMultipleModals (pile avec la modale unsaved, Échap ferme la dernière ouverte) ; focus initial sur Annuler ; type-to-confirm inchangé.

📊 Gestion d'état {#gestion-detat}

🏪 Architecture Pinia

  • useRequestsStore (stores/useRequestsStore.js, id Pinia trainerRequests, FEAT-052) — état des demandes de réservation côté formateur : batches, stats (réponse getReservationRequests), searchQuery, activeTab, sortBy ; calculés tabCounts, urgentBatches (< 48 h, NEW/PENDING), filteredBatches (filtre onglet + recherche + tri urgence/date/formation/école) ; actions fetchBatches, acceptBatch / refuseBatch (rechargement après succès ; accept envoie un corps JSON optionnel { message } via trainerAPI.acceptReservationBatch), startPolling / stopPolling (30 s). Consommé par views/trainer/RequestsView.vue (FEAT-054).
  • useOnboardingStore (stores/onboarding.js, id Pinia onboarding, FEAT-089 / DISC-016) — progression formateur connectée : état progress (miroir de user.onboardingProgress renvoyé par /me) + cache sampleDataPayload; hydrateProgressFromAuth() sans réseau si progress encore null ; fetchProgress() via authStore.fetchMe() (DRY) avec déduplication des appels concurrents — déclenché depuis la page Getting started et par timer 5 min dans TrainerLayout ; completeStep / dismiss / restore appelant onboardingAPI (POST /trainer/onboarding/* FEAT-088), dismissSampleData() (POST /trainer/onboarding/sample-data/dismiss, FEAT-097), fetchSampleDataPayload() (GET /trainer/onboarding/sample-data, FEAT-098, no-op si déjà chargé) ; calculés badgeLabel, showInSidebar (masquage checklist + grace period 7 j après les 5 étapes), isSampleDataActive. Tests unitaires stores/onboarding.spec.js.
  • useStaffRequestsStore (stores/useStaffRequestsStore.js, id Pinia staffRequests, FEAT-056 / FEAT-057) — état école / staff pour « Mes demandes » : groupes issus de staffAPI.getRequestGroups(), onglets + compteurs (filtrés par période), recherche / formateur / période, tri cinq colonnes, pagination 20/ligne, expandedRows (multi-expand), drawer conversation (openConversation / sendMessage, marquage messages lus à l’ouverture du drawer) ; fetchRequests met à jour staffDashboard.setPendingRequestsCount ; pas de polling (notifications = issue séparée). FEAT-057 : groupByTrainer, openGroups, calculé groupedRequests (sections par formateur, agrégats hasNew / newCount / unreadMessagesCount), toggleGroup ; en mode groupe la pagination est masquée. Consommé par views/staff/RequestsView.vue (FEAT-058 : palette commandes branchée sur ce store pour onglets / groupement / compteurs).
  • staffRequestAggregatedStatus.js (utils/, FEAT-057) — isMultiTrainerFulfilled et résolveurs de libellé / classes pour afficher Pourvue lorsqu’une demande multi-formateurs a un lot FULFILLED (first-claim) tout en conservant le statut groupe accepted ; utilisé par views/staff/RequestsView.vue et le drawer (ConversationDrawer) via les helpers agrégés.
  • useSyncStore (stores/syncStore.js, id Pinia sync, FEAT-109 / DISC-019) — état de synchronisation agenda agrégé multi-organisations : orgStates (Map), isBootstrapping, bootstrapError; getters syncingCount, errorCount (seuil ERROR_THRESHOLD = 2), syncGlobalState (priorité error > syncing > stable), calmDotTooltip, mostRecentSyncEndDate, lastGlobalSyncFormatted; actions bootstrap (GET /trainer/organization/dashboard + reprise polling des currentSyncProcessUuid), startSync, stopSync, onSyncComplete, stopAll; polling via createSyncPoller (composables/useSyncPolling.js). Cible : alimentation transversale TrainerLayout / Organizations / ConfigureOrganization.
  • useMessagesStore (stores/messages.js, FEAT-067 / FEAT-068 / FEAT-069 / FEAT-070 / FEAT-118) — inbox messagerie unifiée : conversations — liste paginée (page, limit 30) + infinite scroll (loadMoreConversations, hasMoreConversations), filtres (search client-side, person, dates, type en query comme le back), filteredConversations, fetchConversation (fusion dans la liste ou ajout en tête si absent), markAsRead (POST /conversations/{uuid}/read puis ajustement local des comptes non lus + fetchBadges throttlé), fil de discussion : fetchThreadMessages charge d’abord la dernière page (messages les plus récents en bas, ordre API createdAt ASC), loadMoreThreadMessages préfixe les pages plus anciennes (threadOlderNextPage) ; appendThreadMessage après envoi. Notifications (onglet)fetchNotifications / loadMoreNotifications via GET /notifications/search (même taille de page 30), filteredNotifications (recherche texte client-side), markNotificationRead (POST /notifications/{uuid}/read), markAllNotificationsAsRead (POST /notifications/read-all), selectedType + currentNotificationUuid pour le panneau détail. Compteurs non lus : unreadCount + syncUnreadCountFromBadges(unreadInbox) alignés sur trainerDashboard.unreadInbox / staffDashboard.unreadInbox (GET /trainer|staff/dashboard/badges, polling 2 min dans les layouts) ; inboxToastRequested incrémenté quand le total non lu augmente (toast « Nouveau message »). FEAT-070/118 : BellDropdown (cloche header, pattern DS 2026 : ARIA dynamique, mark-all-read, séparation lu/non-lu, support type backend sync_failure + deep-link organisations), BannerAlert (threads batch_thread non lus), ToastNotification (composant messages/ToastNotification.vue, pas le toast partagé). Consommé par components/messages/MessagesInbox.vue + ConversationDetail.vue / NotificationDetail.vue (sans bandeau titre : Messages + Gérez vos conversations et notifications dans TrainerLayout / StaffLayout via pageTitle / pageSubtitle) ; vues Messages trainer/staff embarquent MessagesInbox.
  • conversation.js (api/conversation.js) — listConversations / getMessages : réponses paginées { items, currentPage, itemsPerPage, totalItems, totalPages } ; markAsRead / markAsUnread en POST (/conversations/{uuid}/read et /unread).
  • NewMessageModal.vue — à l’ouverture : getOrganizationContacts() (GET /trainer/organization/contact/list) côté formateur, getAssociatedTrainers() (GET /staff/associated-trainers) côté staff (même sources que la page Intervenants / contacts org), puis fusion avec conversationPersons du store pour les interlocuteurs déjà en fil de boîte.
  • notification.js (api/notification.js) — searchNotifications (pagination, tri createdAt DESC par défaut), markNotificationRead (POST lecture unitaire), markAllNotificationsAsRead (POST /notifications/read-all).
  • useTimeFormat.js (composables/) — formatTimeLabel / getDateLabel (date-fns + locale fr) pour libellés relatifs dans la liste et les futurs fils de discussion (DISC-014).
graph LR
    A[AuthStore] --> B[User State]
    A --> C[Token Management]
    A --> D[OAuth State]

    E[OnboardingStore] --> F[Onboarding Progress]
    E --> G[Form Data]
    E --> H[Validation State]

    I[AppStore] --> J[Global State]
    I --> K[UI State]
    I --> L[Notifications]

    style A fill:#ff6b6b
    style E fill:#4ecdc4
    style I fill:#45b7d1

🔐 Store d'authentification

// stores/auth.js
export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref(null)
  const token = ref(null)
  const refreshToken = ref(null)
  const isAuthenticated = computed(() => !!token.value)

  // Actions
  const login = async (credentials) => { /* ... */ }
  const logout = () => { /* ... */ }
  const refreshAuth = async () => { /* ... */ }

  return {
    user, token, refreshToken, isAuthenticated,
    login, logout, refreshAuth
  }
})

🔌 Services et API

🏗️ Architecture des services

graph TB
    subgraph "Service Layer"
        A[AuthService] --> B[OAuthService]
        A --> C[TokenService]

        D[OnboardingService] --> E[OnboardingStorage]
        D --> F[OnboardingGuard]
        D --> G[OnboardingFinalization]

        H[CoreServices] --> I[EventService]
        H --> J[OpportunityService]
        H --> K[RedirectService]
    end

    subgraph "API Layer"
        L[AuthAPI] --> M[Backend Auth]
        N[OnboardingAPI] --> O[Backend Onboarding]
        P[TrainerAPI] --> Q[Backend Trainer]
        R[OrganizationAPI] --> S[Backend Organization]
    end

    subgraph "HTTP Client"
        T[Axios Instance] --> U[Interceptors]
        U --> V[Error Handling]
        U --> W[Token Refresh]
    end

    style A fill:#ff6b6b
    style D fill:#4ecdc4
    style H fill:#45b7d1
    style T fill:#f39c12

📡 Configuration API

// api/axios.js
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000
})

// Intercepteurs pour la gestion des tokens
api.interceptors.request.use(config => {
  const auth = useAuthStore()
  if (auth.token) {
    config.headers.Authorization = `Bearer ${auth.token}`
  }
  return config
})

api.interceptors.response.use(
  response => response,
  async error => {
    if (error.response?.status === 401) {
      const auth = useAuthStore()
      await auth.refreshAuth()
    }
    return Promise.reject(error)
  }
)

🛣️ Routing et navigation

🗺️ Structure des routes

graph TD
    A[/] --> B[Signin]
    A --> C[Signup]
    A --> D[Password Reset]

    B --> E[OAuth Redirects]
    E --> F[LinkedIn]
    E --> G[Google]
    E --> H[Microsoft]

    I[Onboarding] --> J[Trainer Onboarding]
    I --> K[Organization Onboarding]

    J --> L[Trainer Signup]
    J --> M[Personal Info]
    J --> N[Profile Options]
    J --> O[CV Analysis]

    K --> P[Organization Signup]
    K --> Q[Organization Info]
    K --> R[Settings]

    S[Authenticated] --> T[Trainer Dashboard]
    S --> U[Organization Dashboard]
    S --> V[Planning]

    style A fill:#42b883
    style I fill:#ff6b6b
    style S fill:#4ecdc4

Hub Paramètres formateur (/trainer/settings)

  • Hub : route trainer-settingsviews/trainer/SettingsHubView.vue (grille de tuiles, contenu centré max-w-3xl). Dernière tuile Clôturer mon compte (zone de danger, sm:col-span-2, bordure rouge) → trainer-settings-close-account.
  • Clôturer mon compte (DISC-026 / FEAT-227 + FEAT-232) : trainer-settings-close-accountviews/trainer/CloseAccountView.vue (/trainer/settings/close-account) — rappel compte (auth.user), CloseAccountDangerZone.vue (cartes Désactiver / Supprimer ; boutons désactivés si accountStatus !== 'active'), renvoi profil public. Désactivation : modale BaseModal role="alertdialog"trainerAPI.deactivateAccount + auth.fetchMe. Suppression (FEAT-232) : DeleteAccountModal.vue en 3 étapes — (1) conséquences + info exports (toast), (2) enquête départ facultative, (3) confirmation par saisie email → trainerAPI.requestDeletion + auth.fetchMe. Bandeaux globaux dans TrainerLayout : AccountDeactivatedBanner.vue si accountStatus === 'deactivated'trainerAPI.reactivateAccount ; AccountDeletionGraceBanner.vue si deletion_pending — date depuis auth.user.scheduledDeletionAt, annulation → trainerAPI.cancelDeletion + auth.fetchMe. API : trainer.jsdeactivateAccount, reactivateAccount, requestDeletion, cancelDeletion. Champs cycle de vie dans auth.user via GET /me : accountStatus, scheduledDeletionAt.
  • Entreprise (paramètres) : trainer-settings-companyviews/trainer/CompanySettingsView.vue (/trainer/settings/company) — trois sections (infos perso avec toggle teal, identité & banque, Annexes). Annexes : liste / upload / suppression via trainerAPI.getCompanyDocuments, uploadCompanyDocument, deleteCompanyDocument (documents entreprise S3, PDF/PNG/JPG max 5 Mo) ; ancres navItems incluent company-annexes ; suppression avec ConfirmDangerModal. Champs texte / listes : composants CompactTextInput et CompactSelectField (components/(shared)/) ; conteneur .input-wrap + classe .dirty dans assets/main.css (bande orange #F7B13B à gauche si le champ diffère du snapshot sauvegardé, border-radius droit seulement). Couleur dirtyBar dans tailwind.config.js. Même modale départ sans save que les notifications : UnsavedLeaveModal. L’ancienne route trainer-company (/trainer/company) redirige vers cette page.
  • Profil (paramètres) : trainer-settings-profileviews/trainer/ProfileSettingsView.vue (/trainer/settings/profile) — ancres Photo, Identité, Adresse publique (TrainerSlugEditor, FEAT-196), Email, Téléphone, Mot de passe, Sécurité : avatar (trainerAPI.uploadAvatar), identité et téléphone via CompactTextInput / CompactSelectField, champ email via ProgressiveEmailInput + isValidEmail (utils/email.js, même principe que les contacts d’organisation dans ContactSidebar.vue), enregistrement email / téléphone via trainerAPI.updateInfo (merge-patch), mot de passe via updatePassword (api/user.js), validation email optionnelle (authCodeAPI). États dirty (bande orange), BlockSaveActions + UnsavedLeaveModal, useTrainerSettingsScrollSpy pour l’onglet actif (scroll / resize, marqueur = bas du menu horizontal mesuré dans le viewport). L’ancienne route nommée trainer-account (/trainer/account) redirige vers cette page (plus de montage direct de AccountView.vue).
  • Notifications (paramètres) : trainer-settings-notificationsviews/trainer/NotificationsSettingsView.vue (/trainer/settings/notifications) — opt-ins, recommandations, partage de documents, préférences (taux de réponse) ; approche hybride : autosave debounced (500ms) sur les toggles (notifications, recommandations, partage docs) + feedback inline « Enregistré ✓ », et boutons explicites (BlockSaveActions) pour la section Préférences uniquement. Garde navigation : components/(shared)/UnsavedLeaveModal.vue déclenchée uniquement si Préférences a des changements non enregistrés (Teleport vers body, événements stay / leave).
  • Coffre-Fort / Mes documents (DISC-027 / FEAT-237 + … + FEAT-242 + FEAT-243 + FEAT-264 + FEAT-245 + FEAT-246 + FEAT-247 + FEAT-258 + FEAT-263) : … NDA — nature « numéro » (FEAT-263 / CR 2026-07-20) : activity_declaration dans constants/compliance.jsCOMPLIANCE_NUMBER_NATURE_TYPES, formatNda() (affichage récépissé XX XX XXXXX XX) ; tuile dédiée dans CoffreFortView.vue (saisie 11 chiffres, statut vert/gris) ; persistance useComplianceStore.saveNda()PUT /trainer/company (objet company complet, champ nda sans espaces) ; types nature numéro exclus du dépôt/renouvellement et du compteur de pièces téléchargeables. Drawer staff (FEAT-250 + FEAT-263) : IntervenantComplianceDrawer.vuenda racine du GET documents (useStaffComplianceStore.drawerNda) ; pièce synthétique nature: 'numero' + valeur copiable (formatNda). Fiche intervenant (FEAT-246 / R16) : header ComplianceHeaderActions.vue — bouton secondaire « Générer ma fiche intervenant » (≥ sm, à gauche de « Déposer un document ») → modale CoffreFortView.vue (BaseModal) : 4 cases profil (cochées par défaut), liste pièces avec badge RAG (expirées décochées + désactivées + barrées + alerte role="alert"), CTA « Générer la fiche » désactivé si rien de sélectionné ; téléchargement via generateComplianceFiche() (GET /trainer/compliance/fiche, blob PDF ou ZIP). Détail / suppression (FEAT-258 + FEAT-244) : components/trainer/ComplianceDetailDrawer.vue (métadonnées, aperçu blob, historique versions, renouveler / remplacer / supprimer ; RIB : bloc « Coordonnées bancaires » — IBAN masqué par défaut, reveal 30 s, copier via trainerAPI.getCompany().iban) + ComplianceDeleteModal.vue (type-to-confirm « Supprimer ») ; entrées table : nom fichier → détail, icône télécharger + kebab (Renouveler / Voir le détail / Supprimer) ; API getComplianceDocumentDetail, downloadComplianceDocument, deleteComplianceDocument ; store deleteDocument (refetch hub best-effort). Export ZIP (FEAT-247) : barre d’action sous la table — bouton « Télécharger mes documents (ZIP) » (BaseButton secondary, icône Archive) avant « Gérer le partage » ; désactivé si !store.hasDownloadableDocs ; téléchargement blob via downloadComplianceZip() (GET /trainer/compliance/export, responseType: 'blob') + toast ; état « Préparation… » pendant la requête. Store : computed hasDownloadableDocs. Honorabilité (FEAT-245) : components/trainer/HonorabilityCard.vue montée dans CoffreFortView.vue (scan PDF local ou saisie manuelle des 4 références non pénales, lien portail justice.gouv.fr) ; extraction 100 % navigateur via composables/useB3References.js (pdfjs-dist + worker Vite ?url — le bulletin n°3 n’est jamais transmis) ; persistance useComplianceStore — état honorability, actions fetchHonorability, saveHonorabilityRefs ; API api/compliance.jsgetHonorability (GET /trainer/honorability, 404null), saveHonorability (PUT). Partage Cas A (FEAT-242) : barre « Gérer le partage », drawer organisations (BaseModal drawer + onglet « Organisations connectées », BaseToggle), GET/POST/DELETE …/compliance/shares ; clôture demandes org via events Symfony (TrainerComplianceShareGrantedEvent / TrainerComplianceDocumentProvidedEventRespondPendingTrainerComplianceRequestsSubscriber). Partage Cas B (FEAT-243 + FEAT-264) : même drawer, onglet « Lien externe temporaire » (CoffreFortView.vue, BaseTabsfetchLinks au changement d’onglet) : sélection multi-pièces (checkboxes + « Tout sélectionner », newLink.documentUuids[]) + durée (7 / 30 / 90 j), génération → useComplianceStore.createLink(documentUuids, durationDays) (POST …/links avec documentUuids[]) + copie {origin}/shared/document/{token} ; liste des liens actifs affiche documentTypeLabels[] (libellé unique ou « N pièces » + tooltip) avec révocation (revokeLink). Store : état links / linksLoading / linksError, actions fetchLinks, createLink, revokeLink. API : api/compliance.jsgetComplianceLinks, createComplianceLink, revokeComplianceLink. Route publique shared-compliance-document/shared/document/:token (meta.requiresAuth: false) → views/(unauthenticated)/Compliance/SharedComplianceDocumentView.vue : GET …/public/compliance-document/{token}/info (liste documents[] si N>1) puis téléchargement via lien <a> vers le proxy GET …/public/compliance-document/{token} (fichier seul ou ZIP si plusieurs pièces).
  • Cloche conformité (FEAT-239) : BellDropdown.vue — 2 onglets « À faire / Récents » (useMessagesStore : bellTodoNotifications, bellRecentNotifications, actionableUnreadCount) ; i18n conformité via utils/notificationDisplay.js (codes titleCode/bodyCode ou title/body API, indépendamment de type — le DiscriminatorMap peut omettre document_expiry) ; sticky J+0 ; « Mettre à jour » → trainer-documents?renew=<type> (consommation FEAT-240). Inbox /trainer/messages : mêmes helpers dans NotificationItem / NotificationDetail (libellés FR + CTA « Voir mes documents »).
  • Facturation (paramètres) : trainer-billing-settingsviews/trainer/BillingSettingsView.vue (/trainer/settings/billing) — 4 onglets (Configuration, Personnalisation, Paiements, Annexes), accessibles (role="tablist" / tab / tabpanel, navigation clavier) ; même rendu visuel que TrainerSettingsSubnav (fond bg-white, bordure border-mushroom-200, shadow-sm, inactif text-grey-600, actif text-corail-600 + trait corail inset). useTrainerSettingsHeader sans navItems. Données : trainerAPI.getBillingSettings / updateBillingSettings (facturation, mentions, email, logo), getCompany / updateCompany (identité + bancaire) ; skeleton + message d’erreur de chargement sur l’onglet Configuration. Deep-link FEAT-096 : route.query.tab est synchronisé vers activeTab (whitelist des IDs d’onglets), ce qui permet les entrées directes de type trainer-billing?tab=personnalisation. Garde changement d’onglet : si l’onglet courant (Configuration ou Personnalisation) a des modifications non enregistrées, UnsavedLeaveModal avant de quitter l’onglet (switchTab). Onglet Personnalisation : logo PNG/JPG/GIF/WebP max 2 Mo (pas SVG), couleur accent, taille de police en fieldset + role="radiogroup" + radios sr-only ; dirty design = couleur + police uniquement (pas le logo, aperçu local). Mentions légales : bouton Enregistrer les mentions (plus d’autosave debounced). Paiements : IBAN/BIC issus de company, lien Entreprise avec #bancaire. Annexes : liste via getBillingDocuments (métadonnées facturation), états chargement / vide / erreur, suppression via ConfirmDangerModal, fichier max 5 Mo dans la modale.
  • Éditeur de modèle de document : trainer-billing-template-editorviews/trainer/DocumentTemplateEditorView.vue (/trainer/template-editor/:uuid selon route active) — meta.hideTrainerNav : pas de sidebar ni bandeau global TrainerLayout ; barre locale Retourtrainer-billing-settings. Page deux colonnes (config + aperçu HTML serveur). Facture : colonnes réordonnables au drag ; CRA : six colonnes sans drag (dont Source, libellé modifiable, colonne verrouillée). Chargement via GET /trainer/billing/template/{uuid}.
  • En-tête commun Paramètres : le bandeau retour + titre + description est rendu dans layouts/TrainerLayout.vue (gouttière horizontale asymétrique : marge gauche serrée pl-3 sm:pl-4 lg:pl-5, droite pr-4 sm:pr-5 lg:pr-6 ; padding vertical py-5 en mode lien retour injecté, sinon hauteur fluide UX-359 min-h-14 py-2 sm:min-h-16 sm:py-1 ; bloc titre retour + titre + description en space-y-1.5, titre text-xl leading-tight truncate, retour text-xs + min-h-11 lg:min-h-0 + text-grey-500 + icône ChevronLeft (lucide-vue-next), description hidden sm:block truncate text-sm text-grey-500 + leading-snug). Les vues sous /trainer/settings/* appellent composables/useTrainerSettingsHeader.js pour remplir settingsHeaderState (inject). La remise à zéro se fait dans TrainerLayout lorsque la route quitte /trainer/settings, pas au onUnmounted de chaque vue (sinon navigation entre sous-routes efface le header). La barre d’ancres est le composant components/Trainer/TrainerSettingsSubnav.vue (monté dans TrainerLayout.vue pour toutes les routes sous /trainer/settings/* qui définissent navItems). Typo maquette : text-sm + leading-5 + font-semibold, onglet actif text-corail-600, inactif text-grey-link (#808080, maquette rgb(128 128 128)) ; p-2.5 symétrique, items-stretch, boutons sans bordure (border-0, box-border) ; l’onglet actif utilise un trait corail en box-shadow inset (inset 0 -3px 0 0 corail-500), pas de border sur les boutons. sticky top-0 sous le bandeau global ; les clics émettent select puis scrollIntoView({ behavior: 'smooth' }) (le layout met à jour l’onglet actif tout de suite) et le routeur applique la même logique pour les URLs avec #hash sur les routes paramètres. Le suivi au défilement utilise useTrainerSettingsScrollSpy.js (pas d’IntersectionObserver avec rootMargin en pourcentage) : scroll / resize sur le conteneur principal, ligne de référence = bas du nav[aria-label="Sections de la page"] (getBoundingClientRect() par rapport au conteneur scroll). Le contenu des pages paramètres reprend une gouttière horizontale cohérente dans les vues (px-4 sm:px-5 lg:px-6 ou alignée sur le layout).
  • Les routes settings/profile, settings/company, settings/notifications sont branchées ; les tuiles du hub pointent vers ces chemins.
  • Sidebar formateur : « Paramètres » (trainer-settings) et « Paramètres facturation » (/trainer/settings/billing) ; l’entrée unique remplace les anciens liens directs « Mon entreprise » et « Mon compte » pour le hub ; /trainer/company redirige vers Entreprise paramètres ; /trainer/account redirige vers le profil paramètres.
  • Mes demandes (formateur, FEAT-054 / DISC-012, palette FEAT-058, UX-046, FEAT-081) : views/trainer/RequestsView.vuefond : layouts/TrainerLayout.vue applique bg-mushroom-100 sur le <main> / conteneur scroll pour /trainer/requests (isRequestsRoute, aligné maquette HTML body). Liste en cartes (5 onglets + compteurs, recherche mushroom-50, tri « ghost » ; états vides icône + texte par panneau uniquement quand l’onglet dédié est actif, pas sous « Toutes »), section « Échéance proche » (bg-orange-50) si tri « Expire bientôt », états chargement / erreur / vide / succès, raccourcis clavier J·K·Entrée·Échap·? (overlay aide max-w-sm) et Ctrl+K → CommandPalette.vue (raccourcis page, actions rapides, navigation vue-router), useMultipleModals pour la pile Échap ; onglets aria-controls vers panneaux id="panel-*" (onglet « Toutes » : liste d’ids). Modales AcceptModal / RefuseModal avec useFocusTrap ; détail via RequestDrawer.vue (FEAT-055) : panneau 500px, Teleport body, animation spring, ombre progressive, piège focus, verrouillage scroll, inert sur #app > :first-child, SlotCard + MiniCalendarWeek, fil messages (getBatchMessages / sendBatchMessage), événements @accept / @refuse (créneaux + batch). Marquage lu : trainerAPI.markBatchViewed uniquement dans openDetail (clic carte ou ouverture équivalente : Entrée, palette) si firstViewAt === null, puis fetchBatches et resynchronisation de selectedBatch pour retirer l’état « non lu » sans double PATCH côté drawer. Composants : RequestCard.vue (3 zones, variantes NEW non lu, acceptée / refusée / annulée / pourvue FULFILLED ; pas d’icône / badge messages sur la carte — UX-066 / DISC-012, fil dans RequestDrawer uniquement), AcceptModal.vue, RefuseModal.vue (listbox ARIA, 8 motifs). Deep-link messagerie → demandes (FEAT-081) : ConversationDetail.vue émet ?highlight={batchUuid} ; après fetchBatches, useCrossPageHighlight().run() active l’onglet « Toutes » si seul highlight (sans ?status=) pour que la carte soit au DOM ; ?status= whitelist (all | pending | …) ; cartes avec data-request-id ; URL nettoyée (router.replace). CSS global assets/main.css : .is-highlighted + @keyframes highlight-card.
  • Mes demandes (école / staff, UX-046, FEAT-081) : views/staff/RequestsView.vueuseCrossPageHighlight après fetchRequests (même params query) ; lignes <tr> avec data-request-id (liste plate et vue groupée par formateur). Modales relance et annulation — useFocusTrap sur chaque role="dialog" ; useMultipleModals pour Échap (priorité à la modale la plus « haute » dans la pile déclarée, fermeture bloquée si close* court-circuite pendant loading).
  • Demandes / drawer formateur (briques UI) : components/Trainer/RequestDrawer.vue (FEAT-055) — panneau 500px (animation spring, ombre progressive, piège à focus, verrouillage scroll, inert sur #app > :first-child) : en-tête école / contact / urgence / mail·téléphone·fermer, bannières statut, SlotCard, MiniCalendarWeek, formation, message école, fil messages (trainerAPI.getBatchMessages avec loader Loader2 + aria-busy, saisie désactivée pendant le fetch ; sendBatchMessage), métadonnées repliées, pied conditionnel pending ; props request, open ; événements close, accept, refuse (le marquage « vu » est géré par la vue liste, pas par le drawer). components/Trainer/SlotCard.vue — carte créneau (bordure gauche accent selon statut local puis disponibilité, conflit inline, actions accepter/refuser si batch + slot en pending, libellés Accepté / Refusé / Annulé). components/Trainer/MiniCalendarWeek.vue — mini-agenda semaine (5 colonnes Lun–Ven × plage 9h–17h en 8 lignes), replié par défaut, lecture seule ; props calendarDays, calendarCols, weekLabel. Les en-têtes de colonnes sont fournis par RequestDrawer.vue : « Lun 12 », « Mar 13 », … (numéros du lundi au vendredi de la semaine du créneau d’ancrage) ou « Lun » … « Ven » sans numéro si pas d’ancre. Si calendarCols[].segments est défini (API weekEvents avec isMainEvent / isBatchEvent), blocs de couleur uniquement (pas de texte dans la grille) ; libellés détaillés en :title au survol ; segments pleins : agenda gris, demande sans conflit bleue, avec conflit rouge ; conflit uniquement en demi-colonnes (agenda gauche, demande droite). Sinon mode legacy events + slots. Calcul des segments dans RequestDrawer.vue (buildSegmentsForDay).

🛡️ Guards de navigation

// router/index.js
router.beforeEach(async (to, from, next) => {
  const auth = useAuthStore()
  const onboardingGuard = useOnboardingGuard()

  // Vérification de l'authentification
  if (to.meta.requiresAuth && !auth.isAuthenticated) {
    return next({ name: 'signin' })
  }

  // Vérification de l'onboarding
  if (to.meta.requiresOnboardingValidation) {
    const validation = onboardingGuard.validateAccess(to.path)
    if (!validation.allowed) {
      return next({ path: validation.redirectTo })
    }
  }

  next()
})

🔒 Sécurité et authentification {#securite-et-authentification}

🔐 Flux d'authentification

sequenceDiagram
    participant U as User
    participant F as Frontend
    participant B as Backend
    participant O as OAuth Provider

    U->>F: Click Login
    F->>O: Redirect to OAuth
    O->>U: OAuth Consent
    U->>O: Authorize
    O->>F: Redirect with Code
    F->>B: Exchange Code for Token
    B->>F: Return JWT Token
    F->>F: Store Token
    F->>F: Redirect to Dashboard

    Note over F: Token refresh flow
    F->>B: API Request
    B->>F: 401 Unauthorized
    F->>B: Refresh Token
    B->>F: New JWT Token
    F->>B: Retry Request
    B->>F: Success Response

🛡️ Sécurité des tokens

// services/(auth)/tokenService.js
class TokenService {
  static storeToken(token) {
    // Stockage sécurisé en sessionStorage
    sessionStorage.setItem('auth_token', token)
  }

  static getToken() {
    return sessionStorage.getItem('auth_token')
  }

  static removeToken() {
    sessionStorage.removeItem('auth_token')
  }

  static isTokenExpired(token) {
    if (!token) return true
    const payload = JSON.parse(atob(token.split('.')[1]))
    return payload.exp * 1000 < Date.now()
  }
}

🌍 Internationalisation

🗣️ Configuration i18n

// i18n/index.js
import { createI18n } from 'vue-i18n'
import fr from './locales/fr.js'
import en from './locales/en.js'

export default createI18n({
  locale: 'fr',
  fallbackLocale: 'en',
  messages: { fr, en }
})

📝 Structure des traductions

// i18n/locales/fr.js
export default {
  common: {
    save: 'Enregistrer',
    cancel: 'Annuler',
    loading: 'Chargement...',
    error: 'Erreur',
    success: 'Succès'
  },
  auth: {
    login: 'Connexion',
    logout: 'Déconnexion',
    email: 'Email',
    password: 'Mot de passe'
  },
  dashboard: {
    title: 'Tableau de bord',
    welcome: 'Bienvenue'
  }
}

🧪 Tests et qualité {#tests-et-qualite}

Contraste WCAG — text-orange-600 (UX-261)

  • Corpus texte : tout texte lisible sur fond clair utilise text-orange-700 minimum (ratio AA ≥ 4,5:1).
  • Hors corpus (conservé volontairement) : icônes décoratives seules aria-hidden="true"SampleDataBanner, PreAvertBanner, PlanningView (icône alerte), ConfigureOrganizationView (icône org) : text-orange-600 aligné maquettes dashboard (maquette-dashboard-intervenant.html, bandeaux info). Le texte adjacent reste en -700/-800.

Alignement maquettes DISC-027 (revue structurelle)

Comparaison teadle-docs/DISC-027/05 Maquettes/ ↔ implémentation (2026-07-30, passe pixel-perfect) :

Surface Maquette Implémentation Statut
Coffre-fort formateur maquette-coffre-fort-intervenant.html CoffreFortView.vue, ComplianceFiltersBar.vue, ComplianceDetailDrawer.vue, HonorabilityCard.vue OK — pills py-2 + touch 44 px, badge conforme text-grey-600, colonne Expire + « Sans expiration », kebab/export/NDA touch, drawer mobile bottom-sheet + preview max-h-40, modale fiche bg-black/40 sans dot
Hub intervenants org maquette-intervenants-ecole.html IntervenantsView.vue, StaffLayout.vue, IntervenantsHeaderActions.vue OK — titre « Mes intervenants », sous-titre maquette, header « Consulter les factures » + « Associer » pill corail, bandeau départ pleine largeur, skeleton 2 blocs, empty/error états maquette, onglets mb-6, checkbox masquée onglet Anciens, table min-w-[560px], actions gap-3, pills py-2, message planning expiré retiré (EXCLU I14)
Drawer org @section:intervenant-drawer IntervenantComplianceDrawer.vue OK — sous-titre parti, table min-w-[440px], footer gap-3, « Demander une pièce » touch 44 px
Cloche @section:notification-bell BellDropdown.vue OK — trigger min-h-11 min-w-11 lg:min-h-0
Chrome org Paramètres sidebar maquette StaffLayout.vue + SettingsView.vue OK — lien actif, page « Bientôt disponible »

Hors scope volontaire : sidebar mobile org, gate limite Q2 (popover vs bandeau orange legacy showLimitWarning).

Revue pixel-perfect complète = validation visuelle navigateur (spacing exact, états demo switcher) — non automatisable ; les TF couvrent structure ARIA et comportement.

Tests Vitest (Unit + Functional)

Politique issue / PR : n’ajouter ou modifier que les tests du code touché (TU collé au fichier, TF miroir vue). Rule Cursor : .cursor/rules/frontend-tests-keeper.mdc ; sous-agent /frontend-tests-keeper. E2E Cypress : CI smoke post-MEP uniquement — pas en fin d’issue (composant souvent livré avant la vue consommatrice).

Suite Emplacement Commande
Unit (TU) src/**/*.spec.js — composants, stores, utils npm run test:unit
Functional (TF) tests/functional/ — vues, parcours (API mockée via vi.mock) npm run test:functional
  • Config : vitest.config.jsjsdom, Allure via allure-vitest.
  • Unit : @vue/test-utils, pas d’appel réseau (props + emits). Ex. SlotCard.spec.js.
  • Functional : montage de vues + router Pinia + mocks @/api/*. Ex. tests/functional/trainer/DashboardView.spec.js. Auth : SigninView.spec.js collé dans src/views/(unauthenticated)/Auth/ (FEAT-178).
  • Unit : fichiers *.spec.js dans src/ (ex. components/ui/BaseSelect.spec.js).
  • E2E Cypress (cypress/e2e/smoke/) : smoke read-only sur https://app.teadle.com, job CI e2e-smoke après deploy-clever (secrets E2E_TRAINER_*, E2E_STAFF_*). Specs rangés par zone : smoke/trainer/, smoke/staff/, racine (publiques). Rapport Allure : parentSuite E2E → suites Trainer / Staff (sous-suite = spec) et specs racine directement sous E2E. Voir cypress/README.md.

🎯 Stratégie de tests

graph LR
    A[Tests Unitaires] --> B[Composants]
    A --> C[Services]
    A --> D[Utils]

    E[Tests d'intégration] --> F[API Calls]
    E --> G[Store Actions]
    E --> H[Router Guards]

    I[Tests E2E] --> J[User Flows]
    I --> K[Critical Paths]
    I --> L[Cross-browser]

    style A fill:#ff6b6b
    style E fill:#4ecdc4
    style I fill:#45b7d1

📊 Métriques de qualité

  • Couverture de code : > 80%
  • Complexité cyclomatique : < 10
  • Maintenabilité : A
  • Performance : Lighthouse > 90

🔧 Outils de qualité

// Configuration des outils
{
  "eslint": {
    "extends": ["@vue/eslint-config-prettier"],
    "rules": {
      "no-console": "warn",
      "no-unused-vars": "error",
      "prefer-const": "error"
    }
  },
  "prettier": {
    "semi": false,
    "singleQuote": true,
    "tabWidth": 2
  }
}

🚀 Déploiement {#deploiement}

🔄 Pipeline CI/CD

graph LR
    A[Code Push] --> B[Lint & Test]
    B --> C[Build]
    C --> D[Deploy Staging]
    D --> E[Manual Review]
    E --> F[Deploy Production]

    style A fill:#42b883
    style B fill:#ff6b6b
    style C fill:#4ecdc4
    style F fill:#45b7d1

📦 Scripts de build

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test:unit": "vitest run src",
    "test:functional": "vitest run tests/functional",
    "test:e2e": "cypress run"
  }
}

📈 Améliorations récentes

🧹 Nettoyage du code

  1. Suppression des console.log - Remplacement par un logger centralisé
  2. Organisation des composants - Structure modulaire claire
  3. Amélioration des services - Respect des principes SOLID
  4. Configuration ESLint - Règles de qualité strictes

🏗️ Architecture SOLID

  • S - Single Responsibility : Chaque service a une responsabilité unique
  • O - Open/Closed : Extension sans modification
  • L - Liskov Substitution : Interfaces cohérentes
  • I - Interface Segregation : Interfaces spécifiques
  • D - Dependency Inversion : Dépendances abstraites

🔧 Utilitaires ajoutés

  • logger.js - Logging centralisé et configurable
  • .eslintrc.js - Configuration de qualité du code
  • Documentation complète avec schémas Mermaid

🎯 Prochaines étapes

  1. Tests automatisés - Augmenter la couverture de tests
  2. Performance - Optimisation des bundles et lazy loading
  3. Accessibilité - Amélioration de l'accessibilité WCAG
  4. PWA - Transformation en Progressive Web App
  5. Monitoring - Intégration d'outils de monitoring

Documentation mise à jour le : ${new Date().toLocaleDateString('fr-FR')}