import type { z } from 'zod'; import { AuthResponseSchema, DemoAccountsResponseSchema, DemoLoginRequestSchema, LoginRequestSchema, } from './schemas/auth'; import { MeResponseSchema } from './schemas/users'; import { HealthResponseSchema } from './schemas/health'; import { AssetCreateSchema, AssetComponentCreateSchema, AssetComponentSchema, AssetDetailSchema, AssetOptionsResponseSchema, AssetsResponseSchema, AssetUpdateSchema, CategoriesResponseSchema, CategoryCreateSchema, CategorySchema, CategoryUpdateSchema, LocationCreateSchema, LocationSchema, LocationsResponseSchema, LocationUpdateSchema, TeamCreateSchema, TeamSchema, TeamsResponseSchema, TeamUpdateSchema, } from './schemas/referentiel'; import { ActivateRequestSchema, ActivateResponseSchema, InvitationCreateSchema, InvitationResponseSchema, RolesResponseSchema, UserAdminSchema, UsersResponseSchema, UserUpdateSchema, } from './schemas/users-admin'; import { PortalAssetSchema, PortalRequestCreatedSchema, PortalRequestCreateSchema, PortalRequestStatusSchema, } from './schemas/portail'; import { AnalyticsSummarySchema, DocumentSchema, DocumentsResponseSchema, } from './schemas/documents'; import { ConsumePartSchema, LaborTimeCreateSchema, PartCreateSchema, PartDetailSchema, PartnerCreateSchema, PartnerSchema, PartnersResponseSchema, PartnerUpdateSchema, PartsResponseSchema, PartUpdateSchema, PurchaseOrderCreateSchema, PurchaseOrderSchema, PurchaseOrdersResponseSchema, PurchaseOrderTransitionSchema, StockMovementCreateSchema, } from './schemas/gestion'; import { MeterReadingCreateSchema, MetersResponseSchema, PreventiveGenerateSchema, PreventiveGenerationResultSchema, PreventiveStatusSchema, TaskTemplateCreateSchema, TaskTemplateSchema, TaskTemplatesResponseSchema, TaskTemplateUpdateSchema, } from './schemas/preventif'; import { AssigneesUpdateSchema, ChecklistItemSchema, ChecklistPatchSchema, CommentCreateSchema, ReferenceValueCreateSchema, ReferenceValueSchema, ReferenceValuesResponseSchema, ReferenceValueUpdateSchema, ReportUpsertSchema, RequestApproveSchema, RequestCreateSchema, RequestRejectSchema, RequestsResponseSchema, RequestSummarySchema, TransitionRequestSchema, WorkOrderCreateSchema, WorkOrderDetailSchema, WorkOrdersResponseSchema, } from './schemas/exploitation'; /** * Contrat d'API R0 — source unique de vérité (règle d'or ADR-001). * `scripts/generate-openapi.ts` en dérive `docs/openapi.json` (committée) ; * les clients web/mobile sont générés depuis cette spec, dans le même commit. */ export interface ApiOperation { operationId: string; method: 'get' | 'post' | 'put' | 'patch' | 'delete'; path: string; summary: string; tags: string[]; /** Route annotée @Public() côté API (pas de JWT requis). */ isPublic?: boolean; /** ADR-002 : la route N'EXISTE PAS (404) si DEMO_MODE n'est pas actif. */ demoOnly?: boolean; /** Paramètres de chemin (`{id}` dans path) — `id`/`…Id` = UUID. */ pathParams?: string[]; /** Paramètres de requête (?a=…&b=…). */ queryParams?: { name: string; required?: boolean }[]; /** Upload multipart/form-data : champs déclarés ('file' = binaire). */ multipartFields?: Record; /** Réponse 200 binaire (téléchargement streamé). */ binaryResponse?: boolean; request?: { name: string; schema: z.ZodType }; responses: Record< number, { description: string; name?: string; schema?: z.ZodType } >; } export const API_CONTRACT: ApiOperation[] = [ { operationId: 'login', method: 'post', path: '/auth/login', summary: 'Connexion par e-mail et mot de passe', tags: ['auth'], isPublic: true, request: { name: 'LoginRequest', schema: LoginRequestSchema }, responses: { 200: { description: 'Jeton émis', name: 'AuthResponse', schema: AuthResponseSchema }, 401: { description: 'Identifiants invalides ou compte inactif' }, }, }, { operationId: 'listDemoAccounts', method: 'get', path: '/auth/demo-accounts', summary: 'Comptes de démonstration (ADR-002 — jamais de secret)', tags: ['auth', 'demo'], isPublic: true, demoOnly: true, responses: { 200: { description: 'Comptes isDemo actifs', name: 'DemoAccountsResponse', schema: DemoAccountsResponseSchema, }, }, }, { operationId: 'demoLogin', method: 'post', path: '/auth/demo-login', summary: 'Connexion 1 clic sur un compte de démonstration (ADR-002)', tags: ['auth', 'demo'], isPublic: true, demoOnly: true, request: { name: 'DemoLoginRequest', schema: DemoLoginRequestSchema }, responses: { 200: { description: 'Jeton émis', name: 'AuthResponse', schema: AuthResponseSchema }, 403: { description: 'Le compte n’est pas un compte de démonstration' }, 404: { description: 'Compte inconnu' }, }, }, { operationId: 'getMe', method: 'get', path: '/users/me', summary: 'Profil courant + matrice de permissions du rôle', tags: ['users'], responses: { 200: { description: 'Profil', name: 'MeResponse', schema: MeResponseSchema }, 401: { description: 'Non authentifié' }, }, }, { operationId: 'activateAccount', method: 'post', path: '/auth/activate', summary: 'Activer un compte invité (lien 7 jours) — connecte directement', tags: ['auth'], isPublic: true, request: { name: 'ActivateRequest', schema: ActivateRequestSchema }, responses: { 200: { description: 'Compte activé et connecté', name: 'AuthResponse', schema: ActivateResponseSchema }, 400: { description: 'Lien invalide ou expiré' }, }, }, // ————— R1 · Référentiel ————— { operationId: 'listCategories', method: 'get', path: '/categories', summary: 'Référentiels administrables (catégories d’équipement, types d’organes)', tags: ['categories'], responses: { 200: { description: 'Liste', name: 'CategoriesResponse', schema: CategoriesResponseSchema }, }, }, { operationId: 'createCategory', method: 'post', path: '/categories', summary: 'Ajouter une catégorie', tags: ['categories'], request: { name: 'CategoryCreate', schema: CategoryCreateSchema }, responses: { 201: { description: 'Créée', name: 'Category', schema: CategorySchema }, 409: { description: 'Nom déjà utilisé pour ce type' }, }, }, { operationId: 'updateCategory', method: 'patch', path: '/categories/{id}', summary: 'Renommer ou (dés)activer — jamais de suppression si utilisée', tags: ['categories'], pathParams: ['id'], request: { name: 'CategoryUpdate', schema: CategoryUpdateSchema }, responses: { 200: { description: 'Mise à jour', name: 'Category', schema: CategorySchema }, 404: { description: 'Inconnue' }, }, }, { operationId: 'listLocations', method: 'get', path: '/locations', summary: 'Sites et zones (liste plate, le client construit l’arbre)', tags: ['locations'], responses: { 200: { description: 'Liste', name: 'LocationsResponse', schema: LocationsResponseSchema }, }, }, { operationId: 'createLocation', method: 'post', path: '/locations', summary: 'Créer un site (sans parent) ou une zone (profondeur max 2)', tags: ['locations'], request: { name: 'LocationCreate', schema: LocationCreateSchema }, responses: { 201: { description: 'Créé', name: 'Location', schema: LocationSchema }, 400: { description: 'Hiérarchie trop profonde' }, }, }, { operationId: 'updateLocation', method: 'patch', path: '/locations/{id}', summary: 'Modifier un emplacement', tags: ['locations'], pathParams: ['id'], request: { name: 'LocationUpdate', schema: LocationUpdateSchema }, responses: { 200: { description: 'Mis à jour', name: 'Location', schema: LocationSchema }, 404: { description: 'Inconnu' }, }, }, { operationId: 'listAssets', method: 'get', path: '/assets', summary: 'Inventaire des appareils', tags: ['assets'], responses: { 200: { description: 'Liste', name: 'AssetsResponse', schema: AssetsResponseSchema }, }, }, { operationId: 'listAssetOptions', method: 'get', path: '/assets/options', summary: 'Options d’appareils pour le signalement (tout rôle authentifié)', tags: ['assets'], responses: { 200: { description: 'Options', name: 'AssetOptionsResponse', schema: AssetOptionsResponseSchema, }, }, }, { operationId: 'getAsset', method: 'get', path: '/assets/{id}', summary: 'Fiche appareil (identité + organes)', tags: ['assets'], pathParams: ['id'], responses: { 200: { description: 'Fiche', name: 'AssetDetail', schema: AssetDetailSchema }, 404: { description: 'Inconnu' }, }, }, { operationId: 'createAsset', method: 'post', path: '/assets', summary: 'Créer un appareil (avec ses organes) — le QR découle de la référence', tags: ['assets'], request: { name: 'AssetCreate', schema: AssetCreateSchema }, responses: { 201: { description: 'Créé', name: 'AssetDetail', schema: AssetDetailSchema }, 409: { description: 'Référence déjà utilisée' }, }, }, { operationId: 'updateAsset', method: 'patch', path: '/assets/{id}', summary: 'Modifier un appareil (dont son statut d’équipement)', tags: ['assets'], pathParams: ['id'], request: { name: 'AssetUpdate', schema: AssetUpdateSchema }, responses: { 200: { description: 'Mis à jour', name: 'AssetDetail', schema: AssetDetailSchema }, 404: { description: 'Inconnu' }, }, }, { operationId: 'addAssetComponent', method: 'post', path: '/assets/{id}/components', summary: 'Ajouter un organe (jamais d’emplacement propre)', tags: ['assets'], pathParams: ['id'], request: { name: 'AssetComponentCreate', schema: AssetComponentCreateSchema }, responses: { 201: { description: 'Ajouté', name: 'AssetComponent', schema: AssetComponentSchema }, 400: { description: 'Le type choisi n’est pas un type d’organe' }, }, }, { operationId: 'removeAssetComponent', method: 'delete', path: '/assets/{id}/components/{componentId}', summary: 'Retirer un organe', tags: ['assets'], pathParams: ['id', 'componentId'], responses: { 204: { description: 'Retiré' }, 404: { description: 'Inconnu' }, }, }, { operationId: 'listTeams', method: 'get', path: '/teams', summary: 'Équipes et leurs membres', tags: ['teams'], responses: { 200: { description: 'Liste', name: 'TeamsResponse', schema: TeamsResponseSchema }, }, }, { operationId: 'createTeam', method: 'post', path: '/teams', summary: 'Créer une équipe', tags: ['teams'], request: { name: 'TeamCreate', schema: TeamCreateSchema }, responses: { 201: { description: 'Créée', name: 'Team', schema: TeamSchema }, 409: { description: 'Nom déjà utilisé' }, }, }, { operationId: 'updateTeam', method: 'patch', path: '/teams/{id}', summary: 'Modifier une équipe (nom, description, membres)', tags: ['teams'], pathParams: ['id'], request: { name: 'TeamUpdate', schema: TeamUpdateSchema }, responses: { 200: { description: 'Mise à jour', name: 'Team', schema: TeamSchema }, 404: { description: 'Inconnue' }, }, }, { operationId: 'listUsers', method: 'get', path: '/users', summary: 'Personnes (statut dérivé : actif / invité / désactivé)', tags: ['users'], responses: { 200: { description: 'Liste', name: 'UsersResponse', schema: UsersResponseSchema }, }, }, { operationId: 'listRoles', method: 'get', path: '/roles', summary: 'Les 7 rôles (pour l’invitation)', tags: ['users'], responses: { 200: { description: 'Liste', name: 'RolesResponse', schema: RolesResponseSchema }, }, }, { operationId: 'inviteUser', method: 'post', path: '/users/invitations', summary: 'Inviter — crée le compte inactif et émet le lien d’activation (7 j)', tags: ['users'], request: { name: 'InvitationCreate', schema: InvitationCreateSchema }, responses: { 201: { description: 'Invitation émise', name: 'InvitationResponse', schema: InvitationResponseSchema }, 409: { description: 'Email déjà utilisé' }, }, }, { operationId: 'resendInvitation', method: 'post', path: '/users/{id}/invitation', summary: 'Régénérer le lien d’activation d’un compte non activé', tags: ['users'], pathParams: ['id'], responses: { 201: { description: 'Nouveau lien', name: 'InvitationResponse', schema: InvitationResponseSchema }, 409: { description: 'Compte déjà activé' }, }, }, { operationId: 'updateUser', method: 'patch', path: '/users/{id}', summary: 'Modifier une personne (rôle, équipes, activation du compte)', tags: ['users'], pathParams: ['id'], request: { name: 'UserUpdate', schema: UserUpdateSchema }, responses: { 200: { description: 'Mise à jour', name: 'UserAdmin', schema: UserAdminSchema }, 404: { description: 'Inconnue' }, }, }, // ————— R3 · Bibliothèque & analytics ————— { operationId: 'listDocuments', method: 'get', path: '/documents', summary: 'Bibliothèque (filtrable par appareil ou OT)', tags: ['documents'], queryParams: [{ name: 'assetId' }, { name: 'workOrderId' }, { name: 'kind' }], responses: { 200: { description: 'Liste', name: 'DocumentsResponse', schema: DocumentsResponseSchema }, }, }, { operationId: 'uploadDocument', method: 'post', path: '/documents', summary: 'Téléverser (PDF/JPG/PNG, 20 Mo max, rattachement appareil OU OT requis)', tags: ['documents'], multipartFields: { file: 'file', kind: 'string', assetId: 'string', workOrderId: 'string' }, responses: { 201: { description: 'Document rangé', name: 'Document', schema: DocumentSchema }, 400: { description: 'Type/taille refusé ou rattachement manquant' }, }, }, { operationId: 'downloadDocument', method: 'get', path: '/documents/{id}/download', summary: 'Télécharger — streamé par l’API (MinIO jamais exposé)', tags: ['documents'], pathParams: ['id'], binaryResponse: true, responses: { 200: { description: 'Fichier' }, 404: { description: 'Inconnu' }, }, }, { operationId: 'deleteDocument', method: 'delete', path: '/documents/{id}', summary: 'Supprimer (permission d’édition sur la cible)', tags: ['documents'], pathParams: ['id'], responses: { 204: { description: 'Supprimé' }, 404: { description: 'Inconnu' }, }, }, { operationId: 'getAnalyticsSummary', method: 'get', path: '/analytics/summary', summary: 'Le tableau de la direction : coûts, pannes par organe (bilans), préventif, top équipements', tags: ['analytics'], responses: { 200: { description: 'Synthèse', name: 'AnalyticsSummary', schema: AnalyticsSummarySchema }, }, }, // ————— R3 · Gestion (stock, achats, tiers, coûts) ————— { operationId: 'listPartners', method: 'get', path: '/partners', summary: 'Tiers (fournisseurs, syndics) — permission PURCHASE_ORDERS', tags: ['partners'], responses: { 200: { description: 'Liste', name: 'PartnersResponse', schema: PartnersResponseSchema }, }, }, { operationId: 'createPartner', method: 'post', path: '/partners', summary: 'Créer un tiers', tags: ['partners'], request: { name: 'PartnerCreate', schema: PartnerCreateSchema }, responses: { 201: { description: 'Créé', name: 'Partner', schema: PartnerSchema }, 409: { description: 'Nom déjà utilisé' }, }, }, { operationId: 'updatePartner', method: 'patch', path: '/partners/{id}', summary: 'Modifier / (dés)activer un tiers', tags: ['partners'], pathParams: ['id'], request: { name: 'PartnerUpdate', schema: PartnerUpdateSchema }, responses: { 200: { description: 'Mis à jour', name: 'Partner', schema: PartnerSchema }, 404: { description: 'Inconnu' }, }, }, { operationId: 'listParts', method: 'get', path: '/parts', summary: 'Stock de pièces — quantités DÉRIVÉES des mouvements', tags: ['parts'], responses: { 200: { description: 'Liste', name: 'PartsResponse', schema: PartsResponseSchema }, }, }, { operationId: 'getPart', method: 'get', path: '/parts/{id}', summary: 'Fiche pièce : les mouvements SONT le stock', tags: ['parts'], pathParams: ['id'], responses: { 200: { description: 'Fiche', name: 'PartDetail', schema: PartDetailSchema }, 404: { description: 'Inconnue' }, }, }, { operationId: 'createPart', method: 'post', path: '/parts', summary: 'Créer une pièce (référence P-#### générée)', tags: ['parts'], request: { name: 'PartCreate', schema: PartCreateSchema }, responses: { 201: { description: 'Créée', name: 'PartDetail', schema: PartDetailSchema }, }, }, { operationId: 'updatePart', method: 'patch', path: '/parts/{id}', summary: 'Modifier une pièce (désignation, seuil, fournisseur…)', tags: ['parts'], pathParams: ['id'], request: { name: 'PartUpdate', schema: PartUpdateSchema }, responses: { 200: { description: 'Mise à jour', name: 'PartDetail', schema: PartDetailSchema }, 404: { description: 'Inconnue' }, }, }, { operationId: 'addStockMovement', method: 'post', path: '/parts/{id}/movements', summary: 'Entrée manuelle (+) ou ajustement (± motif REQUIS) — jamais de saisie de stock', tags: ['parts'], pathParams: ['id'], request: { name: 'StockMovementCreate', schema: StockMovementCreateSchema }, responses: { 201: { description: 'Mouvement tracé', name: 'PartDetail', schema: PartDetailSchema }, 409: { description: 'Le stock ne peut pas devenir négatif' }, }, }, { operationId: 'listPurchaseOrders', method: 'get', path: '/purchase-orders', summary: 'Bons de commande', tags: ['purchase-orders'], responses: { 200: { description: 'Liste', name: 'PurchaseOrdersResponse', schema: PurchaseOrdersResponseSchema, }, }, }, { operationId: 'getPurchaseOrder', method: 'get', path: '/purchase-orders/{id}', summary: 'Fiche BC (lignes, total)', tags: ['purchase-orders'], pathParams: ['id'], responses: { 200: { description: 'Fiche', name: 'PurchaseOrder', schema: PurchaseOrderSchema }, 404: { description: 'Inconnu' }, }, }, { operationId: 'createPurchaseOrder', method: 'post', path: '/purchase-orders', summary: 'Créer un BC (brouillon)', tags: ['purchase-orders'], request: { name: 'PurchaseOrderCreate', schema: PurchaseOrderCreateSchema }, responses: { 201: { description: 'Créé', name: 'PurchaseOrder', schema: PurchaseOrderSchema }, 400: { description: 'Fournisseur ou pièce invalide' }, }, }, { operationId: 'transitionPurchaseOrder', method: 'post', path: '/purchase-orders/{id}/transition', summary: 'Envoyer / réceptionner (→ entrées de stock, PU figés) / annuler', tags: ['purchase-orders'], pathParams: ['id'], request: { name: 'PurchaseOrderTransition', schema: PurchaseOrderTransitionSchema }, responses: { 200: { description: 'État changé', name: 'PurchaseOrder', schema: PurchaseOrderSchema }, 409: { description: 'Transition interdite' }, }, }, { operationId: 'consumePart', method: 'post', path: '/work-orders/{id}/consume-part', summary: 'Consommer une pièce sur l’OT — stock décrémenté, PRIX FIGÉ', tags: ['work-orders'], pathParams: ['id'], request: { name: 'ConsumePart', schema: ConsumePartSchema }, responses: { 201: { description: 'Consommée', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, 409: { description: 'Stock insuffisant ou prix inconnu' }, }, }, { operationId: 'addLaborTime', method: 'post', path: '/work-orders/{id}/labor', summary: 'Saisir de la main-d’œuvre — TAUX FIGÉ à la saisie', tags: ['work-orders'], pathParams: ['id'], request: { name: 'LaborTimeCreate', schema: LaborTimeCreateSchema }, responses: { 201: { description: 'Saisie', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, 409: { description: 'Taux horaire non défini pour cette personne' }, }, }, // ————— R2 · Portail public (QR cabine — aucun compte) ————— { operationId: 'getPortalAsset', method: 'get', path: '/portal/assets/{reference}', summary: 'Résoudre le QR scanné (référence → appareil, préremplissage)', tags: ['portal'], isPublic: true, pathParams: ['reference'], responses: { 200: { description: 'Appareil', name: 'PortalAsset', schema: PortalAssetSchema }, 404: { description: 'Référence inconnue' }, }, }, { operationId: 'createPortalRequest', method: 'post', path: '/portal/requests', summary: 'Signaler sans compte — renvoie le jeton de suivi (à garder sur le téléphone)', tags: ['portal'], isPublic: true, request: { name: 'PortalRequestCreate', schema: PortalRequestCreateSchema }, responses: { 201: { description: 'Signalement enregistré', name: 'PortalRequestCreated', schema: PortalRequestCreatedSchema, }, 404: { description: 'Référence inconnue' }, 429: { description: 'Trop de signalements — réessayez dans une minute' }, }, }, { operationId: 'getPortalRequestStatus', method: 'get', path: '/portal/requests/{reference}/{token}', summary: 'Suivi sans jargon (Reçu → Intervention → Résolu) — jeton requis', tags: ['portal'], isPublic: true, pathParams: ['reference', 'token'], responses: { 200: { description: 'Avancement', name: 'PortalRequestStatus', schema: PortalRequestStatusSchema }, 404: { description: 'Signalement inconnu ou jeton invalide' }, }, }, // ————— R2 · Préventif & compteurs ————— { operationId: 'listTaskTemplates', method: 'get', path: '/preventive/templates', summary: 'Gabarits de tâches du préventif (périodicité en mois)', tags: ['preventive'], responses: { 200: { description: 'Liste', name: 'TaskTemplatesResponse', schema: TaskTemplatesResponseSchema }, }, }, { operationId: 'createTaskTemplate', method: 'post', path: '/preventive/templates', summary: 'Ajouter un gabarit', tags: ['preventive'], request: { name: 'TaskTemplateCreate', schema: TaskTemplateCreateSchema }, responses: { 201: { description: 'Créé', name: 'TaskTemplate', schema: TaskTemplateSchema }, 409: { description: 'Libellé déjà utilisé' }, }, }, { operationId: 'updateTaskTemplate', method: 'patch', path: '/preventive/templates/{id}', summary: 'Modifier / (dés)activer un gabarit — jamais de suppression si utilisé', tags: ['preventive'], pathParams: ['id'], request: { name: 'TaskTemplateUpdate', schema: TaskTemplateUpdateSchema }, responses: { 200: { description: 'Mis à jour', name: 'TaskTemplate', schema: TaskTemplateSchema }, 404: { description: 'Inconnu' }, }, }, { operationId: 'generatePreventive', method: 'post', path: '/preventive/generate', summary: 'Générer la grille du mois — IDEMPOTENT (regénérer ne double rien)', tags: ['preventive'], request: { name: 'PreventiveGenerate', schema: PreventiveGenerateSchema }, responses: { 200: { description: 'Résumé de génération', name: 'PreventiveGenerationResult', schema: PreventiveGenerationResultSchema, }, }, }, { operationId: 'getPreventiveStatus', method: 'get', path: '/preventive/status', summary: 'État du mois courant (générées, terminées, en retard, premiers contrôles)', tags: ['preventive'], responses: { 200: { description: 'État', name: 'PreventiveStatus', schema: PreventiveStatusSchema }, }, }, { operationId: 'getAssetMeters', method: 'get', path: '/assets/{id}/meters', summary: 'Compteurs de l’appareil (relevés récents d’abord)', tags: ['meters'], pathParams: ['id'], responses: { 200: { description: 'Compteurs', name: 'MetersResponse', schema: MetersResponseSchema }, 404: { description: 'Appareil inconnu' }, }, }, { operationId: 'addMeterReading', method: 'post', path: '/assets/{id}/meter-readings', summary: 'Enregistrer un relevé — strictement croissant', tags: ['meters'], pathParams: ['id'], request: { name: 'MeterReadingCreate', schema: MeterReadingCreateSchema }, responses: { 201: { description: 'Relevé enregistré', name: 'MetersResponse', schema: MetersResponseSchema }, 400: { description: 'Relevé inférieur ou égal au précédent' }, }, }, // ————— R2 · Exploitation ————— { operationId: 'listWorkOrders', method: 'get', path: '/work-orders', summary: 'Ordres de travail (sans « voir autre » : seulement les siens)', tags: ['work-orders'], responses: { 200: { description: 'Liste', name: 'WorkOrdersResponse', schema: WorkOrdersResponseSchema }, }, }, { operationId: 'getWorkOrder', method: 'get', path: '/work-orders/{id}', summary: 'Fiche OT (activité, checklist, bilan, transitions autorisées, blocages de clôture)', tags: ['work-orders'], pathParams: ['id'], responses: { 200: { description: 'Fiche', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, 404: { description: 'Inconnu (ou hors de son périmètre)' }, }, }, { operationId: 'createWorkOrder', method: 'post', path: '/work-orders', summary: 'Créer un OT (statut Ouvert)', tags: ['work-orders'], request: { name: 'WorkOrderCreate', schema: WorkOrderCreateSchema }, responses: { 201: { description: 'Créé', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, }, }, { operationId: 'transitionWorkOrder', method: 'post', path: '/work-orders/{id}/transition', summary: 'Changer l’état (machine à états stricte ; DONE exige bilan + checklist)', tags: ['work-orders'], pathParams: ['id'], request: { name: 'TransitionRequest', schema: TransitionRequestSchema }, responses: { 200: { description: 'État changé', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, 409: { description: 'Transition interdite ou clôture bloquée (garde)' }, }, }, { operationId: 'commentWorkOrder', method: 'post', path: '/work-orders/{id}/comments', summary: 'Commenter (activité chronologique)', tags: ['work-orders'], pathParams: ['id'], request: { name: 'CommentCreate', schema: CommentCreateSchema }, responses: { 201: { description: 'Commentaire ajouté', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, }, }, { operationId: 'setWorkOrderAssignees', method: 'put', path: '/work-orders/{id}/assignees', summary: 'Assigner (remplace la liste)', tags: ['work-orders'], pathParams: ['id'], request: { name: 'AssigneesUpdate', schema: AssigneesUpdateSchema }, responses: { 200: { description: 'Assignés', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, }, }, { operationId: 'upsertWorkOrderReport', method: 'put', path: '/work-orders/{id}/report', summary: 'Renseigner le bilan codé (null efface un champ)', tags: ['work-orders'], pathParams: ['id'], request: { name: 'ReportUpsert', schema: ReportUpsertSchema }, responses: { 200: { description: 'Bilan enregistré', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, 400: { description: 'Valeur hors référentiel du champ' }, }, }, { operationId: 'patchChecklistItem', method: 'patch', path: '/work-orders/{id}/checklist/{itemId}', summary: 'Régler une tâche de la grille (Fait / N-A / à faire)', tags: ['work-orders'], pathParams: ['id', 'itemId'], request: { name: 'ChecklistPatch', schema: ChecklistPatchSchema }, responses: { 200: { description: 'Tâche réglée', name: 'ChecklistItem', schema: ChecklistItemSchema }, 404: { description: 'Tâche inconnue' }, }, }, { operationId: 'listRequests', method: 'get', path: '/requests', summary: 'Demandes (sans « voir autre » : seulement les siennes)', tags: ['requests'], responses: { 200: { description: 'Liste', name: 'RequestsResponse', schema: RequestsResponseSchema }, }, }, { operationId: 'createRequest', method: 'post', path: '/requests', summary: 'Signaler (interne — le portail public QR arrive en R2.4)', tags: ['requests'], request: { name: 'RequestCreate', schema: RequestCreateSchema }, responses: { 201: { description: 'Demande créée', name: 'RequestSummary', schema: RequestSummarySchema }, }, }, { operationId: 'approveRequest', method: 'post', path: '/requests/{id}/approve', summary: 'Approuver → crée l’OT lié (1-1, jamais de doublon)', tags: ['requests'], pathParams: ['id'], request: { name: 'RequestApprove', schema: RequestApproveSchema }, responses: { 201: { description: 'OT créé et lié', name: 'WorkOrderDetail', schema: WorkOrderDetailSchema }, 409: { description: 'Demande déjà traitée' }, }, }, { operationId: 'rejectRequest', method: 'post', path: '/requests/{id}/reject', summary: 'Rejeter — motif obligatoire, lisible côté demandeur', tags: ['requests'], pathParams: ['id'], request: { name: 'RequestReject', schema: RequestRejectSchema }, responses: { 200: { description: 'Rejetée', name: 'RequestSummary', schema: RequestSummarySchema }, 409: { description: 'Demande déjà traitée' }, }, }, { operationId: 'listReferenceValues', method: 'get', path: '/reference-values', summary: 'Référentiels du bilan codé (6 champs)', tags: ['reference-values'], responses: { 200: { description: 'Liste', name: 'ReferenceValuesResponse', schema: ReferenceValuesResponseSchema, }, }, }, { operationId: 'createReferenceValue', method: 'post', path: '/reference-values', summary: 'Ajouter une valeur de référentiel', tags: ['reference-values'], request: { name: 'ReferenceValueCreate', schema: ReferenceValueCreateSchema }, responses: { 201: { description: 'Créée', name: 'ReferenceValue', schema: ReferenceValueSchema }, 409: { description: 'Libellé déjà présent pour ce champ' }, }, }, { operationId: 'updateReferenceValue', method: 'patch', path: '/reference-values/{id}', summary: 'Renommer ou (dés)activer — jamais de suppression si utilisée', tags: ['reference-values'], pathParams: ['id'], request: { name: 'ReferenceValueUpdate', schema: ReferenceValueUpdateSchema }, responses: { 200: { description: 'Mise à jour', name: 'ReferenceValue', schema: ReferenceValueSchema }, 404: { description: 'Inconnue' }, }, }, { operationId: 'getHealth', method: 'get', path: '/health', summary: 'État des dépendances (base, Redis, stockage)', tags: ['health'], isPublic: true, responses: { 200: { description: 'État agrégé', name: 'HealthResponse', schema: HealthResponseSchema, }, }, }, ];