Files
siop2/packages/shared/src/contract.ts
pr-daaif 460ef4a80e feat(r3.4): corrections de recette (8 écarts) + recherche globale ⌘K
Arbitrage du référent sur la revue pixel : tout corriger, activer la
recherche.

- Stock : filtre fournisseur, sous-seuil en tête, « Entrée de stock »
  depuis la liste ; fiche pièce : fournisseur → lien Tiers.
- Statistiques : période 3/6/12 mois (paramètre months au contrat).
- Tiers : rattachements syndic→site (migration r3_recette_fixes,
  Location.partnerId gardé CLIENT), éditable sur la fiche site, seedé.
- Bibliothèque : filtre « Rattaché à » + glisser-déposer (modale
  préremplie, rattachement toujours requis).
- Recherche globale : GET /search (73 opérations) — familles OT/
  ascenseurs/sites filtrées par la matrice, « voir autre » respecté ;
  topbar ⌘K, debounce, résultats groupés, navigation clavier.

74 tests API (8 nouveaux sur le scoping de la recherche), 14/14
Playwright dont un parcours « recette corrigée », 18/18 contrôles en
navigateur réel, zéro erreur console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:14:43 +01:00

1028 lines
32 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.

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 { SearchResponseSchema } from './schemas/search';
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; description?: string }[];
/** Upload multipart/form-data : champs déclarés ('file' = binaire). */
multipartFields?: Record<string, 'file' | 'string'>;
/** 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 nest 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 dorganes)',
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 larbre)',
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 dappareils 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 demplacement propre)',
tags: ['assets'],
pathParams: ['id'],
request: { name: 'AssetComponentCreate', schema: AssetComponentCreateSchema },
responses: {
201: { description: 'Ajouté', name: 'AssetComponent', schema: AssetComponentSchema },
400: { description: 'Le type choisi nest pas un type dorgane' },
},
},
{
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 linvitation)',
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 dactivation (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 dactivation dun 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 lAPI (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'],
queryParams: [{ name: 'months', description: 'Période en mois : 3, 6 ou 12 (défaut)' }],
responses: {
200: { description: 'Synthèse', name: 'AnalyticsSummary', schema: AnalyticsSummarySchema },
},
},
{
operationId: 'globalSearch',
method: 'get',
path: '/search',
summary: 'Recherche globale (OT, ascenseurs, sites) — chaque famille filtrée par la matrice',
tags: ['search'],
queryParams: [{ name: 'q', required: true, description: '2 caractères minimum' }],
responses: {
200: { description: 'Résultats (5 max par famille)', name: 'SearchResponse', schema: SearchResponseSchema },
},
},
// ————— 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 lOT — 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 lappareil (relevés récents dabord)',
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 lOT 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,
},
},
},
];