Files
benevoles/frontend/src/api/schema.ts
T
AdamThomasandClaude Sonnet 5 b7fc80a7dc Étape 20 : planches de badges bénévoles (PDF)
Recto personnalisable (fond image + champs positionnés : nom, poste(s)
affectés, catégorie, rôle) via un éditeur visuel glisser-déposer ; verso
fixe, la même image pour tous les bénévoles. Nouvel écran « Badges »
(COORDINATION uniquement, /badges).

Backend : modèle BadgeTemplate/BadgeField (migration 0011), génération
PDF isolée (app/badges/pdf.py, ReportLab — planches recto suivies des
planches verso correspondantes pour une impression recto-verso), route
POST /editions/{id}/badges/generate, tests (module pur + API).

Frontend : client HTTP (upload multipart, réponses binaires images/PDF),
client mock (mode démonstration), éditeur de positionnement en
millimètres (coordonnées partagées avec le backend), écran de génération
avec sélection des bénévoles et téléchargement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 20:05:03 +02:00

524 lines
14 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'
| 'OUTSIDE_AVAILABILITY'
| 'DAILY_HOURS_EXCEEDED'
| 'INSUFFICIENT_REST';
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[];
created_at: string;
updated_at: string;
}
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;
}
/**
* 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;
}
// --------------------------------------------------------------------------- //
// Availability (disponibilités déclarées par le bénévole) //
// --------------------------------------------------------------------------- //
export interface AvailabilityWindowInput {
/** Date ISO (YYYY-MM-DD). */
date: string;
/** Heure « HH:MM » ou « HH:MM:SS ». end <= start ⇒ se termine le lendemain. */
start_time: string;
end_time: string;
}
export interface AvailabilitySet {
windows: AvailabilityWindowInput[];
}
export interface AvailabilityRead {
id: string;
edition_id: string;
volunteer_id: string;
date: string;
start_time: string;
end_time: string;
created_at: string;
updated_at: string;
}
// --------------------------------------------------------------------------- //
// 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) //
// --------------------------------------------------------------------------- //
export type BadgeFieldType = 'FULL_NAME' | 'POSITIONS' | 'CATEGORY' | 'ROLE';
export type BadgeAlign = 'LEFT' | 'CENTER' | 'RIGHT';
export interface BadgeFieldIn {
field_type: BadgeFieldType;
x_mm: number;
y_mm: number;
width_mm: number;
height_mm: number;
font_size: number;
align: BadgeAlign;
bold: boolean;
color: string;
}
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;
fields: BadgeFieldRead[];
}
export interface BadgeGenerateRequest {
volunteer_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[];
}