- Berth
- -
-
- {res.berthId.slice(0, 8)}…
-
+
-
+
- Yacht
- -
-
- {res.yachtId.slice(0, 8)}…
-
+
-
+
- Client
- -
-
- {res.clientId.slice(0, 8)}…
-
+
-
+
@@ -287,6 +356,12 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail
+
+
);
}
diff --git a/src/components/reservations/reservation-list.tsx b/src/components/reservations/reservation-list.tsx
index 4f0ef49..a3e6e00 100644
--- a/src/components/reservations/reservation-list.tsx
+++ b/src/components/reservations/reservation-list.tsx
@@ -41,7 +41,7 @@ export interface ReservationListProps {
* Renders a client's name as a link by fetching the client record.
* Uses TanStack Query cache for memoization of repeated clientId queries.
*/
-function ClientLink({ clientId, portSlug }: { clientId: string; portSlug: string }) {
+export function ClientLink({ clientId, portSlug }: { clientId: string; portSlug: string }) {
const { data } = useQuery<{ fullName: string }>({
queryKey: ['clients', clientId, 'name-only'],
queryFn: () =>
@@ -62,7 +62,7 @@ function ClientLink({ clientId, portSlug }: { clientId: string; portSlug: string
/**
* Renders a yacht's name as a link by fetching the yacht record.
*/
-function YachtLink({ yachtId, portSlug }: { yachtId: string; portSlug: string }) {
+export function YachtLink({ yachtId, portSlug }: { yachtId: string; portSlug: string }) {
const { data } = useQuery<{ name: string }>({
queryKey: ['yachts', yachtId, 'name-only'],
queryFn: () =>
@@ -83,7 +83,7 @@ function YachtLink({ yachtId, portSlug }: { yachtId: string; portSlug: string })
/**
* Renders a berth's mooring number as a link by fetching the berth record.
*/
-function BerthLink({ berthId, portSlug }: { berthId: string; portSlug: string }) {
+export function BerthLink({ berthId, portSlug }: { berthId: string; portSlug: string }) {
const { data } = useQuery<{ mooringNumber: string }>({
queryKey: ['berths', berthId, 'name-only'],
queryFn: () =>
diff --git a/src/components/yachts/yacht-tabs.tsx b/src/components/yachts/yacht-tabs.tsx
index 1608678..cd64af1 100644
--- a/src/components/yachts/yacht-tabs.tsx
+++ b/src/components/yachts/yacht-tabs.tsx
@@ -1,12 +1,13 @@
'use client';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useParams } from 'next/navigation';
import type { DetailTab } from '@/components/shared/detail-layout';
-import { EmptyState } from '@/components/shared/empty-state';
import { InlineEditableField } from '@/components/shared/inline-editable-field';
import { InlineTagEditor } from '@/components/shared/inline-tag-editor';
import { NotesList } from '@/components/shared/notes-list';
+import { ReservationList, type ReservationRow } from '@/components/reservations/reservation-list';
import { YachtOwnershipHistory } from '@/components/yachts/yacht-ownership-history';
import { apiFetch } from '@/lib/api/client';
@@ -206,6 +207,70 @@ function OverviewTab({ yachtId, yacht }: { yachtId: string; yacht: YachtTabsYach
);
}
+function YachtInterestsTab({ yachtId }: { yachtId: string }) {
+ const { data, isLoading } = useQuery<{
+ data: Array<{
+ id: string;
+ pipelineStage: string;
+ clientName: string | null;
+ berthMooringNumber: string | null;
+ updatedAt: string;
+ }>;
+ }>({
+ queryKey: ['interests', 'by-yacht', yachtId],
+ queryFn: () => apiFetch(`/api/v1/interests?yachtId=${yachtId}&limit=50&order=desc`),
+ });
+
+ const interests = data?.data ?? [];
+
+ if (isLoading) return Loading…
;
+ if (interests.length === 0) {
+ return No interests linked to this yacht.
;
+ }
+
+ return (
+
+ {interests.map((i) => (
+ -
+
+ {i.pipelineStage.replace(/_/g, ' ')}
+
+ {i.clientName ?? '—'}
+ {i.berthMooringNumber && (
+
+ Berth {i.berthMooringNumber}
+
+ )}
+
+ ))}
+
+ );
+}
+
+function YachtReservationsTab({ yachtId }: { yachtId: string }) {
+ const routeParams = useParams<{ portSlug: string }>();
+ const portSlug = routeParams?.portSlug ?? '';
+
+ const { data, isLoading } = useQuery<{ data: ReservationRow[] }>({
+ queryKey: ['berth-reservations', 'by-yacht', yachtId],
+ queryFn: () => apiFetch(`/api/v1/berth-reservations?yachtId=${yachtId}&limit=50&order=desc`),
+ });
+
+ if (isLoading) return Loading…
;
+
+ return (
+
+ );
+}
+
export function getYachtTabs({ yachtId, currentUserId, yacht }: YachtTabsOptions): DetailTab[] {
return [
{
@@ -221,12 +286,12 @@ export function getYachtTabs({ yachtId, currentUserId, yacht }: YachtTabsOptions
{
id: 'interests',
label: 'Interests',
- content: ,
+ content: ,
},
{
id: 'reservations',
label: 'Reservations',
- content: ,
+ content: ,
},
{
id: 'notes',
diff --git a/src/lib/services/dashboard.service.ts b/src/lib/services/dashboard.service.ts
index 90ee163..1218f85 100644
--- a/src/lib/services/dashboard.service.ts
+++ b/src/lib/services/dashboard.service.ts
@@ -27,9 +27,11 @@ export async function getKpis(portId: string) {
.from(interests)
.where(and(eq(interests.portId, portId), isNull(interests.archivedAt), isActiveInterest));
- // Pipeline value: SUM berths.price via JOIN from non-archived interests with berthId
+ // Pipeline value: SUM each berth's price ONCE regardless of how many active
+ // interests reference it. A berth with multiple interests would otherwise be
+ // counted multiple times, inflating the total.
const pipelineRows = await db
- .select({ price: berths.price })
+ .selectDistinct({ berthId: interests.berthId, price: berths.price })
.from(interests)
.innerJoin(berths, eq(interests.berthId, berths.id))
.where(