feat(marina): end-reservation UI + global list, yacht tabs, dashboard distinct count

- End-reservation: API handler existed but had no UI surface. Adds an
  "End reservation" button + date dialog on the reservation detail page,
  visible only when status is `active`.
- New port-scoped `GET /api/v1/berth-reservations` list endpoint and
  `[portSlug]/berth-reservations` page so users can see all reservations
  across all berths from one place (was 404).
- Berths "Edit" menu pushed `/berths/{id}?edit=true` but the detail page
  never read the param — it now auto-opens the edit sheet on mount and
  strips `edit` from the URL.
- Reservation detail no longer shows raw 8-char UUIDs for Berth / Yacht
  / Client; reuses the lazy-fetching link components from the list view.
- Yacht "Interests" and "Reservations" tabs replaced their "Coming soon"
  stubs with real lists fetched from the existing service routes.
- Dashboard "Pipeline Value" KPI used `select(berthId, price)` and
  summed per active interest, so a berth with three open interests was
  counted three times. Switched to `selectDistinct(berthId, price)`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-02 23:01:15 +02:00
parent e3e0e69c04
commit a391934b73
8 changed files with 301 additions and 44 deletions

View File

@@ -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 <p className="text-sm text-muted-foreground">Loading</p>;
if (interests.length === 0) {
return <p className="text-sm text-muted-foreground">No interests linked to this yacht.</p>;
}
return (
<ul className="space-y-2">
{interests.map((i) => (
<li
key={i.id}
className="flex items-center gap-3 rounded-md border bg-muted/30 p-3 text-sm"
>
<span className="w-36 shrink-0 text-xs font-medium uppercase text-muted-foreground">
{i.pipelineStage.replace(/_/g, ' ')}
</span>
<span className="flex-1 truncate">{i.clientName ?? '—'}</span>
{i.berthMooringNumber && (
<span className="shrink-0 text-xs text-muted-foreground">
Berth {i.berthMooringNumber}
</span>
)}
</li>
))}
</ul>
);
}
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 <p className="text-sm text-muted-foreground">Loading</p>;
return (
<ReservationList
reservations={data?.data ?? []}
showBerth
portSlug={portSlug}
emptyMessage="No reservations for this yacht."
/>
);
}
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: <EmptyState title="Interests" description="Coming soon" />,
content: <YachtInterestsTab yachtId={yachtId} />,
},
{
id: 'reservations',
label: 'Reservations',
content: <EmptyState title="Reservations" description="Coming soon" />,
content: <YachtReservationsTab yachtId={yachtId} />,
},
{
id: 'notes',