Écran Statistiques : inscriptions, réponses, cotisations et besoins
ci-cd / build-backend (push) Successful in 33s
ci-cd / build-frontend (push) Successful in 19s
ci-cd / deploy (push) Successful in 12s

Nouvel onglet « Statistiques » (/statistiques, COORDINATION), alimenté
par GET /editions/{id}/stats (app/api/stats.py, recalculé à chaque appel,
aucune nouvelle entité) :
- inscriptions : total, 7 derniers jours, évolution cumulée par jour
- réponses : part des bénévoles ayant renseigné chaque information,
  artistes les plus demandés
- cotisations : en ligne / sur place / aucune, montants par type,
  mécénat, formulaires remplis sans paiement abouti
- besoins face aux bénévoles, par journée de festival : heures × personnes
  demandées vs disponibilités déclarées vs affectées ; par poste, places
  pourvues (plafonnées à l'effectif idéal de chaque créneau) et rapport
  vœux / place

Graphiques SVG maison (components/charts.tsx) avec infobulles au survol
et au clavier, vue tableau pour chacun, palette validée (daltonisme,
contraste), mise en page vérifiée sur ordinateur et mobile. Le mock
calcule les mêmes chiffres et étale les inscriptions du jeu de démo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-04 19:40:46 +02:00
co-authored by Claude Opus 5.5
parent ff57bbd863
commit ee1f6ed72e
17 changed files with 1775 additions and 2 deletions
+108
View File
@@ -647,3 +647,111 @@ class SumUpWebhookPayload(ApiModel):
event_type: str
id: str
# --------------------------------------------------------------------------- #
# Statistiques d'une édition (`GET /editions/{id}/stats`, app/api/stats.py) #
# --------------------------------------------------------------------------- #
class StatsRegistrationDay(ApiModel):
date: dt.date
count: int
cumulative: int
class StatsRegistrations(ApiModel):
"""Bénévoles en base, par jour d'ajout (`Volunteer.created_at`) — jours
sans inscription inclus (à 0), pour une courbe sans trou."""
total: int
last_7_days: int
by_day: list[StatsRegistrationDay]
class StatsAnswers(ApiModel):
"""Nombre de bénévoles ayant renseigné chaque information (sur `total`)."""
total: int
email: int
phone: int
photo: int
comment: int
companion_note: int
available: int
position_wishes: int
artist_wishes: int
companion_wishes: int
class StatsPaymentOption(ApiModel):
label: str
mecenat: bool
count: int
amount_total: float
class StatsPayments(ApiModel):
"""Mode de paiement (même règle que `lib/fee.ts:feeMode`) : `online`
(paiement en ligne confirmé), `on_site`, `none`. `pending_unfinished` :
formulaires remplis dont le paiement en ligne n'a jamais abouti — aucun
bénévole créé pour eux (`PendingFormSubmission` non promue)."""
online: int
on_site: int
none: int
pending_unfinished: int
total_collected: float
mecenat_count: int
mecenat_total: float
by_option: list[StatsPaymentOption]
class StatsArtistWish(ApiModel):
id: uuid.UUID
name: str
date: dt.date
count: int
class StatsNeedDay(ApiModel):
"""Par journée de festival (`validation/engine.py:festival_day`) : heures
× personnes demandées (effectif minimal / idéal), heures de disponibilité
déclarées, heures déjà affectées."""
date: dt.date
shifts: int
min_hours: float
ideal_hours: float
available_hours: float
available_volunteers: int
assigned_hours: float
class StatsNeedPosition(ApiModel):
"""Places (personnes × créneaux) demandées et pourvues pour un poste, et
nombre de bénévoles qui l'ont mis dans leurs vœux."""
position_id: uuid.UUID
name: str
shifts: int
min_slots: int
ideal_slots: int
assigned: int
# Places pourvues, plafonnées créneau par créneau à l'effectif idéal : un
# sureffectif ne compense jamais un trou ailleurs.
filled: int
wishes: int
class StatsNeeds(ApiModel):
by_day: list[StatsNeedDay]
by_position: list[StatsNeedPosition]
volunteers_with_availability: int
volunteers_with_assignment: int
class EditionStats(ApiModel):
registrations: StatsRegistrations
answers: StatsAnswers
payments: StatsPayments
artist_wishes: list[StatsArtistWish]
needs: StatsNeeds