feat(CC-065): extend global search to include contacts, risks, projects, contracts, obligations, pipeline

This commit is contained in:
admin-valentin 2026-08-01 21:29:51 +00:00
parent e368f05ca4
commit 972a0f6f9d

View file

@ -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);