feat(r2.4): portail public QR — signalement sans compte, suivi par jeton

- 3 routes publiques /portal (les seules @Public métier), throttlées
  (@nestjs/throttler : 10 signalements/min, 60 lectures/min) : résolution
  du QR, signalement (retourne un jeton de suivi opaque), suivi par
  référence + jeton — pas de jeton, pas de lecture, jamais de liste
- migration r2_portail : Request.publicToken (unique)
- page /q/{réf} fidèle à l'écran 6 validé R0 (mobile d'abord) : équipement
  prérempli « détecté par le QR », interrupteur personne bloquée,
  suivi sans jargon Reçu → Intervention → Résolu (dérivé de l'OT lié),
  rejet affiché « Sans suite : {motif} », photo annoncée (upload R3) ;
  signalements gardés sur le téléphone (localStorage, 10 max)
- e2e : LA boucle produit — gardien sans compte → traitement → ✓ Résolu ;
  11 tests Playwright verts, 50 tests API, 52 opérations au contrat
- générateur OpenAPI : paramètres non-« id » plus typés uuid

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-16 17:33:10 +01:00
parent 6a2dae1710
commit 50254daea4
20 changed files with 1089 additions and 2 deletions

View File

@@ -372,6 +372,57 @@ export interface paths {
patch: operations["updateUser"];
trace?: never;
};
"/portal/assets/{reference}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Résoudre le QR scanné (référence → appareil, préremplissage) */
get: operations["getPortalAsset"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/portal/requests": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Signaler sans compte — renvoie le jeton de suivi (à garder sur le téléphone) */
post: operations["createPortalRequest"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/portal/requests/{reference}/{token}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Suivi sans jargon (Reçu → Intervention → Résolu) — jeton requis */
get: operations["getPortalRequestStatus"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/preventive/templates": {
parameters: {
query?: never;
@@ -1069,6 +1120,42 @@ export interface components {
teamIds?: string[];
isActive?: boolean;
};
PortalAsset: {
reference: string;
siteName: string;
locationName: string;
};
PortalRequestCreated: {
reference: string;
description: string;
isPersonTrapped: boolean;
/** @enum {string} */
status: "RECEIVED" | "APPROVED" | "REJECTED";
/** @enum {string} */
step: "RECEIVED" | "IN_PROGRESS" | "RESOLVED";
rejectionReason: string | null;
/** Format: date-time */
createdAt: string;
publicToken: string;
};
PortalRequestCreate: {
assetReference: string;
description: string;
isPersonTrapped?: boolean;
requesterName?: string;
};
PortalRequestStatus: {
reference: string;
description: string;
isPersonTrapped: boolean;
/** @enum {string} */
status: "RECEIVED" | "APPROVED" | "REJECTED";
/** @enum {string} */
step: "RECEIVED" | "IN_PROGRESS" | "RESOLVED";
rejectionReason: string | null;
/** Format: date-time */
createdAt: string;
};
TaskTemplatesResponse: {
templates: {
/** Format: uuid */
@@ -2172,6 +2259,103 @@ export interface operations {
};
};
};
getPortalAsset: {
parameters: {
query?: never;
header?: never;
path: {
reference: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Appareil */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["PortalAsset"];
};
};
/** @description Référence inconnue */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
createPortalRequest: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["PortalRequestCreate"];
};
};
responses: {
/** @description Signalement enregistré */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["PortalRequestCreated"];
};
};
/** @description Référence inconnue */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Trop de signalements — réessayez dans une minute */
429: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getPortalRequestStatus: {
parameters: {
query?: never;
header?: never;
path: {
reference: string;
token: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Avancement */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["PortalRequestStatus"];
};
};
/** @description Signalement inconnu ou jeton invalide */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
listTaskTemplates: {
parameters: {
query?: never;