mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
- FileStorage : putObject/getObjectStream/removeObject, bucket créé au démarrage — MinIO confiné à son implémentation (règle ESLint intacte) - documents : upload multipart (PDF/JPG/PNG, 20 Mo max, rattachement appareil OU OT requis, permission d'édition sur la CIBLE), liste filtrable, téléchargement STREAMÉ par l'API (MinIO jamais exposé), suppression ; e2e : octets téléchargés identiques aux octets envoyés - analytics : GET /analytics/summary dérivé du réel — coûts/mois (mouvements + main-d'œuvre figés), pannes par organe (bilans codés), taux de préventif, durée moyenne de résolution, top équipements - générateur OpenAPI : query params, multipart, réponse binaire (71 ops) - CI : service MinIO (bitnami) sur les jobs api et e2e - test de régression du tri « Interventions récentes » rendu déterministe (positions absolues instables sous 12 suites parallèles) ; 58 tests, 6 runs complets consécutifs verts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1015 lines
32 KiB
TypeScript
1015 lines
32 KiB
TypeScript
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<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 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,
|
||
},
|
||
},
|
||
},
|
||
];
|