Badges : format dérivé de l'image importée, postes avec horaire et référent

- Le format du badge n'est plus fixé à CR80 : il est déduit des pixels de
  l'image du recto à l'import (résolution supposée 300 dpi,
  app/badges/pdf.py:image_size_mm), stocké sur BadgeTemplate (migration
  0012). L'éditeur (BadgesPage.tsx) et la génération PDF s'alignent sur ce
  format réel ; CR80 reste le repère par défaut tant qu'aucune image n'est
  importée. Le verso est ajusté sans déformation (preserveAspectRatio).

- Le champ « Postes affectés » liste désormais une ligne par créneau
  (triées chronologiquement) : nom du poste + horaire, et le référent du
  poste s'il y en a un et que ce n'est pas le bénévole badgé lui-même.
  Texte multi-lignes pris en charge par la génération PDF (découpe sur
  `\n`, hauteur de ligne calculée en mm) et par l'aperçu de l'éditeur.

- Corrige au passage un bug d'unités dans le positionnement vertical du
  texte (police en points confondue avec des millimètres).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-09-20 20:35:11 +02:00
co-authored by Claude Sonnet 5
parent b7fc80a7dc
commit 579ea5d9db
17 changed files with 534 additions and 87 deletions
+19 -7
View File
@@ -138,16 +138,28 @@ les tables sauf les tables d'association.
demande). `front_image`/`front_image_content_type` (fond du recto),
`back_image`/`back_image_content_type` (verso — **le même pour tous les
bénévoles**). Images stockées en base (`LargeBinary`), pas de volume de
fichiers à gérer.
fichiers à gérer. **Le format du badge n'est pas fixe** :
`front_image_width_mm`/`front_image_height_mm` sont déduits des pixels de
l'image du recto à l'import (résolution supposée 300 dpi — voir
`app/badges/pdf.py:image_size_mm`), donc le badge prend la forme de
l'image importée ; `NULL` tant qu'aucune image n'est importée, l'éditeur
et la génération retombant alors sur le format CR80 par défaut (85,6 ×
54 mm, `DEFAULT_BADGE_WIDTH_MM`/`HEIGHT_MM`). Le verso est ajusté (sans
déformation, `preserveAspectRatio=True`) à ce même format.
- **`BadgeField`** : un champ positionné sur le recto (`field_type` ∈
`FULL_NAME` / `POSITIONS` / `CATEGORY` / `ROLE`, au plus un par édition —
`PUT .../badges/template/fields` remplace tout), coordonnées `x_mm`/`y_mm`/
`width_mm`/`height_mm` en millimètres depuis le coin haut-gauche du badge
(format CR80, 85,6 × 54 mm), `font_size`, `align`, `bold`, `color`. Édité
visuellement (glisser-déposer) depuis l'écran « Badges » du front-end
(`frontend/src/pages/BadgesPage.tsx`), qui envoie ces mêmes millimètres.
`POSITIONS`/`CATEGORY` résolus depuis les affectations décidées du
bénévole, `ROLE` depuis son `Account.role` (`LEAD` ⇒ « Référent »).
`width_mm`/`height_mm` en millimètres depuis le coin haut-gauche du badge,
`font_size`, `align`, `bold`, `color`. Édité visuellement (glisser-déposer)
depuis l'écran « Badges » du front-end (`frontend/src/pages/BadgesPage.tsx`),
qui envoie ces mêmes millimètres et redimensionne son aperçu au format réel
du badge (`GET .../badges/template` renvoie `badge_width_mm`/`height_mm`).
`POSITIONS` résout **une ligne par créneau affecté**, triée
chronologiquement : nom du poste + horaire (`HH:MM–HH:MM`), suivi du
référent du poste (`Position.lead_account_id` → son bénévole) s'il y en a
un et que ce n'est pas le bénévole badgé lui-même (texte multi-lignes,
`\n`) ; `CATEGORY` liste les catégories distinctes de ces mêmes postes ;
`ROLE` vient de `Account.role` du bénévole (`LEAD` ⇒ « Référent »).
`POST /editions/{id}/badges/generate` (COORDINATION, `{volunteer_ids?}` —
absent = tous) génère la planche PDF (`app/badges/pdf.py`, OR-Tools n'y est
pour rien : juste ReportLab) : pages recto personnalisées suivies, planche