Files
benevoles/frontend/src/api/schema.ts
T
AdamThomasandClaude Sonnet 5 e0a0c56307 Édition éditable (dates) + robustesse de l'écran Postes & créneaux
Back-end :
- GET /editions/{id} (tout compte de l'édition) et PATCH /editions/{id}
  (COORDINATION) : name, year, start_date, end_date, retention_days.
  Refus si end_date < start_date (edition.dates_inconsistent). Tests.

Front-end :
- api : EditionRead/Update, editions.get/update (http + mock), hooks
  useEdition / useUpdateEdition.
- ConfigGrid : le bouton « ajouter un poste » sélectionne le poste créé et
  affiche l'état (pending) + les erreurs (ErrorNote) — avant, un échec ou
  même un succès passaient inaperçus. Renommage du poste + choix de la
  catégorie + suppression dans l'en-tête. Une colonne par jour de l'édition
  (même vide) → on peut ajouter un créneau n'importe quel jour.
- ConfigRules : formulaire d'édition (nom + dates) → c'est là qu'on ajoute
  ou retire des jours ; compteur de jours ; retour d'enregistrement.
- i18n mis à jour.

144 tests back OK ; typecheck + lint + build + smoke front OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 20:02:50 +02:00

286 lines
7.6 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 TshirtSize = 'XS' | 'S' | 'M' | 'L' | 'XL' | 'XXL' | 'XXXL';
export type AvailabilityStatus = 'AVAILABLE' | 'IF_NEEDED' | 'UNAVAILABLE';
export type Role = 'COORDINATOR' | 'LEAD' | 'VOLUNTEER';
export type AuthMethod = 'password' | 'magic_link';
export type ViolationCode =
| 'SHIFT_OVERLAP'
| 'OUTSIDE_AVAILABILITY'
| 'DAILY_HOURS_EXCEEDED'
| 'INSUFFICIENT_REST'
| 'MISSING_REQUIRED_SKILL';
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;
}
// --------------------------------------------------------------------------- //
// Volunteer //
// --------------------------------------------------------------------------- //
export interface VolunteerRead {
id: string;
edition_id: string;
first_name: string;
last_name: string;
email: string | null;
phone: string | null;
tshirt_size: TshirtSize | null;
diet: string | null;
comment: string | null;
skill_codes: string[];
created_at: string;
updated_at: string;
}
export interface VolunteerCreate {
first_name: string;
last_name: string;
email?: string | null;
phone?: string | null;
tshirt_size?: TshirtSize | null;
diet?: string | null;
comment?: string | null;
skill_codes?: string[];
}
export interface VolunteerUpdate {
first_name?: string;
last_name?: string;
email?: string | null;
phone?: string | null;
tshirt_size?: TshirtSize | null;
diet?: string | null;
comment?: string | null;
/** null / absent = inchangé ; [] = vider les compétences. */
skill_codes?: string[] | null;
}
// --------------------------------------------------------------------------- //
// 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;
/** Datetime naïf, heure locale Europe/Paris (ISO sans fuseau). */
starts_at: string;
ends_at: string;
min_staff: number;
ideal_staff: number;
required_skill_codes: string[];
created_at: string;
updated_at: string;
}
export interface ShiftCreate {
position_id: string;
starts_at: string;
ends_at: string;
min_staff?: number;
ideal_staff?: number;
required_skill_codes?: string[];
}
export interface ShiftUpdate {
position_id?: string;
starts_at?: string;
ends_at?: string;
min_staff?: number;
ideal_staff?: number;
required_skill_codes?: string[] | null;
}
// --------------------------------------------------------------------------- //
// 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[];
}
// --------------------------------------------------------------------------- //
// 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[];
}
// --------------------------------------------------------------------------- //
// 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 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[];
}