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>
57 lines
1.4 KiB
TypeScript
57 lines
1.4 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
const BUCKET_TYPES = [
|
|
'clients',
|
|
'residentialClients',
|
|
'yachts',
|
|
'companies',
|
|
'interests',
|
|
'residentialInterests',
|
|
'berths',
|
|
'invoices',
|
|
'expenses',
|
|
'documents',
|
|
'files',
|
|
'reminders',
|
|
'brochures',
|
|
'tags',
|
|
'navigation',
|
|
] as const;
|
|
|
|
export const searchQuerySchema = z.object({
|
|
// 2-char minimum keeps `to_tsquery('a:*')` from returning every word
|
|
// starting with "a" — short queries return overwhelming match sets.
|
|
q: z.string().min(2).max(200),
|
|
/** Restrict the result set to a single bucket. */
|
|
type: z.enum(BUCKET_TYPES).optional(),
|
|
/** Per-bucket cap. Defaults to 5 (dropdown). 25 is the typical /search-page value. */
|
|
limit: z.coerce.number().int().min(1).max(50).optional(),
|
|
/** Super-admin only — search ports beyond the current one. */
|
|
includeOtherPorts: z
|
|
.union([z.literal('true'), z.literal('1'), z.literal('false'), z.literal('0')])
|
|
.transform((v) => v === 'true' || v === '1')
|
|
.optional(),
|
|
});
|
|
|
|
export type SearchQuery = z.infer<typeof searchQuerySchema>;
|
|
|
|
const RECENTLY_VIEWED_TYPES = [
|
|
'client',
|
|
'residential-client',
|
|
'yacht',
|
|
'company',
|
|
'interest',
|
|
'residential-interest',
|
|
'berth',
|
|
'invoice',
|
|
'expense',
|
|
'document',
|
|
] as const;
|
|
|
|
export const trackViewSchema = z.object({
|
|
type: z.enum(RECENTLY_VIEWED_TYPES),
|
|
id: z.string().min(1).max(100),
|
|
});
|
|
|
|
export type TrackViewPayload = z.infer<typeof trackViewSchema>;
|