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 { keepPreviousData, useQuery } from '@tanstack/react-query';
|
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 { apiFetch } from '@/lib/api/client';
|
|
|
|
|
import { useDebounce } from '@/hooks/use-debounce';
|
|
|
|
|
|
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
|
|
|
// ─── Types — mirror SearchResults from search.service.ts ─────────────────────
|
|
|
|
|
|
|
|
|
|
export type BucketType =
|
|
|
|
|
| 'clients'
|
|
|
|
|
| 'residentialClients'
|
|
|
|
|
| 'yachts'
|
|
|
|
|
| 'companies'
|
|
|
|
|
| 'interests'
|
|
|
|
|
| 'residentialInterests'
|
|
|
|
|
| 'berths'
|
|
|
|
|
| 'invoices'
|
|
|
|
|
| 'expenses'
|
|
|
|
|
| 'documents'
|
|
|
|
|
| 'files'
|
|
|
|
|
| 'reminders'
|
|
|
|
|
| 'brochures'
|
|
|
|
|
| 'tags'
|
|
|
|
|
| 'navigation'
|
2026-05-21 19:45:50 +02:00
|
|
|
| 'notes'
|
|
|
|
|
| 'stages';
|
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
|
|
|
/**
|
|
|
|
|
* Provenance hint for a result row that surfaced via graph expansion
|
|
|
|
|
* rather than a direct match against the query. Rendered as a "via X"
|
|
|
|
|
* subtitle by the result-row UI.
|
|
|
|
|
*/
|
|
|
|
|
export interface RelatedVia {
|
|
|
|
|
type: 'berth' | 'interest' | 'client' | 'yacht' | 'company';
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
export interface ClientResult {
|
|
|
|
|
id: string;
|
|
|
|
|
fullName: string;
|
|
|
|
|
matchedContact: string | null;
|
|
|
|
|
matchedContactChannel: 'email' | 'phone' | 'whatsapp' | null;
|
|
|
|
|
archivedAt: string | null;
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia?: RelatedVia | null;
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
matchedOn?: 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
|
|
|
}
|
|
|
|
|
export interface ResidentialClientResult {
|
|
|
|
|
id: string;
|
|
|
|
|
fullName: string;
|
|
|
|
|
email: string | null;
|
|
|
|
|
phone: string | null;
|
|
|
|
|
status: string;
|
|
|
|
|
archivedAt: string | null;
|
|
|
|
|
}
|
|
|
|
|
export interface YachtResult {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
hullNumber: string | null;
|
|
|
|
|
registration: string | null;
|
|
|
|
|
archivedAt: string | null;
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia?: 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
|
|
|
}
|
|
|
|
|
export interface CompanyResult {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
legalName: string | null;
|
|
|
|
|
taxId: string | null;
|
|
|
|
|
matchedField: 'name' | 'legalName' | 'taxId' | 'billingEmail' | 'registrationNumber' | null;
|
|
|
|
|
archivedAt: string | null;
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia?: 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
|
|
|
}
|
|
|
|
|
export interface InterestResult {
|
|
|
|
|
id: string;
|
|
|
|
|
clientName: string;
|
|
|
|
|
berthMooringNumber: string | null;
|
|
|
|
|
pipelineStage: string;
|
|
|
|
|
outcome: string | null;
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia?: 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
|
|
|
}
|
|
|
|
|
export interface ResidentialInterestResult {
|
|
|
|
|
id: string;
|
|
|
|
|
clientName: string;
|
|
|
|
|
pipelineStage: string;
|
|
|
|
|
}
|
|
|
|
|
export interface BerthResult {
|
|
|
|
|
id: string;
|
|
|
|
|
mooringNumber: string;
|
|
|
|
|
area: string | null;
|
|
|
|
|
status: string;
|
|
|
|
|
linkedInterestCount: number;
|
2026-05-12 14:50:58 +02:00
|
|
|
relatedVia?: 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
|
|
|
}
|
|
|
|
|
export interface InvoiceResult {
|
|
|
|
|
id: string;
|
|
|
|
|
invoiceNumber: string;
|
|
|
|
|
clientName: string;
|
|
|
|
|
status: string;
|
|
|
|
|
paymentStatus: string | null;
|
|
|
|
|
totalAmount: string | null;
|
|
|
|
|
currency: string;
|
|
|
|
|
}
|
|
|
|
|
export interface ExpenseResult {
|
|
|
|
|
id: string;
|
|
|
|
|
description: string | null;
|
|
|
|
|
vendor: string | null;
|
|
|
|
|
tripLabel: string | null;
|
|
|
|
|
amount: string;
|
|
|
|
|
currency: string;
|
|
|
|
|
paymentStatus: string | null;
|
|
|
|
|
}
|
|
|
|
|
export interface DocumentResult {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string;
|
|
|
|
|
documentType: string;
|
|
|
|
|
status: string;
|
|
|
|
|
matchedSignerName: string | null;
|
|
|
|
|
}
|
|
|
|
|
export interface FileResult {
|
|
|
|
|
id: string;
|
|
|
|
|
filename: string;
|
|
|
|
|
category: string | null;
|
|
|
|
|
ownerLabel: string | null;
|
|
|
|
|
}
|
|
|
|
|
export interface ReminderResult {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string;
|
|
|
|
|
dueAt: string;
|
|
|
|
|
priority: string;
|
|
|
|
|
status: string;
|
|
|
|
|
}
|
|
|
|
|
export interface BrochureResult {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
isDefault: boolean;
|
|
|
|
|
archivedAt: string | null;
|
|
|
|
|
}
|
|
|
|
|
export interface TagResult {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
color: string;
|
|
|
|
|
totalCount: number;
|
|
|
|
|
}
|
|
|
|
|
export interface NavResult {
|
|
|
|
|
id: string;
|
|
|
|
|
href: string;
|
|
|
|
|
label: string;
|
|
|
|
|
category: 'settings' | 'admin' | 'dashboard';
|
|
|
|
|
}
|
2026-05-21 19:45:50 +02:00
|
|
|
export interface StageSuggestionResult {
|
|
|
|
|
/** Canonical pipeline-stage value (matches PIPELINE_STAGES). */
|
|
|
|
|
stage: string;
|
|
|
|
|
/** Human label (STAGE_LABELS[stage]). */
|
|
|
|
|
label: string;
|
|
|
|
|
/** Live count of non-archived interests in this stage. */
|
|
|
|
|
count: number;
|
|
|
|
|
/** Slug-less href. CommandSearch prefixes the portSlug at render time. */
|
|
|
|
|
href: string;
|
|
|
|
|
}
|
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
|
|
|
export interface OtherPortResult {
|
|
|
|
|
portId: string;
|
|
|
|
|
portSlug: string;
|
|
|
|
|
portName: string;
|
|
|
|
|
type: 'client' | 'yacht' | 'company' | 'berth' | 'interest';
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
sub: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SearchResults {
|
|
|
|
|
clients: ClientResult[];
|
|
|
|
|
residentialClients: ResidentialClientResult[];
|
|
|
|
|
yachts: YachtResult[];
|
|
|
|
|
companies: CompanyResult[];
|
|
|
|
|
interests: InterestResult[];
|
|
|
|
|
residentialInterests: ResidentialInterestResult[];
|
|
|
|
|
berths: BerthResult[];
|
|
|
|
|
invoices: InvoiceResult[];
|
|
|
|
|
expenses: ExpenseResult[];
|
|
|
|
|
documents: DocumentResult[];
|
|
|
|
|
files: FileResult[];
|
|
|
|
|
reminders: ReminderResult[];
|
|
|
|
|
brochures: BrochureResult[];
|
|
|
|
|
tags: TagResult[];
|
|
|
|
|
navigation: NavResult[];
|
|
|
|
|
notes: NoteResult[];
|
2026-05-21 19:45:50 +02:00
|
|
|
stages: StageSuggestionResult[];
|
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
|
|
|
totals: Record<BucketType, number>;
|
|
|
|
|
otherPorts?: OtherPortResult[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface NoteResult {
|
|
|
|
|
id: string;
|
|
|
|
|
snippet: string;
|
|
|
|
|
source: 'client' | 'interest' | 'yacht' | 'company';
|
|
|
|
|
sourceId: string;
|
|
|
|
|
sourceLabel: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RecentlyViewedItem {
|
|
|
|
|
type: string;
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
sub: string | null;
|
|
|
|
|
href: string;
|
|
|
|
|
viewedAt: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Hooks ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export interface UseSearchOptions {
|
|
|
|
|
/** When set, narrows the result set to a single bucket. */
|
|
|
|
|
type?: BucketType;
|
|
|
|
|
/** Per-bucket cap. Default 5 (dropdown); use 25 for the /search page. */
|
|
|
|
|
limit?: number;
|
|
|
|
|
/** Super-admin opt-in for cross-port matches. Silently ignored otherwise. */
|
|
|
|
|
includeOtherPorts?: boolean;
|
|
|
|
|
/** Override the 300ms input debounce. */
|
|
|
|
|
debounceMs?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useSearch(query: string, opts: UseSearchOptions = {}) {
|
|
|
|
|
const debouncedQuery = useDebounce(query, opts.debounceMs ?? 300);
|
|
|
|
|
const enabled = debouncedQuery.length >= 2;
|
|
|
|
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
params.set('q', debouncedQuery);
|
|
|
|
|
if (opts.type) params.set('type', opts.type);
|
|
|
|
|
if (opts.limit) params.set('limit', String(opts.limit));
|
|
|
|
|
if (opts.includeOtherPorts) params.set('includeOtherPorts', 'true');
|
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 searchQuery = useQuery<SearchResults>({
|
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
|
|
|
queryKey: [
|
|
|
|
|
'search',
|
|
|
|
|
debouncedQuery,
|
|
|
|
|
opts.type ?? 'all',
|
|
|
|
|
opts.limit ?? 5,
|
|
|
|
|
opts.includeOtherPorts ?? false,
|
|
|
|
|
],
|
|
|
|
|
queryFn: ({ signal }) =>
|
|
|
|
|
apiFetch<SearchResults>(`/api/v1/search?${params.toString()}`, { signal }),
|
|
|
|
|
enabled,
|
|
|
|
|
// Keep previous results visible while the next debounced query loads
|
|
|
|
|
// — eliminates the dropdown flicker when the user is typing fast.
|
|
|
|
|
placeholderData: keepPreviousData,
|
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
|
|
|
staleTime: 30_000,
|
|
|
|
|
});
|
|
|
|
|
|
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 recentSearchQuery = useQuery<{ searches: string[] }>({
|
|
|
|
|
queryKey: ['search', 'recent-terms'],
|
|
|
|
|
queryFn: ({ signal }) => apiFetch<{ searches: string[] }>('/api/v1/search/recent', { signal }),
|
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
|
|
|
staleTime: 60_000,
|
|
|
|
|
});
|
|
|
|
|
|
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs
Wave through the remaining audit-final-deferred items that aren't blocked
on the back-burnered Documenso work.
Multi-tenant isolation:
- Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim;
verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a
buggy issuer in some future code path that mixes port scopes — every
storage key generated by generateStorageKey() already prefixes the
slug. document-sends opts in for 24h emailed download links; other
callers continue working unchanged via the optional field.
DB schema reconciliation:
- Migration 0047 rebuilds system_settings unique index with NULLS NOT
DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are
uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate
(storage_backend, NULL) rows that had accumulated from race-prone
delete-then-insert patterns in ocr-config / settings / residential-
stages / ai-budget services. All four services converted to true
onConflictDoUpdate upserts so the race window is closed.
API uniformity:
- Response shape standardization: 16 routes converted from
`{ success: true }` to 204 No Content. CLAUDE.md documents the
convention (`{ data: <T> }` for content, 204 for empty mutations,
portal-auth retains `{ success: true }` for the frontend's auth chain).
- req.json() → parseBody() migration across 9 admin/CRM routes
(custom-fields, expenses/export ×3, currency convert,
search/recently-viewed, admin/duplicates, berths/pdf-{upload-url,
versions, parse-results}). Uniform 400 error shapes for
ZodError-flagged bodies.
Custom-fields merge tokens (shipped end-to-end):
- merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the
`{{custom.<fieldName>}}` shape.
- document-templates validator accepts the dynamic shape alongside
the static catalog tokens.
- document-sends.service mergeCustomFieldValues resolver fetches
per-port custom_field_definitions for client/interest/berth contexts
and substitutes stored values keyed by `{{custom.fieldName}}`.
- custom-fields-manager amber banner updated to reflect that merge
tokens now expand (search index + entity-diff remain documented
design limitations).
/api/v1/files cross-entity filtering:
- Validator + listFiles + uploadFile accept companyId AND yachtId
alongside clientId. file-upload-zone propagates both.
- New CompanyFilesTab component mirrors ClientFilesTab; restored as a
visible Documents tab in company-tabs.tsx (was a hidden stub).
Inline TODOs:
- Reviewed remaining two TODOs (per-user reminder schedule, import
worker handlers). Both are placeholders for future feature surfaces,
not bugs — per-port digest works for every customer; nothing
currently enqueues import jobs (verified). Annotated in BACKLOG.
BACKLOG.md updated to reflect what landed and what's still pending
(Documenso-related items still bundled with the back-burnered phases).
Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
|
|
|
const recentlyViewedQuery = useQuery<{ data: RecentlyViewedItem[] }>({
|
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
|
|
|
queryKey: ['search', 'recently-viewed'],
|
|
|
|
|
queryFn: ({ signal }) =>
|
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs
Wave through the remaining audit-final-deferred items that aren't blocked
on the back-burnered Documenso work.
Multi-tenant isolation:
- Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim;
verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a
buggy issuer in some future code path that mixes port scopes — every
storage key generated by generateStorageKey() already prefixes the
slug. document-sends opts in for 24h emailed download links; other
callers continue working unchanged via the optional field.
DB schema reconciliation:
- Migration 0047 rebuilds system_settings unique index with NULLS NOT
DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are
uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate
(storage_backend, NULL) rows that had accumulated from race-prone
delete-then-insert patterns in ocr-config / settings / residential-
stages / ai-budget services. All four services converted to true
onConflictDoUpdate upserts so the race window is closed.
API uniformity:
- Response shape standardization: 16 routes converted from
`{ success: true }` to 204 No Content. CLAUDE.md documents the
convention (`{ data: <T> }` for content, 204 for empty mutations,
portal-auth retains `{ success: true }` for the frontend's auth chain).
- req.json() → parseBody() migration across 9 admin/CRM routes
(custom-fields, expenses/export ×3, currency convert,
search/recently-viewed, admin/duplicates, berths/pdf-{upload-url,
versions, parse-results}). Uniform 400 error shapes for
ZodError-flagged bodies.
Custom-fields merge tokens (shipped end-to-end):
- merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the
`{{custom.<fieldName>}}` shape.
- document-templates validator accepts the dynamic shape alongside
the static catalog tokens.
- document-sends.service mergeCustomFieldValues resolver fetches
per-port custom_field_definitions for client/interest/berth contexts
and substitutes stored values keyed by `{{custom.fieldName}}`.
- custom-fields-manager amber banner updated to reflect that merge
tokens now expand (search index + entity-diff remain documented
design limitations).
/api/v1/files cross-entity filtering:
- Validator + listFiles + uploadFile accept companyId AND yachtId
alongside clientId. file-upload-zone propagates both.
- New CompanyFilesTab component mirrors ClientFilesTab; restored as a
visible Documents tab in company-tabs.tsx (was a hidden stub).
Inline TODOs:
- Reviewed remaining two TODOs (per-user reminder schedule, import
worker handlers). Both are placeholders for future feature surfaces,
not bugs — per-port digest works for every customer; nothing
currently enqueues import jobs (verified). Annotated in BACKLOG.
BACKLOG.md updated to reflect what landed and what's still pending
(Documenso-related items still bundled with the back-burnered phases).
Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
|
|
|
apiFetch<{ data: RecentlyViewedItem[] }>('/api/v1/search/recently-viewed', { signal }),
|
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
|
|
|
staleTime: 30_000,
|
|
|
|
|
});
|
|
|
|
|
|
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 {
|
|
|
|
|
results: searchQuery.data,
|
|
|
|
|
isLoading: searchQuery.isLoading,
|
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: searchQuery.isFetching,
|
|
|
|
|
enabled,
|
|
|
|
|
recentSearches: recentSearchQuery.data?.searches ?? [],
|
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs
Wave through the remaining audit-final-deferred items that aren't blocked
on the back-burnered Documenso work.
Multi-tenant isolation:
- Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim;
verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a
buggy issuer in some future code path that mixes port scopes — every
storage key generated by generateStorageKey() already prefixes the
slug. document-sends opts in for 24h emailed download links; other
callers continue working unchanged via the optional field.
DB schema reconciliation:
- Migration 0047 rebuilds system_settings unique index with NULLS NOT
DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are
uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate
(storage_backend, NULL) rows that had accumulated from race-prone
delete-then-insert patterns in ocr-config / settings / residential-
stages / ai-budget services. All four services converted to true
onConflictDoUpdate upserts so the race window is closed.
API uniformity:
- Response shape standardization: 16 routes converted from
`{ success: true }` to 204 No Content. CLAUDE.md documents the
convention (`{ data: <T> }` for content, 204 for empty mutations,
portal-auth retains `{ success: true }` for the frontend's auth chain).
- req.json() → parseBody() migration across 9 admin/CRM routes
(custom-fields, expenses/export ×3, currency convert,
search/recently-viewed, admin/duplicates, berths/pdf-{upload-url,
versions, parse-results}). Uniform 400 error shapes for
ZodError-flagged bodies.
Custom-fields merge tokens (shipped end-to-end):
- merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the
`{{custom.<fieldName>}}` shape.
- document-templates validator accepts the dynamic shape alongside
the static catalog tokens.
- document-sends.service mergeCustomFieldValues resolver fetches
per-port custom_field_definitions for client/interest/berth contexts
and substitutes stored values keyed by `{{custom.fieldName}}`.
- custom-fields-manager amber banner updated to reflect that merge
tokens now expand (search index + entity-diff remain documented
design limitations).
/api/v1/files cross-entity filtering:
- Validator + listFiles + uploadFile accept companyId AND yachtId
alongside clientId. file-upload-zone propagates both.
- New CompanyFilesTab component mirrors ClientFilesTab; restored as a
visible Documents tab in company-tabs.tsx (was a hidden stub).
Inline TODOs:
- Reviewed remaining two TODOs (per-user reminder schedule, import
worker handlers). Both are placeholders for future feature surfaces,
not bugs — per-port digest works for every customer; nothing
currently enqueues import jobs (verified). Annotated in BACKLOG.
BACKLOG.md updated to reflect what landed and what's still pending
(Documenso-related items still bundled with the back-burnered phases).
Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
|
|
|
recentlyViewed: recentlyViewedQuery.data?.data ?? [],
|
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
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Track that the current user just opened an entity. Call once on mount
|
|
|
|
|
* from each entity detail page (via `useTrackEntityView`); the resulting
|
|
|
|
|
* (type, id) pair is used by the search dropdown's "Recently viewed"
|
|
|
|
|
* section to surface the user's working set.
|
|
|
|
|
*/
|
|
|
|
|
export async function trackEntityView(type: string, id: string): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await apiFetch('/api/v1/search/recently-viewed', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: { type, id },
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
// Tracking is non-critical — never bubble up to the user.
|
|
|
|
|
}
|
|
|
|
|
}
|