🏗️ Architecture Frontend Vue.js - Gryly
📋 Table des matières
- Vue d'ensemble
- Architecture générale
- Structure des composants
- Gestion d'état
- Services et API
- Routing et navigation
- Sécurité et authentification
- Internationalisation
- Tests et qualité
- 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:corail50–900,mushroom50–900+250,grey50–900, ainsi queblue/green/orange/reden échelles complètes (tokenstokens.css, sans override dupliqué) pour badges, avatars et états ;togglepour l’état activé des interrupteurs —rgb(13 148 136);dirtyBarpour la bande champ modifié). Pas de couleurs legacyprimary/secondaryVue.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 staffbg-mushroom-100, sidebarborder-mushroom-250, logo « T » et badge compteurbg-corail-500, item actifbg-corail-50 text-corail-600, en-tête fluidemin-h-14 py-2 sm:min-h-16(UX-359, hamburger 44 px hors du bloc titre, titretruncate) /border-mushroom-250/ typotext-xl+ sous-titrehidden 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 baseaveccursor: pointersurbutton:enabledet[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: nonesur SVG dansbutton/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¬ification=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 :variantfull(défaut, logo + copyright + CTA LinkedIn brand-color) |utility(pre-auth DISC-024 : lien discrettext-grey-700/hover:text-blue-500, sans logo ni copyright). Tests :Footer.spec.js.OAuthButtons.vue- Boutons OAuth (LinkedIn, Google, Microsoft) ; propvariant:legacy(défaut, vues signup) |ds(SigninView DISC-024) ; emitsprovider-selected,error(erreur interne affichée uniquement enlegacy)SigninView.vue(FEAT-178) — page/signin: Magic Link primary + password fallback + OAuth ; state machine 10 états ; modal reset inline viaBaseModal+useModalFocusReturn(suppressionPasswordResetModal.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. Propsbare,compact. Respecteprefers-reduced-motion(WCAG 2.2) viausePrefersReducedMotion. 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.vueet pattern DS feedback-skeleton-loader) ; animations pilotées en JS →usePrefersReducedMotion. Filet globalprefers-reduced-motiondansassets/main.cssconservé en défense-en-profondeur (commentaire de convention). Spinnersanimate-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,!importantpour primer surfocus:outline-nonelegacy). Composants DS login (BaseButton,BaseInputtone corail,BaseSelectcorail,MagicLinkStatus, toggle password) : ringscorail-500retirés (outline global suffit)..compact-control: idem. Override.is-highlighted:focus-visible. Cleanup massiffocus:ring-corail-500dans vues métier → issue FIX future ;scroll-padding-top: 4remdéjà surhtml(WCAG 2.4.12). ConfirmDangerModal.vue— Confirmation destructive (suppression) :Teleportversbody, icône corbeille (Trash2, lucide) sur fondbg-red-50, titre + message, Annuler (bordure) / Supprimer (bg-red-500).v-model+ événementsconfirm/cancel. Remplacewindow.confirmpour ce cas d’usage. Exemple : suppression d’un modèle de document dansBillingSettingsView.vue.HelpTooltip.vue— Infobulle d’aide contextuelle : bouton icôneCircleHelp(lucide-vue-next), bulle sombre au survol et au focus. Props :text(obligatoire),placement(top|bottom, défauttop). 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 secondairesUrgencyBadge.vue(FEAT-052 / DISC-012) — badge urgence demandes de réservation : propssecondsBeforeExpiration,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é danscomponents/Trainer/RequestCard.vue(page formateur « Mes demandes ») etviews/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 ; propsmessage,visible,icon(check-circle,x-circle,send,info,bell),iconClass. Branché surviews/trainer/RequestsView.vue(accept / refus / erreurs) etviews/staff/RequestsView.vue(relance, annulation, copie email, erreur d’envoi message).CommandPalette.vue(FEAT-058 / DISC-012) — palette de commandesCtrl+K/Cmd+K:Teleport→body, overlayz-[80], recherche filtrante, flèches + Entrée, ARIA combobox / listbox / options ; propsitems(sections{ title, options[] }avecid,label,iconLucide,kbd,subtitle,route…), événementsselect/close. Intégrée àviews/trainer/RequestsView.vue(12 commandes, 3 sections) etviews/staff/RequestsView.vue(9 commandes, 2 sections + listener globalkeydownavec 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 ;useFocusTrappour le focus dans le dialog + restauration ; Échap →close. Propsopen,request,messages,loading,userInitials,userName, fonctions de badge statut ; événementsclose,send.
Composants de formulaires ((forms)/)
PhoneInput.vue- Téléphone internationalProgressiveEmailInput.vue- Email avec validation progressive (message après blur, puis correction en direct), bordures d’état, librairievalidator(isEmail) centralisée danssrc/utils/email.js(isValidEmail). Props utiles :variant(default= focus rouge formateur,blue= onboarding),external-error(erreur imposée au submit),show-all-errors, slotsuffix(icônes). Événements :blur-valid,input-valid. Utilisé notamment parContactSidebar.vue,StaffInvitationConnectedView.vue.- Rôles contact organisation (FEAT-248) :
GET /trainer/organization/contact/roles→contactAPI.getContactRoles()(tableau deROLE_*) ; cache module-leveluseContactRoles+ traduction i18n frontconstants/contactRoles.js. Consommé parContactSidebar.vue(permissions env-for, retry si échec) etConfigureOrganizationView.vue(chips, filtres incl. Conformité).
Composants de tableau de bord
WeekSchedule.vue- Planning hebdomadairecomponents/Trainer/planning/CalendarWeekGrid.vue(FEAT-132) — grille semaine DS (patterncontainer-calendar-week-grid) : en-têtes 7 jours (today corail, weekend hatch), time-gutter 56px, lignesh-14, off-hours/pause, now-indicator (pulse,setInterval60s), slot scoped#day(day,dayIndex,hourHeight,hours),defineExpose({ HOUR_HEIGHT_PX: 56 }). Branché parPlanningView.vue(FEAT-135) via slot#day+CalendarEventBlockpositionné pareventStyle()(algo lanes R14).components/Trainer/planning/CalendarEventBlock.vue(FEAT-133) — bloc événement vue Semaine (position absolue via:styleparent) : R3 past 50 %, R4 indispo hatch, R5 couleur orgborder-left, R6 conflit +AlertTriangle, R10 formation si durée ≥ 2 h ; emitsclick/hover/clear-hover.components/Trainer/planning/CalendarMonthGrid.vue(FEAT-133) — grille mois 7×N : pills (max 2) + « +N autres », today/hors-mois ; emitsevent-click/day-click.components/Trainer/planning/CalendarDayAgenda.vue(FEAT-133) — liste agenda vue Jour : rows horaire corail + détails + chevron ;emptyMessage; emitevent-click.components/Trainer/planning/CalendarFilterBar.vue(FEAT-134) — filtres calendriers :BaseCheckbox+ accent dynamique, pastille couleur, compteur, « + Ajouter un calendrier » ; emitstoggle-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 contratrange-start/anchor-date.components/Trainer/planning/CalendarLegend.vue(FEAT-134) — légende 4 statuts (dots + hatch indispo),itemsconfigurable.components/Trainer/planning/CalendarToolbar.vue(FEAT-135 / UX-363) — toolbar persistante : nav période, segmented switcher ; props opt-inshowDateButton+dateSheetOpen, emitopen-date(bouton « Choisir une date » sous lg).components/Trainer/planning/CalendarFilterBar.vue(UX-363) — chips calendriers : rangee defilanteoverflow-x-auto snap-xsous lg, retour a la ligne a partir de lg ;p-1 -m-1pour anneaux de focus non rognes.components/Trainer/planning/PlanningMiniCalendar.vue(UX-363) — prop opt-intouchTargets(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, feuilleBaseModal(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.vue— supprimé en FEAT-162 (cleanup DISC-023) après migration complète des vues planning versCalendarWeekGrid/CalendarMonthGrid/CalendarDayAgenda.views/(unauthenticated)/Planning/SharedPlanningView.vue(FEAT-153 + FEAT-154) — vue publique planning en layoutmin-h-screen flex flex-col(header DS + zone scrollflex-1 overflow-y-auto) migrée surCalendarWeekGridpour le pattern drag-to-create : drag layer pointer events (touch-action: nonelocal), ghost persistant valid/invalid, alternative clavier (tabindex+Enter/Space), bouton mobile44x44,aria-live="polite"pendant drag, ouverture wizard pré-rempli viahandleBookingRequest.views/(unauthenticated)/Planning/SharedPlanningView.vue(FEAT-155 / UX-360) — extension dual-zone draft : bouton header secondaireMa demande(badge dotring-2 ring-white) qui redirige vers le bandeau de reprise, bandeau primaire (bg-mushroom-50) avec CTAContinuer ma demande/Annuler, sticky sentinel (useStickyBannerSentinelavecroot: pageScrollRef) pour passage inline/fixed ; mode collé :top= hauteur mesurée de l'en-tête (useElementBounding) ; remontée viapageScrollRef.scrollTo; confirmation remplacement/annulation via modal destructif, et conservation d’instance wizard viav-show(reset explicite parresetWizardData).views/(unauthenticated)/Planning/SharedPlanningView.vue(FEAT-156) — info-bar compacte + popover détails (onClickOutside+ fermeture Escape viauseMultipleModals), sidebarlg+avec mini-map mensuelle et légende, et hachures cellule-par-cellule branchées viaCalendarWeekGrid:isCellAvailableen lisantavailability.rules.views/(unauthenticated)/Planning/SharedPlanningView.vue(FEAT-157) — section inline#tracking(plus de redirection externe) avec machine 4 étatsidle/link-sent/verifying/results, ancrage headerMes reservationsvers scroll local, et gestion?token=au mount avec nettoyage URL (router.replacesanstoken/email).views/(unauthenticated)/Planning/SharedPlanningView.vue(alignement maquettemaquette-planning-public-ecole (2).html) — harmonisation visuelle info-bar/tracking vers le schémablue-*(callout principal, popover actions, steplink-sent), ajout de la toolbar période (prev/next/today) + switcherJour/Semaine/Mois, rendu multi-vues (CalendarWeekGrid/CalendarMonthGrid/CalendarDayAgenda) et ajout de la carte sidebarA propos.views/staff/IntervenantCalendarView.vue(FEAT-160) — vue staff interne modernisée DS (bg-mushroom-100, actionsBaseButton, erreursBaseAlert, icônes Lucide), sansPlanningCalendarlegacy. Rendu multi-vuesday/week/monthviaCalendarDayAgenda,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 (palettegrey/mushroom/corail,BaseButton/BaseInput/BaseCheckbox/BaseAlert, icônes Lucide), migration complète dePlanningCalendarversCalendarWeekGrid/CalendarMonthGrid/CalendarDayAgenda, toolbar unifiée (nav période + switcherJour/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.mapIntervenantRowdérivepresence/departed/pauseddepuis l’API ; ongletsBaseTabsActifs / Anciens (role="tabpanel") ; bandeau de départBaseAlert+ modaleBaseModal(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 + switcherJour/Semaine/Moisdans un blocbg-white rounded-xl, patternmaquette-planning-public-ecole), chevronsChevronLeft/ChevronRight, rechargement silencieux 5 mois via_reloadPlanningForVisibleRange(clélastPlanningRangeKeymise à jour).views/(unauthenticated)/Planning/SharedPlanningView.vue(FEAT-162) — suppression du fallback legacyBookingSidebar/showBookingModal: le flux de réservation passe uniquement parBookingWizard, ce qui retire la dette technique historique sans impact fonctionnel.components/BookingSidebar.vue— supprimé 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 versBaseInput/BaseSelect/BaseCheckbox/BaseAlert/BaseButtonet icônes Lucide, conservation de la logique métier (recherche, alternatives, validations email+téléphone E.164, idempotency). FEAT-159 ajoute le modewizardFromDragen étape 2 : card contextuelle éditable (editDate/editStartTime/editEndTime), validations inline (passé / ordre / durée min / overlap), emitsupdate:slot(sync WYSIWYG du ghost côtéSharedPlanningView) etpause-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/heureven. 29 mai 2026+12:30), étape 3 alignée sur la structure HTML de référence (text-sm/text-xs, titreSujet 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, classesbg-red-*), remplacement par DS/Lucide uniquement, radios rythme migrés en cartes-boutons (aria-pressed) et modal de succès migrée versBaseModal.components/SlotSelectionModal.vue(fix interaction 2026-05-28) — migration de la modale de sélection de créneau versBaseModalDS (au lieu d’un overlay manuelfixed) pour restaurer les interactions imbriquées depuisBookingWizard: 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 (idle→pending→dragging→released) avec seuil adaptatif (mouse/touch), throttlerequestAnimationFrame, guardhasPointerCapture()avantsetPointerCapture(), gestionpointercanceliOS Safari, exposédrag(ghost,ariaAnnounce) +onPointerDown+resetDrag.composables/useStickyBannerSentinel.ts(FEAT-155 / UX-360) — wrapperuseIntersectionObserver(VueUse) pour gérer le passage inline/sticky du bandeau draft via sentinelle ; optionroot(conteneur.page-fullscreen-scroll) pour bascule exacte sans marge en dur ; défaut viewport +rootMargin: -65pxinchangé pour autres consommateurs futurs.composables/useMagicLinkTracking.js(FEAT-157) — composable JS (JSDoc) pour le flux magic-link : envoi email (requestTrackingLink), vérification token (verifyTrackingToken→performToken+getTrackingReservations) et mapping défensifbatches -> liste reservationsviamapBatchesToFlatReservations.components/Trainer/planning/PlanningMiniCalendar.vue(FEAT-156) — extension backward-compatible : props optionnellesshowTodayButton,chevronOrientation(vertical/horizontal) etliveLabel(aria-livesur le label mois) pour réutilisation dans la vue publique sans duplication.components/Trainer/planning/CalendarWeekGrid.vue(FEAT-156) — extension backward-compatible : prop optionnelleisCellAvailable(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 :BaseModalslots#header/#footer,BaseInputdate/heure (step=1800), validation métier injectée (validate), erreurs inline viaBaseAlert, submit versSharedPlanningView.components/SharedConfirmModal.vue(FEAT-155) — modal de confirmation destructive (replace/cancel draft) basé surBaseModal(role="alertdialog"), layout centré (icône/titre/body/actions), bouton safe à gauche, bouton destructive à droite.Heatmap.vue- Carte de chaleur des activitésActions.vue- Actions rapidesIncomes.vue- Affichage des revenusOpportunities.vue- Opportunités disponiblesActiveSchools.vue- Écoles activesMySchools.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}), étatsloading/error/empty/data, accessibilité (captionsr-only,th scope, labels sr-only trend/statut). FEAT-098 : propisSample, badge itemExemple, emitsample-clicket blocage navigation facturation sur entrées sample. FIX-106 : empty state migré versEmptyStateDS avec CTA action +onboardingLink« Comment attirer des organisations ? » verstrainer-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 : propisSample(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 » : toggletoday/week, listes interventions, étatsloading/error/empty/new, émissionopen-intervention. FEAT-098 : badgeExemplepar intervention sample. FIX-106 : état vide "new user" migré versEmptyStateDS aveconboardingLink« 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 : badgeExemplepar demande sample et emitsample-click(pas d’ouverture de la modale d’acceptation réelle). FIX-106 : état vide "new user" migré versEmptyStateDS aveconboardingLink« 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. Grillegrid-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(patternkpi-card-expandable) : déclencheurmin-h-11 lg:min-h-0,aria-expanded+aria-controls, régionaria-labelledby= titre de la carte ; auto-collapse (un seul ouvert). FEAT-098 : mention en-têteDonnées d'exemplevia propisSample. FIX-106 : fallback sanssummarymigré versEmptyStateDS aveconboardingLink« 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êteDonnées d'exemplevia propisSample. FIX-106 : fallback sanssummarymigré versEmptyStateDS aveconboardingLink« 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 lientrainer-getting-startedet action "Supprimer les exemples" (appelleonboardingStore.dismissSampleData()).PreAvertBanner.vue(FEAT-097) - bandeau orange d'alerte J-2 (role="alert"), visible si sample actif etexpiresAt <= now + 2 jours, CTA "Partager mon profil" verstrainer-getting-started.FirstRealRequestBanner.vue(FEAT-097) - bandeau vert maquette (« 🎉 Votre première vraie demande… »), visible si une lignerecent-requestsaisFirst: true(backend) et flag localStorage absent (teadle_disc016_first_request_seen), CTA verstrainer-requests.SampleModal.vue(FEAT-099) - modale guardrail sample data (wrapperBaseModal) ouverte apressample-clickpour expliquer le mode exemple et proposer la redirection verstrainer-getting-started; fermeture via overlay, Escape ou CTA secondaire.EmptyState.vue(FEAT-099) - composant DS dashboard enrichi avec prop optionnelleonboardingLink({ label, to? }) pour rendre unrouter-linkcontextuel vers le guide de demarrage, sans casser l'API historique (icon,message,actionLabel, emitaction).DashboardView.vue(FEAT-098 / FEAT-099) - orchestration sample mode :isSampleMode+ mappingseffectiveSummary,effectiveInterventionsToday/Week,effectiveRecentRequests,effectiveSchoolsdepuisonboardingStore.sampleDataPayload; passageisSampleaux composants dashboard et ouverture/fermeture deSampleModalsur les evenementssample-click.
Composants d'onboarding
PublicLayout.vue(FEAT-170) — pages pre-auth (login, signup, tracking) :min-h-svh flex flex-col bg-mushroom-50, centrage viamy-auto+max-w-md(pasjustify-centerparent), slotsdefault+#footer. Remplace progressivement.page-fullscreen(legacymain.css). Tests :PublicLayout.spec.js.OnboardingLayout.vue- Coquille onboarding avec scroll interne : wrapperh-screen h-svh flex flex-col(UX-358) + zone de contenuflex-1 overflow-y-autopour garantir le défilement des étapes (legacy.page-fullscreenavecoverflow-hiddensur le conteneur).SidebarGettingStartedItem.vue(FEAT-090 / DISC-016) - Entrée menu Démarrer (icône fusée, badgeX/5corail ou coche verte si terminé) entre « Tableau de bord » et « Mon planning » quanduseOnboardingStore().showInSidebarest vrai ; lien nommétrainer-getting-started→/trainer/getting-started.TrainerLayout: si formateur etprogress === null,hydrateProgressFromAuth(); timer 5 min pourfetchProgress()(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) ; rootbg-mushroom-100. BandeauTrainerLayout: « 🚀 Démarrer ». Checklist 5 étapes (OnboardingChecklistItem×5 + séparateur « Recommandé »), ordre core puis IMPORT_CALENDAR / DEFINE_PRICING ; slot#ctastep SHARE_LINK pour FEAT-093 ; CTA « Voir mes demandes » siisComplete. FEAT-094 : modale unique « Votre profil est prêt ! » (OnboardingCelebrationModal, Reka UI viaBaseModal) à la transition 5/5 ou au chargement si déjà complet et flaglocalStorageteadle_onboarding_celebration_shownabsent ; fermeture pose le flag. FEAT-095 : le bouton « Masquer ce guide » devient un flux différé Gmail-style (pendingDismiss) avecBaseToastinline (4 s,pausable, action « Annuler ») ; le commit backend (onboardingStore.dismiss) ne part qu’à l’expiration du toast. FEAT-096 : section everboardingAllerPlusLoinSectionaffichée dèscompletedCount >= 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é,BaseCollapsepour le panneau, CTArouter-linkou slot.components/ui/BaseCollapse.vue(FEAT-102 minimal / FEAT-092) — repli hauteur grid0fr/1fr,inertsi fermé,role="region".components/ui/BaseToast.vue(FEAT-103 / FEAT-095) — toast DS :variant(success / error / warning / info),positioninline|bottom-right,durationMs(0 = pas d’auto-fermeture),role="status"/aria-live="polite", fermeture manuelle (closable) ; supportpausable(pause/reprise timer au hover et au focus), slotdefault(contenu) + slotaction(ex. bouton undo), barre de progressionrole="progressbar"mise à jour en continu.components/ui/SyncToast.vue+ToastContainer.vue(FEAT-111 / DISC-019) — wrapper métier des toasts de synchronisation agenda :SyncToastconsommeBaseToast(success 5s pausable, error persistant 0ms avec CTArouter-link),ToastContainertéléporte en bas-droite (Teleportversbody, max 3 viauseToast, stack vertical).components/ui/BaseModal.vue(FEAT-085 / FEAT-094 / FEAT-163) — wrapperreka-ui(DialogRoot/DialogPortal/DialogOverlay/DialogContent) étendu avec slots structurés#header/default/#footer, variantstype(modal|drawer) +position(center|right|left),role(dialog|alertdialog) etcloseOnOverlayClick(défauttrue). Backward compat critique : si aucun#header/#footer, le body conservep-8historique ; 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) + fallbackprefers-reduced-motion.components/ui/BaseToggle.vue(FEAT-127) — interrupteur on/off DS :v-modelbooléen,sizesm|lg(défautlg),ariaLabel,disabled,class; CVAtoggleTrackVariants/toggleKnobVariantsdanslib/variants.js+cn()(lib/utils.js) ;aria-pressed(pasrole="switch") ; track vert / mushroom + knobtranslate-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 aveclabel/slot#label(markup riche RGPD). Supportrequired,errorinline (role="alert"+aria-invalid),idauto (useId),v-modelbooléen ou tableau (valueen mode groupe),accentColorinline ; forwarding attrs/listeners viainheritAttrs: false+v-bind="$attrs"sur l’input.components/ui/BaseTabs.vue(FEAT-129) — navigationrole="tablist"/role="tab":v-model= id actif,tabs(id,label,icon?,count?), badge sicount != null; CVAtabVariants/tabCountVariants; clavier APG (flèches cycliques, Home/End, rovingtabindex). 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"(RekaSelectRoot) etmode="autocomplete"(RekaComboboxRoot). API enrichie alignéeBaseInput:label,required,hint,error,idauto (useId),ariaLabel,classsur racine (space-y-1), forwarding attrs/listeners avecinheritAttrs: false+v-bind="$attrs"versSelectTrigger/ComboboxInput. Autocomplete :v-model:query,filterFncustomisable,allowCreate, slot#empty-action(payload{ query }) + emitcreate.components/ui/BaseAlert.vue(FEAT-131 + FEAT-179) — calloutrole="note"|status|alert: variantesinfo|success|warning|error(CVAalertVariants+alertIconColors/alertTextColors), icône Lucide par défaut (AlertCirclepourerror) ou:icon, slot#title+ défaut,dismissible+@close, propariaLiveoptionnelle avec auto-dérivation (alert→assertive,status→polite), overrideclass. Tests :BaseAlert.spec.js.components/ui/BaseButton.vue(FEAT-082) — bouton natif :variantprimary(corail-500) |secondary(border mushroom-250),sizemd(h-10 rounded-full), propclassviacn/tailwind-merge; utilisé parPlanningViewheader (FEAT-137).components/ui/MagicLinkStatus.vue(FEAT-169) — suivi Magic Link 4 états :statesent|verifying|expired|error; propsemail,expiresAt(ISO →expiresInHuman),errorMessage; emitsresend,change-email; slots#sent-extra,#expired-extra; ARIArole/aria-live(status/polite vs alert/assertive) ; CVAmagicLink*Variants; CTAsBaseButton. Tests :MagicLinkStatus.spec.js.components/ui/BaseInput.vue(FEAT-084 / FEAT-165 / FEAT-168 / FEAT-180) — champ DS enrichi :v-model,type,idauto (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 viainheritAttrs: false+v-bind="$attrs"sur l’<input>.showPasswordToggle(défautfalse) : bouton eye/eye-off absolu dans.input-wrap(passwordToggleButtonVariants,pr-12,aria-label/aria-pressedFR) sitype="password".defineExpose({ focus })pour autofocus parent (ex. modale reset DISC-024).classsur racine ;inputFieldVariants+tonecorail|blue;aria-describedbyprioriseerrorsurhint. Tests :BaseInput.spec.js.- Getting started — SHARE_LINK (FEAT-093) : pas d’appel
GET /trainer/planning/share; statut d’étape dérivé decompletedSteps(GET /meviafetchProgress). SiSHARE_LINKabsent : CTA verstrainer-planning-share, « Je l’ai déjà partagé » →completeStep('SHARE_LINK');BaseToastsur 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, respectprefers-reduced-motion), titre / sous-titre RekaDialogTitle/DialogDescription, CTAs demandes + fermer ; consommeBaseModal.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 retourDatePicker.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 surBillingSettingsView/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’avoirCreditNoteEditorView.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 saisieBillingSettingsView.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 + typeINVOICEouACTIVITY_REPORT) avantPOST /trainer/billing/templatespuis redirection vers l’éditeur.DocumentTemplateEditorView.vue- Éditeur fullscreen/trainer/template-editor/:uuid(ancienne route redirigée) : API templates.documentTypeissu du modèle (INVOICEvsACTIVITY_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 deTrainerLayout, JWT requis) : après redirection Google/Outlook, litcode/state/errordans la query, appellePOST …/calendar/list(calendarAPI.oauthCalendarList), affiche le choix d’agenda (radio), puisPOST …/callback(calendarAPI.oauthCalendarCallback) avec jetons +calendarId+state; réponseProcessResponse; redirection verstrainer-organizations-configureavec?tab=connecteurs&syncProcess=<uuid>pour le suivi dansConfigureOrganizationView(comme après ajout iCal).- Re-sync calendrier (
ConfigureOrganizationView) : état « connecté » siicalUrl,hasOAuthCredentialsouproviderType === ical_file; liste connecteurs (GET/trainer/calendar/connectors/config: entrées = stratégies backend, sans libellés ; ordre + noms FR + OAuth « stub » danscalendarConnectorProviders/useTrainerCalendarConnectorsConfig; une carte iCal pour lien ou fichier — entréeical_filefiltrée ; à l’affichageical_fileest normalisé vers la carteical;allowFileUploadpourical,hyperplanning,ypareo(URL ou fichier .ics exclusifs)) chargée avant le GET configurateur (await loadConnectorProvidersFromApi) pour afficher le bon picto/nom ;providerTypenormalisé en minuscules pour matcher l’API hub. Query optionnelle?calendarConfiguratorUuid=ou?configuratorUuid=(deep-link) surcharge l’UUID renvoyé par l’organisation. Bouton Re-synchroniser →POST /trainer/calendar/configurator/{uuid}/sync(calendarAPI.sync). Bloc « Lien iCal » masqué si pas d’URL ; texte Source pour OAuth / fichier. Retour OAuth?syncProcess=:initConnectorStatene remplace pas l’étatsyncingtant queisSyncingou 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 globalTrainerLayout(commetrainer/settings/*) : titre« Nom · Configuration »viaorganizationsHeaderState+useTrainerOrganizationsHeader, sous-titre « Configurez votre organisation partenaire », badge « Organisation associée » (OrganizationConfigureHeaderBadge.vue+setHeaderActions), cloche header =BellDropdowndu layout (pattern DS 2026). Racine vueflex-1 flex flex-col overflow-hidden+ fondbg-mushroom-100; lien retour « Organisations » sous le header global, onglets horizontaux (pattern APG :role="tablist"/tabpanel, flèches, badges check ou compteur surfilteredAssignments). Contenu scrollableflex-1 overflow-y-auto p-6 bg-mushroom-100; cartes principalesbg-white rounded-xl border-mushroom-200(informations, connecteurs, zone sensible) ; onglet Tarifs en pleine largeur, les autres enmax-w-3xl mx-auto. Deep link (FEAT-035) : query?tab=informations|connecteurs|formations|tarifs|contacts|events; les anciens?section=general|ical|training|pricing|contactsrestent mappés pour compatibilité. L’onglet actif est recalculé depuis la route via unwatchavecimmediatesurquery.tab,query.sectionetparams.uuid(pas de gardeisTabEnabledà ce stade —initializeData()est async). DepuisOrganizationsView, liens nomméstrainer-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(voirtailwind.config.js). Bandes de suggestion (auto-assigné / suggestion disponible) : scores de confiance individuels (Formation : X% | Matière : Y%) viagetConfidenceDisplay(assignment)etsuggestedPrograms[].matchScoring/suggestedModules[].matchScoring; repli surassignment.confidencesi besoin. Tab Informations (FEAT-042, FEAT-048) : aligné maquettemaquette-configuration-organisation— une seule carte blanche (v-show="!loading") avec en-tête conditionnel (titres / sous-titres ajout vs édition comme l’étatorgState === 'not-associated'de la maquette) ; sans association (FEAT-048), segments Écoles & Universités / Entreprises & Centres (py-2.5comme la maquette), rechercheorganizationAPI.searchTrainerOrganizations(debounce 800 ms, min. 3 caractères), association (router.replace+assignTrainerOrganization), création (?create=true+createTrainerOrganization) ; l’ancienne pageAddOrganizationViewest retirée et/trainer/organizations/addredirige verstrainer-organizations-configure. Une fois associé, champs dans.input-wrap+ indicateur.dirty(bloc<style>non scoped dans la vue, réutilisable ailleurs) ; snapshotorigFormaprèspopulateForm/ sauvegarde / association ;informationsDirty(non-propriétaire : campus seul) ; boutons Annuler + Enregistrer toujours visibles, désactivés si pristine ;isFieldDisabledForNonOwnersur 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 })→CustomEventset-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 viacalendarAPI.oauthCalendarAuthorize(provider, organizationUuid)(réponse 200 +authorizationUrl→ redirection navigateur ; 501 si fournisseur non prêt → toast + ouverture du fallback URL), synchro initiale / fichier.icsavecproviderTypesuraddIcalLink/uploadIcalFile, re-sync et changement d’URL viasyncou nouveladdIcalLink, barre de progression globale (style « ribbing ») + six étapes alignées sur les états duProcess, étatproviderType/icalUrlissu du configurateur (initConnectorStateaprèsgetCalendarConfigurator), 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 (selectedSchoolYearnull = toutes ; défaut année courante au chargement config), filtre avec épinglage des fichesdirtyhors période ; barre d’enregistrement siformationsDirtyet onglet Formations actif : entre letablistetcontentScroll, 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, boutonspy-1.5) ; le scroll de la liste Formations est uniquement danscontentScroll(flex-1 overflow-y-auto), la vue remplit la colonne viaTrainerLayout(router-viewrendu dans<component class="flex min-h-0 flex-1 flex-col">sousTrainerSettingsSubnavshrink-0) (snapshots_orig*+checkItemDirty,cancelFormations, événementset-unsavedsectionformations) ; champs eninput-wrap+ classedirty; Formation / Matière :SelectWithCreatedans.input-wrap.input-wrap--dropdown(assets/main.css,overflow: visiblepour ne pas clipper le panneau) et carte cours enrelative z-0 focus-within:z-20pour empiler la liste au-dessus de la carte suivante ; grille des quatre champsgrid-cols-1 sm:grid-cols-4; interrupteur actif/inactif (boutonrole="switch") ; modale « Valider le groupe » (openValidateGroupModal/confirmValidateGroup, détection niveauextractLevel) ; bannières transfert de période en tête de liste (transferBannerList) ; empty state + skeleton ; échec save (assignPmo) : toast fixe bas d’écran (Teleport→body,raiseFormationsSaveError/clearFormationsSaveError, fermeture manuelle + auto 12 s, sanserrorglobal) ; bannière verte lorsque toutes les clés actives sont associées. Tab Tarifs (FEAT-045) : aligné maquettemaquette-configuration-organisation (4).html(tab Tarifs) ; pleine largeur ; skeleton / empty / table (matière, période, tarif/hinput-wrap+ dirty, badges) ; pillspy-1.5,selectedSchoolYearpartagé avec Formations ; bandeau prix défaut (€ /h) alimenté pardefaultPrice(GET /trainer/calendar/configurator/{uuid}) + lientrainer-billing-settings(/trainer/settings/billing) ; alerte si!selectedProgram || !selectedModuledans le filtre ; champ prix désactivé tant que formation ou matière manquante ; barre Annuler + Enregistrer uniquement sitarifDirty(transition opacity) ;saveTarifs/ toast /set-unsaved/calendarAPI.setPricessans reload après succès. Tab Contacts (FEAT-046) : skeleton (chargement page global), empty, erreurcontactsLoadError+ réessailoadContacts, en-tête + recherche (mushroom-50/ focus corail), pills, cartesrounded-xl, avatarsgetColorForId(uuid)+getInitials(useAvatarColor),TransitionGroupsur les cartes, toast invitation + modale 403 (Teleportbody) ; CRUD dansContactSidebar.vue. Tab Événements : à droite de Contacts ; actif dès association (comme Contacts) ;OrganizationEventsPanelchargeGET /trainer/organization/{uuid}/eventsuniquement au focus d’onglet (pas à l’init page) ; tableau type Factures (sans facettes) Date / Horaires / Intitulé / Provenance (Connecteur|Demande) ; skeletonvue3-skeleton; drawer lecture seuleOrganizationEventDrawer(lien planning, lien demande sirequestId). FEAT-047 :_dirtySections(Setréassigné pour la réactivité) alimenté parCustomEventset-unsaved(listener dès lesetup, retrait auonUnmounted) ; garde surswitchTab/navigateTab(_doSwitchTab,pendingTab) avec modale « Modifications non enregistrées » (Teleport→body,max-w-sm, clic overlay = Rester) ; abandon viaconfirmLeaveTab(cancelInformations/cancelFormations/cancelTarifspar section). Modale suppression d’association : layout A centré,Teleport→body,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 PiniatrainerRequests, FEAT-052) — état des demandes de réservation côté formateur :batches,stats(réponsegetReservationRequests),searchQuery,activeTab,sortBy; calculéstabCounts,urgentBatches(< 48 h,NEW/PENDING),filteredBatches(filtre onglet + recherche + tri urgence/date/formation/école) ; actionsfetchBatches,acceptBatch/refuseBatch(rechargement après succès ; accept envoie un corps JSON optionnel{ message }viatrainerAPI.acceptReservationBatch),startPolling/stopPolling(30 s). Consommé parviews/trainer/RequestsView.vue(FEAT-054).useOnboardingStore(stores/onboarding.js, id Piniaonboarding, FEAT-089 / DISC-016) — progression formateur connectée : étatprogress(miroir deuser.onboardingProgressrenvoyé par/me) + cachesampleDataPayload;hydrateProgressFromAuth()sans réseau siprogressencore null ;fetchProgress()viaauthStore.fetchMe()(DRY) avec déduplication des appels concurrents — déclenché depuis la page Getting started et par timer 5 min dansTrainerLayout;completeStep/dismiss/restoreappelantonboardingAPI(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ésbadgeLabel,showInSidebar(masquage checklist + grace period 7 j après les 5 étapes),isSampleDataActive. Tests unitairesstores/onboarding.spec.js.useStaffRequestsStore(stores/useStaffRequestsStore.js, id PiniastaffRequests, FEAT-056 / FEAT-057) — état école / staff pour « Mes demandes » : groupes issus destaffAPI.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) ;fetchRequestsmet à jourstaffDashboard.setPendingRequestsCount; pas de polling (notifications = issue séparée). FEAT-057 :groupByTrainer,openGroups, calculégroupedRequests(sections par formateur, agrégatshasNew/newCount/unreadMessagesCount),toggleGroup; en mode groupe la pagination est masquée. Consommé parviews/staff/RequestsView.vue(FEAT-058 : palette commandes branchée sur ce store pour onglets / groupement / compteurs).staffRequestAggregatedStatus.js(utils/, FEAT-057) —isMultiTrainerFulfilledet résolveurs de libellé / classes pour afficher Pourvue lorsqu’une demande multi-formateurs a un lotFULFILLED(first-claim) tout en conservant le statut groupeaccepted; utilisé parviews/staff/RequestsView.vueet le drawer (ConversationDrawer) via les helpers agrégés.useSyncStore(stores/syncStore.js, id Piniasync, FEAT-109 / DISC-019) — état de synchronisation agenda agrégé multi-organisations :orgStates(Map),isBootstrapping,bootstrapError; getterssyncingCount,errorCount(seuilERROR_THRESHOLD = 2),syncGlobalState(prioritéerror > syncing > stable),calmDotTooltip,mostRecentSyncEndDate,lastGlobalSyncFormatted; actionsbootstrap(GET/trainer/organization/dashboard+ reprise polling descurrentSyncProcessUuid),startSync,stopSync,onSyncComplete,stopAll; polling viacreateSyncPoller(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,limit30) + infinite scroll (loadMoreConversations,hasMoreConversations), filtres (searchclient-side,person, dates,typeen query comme le back),filteredConversations,fetchConversation(fusion dans la liste ou ajout en tête si absent),markAsRead(POST/conversations/{uuid}/readpuis ajustement local des comptes non lus +fetchBadgesthrottlé), fil de discussion :fetchThreadMessagescharge d’abord la dernière page (messages les plus récents en bas, ordre APIcreatedAtASC),loadMoreThreadMessagespréfixe les pages plus anciennes (threadOlderNextPage) ;appendThreadMessageaprès envoi. Notifications (onglet) —fetchNotifications/loadMoreNotificationsviaGET /notifications/search(même taille de page 30),filteredNotifications(recherche texte client-side),markNotificationRead(POST/notifications/{uuid}/read),markAllNotificationsAsRead(POST/notifications/read-all),selectedType+currentNotificationUuidpour le panneau détail. Compteurs non lus :unreadCount+syncUnreadCountFromBadges(unreadInbox)alignés surtrainerDashboard.unreadInbox/staffDashboard.unreadInbox(GET/trainer|staff/dashboard/badges, polling 2 min dans les layouts) ;inboxToastRequestedincré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 backendsync_failure+ deep-link organisations),BannerAlert(threadsbatch_threadnon lus),ToastNotification(composantmessages/ToastNotification.vue, pas le toast partagé). Consommé parcomponents/messages/MessagesInbox.vue+ConversationDetail.vue/NotificationDetail.vue(sans bandeau titre : Messages + Gérez vos conversations et notifications dansTrainerLayout/StaffLayoutviapageTitle/pageSubtitle) ; vues Messages trainer/staff embarquentMessagesInbox.conversation.js(api/conversation.js) —listConversations/getMessages: réponses paginées{ items, currentPage, itemsPerPage, totalItems, totalPages };markAsRead/markAsUnreaden POST (/conversations/{uuid}/readet/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 avecconversationPersonsdu store pour les interlocuteurs déjà en fil de boîte.notification.js(api/notification.js) —searchNotifications(pagination, tricreatedAtDESC par défaut),markNotificationRead(POST lecture unitaire),markAllNotificationsAsRead(POST/notifications/read-all).useTimeFormat.js(composables/) —formatTimeLabel/getDateLabel(date-fns + localefr) 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-settings→views/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-account→views/trainer/CloseAccountView.vue(/trainer/settings/close-account) — rappel compte (auth.user),CloseAccountDangerZone.vue(cartes Désactiver / Supprimer ; boutons désactivés siaccountStatus !== 'active'), renvoi profil public. Désactivation : modaleBaseModalrole="alertdialog"→trainerAPI.deactivateAccount+auth.fetchMe. Suppression (FEAT-232) :DeleteAccountModal.vueen 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 dansTrainerLayout:AccountDeactivatedBanner.vuesiaccountStatus === 'deactivated'→trainerAPI.reactivateAccount;AccountDeletionGraceBanner.vuesideletion_pending— date depuisauth.user.scheduledDeletionAt, annulation →trainerAPI.cancelDeletion+auth.fetchMe. API :trainer.js—deactivateAccount,reactivateAccount,requestDeletion,cancelDeletion. Champs cycle de vie dansauth.userviaGET /me:accountStatus,scheduledDeletionAt. - Entreprise (paramètres) :
trainer-settings-company→views/trainer/CompanySettingsView.vue(/trainer/settings/company) — trois sections (infos perso avec toggle teal, identité & banque, Annexes). Annexes : liste / upload / suppression viatrainerAPI.getCompanyDocuments,uploadCompanyDocument,deleteCompanyDocument(documents entreprise S3, PDF/PNG/JPG max 5 Mo) ; ancresnavItemsincluentcompany-annexes; suppression avecConfirmDangerModal. Champs texte / listes : composantsCompactTextInputetCompactSelectField(components/(shared)/) ; conteneur.input-wrap+ classe.dirtydansassets/main.css(bande orange#F7B13Bà gauche si le champ diffère du snapshot sauvegardé,border-radiusdroit seulement). CouleurdirtyBardanstailwind.config.js. Même modale départ sans save que les notifications :UnsavedLeaveModal. L’ancienne routetrainer-company(/trainer/company) redirige vers cette page. - Profil (paramètres) :
trainer-settings-profile→views/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 viaCompactTextInput/CompactSelectField, champ email viaProgressiveEmailInput+isValidEmail(utils/email.js, même principe que les contacts d’organisation dansContactSidebar.vue), enregistrement email / téléphone viatrainerAPI.updateInfo(merge-patch), mot de passe viaupdatePassword(api/user.js), validation email optionnelle (authCodeAPI). États dirty (bande orange),BlockSaveActions+UnsavedLeaveModal,useTrainerSettingsScrollSpypour l’onglet actif (scroll / resize, marqueur = bas du menu horizontal mesuré dans le viewport). L’ancienne route nomméetrainer-account(/trainer/account) redirige vers cette page (plus de montage direct deAccountView.vue). - Notifications (paramètres) :
trainer-settings-notifications→views/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.vuedéclenchée uniquement si Préférences a des changements non enregistrés (Teleportversbody, événementsstay/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_declarationdansconstants/compliance.js—COMPLIANCE_NUMBER_NATURE_TYPES,formatNda()(affichage récépisséXX XX XXXXX XX) ; tuile dédiée dansCoffreFortView.vue(saisie 11 chiffres, statut vert/gris) ; persistanceuseComplianceStore.saveNda()→PUT /trainer/company(objet company complet, champndasans 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.vue—ndaracine du GET documents (useStaffComplianceStore.drawerNda) ; pièce synthétiquenature: 'numero'+valeurcopiable (formatNda). Fiche intervenant (FEAT-246 / R16) : headerComplianceHeaderActions.vue— bouton secondaire « Générer ma fiche intervenant » (≥sm, à gauche de « Déposer un document ») → modaleCoffreFortView.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 + alerterole="alert"), CTA « Générer la fiche » désactivé si rien de sélectionné ; téléchargement viagenerateComplianceFiche()(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 viatrainerAPI.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) ; APIgetComplianceDocumentDetail,downloadComplianceDocument,deleteComplianceDocument; storedeleteDocument(refetch hub best-effort). Export ZIP (FEAT-247) : barre d’action sous la table — bouton « Télécharger mes documents (ZIP) » (BaseButtonsecondary, icôneArchive) avant « Gérer le partage » ; désactivé si!store.hasDownloadableDocs; téléchargement blob viadownloadComplianceZip()(GET /trainer/compliance/export,responseType: 'blob') + toast ; état « Préparation… » pendant la requête. Store : computedhasDownloadableDocs. Honorabilité (FEAT-245) :components/trainer/HonorabilityCard.vuemontée dansCoffreFortView.vue(scan PDF local ou saisie manuelle des 4 références non pénales, lien portail justice.gouv.fr) ; extraction 100 % navigateur viacomposables/useB3References.js(pdfjs-dist+ worker Vite?url— le bulletin n°3 n’est jamais transmis) ; persistanceuseComplianceStore— étathonorability, actionsfetchHonorability,saveHonorabilityRefs; APIapi/compliance.js—getHonorability(GET /trainer/honorability,404→null),saveHonorability(PUT). Partage Cas A (FEAT-242) : barre « Gérer le partage », drawer organisations (BaseModaldrawer + onglet « Organisations connectées »,BaseToggle),GET/POST/DELETE …/compliance/shares; clôture demandes org via events Symfony (TrainerComplianceShareGrantedEvent/TrainerComplianceDocumentProvidedEvent→RespondPendingTrainerComplianceRequestsSubscriber). Partage Cas B (FEAT-243 + FEAT-264) : même drawer, onglet « Lien externe temporaire » (CoffreFortView.vue,BaseTabs—fetchLinksau 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 …/linksavecdocumentUuids[]) + copie{origin}/shared/document/{token}; liste des liens actifs affichedocumentTypeLabels[](libellé unique ou « N pièces » + tooltip) avec révocation (revokeLink). Store : étatlinks/linksLoading/linksError, actionsfetchLinks,createLink,revokeLink. API :api/compliance.js—getComplianceLinks,createComplianceLink,revokeComplianceLink. Route publiqueshared-compliance-document→/shared/document/:token(meta.requiresAuth: false) →views/(unauthenticated)/Compliance/SharedComplianceDocumentView.vue:GET …/public/compliance-document/{token}/info(listedocuments[]si N>1) puis téléchargement via lien<a>vers le proxyGET …/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é viautils/notificationDisplay.js(codestitleCode/bodyCodeoutitle/bodyAPI, indépendamment detype— le DiscriminatorMap peut omettredocument_expiry) ; sticky J+0 ; « Mettre à jour » →trainer-documents?renew=<type>(consommation FEAT-240). Inbox/trainer/messages: mêmes helpers dansNotificationItem/NotificationDetail(libellés FR + CTA « Voir mes documents »). - Facturation (paramètres) :
trainer-billing-settings→views/trainer/BillingSettingsView.vue(/trainer/settings/billing) — 4 onglets (Configuration, Personnalisation, Paiements, Annexes), accessibles (role="tablist"/tab/tabpanel, navigation clavier) ; même rendu visuel queTrainerSettingsSubnav(fondbg-white, bordureborder-mushroom-200,shadow-sm, inactiftext-grey-600, actiftext-corail-600+ trait corail inset).useTrainerSettingsHeadersansnavItems. 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.tabest synchronisé versactiveTab(whitelist des IDs d’onglets), ce qui permet les entrées directes de typetrainer-billing?tab=personnalisation. Garde changement d’onglet : si l’onglet courant (Configuration ou Personnalisation) a des modifications non enregistrées,UnsavedLeaveModalavant de quitter l’onglet (switchTab). Onglet Personnalisation : logo PNG/JPG/GIF/WebP max 2 Mo (pas SVG), couleur accent, taille de police enfieldset+role="radiogroup"+ radiossr-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 decompany, lien Entreprise avec#bancaire. Annexes : liste viagetBillingDocuments(métadonnées facturation), états chargement / vide / erreur, suppression viaConfirmDangerModal, fichier max 5 Mo dans la modale. - Éditeur de modèle de document :
trainer-billing-template-editor→views/trainer/DocumentTemplateEditorView.vue(/trainer/template-editor/:uuidselon route active) —meta.hideTrainerNav: pas de sidebar ni bandeau globalTrainerLayout; barre locale Retour →trainer-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 viaGET /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éepl-3 sm:pl-4 lg:pl-5, droitepr-4 sm:pr-5 lg:pr-6; padding verticalpy-5en mode lien retour injecté, sinon hauteur fluide UX-359min-h-14 py-2 sm:min-h-16 sm:py-1; bloc titre retour + titre + description enspace-y-1.5, titretext-xl leading-tight truncate, retourtext-xs+min-h-11 lg:min-h-0+text-grey-500+ icôneChevronLeft(lucide-vue-next), descriptionhidden sm:block truncate text-sm text-grey-500+leading-snug). Les vues sous/trainer/settings/*appellentcomposables/useTrainerSettingsHeader.jspour remplirsettingsHeaderState(inject). La remise à zéro se fait dansTrainerLayoutlorsque la route quitte/trainer/settings, pas auonUnmountedde chaque vue (sinon navigation entre sous-routes efface le header). La barre d’ancres est le composantcomponents/Trainer/TrainerSettingsSubnav.vue(monté dansTrainerLayout.vuepour toutes les routes sous/trainer/settings/*qui définissentnavItems). Typo maquette :text-sm+leading-5+font-semibold, onglet actiftext-corail-600, inactiftext-grey-link(#808080, maquette rgb(128 128 128)) ;p-2.5symétrique,items-stretch, boutons sans bordure (border-0,box-border) ; l’onglet actif utilise un trait corail enbox-shadowinset (inset 0 -3px 0 0corail-500), pas debordersur les boutons.sticky top-0sous le bandeau global ; les clics émettentselectpuisscrollIntoView({ behavior: 'smooth' })(le layout met à jour l’onglet actif tout de suite) et le routeur applique la même logique pour les URLs avec#hashsur les routes paramètres. Le suivi au défilement utiliseuseTrainerSettingsScrollSpy.js(pas d’IntersectionObserveravecrootMarginen pourcentage) : scroll / resize sur le conteneur principal, ligne de référence = bas dunav[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-6ou alignée sur le layout). - Les routes
settings/profile,settings/company,settings/notificationssont 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/companyredirige vers Entreprise paramètres ;/trainer/accountredirige vers le profil paramètres. - Mes demandes (formateur, FEAT-054 / DISC-012, palette FEAT-058, UX-046, FEAT-081) :
views/trainer/RequestsView.vue— fond :layouts/TrainerLayout.vueappliquebg-mushroom-100sur le<main>/ conteneur scroll pour/trainer/requests(isRequestsRoute, aligné maquette HTMLbody). Liste en cartes (5 onglets + compteurs, recherchemushroom-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 aidemax-w-sm) et Ctrl+K →CommandPalette.vue(raccourcis page, actions rapides, navigationvue-router),useMultipleModalspour la pile Échap ; ongletsaria-controlsvers panneauxid="panel-*"(onglet « Toutes » : liste d’ids). ModalesAcceptModal/RefuseModalavecuseFocusTrap; détail viaRequestDrawer.vue(FEAT-055) : panneau 500px,Teleportbody, animation spring, ombre progressive, piège focus, verrouillage scroll,inertsur#app > :first-child,SlotCard+MiniCalendarWeek, fil messages (getBatchMessages/sendBatchMessage), événements@accept/@refuse(créneaux + batch). Marquage lu :trainerAPI.markBatchVieweduniquement dansopenDetail(clic carte ou ouverture équivalente : Entrée, palette) sifirstViewAt === null, puisfetchBatcheset resynchronisation deselectedBatchpour retirer l’état « non lu » sans double PATCH côté drawer. Composants :RequestCard.vue(3 zones, variantesNEWnon lu, acceptée / refusée / annulée / pourvueFULFILLED; pas d’icône / badge messages sur la carte — UX-066 / DISC-012, fil dansRequestDraweruniquement),AcceptModal.vue,RefuseModal.vue(listbox ARIA, 8 motifs). Deep-link messagerie → demandes (FEAT-081) :ConversationDetail.vueémet?highlight={batchUuid}; aprèsfetchBatches,useCrossPageHighlight().run()active l’onglet « Toutes » si seulhighlight(sans?status=) pour que la carte soit au DOM ;?status=whitelist (all|pending| …) ; cartes avecdata-request-id; URL nettoyée (router.replace). CSS globalassets/main.css:.is-highlighted+@keyframes highlight-card. - Mes demandes (école / staff, UX-046, FEAT-081) :
views/staff/RequestsView.vue—useCrossPageHighlightaprèsfetchRequests(même params query) ; lignes<tr>avecdata-request-id(liste plate et vue groupée par formateur). Modales relance et annulation —useFocusTrapsur chaquerole="dialog";useMultipleModalspour Échap (priorité à la modale la plus « haute » dans la pile déclarée, fermeture bloquée siclose*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,inertsur#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.getBatchMessagesavec loaderLoader2+aria-busy, saisie désactivée pendant le fetch ;sendBatchMessage), métadonnées repliées, pied conditionnel pending ; propsrequest,open; événementsclose,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 ; propscalendarDays,calendarCols,weekLabel. Les en-têtes de colonnes sont fournis parRequestDrawer.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. SicalendarCols[].segmentsest défini (APIweekEventsavecisMainEvent/isBatchEvent), blocs de couleur uniquement (pas de texte dans la grille) ; libellés détaillés en:titleau survol ; segments pleins : agenda gris, demande sans conflit bleue, avec conflit rouge ; conflit uniquement en demi-colonnes (agenda gauche, demande droite). Sinon mode legacyevents+slots. Calcul des segments dansRequestDrawer.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-700minimum (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-600aligné 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.js—jsdom, Allure viaallure-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.jscollé danssrc/views/(unauthenticated)/Auth/(FEAT-178). - Unit : fichiers
*.spec.jsdanssrc/(ex.components/ui/BaseSelect.spec.js). - E2E Cypress (
cypress/e2e/smoke/) : smoke read-only surhttps://app.teadle.com, job CIe2e-smokeaprèsdeploy-clever(secretsE2E_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. Voircypress/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
- Suppression des console.log - Remplacement par un logger centralisé
- Organisation des composants - Structure modulaire claire
- Amélioration des services - Respect des principes SOLID
- 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
- Tests automatisés - Augmenter la couverture de tests
- Performance - Optimisation des bundles et lazy loading
- Accessibilité - Amélioration de l'accessibilité WCAG
- PWA - Transformation en Progressive Web App
- Monitoring - Intégration d'outils de monitoring
Documentation mise à jour le : ${new Date().toLocaleDateString('fr-FR')}