Files
benevoles/frontend/src/api/schema.ts
T
AdamThomasandClaude Opus 5.5 1026d9b9e1
ci-cd / build-backend (push) Failing after 1m5s
ci-cd / build-frontend (push) Skipped
ci-cd / deploy (push) Skipped
Badges : ordre de superposition des champs
La photo recouvrait le nom et le texte libre (champs dessinés dans l'ordre
alphabétique de leur type). BadgeField.z_order (migration 0028, champs
existants renumérotés photo dessous) fixe l'ordre de dessin du PDF et de
l'aperçu ; boutons « Premier plan / Avancer / Reculer / Arrière-plan »
dans l'inspecteur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:33:10 +01:00

817 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Types miroir des schémas de l'API back-end (`backend/app/api/schemas.py`).
// Toute évolution du contrat doit être répercutée ici.
export type PositionCategory =
| 'BAR'
| 'TICKETING'
| 'WELCOME'
| 'PARKING'
| 'CLEANING'
| 'CATERING'
| 'SETUP'
| 'STAGE'
| 'OTHER';
// Depuis l'Étape 9 back-end : une affectation est valide dès sa création,
// il n'y a plus de statut (proposée / confirmée…). La retirer = supprimer la ligne.
export type AssignmentSource = 'SOLVER' | 'MANUAL';
export type Role = 'COORDINATOR' | 'LEAD' | 'VOLUNTEER';
export type AuthMethod = 'password' | 'magic_link';
export type ViolationCode = 'SHIFT_OVERLAP';
export type ViolationSeverity = 'BLOCKING' | 'WARNING';
export interface Violation {
code: ViolationCode;
severity: ViolationSeverity;
params: Record<string, string | number>;
}
// --------------------------------------------------------------------------- //
// Edition //
// --------------------------------------------------------------------------- //
export interface EditionRead {
id: string;
year: number;
name: string | null;
/** Dates ISO (YYYY-MM-DD). */
start_date: string;
end_date: string;
retention_days: number | null;
created_at: string;
updated_at: string;
}
export interface EditionUpdate {
name?: string | null;
year?: number;
start_date?: string;
end_date?: string;
retention_days?: number | null;
}
// --------------------------------------------------------------------------- //
// DayPart (créneau type) //
// --------------------------------------------------------------------------- //
export interface DayPartRead {
id: string;
edition_id: string;
code: string;
label: string;
/** Heures « HH:MM:SS ». end < start ⇒ passe minuit. */
start_time: string;
end_time: string;
order: number;
created_at: string;
updated_at: string;
}
export interface DayPartCreate {
label: string;
start_time: string;
end_time: string;
/** Déduit du label si absent ; figé ensuite. */
code?: string;
order?: number;
}
export interface DayPartUpdate {
label?: string;
start_time?: string;
end_time?: string;
order?: number;
}
export interface GenerateShiftsResult {
created: number;
}
// --------------------------------------------------------------------------- //
// Volunteer //
// --------------------------------------------------------------------------- //
export interface VolunteerRead {
id: string;
edition_id: string;
first_name: string;
last_name: string;
email: string | null;
phone: string | null;
comment: string | null;
/** Vœux : artistes que le bénévole veut voir. */
artist_ids: string[];
/** Vœux : postes que le bénévole voudrait tenir. */
wished_position_ids: string[];
/** Vœux : bénévoles avec qui il voudrait être affecté (0 à N, à sens unique). */
wished_companion_ids: string[];
/** Renseignés par le formulaire public d'inscription (voir schema forms). */
has_photo: boolean;
companion_wish_note: string | null;
/** Texte libre imprimé sur le badge (champ `FREE_TEXT`), saisi par la
* coordination. */
badge_text: string | null;
/** Exemplaires de son badge imprimés sur les planches (1 par défaut). */
badge_count: number;
self_declared_available: boolean;
willing_to_pay_fee: boolean;
/** Paiement de la cotisation via SumUp (voir schema forms) —
* `fee_payment_label`/`fee_amount` : copie figée du type de paiement
* choisi (pas la config actuelle). `fee_payment_status` reste `null` tant
* qu'aucun checkout n'a jamais été créé pour ce bénévole. */
fee_payment_label: string | null;
fee_amount: number | null;
fee_payment_status: FeePaymentStatus | null;
fee_paid_at: string | null;
/** Transaction SumUp qui a réglé la cotisation : `fee_transaction_code` est
* le code affiché dans le tableau de bord SumUp (ex. `TEENSK4W2K`),
* `fee_transaction_id` l'identifiant technique. `null` tant que non payée
* (ou payée avant que l'appli ne l'enregistre). */
fee_transaction_code: string | null;
fee_transaction_id: string | null;
/** Coordonnées pour un reçu fiscal (cerfa) — seulement si payé via un type
* de paiement « mécénat ». */
mecenat_address: string | null;
mecenat_city: string | null;
mecenat_postal_code: string | null;
mecenat_country: string | null;
created_at: string;
updated_at: string;
}
export type FeePaymentStatus = 'PENDING' | 'PAID' | 'FAILED' | 'EXPIRED';
export interface VolunteerCreate {
first_name: string;
last_name: string;
email?: string | null;
phone?: string | null;
comment?: string | null;
artist_ids?: string[];
wished_position_ids?: string[];
wished_companion_ids?: string[];
}
export interface VolunteerUpdate {
first_name?: string;
last_name?: string;
email?: string | null;
phone?: string | null;
comment?: string | null;
/** null / absent = inchangé ; [] = vider. */
artist_ids?: string[] | null;
wished_position_ids?: string[] | null;
wished_companion_ids?: string[] | null;
companion_wish_note?: string | null;
badge_text?: string | null;
/** 1 à 100. */
badge_count?: number | null;
self_declared_available?: boolean | null;
willing_to_pay_fee?: boolean | null;
}
/**
* Un créneau affecté, enrichi (poste, lieu, référent à contacter) — pour
* l'écran bénévole. Évite de donner à un compte VOLUNTEER un accès en
* lecture aux postes/créneaux de toute l'édition (réservé à COORD/LEAD).
*/
export interface VolunteerPlanningEntry {
assignment_id: string;
shift_id: string;
starts_at: string;
ends_at: string;
position_name: string;
position_location: string | null;
/** null si le poste n'a pas 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: string | null;
referent_phone: string | null;
}
// --------------------------------------------------------------------------- //
// Artist (programmation) //
// --------------------------------------------------------------------------- //
export interface ArtistRead {
id: string;
edition_id: string;
name: string;
/** Jour de passage, date ISO (YYYY-MM-DD). */
performance_date: string;
/** Heures « HH:MM:SS ». end_time <= start_time ⇒ finit le lendemain. */
start_time: string;
end_time: string;
created_at: string;
updated_at: string;
}
export interface ArtistCreate {
name: string;
performance_date: string;
start_time: string;
end_time: string;
}
export interface ArtistUpdate {
name?: string;
performance_date?: string;
start_time?: string;
end_time?: string;
}
// --------------------------------------------------------------------------- //
// Import de bénévoles (fichier CSV / Excel) //
// --------------------------------------------------------------------------- //
export type ImportFieldKey =
| 'first_name'
| 'last_name'
| 'email'
| 'phone'
| 'comment';
export interface ImportColumnMapping {
first_name: string;
last_name: string;
email?: string | null;
phone?: string | null;
comment?: string | null;
}
export interface ImportMapping {
dry_run: boolean;
columns: ImportColumnMapping;
}
export interface ImportPreviewRow {
line: number;
values: Record<string, string>;
}
export interface ImportAnalyzeResponse {
filename: string;
format: 'csv' | 'xlsx';
headers: string[];
row_count: number;
preview: ImportPreviewRow[];
suggested_mapping: Partial<Record<ImportFieldKey, string>>;
}
export type ImportSeverity = 'ERROR' | 'WARNING';
export type ImportRowAction = 'CREATED' | 'UPDATED' | 'SKIPPED';
export interface ImportIssue {
code: string;
severity: ImportSeverity;
field?: string | null;
params: Record<string, string | number | null>;
}
export interface ImportRowReport {
line: number;
action: ImportRowAction;
volunteer_id: string | null;
issues: ImportIssue[];
}
export interface ImportReport {
dry_run: boolean;
total_rows: number;
created: number;
updated: number;
skipped: number;
rows: ImportRowReport[];
}
// --------------------------------------------------------------------------- //
// Position //
// --------------------------------------------------------------------------- //
export interface PositionRead {
id: string;
edition_id: string;
name: string;
location: string | null;
category: PositionCategory;
lead_account_id: string | null;
created_at: string;
updated_at: string;
}
export interface PositionCreate {
name: string;
location?: string | null;
category: PositionCategory;
lead_account_id?: string | null;
}
export interface PositionUpdate {
name?: string;
location?: string | null;
category?: PositionCategory;
lead_account_id?: string | null;
}
// --------------------------------------------------------------------------- //
// Shift //
// --------------------------------------------------------------------------- //
export interface ShiftRead {
id: string;
edition_id: string;
position_id: string;
/** Créneau type d'origine, ou null pour un créneau ad hoc. */
day_part_id: string | null;
/** Datetime naïf, heure locale Europe/Paris (ISO sans fuseau). */
starts_at: string;
ends_at: string;
min_staff: number;
ideal_staff: number;
created_at: string;
updated_at: string;
}
export interface ShiftCreate {
position_id: string;
starts_at: string;
ends_at: string;
min_staff?: number;
ideal_staff?: number;
}
export interface ShiftUpdate {
position_id?: string;
starts_at?: string;
ends_at?: string;
min_staff?: number;
ideal_staff?: number;
}
// --------------------------------------------------------------------------- //
// Assignment //
// --------------------------------------------------------------------------- //
export interface AssignmentRead {
id: string;
edition_id: string;
volunteer_id: string;
shift_id: string;
source: AssignmentSource;
locked: boolean;
created_at: string;
updated_at: string;
}
export interface AssignmentCreate {
volunteer_id: string;
shift_id: string;
source?: AssignmentSource;
locked?: boolean;
}
export interface AssignmentUpdate {
source?: AssignmentSource;
locked?: boolean;
}
export interface AssignmentWriteResult {
assignment: AssignmentRead;
warnings: Violation[];
}
export interface AssignmentValidateRequest {
volunteer_id: string;
shift_id: string;
}
export interface AssignmentValidateResult {
blocking: boolean;
violations: Violation[];
}
// --------------------------------------------------------------------------- //
// Solveur (suggestions, CP-SAT) — ne place jamais personne //
// --------------------------------------------------------------------------- //
export interface SolverSuggestion {
shift_id: string;
/** Bénévoles suggérés pour ce créneau, meilleur en tête. */
volunteer_ids: string[];
}
export interface SolverRunResult {
suggestions: SolverSuggestion[];
shifts_considered: number;
volunteers_considered: number;
}
// --------------------------------------------------------------------------- //
// Matrice poste × créneau //
// --------------------------------------------------------------------------- //
export interface MatrixShift {
shift_id: string;
starts_at: string;
ends_at: string;
min_staff: number;
ideal_staff: number;
/** Nombre d'affectations sur ce créneau. */
assigned: number;
}
export interface MatrixPosition {
id: string;
name: string;
category: PositionCategory;
location: string | null;
shifts: MatrixShift[];
}
export interface MatrixResponse {
edition_id: string;
positions: MatrixPosition[];
}
// --------------------------------------------------------------------------- //
// Badges (planches PDF) //
// --------------------------------------------------------------------------- //
/** `PHOTO` : photo du bénévole ajustée au cadre ; `FREE_TEXT` : texte libre
* de sa fiche (`VolunteerRead.badge_text`), saisi par la coordination. */
export type BadgeFieldType =
| 'FULL_NAME'
| 'POSITIONS'
| 'CATEGORY'
| 'ROLE'
| 'PHOTO'
| 'FREE_TEXT';
export type BadgeAlign = 'LEFT' | 'CENTER' | 'RIGHT';
/** Face du badge où se trouve un champ : chaque type de champ peut exister
* une fois par face. */
export type BadgeFace = 'FRONT' | 'BACK';
/** Forme du cadre de la photo (`PHOTO`). */
export type BadgePhotoShape = 'RECT' | 'OVAL';
export interface BadgeFieldIn {
field_type: BadgeFieldType;
/** Défaut `FRONT`. */
face: BadgeFace;
x_mm: number;
y_mm: number;
width_mm: number;
height_mm: number;
font_size: number;
align: BadgeAlign;
bold: boolean;
color: string;
/** Fond de la zone du champ (`#rrggbb`), `null` = transparent. */
bg_color: string | null;
/** Réduit la taille du texte (par pas de 0,5 pt, jusqu'à 4 pt) quand il
* ne tient pas dans le cadre. */
auto_fit: boolean;
/** `PHOTO` seulement : rectangle ou ovale inscrit dans le cadre. */
photo_shape: BadgePhotoShape;
/** Ordre d'empilement sur la face : un champ recouvre ceux d'un
* `z_order` inférieur (photo dessous à égalité). */
z_order: number;
/** `FREE_TEXT` seulement : texte commun à tous les badges, remplacé pour
* un bénévole par le « Texte du badge » de sa fiche (`badge_text`). */
text?: string | null;
}
export interface BadgeFieldsSet {
fields: BadgeFieldIn[];
}
export interface BadgeFieldRead extends BadgeFieldIn {
id: string;
edition_id: string;
}
export interface BadgeTemplateRead {
edition_id: string;
has_front_image: boolean;
has_back_image: boolean;
/** Dérivée de l'image du recto (300 dpi supposés) ; format CR80 par défaut
* tant qu'aucune image n'est importée. */
badge_width_mm: number;
badge_height_mm: number;
fields: BadgeFieldRead[];
}
export interface BadgeGenerateRequest {
volunteer_ids?: string[] | null;
}
/** Redimensionne le badge indépendamment d'un nouvel import d'image — ex.
* agrandir proportionnellement une image importée à basse résolution. */
export interface BadgeSizeUpdate {
badge_width_mm: number;
badge_height_mm: number;
}
/** Feuilles de poste (PDF) : une page par poste et par jour, créneaux +
* bénévoles affectés + téléphone. */
export interface PositionSheetsRequest {
// null/absent = tous les postes de l'édition.
position_ids?: string[] | null;
}
// --------------------------------------------------------------------------- //
// Authentification //
// --------------------------------------------------------------------------- //
export interface AccountRead {
id: string;
edition_id: string;
email: string;
role: Role;
volunteer_id: string | null;
/** "password" (coordination) ou "magic_link" (référents, bénévoles). */
auth_method: AuthMethod;
created_at: string;
updated_at: string;
}
export interface AccountCreate {
email: string;
role: Role;
volunteer_id?: string | null;
/** Requis pour un compte COORDINATOR, interdit sinon. */
password?: string | null;
}
export interface AccountUpdate {
/** Seulement entre LEAD et VOLUNTEER — jamais vers/depuis COORDINATOR. */
role?: Role;
}
export interface TokenResponse {
access_token: string;
token_type: string;
account: AccountRead;
}
export interface LoginRequest {
email: string;
password: string;
}
export interface MagicLinkRequest {
email: string;
}
export interface VerifyRequest {
token: string;
}
// --------------------------------------------------------------------------- //
// Notifications par email //
// --------------------------------------------------------------------------- //
/**
* `subject` / `body` sont des modèles. Variables : {prenom} {nom}
* {nb_creneaux} {nb_heures} {creneaux} {lien_connexion} (ce dernier déclenche
* l'émission d'un lien magique personnel).
*/
export interface PlanningNotificationRequest {
subject?: string | null;
body?: string | null;
/** null / absent = tous les bénévoles de l'édition qui ont un email. */
volunteer_ids?: string[] | null;
}
export interface PlanningNotificationResult {
queued: number;
skipped_no_email: number;
recipients: string[];
}
// --------------------------------------------------------------------------- //
// Formulaire public d'inscription //
// --------------------------------------------------------------------------- //
// Nom/prénom toujours collectés, pas dans cette liste (voir app/api/forms.py).
export type FormFieldType =
| 'EMAIL'
| 'PHONE'
| 'POSITION_WISHES'
| 'ARTIST_WISHES'
| 'COMPANION_WISH'
| 'AVAILABLE'
| 'WILLING_TO_PAY_FEE'
| 'PHOTO'
| 'COMMENT';
export interface FormFieldsSet {
fields: FormFieldType[];
}
export interface FormTemplateRead {
edition_id: string;
is_open: boolean;
fields: FormFieldType[];
has_logo: boolean;
description: string | null;
sumup_merchant_code: string | null;
/** `sumup_api_key` (secret) n'est jamais renvoyé — seulement s'il est défini. */
sumup_api_key_set: boolean;
/** Jeton requis pour atteindre `/inscription/:token` — voir
* CLAUDE.md § 2, `FormTemplate`. Visible ici seulement (COORDINATION). */
access_token: string;
}
/** `undefined`/absent = inchangé pour chaque champ. Chaîne vide sur
* `sumup_api_key` efface le secret enregistré. */
export interface FormTemplateUpdate {
is_open?: boolean;
description?: string | null;
sumup_merchant_code?: string | null;
sumup_api_key?: string | null;
}
/** Un type de paiement proposé sur le formulaire public (ex. « Cotisation
* classique », « Mécénat ») — libellé + montant fixe, configurés librement
* par la coordination (comme les créneaux types `DayPart`). */
export interface FormPaymentOptionRead {
id: string;
edition_id: string;
label: string;
amount: number;
order: number;
/** Type « mécénat » : le bénévole passe par une étape d'adresse postale
* avant le paiement (reçu fiscal / cerfa). */
requires_mecenat_info: boolean;
/** Montant libre : le bénévole saisit son montant, `amount` = minimum. */
free_amount: boolean;
created_at: string;
updated_at: string;
}
export interface FormPaymentOptionCreate {
label: string;
amount: number;
order?: number;
requires_mecenat_info?: boolean;
free_amount?: boolean;
}
export interface FormPaymentOptionUpdate {
label?: string;
amount?: number;
order?: number;
requires_mecenat_info?: boolean;
free_amount?: boolean;
}
export interface PublicPositionOption {
id: string;
name: string;
}
export interface PublicArtistOption {
id: string;
name: string;
performance_date: string;
}
/** Type de paiement affiché sur le formulaire public — un montant n'est pas
* un secret, contrairement aux identifiants SumUp. */
export interface PublicPaymentOption {
id: string;
label: string;
/** Montant fixe, ou minimum si `free_amount`. */
amount: number;
requires_mecenat_info: boolean;
free_amount: boolean;
}
/** Tout ce qu'il faut pour afficher le formulaire public — jamais plus. */
export interface PublicFormConfig {
is_open: boolean;
fields: FormFieldType[];
positions: PublicPositionOption[];
artists: PublicArtistOption[];
has_logo: boolean;
description: string | null;
/** `true` si l'édition a configuré SumUp (identifiants + au moins un type
* de paiement). */
sumup_enabled: boolean;
payment_options: PublicPaymentOption[];
}
/** Trois formes possibles, selon le choix de paiement (voir
* `app/api/forms.py:submit_public_form`) :
* - aucun paiement en ligne (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 (`submitMecenatInfo`) ;
* - paiement en ligne, type simple : `pending_token` + `sumup_checkout_url`,
* direction la page de paiement SumUp. */
export interface FormSubmissionResult {
volunteer_id: string | null;
pending_token: string | null;
requires_mecenat_info: boolean;
sumup_checkout_url: string | null;
}
/** Étape d'adresse postale d'un type de paiement « mécénat » — tous
* obligatoires, nécessaires à l'édition d'un reçu fiscal (cerfa). */
export interface MecenatInfoSubmit {
address: string;
city: string;
postal_code: string;
country: string;
}
/** Réponse de la vérification en direct du paiement (page de confirmation) —
* interrogée jusqu'à ce que le statut ne soit plus `PENDING`. */
export interface PendingSubmissionStatus {
status: FeePaymentStatus;
label: string;
amount: number;
volunteer_id: string | null;
}
/** Champs du formulaire public — envoyés en `multipart/form-data` (photo
* possible). `position_wish_ids`/`artist_wish_ids` : JSON stringifié
* (`'["id1","id2"]'`), pas de tableau natif possible en multipart. */
export interface PublicFormSubmission {
first_name: string;
last_name: string;
hcaptcha_token: string;
email?: string;
phone?: string;
comment?: string;
companion_wish_note?: string;
available?: boolean;
/** Type de paiement choisi (voir `PublicPaymentOption`) — absent = « je
* paierai sur place » (aucun paiement en ligne). */
payment_option_id?: string;
/** Montant saisi, pour un type de paiement à montant libre seulement. */
payment_amount?: string;
position_wish_ids?: string[];
artist_wish_ids?: string[];
photo?: File | null;
}
// --------------------------------------------------------------------------- //
// Statistiques d'une édition (`GET /editions/{id}/stats`, app/api/stats.py) //
// --------------------------------------------------------------------------- //
export interface StatsRegistrationDay {
date: string;
count: number;
cumulative: number;
}
export interface StatsAnswers {
total: number;
email: number;
phone: number;
photo: number;
comment: number;
companion_note: number;
available: number;
position_wishes: number;
artist_wishes: number;
companion_wishes: number;
}
export interface StatsPaymentOption {
label: string;
mecenat: boolean;
count: number;
amount_total: number;
}
export interface StatsNeedDay {
date: string;
shifts: number;
min_hours: number;
ideal_hours: number;
assigned_hours: number;
}
export interface StatsNeedPosition {
position_id: string;
name: string;
shifts: number;
min_slots: number;
ideal_slots: number;
assigned: number;
/** Places pourvues, plafonnées créneau par créneau à l'effectif idéal. */
filled: number;
wishes: number;
}
export interface EditionStats {
registrations: { total: number; last_7_days: number; by_day: StatsRegistrationDay[] };
answers: StatsAnswers;
payments: {
online: number;
on_site: number;
none: number;
/** Formulaires remplis dont le paiement en ligne n'a jamais abouti. */
pending_unfinished: number;
total_collected: number;
mecenat_count: number;
mecenat_total: number;
by_option: StatsPaymentOption[];
};
artist_wishes: Array<{ id: string; name: string; date: string; count: number }>;
needs: {
by_day: StatsNeedDay[];
by_position: StatsNeedPosition[];
volunteers_with_assignment: number;
};
}