feat(search): index yachts and companies alongside clients

Extend the global search service to include yacht and company results
using ILIKE matching on name, hull number, registration, legal name,
and tax ID. Results are tenant-scoped and exclude archived rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-24 15:47:54 +02:00
parent 1fd05a886d
commit 71d7daf1ae
5 changed files with 384 additions and 7 deletions

View File

@@ -25,16 +25,32 @@ interface BerthResult {
status: string;
}
interface YachtResult {
id: string;
name: string;
hullNumber: string | null;
registration: string | null;
}
interface CompanyResult {
id: string;
name: string;
legalName: string | null;
taxId: string | null;
}
interface SearchResults {
clients: ClientResult[];
interests: InterestResult[];
berths: BerthResult[];
yachts: YachtResult[];
companies: CompanyResult[];
}
// ─── Search ───────────────────────────────────────────────────────────────────
export async function search(portId: string, query: string): Promise<SearchResults> {
const [clientRows, berthRows, interestRows] = await Promise.all([
const [clientRows, berthRows, interestRows, yachtRows, companyRows] = await Promise.all([
// Clients: full-text search via tsvector
db.execute<{ id: string; full_name: string; company_name: string | null }>(sql`
SELECT id, full_name, company_name
@@ -83,6 +99,58 @@ export async function search(portId: string, query: string): Promise<SearchResul
)
LIMIT 10
`),
// Yachts: ILIKE on name, hull_number, registration
db.execute<{
id: string;
name: string;
hull_number: string | null;
registration: string | null;
}>(sql`
SELECT id, name, hull_number, registration
FROM yachts
WHERE port_id = ${portId}
AND archived_at IS NULL
AND (
name ILIKE ${'%' + query + '%'}
OR hull_number ILIKE ${'%' + query + '%'}
OR registration ILIKE ${'%' + query + '%'}
)
ORDER BY
CASE
WHEN name ILIKE ${query + '%'} THEN 1
WHEN name ILIKE ${'%' + query + '%'} THEN 2
ELSE 3
END,
name
LIMIT 10
`),
// Companies: ILIKE on name, legal_name, tax_id
db.execute<{
id: string;
name: string;
legal_name: string | null;
tax_id: string | null;
}>(sql`
SELECT id, name, legal_name, tax_id
FROM companies
WHERE port_id = ${portId}
AND archived_at IS NULL
AND (
name ILIKE ${'%' + query + '%'}
OR legal_name ILIKE ${'%' + query + '%'}
OR tax_id ILIKE ${'%' + query + '%'}
)
ORDER BY
CASE
WHEN name ILIKE ${query + '%'} THEN 1
WHEN name ILIKE ${'%' + query + '%'} THEN 2
ELSE 3
END,
name
LIMIT 10
`),
]);
return {
@@ -103,6 +171,18 @@ export async function search(portId: string, query: string): Promise<SearchResul
berthMooringNumber: r.mooring_number ?? null,
pipelineStage: r.pipeline_stage,
})),
yachts: Array.from(yachtRows).map((r) => ({
id: r.id,
name: r.name,
hullNumber: r.hull_number ?? null,
registration: r.registration ?? null,
})),
companies: Array.from(companyRows).map((r) => ({
id: r.id,
name: r.name,
legalName: r.legal_name ?? null,
taxId: r.tax_id ?? null,
})),
};
}