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
286 lines
7.6 KiB
TypeScript
286 lines
7.6 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 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[];
|
||
}
|