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>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
54
src/components/search/highlight-match.tsx
Normal file
54
src/components/search/highlight-match.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Fragment, type ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* Wrap occurrences of `query` inside `text` with `<mark>` so the user
|
||||
* can see why a result matched. Case-insensitive, escapes regex meta-
|
||||
* chars in the query so a paste of "INV-2025" doesn't blow up.
|
||||
*
|
||||
* Tokenized matching — splits the query on whitespace and highlights
|
||||
* each token independently, so "joh smi" highlights both "Joh" and
|
||||
* "Smi" in "John Smith". Mirrors the prefix-tsquery the server uses.
|
||||
*/
|
||||
export function HighlightMatch({
|
||||
text,
|
||||
query,
|
||||
className,
|
||||
}: {
|
||||
text: string | null | undefined;
|
||||
query: string;
|
||||
className?: string;
|
||||
}): ReactNode {
|
||||
if (!text) return null;
|
||||
const tokens = query
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((t) => t.length > 0)
|
||||
.map(escapeRegex);
|
||||
if (tokens.length === 0) return text;
|
||||
|
||||
const re = new RegExp(`(${tokens.join('|')})`, 'gi');
|
||||
const parts = text.split(re);
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{parts.map((part, i) => {
|
||||
if (i % 2 === 1) {
|
||||
// The capture group lands on odd indices.
|
||||
return (
|
||||
<mark
|
||||
key={i}
|
||||
className="bg-brand/15 text-foreground rounded-[2px] px-[1px] font-medium"
|
||||
>
|
||||
{part}
|
||||
</mark>
|
||||
);
|
||||
}
|
||||
return <Fragment key={i}>{part}</Fragment>;
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function escapeRegex(input: string): string {
|
||||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
33
src/components/search/track-entity-view.tsx
Normal file
33
src/components/search/track-entity-view.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { useTrackEntityView } from '@/hooks/use-track-entity-view';
|
||||
|
||||
/**
|
||||
* Render-only client component that records "the user opened this
|
||||
* entity" via the global-search recently-viewed Redis log. Drop into a
|
||||
* server-rendered detail page as `<TrackEntityView type="client" id={…} />`
|
||||
* — the component renders nothing.
|
||||
*
|
||||
* Centralises the tracking call so future changes (debounce, schema,
|
||||
* batching) only need to touch one file rather than every detail page.
|
||||
*/
|
||||
export function TrackEntityView({
|
||||
type,
|
||||
id,
|
||||
}: {
|
||||
type:
|
||||
| 'client'
|
||||
| 'residential-client'
|
||||
| 'yacht'
|
||||
| 'company'
|
||||
| 'interest'
|
||||
| 'residential-interest'
|
||||
| 'berth'
|
||||
| 'invoice'
|
||||
| 'expense'
|
||||
| 'document';
|
||||
id: string | null | undefined;
|
||||
}) {
|
||||
useTrackEntityView(type, id);
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user