Frontend : intégration du design Claude Design (6 écrans)

Portage du projet « Bénévoles Festival.dc.html » en implémentation statique
pilotée par un jeu de démonstration déterministe (portage du générateur du
design : 15 postes, 64 bénévoles, ~209 affectations).

- Système visuel néo-brutaliste : palette + primitives Tailwind, polices
  Archivo / DM Mono, primitives dans components/ui.tsx.
- Domaine (aligné CLAUDE.md) : types, jours/plages, moteur de validation
  (mêmes codes BLOCKING/WARNING que le back-end), sélecteurs, différentiel
  de publication.
- État applicatif : PlanningProvider (reducer) — affecter / désaffecter /
  publier / confirmer / éditer besoins et bénévoles / import simulé / pointage.
- Écrans : 01 Grille des besoins + panneau d'affectation (candidats triés,
  alertes), 02 Vue par bénévole, 03 Publication, 04 Mobile bénévole,
  05 Mobile référent, 06 Configuration (4 sous-onglets).
- i18n : aucun texte d'interface en dur, locales/fr.json.
- Vérifs : build tsc + vite, eslint (0 erreur), rendu headless des 6 écrans
  (npm run smoke).

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 5154a3c61b
commit ac469f0cd1
35 changed files with 4267 additions and 125 deletions
+206 -29
View File
@@ -1,46 +1,223 @@
{
"app.title": "Gestion des bénévoles",
"app.loading": "Chargement…",
"app.error.generic": "Une erreur est survenue",
"app.error.network": "Impossible de joindre le serveur",
"filter.all": "TOUS",
"legend.title": "LÉGENDE",
"action.close": "Fermer",
"action.remove": "Retirer",
"nav.dashboard": "Tableau de bord",
"nav.volunteers": "Bénévoles",
"nav.positions": "Postes",
"nav.shifts": "Créneaux",
"nav.assignments": "Affectations",
"nav.availabilities": "Disponibilités",
"nav.import": "Import",
"unit.hours": "{count} h",
"unit.days": "{count} j",
"time.now": "à l’instant",
"time.yesterday": "hier 18:42",
"role.coordinator": "Coordinateur",
"role.lead": "Référent",
"role.volunteer": "Bénévole",
"header.meta": "BÉNÉVOLAT · {count} INSCRITS",
"header.draft": "BROUILLON · {count} MODIF.",
"header.published": "✓ TOUT EST PUBLIÉ",
"tab.grille": "Grille des besoins",
"tab.volunteers": "Vue par bénévole",
"tab.publication": "Publication",
"tab.mobileVolunteer": "Bénévole · mobile",
"tab.mobileLead": "Référent · mobile",
"tab.config": "Configuration",
"assignment.status.proposed": "Proposée",
"assignment.status.confirmed": "Confirmée",
"assignment.status.declined": "Refusée",
"assignment.status.cancelled": "Annulée",
"assignment.source.solver": "Solveur",
"assignment.source.manual": "Manuelle",
"availability.status.available": "Disponible",
"availability.status.if_needed": "Si besoin",
"availability.status.unavailable": "Indisponible",
"position.category.bar": "Bar",
"position.category.ticketing": "Billetterie",
"position.category.welcome": "Accueil",
"position.category.parking": "Parking",
"position.category.cleaning": "Nettoyage",
"position.category.catering": "Restauration",
"position.category.cleaning": "Propreté",
"position.category.catering": "Catering",
"position.category.setup": "Montage",
"position.category.stage": "Scène",
"position.category.stage": "Régie",
"position.category.other": "Autre",
"validation.SHIFT_OVERLAP": "Deux créneaux se chevauchent",
"validation.OUTSIDE_AVAILABILITY": "En dehors des disponibilités déclarées",
"validation.DAILY_HOURS_EXCEEDED": "Plus de 8 h d'affectations sur la journée",
"validation.INSUFFICIENT_REST": "Moins de 8 h de repos entre deux créneaux",
"validation.MISSING_REQUIRED_SKILL": "Compétence requise manquante"
"validation.SHIFT_OVERLAP": "Déjà affecté sur {position} au même créneau",
"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.MISSING_REQUIRED_SKILL": "Compétence requise manquante : {skill}",
"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.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.slotDuration": "CRÉNEAU DE {hours} H",
"grille.panel.assigned": "AFFECTÉS",
"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.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.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",
"volunteers.filter.show": "AFFICHER",
"volunteers.filter.all": "TOUS",
"volunteers.filter.overloaded": "SURCHARGÉS",
"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.",
"publication.stat.changes": "MODIFICATIONS NON PUBLIÉ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.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.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}",
"design.noteTitle": "CHOIX DE CONCEPTION",
"mobileVol.statusTime": "9:41",
"mobileVol.kicker": "MON PLANNING BÉNÉVOLE",
"mobileVol.greeting": "SALUT {name}",
"mobileVol.slots": "{count} CRÉNEAUX",
"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.",
"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.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.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.",
"config.admin": "ADMINISTRATION",
"config.reserved": "RÉSERVÉ À LA COORDINATION",
"config.tab.grid": "GRILLE DES BESOINS",
"config.tab.volunteers": "BÉNÉVOLES & IMPORT",
"config.tab.mail": "MESSAGES & ENVOIS",
"config.tab.rules": "RÈGLES DU FESTIVAL",
"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.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.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.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.ficheKicker": "FICHE BÉNÉVOLE",
"config.vol.fullName": "NOM COMPLET",
"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.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.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.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"
}