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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user