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>
524 lines
14 KiB
TypeScript
524 lines
14 KiB
TypeScript
// 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[];
|
||
}
|