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:
pr-daaif
2026-07-16 21:24:04 +01:00
parent 32eb20b5a0
commit 2ffdd13cf4
23 changed files with 1652 additions and 30 deletions

View 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>;