diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 6b2e4ef..b6326ab 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -47,7 +47,13 @@ export default async function DashboardLayout({ children }: { children: React.Re }} />
- +
{children}
diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx index 2fecd04..3658b05 100644 --- a/src/components/layout/topbar.tsx +++ b/src/components/layout/topbar.tsx @@ -23,9 +23,10 @@ import type { Port } from '@/lib/db/schema/ports'; interface TopbarProps { ports: Port[]; + user?: { name: string; email: string }; } -export function Topbar({ ports }: TopbarProps) { +export function Topbar({ ports, user }: TopbarProps) { const router = useRouter(); const currentPortSlug = useUIStore((s) => s.currentPortSlug); const darkMode = useUIStore((s) => s.darkMode); @@ -95,13 +96,18 @@ export function Topbar({ ports }: TopbarProps) { - U + {(user?.name ?? 'U').slice(0, 1).toUpperCase()} - - My Account + + +
{user?.name ?? 'My Account'}
+ {user?.email && ( +
{user.email}
+ )} +
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} router.push(`${base}/settings/profile` as any)}> diff --git a/src/components/yachts/yacht-columns.tsx b/src/components/yachts/yacht-columns.tsx index 0df67c5..289a9a9 100644 --- a/src/components/yachts/yacht-columns.tsx +++ b/src/components/yachts/yacht-columns.tsx @@ -20,6 +20,7 @@ export interface YachtRow { registration: string | null; currentOwnerType: 'client' | 'company'; currentOwnerId: string; + currentOwnerName?: string | null; lengthFt: string | null; widthFt: string | null; draftFt: string | null; @@ -92,6 +93,7 @@ export function getYachtColumns({ portSlug={portSlug} type={row.original.currentOwnerType} id={row.original.currentOwnerId} + preloadedName={row.original.currentOwnerName ?? null} /> ), }, diff --git a/src/components/yachts/yacht-detail-header.tsx b/src/components/yachts/yacht-detail-header.tsx index 99b8a14..08eb8b2 100644 --- a/src/components/yachts/yacht-detail-header.tsx +++ b/src/components/yachts/yacht-detail-header.tsx @@ -58,20 +58,27 @@ export function OwnerLink({ portSlug, type, id, + preloadedName, }: { portSlug: string; type: 'client' | 'company'; id: string; + /** Optional name supplied by the parent list/detail endpoint to skip the + * per-row fetch (avoids an N+1 round-trip on lists). */ + preloadedName?: string | null; }) { + // Only fetch when the parent didn't already supply a name — list endpoints + // batch-resolve owners server-side via a join. const { data } = useQuery<{ fullName?: string; name?: string }>({ queryKey: [type === 'client' ? 'clients' : 'companies', id, 'name-only'], queryFn: () => apiFetch<{ data: { fullName?: string; name?: string } }>( type === 'client' ? `/api/v1/clients/${id}` : `/api/v1/companies/${id}`, ).then((r) => r.data), + enabled: preloadedName === undefined || preloadedName === null, }); - const label = type === 'client' ? data?.fullName : data?.name; + const label = preloadedName ?? (type === 'client' ? data?.fullName : data?.name); const href = type === 'client' ? `/${portSlug}/clients/${id}` : `/${portSlug}/companies/${id}`; return ( diff --git a/src/lib/services/yachts.service.ts b/src/lib/services/yachts.service.ts index 874ee7f..1186ff8 100644 --- a/src/lib/services/yachts.service.ts +++ b/src/lib/services/yachts.service.ts @@ -1,4 +1,4 @@ -import { and, eq, ilike, or, sql } from 'drizzle-orm'; +import { and, eq, ilike, inArray, 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'; @@ -307,7 +307,45 @@ export async function listYachts(portId: string, query: ListYachtsInput) { archivedAtColumn: yachts.archivedAt, }); - return result; + if (result.data.length === 0) return result; + + // Resolve current owner names in two parallel batched queries instead of + // an N+1 fetch from the client (was 1 round-trip per row from yacht-columns). + const clientIds = result.data + .filter((y) => y.currentOwnerType === 'client') + .map((y) => y.currentOwnerId); + const companyIds = result.data + .filter((y) => y.currentOwnerType === 'company') + .map((y) => y.currentOwnerId); + + const [clientRows, companyRows] = await Promise.all([ + clientIds.length > 0 + ? db + .select({ id: clients.id, fullName: clients.fullName }) + .from(clients) + .where(inArray(clients.id, clientIds)) + : Promise.resolve([] as { id: string; fullName: string }[]), + companyIds.length > 0 + ? db + .select({ id: companies.id, name: companies.name }) + .from(companies) + .where(inArray(companies.id, companyIds)) + : Promise.resolve([] as { id: string; name: string }[]), + ]); + + const clientNames = new Map(clientRows.map((r) => [r.id, r.fullName])); + const companyNames = new Map(companyRows.map((r) => [r.id, r.name])); + + return { + ...result, + data: result.data.map((y) => ({ + ...y, + currentOwnerName: + y.currentOwnerType === 'client' + ? (clientNames.get(y.currentOwnerId) ?? null) + : (companyNames.get(y.currentOwnerId) ?? null), + })), + }; } // ─── List for owner ───────────────────────────────────────────────────────────