// 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; } // --------------------------------------------------------------------------- // // 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; } export interface ImportAnalyzeResponse { filename: string; format: 'csv' | 'xlsx'; headers: string[]; row_count: number; preview: ImportPreviewRow[]; suggested_mapping: Partial>; } export type ImportSeverity = 'ERROR' | 'WARNING'; export type ImportRowAction = 'CREATED' | 'UPDATED' | 'SKIPPED'; export interface ImportIssue { code: string; severity: ImportSeverity; field?: string | null; params: Record; } 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; }; }