feat(CC-056): add SearchController — unified ILIKE search across orgs/tasks/goals/decisions/research/segments

This commit is contained in:
admin-valentin 2026-08-01 20:55:50 +00:00
parent ff68bb2ca5
commit 67cfd7cb12

View 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 };
}
}