Bénévoles : ajout manuel, import de fichier guidé et gestion complète

Écran « Bénévoles & import » remanié autour des trois méthodes d'ajout
demandées (la 3e — formulaire d'inscription en ligne — reste à faire) :

- Ajout manuel : formulaire complet (prénom, nom, email, téléphone,
  taille de t-shirt, régime, commentaire, compétences) ; l'erreur 409
  `volunteer.duplicate_email` est affichée.
- Import de fichier : assistant en 3 étapes (fichier → association des
  colonnes + colonnes de disponibilité optionnelles → simulation puis
  import réel) avec aperçu et rapport ligne par ligne.
- Gestion : recherche par nom / email, fiche éditable étendue (régime et
  commentaire ajoutés), suppression RGPD en cascade inchangée.

Le bouton « créer 4 exemples » (maquette) est retiré. Traductions
`config.vol.*` et miroir des messages `import.*` du back-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-10 21:55:59 +02:00
co-authored by Claude Sonnet 5
parent df8d6733db
commit ccf3438eef
2 changed files with 869 additions and 101 deletions
+76 -22
View File
@@ -4,10 +4,8 @@
"action.close": "Fermer",
"action.remove": "Retirer",
"action.add": "Ajouter",
"app.loading": "Chargement…",
"app.error.generic": "Une erreur est survenue",
"error.generic": "Erreur inattendue",
"error.not_found": "Ressource introuvable",
"error.forbidden": "Accès refusé",
@@ -20,11 +18,9 @@
"shift.position_wrong_edition": "Le poste n'appartient pas à cette édition",
"auth.invalid_credentials": "Email ou mot de passe incorrect",
"auth.invalid_token": "Ce lien de connexion est invalide ou expiré",
"role.coordinator": "Coordination",
"role.lead": "Référent",
"role.volunteer": "Bénévole",
"auth.logout": "Déconnexion",
"auth.email": "Email",
"auth.password": "Mot de passe",
@@ -46,17 +42,14 @@
"auth.magic.verifyTitle": "Connexion en cours",
"auth.magic.verifying": "Vérification du lien…",
"auth.magic.retry": "Demander un nouveau lien",
"header.title": "GESTION DES BÉNÉVOLES",
"header.meta": "BÉNÉVOLAT · {count} INSCRITS",
"header.mockMode": "DONNÉES DE DÉMO",
"tab.grille": "Grille des besoins",
"tab.volunteers": "Vue par bénévole",
"tab.mobileVolunteer": "Bénévole · mobile",
"tab.mobileLead": "Référent · mobile",
"tab.config": "Configuration",
"position.category.bar": "Bar",
"position.category.ticketing": "Billetterie",
"position.category.welcome": "Accueil",
@@ -66,7 +59,6 @@
"position.category.setup": "Montage",
"position.category.stage": "Régie",
"position.category.other": "Autre",
"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.",
@@ -79,7 +71,6 @@
"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.critical": "SOUS-EFFECTIF CRITIQUE",
@@ -107,7 +98,6 @@
"grille.panel.blocked": "BLOQUÉ",
"grille.panel.blockingPrefix": "BLOQUANT — {reason}",
"grille.panel.warningsTitle": "⚠ AVERTISSEMENTS — AFFECTATION POSSIBLE",
"volunteers.filter.show": "AFFICHER",
"volunteers.filter.all": "TOUS",
"volunteers.filter.overloaded": "SURCHARGÉS",
@@ -122,9 +112,7 @@
"volunteers.empty": "Aucun bénévole ne correspond à ce filtre — bonne nouvelle. Repassez sur « TOUS » pour revoir l'ensemble de l'équipe.",
"volunteers.noData": "Aucun bénévole pour le moment. Ajoutez-les dans Configuration → « Bénévoles & import ».",
"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.",
"design.noteTitle": "CHOIX DE CONCEPTION",
"mobileVol.statusTime": "9:41",
"mobileVol.kicker": "MON PLANNING BÉNÉVOLE",
"mobileVol.greeting": "SALUT {name}",
@@ -136,7 +124,6 @@
"mobileVol.noAccount": "Ce compte n'est rattaché à aucun bénévole. Contactez la coordination.",
"mobileVol.detailsUnavailable": "Le détail des créneaux (poste, horaires) n'est pas encore exposé aux bénévoles par l'API.",
"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",
"mobileLead.inProgress": "CRÉNEAU · {hours}",
@@ -150,14 +137,12 @@
"mobileLead.markAbsent": "ABSENT",
"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": "POSTES & CRÉNEAUX",
"config.tab.volunteers": "BÉNÉVOLES & IMPORT",
"config.tab.mail": "MESSAGES",
"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",
@@ -172,10 +157,8 @@
"config.grid.generateShifts": "GÉNÉRER LES CRÉNEAUX",
"config.grid.generated": "{count} créneau(x) ajouté(s).",
"config.grid.note": "Effectifs, créneaux, nom et catégorie du poste sont enregistrés immédiatement via l'API. Pour ajouter ou retirer des jours, modifiez les dates de l'édition dans l'onglet « Règles & édition ».",
"config.vol.importTitle": "IMPORTER DES INSCRIPTIONS",
"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.importTitle": "IMPORTER DES BÉNÉVOLES",
"config.vol.importHint": "Trois façons d'ajouter des bénévoles : saisie manuelle, import d'un fichier CSV / Excel, ou (à venir) le formulaire d'inscription en ligne.",
"config.vol.profiles": "PROFILS ENREGISTRÉS",
"config.vol.colName": "NOM",
"config.vol.colPhone": "TÉLÉPHONE",
@@ -193,7 +176,6 @@
"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.smtpBanner": "Envoie un récapitulatif de planning à chaque bénévole. L'email part si le serveur SMTP est configuré (EMAIL_ENABLED) ; sinon il est seulement journalisé.",
"config.mail.templateTitle": "MODÈLE DE MESSAGE",
"config.mail.senderNote": "Expéditeur défini côté serveur (SMTP_FROM).",
@@ -208,7 +190,6 @@
"config.mail.loginLinkLine": "Accède à ton planning : {lien_connexion}",
"config.mail.previewTitle": "APERÇU",
"config.mail.previewSender": "DE : expéditeur configuré côté serveur",
"config.rules.editionTitle": "ÉDITION",
"config.rules.editionName": "Nom",
"config.rules.editionStart": "Premier jour",
@@ -238,5 +219,78 @@
"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"
"config.rules.roleVolunteerDesc": "Ses seules données, lien magique par email",
"config.vol.addButton": "AJOUTER MANUELLEMENT",
"config.vol.importButton2": "IMPORTER UN FICHIER",
"config.vol.search": "Rechercher un nom, un email…",
"config.vol.emptyList": "Aucun bénévole pour l'instant. Ajoutez-en un ou importez un fichier.",
"config.vol.noMatch": "Aucun bénévole ne correspond à la recherche.",
"config.vol.count": "{count} bénévole(s)",
"config.vol.cancel": "ANNULER",
"config.vol.field.firstName": "Prénom",
"config.vol.field.lastName": "Nom",
"config.vol.field.email": "Email (lien magique)",
"config.vol.field.phone": "Téléphone",
"config.vol.field.tshirt": "Taille de t-shirt",
"config.vol.field.diet": "Régime alimentaire",
"config.vol.field.comment": "Commentaire",
"config.vol.field.skills": "Compétences",
"config.vol.addTitle": "NOUVEAU BÉNÉVOLE",
"config.vol.addHint": "Le bénévole reçoit un accès par lien magique uniquement si un email est renseigné.",
"config.vol.create": "CRÉER LE BÉNÉVOLE",
"config.vol.requiredNames": "Le prénom et le nom sont obligatoires.",
"config.vol.wiz.title": "IMPORTER UN FICHIER",
"config.vol.wiz.step1": "1 · FICHIER",
"config.vol.wiz.step2": "2 · COLONNES",
"config.vol.wiz.step3": "3 · APERÇU & IMPORT",
"config.vol.wiz.pickFile": "CHOISIR UN FICHIER CSV OU EXCEL",
"config.vol.wiz.fileHint": "Une ligne par bénévole. La première ligne contient les intitulés de colonnes. En mode démonstration, seuls les fichiers CSV sont pris en charge.",
"config.vol.wiz.analyzing": "Analyse du fichier…",
"config.vol.wiz.detected": "{rows} ligne(s) · {cols} colonne(s) détectée(s)",
"config.vol.wiz.preview": "APERÇU DES PREMIÈRES LIGNES",
"config.vol.wiz.mapTitle": "ASSOCIER LES COLONNES",
"config.vol.wiz.mapHint": "« Prénom » et « Nom » sont obligatoires. Laissez « — » pour ignorer une colonne.",
"config.vol.wiz.ignore": "—",
"config.vol.wiz.availTitle": "DISPONIBILITÉS (OPTIONNEL)",
"config.vol.wiz.availHint": "Associez une colonne du fichier à une date et une demi-journée pour importer les disponibilités déclarées.",
"config.vol.wiz.availAdd": "AJOUTER UNE COLONNE DE DISPONIBILITÉ",
"config.vol.wiz.availColumn": "Colonne",
"config.vol.wiz.availDate": "Date",
"config.vol.wiz.availPart": "Demi-journée",
"config.vol.wiz.mapMissing": "Associez au moins les colonnes « Prénom » et « Nom ».",
"config.vol.wiz.simulate": "SIMULER",
"config.vol.wiz.runImport": "IMPORTER POUR DE VRAI",
"config.vol.wiz.back": "RETOUR",
"config.vol.wiz.next": "SUIVANT",
"config.vol.wiz.dryRunTitle": "SIMULATION — RIEN N'A ÉTÉ ENREGISTRÉ",
"config.vol.wiz.doneTitle": "IMPORT TERMINÉ",
"config.vol.wiz.created": "{count} créé(s)",
"config.vol.wiz.updated": "{count} mis à jour",
"config.vol.wiz.skipped": "{count} ignoré(s)",
"config.vol.wiz.availWritten": "{count} disponibilité(s)",
"config.vol.wiz.skillsCreated": "Compétences créées : {list}",
"config.vol.wiz.rowsWithIssues": "LIGNES AVEC REMARQUES",
"config.vol.wiz.line": "Ligne {line}",
"config.vol.wiz.noIssues": "Aucune remarque : tout est prêt à être importé.",
"config.vol.wiz.close": "FERMER",
"import.error.empty_file": "Le fichier est vide.",
"import.error.encoding": "L'encodage du fichier n'est pas reconnu.",
"import.error.unreadable_file": "Fichier illisible ou format non pris en charge.",
"import.error.file_too_large": "Le fichier est trop volumineux (5 Mo maximum).",
"import.error.xlsx_demo_only": "En mode démonstration, l'import Excel n'est pas disponible : exportez votre fichier en CSV.",
"import.mapping.invalid": "Le paramétrage des colonnes est invalide.",
"import.mapping.required_field_missing": "Le champ « {field} » doit être associé à une colonne.",
"import.mapping.column_not_found": "La colonne « {column} » est absente du fichier.",
"import.mapping.unknown_day_part": "Demi-journée « {day_part_code} » inconnue (colonne « {column} »).",
"import.mapping.date_outside_edition": "La date {date} de la colonne « {column} » est hors des dates de l’édition.",
"import.mapping.duplicate_availability_column": "La colonne « {column} » est associée plusieurs fois.",
"import.row.missing_required_field": "Champ obligatoire manquant : {field}.",
"import.row.invalid_email": "Adresse email invalide : {value}.",
"import.row.duplicate_email_in_file": "Email déjà présent à la ligne {first_line} du fichier.",
"import.row.duplicate_name_in_file": "Nom et prénom déjà présents à la ligne {first_line} du fichier.",
"import.row.updated_existing": "Bénévole existant mis à jour.",
"import.row.possible_duplicate_name": "Un bénévole portant le même nom existe déjà dans l’édition.",
"import.row.invalid_tshirt_size": "Taille de t-shirt non reconnue : {value}.",
"import.row.skill_created": "Nouvelle compétence créée : {label}.",
"import.row.unknown_availability_value": "Valeur de disponibilité non comprise « {value} » (colonne « {column} »)."
}