From 2bcf544cbc487202474b8f040901c7cf5264827d Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 May 2026 18:06:41 +0200 Subject: [PATCH] feat(uat-batch-11): picker polish + BulkAddBerthsWizard currency + DocumentsHub root cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BulkAddBerthsWizard `priceCurrency` row + apply-to-all swapped from freetext Input to the shared CurrencySelect. Same idiom as berth-form + expense-form-dialog. - /api/v1/yachts/autocomplete no longer short-circuits to `[]` when the search query is empty — the service returns the top 20 most-recently-updated yachts so the picker has a useful default view the moment it opens. Saves the rep from a dead-end empty state. - YachtPicker gains a fallback useQuery against `/api/v1/yachts/{id}` when the selected yacht isn't present in the current autocomplete window. Trigger label now shows the real name (was falling back to "Yacht " when a parent pre-selected a value from a URL param). - DocumentsHub: breadcrumb row only renders when a folder is selected. The "Home / All documents" placeholder was wasted vertical space above the PageHeader on the root view. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/v1/yachts/autocomplete/handlers.ts | 5 +---- .../admin/bulk-add-berths-wizard.tsx | 19 ++++++++----------- src/components/documents/documents-hub.tsx | 10 +++++----- src/components/yachts/yacht-picker.tsx | 15 ++++++++++++++- src/lib/services/yachts.service.ts | 13 ++++++++++++- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/app/api/v1/yachts/autocomplete/handlers.ts b/src/app/api/v1/yachts/autocomplete/handlers.ts index 48d6166b..7f00e251 100644 --- a/src/app/api/v1/yachts/autocomplete/handlers.ts +++ b/src/app/api/v1/yachts/autocomplete/handlers.ts @@ -6,10 +6,7 @@ import { autocomplete } from '@/lib/services/yachts.service'; export const autocompleteHandler: RouteHandler = async (req, ctx) => { try { - const q = req.nextUrl.searchParams.get('q'); - if (!q) { - return NextResponse.json({ data: [] }); - } + const q = req.nextUrl.searchParams.get('q') ?? ''; const yachts = await autocomplete(ctx.portId, q); return NextResponse.json({ data: yachts }); } catch (error) { diff --git a/src/components/admin/bulk-add-berths-wizard.tsx b/src/components/admin/bulk-add-berths-wizard.tsx index 1b937115..a785006e 100644 --- a/src/components/admin/bulk-add-berths-wizard.tsx +++ b/src/components/admin/bulk-add-berths-wizard.tsx @@ -36,6 +36,7 @@ import { import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { useVocabulary } from '@/hooks/use-vocabulary'; +import { CurrencySelect } from '@/components/shared/currency-select'; const DOCK_LETTERS = ['A', 'B', 'C', 'D', 'E'] as const; type DockLetter = (typeof DOCK_LETTERS)[number]; @@ -283,12 +284,10 @@ export function BulkAddBerthsWizard() { /> - applyToAll('priceCurrency', v)} className="h-7 text-xs" - onBlur={(e) => { - if (e.target.value) applyToAll('priceCurrency', e.target.value.toUpperCase()); - }} - placeholder="all" /> @@ -351,12 +350,10 @@ export function BulkAddBerthsWizard() { /> - - setRowField(idx, 'priceCurrency', e.target.value.toUpperCase()) - } + setRowField(idx, 'priceCurrency', v)} + className="h-7 w-24 text-xs" /> diff --git a/src/components/documents/documents-hub.tsx b/src/components/documents/documents-hub.tsx index 9cc35baf..25e1cb61 100644 --- a/src/components/documents/documents-hub.tsx +++ b/src/components/documents/documents-hub.tsx @@ -193,9 +193,9 @@ export function DocumentsHub({ portSlug }: DocumentsHubProps) { const contentPane = (
-
- - {selectedFolderId !== undefined && ( + {selectedFolderId !== undefined && ( +
+ - )} -
+
+ )} {selectedFolderId === undefined ? ( <> diff --git a/src/components/yachts/yacht-picker.tsx b/src/components/yachts/yacht-picker.tsx index 2bddc9e0..c3c02333 100644 --- a/src/components/yachts/yacht-picker.tsx +++ b/src/components/yachts/yacht-picker.tsx @@ -72,10 +72,23 @@ export function YachtPicker({ ) : rawOptions; + // When `value` is set but the selected yacht isn't in the current + // autocomplete window (e.g. parent pre-selected it from a URL param + // or the rep typed something that filtered it out), fetch its name + // by id so the trigger doesn't fall back to "Yacht ". + const fallbackQuery = useQuery<{ data: { name: string } }>({ + queryKey: ['yacht-detail-label', value], + queryFn: () => apiFetch(`/api/v1/yachts/${value}`), + enabled: !!value && !rawOptions.some((o) => o.id === value), + staleTime: 60_000, + }); + const selectedLabel = (() => { if (!value) return placeholder; const match = rawOptions.find((o) => o.id === value); - return match?.name ?? `Yacht ${value.slice(0, 8)}`; + if (match) return match.name; + if (fallbackQuery.data?.data.name) return fallbackQuery.data.data.name; + return `Yacht ${value.slice(0, 8)}`; })(); return ( diff --git a/src/lib/services/yachts.service.ts b/src/lib/services/yachts.service.ts index 39435c94..5f277524 100644 --- a/src/lib/services/yachts.service.ts +++ b/src/lib/services/yachts.service.ts @@ -1,4 +1,4 @@ -import { and, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm'; +import { and, desc, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm'; import { db } from '@/lib/db'; import { yachts, yachtOwnershipHistory, yachtTags, clients } from '@/lib/db/schema'; import type { Yacht } from '@/lib/db/schema/yachts'; @@ -393,6 +393,17 @@ export async function listOwnershipHistory(yachtId: string, portId: string) { // ─── Autocomplete ───────────────────────────────────────────────────────────── export async function autocomplete(portId: string, q: string) { + // Empty query returns the top 20 most-recently-updated yachts so the + // picker has something useful to show the moment it opens, instead of + // a dead-end empty state until the rep types something. + if (!q) { + return await db + .select() + .from(yachts) + .where(eq(yachts.portId, portId)) + .orderBy(desc(yachts.updatedAt)) + .limit(20); + } const pattern = `%${q}%`; return await db .select()