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>
758 lines
23 KiB
Python
758 lines
23 KiB
Python
"""Schémas d'entrée/sortie de l'API CRUD et de la matrice."""
|
||
|
||
import datetime as dt
|
||
import uuid
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
|
||
from app.models import (
|
||
AssignmentSource,
|
||
BadgeAlign,
|
||
BadgeFieldType,
|
||
FeePaymentStatus,
|
||
FormFieldType,
|
||
PositionCategory,
|
||
Role,
|
||
)
|
||
from app.validation import Violation
|
||
|
||
|
||
class ApiModel(BaseModel):
|
||
"""Base commune : autorise la construction depuis un objet ORM."""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Edition #
|
||
# --------------------------------------------------------------------------- #
|
||
class EditionRead(ApiModel):
|
||
id: uuid.UUID
|
||
year: int
|
||
name: str | None
|
||
start_date: dt.date
|
||
end_date: dt.date
|
||
retention_days: int | None
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class EditionUpdate(ApiModel):
|
||
name: str | None = None
|
||
year: int | None = None
|
||
start_date: dt.date | None = None
|
||
end_date: dt.date | None = None
|
||
retention_days: int | None = None
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Volunteer #
|
||
# --------------------------------------------------------------------------- #
|
||
class VolunteerCreate(ApiModel):
|
||
first_name: str
|
||
last_name: str
|
||
email: str | None = None
|
||
phone: str | None = None
|
||
comment: str | None = None
|
||
# Vœux du bénévole : artistes à voir, postes souhaités, bénévoles avec
|
||
# qui il voudrait être affecté (à sens unique, 0 à N).
|
||
artist_ids: list[uuid.UUID] = Field(default_factory=list)
|
||
wished_position_ids: list[uuid.UUID] = Field(default_factory=list)
|
||
wished_companion_ids: list[uuid.UUID] = Field(default_factory=list)
|
||
|
||
|
||
class VolunteerUpdate(ApiModel):
|
||
first_name: str | None = None
|
||
last_name: str | None = None
|
||
email: str | None = None
|
||
phone: str | None = None
|
||
comment: str | None = None
|
||
# None = inchangé ; [] = vider. Idem pour artist_ids / wished_position_ids
|
||
# / wished_companion_ids.
|
||
artist_ids: list[uuid.UUID] | None = None
|
||
wished_position_ids: list[uuid.UUID] | None = None
|
||
wished_companion_ids: list[uuid.UUID] | None = None
|
||
# Issus du formulaire public (`app/api/forms.py`), éditables ensuite
|
||
# depuis la fiche — voir CLAUDE.md § 2 (Volunteer).
|
||
companion_wish_note: str | None = None
|
||
self_declared_available: bool | None = None
|
||
willing_to_pay_fee: bool | None = None
|
||
|
||
|
||
class VolunteerRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
first_name: str
|
||
last_name: str
|
||
email: str | None
|
||
phone: str | None
|
||
comment: str | None
|
||
artist_ids: list[uuid.UUID]
|
||
wished_position_ids: list[uuid.UUID]
|
||
wished_companion_ids: list[uuid.UUID]
|
||
has_photo: bool
|
||
companion_wish_note: str | None
|
||
self_declared_available: bool
|
||
willing_to_pay_fee: bool
|
||
# Paiement de la cotisation via SumUp (voir CLAUDE.md § 2, `Volunteer`) —
|
||
# `fee_payment_label`/`fee_amount` : copie figée du type de paiement
|
||
# choisi (pas la config actuelle, qui peut avoir changé depuis).
|
||
# `fee_payment_status` reste `None` tant qu'aucun checkout n'a jamais été
|
||
# créé pour ce bénévole.
|
||
fee_payment_label: str | None
|
||
fee_amount: float | None
|
||
fee_payment_status: FeePaymentStatus | None
|
||
fee_paid_at: dt.datetime | None
|
||
# Coordonnées pour un reçu fiscal (cerfa) — seulement si payé via un
|
||
# type de paiement « mécénat » (voir `FormPaymentOption.requires_mecenat_info`).
|
||
mecenat_address: str | None
|
||
mecenat_city: str | None
|
||
mecenat_postal_code: str | None
|
||
mecenat_country: str | None
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class VolunteerPlanningEntry(ApiModel):
|
||
"""Un créneau affecté, enrichi (poste, lieu, référent à contacter) — pour
|
||
l'écran bénévole (`GET .../volunteers/{id}/planning`), sans jamais
|
||
exposer la liste complète des postes/créneaux de l'édition à un compte
|
||
VOLUNTEER (lecture restreinte à sa propre fiche, cf. § 2 matrice)."""
|
||
|
||
assignment_id: uuid.UUID
|
||
shift_id: uuid.UUID
|
||
starts_at: dt.datetime
|
||
ends_at: dt.datetime
|
||
position_name: str
|
||
position_location: str | None
|
||
# None si le poste n'a pas (encore) de référent, ou si son compte n'est
|
||
# pas rattaché à une fiche bénévole (pas de téléphone à donner alors).
|
||
referent_name: str | None
|
||
referent_phone: str | None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Artist (programmation) #
|
||
# --------------------------------------------------------------------------- #
|
||
class ArtistCreate(ApiModel):
|
||
name: str
|
||
performance_date: dt.date
|
||
# end_time <= start_time ⇒ le passage finit le lendemain.
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
|
||
|
||
class ArtistUpdate(ApiModel):
|
||
name: str | None = None
|
||
performance_date: dt.date | None = None
|
||
start_time: dt.time | None = None
|
||
end_time: dt.time | None = None
|
||
|
||
|
||
class ArtistRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
name: str
|
||
performance_date: dt.date
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Position #
|
||
# --------------------------------------------------------------------------- #
|
||
class PositionCreate(ApiModel):
|
||
name: str
|
||
location: str | None = None
|
||
category: PositionCategory
|
||
lead_account_id: uuid.UUID | None = None
|
||
|
||
|
||
class PositionUpdate(ApiModel):
|
||
name: str | None = None
|
||
location: str | None = None
|
||
category: PositionCategory | None = None
|
||
lead_account_id: uuid.UUID | None = None
|
||
|
||
|
||
class PositionRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
name: str
|
||
location: str | None
|
||
category: PositionCategory
|
||
lead_account_id: uuid.UUID | None
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Shift #
|
||
# --------------------------------------------------------------------------- #
|
||
class ShiftCreate(ApiModel):
|
||
position_id: uuid.UUID
|
||
starts_at: dt.datetime
|
||
ends_at: dt.datetime
|
||
min_staff: int = Field(default=0, ge=0)
|
||
ideal_staff: int = Field(default=0, ge=0)
|
||
|
||
|
||
class ShiftUpdate(ApiModel):
|
||
position_id: uuid.UUID | None = None
|
||
starts_at: dt.datetime | None = None
|
||
ends_at: dt.datetime | None = None
|
||
min_staff: int | None = Field(default=None, ge=0)
|
||
ideal_staff: int | None = Field(default=None, ge=0)
|
||
|
||
|
||
class ShiftRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
position_id: uuid.UUID
|
||
day_part_id: uuid.UUID | None
|
||
starts_at: dt.datetime
|
||
ends_at: dt.datetime
|
||
min_staff: int
|
||
ideal_staff: int
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class GenerateShiftsResult(ApiModel):
|
||
created: int
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# DayPart (créneau type) #
|
||
# --------------------------------------------------------------------------- #
|
||
class DayPartRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
code: str
|
||
label: str
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
order: int
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class DayPartCreate(ApiModel):
|
||
label: str
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
# Déduit du label si absent. Immuable ensuite (référencé par l'import).
|
||
code: str | None = None
|
||
order: int = 0
|
||
|
||
|
||
class DayPartUpdate(ApiModel):
|
||
label: str | None = None
|
||
start_time: dt.time | None = None
|
||
end_time: dt.time | None = None
|
||
order: int | None = None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Availability (disponibilités déclarées par le bénévole) #
|
||
# --------------------------------------------------------------------------- #
|
||
class AvailabilityWindowIn(ApiModel):
|
||
date: dt.date
|
||
start_time: dt.time
|
||
# end_time <= start_time ⇒ la fenêtre se termine le lendemain.
|
||
end_time: dt.time
|
||
|
||
|
||
class AvailabilitySet(ApiModel):
|
||
"""Remplace l'intégralité des disponibilités d'un bénévole."""
|
||
|
||
windows: list[AvailabilityWindowIn] = Field(default_factory=list)
|
||
|
||
|
||
class AvailabilityRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
volunteer_id: uuid.UUID
|
||
date: dt.date
|
||
start_time: dt.time
|
||
end_time: dt.time
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Assignment #
|
||
# --------------------------------------------------------------------------- #
|
||
class AssignmentCreate(ApiModel):
|
||
volunteer_id: uuid.UUID
|
||
shift_id: uuid.UUID
|
||
source: AssignmentSource = AssignmentSource.MANUAL
|
||
locked: bool = False
|
||
|
||
|
||
class AssignmentUpdate(ApiModel):
|
||
source: AssignmentSource | None = None
|
||
locked: bool | None = None
|
||
|
||
|
||
class AssignmentRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
volunteer_id: uuid.UUID
|
||
shift_id: uuid.UUID
|
||
source: AssignmentSource
|
||
locked: bool
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class AssignmentWriteResult(ApiModel):
|
||
assignment: AssignmentRead
|
||
warnings: list[Violation] = Field(default_factory=list)
|
||
|
||
|
||
class AssignmentValidateRequest(ApiModel):
|
||
volunteer_id: uuid.UUID
|
||
shift_id: uuid.UUID
|
||
|
||
|
||
class AssignmentValidateResult(ApiModel):
|
||
blocking: bool
|
||
violations: list[Violation] = Field(default_factory=list)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Solveur (suggestions, CP-SAT) — ne place jamais personne #
|
||
# --------------------------------------------------------------------------- #
|
||
class SolverSuggestion(ApiModel):
|
||
shift_id: uuid.UUID
|
||
# Bénévoles suggérés pour ce créneau, meilleur en tête.
|
||
volunteer_ids: list[uuid.UUID]
|
||
|
||
|
||
class SolverRunResult(ApiModel):
|
||
suggestions: list[SolverSuggestion]
|
||
shifts_considered: int
|
||
volunteers_considered: int
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Matrice poste × créneau #
|
||
# --------------------------------------------------------------------------- #
|
||
class MatrixShift(ApiModel):
|
||
shift_id: uuid.UUID
|
||
starts_at: dt.datetime
|
||
ends_at: dt.datetime
|
||
min_staff: int
|
||
ideal_staff: int
|
||
assigned: int
|
||
|
||
|
||
class MatrixPosition(ApiModel):
|
||
id: uuid.UUID
|
||
name: str
|
||
category: PositionCategory
|
||
location: str | None
|
||
shifts: list[MatrixShift]
|
||
|
||
|
||
class MatrixResponse(ApiModel):
|
||
edition_id: uuid.UUID
|
||
positions: list[MatrixPosition]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Badges (planches PDF) #
|
||
# --------------------------------------------------------------------------- #
|
||
class BadgeFieldIn(ApiModel):
|
||
field_type: BadgeFieldType
|
||
x_mm: float
|
||
y_mm: float
|
||
width_mm: float
|
||
height_mm: float
|
||
font_size: float = 10.0
|
||
align: BadgeAlign = BadgeAlign.LEFT
|
||
bold: bool = False
|
||
color: str = "#000000"
|
||
|
||
|
||
class BadgeFieldsSet(ApiModel):
|
||
"""Remplace l'intégralité des champs du recto — au plus un par
|
||
`field_type` (décocher un champ dans l'éditeur retire sa ligne)."""
|
||
|
||
fields: list[BadgeFieldIn] = Field(default_factory=list)
|
||
|
||
|
||
class BadgeFieldRead(BadgeFieldIn):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
|
||
|
||
class BadgeTemplateRead(ApiModel):
|
||
edition_id: uuid.UUID
|
||
has_front_image: bool
|
||
has_back_image: bool
|
||
# Dérivée de l'image du recto (voir `app/badges/pdf.py:image_size_mm`) ;
|
||
# format CR80 par défaut tant qu'aucune image n'est importée.
|
||
badge_width_mm: float
|
||
badge_height_mm: float
|
||
fields: list[BadgeFieldRead]
|
||
|
||
|
||
class BadgeSizeUpdate(ApiModel):
|
||
"""Ajuste la taille du badge indépendamment d'un nouvel import d'image —
|
||
ex. agrandir proportionnellement une image importée trop petite (voir
|
||
`app/api/badges.py:set_size`). Écrase la taille déduite de l'image."""
|
||
|
||
badge_width_mm: float
|
||
badge_height_mm: float
|
||
|
||
|
||
class BadgeGenerateRequest(ApiModel):
|
||
# None/absent = tous les bénévoles de l'édition.
|
||
volunteer_ids: list[uuid.UUID] | None = None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Feuilles de poste (PDF) #
|
||
# --------------------------------------------------------------------------- #
|
||
class PositionSheetsRequest(ApiModel):
|
||
# None/absent = tous les postes de l'édition.
|
||
position_ids: list[uuid.UUID] | None = None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Authentification et comptes #
|
||
# --------------------------------------------------------------------------- #
|
||
class MagicLinkRequest(ApiModel):
|
||
email: str
|
||
|
||
|
||
class VerifyRequest(ApiModel):
|
||
token: str
|
||
|
||
|
||
class LoginRequest(ApiModel):
|
||
email: str
|
||
password: str
|
||
|
||
|
||
class ChangePasswordRequest(ApiModel):
|
||
current_password: str
|
||
new_password: str
|
||
|
||
|
||
class AccountRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
email: str
|
||
role: Role
|
||
volunteer_id: uuid.UUID | None
|
||
# Mode de connexion : "password" (admins) ou "magic_link".
|
||
auth_method: str
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class TokenResponse(ApiModel):
|
||
access_token: str
|
||
token_type: str
|
||
account: AccountRead
|
||
|
||
|
||
class AccountCreate(ApiModel):
|
||
email: str
|
||
role: Role
|
||
volunteer_id: uuid.UUID | None = None
|
||
# Requis pour un compte COORDINATION, interdit sinon.
|
||
password: str | None = None
|
||
|
||
|
||
class AccountUpdate(ApiModel):
|
||
# Seul le rôle est modifiable, et seulement entre LEAD et VOLUNTEER (pas
|
||
# de mot de passe en jeu) — voir `api/accounts.py:update_account`.
|
||
role: Role | None = None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Notifications par email #
|
||
# --------------------------------------------------------------------------- #
|
||
class PlanningNotificationRequest(ApiModel):
|
||
# Modèle saisi par la coordination. Variables : {prenom}, {nom},
|
||
# {nb_creneaux}, {nb_heures}, {creneaux}, {lien_connexion}.
|
||
subject: str | None = None
|
||
body: str | None = None
|
||
# None = tous les bénévoles de l'édition qui ont un email.
|
||
volunteer_ids: list[uuid.UUID] | None = None
|
||
|
||
|
||
class PlanningNotificationResult(ApiModel):
|
||
queued: int
|
||
skipped_no_email: int
|
||
recipients: list[uuid.UUID] = Field(default_factory=list)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Formulaire public d'inscription #
|
||
# --------------------------------------------------------------------------- #
|
||
class FormFieldsSet(ApiModel):
|
||
"""Remplace l'intégralité des champs activés du formulaire — nom/prénom
|
||
toujours collectés, pas dans cette liste (voir `FormFieldType`)."""
|
||
|
||
fields: list[FormFieldType] = Field(default_factory=list)
|
||
|
||
|
||
class FormTemplateRead(ApiModel):
|
||
edition_id: uuid.UUID
|
||
is_open: bool
|
||
fields: list[FormFieldType]
|
||
has_logo: bool
|
||
description: str | None
|
||
sumup_merchant_code: str | None
|
||
# `sumup_api_key` (secret) n'est jamais renvoyé — seulement s'il est
|
||
# défini, comme `has_logo` pour le logo.
|
||
sumup_api_key_set: bool
|
||
# Secret nécessaire pour atteindre le formulaire public
|
||
# (`/inscription/<access_token>`, voir CLAUDE.md § 2, `FormTemplate`) —
|
||
# exposé seulement ici (COORDINATION), jamais par `PublicFormConfig`.
|
||
access_token: str
|
||
|
||
|
||
class FormTemplateUpdate(ApiModel):
|
||
"""`None`/absent = inchangé pour chaque champ (`exclude_unset`) — permet
|
||
de changer `is_open` sans toucher `description`/`sumup_*`, etc. Chaîne
|
||
vide sur `sumup_api_key` = efface le secret enregistré."""
|
||
|
||
is_open: bool | None = None
|
||
description: str | None = None
|
||
sumup_merchant_code: str | None = None
|
||
sumup_api_key: str | None = None
|
||
|
||
|
||
class FormPaymentOptionRead(ApiModel):
|
||
id: uuid.UUID
|
||
edition_id: uuid.UUID
|
||
label: str
|
||
amount: float
|
||
order: int
|
||
requires_mecenat_info: bool
|
||
free_amount: bool
|
||
created_at: dt.datetime
|
||
updated_at: dt.datetime
|
||
|
||
|
||
class FormPaymentOptionCreate(ApiModel):
|
||
label: str
|
||
amount: float
|
||
order: int = 0
|
||
requires_mecenat_info: bool = False
|
||
free_amount: bool = False
|
||
|
||
|
||
class FormPaymentOptionUpdate(ApiModel):
|
||
label: str | None = None
|
||
amount: float | None = None
|
||
order: int | None = None
|
||
requires_mecenat_info: bool | None = None
|
||
free_amount: bool | None = None
|
||
|
||
|
||
class PublicPositionOption(ApiModel):
|
||
id: uuid.UUID
|
||
name: str
|
||
|
||
|
||
class PublicArtistOption(ApiModel):
|
||
id: uuid.UUID
|
||
name: str
|
||
performance_date: dt.date
|
||
|
||
|
||
class PublicPaymentOption(ApiModel):
|
||
"""Type de paiement proposé au bénévole sur le formulaire public — un
|
||
montant n'est pas un secret, contrairement aux identifiants SumUp."""
|
||
|
||
id: uuid.UUID
|
||
label: str
|
||
# Montant fixe, ou minimum si `free_amount`.
|
||
amount: float
|
||
requires_mecenat_info: bool
|
||
free_amount: bool
|
||
|
||
|
||
class PublicFormConfig(ApiModel):
|
||
"""Tout ce qu'il faut pour afficher le formulaire public — jamais plus :
|
||
ni les bénévoles déjà inscrits, ni les créneaux/affectations de
|
||
l'édition (voir `app/api/forms.py:get_public_form`)."""
|
||
|
||
is_open: bool
|
||
fields: list[FormFieldType]
|
||
positions: list[PublicPositionOption]
|
||
artists: list[PublicArtistOption]
|
||
has_logo: bool
|
||
description: str | None
|
||
# `True` si l'édition a configuré SumUp (identifiants marchand + au
|
||
# moins un type de paiement) — utilisé par le front-end pour afficher le
|
||
# choix de paiement près de `WILLING_TO_PAY_FEE`, sans jamais exposer les
|
||
# identifiants eux-mêmes.
|
||
sumup_enabled: bool
|
||
payment_options: list[PublicPaymentOption]
|
||
|
||
|
||
class FormSubmissionResult(ApiModel):
|
||
"""Trois formes possibles, selon le choix de paiement (voir
|
||
`app/api/forms.py:submit_public_form`) :
|
||
- aucun paiement en ligne choisi (case désactivée, ou « je paierai sur
|
||
place ») : `volunteer_id` seul, le bénévole est déjà en base ;
|
||
- paiement en ligne, type « mécénat » : `pending_token` +
|
||
`requires_mecenat_info=True`, rien en base encore — direction l'étape
|
||
d'adresse (`submit_mecenat_info`) ;
|
||
- paiement en ligne, type simple : `pending_token` + `sumup_checkout_url`,
|
||
direction la page de paiement SumUp. Toujours rien en base tant que le
|
||
paiement n'est pas confirmé (voir `PendingFormSubmission`)."""
|
||
|
||
volunteer_id: uuid.UUID | None = None
|
||
pending_token: uuid.UUID | None = None
|
||
requires_mecenat_info: bool = False
|
||
sumup_checkout_url: str | None = None
|
||
|
||
|
||
class MecenatInfoSubmit(ApiModel):
|
||
"""Étape d'adresse postale pour un type de paiement « mécénat »
|
||
(`POST .../form/public/pending/{token}/mecenat-info`) — tous obligatoires,
|
||
nécessaires à l'édition d'un reçu fiscal (cerfa)."""
|
||
|
||
address: str
|
||
city: str
|
||
postal_code: str
|
||
country: str
|
||
|
||
|
||
class PendingSubmissionStatus(ApiModel):
|
||
"""Réponse de `GET .../form/public/pending/{token}` — interrogée en
|
||
direct par la page de confirmation jusqu'à ce que le statut ne soit plus
|
||
`PENDING`. Relit toujours l'état réel auprès de SumUp (jamais seulement
|
||
la dernière valeur connue), comme le webhook."""
|
||
|
||
status: FeePaymentStatus
|
||
label: str
|
||
amount: float
|
||
volunteer_id: uuid.UUID | None = None
|
||
|
||
|
||
class SumUpWebhookPayload(ApiModel):
|
||
"""Corps envoyé par SumUp à `return_url` — voir la doc SumUp sur les
|
||
webhooks. Le seul événement existant à ce jour."""
|
||
|
||
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
|