mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r3.2): bibliothèque de documents (FileStorage réel) + analytics
- 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>
This commit is contained in:
@@ -44,6 +44,11 @@ import {
|
||||
PortalRequestCreateSchema,
|
||||
PortalRequestStatusSchema,
|
||||
} from './schemas/portail';
|
||||
import {
|
||||
AnalyticsSummarySchema,
|
||||
DocumentSchema,
|
||||
DocumentsResponseSchema,
|
||||
} from './schemas/documents';
|
||||
import {
|
||||
ConsumePartSchema,
|
||||
LaborTimeCreateSchema,
|
||||
@@ -108,8 +113,14 @@ export interface ApiOperation {
|
||||
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) — tous UUID en R1. */
|
||||
/** 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,
|
||||
@@ -436,6 +447,66 @@ export const API_CONTRACT: ApiOperation[] = [
|
||||
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',
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from './schemas/exploitation';
|
||||
export * from './schemas/preventif';
|
||||
export * from './schemas/portail';
|
||||
export * from './schemas/gestion';
|
||||
export * from './schemas/documents';
|
||||
export * from './schemas/auth';
|
||||
export * from './schemas/users';
|
||||
export * from './schemas/users-admin';
|
||||
|
||||
62
packages/shared/src/schemas/documents.ts
Normal file
62
packages/shared/src/schemas/documents.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** Bibliothèque de documents (R3) — types fermés, rattachement obligatoire. */
|
||||
|
||||
export const DOCUMENT_KINDS = ['NOTICE', 'CERTIFICATE', 'PHOTO', 'OTHER'] as const;
|
||||
export type DocumentKind = (typeof DOCUMENT_KINDS)[number];
|
||||
export const DOCUMENT_KIND_LABELS: Record<DocumentKind, string> = {
|
||||
NOTICE: 'Notice',
|
||||
CERTIFICATE: 'Certificat',
|
||||
PHOTO: 'Photo',
|
||||
OTHER: 'Autre',
|
||||
};
|
||||
|
||||
export const DOCUMENT_MAX_BYTES = 20 * 1024 * 1024; // 20 Mo
|
||||
export const DOCUMENT_CONTENT_TYPES = [
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
] as const;
|
||||
|
||||
export const DocumentSchema = z.object({
|
||||
id: z.uuid(),
|
||||
kind: z.enum(DOCUMENT_KINDS),
|
||||
fileName: z.string(),
|
||||
size: z.number().int(),
|
||||
contentType: z.string(),
|
||||
assetReference: z.string().nullable(),
|
||||
workOrderReference: z.string().nullable(),
|
||||
uploadedByName: z.string().nullable(),
|
||||
createdAt: z.iso.datetime(),
|
||||
});
|
||||
export type DocumentDto = z.infer<typeof DocumentSchema>;
|
||||
|
||||
export const DocumentsResponseSchema = z.object({
|
||||
documents: z.array(DocumentSchema),
|
||||
});
|
||||
export type DocumentsResponse = z.infer<typeof DocumentsResponseSchema>;
|
||||
|
||||
// ————— Analytics (écran Statistiques, maquette R3) —————
|
||||
|
||||
export const AnalyticsSummarySchema = z.object({
|
||||
monthCost: z.number(), // MAD, mois courant (pièces + main-d'œuvre)
|
||||
previousMonthCost: z.number(),
|
||||
closed12m: z.object({ total: z.number().int(), preventive: z.number().int() }),
|
||||
preventiveRate: z.number().nullable(), // grilles terminées / générées (12 mois)
|
||||
avgResolutionDays: z.number().nullable(), // dépannages terminés (12 mois)
|
||||
failuresByComponent: z.array(
|
||||
z.object({ label: z.string(), count: z.number().int() }),
|
||||
),
|
||||
costsByMonth: z.array(z.object({ month: z.string(), total: z.number() })), // 6 derniers
|
||||
topAssets: z.array(
|
||||
z.object({
|
||||
reference: z.string(),
|
||||
siteName: z.string(),
|
||||
correctives: z.number().int(),
|
||||
partsCost: z.number(),
|
||||
laborCost: z.number(),
|
||||
total: z.number(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
export type AnalyticsSummary = z.infer<typeof AnalyticsSummarySchema>;
|
||||
Reference in New Issue
Block a user