Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'use client';
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
import {
|
|
|
|
|
type KeyboardEvent,
|
|
|
|
|
type ReactNode,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useId,
|
|
|
|
|
useMemo,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from 'react';
|
|
|
|
|
import Link from 'next/link';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { useRouter } from 'next/navigation';
|
2026-05-12 14:50:58 +02:00
|
|
|
import { useQueryClient } from '@tanstack/react-query';
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
import {
|
|
|
|
|
Anchor,
|
|
|
|
|
Bell,
|
|
|
|
|
Briefcase,
|
|
|
|
|
Building2,
|
|
|
|
|
Camera,
|
|
|
|
|
Clock,
|
|
|
|
|
FileText,
|
|
|
|
|
Folder,
|
|
|
|
|
History,
|
|
|
|
|
Home,
|
|
|
|
|
LayoutDashboard,
|
|
|
|
|
MessageSquare,
|
|
|
|
|
Plus,
|
|
|
|
|
Receipt,
|
|
|
|
|
Search,
|
|
|
|
|
Settings as SettingsIcon,
|
|
|
|
|
Ship,
|
|
|
|
|
Tag as TagIcon,
|
|
|
|
|
TrendingUp,
|
|
|
|
|
User,
|
|
|
|
|
} from 'lucide-react';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
import { apiFetch } from '@/lib/api/client';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { cn } from '@/lib/utils';
|
2026-05-09 18:35:34 +02:00
|
|
|
import { formatCurrency } from '@/lib/utils/currency';
|
2026-05-12 16:52:35 +02:00
|
|
|
import { STAGE_LABELS, formatOutcome, type PipelineStage } from '@/lib/constants';
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
import {
|
|
|
|
|
useSearch,
|
|
|
|
|
type BucketType,
|
|
|
|
|
type RecentlyViewedItem,
|
|
|
|
|
type SearchResults,
|
|
|
|
|
} from '@/hooks/use-search';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { useUIStore } from '@/stores/ui-store';
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
import { HighlightMatch } from '@/components/search/highlight-match';
|
|
|
|
|
|
|
|
|
|
// ─── Bucket configuration ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
interface BucketConfig {
|
|
|
|
|
type: BucketType;
|
|
|
|
|
label: string;
|
|
|
|
|
icon: typeof User;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BUCKETS: BucketConfig[] = [
|
|
|
|
|
{ type: 'clients', label: 'Clients', icon: User },
|
|
|
|
|
{ type: 'residentialClients', label: 'Residential', icon: Home },
|
|
|
|
|
{ type: 'yachts', label: 'Yachts', icon: Ship },
|
|
|
|
|
{ type: 'companies', label: 'Companies', icon: Building2 },
|
|
|
|
|
{ type: 'interests', label: 'Interests', icon: TrendingUp },
|
|
|
|
|
{ type: 'residentialInterests', label: 'Res. interests', icon: TrendingUp },
|
|
|
|
|
{ type: 'berths', label: 'Berths', icon: Anchor },
|
|
|
|
|
{ type: 'invoices', label: 'Invoices', icon: FileText },
|
|
|
|
|
{ type: 'expenses', label: 'Expenses', icon: Receipt },
|
|
|
|
|
{ type: 'documents', label: 'Documents', icon: Briefcase },
|
|
|
|
|
{ type: 'files', label: 'Files', icon: Folder },
|
|
|
|
|
{ type: 'reminders', label: 'Reminders', icon: Bell },
|
|
|
|
|
{ type: 'brochures', label: 'Brochures', icon: Camera },
|
|
|
|
|
{ type: 'tags', label: 'Tags', icon: TagIcon },
|
2026-05-12 16:14:12 +02:00
|
|
|
// Notes are noisy content search.
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
{ type: 'notes', label: 'Notes', icon: MessageSquare },
|
2026-05-12 16:14:12 +02:00
|
|
|
// Navigation (settings pages + admin sub-cards) lives at the very bottom —
|
|
|
|
|
// users open the search to find entity records first; pages/settings are
|
|
|
|
|
// the long-tail jump targets.
|
|
|
|
|
{ type: 'navigation', label: 'Settings', icon: SettingsIcon },
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const NAV_ICON: Record<string, typeof User> = {
|
|
|
|
|
dashboard: LayoutDashboard,
|
|
|
|
|
settings: SettingsIcon,
|
|
|
|
|
admin: SettingsIcon,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ─── Paste detection ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
|
const INVOICE_RE = /^INV-\d{6}-\d+$/i;
|
|
|
|
|
|
|
|
|
|
function looksLikePastedId(input: string): boolean {
|
|
|
|
|
const trimmed = input.trim();
|
|
|
|
|
return UUID_RE.test(trimmed) || INVOICE_RE.test(trimmed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Component ───────────────────────────────────────────────────────────────
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
export function CommandSearch() {
|
|
|
|
|
const [query, setQuery] = useState('');
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
const [focused, setFocused] = useState(false);
|
|
|
|
|
const [activeBucket, setActiveBucket] = useState<BucketType | 'all'>('all');
|
|
|
|
|
const [focusIndex, setFocusIndex] = useState<number>(-1);
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const router = useRouter();
|
2026-05-12 14:50:58 +02:00
|
|
|
const queryClient = useQueryClient();
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const portSlug = useUIStore((s) => s.currentPortSlug);
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
const listboxId = useId();
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
const { results, isFetching, recentSearches, recentlyViewed } = useSearch(query, {
|
|
|
|
|
type: activeBucket === 'all' ? undefined : activeBucket,
|
|
|
|
|
// Slightly higher cap when narrowed to one bucket — gives the user
|
|
|
|
|
// room to scan more matches without paging out to /search.
|
|
|
|
|
limit: activeBucket === 'all' ? 5 : 15,
|
|
|
|
|
});
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
// Persist the totals from the last "all" query so the filter chips stay
|
|
|
|
|
// populated when the user narrows to a single bucket. Without this, the
|
|
|
|
|
// narrowed query only returns counts for the active bucket and every
|
|
|
|
|
// other chip would vanish — making it impossible to swap between
|
|
|
|
|
// filters without clearing back to "All" first.
|
|
|
|
|
const lastAllTotalsRef = useRef<SearchResults['totals'] | null>(null);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (activeBucket === 'all' && results?.totals) {
|
|
|
|
|
lastAllTotalsRef.current = results.totals;
|
|
|
|
|
}
|
|
|
|
|
}, [activeBucket, results]);
|
|
|
|
|
const chipTotals: SearchResults['totals'] | undefined =
|
|
|
|
|
activeBucket === 'all' ? results?.totals : (lastAllTotalsRef.current ?? results?.totals);
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
const showDropdown = focused;
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
// CommandSearch lives in the header and persists across navigations,
|
|
|
|
|
// so its React Query cache never sees a remount. Invalidate the
|
|
|
|
|
// recently-viewed + recent-terms queries whenever the dropdown opens
|
|
|
|
|
// so the user sees fresh data after navigating around the app.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!showDropdown) return;
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['search', 'recently-viewed'] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['search', 'recent-terms'] });
|
|
|
|
|
}, [showDropdown, queryClient]);
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
// Cmd/Ctrl+K focuses the input from anywhere on the page.
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
useEffect(() => {
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
function onKeyDown(e: globalThis.KeyboardEvent) {
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
inputRef.current?.focus();
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
inputRef.current?.select();
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener('keydown', onKeyDown);
|
|
|
|
|
return () => document.removeEventListener('keydown', onKeyDown);
|
|
|
|
|
}, []);
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
// Click outside closes the dropdown.
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!focused) return;
|
|
|
|
|
function onClick(e: MouseEvent) {
|
|
|
|
|
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
|
|
|
|
setFocused(false);
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
setFocusIndex(-1);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener('mousedown', onClick);
|
|
|
|
|
return () => document.removeEventListener('mousedown', onClick);
|
|
|
|
|
}, [focused]);
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
const closeAndNavigate = useCallback(
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
(path: string) => {
|
|
|
|
|
setFocused(false);
|
|
|
|
|
setQuery('');
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
setFocusIndex(-1);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
inputRef.current?.blur();
|
2026-03-26 12:29:55 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
router.push(path as any);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
},
|
|
|
|
|
[router],
|
|
|
|
|
);
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
// ── Paste detection: if the user pastes a UUID/INV-… into the input,
|
|
|
|
|
// fire the resolve-id endpoint and jump straight to the entity if
|
|
|
|
|
// it exists. Falls through to normal search otherwise.
|
|
|
|
|
const onPaste = useCallback(
|
|
|
|
|
async (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
|
|
|
const pasted = e.clipboardData.getData('text').trim();
|
|
|
|
|
if (!looksLikePastedId(pasted)) return;
|
|
|
|
|
try {
|
|
|
|
|
const res = await apiFetch<{ found: boolean; href: string | null }>(
|
|
|
|
|
`/api/v1/search/resolve-id?id=${encodeURIComponent(pasted)}`,
|
|
|
|
|
);
|
|
|
|
|
if (res.found && res.href) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
closeAndNavigate(res.href);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Best-effort — fall through to normal search.
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[closeAndNavigate],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Build the flat list of focusable rows in render order, so arrow-key
|
|
|
|
|
// navigation walks them in the same order they appear visually.
|
|
|
|
|
const flatRows = useMemo<FlatRow[]>(() => {
|
|
|
|
|
if (!showDropdown) return [];
|
|
|
|
|
return buildFlatRows({
|
|
|
|
|
query,
|
|
|
|
|
results,
|
|
|
|
|
recentlyViewed,
|
|
|
|
|
recentSearches,
|
|
|
|
|
activeBucket,
|
|
|
|
|
portSlug: portSlug ?? null,
|
|
|
|
|
});
|
|
|
|
|
}, [showDropdown, query, results, recentlyViewed, recentSearches, activeBucket, portSlug]);
|
|
|
|
|
|
|
|
|
|
// Reset focus index when the visible row set changes.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setFocusIndex(-1);
|
|
|
|
|
}, [activeBucket, query]);
|
|
|
|
|
|
|
|
|
|
function onInputKeyDown(e: KeyboardEvent<HTMLInputElement>) {
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
if (e.key === 'Escape') {
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
e.preventDefault();
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
setFocused(false);
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
setFocusIndex(-1);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
inputRef.current?.blur();
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setFocusIndex((i) => Math.min(i + 1, flatRows.length - 1));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === 'ArrowUp') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setFocusIndex((i) => Math.max(i - 1, -1));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
const row = focusIndex >= 0 ? flatRows[focusIndex] : null;
|
|
|
|
|
if (row) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (row.kind === 'recent-term') {
|
|
|
|
|
setQuery(row.term);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
closeAndNavigate(row.href);
|
|
|
|
|
}
|
|
|
|
|
// Enter without a focused row is a no-op — the dropdown is already
|
|
|
|
|
// showing every relevant match (filter chips raise the cap when the
|
|
|
|
|
// user wants to see more of one bucket). No standalone /search
|
|
|
|
|
// page; refining the query is faster than scrolling further.
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
const activeOptionId =
|
|
|
|
|
focusIndex >= 0 && flatRows[focusIndex]
|
|
|
|
|
? `${listboxId}-${flatRows[focusIndex].key}`
|
|
|
|
|
: undefined;
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
return (
|
2026-05-04 22:57:01 +02:00
|
|
|
<div ref={wrapperRef} className="relative w-full">
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
2026-05-04 22:57:01 +02:00
|
|
|
'flex items-center gap-2 rounded-lg border bg-background px-3 shadow-xs transition-colors w-full',
|
|
|
|
|
focused ? 'border-brand/70' : 'border-input',
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Search className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
|
|
|
<input
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
type="text"
|
|
|
|
|
value={query}
|
|
|
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
|
|
|
onFocus={() => setFocused(true)}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
onPaste={onPaste}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
onKeyDown={onInputKeyDown}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
placeholder="Search clients, yachts, berths, invoices… (⌘K)"
|
|
|
|
|
aria-label="Search"
|
|
|
|
|
role="combobox"
|
|
|
|
|
aria-expanded={showDropdown}
|
|
|
|
|
aria-controls={listboxId}
|
|
|
|
|
aria-autocomplete="list"
|
|
|
|
|
aria-activedescendant={activeOptionId}
|
|
|
|
|
// Wrapper border swap is the focus indicator; suppress the
|
|
|
|
|
// global *:focus-visible ring that would otherwise paint a
|
|
|
|
|
// rectangular box clashing with the rounded wrapper.
|
|
|
|
|
className="h-9 flex-1 min-w-0 bg-transparent text-sm outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground"
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
/>
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
{isFetching && query.length >= 2 && (
|
|
|
|
|
<span
|
|
|
|
|
className="h-3 w-3 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent"
|
|
|
|
|
aria-hidden
|
|
|
|
|
/>
|
|
|
|
|
)}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showDropdown && (
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
<div
|
|
|
|
|
id={listboxId}
|
|
|
|
|
role="listbox"
|
|
|
|
|
aria-label="Search results"
|
|
|
|
|
className={cn(
|
|
|
|
|
'absolute top-[calc(100%+4px)] left-0 z-50 rounded-md border bg-popover shadow-lg overflow-hidden',
|
|
|
|
|
// Desktop: anchored to the input width, capped on viewport
|
|
|
|
|
'w-full max-w-[min(720px,calc(100vw-2rem))]',
|
|
|
|
|
// Mobile (<lg): full-screen sheet so cramped phone widths
|
|
|
|
|
// still render comfortable rows
|
|
|
|
|
'max-lg:fixed max-lg:inset-x-2 max-lg:top-16 max-lg:bottom-2 max-lg:max-w-none',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{/* Filter chip row — always visible while the dropdown is open. */}
|
|
|
|
|
<FilterChipRow
|
2026-05-12 14:50:58 +02:00
|
|
|
totals={chipTotals}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
active={activeBucket}
|
|
|
|
|
onChange={setActiveBucket}
|
|
|
|
|
disabled={query.length < 2}
|
|
|
|
|
/>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
<div className="max-h-[60vh] max-lg:max-h-[calc(100%-3rem)] overflow-y-auto py-1">
|
|
|
|
|
{/* No query yet — recently viewed + recent terms. */}
|
|
|
|
|
{query.length < 2 && (
|
|
|
|
|
<EmptyStateBeforeSearch
|
|
|
|
|
listboxId={listboxId}
|
|
|
|
|
recentlyViewed={recentlyViewed}
|
|
|
|
|
recentSearches={recentSearches}
|
|
|
|
|
flatRows={flatRows}
|
|
|
|
|
focusIndex={focusIndex}
|
|
|
|
|
onSelect={closeAndNavigate}
|
|
|
|
|
onSelectTerm={setQuery}
|
|
|
|
|
/>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
{/* Active query — results or zero-state. */}
|
|
|
|
|
{query.length >= 2 && (
|
|
|
|
|
<ResultsRegion
|
|
|
|
|
listboxId={listboxId}
|
|
|
|
|
query={query}
|
|
|
|
|
results={results}
|
|
|
|
|
portSlug={portSlug ?? null}
|
|
|
|
|
activeBucket={activeBucket}
|
|
|
|
|
flatRows={flatRows}
|
|
|
|
|
focusIndex={focusIndex}
|
|
|
|
|
onSelect={closeAndNavigate}
|
|
|
|
|
/>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
</div>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
{/* Footer — keyboard hint only. */}
|
|
|
|
|
{query.length >= 2 && (
|
|
|
|
|
<div className="border-t bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
|
|
|
|
↑↓ navigate · ↵ open · esc close
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
// ─── Filter chips ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function FilterChipRow({
|
2026-05-12 14:50:58 +02:00
|
|
|
totals,
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
active,
|
|
|
|
|
onChange,
|
|
|
|
|
disabled,
|
|
|
|
|
}: {
|
2026-05-12 14:50:58 +02:00
|
|
|
/** Counts from the last "all" query, persisted so chips stay visible
|
|
|
|
|
* when the user narrows to a single bucket. Falls back to the current
|
|
|
|
|
* results.totals when no "all" snapshot exists yet. */
|
|
|
|
|
totals: SearchResults['totals'] | undefined;
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
active: BucketType | 'all';
|
|
|
|
|
onChange: (b: BucketType | 'all') => void;
|
|
|
|
|
disabled: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
role="tablist"
|
|
|
|
|
aria-label="Filter results by type"
|
|
|
|
|
className="flex gap-1 overflow-x-auto border-b bg-muted/20 px-2 py-1.5"
|
|
|
|
|
>
|
|
|
|
|
<ChipButton
|
|
|
|
|
active={active === 'all'}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onClick={() => onChange('all')}
|
|
|
|
|
count={undefined}
|
|
|
|
|
>
|
|
|
|
|
All
|
|
|
|
|
</ChipButton>
|
|
|
|
|
{BUCKETS.map((b) => {
|
2026-05-12 14:50:58 +02:00
|
|
|
const count = totals?.[b.type] ?? 0;
|
|
|
|
|
// Hide chips for buckets with zero matches in the last "all"
|
|
|
|
|
// snapshot — keeps the row tight and avoids dead-end clicks.
|
|
|
|
|
// Always show the active chip + every chip before a query has run.
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
if (!disabled && count === 0 && active !== b.type) return null;
|
|
|
|
|
return (
|
|
|
|
|
<ChipButton
|
|
|
|
|
key={b.type}
|
|
|
|
|
active={active === b.type}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onClick={() => onChange(b.type)}
|
|
|
|
|
count={count > 0 ? count : undefined}
|
|
|
|
|
>
|
|
|
|
|
{b.label}
|
|
|
|
|
</ChipButton>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ChipButton({
|
|
|
|
|
active,
|
|
|
|
|
disabled,
|
|
|
|
|
count,
|
|
|
|
|
onClick,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
active: boolean;
|
|
|
|
|
disabled: boolean;
|
|
|
|
|
count?: number;
|
|
|
|
|
onClick: () => void;
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
role="tab"
|
|
|
|
|
aria-selected={active}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
className={cn(
|
|
|
|
|
'shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors',
|
|
|
|
|
active
|
|
|
|
|
? 'bg-brand text-white'
|
|
|
|
|
: 'bg-background text-muted-foreground hover:text-foreground hover:bg-accent',
|
|
|
|
|
disabled && 'opacity-50 cursor-not-allowed',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
{typeof count === 'number' && <span className="ml-1 opacity-70">({count})</span>}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Empty state (no query) ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function EmptyStateBeforeSearch({
|
|
|
|
|
listboxId,
|
|
|
|
|
recentlyViewed,
|
|
|
|
|
recentSearches,
|
|
|
|
|
flatRows,
|
|
|
|
|
focusIndex,
|
|
|
|
|
onSelect,
|
|
|
|
|
onSelectTerm,
|
|
|
|
|
}: {
|
|
|
|
|
listboxId: string;
|
|
|
|
|
recentlyViewed: RecentlyViewedItem[];
|
|
|
|
|
recentSearches: string[];
|
|
|
|
|
flatRows: FlatRow[];
|
|
|
|
|
focusIndex: number;
|
|
|
|
|
onSelect: (href: string) => void;
|
|
|
|
|
onSelectTerm: (term: string) => void;
|
|
|
|
|
}) {
|
|
|
|
|
if (recentlyViewed.length === 0 && recentSearches.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
|
|
|
Type at least 2 characters to search clients, yachts, berths, invoices, and more.
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{recentlyViewed.length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
<SectionHeading icon={History}>Recently viewed</SectionHeading>
|
|
|
|
|
{recentlyViewed.map((item) => {
|
|
|
|
|
const row = flatRows.find(
|
|
|
|
|
(r) => r.kind === 'recent-view' && r.item.id === item.id && r.item.type === item.type,
|
|
|
|
|
);
|
|
|
|
|
const isFocused = !!row && focusIndex >= 0 && flatRows[focusIndex] === row;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={`${item.type}:${item.id}`}
|
|
|
|
|
id={row ? `${listboxId}-${row.key}` : undefined}
|
|
|
|
|
role="option"
|
|
|
|
|
aria-selected={isFocused}
|
|
|
|
|
onClick={() => onSelect(item.href)}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex w-full items-center gap-2 px-3 py-2 text-sm text-left hover:bg-accent cursor-pointer',
|
|
|
|
|
isFocused && 'bg-accent',
|
2026-04-24 15:47:54 +02:00
|
|
|
)}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
>
|
|
|
|
|
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
|
|
|
<span className="truncate font-medium">{item.label}</span>
|
|
|
|
|
{item.sub && (
|
|
|
|
|
<span className="ml-auto truncate text-xs text-muted-foreground">{item.sub}</span>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{recentSearches.length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
<SectionHeading icon={Search}>Recent searches</SectionHeading>
|
|
|
|
|
{recentSearches.map((term) => {
|
|
|
|
|
const row = flatRows.find((r) => r.kind === 'recent-term' && r.term === term);
|
|
|
|
|
const isFocused = !!row && focusIndex >= 0 && flatRows[focusIndex] === row;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={term}
|
|
|
|
|
id={row ? `${listboxId}-${row.key}` : undefined}
|
|
|
|
|
role="option"
|
|
|
|
|
aria-selected={isFocused}
|
|
|
|
|
onClick={() => onSelectTerm(term)}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex w-full items-center gap-2 px-3 py-2 text-sm text-left hover:bg-accent cursor-pointer',
|
|
|
|
|
isFocused && 'bg-accent',
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
>
|
|
|
|
|
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
|
|
|
{term}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
</>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
// ─── Results region (active query) ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function ResultsRegion({
|
|
|
|
|
listboxId,
|
|
|
|
|
query,
|
|
|
|
|
results,
|
|
|
|
|
portSlug,
|
|
|
|
|
activeBucket,
|
|
|
|
|
flatRows,
|
|
|
|
|
focusIndex,
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
onSelect,
|
|
|
|
|
}: {
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
listboxId: string;
|
|
|
|
|
query: string;
|
|
|
|
|
results: SearchResults | undefined;
|
|
|
|
|
portSlug: string | null;
|
|
|
|
|
activeBucket: BucketType | 'all';
|
|
|
|
|
flatRows: FlatRow[];
|
|
|
|
|
focusIndex: number;
|
|
|
|
|
onSelect: (href: string) => void;
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}) {
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
if (!results) {
|
|
|
|
|
return <div className="px-3 py-6 text-center text-sm text-muted-foreground">Searching…</div>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totalHits = Object.values(results.totals).reduce((acc, n) => acc + n, 0);
|
|
|
|
|
if (totalHits === 0) {
|
|
|
|
|
return <ZeroState query={query} portSlug={portSlug} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render every bucket the active filter allows.
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
return (
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
<>
|
|
|
|
|
{BUCKETS.map((b) => {
|
|
|
|
|
if (activeBucket !== 'all' && activeBucket !== b.type) return null;
|
|
|
|
|
const rowsForBucket = flatRows.filter((r) => r.kind === 'result' && r.bucket === b.type);
|
|
|
|
|
if (rowsForBucket.length === 0) return null;
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
return (
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
<BucketSection key={b.type} icon={b.icon} label={b.label}>
|
|
|
|
|
{rowsForBucket.map((row) => {
|
|
|
|
|
if (row.kind !== 'result') return null;
|
|
|
|
|
const isFocused = focusIndex >= 0 && flatRows[focusIndex] === row;
|
|
|
|
|
return (
|
|
|
|
|
<ResultRow
|
|
|
|
|
key={row.key}
|
|
|
|
|
id={`${listboxId}-${row.key}`}
|
|
|
|
|
row={row}
|
|
|
|
|
query={query}
|
|
|
|
|
isFocused={isFocused}
|
|
|
|
|
onSelect={onSelect}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</BucketSection>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
);
|
|
|
|
|
})}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
|
|
|
|
|
{results.otherPorts && results.otherPorts.length > 0 && activeBucket === 'all' && (
|
|
|
|
|
<BucketSection icon={Building2} label="Other ports (super-admin)">
|
|
|
|
|
{results.otherPorts.map((row) => {
|
|
|
|
|
const flatRow = flatRows.find((r) => r.kind === 'other-port' && r.item === row);
|
|
|
|
|
const isFocused = !!flatRow && focusIndex >= 0 && flatRows[focusIndex] === flatRow;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={`${row.portId}:${row.type}:${row.id}`}
|
|
|
|
|
id={flatRow ? `${listboxId}-${flatRow.key}` : undefined}
|
|
|
|
|
role="option"
|
|
|
|
|
aria-selected={isFocused}
|
|
|
|
|
onClick={() => onSelect(`/${row.portSlug}/${pluralize(row.type)}/${row.id}`)}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex w-full items-center gap-2.5 px-3 py-2 text-sm text-left hover:bg-accent cursor-pointer text-muted-foreground',
|
|
|
|
|
isFocused && 'bg-accent text-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span className="text-[10px] uppercase tracking-wide text-muted-foreground/70">
|
|
|
|
|
{row.portName}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="truncate text-foreground">
|
|
|
|
|
<HighlightMatch text={row.label} query={query} />
|
|
|
|
|
</span>
|
|
|
|
|
{row.sub && <span className="ml-auto truncate text-xs">{row.sub}</span>}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</BucketSection>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ZeroState({ query, portSlug }: { query: string; portSlug: string | null }) {
|
|
|
|
|
if (!portSlug) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
|
|
|
No results for “{query}”
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div className="px-3 py-5">
|
|
|
|
|
<p className="text-sm text-muted-foreground mb-3">
|
|
|
|
|
No results for <span className="font-medium text-foreground">“{query}”</span>
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs uppercase tracking-wide text-muted-foreground/70 mb-2">Quick create</p>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<QuickCreateButton
|
|
|
|
|
icon={User}
|
|
|
|
|
label={`New client "${query}"`}
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
href={`/${portSlug}/clients/new?fullName=${encodeURIComponent(query)}` as any}
|
|
|
|
|
/>
|
|
|
|
|
<QuickCreateButton
|
|
|
|
|
icon={Ship}
|
|
|
|
|
label={`New yacht "${query}"`}
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
href={`/${portSlug}/yachts/new?name=${encodeURIComponent(query)}` as any}
|
|
|
|
|
/>
|
|
|
|
|
<QuickCreateButton
|
|
|
|
|
icon={Building2}
|
|
|
|
|
label={`New company "${query}"`}
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
href={`/${portSlug}/companies/new?name=${encodeURIComponent(query)}` as any}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
function QuickCreateButton({
|
|
|
|
|
icon: Icon,
|
|
|
|
|
label,
|
|
|
|
|
href,
|
|
|
|
|
}: {
|
|
|
|
|
icon: typeof User;
|
|
|
|
|
label: string;
|
|
|
|
|
href: string;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
href={href as any}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md border bg-background px-2.5 py-1 text-xs font-medium text-foreground hover:bg-accent transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3 w-3" />
|
|
|
|
|
<Icon className="h-3 w-3" />
|
|
|
|
|
<span>{label}</span>
|
|
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Result row ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function ResultRow({
|
|
|
|
|
id,
|
|
|
|
|
row,
|
|
|
|
|
query,
|
|
|
|
|
isFocused,
|
|
|
|
|
onSelect,
|
|
|
|
|
}: {
|
|
|
|
|
id: string;
|
|
|
|
|
row: Extract<FlatRow, { kind: 'result' }>;
|
|
|
|
|
query: string;
|
|
|
|
|
isFocused: boolean;
|
|
|
|
|
onSelect: (href: string) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const Icon = row.icon;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
id={id}
|
|
|
|
|
role="option"
|
|
|
|
|
aria-selected={isFocused}
|
|
|
|
|
onClick={() => onSelect(row.href)}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex w-full items-start gap-2.5 px-3 py-2 text-sm text-left hover:bg-accent cursor-pointer',
|
|
|
|
|
isFocused && 'bg-accent',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Icon className="h-4 w-4 shrink-0 text-muted-foreground mt-0.5" />
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
|
|
|
<span className="truncate font-medium text-foreground">
|
|
|
|
|
<HighlightMatch text={row.label} query={query} />
|
|
|
|
|
</span>
|
|
|
|
|
{row.badges?.map((badge) => (
|
|
|
|
|
<Badge key={badge.label} tone={badge.tone} label={badge.label} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{row.sub && (
|
|
|
|
|
<div className="text-xs text-muted-foreground truncate mt-0.5">
|
|
|
|
|
<HighlightMatch text={row.sub} query={query} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-12 14:50:58 +02:00
|
|
|
{row.relatedVia && (
|
|
|
|
|
<div className="text-[11px] italic text-muted-foreground/80 truncate mt-0.5">
|
|
|
|
|
via {row.relatedVia.label}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Badge({
|
|
|
|
|
label,
|
|
|
|
|
tone,
|
|
|
|
|
}: {
|
|
|
|
|
label: string;
|
|
|
|
|
tone: 'neutral' | 'warning' | 'success' | 'danger';
|
|
|
|
|
}) {
|
|
|
|
|
const cls = {
|
|
|
|
|
neutral: 'bg-muted text-muted-foreground',
|
|
|
|
|
warning: 'bg-amber-100 text-amber-800',
|
|
|
|
|
success: 'bg-emerald-100 text-emerald-800',
|
|
|
|
|
danger: 'bg-rose-100 text-rose-800',
|
|
|
|
|
}[tone];
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
'inline-flex items-center rounded px-1.5 py-0 text-[10px] font-semibold uppercase tracking-wide',
|
|
|
|
|
cls,
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SectionHeading({ icon: Icon, children }: { icon: typeof User; children: ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
<Icon className="h-3 w-3" />
|
|
|
|
|
<span>{children}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function BucketSection({
|
|
|
|
|
icon,
|
|
|
|
|
label,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
icon: typeof User;
|
|
|
|
|
label: string;
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<SectionHeading icon={icon}>{label}</SectionHeading>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Flat-row construction (drives keyboard nav + ARIA) ──────────────────────
|
|
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
export type ResultBadge = { label: string; tone: 'neutral' | 'warning' | 'success' | 'danger' };
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
export type FlatRow =
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
| {
|
|
|
|
|
kind: 'recent-view';
|
|
|
|
|
key: string;
|
|
|
|
|
item: RecentlyViewedItem;
|
|
|
|
|
href: string;
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
kind: 'recent-term';
|
|
|
|
|
key: string;
|
|
|
|
|
term: string;
|
|
|
|
|
href: string;
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
kind: 'result';
|
|
|
|
|
key: string;
|
|
|
|
|
bucket: BucketType;
|
|
|
|
|
icon: typeof User;
|
|
|
|
|
label: string;
|
|
|
|
|
sub: string | null;
|
|
|
|
|
href: string;
|
|
|
|
|
badges?: ResultBadge[];
|
2026-05-12 14:50:58 +02:00
|
|
|
/** Provenance hint when the row was surfaced via graph expansion.
|
|
|
|
|
* Rendered as a subtle "via Berth A10" line below the sub. */
|
|
|
|
|
relatedVia?: { type: string; label: string } | null;
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
kind: 'other-port';
|
|
|
|
|
key: string;
|
|
|
|
|
item: SearchResults['otherPorts'] extends (infer U)[] | undefined ? U : never;
|
|
|
|
|
href: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
export interface BuildFlatRowsArgs {
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
query: string;
|
|
|
|
|
results: SearchResults | undefined;
|
|
|
|
|
recentlyViewed: RecentlyViewedItem[];
|
|
|
|
|
recentSearches: string[];
|
|
|
|
|
activeBucket: BucketType | 'all';
|
|
|
|
|
portSlug: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
export function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
const { query, results, recentlyViewed, recentSearches, activeBucket, portSlug } = args;
|
|
|
|
|
const rows: FlatRow[] = [];
|
|
|
|
|
|
|
|
|
|
if (query.length < 2) {
|
|
|
|
|
for (const item of recentlyViewed) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'recent-view',
|
|
|
|
|
key: `recent-view:${item.type}:${item.id}`,
|
|
|
|
|
item,
|
|
|
|
|
href: item.href,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
for (const term of recentSearches) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'recent-term',
|
|
|
|
|
key: `recent-term:${term}`,
|
|
|
|
|
term,
|
|
|
|
|
href: '',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!results || !portSlug) return rows;
|
|
|
|
|
|
|
|
|
|
const include = (b: BucketType) => activeBucket === 'all' || activeBucket === b;
|
|
|
|
|
|
|
|
|
|
if (include('clients')) {
|
|
|
|
|
for (const c of results.clients) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `clients:${c.id}`,
|
|
|
|
|
bucket: 'clients',
|
|
|
|
|
icon: User,
|
|
|
|
|
label: c.fullName,
|
|
|
|
|
sub: c.matchedContact ?? null,
|
|
|
|
|
href: `/${portSlug}/clients/${c.id}`,
|
|
|
|
|
badges: c.archivedAt ? [{ label: 'Archived', tone: 'neutral' }] : undefined,
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia: c.relatedVia ?? null,
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('residentialClients')) {
|
|
|
|
|
for (const c of results.residentialClients) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `residentialClients:${c.id}`,
|
|
|
|
|
bucket: 'residentialClients',
|
|
|
|
|
icon: Home,
|
|
|
|
|
label: c.fullName,
|
|
|
|
|
sub: c.email ?? c.phone ?? null,
|
|
|
|
|
href: `/${portSlug}/residential/clients/${c.id}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('yachts')) {
|
|
|
|
|
for (const y of results.yachts) {
|
|
|
|
|
const sub = [y.hullNumber, y.registration].filter(Boolean).join(' · ') || null;
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `yachts:${y.id}`,
|
|
|
|
|
bucket: 'yachts',
|
|
|
|
|
icon: Ship,
|
|
|
|
|
label: y.name,
|
|
|
|
|
sub,
|
|
|
|
|
href: `/${portSlug}/yachts/${y.id}`,
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia: y.relatedVia ?? null,
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('companies')) {
|
|
|
|
|
for (const co of results.companies) {
|
|
|
|
|
const sub = [co.legalName, co.taxId].filter(Boolean).join(' · ') || null;
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `companies:${co.id}`,
|
|
|
|
|
bucket: 'companies',
|
|
|
|
|
icon: Building2,
|
|
|
|
|
label: co.name,
|
|
|
|
|
sub,
|
|
|
|
|
href: `/${portSlug}/companies/${co.id}`,
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia: co.relatedVia ?? null,
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('interests')) {
|
|
|
|
|
for (const i of results.interests) {
|
|
|
|
|
const badges: ResultBadge[] = [];
|
|
|
|
|
if (i.outcome) {
|
|
|
|
|
badges.push({
|
2026-05-12 16:52:35 +02:00
|
|
|
label: formatOutcome(i.outcome) ?? i.outcome,
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
tone: i.outcome === 'won' ? 'success' : 'neutral',
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2026-05-12 16:52:35 +02:00
|
|
|
badges.push({
|
|
|
|
|
label:
|
|
|
|
|
STAGE_LABELS[i.pipelineStage as PipelineStage] ?? i.pipelineStage.replace(/_/g, ' '),
|
|
|
|
|
tone: 'warning',
|
|
|
|
|
});
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
}
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `interests:${i.id}`,
|
|
|
|
|
bucket: 'interests',
|
|
|
|
|
icon: TrendingUp,
|
|
|
|
|
label: i.clientName,
|
|
|
|
|
sub: i.berthMooringNumber,
|
|
|
|
|
href: `/${portSlug}/interests/${i.id}`,
|
|
|
|
|
badges: badges.length > 0 ? badges : undefined,
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia: i.relatedVia ?? null,
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('residentialInterests')) {
|
|
|
|
|
for (const i of results.residentialInterests) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `residentialInterests:${i.id}`,
|
|
|
|
|
bucket: 'residentialInterests',
|
|
|
|
|
icon: TrendingUp,
|
|
|
|
|
label: i.clientName,
|
2026-05-12 17:02:10 +02:00
|
|
|
sub: STAGE_LABELS[i.pipelineStage as PipelineStage] ?? i.pipelineStage.replace(/_/g, ' '),
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
href: `/${portSlug}/residential/interests/${i.id}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('berths')) {
|
|
|
|
|
for (const b of results.berths) {
|
|
|
|
|
const badges: ResultBadge[] = [];
|
|
|
|
|
if (b.status === 'sold') badges.push({ label: 'Sold', tone: 'success' });
|
|
|
|
|
else if (b.status === 'under_offer') badges.push({ label: 'Under offer', tone: 'warning' });
|
|
|
|
|
const sub =
|
|
|
|
|
[
|
|
|
|
|
b.area,
|
|
|
|
|
b.linkedInterestCount > 0
|
|
|
|
|
? `${b.linkedInterestCount} interest${b.linkedInterestCount === 1 ? '' : 's'}`
|
|
|
|
|
: null,
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(' · ') || null;
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `berths:${b.id}`,
|
|
|
|
|
bucket: 'berths',
|
|
|
|
|
icon: Anchor,
|
|
|
|
|
label: b.mooringNumber,
|
|
|
|
|
sub,
|
|
|
|
|
href: `/${portSlug}/berths/${b.id}`,
|
|
|
|
|
badges: badges.length > 0 ? badges : undefined,
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia: b.relatedVia ?? null,
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('invoices')) {
|
|
|
|
|
for (const inv of results.invoices) {
|
|
|
|
|
const badges: ResultBadge[] = [];
|
|
|
|
|
if (inv.status === 'overdue') badges.push({ label: 'Overdue', tone: 'danger' });
|
|
|
|
|
else if (inv.paymentStatus === 'paid') badges.push({ label: 'Paid', tone: 'success' });
|
|
|
|
|
else if (inv.status === 'sent') badges.push({ label: 'Sent', tone: 'neutral' });
|
|
|
|
|
const sub = inv.totalAmount
|
2026-05-09 18:35:34 +02:00
|
|
|
? `${inv.clientName} · ${formatCurrency(inv.totalAmount, inv.currency)}`
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
: inv.clientName;
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `invoices:${inv.id}`,
|
|
|
|
|
bucket: 'invoices',
|
|
|
|
|
icon: FileText,
|
|
|
|
|
label: inv.invoiceNumber,
|
|
|
|
|
sub,
|
|
|
|
|
href: `/${portSlug}/invoices/${inv.id}`,
|
|
|
|
|
badges: badges.length > 0 ? badges : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('expenses')) {
|
|
|
|
|
for (const e of results.expenses) {
|
|
|
|
|
const badges: ResultBadge[] = [];
|
|
|
|
|
if (e.paymentStatus === 'paid') badges.push({ label: 'Paid', tone: 'success' });
|
|
|
|
|
const sub = [e.vendor, e.tripLabel].filter(Boolean).join(' · ') || null;
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `expenses:${e.id}`,
|
|
|
|
|
bucket: 'expenses',
|
|
|
|
|
icon: Receipt,
|
2026-05-09 18:35:34 +02:00
|
|
|
label: e.description ?? e.vendor ?? formatCurrency(e.amount, e.currency),
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
sub,
|
|
|
|
|
href: `/${portSlug}/expenses/${e.id}`,
|
|
|
|
|
badges: badges.length > 0 ? badges : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('documents')) {
|
|
|
|
|
for (const d of results.documents) {
|
|
|
|
|
const badges: ResultBadge[] = [];
|
|
|
|
|
if (d.status === 'completed') badges.push({ label: 'Signed', tone: 'success' });
|
|
|
|
|
else if (d.status === 'expired') badges.push({ label: 'Expired', tone: 'danger' });
|
|
|
|
|
else if (d.status === 'sent') badges.push({ label: 'Awaiting signature', tone: 'warning' });
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `documents:${d.id}`,
|
|
|
|
|
bucket: 'documents',
|
|
|
|
|
icon: Briefcase,
|
|
|
|
|
label: d.title,
|
|
|
|
|
sub: d.matchedSignerName,
|
|
|
|
|
href: `/${portSlug}/documents/${d.id}`,
|
|
|
|
|
badges: badges.length > 0 ? badges : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('files')) {
|
|
|
|
|
for (const f of results.files) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `files:${f.id}`,
|
|
|
|
|
bucket: 'files',
|
|
|
|
|
icon: Folder,
|
|
|
|
|
label: f.filename,
|
|
|
|
|
sub: f.ownerLabel,
|
2026-05-11 12:47:11 +02:00
|
|
|
href: `/${portSlug}/documents`,
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('reminders')) {
|
|
|
|
|
for (const r of results.reminders) {
|
|
|
|
|
const badges: ResultBadge[] = [];
|
|
|
|
|
const due = new Date(r.dueAt);
|
|
|
|
|
if (due.getTime() < Date.now()) badges.push({ label: 'Overdue', tone: 'danger' });
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `reminders:${r.id}`,
|
|
|
|
|
bucket: 'reminders',
|
|
|
|
|
icon: Bell,
|
|
|
|
|
label: r.title,
|
|
|
|
|
sub: due.toLocaleDateString(),
|
|
|
|
|
href: `/${portSlug}/reminders`,
|
|
|
|
|
badges: badges.length > 0 ? badges : undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('brochures')) {
|
|
|
|
|
for (const b of results.brochures) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `brochures:${b.id}`,
|
|
|
|
|
bucket: 'brochures',
|
|
|
|
|
icon: Camera,
|
|
|
|
|
label: b.label,
|
|
|
|
|
sub: b.isDefault ? 'Default brochure' : null,
|
|
|
|
|
href: `/${portSlug}/settings`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (include('tags')) {
|
|
|
|
|
for (const t of results.tags) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `tags:${t.id}`,
|
|
|
|
|
bucket: 'tags',
|
|
|
|
|
icon: TagIcon,
|
|
|
|
|
label: `Tag: ${t.name}`,
|
|
|
|
|
sub: `${t.totalCount} tagged`,
|
|
|
|
|
// Tag-filtered list view; until that list page exists, fall back
|
|
|
|
|
// to the tags settings page.
|
|
|
|
|
href: `/${portSlug}/clients?tag=${encodeURIComponent(t.name)}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-12 16:14:12 +02:00
|
|
|
// Notes — content matches inside free-text notes are noisy by nature, so
|
|
|
|
|
// the user sees them after the entity-specific buckets above have
|
|
|
|
|
// surfaced their tighter matches.
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
if (include('notes')) {
|
|
|
|
|
for (const n of results.notes) {
|
|
|
|
|
const sourceCollection =
|
|
|
|
|
n.source === 'client'
|
|
|
|
|
? 'clients'
|
|
|
|
|
: n.source === 'interest'
|
|
|
|
|
? 'interests'
|
|
|
|
|
: n.source === 'yacht'
|
|
|
|
|
? 'yachts'
|
|
|
|
|
: 'companies';
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `notes:${n.id}`,
|
|
|
|
|
bucket: 'notes',
|
|
|
|
|
icon: MessageSquare,
|
|
|
|
|
label: `${n.source.charAt(0).toUpperCase() + n.source.slice(1)} note · ${n.sourceLabel}`,
|
|
|
|
|
sub: n.snippet,
|
|
|
|
|
href: `/${portSlug}/${sourceCollection}/${n.sourceId}?tab=notes`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-12 16:14:12 +02:00
|
|
|
// Navigation (settings pages + admin section cards) goes LAST — these
|
|
|
|
|
// are jump targets, not the primary thing a user opens the search to
|
|
|
|
|
// find. Surfacing them after entity matches keeps the top of the
|
|
|
|
|
// dropdown focused on records.
|
|
|
|
|
if (include('navigation')) {
|
|
|
|
|
for (const n of results.navigation) {
|
|
|
|
|
const Icon = NAV_ICON[n.category] ?? SettingsIcon;
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'result',
|
|
|
|
|
key: `navigation:${n.id}`,
|
|
|
|
|
bucket: 'navigation',
|
|
|
|
|
icon: Icon,
|
|
|
|
|
label: n.label,
|
|
|
|
|
sub: n.category,
|
|
|
|
|
// Catalog hrefs already have :portSlug substituted server-side.
|
|
|
|
|
href: n.href,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients,
yachts, companies, interests, residential interests, berths, invoices,
expenses, documents, files, reminders, brochures, tags, notes, navigation)
with prefix tsquery + trigram fallback, phone-digit normalization,
and JOINs to client_contacts for email matching.
New `notes` bucket searches across the four note tables (client,
interest, yacht, company) via UNION + parent-entity label resolution
(berth mooring for interests, name for yachts/companies). Renders at
the bottom of the dropdown so broad-content matches don't crowd
entity-specific hits — per the user's "low-noise" preference.
Recently-viewed tracking persists last 20 entity views per user in
Redis sorted set; CommandSearch surfaces them as the dropdown's
default state and applies affinity ranking when the user types.
ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like
`INV-2025-001`) and routes the rep straight to the entity, skipping
the normal search bucket.
Audit search service gains `entityIds[]` array filter for the new
loadClientActivityAggregated() path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:58:34 +02:00
|
|
|
|
|
|
|
|
if (results.otherPorts && activeBucket === 'all') {
|
|
|
|
|
for (const op of results.otherPorts) {
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: 'other-port',
|
|
|
|
|
key: `other:${op.portId}:${op.type}:${op.id}`,
|
|
|
|
|
item: op,
|
|
|
|
|
href: `/${op.portSlug}/${pluralize(op.type)}/${op.id}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pluralize(type: string): string {
|
|
|
|
|
if (type === 'company') return 'companies';
|
|
|
|
|
return `${type}s`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep a no-op export for any legacy import sites.
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
export function SearchTrigger() {
|
|
|
|
|
return null;
|
|
|
|
|
}
|