feat(CC-056): add SearchController — unified ILIKE search across orgs/tasks/goals/decisions/research/segments
This commit is contained in:
parent
ff68bb2ca5
commit
67cfd7cb12
1 changed files with 127 additions and 0 deletions
127
src/search/search.controller.ts
Normal file
127
src/search/search.controller.ts
Normal file
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue