feat(ui): add reservations tab to berth detail
This commit is contained in:
90
src/components/berths/berth-reservations-tab.tsx
Normal file
90
src/components/berths/berth-reservations-tab.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { PermissionGate } from '@/components/shared/permission-gate';
|
||||||
|
import { EmptyState } from '@/components/shared/empty-state';
|
||||||
|
import { ReservationList, type ReservationRow } from '@/components/reservations/reservation-list';
|
||||||
|
import { BerthReserveDialog } from '@/components/reservations/berth-reserve-dialog';
|
||||||
|
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
||||||
|
import { apiFetch } from '@/lib/api/client';
|
||||||
|
|
||||||
|
interface BerthReservationsTabProps {
|
||||||
|
berthId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BerthReservationsTab({ berthId }: BerthReservationsTabProps) {
|
||||||
|
const routeParams = useParams<{ portSlug: string }>();
|
||||||
|
const portSlug = routeParams?.portSlug ?? '';
|
||||||
|
const [reserveOpen, setReserveOpen] = useState(false);
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery<{ data: ReservationRow[]; pagination?: unknown }>({
|
||||||
|
queryKey: ['berths', berthId, 'reservations'],
|
||||||
|
queryFn: () =>
|
||||||
|
apiFetch(
|
||||||
|
`/api/v1/berths/${berthId}/reservations?page=1&limit=50&order=desc&includeArchived=false`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
useRealtimeInvalidation({
|
||||||
|
'berth_reservation:created': [['berths', berthId, 'reservations']],
|
||||||
|
'berth_reservation:activated': [['berths', berthId, 'reservations']],
|
||||||
|
'berth_reservation:ended': [['berths', berthId, 'reservations']],
|
||||||
|
'berth_reservation:cancelled': [['berths', berthId, 'reservations']],
|
||||||
|
});
|
||||||
|
|
||||||
|
const reservations = data?.data ?? [];
|
||||||
|
const active = reservations.find((r) => r.status === 'active');
|
||||||
|
const history = reservations.filter((r) => r.status !== 'active');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold">Reservations</h3>
|
||||||
|
<PermissionGate resource="reservations" action="create">
|
||||||
|
<Button size="sm" onClick={() => setReserveOpen(true)}>
|
||||||
|
<Plus className="mr-1.5 h-4 w-4" />
|
||||||
|
Reserve this berth
|
||||||
|
</Button>
|
||||||
|
</PermissionGate>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active reservation card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium">Active reservation</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{active ? (
|
||||||
|
<ReservationList reservations={[active]} portSlug={portSlug} />
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">No active reservation.</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* History */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium">History</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Loading…</p>
|
||||||
|
) : history.length === 0 ? (
|
||||||
|
<EmptyState title="No past reservations" description="Nothing here yet." />
|
||||||
|
) : (
|
||||||
|
<ReservationList reservations={history} portSlug={portSlug} />
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<BerthReserveDialog open={reserveOpen} onOpenChange={setReserveOpen} berthId={berthId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { type DetailTab } from '@/components/shared/detail-layout';
|
import { type DetailTab } from '@/components/shared/detail-layout';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { TagBadge } from '@/components/shared/tag-badge';
|
import { TagBadge } from '@/components/shared/tag-badge';
|
||||||
|
import { BerthReservationsTab } from './berth-reservations-tab';
|
||||||
|
|
||||||
type BerthData = {
|
type BerthData = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -87,7 +88,10 @@ function OverviewTab({ berth }: { berth: BerthData }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SpecRow label="Draft" value={formatDim(berth.draftFt, berth.draftM)} />
|
<SpecRow label="Draft" value={formatDim(berth.draftFt, berth.draftM)} />
|
||||||
<SpecRow label="Nominal Boat Size" value={berth.nominalBoatSize || berth.nominalBoatSizeM} />
|
<SpecRow
|
||||||
|
label="Nominal Boat Size"
|
||||||
|
value={berth.nominalBoatSize || berth.nominalBoatSizeM}
|
||||||
|
/>
|
||||||
<SpecRow
|
<SpecRow
|
||||||
label="Water Depth"
|
label="Water Depth"
|
||||||
value={
|
value={
|
||||||
@@ -179,6 +183,11 @@ export function buildBerthTabs(berth: BerthData): DetailTab[] {
|
|||||||
label: 'Interests',
|
label: 'Interests',
|
||||||
content: <StubTab label="Interests" />,
|
content: <StubTab label="Interests" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'reservations',
|
||||||
|
label: 'Reservations',
|
||||||
|
content: <BerthReservationsTab berthId={berth.id} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'waiting-list',
|
id: 'waiting-list',
|
||||||
label: 'Waiting List',
|
label: 'Waiting List',
|
||||||
|
|||||||
Reference in New Issue
Block a user