🏗️ 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
- 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êteh-16/border-mushroom-250/ typotext-xl+ sous-titretext-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
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 ; `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 OAuthPasswordResetModal.vue- Modal de réinitialisation de mot de passeOAuthRedirect.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). 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.
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) — toolbar persistante : nav période (ChevronLeft/ChevronRight, labelaria-live="polite", « Aujourd'hui »), segmented switcherrole="group"+aria-current(Jour / Semaine / Mois) ; emitsupdate: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 versShareCalendarView) ; layouth-screen; grilles DS (FEAT-135) ; cloche inbox reste dansTrainerLayoutuniquement.utils/planningEmptyPeriod.js(FEAT-135) — libellé état vide selon vue DISC-023 (jour|semaine|mois, plus les types FullCalendar).PlanningCalendar.vue— FullCalendar partagé (horsPlanningViewformateur depuis FEAT-135) :SharedPlanningView,IntervenantCalendarView,IntervenantsCalendarsView. Dépendances@fullcalendar/*conservées tant que ces vues consomment le wrapper. Événementstype: personal: côté formateur (nouvelle vue DS), filtre « Agenda personnel » via__personal_calendar__+CalendarEventBlock(hatch indispo) ; côté public / staff (FullCalendar),extendedProps.isPersonalBusyuniquement (libellés génériques, stylefc-event-busy-public). Convention scroll vues publiques pleine page :h-screen flex flex-col+ zoneflex-1 overflow-y-auto(ex.SharedPlanningView.vue).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) - BLOC 3 : 4 KPI avec count-up, tooltips, panels expand/collapse, donut SVG heures et breakdowns écoles. 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
OnboardingLayout.vue- Coquille onboarding avec scroll interne : wrapperh-screen flex flex-col+ zone de contenuflex-1 overflow-y-autopour garantir le défilement des étapes même quandbodyest enoverflow: hidden(modales / overlays).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) —DialogRoot/DialogPortal/DialogOverlay/DialogContent(reka-ui) : propsopen,size(sm|md|lg),@update:open, overlaybg-black/50 backdrop-blur-sm, panneau centrémax-w-md, animation scale-in 200 ms (désactivée siprefers-reduced-motion), clic overlay ferme ; piège à focus etaria-modalgérés par le primitive.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) — case à cocher native :v-modelbooléen ou tableau (valueen mode groupe),accentColor(style inline, ex. couleur calendrier), mode contrôlé:model-value+@update:model-value; CVAcheckboxVariants; libellé porté par le<label>consommateur.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) — listbox Reka UI (SelectRoot+SelectPortal+position="popper"+avoidCollisions) :v-modelstring,options(String[]ou{ value, label }[]), placeholder,ariaLabel; CVAselectTriggerVariants/selectContentVariants/selectItemVariants; téléport body (modales), chevron SVG inlinegroup-data-[state=open]:rotate-180, clavier/typeahead Reka.components/ui/BaseAlert.vue(FEAT-131) — calloutrole="note"|status: variantesinfo|success|warning|error(CVAalertVariants+alertIconColors/alertTextColors), icône Lucide par défaut ou:icon, slot#title+ défaut,dismissible+@close, overrideclass(ex. banner compact Planning).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/BaseInput.vue(FEAT-084) — champ texte natif dans.input-wrap:v-model,type(donttime),id,disabled,class; utilisé parAvailabilityViewabsences plage horaire (FEAT-139).- 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) — 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 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 lignesBillingSettingsView.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; 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. 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
🏪 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). - 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) — une page à six ancres (Photo, Identité, 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). - 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-4sur les routes paramètres,py-5ailleurs ; bloc titre retour + titre + description enspace-y-1.5, titretext-xl leading-tight, retourtext-xs+text-grey-500+ icôneChevronLeft(lucide-vue-next), descriptiontext-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
🔐 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.js—test.environment: 'jsdom', fichierssrc/**/*.{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
- 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')}