import { Injectable } from '@nestjs/common'; import type { Prisma } from '@prisma/client'; import { SEARCH_MAX_PER_KIND, SEARCH_MIN_CHARS, type SearchResponse } from '@siop/shared'; import type { AuthenticatedUser } from '../auth/current-user.decorator'; import { PermissionsService } from '../permissions/permissions.service'; import { PrismaService } from '../prisma/prisma.service'; /** Recherche globale de la topbar (⌘K). Chaque famille n'est interrogée que * si le rôle a la permission `view` du domaine ; les OT respectent en plus * l'invariant « voir autre » (même règle que la liste des OT). */ @Injectable() export class SearchService { constructor( private readonly prisma: PrismaService, private readonly permissions: PermissionsService, ) {} async search(user: AuthenticatedUser, q: string): Promise { const terme = q.trim(); const vide: SearchResponse = { workOrders: [], assets: [], sites: [] }; if (terme.length < SEARCH_MIN_CHARS) return vide; const [voitOT, voitParc, voitSites] = await Promise.all([ this.permissions.can(user.roleId, 'WORK_ORDERS', 'view'), this.permissions.can(user.roleId, 'ASSETS', 'view'), this.permissions.can(user.roleId, 'LOCATIONS', 'view'), ]); const contient = (champ: string): Prisma.StringFilter => ({ contains: champ, mode: 'insensitive', }); const [workOrders, assets, sites] = await Promise.all([ voitOT ? this.prisma.workOrder.findMany({ where: { AND: [ { OR: [{ reference: contient(terme) }, { title: contient(terme) }] }, await this.scopeOT(user), ], }, select: { id: true, reference: true, title: true, status: true }, orderBy: { createdAt: 'desc' }, take: SEARCH_MAX_PER_KIND, }) : [], voitParc ? this.prisma.asset.findMany({ where: { OR: [ { reference: contient(terme) }, { brand: contient(terme) }, { model: contient(terme) }, ], }, select: { id: true, reference: true, brand: true, model: true, location: { select: { name: true, parent: { select: { name: true } } } }, }, orderBy: { reference: 'asc' }, take: SEARCH_MAX_PER_KIND, }) : [], voitSites ? this.prisma.location.findMany({ where: { parentId: null, OR: [{ name: contient(terme) }, { city: contient(terme) }], }, select: { id: true, name: true, city: true }, orderBy: { name: 'asc' }, take: SEARCH_MAX_PER_KIND, }) : [], ]); return { workOrders, assets: assets.map((a) => ({ id: a.id, reference: a.reference, brand: a.brand, model: a.model, siteName: a.location.parent?.name ?? a.location.name, })), sites, }; } /** Même invariant que WorkOrdersService.scope — sans le droit « voir * autre », on ne trouve que SES OT. */ private async scopeOT(user: AuthenticatedUser): Promise { const viewOther = await this.permissions.can(user.roleId, 'WORK_ORDERS', 'viewOther'); if (viewOther) return {}; return { OR: [ { assignees: { some: { id: user.userId } } }, { createdById: user.userId }, ], }; } }