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
  • 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 h-16 / border-mushroom-250 / typo text-xl + sous-titre 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

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 ; `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, FEAT-136) : Tableau de bord → Démarrer → Mon planning → Mes demandes → Organisations (SidebarSyncDot) → Messages (badge unread) → Mes factures → 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.

🔧 Composants principaux

Composants d'authentification

  • OAuthButtons.vue - Boutons de connexion OAuth
  • PasswordResetModal.vue - Modal de réinitialisation de mot de passe
  • 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).
  • 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.

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) — toolbar persistante : nav période (ChevronLeft / ChevronRight, label aria-live="polite", « Aujourd'hui »), segmented switcher role="group" + aria-current (Jour / Semaine / Mois) ; emits update:view, prev, next, today.
  • views/trainer/PlanningView.vue (FEAT-135 + FEAT-137 + FEAT-141) — rewrite Phase 1 DISC-023 + header actions + drawer partages quick-access (useFocusTrap, liens vers ShareCalendarView) ; layout h-screen ; grilles DS (FEAT-135) ; cloche inbox reste dans TrainerLayout uniquement.
  • utils/planningEmptyPeriod.js (FEAT-135) — libellé état vide selon vue DISC-023 (jour | semaine | mois, plus les types FullCalendar).
  • PlanningCalendar.vue — FullCalendar partagé (hors PlanningView formateur depuis FEAT-135) : SharedPlanningView, IntervenantCalendarView, IntervenantsCalendarsView. Dépendances @fullcalendar/* conservées tant que ces vues consomment le wrapper. Événements type: personal : côté formateur (nouvelle vue DS), filtre « Agenda personnel » via __personal_calendar__ + CalendarEventBlock (hatch indispo) ; côté public / staff (FullCalendar), extendedProps.isPersonalBusy uniquement (libellés génériques, style fc-event-busy-public). Convention scroll vues publiques pleine page : h-screen flex flex-col + zone flex-1 overflow-y-auto (ex. SharedPlanningView.vue).
  • 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) - BLOC 3 : 4 KPI avec count-up, tooltips, panels expand/collapse, donut SVG heures et breakdowns écoles. 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

  • OnboardingLayout.vue - Coquille onboarding avec scroll interne : wrapper h-screen flex flex-col + zone de contenu flex-1 overflow-y-auto pour garantir le défilement des étapes même quand body est en overflow: hidden (modales / overlays).
  • 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) — DialogRoot / DialogPortal / DialogOverlay / DialogContent (reka-ui) : props open, size (sm | md | lg), @update:open, overlay bg-black/50 backdrop-blur-sm, panneau centré max-w-md, animation scale-in 200 ms (désactivée si prefers-reduced-motion), clic overlay ferme ; piège à focus et aria-modal gérés par le primitive.
  • 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) — case à cocher native : v-model booléen ou tableau (value en mode groupe), accentColor (style inline, ex. couleur calendrier), mode contrôlé :model-value + @update:model-value ; CVA checkboxVariants ; libellé porté par le <label> consommateur.
  • 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) — listbox Reka UI (SelectRoot + SelectPortal + position="popper" + avoidCollisions) : v-model string, options (String[] ou { value, label }[]), placeholder, ariaLabel ; CVA selectTriggerVariants / selectContentVariants / selectItemVariants ; téléport body (modales), chevron SVG inline group-data-[state=open]:rotate-180, clavier/typeahead Reka.
  • components/ui/BaseAlert.vue (FEAT-131) — callout role="note" | status : variantes info | success | warning | error (CVA alertVariants + alertIconColors / alertTextColors), icône Lucide par défaut ou :icon, slot #title + défaut, dismissible + @close, override class (ex. banner compact Planning).
  • 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/BaseInput.vue (FEAT-084) — champ texte natif dans .input-wrap : v-model, type (dont time), id, disabled, class ; utilisé par AvailabilityView absences plage horaire (FEAT-139).
  • 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) — section everboarding progressive : grille responsive 2x2, liens internes vers facturation (trainer-billing?tab=personnalisation), messages (trainer-messages) et dashboard (trainer-dashboard) + carte « Bientot » non interactive (aria-disabled) et 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
  • 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 ; 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. 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

🏪 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).
  • 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) — une page à six ancres (Photo, Identité, 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).
  • 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-4 sur les routes paramètres, py-5 ailleurs ; bloc titre retour + titre + description en space-y-1.5, titre text-xl leading-tight, retour text-xs + text-grey-500 + icône ChevronLeft (lucide-vue-next), description 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

🔐 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 unitaires (Vitest)

  • Commandes : npm run test:unit (CI / une passe), npm run test:unit:watch (développement).
  • Config : vite.config.jstest.environment: 'jsdom', fichiers src/**/*.{spec,test}.js.
  • Stack : Vitest, @vue/test-utils, sans fetch dans les composants testés (props + emits). Exemple : components/Trainer/SlotCard.spec.js, MiniCalendarWeek.spec.js.

🎯 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

🔄 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",
    "test:unit:watch": "vitest",
    "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')}