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

@@ -21,9 +21,16 @@ type LocationRow = {
guardianPhone: string | null;
latitude: number | null;
longitude: number | null;
partnerId: string | null;
partner: { name: string } | null;
_count: { assets: number };
};
const locationInclude = {
partner: { select: { name: true } },
_count: { select: { assets: true } },
} as const;
@Injectable()
export class LocationsService {
constructor(private readonly prisma: PrismaService) {}
@@ -31,7 +38,7 @@ export class LocationsService {
/** Liste plate ; l'assetCount d'un SITE inclut les appareils de ses zones. */
async list(): Promise<LocationsResponse> {
const rows: LocationRow[] = await this.prisma.location.findMany({
include: { _count: { select: { assets: true } } },
include: locationInclude,
orderBy: { name: 'asc' },
});
const childAssets = new Map<string, number>();
@@ -52,9 +59,10 @@ export class LocationsService {
async create(dto: LocationCreate): Promise<LocationDto> {
await this.assertDepth(dto.parentId);
await this.assertClientPartner(dto.partnerId);
const created = await this.prisma.location.create({
data: dto,
include: { _count: { select: { assets: true } } },
include: locationInclude,
});
return this.toDto(created, 0);
}
@@ -76,10 +84,11 @@ export class LocationsService {
}
await this.assertDepth(dto.parentId);
}
await this.assertClientPartner(dto.partnerId);
const updated = await this.prisma.location.update({
where: { id },
data: dto,
include: { _count: { select: { assets: true } } },
include: locationInclude,
});
return this.toDto(updated, updated._count.assets);
}
@@ -96,6 +105,16 @@ export class LocationsService {
}
}
/** R3 : seul un tiers Client/syndic peut gérer un site. */
private async assertClientPartner(partnerId?: string | null): Promise<void> {
if (!partnerId) return;
const partner = await this.prisma.partner.findUnique({ where: { id: partnerId } });
if (!partner) throw new BadRequestException('Tiers inconnu');
if (partner.kind !== 'CLIENT') {
throw new BadRequestException('Seul un tiers « Client / syndic » peut être rattaché à un site');
}
}
private toDto(row: Omit<LocationRow, '_count'>, assetCount: number): LocationDto {
return {
id: row.id,
@@ -107,6 +126,8 @@ export class LocationsService {
guardianPhone: row.guardianPhone,
latitude: row.latitude,
longitude: row.longitude,
partnerId: row.partnerId,
partnerName: row.partner?.name ?? null,
assetCount,
};
}