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