feat(r3.4): corrections de recette (8 écarts) + recherche globale ⌘K

Arbitrage du référent sur la revue pixel : tout corriger, activer la
recherche.

- Stock : filtre fournisseur, sous-seuil en tête, « Entrée de stock »
  depuis la liste ; fiche pièce : fournisseur → lien Tiers.
- Statistiques : période 3/6/12 mois (paramètre months au contrat).
- Tiers : rattachements syndic→site (migration r3_recette_fixes,
  Location.partnerId gardé CLIENT), éditable sur la fiche site, seedé.
- Bibliothèque : filtre « Rattaché à » + glisser-déposer (modale
  préremplie, rattachement toujours requis).
- Recherche globale : GET /search (73 opérations) — familles OT/
  ascenseurs/sites filtrées par la matrice, « voir autre » respecté ;
  topbar ⌘K, debounce, résultats groupés, navigation clavier.

74 tests API (8 nouveaux sur le scoping de la recherche), 14/14
Playwright dont un parcours « recette corrigée », 18/18 contrôles en
navigateur réel, zéro erreur console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-17 02:14:43 +01:00
parent c46a97ed08
commit 460ef4a80e
38 changed files with 1318 additions and 65 deletions

View File

@@ -38,16 +38,20 @@ export type DocumentsResponse = z.infer<typeof DocumentsResponseSchema>;
// ————— Analytics (écran Statistiques, maquette R3) —————
export const ANALYTICS_PERIODS = [3, 6, 12] as const;
export type AnalyticsPeriod = (typeof ANALYTICS_PERIODS)[number];
export const AnalyticsSummarySchema = z.object({
months: z.number().int(), // période appliquée (3, 6 ou 12 mois)
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)
closed: z.object({ total: z.number().int(), preventive: z.number().int() }),
preventiveRate: z.number().nullable(), // grilles terminées / générées (période)
avgResolutionDays: z.number().nullable(), // dépannages terminés (période)
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
costsByMonth: z.array(z.object({ month: z.string(), total: z.number() })), // un point par mois de la période
topAssets: z.array(
z.object({
reference: z.string(),

View File

@@ -39,6 +39,7 @@ export const PartnerSchema = z.object({
city: z.string().nullable(),
isActive: z.boolean(),
openOrders: z.number().int(), // BC non reçus/annulés (fournisseur)
siteNames: z.array(z.string()), // sites gérés (client/syndic) — maquette « Rattachements »
});
export type PartnerDto = z.infer<typeof PartnerSchema>;

View File

@@ -41,6 +41,8 @@ export const LocationSchema = z.object({
guardianPhone: z.string().nullable(),
latitude: z.number().nullable(),
longitude: z.number().nullable(),
partnerId: z.uuid().nullable(), // client/syndic du site (R3 — rattachements Tiers)
partnerName: z.string().nullable(),
assetCount: z.number().int(), // appareils rattachés (zones incluses pour un site)
});
export type LocationDto = z.infer<typeof LocationSchema>;
@@ -59,6 +61,7 @@ export const LocationCreateSchema = z.object({
guardianPhone: z.string().max(40).optional(),
latitude: z.number().min(-90).max(90).optional(),
longitude: z.number().min(-180).max(180).optional(),
partnerId: z.uuid().nullable().optional(), // null = détacher le client/syndic
});
export type LocationCreate = z.infer<typeof LocationCreateSchema>;

View File

@@ -0,0 +1,37 @@
import { z } from 'zod';
import { WORK_ORDER_STATUSES } from '../exploitation';
/** Recherche globale (topbar ⌘K) — chaque famille de résultats n'apparaît
* que si l'appelant a la permission `view` du domaine ; les OT respectent
* en plus l'invariant « voir autre ». */
export const SEARCH_MIN_CHARS = 2;
export const SEARCH_MAX_PER_KIND = 5;
export const SearchResponseSchema = z.object({
workOrders: z.array(
z.object({
id: z.uuid(),
reference: z.string(),
title: z.string(),
status: z.enum(WORK_ORDER_STATUSES),
}),
),
assets: z.array(
z.object({
id: z.uuid(),
reference: z.string(),
brand: z.string(),
model: z.string().nullable(),
siteName: z.string(),
}),
),
sites: z.array(
z.object({
id: z.uuid(),
name: z.string(),
city: z.string().nullable(),
}),
),
});
export type SearchResponse = z.infer<typeof SearchResponseSchema>;