Frontend : consommation de l'API CRUD (matrice, affectations, validation)

Remplace le magasin d'état fabriqué par une vraie couche API typée, alignée
sur le back-end (Étape 5) :

- src/api/ : schema.ts (miroir de backend/app/api/schemas.py), client HTTP,
  implémentation mock en mémoire (même surface + portage du moteur de
  validation), sélection mock/http via VITE_API_MODE, hooks React Query
  (lectures + mutations avec invalidation).
- Grille des besoins ← GET /matrix ; treillis de colonnes dérivé des
  créneaux réels (plus de plages figées).
- Panneau d'affectation ← GET /assignments?shift_id, POST /assignments/validate
  par candidat (alertes bloquantes / avertissements du moteur),
  POST / PATCH / DELETE /assignments.
- Vue par bénévole ← volunteers + shifts + assignments (surcharge / oubliés
  calculés côté client).
- Confirmations (ex-« Publication ») ← affectations PROPOSED ; publication
  versionnée et emails signalés hors périmètre.
- Mobiles ← assignments filtrées ; confirmation = PATCH ; pointage référent
  local (hors périmètre).
- Configuration ← positions & shifts CRUD (min/idéal, ajout/retrait),
  volunteers CRUD dont suppression RGPD ; messages = maquette locale.
- i18n mis à jour, aucun texte d'interface en dur.
- Vérifs : build tsc + vite, eslint (0 problème), npm run smoke (API mock +
  rendu headless des 6 écrans avec données préchargées).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2
This commit is contained in:
AdamThomas
2026-09-09 23:16:38 +02:00
co-authored by Claude Sonnet 5
parent ac469f0cd1
commit c9dda65361
38 changed files with 2930 additions and 2414 deletions
+97 -107
View File
@@ -3,19 +3,30 @@
"legend.title": "LÉGENDE",
"action.close": "Fermer",
"action.remove": "Retirer",
"action.add": "Ajouter",
"unit.hours": "{count} h",
"unit.days": "{count} j",
"time.now": "à l’instant",
"time.yesterday": "hier 18:42",
"app.loading": "Chargement…",
"app.error.generic": "Une erreur est survenue",
"error.generic": "Erreur inattendue",
"error.not_found": "Ressource introuvable",
"error.forbidden": "Accès refusé",
"error.unauthorized": "Authentification requise",
"error.validation_failed": "La validation a échoué (règle bloquante)",
"assignment.duplicate": "Ce bénévole est déjà affecté à ce créneau",
"assignment.wrong_edition": "Bénévole ou créneau hors de cette édition",
"volunteer.duplicate_email": "Un bénévole avec cet email existe déjà",
"shift.ends_not_after_starts": "La fin doit être après le début",
"shift.position_wrong_edition": "Le poste n'appartient pas à cette édition",
"header.title": "GESTION DES BÉNÉVOLES",
"header.meta": "BÉNÉVOLAT · {count} INSCRITS",
"header.draft": "BROUILLON · {count} MODIF.",
"header.published": "✓ TOUT EST PUBLIÉ",
"header.mockMode": "DONNÉES DE DÉMO",
"header.proposed": "{count} PROPOSITION(S)",
"tab.grille": "Grille des besoins",
"tab.volunteers": "Vue par bénévole",
"tab.publication": "Publication",
"tab.publication": "Confirmations",
"tab.mobileVolunteer": "Bénévole · mobile",
"tab.mobileLead": "Référent · mobile",
"tab.config": "Configuration",
@@ -35,38 +46,42 @@
"position.category.stage": "Régie",
"position.category.other": "Autre",
"validation.SHIFT_OVERLAP": "Déjà affecté sur {position} au même créneau",
"validation.SHIFT_OVERLAP": "Se chevauche avec un autre créneau",
"validation.SHIFT_OVERLAP.at": "Déjà affecté sur {position} au même moment",
"validation.SHIFT_OVERLAP.rule": "Deux créneaux d'un même bénévole se chevauchent.",
"validation.OUTSIDE_AVAILABILITY": "Hors disponibilités déclarées",
"validation.DAILY_HOURS_EXCEEDED": "Plus de {max} h le {day} ({total} h)",
"validation.INSUFFICIENT_REST": "Moins de {min} h entre deux créneaux ({rest} h)",
"validation.OUTSIDE_AVAILABILITY.unavailable": "Déclaré indisponible sur ce créneau",
"validation.OUTSIDE_AVAILABILITY.rule": "Une demi-journée du créneau est indisponible ou sans déclaration.",
"validation.DAILY_HOURS_EXCEEDED": "Plus de 8 h sur la journée ({hours})",
"validation.DAILY_HOURS_EXCEEDED.rule": "Plus de 8 h 00 d'affectations cumulées sur une même journée.",
"validation.INSUFFICIENT_REST": "Moins de 8 h de repos entre deux créneaux ({rest})",
"validation.INSUFFICIENT_REST.rule": "Moins de 8 h 00 entre la fin d'un créneau et le début du suivant.",
"validation.MISSING_REQUIRED_SKILL": "Compétence requise manquante : {skill}",
"validation.MISSING_REQUIRED_SKILL.rule": "Le bénévole n'a pas une compétence requise par le créneau.",
"grille.filter.day": "JOUR",
"grille.filter.position": "POSTE",
"grille.legend.title": "LÉGENDE",
"grille.legend.critical": "SOUS-EFFECTIF CRITIQUE",
"grille.legend.incomplete": "INCOMPLET",
"grille.legend.complete": "COMPLET",
"grille.legend.unpublished": "NON PUBLIÉ",
"grille.positionsCount": "{count} POSTES",
"grille.assignedRequired": "AFFECTÉS / REQUIS",
"grille.footnote": "Colonnes = plages de la journée (08–12 / 12–17 / 17–22 / 22–02). Cliquer une cellule ouvre le panneau d’affectation à droite.",
"grille.assignedRequired": "CONFIRMÉS / IDÉAL",
"grille.footnote": "Chaque cellule est un créneau. Nombre = confirmés, + = proposés, / = effectif idéal. Cliquer ouvre le panneau d'affectation à droite.",
"grille.panel.empty.title": "Cliquez une cellule de la grille pour affecter des bénévoles.",
"grille.panel.empty.body": "Le panneau s’ouvre ici, sans quitter la grille : candidats triés par pertinence, alertes bloquantes et avertissements.",
"grille.panel.empty.body": "Le panneau s'ouvre ici, sans quitter la grille : candidats triés par charge, alertes bloquantes et avertissements du moteur de validation.",
"grille.panel.slotDuration": "CRÉNEAU DE {hours} H",
"grille.panel.assigned": "AFFECTÉS",
"grille.panel.confirmed": "CONFIRMÉS",
"grille.panel.proposedCount": "PROPOSÉS {count}",
"grille.panel.min": "MINIMUM {count}",
"grille.panel.ideal": "IDÉAL {count}",
"grille.panel.assignmentsTitle": "AFFECTATIONS ({count})",
"grille.panel.noneAssigned": "Personne n’est encore affecté sur ce créneau.",
"grille.panel.noneAssigned": "Personne n'est encore affecté sur ce créneau.",
"grille.panel.hoursTotal": "{hours} H AU TOTAL",
"grille.panel.candidates": "CANDIDATS",
"grille.panel.candidatesHint": "TRIÉS PAR PERTINENCE",
"grille.panel.hoursAssigned": "{hours} H DÉJÀ AFFECTÉES · {slots} CRÉNEAUX",
"grille.panel.candidatesHint": "LES MOINS CHARGÉS D'ABORD",
"grille.panel.hoursAssigned": "{hours} H · {slots} CRÉNEAUX",
"grille.panel.assign": "AFFECTER",
"grille.panel.blocked": "BLOQUÉ",
"grille.panel.availableTag": "✓ Disponible sur ce créneau",
"grille.panel.wantedTag": "★ A demandé ce poste",
"grille.panel.blockingPrefix": "BLOQUANT — {reason}",
"grille.panel.warningsTitle": "⚠ AVERTISSEMENTS — AFFECTATION POSSIBLE",
@@ -76,36 +91,26 @@
"volunteers.filter.unassigned": "SANS AFFECTATION",
"volunteers.legend.confirmed": "AFFECTATION CONFIRMÉE",
"volunteers.legend.proposed": "AFFECTATION PROPOSÉE",
"volunteers.legend.availability": "DISPONIBILITÉ DÉCLARÉE (NON AFFECTÉ)",
"volunteers.count": "{count} BÉNÉVOLES",
"volunteers.totalHours": "TOTAL H",
"volunteers.name": "NOM",
"volunteers.cumulative": "CUMUL",
"volunteers.badge.overloaded": "▲ SURCHARGE",
"volunteers.badge.unassigned": "○ OUBLIÉ",
"volunteers.empty": "Aucun bénévole ne correspond à ce filtre — bonne nouvelle. Repassez sur « TOUS » pour revoir l’ensemble de l’équipe.",
"volunteers.footnote": "Surcharge = plus de {maxHours} h sur une même journée ou moins de {restHours} h entre deux créneaux. Oublié = aucune affectation sur les 4 jours.",
"volunteers.empty": "Aucun bénévole ne correspond à ce filtre — bonne nouvelle. Repassez sur « TOUS » pour revoir l'ensemble de l'équipe.",
"volunteers.footnote": "Surcharge = plus de 8 h sur une même journée ou moins de 8 h entre deux créneaux. Oublié = aucune affectation active.",
"publication.stat.changes": "MODIFICATIONS NON PUBLIÉES",
"publication.stat.proposed": "AFFECTATIONS PROPOSÉES",
"publication.stat.concerned": "BÉNÉVOLES CONCERNÉS",
"publication.stat.version": "VERSION PUBLIÉE · {when}",
"publication.draftTitle": "BROUILLON EN COURS D’ÉDITION",
"publication.diffWith": "DIFFÉRENTIEL AVEC LA v{version}",
"publication.add": "+ AJOUT",
"publication.remove": "− RETRAIT",
"publication.noDiff": "Le brouillon est identique à la version publiée. Chaque bénévole a reçu son planning à jour.",
"publication.mailPreviewTitle": "APERÇU DU MESSAGE ENVOYÉ",
"publication.mailFrom": "DE : {from}",
"publication.stat.confirmed": "AFFECTATIONS CONFIRMÉES",
"publication.toConfirmTitle": "EN ATTENTE DE CONFIRMATION",
"publication.allConfirmed": "Aucune affectation en attente : tout est confirmé.",
"publication.mailPreviewTitle": "APERÇU DU MESSAGE (MAQUETTE)",
"publication.mailTo": "À : {count} bénévoles concernés",
"publication.mailSubjectPrefix": "Objet — {subject}",
"publication.mailGreeting": "Salut {name},",
"publication.mailBody": "Ton planning pour le festival vient d’être mis à jour. Tu as désormais {slots} créneaux, soit {hours} heures.",
"publication.mailBody2": "Merci de confirmer chaque créneau depuis ton planning personnel : ton référent de poste s’appuie dessus pour le pointage sur le site.",
"publication.mailSubject": "Objet — Ton planning bénévole a été mis à jour",
"publication.mailBody": "Ton planning pour le festival vient d'être mis à jour. Merci de confirmer chaque créneau depuis ton planning personnel.",
"publication.mailCta": "VOIR MON PLANNING →",
"publication.magicLinkNote": "Lien magique valable {days} jours, aucun mot de passe.",
"publication.publishButton": "PUBLIER ET ENVOYER À {count} BÉNÉVOLES",
"publication.publishNote": "La publication fige le brouillon en version v{version}. Les affectations restent « proposées » jusqu’à confirmation par chaque bénévole.",
"publication.publishedState": "✓ PUBLIÉ — VERSION v{version} ENVOYÉE {when}",
"publication.outOfScope": "La publication versionnée et l'envoi d'emails / SMS sont hors périmètre du premier incrément (CLAUDE.md §4). Cet écran liste les affectations « proposées » que les bénévoles n'ont pas encore confirmées.",
"design.noteTitle": "CHOIX DE CONCEPTION",
@@ -116,108 +121,93 @@
"mobileVol.hours": "{count} HEURES",
"mobileVol.toConfirm": "⚠ {count} CRÉNEAU(X) À CONFIRMER",
"mobileVol.location": "📍 {location}",
"mobileVol.lead": "RÉFÉRENT · {name}",
"mobileVol.call": "📞 {phone}",
"mobileVol.confirmed": "✓ CRÉNEAU CONFIRMÉ",
"mobileVol.confirm": "JE CONFIRME",
"mobileVol.orgMessageTitle": "MESSAGE DE L’ORGANISATION",
"mobileVol.orgMessageBody": "Le briefing bénévoles a lieu jeudi 11h sous le chapiteau nord. Récupération des t-shirts et des badges à l’accueil bénévoles dès mercredi 14h.",
"mobileVol.orgMessageMeta": "COORDINATION · MARIE B. · HIER 18:42",
"mobileVol.designNote": "Lisibilité plein soleil : fonds opaques, texte noir sur jaune ou blanc, aucun gris clair, aucune ombre portée sur du texte. Cibles tactiles à 52–62 px de haut. Une seule action par créneau, hiérarchie en trois niveaux : jour + horaire, poste, référent. Le téléphone du référent est un lien d’appel direct.",
"mobileVol.confirmed": "✓ CRÉNEAU CONFIRMÉ",
"mobileVol.noSlots": "Aucun créneau pour l'instant. La coordination t'affectera bientôt.",
"mobileVol.designNote": "Lisibilité plein soleil : fonds opaques, texte noir sur jaune ou blanc, aucun gris clair. Cibles tactiles à 52–62 px de haut. Une seule action par créneau. Les données proviennent des affectations réelles de l'API.",
"mobileLead.statusTime": "18:07",
"mobileLead.kicker": "RÉFÉRENT DE POSTE · {name}",
"mobileLead.shiftHours": "17:00 → 22:00",
"mobileLead.inProgress": "EN COURS · {hours}",
"mobileLead.present": "{present}/{total} PRÉSENTS",
"mobileLead.kicker": "RÉFÉRENT DE POSTE",
"mobileLead.inProgress": "CRÉNEAU · {hours}",
"mobileLead.present": "{present}/{total} POINTÉS PRÉSENTS",
"mobileLead.absenceAlert": "⚠ {count} ABSENCE(S) SUR LE CRÉNEAU",
"mobileLead.requestBackup": "DEMANDER UN RENFORT À LA COORDINATION",
"mobileLead.teamTitle": "POINTAGE DE L’ÉQUIPE",
"mobileLead.statusConfirmed": "Confirmée",
"mobileLead.statusProposed": "Proposée · non confirmée",
"mobileLead.teamTitle": "POINTAGE DE L'ÉQUIPE",
"mobileLead.badgePresent": "✓ PRÉSENT",
"mobileLead.badgeAbsent": "✕ ABSENT",
"mobileLead.markPresent": "PRÉSENT",
"mobileLead.markAbsent": "ABSENT",
"mobileLead.nextSlot": "Créneau suivant · 22:00 → 02:00 · 4 bénévoles attendus",
"mobileLead.designNote": "Le référent ne voit que son poste et le créneau en cours — aucune navigation, aucun filtre. Le pointage est un geste unique par personne, deux boutons pleine largeur de 56 px. Toute absence remonte immédiatement en haut de l’écran avec une action unique vers la coordination.",
"mobileLead.emptyTeam": "Aucun bénévole affecté à ce créneau.",
"mobileLead.designNote": "Le référent ne voit que son poste et un créneau. Le pointage est un geste unique par personne. Il est local à cette maquette : le pointage sur site est hors périmètre (CLAUDE.md §4).",
"config.admin": "ADMINISTRATION",
"config.reserved": "RÉSERVÉ À LA COORDINATION",
"config.tab.grid": "GRILLE DES BESOINS",
"config.tab.grid": "POSTES & CRÉNEAUX",
"config.tab.volunteers": "BÉNÉVOLES & IMPORT",
"config.tab.mail": "MESSAGES & ENVOIS",
"config.tab.rules": "RÈGLES DU FESTIVAL",
"config.tab.mail": "MESSAGES (MAQUETTE)",
"config.tab.rules": "RÈGLES & ÉDITION",
"config.grid.positionsHeader": "POSTES · {count} CRÉNEAUX",
"config.grid.slotsCount": "{count} créneaux",
"config.grid.addPosition": "+ AJOUTER UN POSTE",
"config.grid.minTotal": "BESOIN MINIMUM · TOUT LE FESTIVAL",
"config.grid.idealTotal": "BESOIN IDÉAL",
"config.grid.addShift": "+ AJOUTER UN CRÉNEAU (12–17)",
"config.grid.newPositionName": "Nouveau poste",
"config.grid.minTotal": "BESOIN MINIMUM · TOTAL",
"config.grid.idealTotal": "BESOIN IDÉAL · TOTAL",
"config.grid.min": "MIN",
"config.grid.ideal": "IDÉAL",
"config.grid.closed": "POSTE FERMÉ",
"config.grid.open": "+ OUVRIR",
"config.grid.note": "Toute modification est répercutée immédiatement sur la grille des besoins (onglet 01) : baisser un effectif idéal peut faire passer une cellule de « incomplet » à « complet ».",
"config.grid.noShifts": "Aucun créneau sur ce poste. Ajoutez-en un pour qu'il apparaisse dans la grille.",
"config.grid.note": "Les modifications d'effectif (min / idéal) et les créneaux sont enregistrés immédiatement via l'API et se répercutent sur la grille des besoins (onglet 01).",
"config.vol.importTitle": "IMPORTER DES INSCRIPTIONS",
"config.vol.importHint": "Glissez un export CSV du formulaire · colonnes reconnues : nom, email, téléphone, postes souhaités, disponibilités",
"config.vol.importButton": "SIMULER UN IMPORT",
"config.vol.importHint": "L'import de fichier CSV / Excel se fait via l'endpoint dédié. Ce bouton crée 4 bénévoles d'exemple via l'API.",
"config.vol.importButton": "CRÉER 4 EXEMPLES",
"config.vol.profiles": "PROFILS ENREGISTRÉS",
"config.vol.imported": "IMPORTÉS · À AFFECTER",
"config.vol.colName": "NOM",
"config.vol.colPhone": "TÉLÉPHONE",
"config.vol.colWanted": "POSTES SOUHAITÉS",
"config.vol.colAvailability": "DISPONIBILITÉS",
"config.vol.new": "NOUVEAU",
"config.vol.slotsDeclared": "{count} plages",
"config.vol.colEmail": "EMAIL",
"config.vol.colSkills": "COMPÉTENCES",
"config.vol.ficheKicker": "FICHE BÉNÉVOLE",
"config.vol.fullName": "NOM COMPLET",
"config.vol.firstName": "PRÉNOM",
"config.vol.lastName": "NOM",
"config.vol.phone": "TÉLÉPHONE",
"config.vol.email": "EMAIL (LIEN MAGIQUE)",
"config.vol.wanted": "POSTES SOUHAITÉS",
"config.vol.hoursAssigned": "DÉJÀ AFFECTÉES",
"config.vol.availabilityDeclared": "DISPONIBILITÉS DÉCLARÉES",
"config.vol.availabilityValue": "{count} plages déclarées sur 16",
"config.vol.note": "Les disponibilités proviennent du formulaire d’inscription : la coordination peut les corriger mais jamais les confondre avec une affectation.",
"config.vol.tshirt": "TAILLE DE T-SHIRT",
"config.vol.skills": "COMPÉTENCES",
"config.vol.noSkills": "Aucune compétence déclarée",
"config.vol.skillPlaceholder": "Code (ex. CAISSE)",
"config.vol.delete": "SUPPRIMER (RGPD — EFFACEMENT COMPLET)",
"config.vol.deleteConfirm": "Supprimer définitivement ce bénévole et toutes ses données (disponibilités, affectations) ?",
"config.vol.note": "Modifications enregistrées immédiatement via l'API. La suppression est un effacement complet en cascade (RGPD).",
"config.mail.draftBanner": "Écran maquette : l'envoi d'emails / SMS est hors périmètre. Ces réglages ne sont pas enregistrés.",
"config.mail.templateTitle": "MODÈLE DE MESSAGE DE PUBLICATION",
"config.mail.from": "EXPÉDITEUR",
"config.mail.replyTo": "RÉPONDRE À",
"config.mail.subject": "OBJET",
"config.mail.body": "CORPS DU MESSAGE",
"config.mail.variables": "VARIABLES DISPONIBLES",
"config.mail.sendsTitle": "ENVOIS ET RELANCES",
"config.mail.reminderToggle": "Relance automatique des créneaux non confirmés",
"config.mail.on": "ACTIVÉE",
"config.mail.off": "DÉSACTIVÉE",
"config.mail.reminderDelay": "Délai avant relance",
"config.mail.magicLinkValidity": "Validité du lien magique",
"config.mail.smsToggle": "Doubler par SMS le jour J",
"config.mail.smsCost": "Coût estimé : 0,05 € par bénévole",
"config.mail.onM": "ACTIVÉ",
"config.mail.offM": "DÉSACTIVÉ",
"config.mail.previewTitle": "APERÇU AVEC DONNÉES RÉELLES",
"config.mail.previewTitle": "APERÇU",
"config.mail.previewFrom": "DE : {from}",
"config.mail.cta": "VOIR MON PLANNING →",
"config.rules.editionTitle": "ÉDITION",
"config.rules.festivalName": "NOM DU FESTIVAL",
"config.rules.dates": "DATES COUVERTES",
"config.rules.datesNote": "Les colonnes de la grille sont générées à partir de ces dates : 4 journées × 4 plages (08–12 / 12–17 / 17–22 / 22–02).",
"config.rules.thresholdsTitle": "SEUILS D’ALERTE",
"config.rules.maxDaily": "Maximum d’heures par journée",
"config.rules.maxDailyNote": "Avertissement non bloquant lors de l’affectation",
"config.rules.editionTitle": "ÉDITION & CONNEXION",
"config.rules.editionId": "Identifiant d'édition",
"config.rules.apiMode": "Mode API",
"config.rules.volunteersCount": "Bénévoles",
"config.rules.positionsCount": "Postes",
"config.rules.shiftsCount": "Créneaux",
"config.rules.editionNote": "Il n'y a pas encore d'endpoint de configuration d'édition : nom, dates et rétention se règlent en base. En mode HTTP, VITE_EDITION_ID désigne l'édition.",
"config.rules.thresholdsTitle": "MOTEUR DE VALIDATION",
"config.rules.maxDaily": "Maximum d'heures par journée",
"config.rules.minRest": "Repos minimum entre deux créneaux",
"config.rules.minRestNote": "Les créneaux enchaînés dans la même journée ne comptent pas",
"config.rules.overCount": "{count} bénévoles dépassent les seuils actuels — visibles dans la vue par bénévole.",
"config.rules.overlapNote": "Le double emploi au même créneau reste toujours bloquant : ce seuil n’est pas paramétrable.",
"config.rules.rolesTitle": "RÔLES ET ACCÈS",
"config.rules.roleCoordinator": "Coordination · 3 personnes",
"config.rules.roleCoordinatorDesc": "Accès complet, publication, configuration",
"config.rules.roleLead": "Référents de poste · 15 personnes",
"config.rules.roleLeadDesc": "Leur poste, leur créneau, pointage seulement",
"config.rules.roleVolunteer": "Bénévoles · {count} personnes",
"config.rules.roleVolunteerDesc": "Lien magique par email, sans mot de passe",
"config.rules.designateLead": "DÉSIGNER UN RÉFÉRENT DE POSTE"
"config.rules.thresholdsNote": "Seuils figés côté back-end (CLAUDE.md §3) : non paramétrables. Le double emploi au même créneau est toujours bloquant.",
"config.rules.rulesTitle": "RÈGLES APPLIQUÉES",
"config.rules.rolesTitle2": "RÔLES ET ACCÈS",
"config.rules.roleCoordinator": "Coordination",
"config.rules.roleCoordinatorDesc": "Accès complet, construit et publie les plannings",
"config.rules.roleLead": "Référent de poste",
"config.rules.roleLeadDesc": "Lecture sur l'édition, écriture sur ses postes",
"config.rules.roleVolunteer": "Bénévole",
"config.rules.roleVolunteerDesc": "Ses seules données, lien magique par email"
}