feat(CC-065): extend global search to include contacts, risks, projects, contracts, obligations, pipeline
This commit is contained in:
parent
e368f05ca4
commit
972a0f6f9d
1 changed files with 86 additions and 77 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue