diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts index 18d8963..5e344db 100644 --- a/src/search/search.controller.ts +++ b/src/search/search.controller.ts @@ -1,17 +1,16 @@ import { Controller, Get, Query } from '@nestjs/common'; -import { and, eq, ilike, isNull, or } from 'drizzle-orm'; +import { and, desc, eq, ilike, isNull, or } from 'drizzle-orm'; import { CurrentSession } from '../auth/session.decorator'; import type { AuthenticatedSession } from '../auth/tenant.guard'; import { db } from '../db/client'; import { - decisions, - goals, - organizations, - researchBriefs, - savedSegments, - tasks, + organizations, tasks, goals, decisions, researchBriefs, savedSegments, + contacts, risks, projects, contracts, obligations, pipelineDeals, } from '../db/schema'; +interface SearchItem { type: string; id: string; title: string; subtitle?: string; route: string; } +interface SearchGroup { type: string; label: string; items: SearchItem[]; } + @Controller('search') export class SearchController { @Get() @@ -20,104 +19,114 @@ export class SearchController { @Query('q') q = '', ) { const term = q.trim(); - if (!term || term.length < 2) { - return { q: term, groups: [] }; - } + if (!term || term.length < 2) return { q: term, groups: [] }; - const pat = `%${term}%`; const tid = session.tenantId; + const pat = `%${term}%`; + const B = '/dashboard'; - const [orgs, tks, gls, decs, briefs, segs] = await Promise.all([ + const [ + orgs, tks, gls, decs, briefs, segs, + ctcs, rsks, projs, ctrcts, obligs, deals, + ] = await Promise.all([ db.query.organizations.findMany({ - where: and( - eq(organizations.tenantId, tid), - isNull(organizations.deletedAt), - or(ilike(organizations.name, pat), ilike(organizations.domain, pat)), - ), - columns: { id: true, name: true, country: true, domain: true }, - limit: 6, + where: and(eq(organizations.tenantId, tid), isNull(organizations.deletedAt), or(ilike(organizations.name, pat), ilike(organizations.domain, pat))), + columns: { id: true, name: true, country: true, domain: true }, limit: 5, }), db.query.tasks.findMany({ - where: and( - eq(tasks.tenantId, tid), - isNull(tasks.deletedAt), - ilike(tasks.title, pat), - ), - columns: { id: true, title: true, status: true }, - limit: 6, + where: and(eq(tasks.tenantId, tid), ilike(tasks.title, pat)), + columns: { id: true, title: true, status: true }, limit: 5, }), db.query.goals.findMany({ - where: and( - eq(goals.tenantId, tid), - isNull(goals.deletedAt), - or(ilike(goals.metric, pat), ilike(goals.horizon, pat)), - ), - columns: { id: true, metric: true, target: true, horizon: true, status: true }, - limit: 6, + where: and(eq(goals.tenantId, tid), or(ilike(goals.metric, pat), ilike(goals.horizon, pat))), + columns: { id: true, metric: true, target: true, horizon: true, status: true }, limit: 5, }), db.query.decisions.findMany({ - where: and( - eq(decisions.tenantId, tid), - ilike(decisions.context, pat), - ), - columns: { id: true, context: true, selectedOption: true }, - limit: 6, + where: and(eq(decisions.tenantId, tid), ilike(decisions.context, pat)), + columns: { id: true, context: true, selectedOption: true }, limit: 5, }), db.query.researchBriefs.findMany({ - where: and( - eq(researchBriefs.tenantId, tid), - or(ilike(researchBriefs.title, pat), ilike(researchBriefs.organizationName, pat)), - ), - columns: { id: true, title: true, organizationName: true }, - limit: 6, + where: and(eq(researchBriefs.tenantId, tid), or(ilike(researchBriefs.title, pat), ilike(researchBriefs.organizationName, pat))), + columns: { id: true, title: true, organizationName: true }, limit: 4, }), db.query.savedSegments.findMany({ - where: and( - eq(savedSegments.tenantId, tid), - ilike(savedSegments.name, pat), - ), - columns: { id: true, name: true }, - limit: 6, + where: and(eq(savedSegments.tenantId, tid), ilike(savedSegments.name, pat)), + columns: { id: true, name: true }, limit: 4, + }), + db.query.contacts.findMany({ + where: and(eq(contacts.tenantId, tid), isNull(contacts.deletedAt), or(ilike(contacts.fullName, pat), ilike(contacts.email, pat), ilike(contacts.role, pat))), + columns: { id: true, fullName: true, email: true, role: true }, limit: 5, + }), + db.query.risks.findMany({ + where: and(eq(risks.tenantId, tid), or(ilike(risks.title, pat), ilike(risks.category, pat))), + columns: { id: true, title: true, status: true, probability: true, impact: true }, limit: 5, + }), + db.query.projects.findMany({ + where: and(eq(projects.tenantId, tid), isNull(projects.deletedAt), or(ilike(projects.name, pat), ilike(projects.description, pat))), + columns: { id: true, name: true, status: true, priority: true }, limit: 5, + }), + db.query.contracts.findMany({ + where: and(eq(contracts.tenantId, tid), isNull(contracts.deletedAt), ilike(contracts.title, pat)), + columns: { id: true, title: true, contractType: true, status: true }, limit: 4, + }), + db.query.obligations.findMany({ + where: and(eq(obligations.tenantId, tid), ilike(obligations.title, pat)), + columns: { id: true, title: true, status: true, category: true }, limit: 4, + }), + db.query.pipelineDeals.findMany({ + where: and(eq(pipelineDeals.tenantId, tid), isNull(pipelineDeals.deletedAt), ilike(pipelineDeals.title, pat)), + columns: { id: true, title: true, stage: true }, limit: 4, }), ]); - type SearchItem = { type: string; id: string; title: string; subtitle: string; route: string }; - const item = (type: string, id: string, title: string, subtitle: string, route: string): SearchItem => ({ - type, id, title, subtitle, route, - }); - - const groups = [ + const groups: SearchGroup[] = [ { - type: 'organization', - label: 'Organizații', - items: orgs.map((r) => item('organization', r.id, r.name, r.domain ?? r.country ?? '', '/dashboard/organizations')), + type: 'organization', label: 'Organizații', + items: orgs.map((o) => ({ type: 'organization', id: o.id, title: o.name, subtitle: o.country ?? o.domain ?? undefined, route: `${B}/organizations` })), }, { - type: 'task', - label: 'Taskuri', - items: tks.map((r) => item('task', r.id, r.title, r.status, '/dashboard/tasks')), + type: 'task', label: 'Taskuri', + items: tks.map((t) => ({ type: 'task', id: t.id, title: t.title, subtitle: t.status, route: `${B}/tasks` })), }, { - type: 'goal', - label: 'Obiective', - items: gls.map((r) => item('goal', r.id, r.metric, `${r.horizon} → ${r.target}`, '/dashboard/goals')), + type: 'goal', label: 'Obiective', + items: gls.map((g) => ({ type: 'goal', id: g.id, title: g.metric, subtitle: `${g.target} · ${g.horizon}`, route: `${B}/goals` })), }, { - type: 'decision', - label: 'Decizii', - items: decs.map((r) => - item('decision', r.id, r.context.slice(0, 80), r.selectedOption ?? 'în analiză', '/dashboard/decisions/workspace'), - ), + type: 'decision', label: 'Decizii', + items: decs.map((d) => ({ type: 'decision', id: d.id, title: (d.context ?? '').slice(0, 60), subtitle: d.selectedOption ?? 'În analiză', route: `${B}/decisions/workspace` })), }, { - type: 'research', - label: 'Research Briefs', - items: briefs.map((r) => item('research', r.id, r.title, r.organizationName ?? '', '/dashboard/research')), + type: 'contact', label: 'Contacte', + items: ctcs.map((c) => ({ type: 'contact', id: c.id, title: c.fullName, subtitle: c.role ?? c.email ?? undefined, route: `${B}/crm` })), }, { - type: 'segment', - label: 'Segmente', - items: segs.map((r) => item('segment', r.id, r.name, '', '/dashboard/segments')), + type: 'project', label: 'Proiecte', + items: projs.map((p) => ({ type: 'project', id: p.id, title: p.name, subtitle: `${p.status} · ${p.priority}`, route: `${B}/projects` })), + }, + { + type: 'risk', label: 'Riscuri', + items: rsks.map((r) => ({ type: 'risk', id: r.id, title: r.title, subtitle: `${r.probability}/${r.impact} · ${r.status}`, route: `${B}/risks` })), + }, + { + type: 'contract', label: 'Contracte', + items: ctrcts.map((c) => ({ type: 'contract', id: c.id, title: c.title, subtitle: `${c.contractType} · ${c.status}`, route: `${B}/contracts` })), + }, + { + type: 'obligation', label: 'Obligații', + items: obligs.map((o) => ({ type: 'obligation', id: o.id, title: o.title, subtitle: o.category, route: `${B}/obligations` })), + }, + { + type: 'deal', label: 'Pipeline', + items: deals.map((d) => ({ type: 'deal', id: d.id, title: d.title, subtitle: d.stage, route: `${B}/pipeline` })), + }, + { + type: 'research', label: 'Research', + items: briefs.map((b) => ({ type: 'research', id: b.id, title: b.title, subtitle: b.organizationName ?? undefined, route: `${B}/research` })), + }, + { + type: 'segment', label: 'Segmente', + items: segs.map((s) => ({ type: 'segment', id: s.id, title: s.name, route: `${B}/segments` })), }, ].filter((g) => g.items.length > 0);