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