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

@@ -2,7 +2,7 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Search, Clock, User, TrendingUp, Anchor } from 'lucide-react';
import { Search, Clock, User, TrendingUp, Anchor, Ship, Building2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useSearch } from '@/hooks/use-search';
@@ -22,7 +22,11 @@ export function CommandSearch() {
const hasQuery = query.length >= 2;
const hasResults =
results &&
(results.clients.length > 0 || results.interests.length > 0 || results.berths.length > 0);
(results.clients.length > 0 ||
results.interests.length > 0 ||
results.berths.length > 0 ||
results.yachts.length > 0 ||
results.companies.length > 0);
// Cmd/Ctrl+K focuses the input
useEffect(() => {
@@ -67,7 +71,13 @@ export function CommandSearch() {
}
}
const iconMap = { client: User, interest: TrendingUp, berth: Anchor } as const;
const iconMap = {
client: User,
interest: TrendingUp,
berth: Anchor,
yacht: Ship,
company: Building2,
} as const;
return (
<div ref={wrapperRef} className="relative">
@@ -148,6 +158,32 @@ export function CommandSearch() {
onSelect={(id) => navigate(`/${portSlug}/clients/${id}`)}
/>
)}
{results.yachts.length > 0 && (
<ResultGroup
heading="Yachts"
items={results.yachts.map((y) => ({
id: y.id,
icon: 'yacht',
label: y.name,
sub: [y.hullNumber, y.registration].filter(Boolean).join(' · ') || null,
}))}
iconMap={iconMap}
onSelect={(id) => navigate(`/${portSlug}/yachts/${id}`)}
/>
)}
{results.companies.length > 0 && (
<ResultGroup
heading="Companies"
items={results.companies.map((c) => ({
id: c.id,
icon: 'company',
label: c.name,
sub: [c.legalName, c.taxId].filter(Boolean).join(' · ') || null,
}))}
iconMap={iconMap}
onSelect={(id) => navigate(`/${portSlug}/companies/${id}`)}
/>
)}
{results.interests.length > 0 && (
<ResultGroup
heading="Interests"
@@ -190,7 +226,12 @@ function ResultGroup({
onSelect,
}: {
heading: string;
items: Array<{ id: string; icon: 'client' | 'interest' | 'berth'; label: string; sub?: string | null }>;
items: Array<{
id: string;
icon: 'client' | 'interest' | 'berth' | 'yacht' | 'company';
label: string;
sub?: string | null;
}>;
iconMap: Record<string, React.ElementType | undefined>;
onSelect: (id: string) => void;
}) {