Lien d'inscription protégé par jeton, reçus fiscaux mécénat, infobulles et revue de sécurité
ci-cd / build-backend (push) Successful in 1m12s
ci-cd / build-frontend (push) Successful in 18s
ci-cd / deploy (push) Successful in 12s

- Lien d'inscription protégé par un jeton (FormTemplate.access_token,
  /inscription/<token>), avec régénération depuis l'écran de configuration.
- Reçus fiscaux mécénat (cerfa 11580*05) : un PDF par bénévole ayant payé en
  mécénat, donateur/montant (chiffres et toutes lettres en français,
  app/mecenat/number_words.py)/date complétés par-dessus le modèle officiel.
- Jeu de démonstration : bénévoles avec cotisation classique ou mécénat payée.
- Infobulles (HelpTip et détail de cellule de la grille des besoins)
  repositionnées en position: fixed via JS, bornées à l'écran.
- Revue de sécurité : logo des emails en pièce jointe inline au lieu d'une
  URL (fuyait le jeton d'inscription), sujet retiré du log de remise des
  emails (pouvait porter un nom de bénévole), photo du formulaire public
  revalidée par décodage réel avant stockage, export Excel protégé contre
  l'injection de formule, sondage du statut de paiement limité en
  fréquence, pyjwt mis à jour.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-02 07:47:17 +02:00
co-authored by Claude Sonnet 5
parent a2b748566b
commit 096b7cd26a
44 changed files with 1228 additions and 156 deletions
+17 -11
View File
@@ -24,6 +24,7 @@ import {
import { hourRangeLabel, shortDayLabel } from '@/lib/time';
import { t } from '@/i18n';
import { cx } from '@/lib/cx';
import { useTooltipPosition } from '@/lib/useTooltipPosition';
export function GrillePage() {
const matrix = useMatrix();
@@ -437,22 +438,19 @@ function ShiftCell({
const isInc = !undefinedNeed && !isCrit && !isOver && assigned < shift.ideal_staff;
const isOk = !undefinedNeed && !isCrit && !isOver && !isInc;
const glyph = undefinedNeed ? '·' : isCrit ? '▲' : isOver ? '▼' : isInc ? '◧' : '✓';
const { anchorRef, tooltipRef, pos, reposition } = useTooltipPosition<
HTMLDivElement,
HTMLDivElement
>('bottom');
return (
<div
ref={anchorRef}
onClick={() => onSelect(shift.shift_id)}
onMouseEnter={reposition}
style={{ gridColumn, gridRow }}
className={cx(
// `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',
'group relative 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',
@@ -463,7 +461,15 @@ function ShiftCell({
<span className="font-mono text-[13px] font-bold">{assigned}</span>
<span className="font-mono text-[9px] opacity-50">/{shift.ideal_staff}</span>
<span className={cx('font-mono text-[10px] font-bold', isOk && 'text-green')}>{glyph}</span>
<div className="pointer-events-none absolute left-1/2 top-full z-[100] mt-[4px] w-max max-w-[220px] -translate-x-1/2 border-2 border-ink bg-ink px-[9px] py-[7px] text-left font-mono text-[10px] font-normal normal-case leading-relaxed tracking-normal text-cream opacity-0 shadow-brutal-md transition-opacity duration-100 group-hover:opacity-100">
<div
ref={tooltipRef}
style={{ left: pos.left, top: pos.top }}
// `fixed` (plutôt qu'`absolute` ancré sur la cellule) : la bulle
// s'affranchit à la fois du cadrage de la zone de défilement de la
// grille (`overflow-auto`, qui la découpait) et de la pile locale
// des en-têtes figés (qui la faisait passer dessous, invisible) —
// voir `useTooltipPosition`. Recalculée à chaque survol.
className="pointer-events-none fixed z-30 w-max max-w-[220px] border-2 border-ink bg-ink px-[9px] py-[7px] text-left font-mono text-[10px] font-normal normal-case leading-relaxed tracking-normal text-cream opacity-0 shadow-brutal-md transition-opacity duration-100 group-hover:opacity-100">
<div className="mb-[1px] font-mono text-[9px] font-bold text-cream opacity-80">
{hourRangeLabel(shift.starts_at, shift.ends_at)}
</div>