diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts new file mode 100644 index 0000000..18d8963 --- /dev/null +++ b/src/search/search.controller.ts @@ -0,0 +1,127 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { and, 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, +} from '../db/schema'; + +@Controller('search') +export class SearchController { + @Get() + async search( + @CurrentSession() session: AuthenticatedSession, + @Query('q') q = '', + ) { + const term = q.trim(); + if (!term || term.length < 2) { + return { q: term, groups: [] }; + } + + const pat = `%${term}%`; + const tid = session.tenantId; + + const [orgs, tks, gls, decs, briefs, segs] = 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, + }), + 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, + }), + 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, + }), + db.query.decisions.findMany({ + where: and( + eq(decisions.tenantId, tid), + ilike(decisions.context, pat), + ), + columns: { id: true, context: true, selectedOption: true }, + limit: 6, + }), + 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, + }), + db.query.savedSegments.findMany({ + where: and( + eq(savedSegments.tenantId, tid), + ilike(savedSegments.name, pat), + ), + columns: { id: true, name: true }, + limit: 6, + }), + ]); + + 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 = [ + { + type: 'organization', + label: 'Organizații', + items: orgs.map((r) => item('organization', r.id, r.name, r.domain ?? r.country ?? '', '/dashboard/organizations')), + }, + { + type: 'task', + label: 'Taskuri', + items: tks.map((r) => item('task', r.id, r.title, r.status, '/dashboard/tasks')), + }, + { + type: 'goal', + label: 'Obiective', + items: gls.map((r) => item('goal', r.id, r.metric, `${r.horizon} → ${r.target}`, '/dashboard/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: 'research', + label: 'Research Briefs', + items: briefs.map((r) => item('research', r.id, r.title, r.organizationName ?? '', '/dashboard/research')), + }, + { + type: 'segment', + label: 'Segmente', + items: segs.map((r) => item('segment', r.id, r.name, '', '/dashboard/segments')), + }, + ].filter((g) => g.items.length > 0); + + const totalCount = groups.reduce((s, g) => s + g.items.length, 0); + return { q: term, totalCount, groups }; + } +}