Retrait des disponibilités et des limites de temps dans la journée
ci-cd / build-backend (push) Successful in 31s
ci-cd / build-frontend (push) Successful in 19s
ci-cd / deploy (push) Successful in 12s

Disponibilités déclarées supprimées (table `availability`, migration 0022,
API, éditeur de la fiche bénévole, badge de couverture du panneau
d'affectation, colonnes des statistiques) ainsi que les règles
OUTSIDE_AVAILABILITY, DAILY_HOURS_EXCEEDED et INSUFFICIENT_REST, les malus
de disponibilité et de repos du solveur et le filtre « Surchargés » de la
vue bénévoles. Seul le chevauchement reste contrôlé. La case « je suis
disponible » du formulaire public est conservée.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-05 21:29:58 +02:00
co-authored by Claude Opus 5.5
parent f671bb85bf
commit d722a81a87
40 changed files with 213 additions and 1845 deletions
+36 -53
View File
@@ -20,7 +20,6 @@ colonne, route et variable utilise la colonne « Code / base ».
| Poste | `Position` |
| Référent d'un poste | `Position.lead_account_id` → `Account` |
| Créneau | `Shift` |
| Disponibilité (fenêtre horaire libre) | `Availability` (`date`, `start_time`, `end_time`) |
| Affectation | `Assignment` |
| Artiste programmé | `Artist` (`name`, `performance_date`, `start_time`, `end_time`) |
| Vœu « je veux voir cet artiste » | `VolunteerArtist` (N-N `Volunteer` ↔ `Artist`) |
@@ -39,14 +38,14 @@ colonne, route et variable utilise la colonne « Code / base ».
| Rôle | `COORDINATOR` / `LEAD` / `VOLUNTEER` |
| Catégorie de poste | `BAR` / `TICKETING` / `WELCOME` / `PARKING` / `CLEANING` / `CATERING` / `SETUP` / `STAGE` / `OTHER` |
### Distinction critique (ne jamais confondre, ni en base ni dans l'UI)
### Pas de disponibilités déclarées
- **`Availability`** = une *déclaration* du bénévole : « le samedi, je peux
venir de 14 h à 23 h » — fenêtre horaire libre, plusieurs par jour.
- **`Assignment`** = une *décision* de l'organisation : « tu tiens le bar
samedi 18 h – 22 h ».
Deux tables distinctes, deux écrans distincts, deux vocabulaires distincts.
Les fenêtres de disponibilité (`Availability`) ont été retirées à l'étape 42
(table supprimée, migration `0022`) : l'appli ne connaît que les
**`Assignment`**, *décisions* de l'organisation (« tu tiens le bar samedi
18 h – 22 h »). Seule subsiste la case informative « je suis disponible » du
formulaire public (`Volunteer.self_declared_available`), sans aucun effet sur
les affectations.
---
@@ -63,7 +62,7 @@ les tables sauf les tables d'association.
(nombre et bornes libres). `code`, `label`, `start_time`, `end_time`
(`end_time < start_time` ⇒ passe minuit), `order`. Unique (`edition_id`,
`code`) ; `code` figé après création. Sert à générer les créneaux 0/0 des
postes (pas les disponibilités, qui sont des fenêtres libres).
postes.
CRUD `GET/POST/PATCH/DELETE /editions/{id}/day-parts` (COORDINATION pour
l'écriture).
- **`Volunteer`** : `email` (nullable, **pas unique** — plusieurs bénévoles
@@ -81,7 +80,7 @@ les tables sauf les tables d'association.
direct vers un `VolunteerCompanionWish` ; la coordination la résout à la
main depuis la fiche une fois les deux bénévoles inscrits),
`self_declared_available` (case à cocher **purement informative**, sans
lien avec `Availability`) et `willing_to_pay_fee` (« je participe à la
effet sur les affectations) et `willing_to_pay_fee` (« je participe à la
cotisation » — déclenche, lui, un vrai paiement : voir `fee_payment_label`/
`fee_amount`/`fee_checkout_id`/`fee_payment_status`/`fee_paid_at` et le
paiement SumUp plus bas).
@@ -131,14 +130,6 @@ les tables sauf les tables d'association.
0/0 par jour de l'édition et par `DayPart` manquant (bouton « Générer les
créneaux », idempotent — n'écrase jamais un créneau déjà généré, même
modifié depuis).
- **`Availability`** : `volunteer_id`, `date`, `start_time`, `end_time`
(`end_time <= start_time` ⇒ la fenêtre se termine le lendemain). Fenêtre
horaire **libre** ; plusieurs par (`volunteer`, `date`). Unique
(`volunteer_id`, `date`, `start_time`, `end_time`). Éditée en bloc :
`GET`/`PUT /editions/{id}/volunteers/{vid}/availability` (lecture : COORD /
LEAD ou le bénévole lui-même ; `PUT` : COORDINATION, remplace tout).
`GET /editions/{id}/availability` liste celles de toute l'édition (COORD /
LEAD) — pour la grille des besoins.
- **`Assignment`** : `volunteer_id`, `shift_id`, `source` (`AssignmentSource`,
défaut `MANUAL`), `locked` (défaut `false`). Unique (`volunteer_id`,
`shift_id`). **Pas de statut** : l'affectation est valide dès sa création
@@ -203,8 +194,7 @@ les tables sauf les tables d'association.
paiement — même règle que `lib/fee.ts:feeMode` —, montants par type,
mécénat, `PendingFormSubmission` jamais payées), besoins face aux
bénévoles : par **journée de festival** (§ 3), heures × personnes
demandées (min/idéal) vs heures de disponibilité déclarées vs heures
affectées ; par poste, places min/idéales, places **pourvues plafonnées
demandées (min/idéal) vs heures affectées ; par poste, places min/idéales, places **pourvues plafonnées
créneau par créneau à l'effectif idéal** (`filled` — un sureffectif ne
compense jamais un trou ailleurs ; `assigned` garde le total brut) et
nombre de vœux. Graphiques en SVG maison (`components/charts.tsx`,
@@ -386,7 +376,7 @@ Conventions techniques :
- Suppression en cascade au niveau base (`ON DELETE CASCADE`) sur tous les
`edition_id` et sur les liens vers `volunteer`, `shift`, `position`,
`artist`, `account`. La suppression complète d'un bénévole efface
donc ses `Availability`, `Assignment`, vœux (artistes, postes, compagnons —
donc ses `Assignment`, vœux (artistes, postes, compagnons —
dans les deux sens : ceux qu'il a émis et ceux qui le visaient) et son
`Account`.
- Dates/heures **naïves**, heure locale Europe/Paris (`TZ` du conteneur +
@@ -411,7 +401,6 @@ Matrice appliquée par les endpoints :
| `assignments` écriture | ✔ | ses postes | — |
| `assignments` lecture | ✔ | ✔ | les siennes |
| `artists` lecture / écriture | ✔ / ✔ | ✔ / — | ✔ / — |
| `availability` lecture / écriture | ✔ / ✔ | ✔ / — | la sienne / — |
| `matrix`, `import`, `accounts` | ✔ / ✔ / ✔ | ✔ / — / — | — |
| `badges` (config + génération) | ✔ | — | — |
| `position-sheets` (génération) | ✔ | — | — |
@@ -536,26 +525,28 @@ ni corps**). Config : `SMTP_HOST/PORT/USER/PASSWORD/STARTTLS/SSL/FROM`,
Module isolé `backend/app/validation/`. **Tests écrits avant l'implémentation**,
un cas passant + un cas déclenchant par règle.
Retourne une liste de violations typées `BLOCKING` ou `WARNING`. Chaque
Retourne une liste de violations typées `BLOCKING` ou `WARNING` (aucune règle
`WARNING` aujourd'hui : le type reste prévu par le contrat de l'API). Chaque
violation est un **code** + des paramètres structurés, jamais une phrase (la
traduction se fait via `locales/fr.json`).
| Code | Sévérité | Règle |
|---------------------------|------------|-------------------------------------------------------------|
| `SHIFT_OVERLAP` | `BLOCKING` | Deux créneaux d'un même bénévole se chevauchent. |
| `OUTSIDE_AVAILABILITY` | `WARNING` | Le créneau n'est pas entièrement couvert par les fenêtres de disponibilité déclarées ce jour-là (params : `date`, `uncovered_minutes`). |
| `DAILY_HOURS_EXCEEDED` | `WARNING` | > 8 h 00 d'affectations cumulées sur une même journée. |
| `INSUFFICIENT_REST` | `WARNING` | < 8 h 00 entre la fin d'un créneau et le début du suivant. |
**Aucune limite de temps dans la journée** (étape 42) : plus de cumul
journalier maximal (`DAILY_HOURS_EXCEEDED`, ex-8 h), plus de repos minimal
entre deux créneaux (`INSUFFICIENT_REST`, ex-8 h), plus de contrôle de
disponibilité (`OUTSIDE_AVAILABILITY`). Ni la validation ni le solveur n'en
tiennent compte.
Sémantique figée :
- Toutes les affectations du bénévole comptent pour chevauchement / cumul /
repos (une affectation = une décision, toujours active).
- `OUTSIDE_AVAILABILITY` : on prend l'**union** des fenêtres `Availability` du
bénévole (converties en instants, minuit géré) ; les minutes du créneau hors
de cette union déclenchent l'avertissement. Aucune fenêtre déclarée ⇒
créneau entièrement à découvert.
- « Journée » (cumul 8 h) : commence au **début du plus long creux** que
- Toutes les affectations du bénévole comptent pour le chevauchement (une
affectation = une décision, toujours active).
- « Journée de festival » (plus utilisée par la validation elle-même, mais par
les grilles, les feuilles de poste, les statistiques et le chaînage du
solveur) : commence au **début du plus long creux** que
laissent les `DayPart` sur le cadran de 24 h, c.-à-d. à l'heure où le
dernier créneau type de la journée se termine (Matin 08–13, Après-midi
13–19, Soirée 19–02 ⇒ journée de 02:00 à 02:00). **Indépendant de
@@ -574,8 +565,6 @@ Sémantique figée :
`shiftDatesForFestivalDay`), une heure de début avant cette borne désigne
la nuit qui suit (00:00 sur le samedi ⇒ dimanche 00:00), pas le petit
matin du même jour calendaire.
- Cumul : `> 8 h 00` strict (8 h 00 pile passe).
- Repos : `< 8 h 00` strict.
- Créneaux jointifs (`end_at == start_at`) : **pas** un chevauchement.
- Une violation `BLOCKING` empêche l'enregistrement, sans dérogation.
- Une violation `WARNING` est retournée à l'appelant mais n'empêche rien
@@ -607,13 +596,9 @@ affectation n'est jamais persistée par cet endpoint.
moment où la coordination confirme une affectation :
- un créneau encore sous son effectif **minimal** prime largement sur tout
le reste (le solveur doit presque toujours proposer quelqu'un pour un
trou urgent, quitte à ce que ce soit un pis-aller signalé ensuite par un
avertissement) ;
trou urgent, quitte à ce que ce soit un pis-aller) ;
- bonus si le poste fait partie des vœux du bénévole
(`VolunteerPositionWish`) ;
- malus proportionnel aux minutes hors des fenêtres de disponibilité
déclarées (réutilise `validation.engine.uncovered_minutes`), et si le
repos avec une affectation déjà décidée serait `< 8 h 00` ;
- malus si le bénévole a déjà (affectation décidée) le créneau
immédiatement précédent, le même jour — **n'importe quel poste**
(`SolverShift.predecessor_shift_ids`) : limite les enchaînements de
@@ -626,11 +611,9 @@ affectation n'est jamais persistée par cet endpoint.
au créneau — plusieurs compagnons présents cumulent le bonus ;
- léger malus pour les bénévoles déjà bien chargés (répartition de la
charge).
- **Pas de plafond de cumul journalier côté solveur** : contrairement à la
validation (`DAILY_HOURS_EXCEEDED`, § 3), le solveur ne pénalise pas un
bénévole qui dépasserait 8 h sur la journée — seule la validation le
signale, à la confirmation.
- Simplification assumée : repos, chaînage et compagnons ne sont vérifiés
- **Aucune limite de temps** : ni cumul journalier, ni repos minimal, ni
disponibilité (§ 3).
- Simplification assumée : chaînage et compagnons ne sont vérifiés
que contre les affectations **déjà décidées**, pas entre deux suggestions
du même calcul (deux compagnons suggérés ensemble dans le même calcul ne
se renforcent pas mutuellement) — la validation finale à la confirmation
@@ -638,9 +621,8 @@ affectation n'est jamais persistée par cet endpoint.
**Classement par défaut, sans lancer le solveur** — `frontend/src/lib/planning.ts`
(`candidateChanceScore`) porte une version simplifiée du même scoring (vœu de
poste, disponibilité y compris conflit d'artiste, compagnons souhaités déjà
présents sur le créneau, charge déjà affectée ; pas de cumul 8 h / repos /
chaînage, déjà couverts par les badges de validation). C'est ce
poste, conflit d'artiste, compagnons souhaités déjà présents sur le créneau,
charge déjà affectée ; pas de chaînage). C'est ce
qui classe la colonne des candidats dans le panneau d'affectation dès
l'ouverture du détail d'un poste, sans requête réseau. Si le solveur a
effectivement tourné (`POST .../solver/suggestions`), ses bénévoles retenus
@@ -693,7 +675,7 @@ adapté à son tableau bénévole × créneau.
1. Modèle de données complet + migrations Alembic.
2. Import CSV/Excel (identité des bénévoles : nom, email, téléphone,
commentaire — pas les disponibilités, qui se déclarent dans l'app), en deux
commentaire), en deux
temps : analyse (colonnes détectées, aperçu) puis exécution avec mapping
validé. Rapport ligne par ligne. Détection des doublons : upsert sur
(`email`, `last_name` + `first_name`) — l'email seul ne suffit pas,
@@ -706,7 +688,7 @@ adapté à son tableau bénévole × créneau.
5. Endpoint matrice d'une édition :
`[{ position, shifts: [{ shift, min_staff, ideal_staff, assigned }] }]`.
6. Jeu de données de démonstration : 120 bénévoles, 15 postes, 4 jours,
quelques trous d'effectif, 2–3 avertissements actifs, programmation
quelques trous d'effectif, programmation
(artistes les jours de festival) et vœux des bénévoles (postes, artistes)
pour que le solveur ait matière à arbitrer.
7. Solveur de suggestions OR-Tools CP-SAT (section 4) — en cours.
@@ -746,7 +728,7 @@ l'étape 21.)*
│ │ ├── api/ # routers + schemas Pydantic + deps
│ │ │ # health, auth, accounts, editions,
│ │ │ # day_parts, imports, artists, volunteers,
│ │ │ # availability, positions, shifts,
│ │ │ # positions, shifts,
│ │ │ # assignments, solver, matrix, notifications,
│ │ │ # badges, position_sheets, forms, stats
│ │ ├── auth/ # mot de passe + lien magique + JWT + rôles
@@ -806,8 +788,8 @@ Aucune dépendance locale requise hors Docker.
- `Edition.retention_days` : durée de rétention paramétrable.
- Endpoint de suppression complète d'un bénévole : *hard delete* en cascade.
- **Dates et heures :** stockées naïves, interprétées en heure locale
Europe/Paris (heure murale). Les règles « 8 h », « journée », « demi-journée »
sont toutes en heure locale.
Europe/Paris (heure murale). La « journée de festival » est en heure
locale.
- **Validation TDD :** tests avant implémentation, un cas passant + un cas
déclenchant par règle.
- **Git :** un commit atomique par étape validée, message en français.
@@ -892,6 +874,7 @@ Aucune dépendance locale requise hors Docker.
| 39 | Mode de paiement de la cotisation visible partout (`lib/fee.ts:feeMode`, `app/exporting/xlsx.py:_payment_mode`) : en ligne (type, montant, statut, mécénat), sur place, ou rien — section « Cotisation » sur la fiche (case « Paiera sur place » modifiable à la main), colonne « Cotisation » dans la liste, colonne « Mode de paiement » dans l'export Excel | à valider |
| 40 | Écran Statistiques (`/statistiques`, COORDINATION, `GET /editions/{id}/stats`) : inscriptions (nombre, évolution cumulée), réponses au formulaire, artistes demandés, cotisations (en ligne / sur place / aucune, par type, mécénat, paiements jamais aboutis), besoins par journée de festival face aux disponibilités déclarées et aux affectations, places pourvues et vœux par poste — graphiques SVG maison avec infobulles et vues tableau | à valider |
| 41 | Début de « journée de festival » indépendant de l'ordre des créneaux types : début du plus long creux qu'ils laissent sur 24 h (`validation.engine.day_boundary`, miroir `lib/planning.ts:dayBoundaryFromDayParts`). Avant, un créneau type d'ordre 0 déplacé à 21:00 faisait commencer chaque journée des grilles (besoins, vue bénévoles) à 21:00, jusqu'à 03:00 le lendemain | à valider |
| 42 | Retrait des disponibilités déclarées (table `availability` supprimée, migration `0022` ; API, éditeur de la fiche bénévole, badge de couverture du panneau d'affectation, colonnes des statistiques) et des limites de temps dans la journée (règles `OUTSIDE_AVAILABILITY`, `DAILY_HOURS_EXCEEDED`, `INSUFFICIENT_REST`, malus de disponibilité et de repos du solveur, filtre « Surchargés » de la vue bénévoles) — seul le chevauchement reste contrôlé. La case « je suis disponible » du formulaire public est conservée | à valider |
**Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur
de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans +