diff --git a/CLAUDE.md b/CLAUDE.md index 2f6b162..a9ac4fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,21 +75,14 @@ les tables sauf les tables d'association. `wished_companion_ids` ; `None`/absent = inchangé, `[]` = vider). Renseignables aussi via le formulaire public (§ 9) : `photo`/ `photo_content_type` (image, servie séparément par - `GET .../volunteers/{id}/photo`, COORD/LEAD) — recadrée en carré côté - navigateur avant l'envoi (`PublicSignupPage.tsx:cropToSquareJpeg`, centré, - tronque le surplus, réencodée en JPEG ≤ 1024 px de côté), donc toujours - carrée en base sans recadrage serveur ; aperçu agrandi (180 px) dès que la - photo est choisie. `companion_wish_note` (« je + `GET .../volunteers/{id}/photo`, COORD/LEAD), `companion_wish_note` (« je voudrais être avec » en texte libre — la personne visée n'existe pas forcément encore en base au moment de l'inscription, donc pas de lien 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`/`willing_to_pay_fee` (cases à cocher **purement - informatives** : rien ne vérifie qu'un paiement a réellement eu lieu, - aucun lien avec `Availability`). Si `FormTemplate.yapla_url` (ci-dessous) - est configuré, un encart « Payer ma cotisation » apparaît sous la case - `WILLING_TO_PAY_FEE`, vers la page de cotisation Yapla (externe) — la - déclaration et le paiement restent deux choses distinctes, non reliées. + informatives**, sans lien avec `Availability` ni aucun workflow de + paiement). - **`Artist`** : programmation de l'édition (`name`, `performance_date`, `start_time`, `end_time` — `end_time <= start_time` ⇒ le passage finit le lendemain). Unique (`edition_id`, `name`, `performance_date`). La @@ -195,29 +188,13 @@ les tables sauf les tables d'association. bénévole ; une feuille identifie un poste pour la journée) : une page par poste et par jour, avec son référent (`Position.lead_account_id`, nom + téléphone — « à désigner » si aucun) en en-tête, puis les créneaux du jour - avec les bénévoles affectés, leur téléphone et leur photo (`Volunteer.photo` - — vignette carrée 14 mm, proportions préservées, case vide si le bénévole - n'en a pas), pour affichage sur place. Sous le référent, la programmation - du jour (`Artist`, § 2) : tous les passages de cette date, quel que soit le - poste — repère utile même hors de la régie (affluence au bar, sécurité...) - — triés par heure, omise si aucun artiste ne joue ce jour-là. + avec les bénévoles affectés et leur téléphone, pour affichage sur place. `POST /editions/{id}/position-sheets/generate` (COORDINATION, `{position_ids?}` — absent = tous) ; un créneau à besoin 0/0 sans aucune affectation n'apparaît pas (rien à imprimer). - **`FormTemplate`** : configuration du formulaire public d'inscription d'une édition (une ligne, créée à la demande). `is_open` coupe les - nouvelles soumissions sans effacer la configuration. `logo`/ - `logo_content_type` (image, `PUT .../form/logo`, stockée en base comme les - autres images de l'appli), `description` (texte libre) et `yapla_url` - habillent la page publique — aucun rapport avec les champs collectés - (`FormField` ci-dessous) : affichés au-dessus/dans le formulaire, jamais - soumis par le bénévole. `yapla_url` : lien vers la page de cotisation sur - Yapla (yapla.com, plateforme externe de paiement pour associations) — - affiché en encart sous la case `WILLING_TO_PAY_FEE` si activée (voir - `Volunteer` ci-dessus) ; doit commencer par `http://`/`https://` - (422 `form.invalid_yapla_url` sinon), aucune autre vérification (rien ne - confirme qu'un paiement a eu lieu). `PATCH /editions/{id}/form` prend - `is_open`/`description`/`yapla_url`, chacun optionnel (absent = inchangé). + nouvelles soumissions sans effacer la configuration. - **`FormField`** : un champ activé sur le formulaire (`field_type` ∈ `EMAIL` / `PHONE` / `POSITION_WISHES` / `ARTIST_WISHES` / `COMPANION_WISH` / `AVAILABLE` / `WILLING_TO_PAY_FEE` / `PHOTO` / `COMMENT`, au plus un par @@ -229,16 +206,13 @@ les tables sauf les tables d'association. souhaité par jour de festival (`app/api/forms.py:_check_one_artist_wish_per_day`). `GET/PUT/PATCH /editions/{id}/form[...]` (COORDINATION) configurent ; `GET /editions/{id}/form/public` et `POST .../form/public/submit` sont les - **seuls endpoints de toute l'API sans authentification qui écrivent ou - révèlent des données de l'édition** — protégés par hCaptcha - (`app/forms/captcha.py`, `HCAPTCHA_SECRET_KEY` ; vérification désactivée si - absente, comme `EMAIL_ENABLED=false`) et un filet de sécurité global par - édition (`app/auth/rate_limit.py:form_submission_limiter`, hCaptcha reste - la défense principale). `GET .../form/logo` est également public (affiché - sur la page), mais ne sert qu'une image choisie par la coordination elle- - même — aucune saisie ni donnée nominative, pas de protection hCaptcha - nécessaire. Le bénévole créé par une soumission apparaît immédiatement - dans la liste de la coordination, comme n'importe quel autre bénévole. + **deux seuls endpoints de toute l'API sans authentification** — protégés + par hCaptcha (`app/forms/captcha.py`, `HCAPTCHA_SECRET_KEY` ; vérification + désactivée si absente, comme `EMAIL_ENABLED=false`) et un filet de + sécurité global par édition (`app/auth/rate_limit.py:form_submission_limiter`, + hCaptcha reste la défense principale). Le bénévole créé par une soumission + apparaît immédiatement dans la liste de la coordination, comme n'importe + quel autre bénévole. Conventions techniques : @@ -277,8 +251,8 @@ Matrice appliquée par les endpoints : | `matrix`, `import`, `accounts` | ✔ / ✔ / ✔ | ✔ / — / — | — | | `badges` (config + génération) | ✔ | — | — | | `position-sheets` (génération) | ✔ | — | — | -| `form` (config, dont logo) | ✔ | — | — | -| `form/public`, `form/public/submit`, `form/logo` | *(aucune authentification — voir § 2, `FormTemplate`/`FormField`)* ||| +| `form` (config) | ✔ | — | — | +| `form/public`, `form/public/submit` | *(aucune authentification — voir § 2, `FormField`)* ||| **Authentification — deux systèmes selon le compte**, une même session JWT HS256 (`Authorization: Bearer `, TTL `session_ttl_hours`) à l'arrivée. @@ -475,10 +449,16 @@ suivantes — bug corrigé une fois (postes manquants au-delà du premier), un positionnement implicite (auto-flow) recréerait la même classe de bug. Vérifié par `scripts/smoke.tsx:checkGridLayout` (inspecte les `grid-row` rendus, pas seulement l'absence de crash — un décalage CSS n'est pas visible -dans un simple rendu SSR). Largeur adaptative par jour (`columnWidthPx`, en -fonction du nombre de tranches) : peu de tranches ⇒ colonnes larges, beaucoup -⇒ largeur compacte avec défilement horizontal — jamais de colonnes étroites -par défaut. +dans un simple rendu SSR). Hiérarchie de `z-index` volontairement large et +sans égalité (coins figés 20/21 > en-têtes jour/heure 12/11 > libellés de +poste 10 > cellules de créneau, chacune isolée dans son propre contexte +d'empilement — `relative z-0` — pour que son infobulle au survol, `z-20`, +ne rivalise jamais avec les en-têtes figés) : deux éléments sticky à égalité +de `z-index` peuvent se chevaucher de façon imprévisible pendant un +défilement selon le navigateur. Largeur adaptative par jour (`columnWidthPx`, +en fonction du nombre de tranches) : peu de tranches ⇒ colonnes larges, +beaucoup ⇒ largeur compacte avec défilement horizontal — jamais de colonnes +étroites par défaut. La « Vue bénévoles » (`VolunteerViewPage.tsx`) reste sur l'ancien modèle, distinct : une colonne par **créneau** (signature `starts_at`/`ends_at`) plutôt qu'un axe temporel continu (`buildLattice` / `LatticeColumn`), plus @@ -541,7 +521,7 @@ l'étape 21.)* │ │ ├── main.py # instance FastAPI │ │ ├── config.py # settings (pydantic-settings) │ │ ├── db.py # engine + session -│ │ ├── logging_config.py # pas de PII dans les logs +│ │ ├── logging_config.py # RGPD : pas de PII dans les logs │ │ ├── api/ # routers + schemas Pydantic + deps │ │ │ # health, auth, accounts, editions, │ │ │ # day_parts, imports, artists, volunteers, @@ -670,11 +650,7 @@ Aucune dépendance locale requise hors Docker. | 21 | Formulaire public d'inscription (`/inscription`, sans authentification) : champs activables par la coordination (email, téléphone, vœux de poste — max 2, vœux d'artiste — un par soir, vœu de compagnon en texte libre, disponibilité/cotisation déclaratives, photo, commentaire), captcha hCaptcha, ouverture/fermeture, lien à partager, écran de configuration dédié (`/inscriptions`, COORDINATION) — les bénévoles créés rejoignent directement la liste | à valider | | 22 | Feuilles de poste (PDF, complément des badges) : une page par poste et par jour avec le référent, les créneaux, les bénévoles affectés et leur téléphone — bouton depuis la grille des besoins (COORDINATION) ; photo du bénévole affichée sur son écran et sur l'écran référent (en face du nom de chaque membre de l'équipe) | à valider | | 23 | Créneaux : plus de génération automatique à la création d'un poste — création manuelle à horaire libre (`ConfigGrid.tsx`), génération depuis les créneaux types conservée en option (« Générer les créneaux ») ; grille des besoins redessinée en vrai axe des temps (une colonne = 30 min, bornée à la plage utilisée par jour, cases fusionnées pour chaque créneau — `buildTimeGrid`/`placeInTimeGrid`) | à valider | -| 24 | Feuilles de poste : photo des bénévoles affectés à côté de leur nom (vignette 14 mm, case vide si absente) | à valider | -| 25 | Formulaire public : photo recadrée en carré côté navigateur avant l'envoi, aperçu agrandi ; personnalisation de la page (logo, texte d'introduction) depuis l'écran de configuration (`/inscriptions`) | à valider | -| 26 | Feuilles de poste : programmation du jour (horaires des artistes, tous postes confondus) sous le référent | à valider | -| 27 | Formulaire public : encart de paiement de la cotisation via Yapla (lien externe configurable), affiché sous la case correspondante | à valider | -| 28 | Correctif grille des besoins : positionnement explicite (`gridColumn`/`gridRow`) de chaque cellule — un poste aux créneaux non alignés sur 30 min ne décale plus les postes suivants (noms de poste manquants au-delà du premier) | à valider | +| 24 | Corrections de stabilité de la grille des besoins (suite à l'étape 23) : positionnement explicite (`gridColumn`/`gridRow`) de chaque cellule (un poste aux créneaux non alignés sur 30 min ne décale plus les postes suivants) ; hiérarchie de `z-index` sans égalité entre les en-têtes figés et isolation de l'infobulle de survol dans son propre contexte d'empilement (ne recouvrait plus la ligne « Affectés / idéal » pendant un défilement) | à valider | **Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans + diff --git a/frontend/src/pages/GrillePage.tsx b/frontend/src/pages/GrillePage.tsx index 0a71a4f..997e29d 100644 --- a/frontend/src/pages/GrillePage.tsx +++ b/frontend/src/pages/GrillePage.tsx @@ -223,7 +223,7 @@ export function GrillePage() {
{t('grille.positionsCount', { count: matrix.data.positions.length })} @@ -231,7 +231,7 @@ export function GrillePage() { {shownDays.map((day, di) => (
{t('grille.assignedRequired')}
@@ -254,7 +254,7 @@ export function GrillePage() {
{label}
@@ -354,7 +354,7 @@ function PositionRow({ <>
{position.name} @@ -443,7 +443,16 @@ function ShiftCell({ onClick={() => onSelect(shift.shift_id)} style={{ gridColumn, gridRow }} className={cx( - 'group relative flex h-9 cursor-pointer items-center justify-center gap-[3px] border-b border-r border-ink', + // `relative z-0` (plutôt que `relative` seul) crée un contexte + // d'empilement local : l'infobulle interne (`z-20`, voir plus bas) + // ne rivalise qu'avec les autres enfants de cette cellule, jamais + // avec les en-têtes figés de la grille (z-10 à 21) — sans ça, un + // survol déclenché par la molette pendant un défilement (le + // curseur reste immobile à l'écran, ce sont les cellules qui + // défilent dessous) pouvait faire passer l'infobulle au-dessus de + // la ligne d'en-tête « Affectés / idéal » pendant sa transition de + // disparition. + 'group relative z-0 flex h-9 cursor-pointer items-center justify-center gap-[3px] border-b border-r border-ink', undefinedNeed && 'bg-sand-dark hover:bg-sand', isCrit && 'bg-hatch-orange', isOver && 'bg-hatch-mauve hover:bg-mauve-hover',