Files
pn-new-crm/src/lib/validators/search.ts
Matt 267c2b6d1f 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

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>;