mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r2.3): écrans web exploitation — OT, demandes, préventif, compteurs, urgence
- liste OT (filtres, strie rouge, « immédiat ») ; fiche OT : transitions pilotées par allowedTransitions, clôture grisée avec la garde expliquée, bilan codé (6 selects sur référentiels), checklist Fait→N-A→à faire, activité + commentaires, assignation, annulation motivée - nouvel OT : interrupteur « personne bloquée » qui force la priorité - demandes : table + panneau d'approbation (priorité, assignation, approuver → fiche OT), rejet en modale à motif obligatoire, signalement interne ; statut « Résolue » dérivé de l'OT lié - préventif : tuiles réelles, générer + résumé (« regénérer ne double rien »), gabarits administrables ; compteurs : saisie + historique - tableau de bord réel : bandeau urgence cliquable, KPIs, OT par statut, interventions récentes ; accueil dédié aux rôles sans exploitation - urgence traversante : chip topbar pulsante (60 s), badges de nav - GET /assets/options (auth seule) : le Demandeur peut désigner l'appareil qu'il signale — trou débusqué par l'e2e (49 opérations au contrat) - génération préventive durcie : collision de référence RETENTÉE (plus de saut silencieux), P2003 toléré ; 3 runs Jest complets consécutifs verts - 9 tests Playwright (recette R2 officielle rejouée intégralement), 50 tests API (94,6 % / 78,9 %) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,13 @@ export class AssetsController {
|
||||
return this.assetsService.list();
|
||||
}
|
||||
|
||||
/** Avant ':id' (ordre des routes) — authentification seule : le demandeur
|
||||
* doit pouvoir désigner l'appareil qu'il signale. */
|
||||
@Get('options')
|
||||
options() {
|
||||
return this.assetsService.options();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission('ASSETS', 'view')
|
||||
get(@Param('id', ParseUUIDPipe) id: string) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
AssetCreate,
|
||||
AssetDetail,
|
||||
AssetDto,
|
||||
AssetOptionsResponse,
|
||||
AssetsResponse,
|
||||
AssetUpdate,
|
||||
} from '@siop/shared';
|
||||
@@ -28,6 +29,21 @@ type AssetRow = Prisma.AssetGetPayload<{ include: typeof assetInclude }>;
|
||||
export class AssetsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/** Options minimales pour le signalement — ouvert à tout rôle authentifié. */
|
||||
async options(): Promise<AssetOptionsResponse> {
|
||||
const rows = await this.prisma.asset.findMany({
|
||||
include: { location: { include: { parent: true } } },
|
||||
orderBy: { reference: 'asc' },
|
||||
});
|
||||
return {
|
||||
options: rows.map((r) => ({
|
||||
id: r.id,
|
||||
reference: r.reference,
|
||||
siteName: r.location.parent?.name ?? r.location.name,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async list(): Promise<AssetsResponse> {
|
||||
const rows = await this.prisma.asset.findMany({
|
||||
include: assetInclude,
|
||||
|
||||
@@ -129,44 +129,57 @@ export class PreventiveService {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const ot = await this.prisma.workOrder.create({
|
||||
data: {
|
||||
reference: await this.nextReference(),
|
||||
title: premierControle
|
||||
? `Premier contrôle — ${libelleMois}`
|
||||
: `Grille du mois — ${libelleMois}`,
|
||||
type: 'PREVENTIVE',
|
||||
priority: 'LOW',
|
||||
assetId: asset.id,
|
||||
periodKey: period,
|
||||
dueDate: dernierJour,
|
||||
createdById: user.userId,
|
||||
checklist: {
|
||||
create: dues.map((t) => ({ label: t.label, templateId: t.id })),
|
||||
},
|
||||
events: {
|
||||
create: {
|
||||
kind: 'GENERATED',
|
||||
message: `Généré (${premierControle ? 'premier contrôle' : 'grille mensuelle'} · ${dues.length} tâches)`,
|
||||
byId: user.userId,
|
||||
let issue: 'créé' | 'sauté' | null = null;
|
||||
for (let essai = 0; essai < 4 && issue === null; essai++) {
|
||||
try {
|
||||
const ot = await this.prisma.workOrder.create({
|
||||
data: {
|
||||
reference: await this.nextReference(),
|
||||
title: premierControle
|
||||
? `Premier contrôle — ${libelleMois}`
|
||||
: `Grille du mois — ${libelleMois}`,
|
||||
type: 'PREVENTIVE',
|
||||
priority: 'LOW',
|
||||
assetId: asset.id,
|
||||
periodKey: period,
|
||||
dueDate: dernierJour,
|
||||
createdById: user.userId,
|
||||
checklist: {
|
||||
create: dues.map((t) => ({ label: t.label, templateId: t.id })),
|
||||
},
|
||||
events: {
|
||||
create: {
|
||||
kind: 'GENERATED',
|
||||
message: `Généré (${premierControle ? 'premier contrôle' : 'grille mensuelle'} · ${dues.length} tâches)`,
|
||||
byId: user.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: { reference: true },
|
||||
});
|
||||
created++;
|
||||
if (premierControle) firstControls++;
|
||||
references.push(ot.reference);
|
||||
} catch (e) {
|
||||
// [assetId, periodKey] déjà pris → grille déjà en place : idempotence.
|
||||
// (Collision de référence, rarissime : le prochain appel rattrape.)
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
skipped++;
|
||||
continue;
|
||||
select: { reference: true },
|
||||
});
|
||||
created++;
|
||||
if (premierControle) firstControls++;
|
||||
references.push(ot.reference);
|
||||
issue = 'créé';
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
const cible = (e.meta?.target as string[] | string | undefined) ?? '';
|
||||
if (String(cible).includes('periodKey')) {
|
||||
issue = 'sauté'; // grille déjà en place : idempotence
|
||||
}
|
||||
// sinon : collision de référence (course avec une création d'OT)
|
||||
// → on recalcule et on retente
|
||||
} else if (
|
||||
e instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
e.code === 'P2003'
|
||||
) {
|
||||
issue = 'sauté'; // appareil supprimé entre lecture et écriture
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (issue !== 'créé') skipped++;
|
||||
}
|
||||
this.logger.log(
|
||||
`Préventif ${period} : ${created} générés, ${skipped} déjà en place, ${firstControls} premiers contrôles.`,
|
||||
|
||||
Reference in New Issue
Block a user