feat(tenancies-p2): rename berth_reservations → berth_tenancies (schema + perms + UI)
73-file atomic rename per docs/tenancies-design.md:
- Migration 0085: rename table + indexes + FK constraints; rename
documents.reservation_id → tenancy_id; migrate jsonb permission maps
(reservations resource → tenancies; collapse create+activate → manage);
rewrite historical audit_logs.entity_type='berth_reservation' →
'berth_tenancy'. FK renames wrapped in DO blocks so dev DBs that pre-date
the FK additions don't abort.
- Schema: berthReservations → berthTenancies; BerthReservation type →
BerthTenancy; indexes idx_br_* / idx_brr_* → idx_bt_*.
- RolePermissions: resource { view, create, activate, cancel } collapses to
{ view, manage, cancel }; all 8 default seed bundles + role-form + matrix
updated.
- Service: berth-reservations.service.ts → berth-tenancies.service.ts;
endReservation → endTenancy; listReservations → listTenancies.
- API: /api/v1/berth-reservations → /api/v1/tenancies (+ nested [id]);
/api/v1/berths/[id]/reservations → /api/v1/berths/[id]/tenancies.
- Validators: reservations.ts → tenancies.ts; RESERVATION_STATUSES →
TENANCY_STATUSES; endReservationSchema → endTenancySchema.
- Routes: /{portSlug}/berth-reservations → /{portSlug}/tenancies;
/portal/my-reservations → /portal/my-tenancies.
- Components: src/components/reservations/* → src/components/tenancies/*;
BerthReservationsTab → BerthTenanciesTab; ClientReservationsTab →
ClientTenanciesTab; ReservationList → TenancyList.
- Socket events: berth_reservation:* → berth_tenancy:*; payload
reservationId → tenancyId.
- Webhook events: berth_reservation.* → berth_tenancy.*.
- Portal: getPortalUserReservations → getPortalUserTenancies;
PortalReservation → PortalTenancy; PortalDashboard.counts.activeReservations
→ activeTenancies; PortalNav label "Reservations" → "Tenancies".
- Dossier: DossierReservation → DossierTenancy; reservationDecisions →
tenancyDecisions across smart-archive-dialog + bulk-archive routes.
- Documents schema: documents.reservationId → documents.tenancyId
(TS + DB column + index + FK constraint).
- Activity feed label berth_reservation → berth_tenancy (matched against
migrated historical audit rows).
KEPT (separate concepts):
- Reservation Agreement document type (the contract sent to clients).
- "Reservation" pipeline stage name.
- {{reservation.*}} merge tokens in template authoring.
- interest.reservationStatus / reservationDocStatus / dateReservationSent
fields (track agreement signing on the deal).
- reservation-agreement-context.ts service (builds merge context for the
Reservation Agreement doc; only its DB imports were renamed).
Verified: tsc clean, 1480/1480 vitest passing, migration applied.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,7 @@ const DEFAULT_PERMISSIONS: Record<string, Record<string, boolean>> = {
|
||||
yachts: { view: false, create: false, edit: false, delete: false, transfer: false },
|
||||
companies: { view: false, create: false, edit: false, delete: false },
|
||||
memberships: { view: false, manage: false },
|
||||
reservations: { view: false, create: false, activate: false, cancel: false },
|
||||
tenancies: { view: false, manage: false, cancel: false },
|
||||
admin: {
|
||||
manage_users: false,
|
||||
view_audit_log: false,
|
||||
@@ -122,7 +122,7 @@ const GROUP_LABELS: Record<string, string> = {
|
||||
yachts: 'Yachts',
|
||||
companies: 'Companies',
|
||||
memberships: 'Company Memberships',
|
||||
reservations: 'Reservations',
|
||||
tenancies: 'Tenancies',
|
||||
admin: 'Administration',
|
||||
residential_clients: 'Residential Clients',
|
||||
residential_interests: 'Residential Interests',
|
||||
|
||||
@@ -46,7 +46,7 @@ const GROUP_LABELS: Record<string, string> = {
|
||||
yachts: 'Yachts',
|
||||
companies: 'Companies',
|
||||
memberships: 'Company Memberships',
|
||||
reservations: 'Reservations',
|
||||
tenancies: 'Tenancies',
|
||||
admin: 'Administration',
|
||||
residential_clients: 'Residential Clients',
|
||||
residential_interests: 'Residential Interests',
|
||||
@@ -89,7 +89,7 @@ const PERMISSION_LEAVES: Record<string, string[]> = {
|
||||
yachts: ['view', 'create', 'edit', 'delete', 'transfer'],
|
||||
companies: ['view', 'create', 'edit', 'delete'],
|
||||
memberships: ['view', 'manage'],
|
||||
reservations: ['view', 'create', 'activate', 'cancel'],
|
||||
tenancies: ['view', 'manage', 'cancel'],
|
||||
admin: [
|
||||
'manage_users',
|
||||
'view_audit_log',
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
BERTH_SIDE_PONTOON_OPTIONS,
|
||||
toSelectOptions,
|
||||
} from '@/lib/constants';
|
||||
import { BerthReservationsTab } from './berth-reservations-tab';
|
||||
import { BerthTenanciesTab } from './berth-tenancies-tab';
|
||||
import { BerthInterestsTab } from './berth-interests-tab';
|
||||
import { BerthInterestPulse } from './berth-interest-pulse';
|
||||
import { BerthDocumentsTab } from './berth-documents-tab';
|
||||
@@ -432,9 +432,9 @@ export function buildBerthTabs(berth: BerthData): DetailTab[] {
|
||||
content: <BerthInterestsTab berthId={berth.id} />,
|
||||
},
|
||||
{
|
||||
id: 'reservations',
|
||||
label: 'Reservations',
|
||||
content: <BerthReservationsTab berthId={berth.id} />,
|
||||
id: 'tenancies',
|
||||
label: 'Tenancies',
|
||||
content: <BerthTenanciesTab berthId={berth.id} />,
|
||||
},
|
||||
{
|
||||
id: 'spec',
|
||||
|
||||
@@ -9,61 +9,61 @@ 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 { TenancyList, type TenancyRow } from '@/components/tenancies/tenancy-list';
|
||||
import { BerthReserveDialog } from '@/components/tenancies/berth-reserve-dialog';
|
||||
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
|
||||
interface BerthReservationsTabProps {
|
||||
interface BerthTenanciesTabProps {
|
||||
berthId: string;
|
||||
}
|
||||
|
||||
export function BerthReservationsTab({ berthId }: BerthReservationsTabProps) {
|
||||
export function BerthTenanciesTab({ berthId }: BerthTenanciesTabProps) {
|
||||
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'],
|
||||
const { data, isLoading } = useQuery<{ data: TenancyRow[]; pagination?: unknown }>({
|
||||
queryKey: ['berths', berthId, 'tenancies'],
|
||||
queryFn: () =>
|
||||
apiFetch(
|
||||
`/api/v1/berths/${berthId}/reservations?page=1&limit=50&order=desc&includeArchived=false`,
|
||||
`/api/v1/berths/${berthId}/tenancies?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']],
|
||||
'berth_tenancy:created': [['berths', berthId, 'tenancies']],
|
||||
'berth_tenancy:activated': [['berths', berthId, 'tenancies']],
|
||||
'berth_tenancy:ended': [['berths', berthId, 'tenancies']],
|
||||
'berth_tenancy:cancelled': [['berths', berthId, 'tenancies']],
|
||||
});
|
||||
|
||||
const reservations = data?.data ?? [];
|
||||
const active = reservations.find((r) => r.status === 'active');
|
||||
const history = reservations.filter((r) => r.status !== 'active');
|
||||
const tenancies = data?.data ?? [];
|
||||
const active = tenancies.find((r) => r.status === 'active');
|
||||
const history = tenancies.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">
|
||||
<h3 className="text-lg font-semibold">Tenancies</h3>
|
||||
<PermissionGate resource="tenancies" action="manage">
|
||||
<Button size="sm" onClick={() => setReserveOpen(true)}>
|
||||
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
|
||||
Reserve this berth
|
||||
Create tenancy
|
||||
</Button>
|
||||
</PermissionGate>
|
||||
</div>
|
||||
|
||||
{/* Active reservation card */}
|
||||
{/* Active tenancy card */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Active reservation</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">Active tenancy</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{active ? (
|
||||
<ReservationList reservations={[active]} portSlug={portSlug} />
|
||||
<TenancyList tenancies={[active]} portSlug={portSlug} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No active reservation.</p>
|
||||
<p className="text-sm text-muted-foreground">No active tenancy.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -77,9 +77,9 @@ export function BerthReservationsTab({ berthId }: BerthReservationsTabProps) {
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading…</p>
|
||||
) : history.length === 0 ? (
|
||||
<EmptyState title="No past reservations" description="Nothing here yet." />
|
||||
<EmptyState title="No past tenancies" description="Nothing here yet." />
|
||||
) : (
|
||||
<ReservationList reservations={history} portSlug={portSlug} />
|
||||
<TenancyList tenancies={history} portSlug={portSlug} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -26,7 +26,7 @@ interface PreflightItem {
|
||||
stakeLevel: 'low' | 'high';
|
||||
highStakesStage: string | null;
|
||||
blockers: string[];
|
||||
summary: { berths: number; yachts: number; reservations: number; signedDocs: number };
|
||||
summary: { berths: number; yachts: number; tenancies: number; signedDocs: number };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -215,8 +215,8 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
|
||||
{currentHighStakes.summary.signedDocs > 0
|
||||
? `${currentHighStakes.summary.signedDocs} signed doc(s), `
|
||||
: ''}
|
||||
{currentHighStakes.summary.reservations > 0
|
||||
? `${currentHighStakes.summary.reservations} reservation(s)`
|
||||
{currentHighStakes.summary.tenancies > 0
|
||||
? `${currentHighStakes.summary.tenancies} tenancy(ies)`
|
||||
: ''}
|
||||
</span>
|
||||
</WarningCallout>
|
||||
|
||||
@@ -64,7 +64,7 @@ interface ClientData {
|
||||
status: string;
|
||||
};
|
||||
}>;
|
||||
activeReservations: Array<{
|
||||
activeTenancies: Array<{
|
||||
id: string;
|
||||
berthId: string;
|
||||
yachtId: string;
|
||||
@@ -113,9 +113,9 @@ export function ClientDetail({ clientId, currentUserId }: ClientDetailProps) {
|
||||
'yacht:ownership_transferred': [['clients', clientId]],
|
||||
'company_membership:added': [['clients', clientId]],
|
||||
'company_membership:ended': [['clients', clientId]],
|
||||
'berth_reservation:activated': [['clients', clientId]],
|
||||
'berth_reservation:ended': [['clients', clientId]],
|
||||
'berth_reservation:cancelled': [['clients', clientId]],
|
||||
'berth_tenancy:activated': [['clients', clientId]],
|
||||
'berth_tenancy:ended': [['clients', clientId]],
|
||||
'berth_tenancy:cancelled': [['clients', clientId]],
|
||||
});
|
||||
|
||||
if (error && !isLoading) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ClientInterestsTab } from '@/components/clients/client-interests-tab';
|
||||
import { ClientPipelineSummary } from '@/components/clients/client-pipeline-summary';
|
||||
import { ClientYachtsTab } from '@/components/clients/client-yachts-tab';
|
||||
import { ClientCompaniesTab } from '@/components/clients/client-companies-tab';
|
||||
import { ClientReservationsTab } from '@/components/clients/client-reservations-tab';
|
||||
import { ClientTenanciesTab } from '@/components/clients/client-tenancies-tab';
|
||||
import { ClientFilesTab } from '@/components/clients/client-files-tab';
|
||||
import { ContactsEditor } from '@/components/clients/contacts-editor';
|
||||
import { AddressesEditor, type Address } from '@/components/shared/addresses-editor';
|
||||
@@ -123,7 +123,7 @@ interface ClientTabsOptions {
|
||||
status: string;
|
||||
};
|
||||
}>;
|
||||
activeReservations: Array<{
|
||||
activeTenancies: Array<{
|
||||
id: string;
|
||||
berthId: string;
|
||||
yachtId: string;
|
||||
@@ -276,12 +276,10 @@ export function getClientTabs({ clientId, currentUserId, client }: ClientTabsOpt
|
||||
content: <ClientCompaniesTab clientId={clientId} companies={client.companies} />,
|
||||
},
|
||||
{
|
||||
id: 'reservations',
|
||||
label: 'Reservations',
|
||||
badge: client.activeReservations.length,
|
||||
content: (
|
||||
<ClientReservationsTab clientId={clientId} activeReservations={client.activeReservations} />
|
||||
),
|
||||
id: 'tenancies',
|
||||
label: 'Tenancies',
|
||||
badge: client.activeTenancies.length,
|
||||
content: <ClientTenanciesTab clientId={clientId} activeTenancies={client.activeTenancies} />,
|
||||
},
|
||||
{
|
||||
id: 'addresses',
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { ReservationList, type ReservationRow } from '@/components/reservations/reservation-list';
|
||||
import { TenancyList, type TenancyRow } from '@/components/tenancies/tenancy-list';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
|
||||
interface ClientReservationsTabProps {
|
||||
interface ClientTenanciesTabProps {
|
||||
clientId: string;
|
||||
activeReservations: Array<{
|
||||
activeTenancies: Array<{
|
||||
id: string;
|
||||
berthId: string;
|
||||
yachtId: string;
|
||||
@@ -19,24 +19,21 @@ interface ClientReservationsTabProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ReservationListResponse {
|
||||
data: ReservationRow[];
|
||||
interface TenancyListResponse {
|
||||
data: TenancyRow[];
|
||||
pagination?: { total: number };
|
||||
}
|
||||
|
||||
export function ClientReservationsTab({
|
||||
clientId,
|
||||
activeReservations,
|
||||
}: ClientReservationsTabProps) {
|
||||
export function ClientTenanciesTab({ clientId, activeTenancies }: ClientTenanciesTabProps) {
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
|
||||
const activeRows: ReservationRow[] = activeReservations.map((r) => ({
|
||||
const activeRows: TenancyRow[] = activeTenancies.map((r) => ({
|
||||
id: r.id,
|
||||
berthId: r.berthId,
|
||||
portId: '',
|
||||
clientId,
|
||||
yachtId: r.yachtId,
|
||||
status: r.status as ReservationRow['status'],
|
||||
status: r.status as TenancyRow['status'],
|
||||
startDate: typeof r.startDate === 'string' ? r.startDate : r.startDate.toISOString(),
|
||||
endDate: null,
|
||||
tenureType: r.tenureType,
|
||||
@@ -48,23 +45,23 @@ export function ClientReservationsTab({
|
||||
// Lazy-load history (ended + cancelled). Two parallel queries because
|
||||
// the API takes one status at a time; combining once both resolve.
|
||||
const endedQuery = useQuery({
|
||||
queryKey: ['reservations', { clientId, status: 'ended' }],
|
||||
queryKey: ['tenancies', { clientId, status: 'ended' }],
|
||||
queryFn: () =>
|
||||
apiFetch<ReservationListResponse>(
|
||||
`/api/v1/berth-reservations?clientId=${encodeURIComponent(clientId)}&status=ended&pageSize=50`,
|
||||
apiFetch<TenancyListResponse>(
|
||||
`/api/v1/tenancies?clientId=${encodeURIComponent(clientId)}&status=ended&pageSize=50`,
|
||||
),
|
||||
enabled: showHistory,
|
||||
});
|
||||
const cancelledQuery = useQuery({
|
||||
queryKey: ['reservations', { clientId, status: 'cancelled' }],
|
||||
queryKey: ['tenancies', { clientId, status: 'cancelled' }],
|
||||
queryFn: () =>
|
||||
apiFetch<ReservationListResponse>(
|
||||
`/api/v1/berth-reservations?clientId=${encodeURIComponent(clientId)}&status=cancelled&pageSize=50`,
|
||||
apiFetch<TenancyListResponse>(
|
||||
`/api/v1/tenancies?clientId=${encodeURIComponent(clientId)}&status=cancelled&pageSize=50`,
|
||||
),
|
||||
enabled: showHistory,
|
||||
});
|
||||
|
||||
const historyRows: ReservationRow[] = [
|
||||
const historyRows: TenancyRow[] = [
|
||||
...(endedQuery.data?.data ?? []),
|
||||
...(cancelledQuery.data?.data ?? []),
|
||||
].sort((a, b) => (a.startDate < b.startDate ? 1 : -1));
|
||||
@@ -75,12 +72,12 @@ export function ClientReservationsTab({
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Active reservations</h3>
|
||||
<h3 className="text-sm font-medium">Active tenancies</h3>
|
||||
</div>
|
||||
<ReservationList
|
||||
reservations={activeRows}
|
||||
<TenancyList
|
||||
tenancies={activeRows}
|
||||
showBerth
|
||||
emptyMessage="This client has no active reservations."
|
||||
emptyMessage="This client has no active tenancies."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -100,15 +97,15 @@ export function ClientReservationsTab({
|
||||
isHistoryLoading ? (
|
||||
<p className="text-xs text-muted-foreground">Loading…</p>
|
||||
) : (
|
||||
<ReservationList
|
||||
reservations={historyRows}
|
||||
<TenancyList
|
||||
tenancies={historyRows}
|
||||
showBerth
|
||||
emptyMessage="No ended or cancelled reservations."
|
||||
emptyMessage="No ended or cancelled tenancies."
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click “Show history” to load ended and cancelled reservations.
|
||||
Click “Show history” to load ended and cancelled tenancies.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -47,8 +47,8 @@ interface DossierYacht {
|
||||
hullNumber: string | null;
|
||||
status: string;
|
||||
}
|
||||
interface DossierReservation {
|
||||
reservationId: string;
|
||||
interface DossierTenancy {
|
||||
tenancyId: string;
|
||||
berthId: string;
|
||||
mooringNumber: string;
|
||||
status: string;
|
||||
@@ -75,7 +75,7 @@ interface ArchiveDossier {
|
||||
berths: DossierBerth[];
|
||||
yachts: DossierYacht[];
|
||||
companies: Array<{ companyId: string; name: string; membershipRole: string | null }>;
|
||||
reservations: DossierReservation[];
|
||||
tenancies: DossierTenancy[];
|
||||
invoices: DossierInvoice[];
|
||||
documents: DossierDocument[];
|
||||
hasPortalUser: boolean;
|
||||
@@ -84,7 +84,7 @@ interface ArchiveDossier {
|
||||
|
||||
type BerthAction = 'release' | 'retain';
|
||||
type YachtAction = 'transfer' | 'mark_sold_away' | 'retain';
|
||||
type ReservationAction = 'cancel' | 'transfer';
|
||||
type TenancyAction = 'cancel' | 'transfer';
|
||||
type InvoiceAction = 'void' | 'write_off' | 'leave';
|
||||
type DocumentAction = 'void_documenso' | 'leave';
|
||||
|
||||
@@ -168,13 +168,9 @@ function SmartArchiveDialogBody({
|
||||
? Object.fromEntries(dossier.yachts.map((y) => [y.yachtId, 'retain' as YachtAction]))
|
||||
: {},
|
||||
);
|
||||
const [reservationDecisions, setReservationDecisions] = useState<
|
||||
Record<string, ReservationAction>
|
||||
>(() =>
|
||||
const [tenancyDecisions, setTenancyDecisions] = useState<Record<string, TenancyAction>>(() =>
|
||||
dossier
|
||||
? Object.fromEntries(
|
||||
dossier.reservations.map((r) => [r.reservationId, 'cancel' as ReservationAction]),
|
||||
)
|
||||
? Object.fromEntries(dossier.tenancies.map((r) => [r.tenancyId, 'cancel' as TenancyAction]))
|
||||
: {},
|
||||
);
|
||||
const [invoiceDecisions, setInvoiceDecisions] = useState<Record<string, InvoiceAction>>(() =>
|
||||
@@ -233,9 +229,9 @@ function SmartArchiveDialogBody({
|
||||
yachtId: y.yachtId,
|
||||
action: yachtDecisions[y.yachtId] ?? 'retain',
|
||||
})),
|
||||
reservationDecisions: dossier.reservations.map((r) => ({
|
||||
reservationId: r.reservationId,
|
||||
action: reservationDecisions[r.reservationId] ?? 'cancel',
|
||||
tenancyDecisions: dossier.tenancies.map((r) => ({
|
||||
tenancyId: r.tenancyId,
|
||||
action: tenancyDecisions[r.tenancyId] ?? 'cancel',
|
||||
})),
|
||||
invoiceDecisions: dossier.invoices.map((i) => ({
|
||||
invoiceId: i.invoiceId,
|
||||
@@ -459,33 +455,30 @@ function SmartArchiveDialogBody({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Reservations */}
|
||||
{dossier.reservations.length > 0 && (
|
||||
{/* Tenancies */}
|
||||
{dossier.tenancies.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Anchor className="h-4 w-4" aria-hidden /> Active reservations (
|
||||
{dossier.reservations.length})
|
||||
<Anchor className="h-4 w-4" aria-hidden /> Active tenancies (
|
||||
{dossier.tenancies.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{dossier.reservations.map((r) => (
|
||||
<div
|
||||
key={r.reservationId}
|
||||
className="flex items-center justify-between text-xs"
|
||||
>
|
||||
{dossier.tenancies.map((r) => (
|
||||
<div key={r.tenancyId} className="flex items-center justify-between text-xs">
|
||||
<span>Berth {r.mooringNumber}</span>
|
||||
<select
|
||||
className="rounded border bg-background px-2 py-1 text-xs"
|
||||
value={reservationDecisions[r.reservationId] ?? 'cancel'}
|
||||
value={tenancyDecisions[r.tenancyId] ?? 'cancel'}
|
||||
onChange={(e) =>
|
||||
setReservationDecisions((prev) => ({
|
||||
setTenancyDecisions((prev) => ({
|
||||
...prev,
|
||||
[r.reservationId]: e.target.value as ReservationAction,
|
||||
[r.tenancyId]: e.target.value as TenancyAction,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="cancel">Cancel reservation</option>
|
||||
<option value="cancel">Cancel tenancy</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -58,7 +58,7 @@ function humanizeFieldName(name: string): string {
|
||||
const ENTITY_TYPE_LABELS: Record<string, string> = {
|
||||
residential_client: 'Residential client',
|
||||
residential_interest: 'Residential interest',
|
||||
berth_reservation: 'Berth reservation',
|
||||
berth_tenancy: 'Berth tenancy',
|
||||
berth_maintenance_log: 'Berth maintenance',
|
||||
berth_recommendation: 'Berth recommendation',
|
||||
client_note: 'Client note',
|
||||
|
||||
@@ -45,7 +45,7 @@ const SIGNER_ROLES = ['client', 'sales', 'approver', 'developer', 'other'] as co
|
||||
|
||||
const SUBJECT_TYPES = [
|
||||
{ key: 'interest', label: 'Interest', field: 'interestId' as const },
|
||||
{ key: 'reservation', label: 'Reservation', field: 'reservationId' as const },
|
||||
{ key: 'tenancy', label: 'Tenancy', field: 'tenancyId' as const },
|
||||
{ key: 'client', label: 'Client', field: 'clientId' as const },
|
||||
{ key: 'company', label: 'Company', field: 'companyId' as const },
|
||||
{ key: 'yacht', label: 'Yacht', field: 'yachtId' as const },
|
||||
@@ -364,7 +364,7 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
|
||||
<Input
|
||||
value={subjectId}
|
||||
onChange={(e) => setSubjectId(e.target.value)}
|
||||
placeholder="Reservation id"
|
||||
placeholder="Tenancy id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ interface DetailDoc {
|
||||
documentType: string;
|
||||
documensoId: string | null;
|
||||
signedFileId: string | null;
|
||||
reservationId: string | null;
|
||||
tenancyId: string | null;
|
||||
interestId: string | null;
|
||||
clientId: string | null;
|
||||
yachtId: string | null;
|
||||
@@ -232,10 +232,10 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
|
||||
// render as a chip row; nothing renders when there's nothing to
|
||||
// link.
|
||||
const linkedRows: Array<{ href: string; label: string; sub: string | null }> = [];
|
||||
if (doc.reservationId) {
|
||||
if (doc.tenancyId) {
|
||||
linkedRows.push({
|
||||
href: `/${portSlug}/berth-reservations/${doc.reservationId}`,
|
||||
label: 'Reservation',
|
||||
href: `/${portSlug}/tenancies/${doc.tenancyId}`,
|
||||
label: 'Tenancy',
|
||||
sub: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ interface InterestData {
|
||||
reminderDays: number | null;
|
||||
reminderLastFired: string | null;
|
||||
/** Phase 2 risk-signal dates derived in getInterestById from event
|
||||
* tables (document_events, berth_reservations, conflicting won
|
||||
* tables (document_events, berth_tenancies, conflicting won
|
||||
* interests). Feed DealPulseChip; null when no matching event. */
|
||||
dateDocumentDeclined: string | null;
|
||||
dateReservationCancelled: string | null;
|
||||
|
||||
@@ -17,7 +17,7 @@ const navItems = [
|
||||
{ label: 'Dashboard', href: '/portal/dashboard', icon: LayoutDashboard },
|
||||
{ label: 'Interests', href: '/portal/interests', icon: Anchor },
|
||||
{ label: 'My Yachts', href: '/portal/my-yachts', icon: Sailboat },
|
||||
{ label: 'Reservations', href: '/portal/my-reservations', icon: CalendarCheck },
|
||||
{ label: 'Tenancies', href: '/portal/my-tenancies', icon: CalendarCheck },
|
||||
{ label: 'Documents', href: '/portal/documents', icon: FileText },
|
||||
{ label: 'Invoices', href: '/portal/invoices', icon: Receipt },
|
||||
{ label: 'Profile', href: '/portal/profile', icon: User },
|
||||
|
||||
@@ -102,7 +102,7 @@ export function BerthReserveDialog({ open, onOpenChange, berthId }: BerthReserve
|
||||
}
|
||||
|
||||
async function createPending(data: FormValues): Promise<{ id: string }> {
|
||||
const res = await apiFetch<{ data: { id: string } }>(`/api/v1/berths/${berthId}/reservations`, {
|
||||
const res = await apiFetch<{ data: { id: string } }>(`/api/v1/berths/${berthId}/tenancies`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
clientId: data.clientId!,
|
||||
@@ -122,9 +122,9 @@ export function BerthReserveDialog({ open, onOpenChange, berthId }: BerthReserve
|
||||
await createPending(data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['berths', berthId, 'reservations'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['berth-reservations'] });
|
||||
toast.success('Reservation created');
|
||||
queryClient.invalidateQueries({ queryKey: ['berths', berthId, 'tenancies'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tenancies'] });
|
||||
toast.success('Tenancy created');
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
@@ -139,22 +139,22 @@ export function BerthReserveDialog({ open, onOpenChange, berthId }: BerthReserve
|
||||
if (err) throw new Error(err);
|
||||
const pending = await createPending(data);
|
||||
// Immediately activate
|
||||
await apiFetch(`/api/v1/berth-reservations/${pending.id}`, {
|
||||
await apiFetch(`/api/v1/tenancies/${pending.id}`, {
|
||||
method: 'PATCH',
|
||||
body: { action: 'activate' },
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['berths', berthId, 'reservations'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['berth-reservations'] });
|
||||
toast.success('Reservation created and activated');
|
||||
queryClient.invalidateQueries({ queryKey: ['berths', berthId, 'tenancies'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tenancies'] });
|
||||
toast.success('Tenancy created and activated');
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : 'Failed to activate';
|
||||
if (/active reservation|conflict|409/i.test(msg)) {
|
||||
if (/active tenancy|active reservation|conflict|409/i.test(msg)) {
|
||||
setFormError(
|
||||
'This berth already has an active reservation. The pending record was created - activate it manually once the other reservation ends.',
|
||||
'This berth already has an active tenancy. The pending record was created - activate it manually once the other tenancy ends.',
|
||||
);
|
||||
} else {
|
||||
setFormError(msg);
|
||||
@@ -5,30 +5,30 @@ import { useParams } from 'next/navigation';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { PageHeader } from '@/components/shared/page-header';
|
||||
import { ReservationList, type ReservationRow } from '@/components/reservations/reservation-list';
|
||||
import { TenancyList, type TenancyRow } from '@/components/tenancies/tenancy-list';
|
||||
import { TableSkeleton } from '@/components/shared/loading-skeleton';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
|
||||
interface ReservationsApiResponse {
|
||||
data: ReservationRow[];
|
||||
interface TenanciesApiResponse {
|
||||
data: TenancyRow[];
|
||||
pagination: { total: number; page: number; pageSize: number };
|
||||
}
|
||||
|
||||
export function BerthReservationsList() {
|
||||
export function TenanciesListPage() {
|
||||
const params = useParams<{ portSlug: string }>();
|
||||
const portSlug = params?.portSlug ?? '';
|
||||
|
||||
const { data, isLoading } = useQuery<ReservationsApiResponse>({
|
||||
queryKey: ['berth-reservations', 'list'],
|
||||
queryFn: () => apiFetch('/api/v1/berth-reservations?page=1&limit=100&order=desc'),
|
||||
const { data, isLoading } = useQuery<TenanciesApiResponse>({
|
||||
queryKey: ['tenancies', 'list'],
|
||||
queryFn: () => apiFetch('/api/v1/tenancies?page=1&limit=100&order=desc'),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<PageHeader
|
||||
eyebrow="Marina"
|
||||
title="Berth Reservations"
|
||||
description="All reservations across all berths"
|
||||
title="Tenancies"
|
||||
description="All tenancies across all berths"
|
||||
actions={
|
||||
<Link
|
||||
href={`/${portSlug}/berths`}
|
||||
@@ -42,11 +42,11 @@ export function BerthReservationsList() {
|
||||
{isLoading ? (
|
||||
<TableSkeleton />
|
||||
) : (
|
||||
<ReservationList
|
||||
reservations={data?.data ?? []}
|
||||
<TenancyList
|
||||
tenancies={data?.data ?? []}
|
||||
showBerth
|
||||
portSlug={portSlug}
|
||||
emptyMessage="No reservations found."
|
||||
emptyMessage="No tenancies found."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -23,9 +23,9 @@ import { EmptyState } from '@/components/ui/empty-state';
|
||||
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { toastError } from '@/lib/api/toast-error';
|
||||
import { ClientLink, YachtLink, BerthLink } from '@/components/reservations/reservation-list';
|
||||
import { ClientLink, YachtLink, BerthLink } from '@/components/tenancies/tenancy-list';
|
||||
|
||||
interface ReservationDoc {
|
||||
interface TenancyDoc {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
@@ -34,7 +34,7 @@ interface ReservationDoc {
|
||||
signers: Array<{ id: string; status: string; signerName: string }>;
|
||||
}
|
||||
|
||||
interface ReservationData {
|
||||
interface TenancyData {
|
||||
id: string;
|
||||
status: string;
|
||||
startDate: string;
|
||||
@@ -47,7 +47,7 @@ interface ReservationData {
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
const RESERVATION_PILL: Record<string, StatusPillStatus> = {
|
||||
const TENANCY_PILL: Record<string, StatusPillStatus> = {
|
||||
pending: 'pending',
|
||||
active: 'active',
|
||||
ended: 'archived',
|
||||
@@ -58,13 +58,13 @@ function todayIso(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
interface EndReservationDialogProps {
|
||||
reservationId: string;
|
||||
interface EndTenancyDialogProps {
|
||||
tenancyId: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function EndReservationDialog({ reservationId, open, onOpenChange }: EndReservationDialogProps) {
|
||||
function EndTenancyDialog({ tenancyId, open, onOpenChange }: EndTenancyDialogProps) {
|
||||
const qc = useQueryClient();
|
||||
const [endDate, setEndDate] = useState(todayIso);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -73,12 +73,12 @@ function EndReservationDialog({ reservationId, open, onOpenChange }: EndReservat
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await apiFetch(`/api/v1/berth-reservations/${reservationId}`, {
|
||||
await apiFetch(`/api/v1/tenancies/${tenancyId}`, {
|
||||
method: 'PATCH',
|
||||
body: { action: 'end', endDate },
|
||||
});
|
||||
qc.invalidateQueries({ queryKey: ['reservation', reservationId] });
|
||||
toast.success('Reservation ended');
|
||||
qc.invalidateQueries({ queryKey: ['tenancy', tenancyId] });
|
||||
toast.success('Tenancy ended');
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
toastError(err);
|
||||
@@ -91,7 +91,7 @@ function EndReservationDialog({ reservationId, open, onOpenChange }: EndReservat
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>End reservation</DialogTitle>
|
||||
<DialogTitle>End tenancy</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4 pt-2">
|
||||
<div className="space-y-1.5">
|
||||
@@ -103,7 +103,7 @@ function EndReservationDialog({ reservationId, open, onOpenChange }: EndReservat
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="destructive" disabled={submitting}>
|
||||
{submitting ? 'Ending…' : 'End reservation'}
|
||||
{submitting ? 'Ending…' : 'End tenancy'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -112,50 +112,50 @@ function EndReservationDialog({ reservationId, open, onOpenChange }: EndReservat
|
||||
);
|
||||
}
|
||||
|
||||
interface ReservationDetailProps {
|
||||
reservationId: string;
|
||||
interface TenancyDetailProps {
|
||||
tenancyId: string;
|
||||
portSlug: string;
|
||||
}
|
||||
|
||||
export function ReservationDetail({ reservationId, portSlug }: ReservationDetailProps) {
|
||||
export function TenancyDetail({ tenancyId, portSlug }: TenancyDetailProps) {
|
||||
const [endDialogOpen, setEndDialogOpen] = useState(false);
|
||||
const reservation = useQuery<{ data: ReservationData }>({
|
||||
queryKey: ['reservation', reservationId],
|
||||
queryFn: () => apiFetch(`/api/v1/berth-reservations/${reservationId}`),
|
||||
const tenancy = useQuery<{ data: TenancyData }>({
|
||||
queryKey: ['tenancy', tenancyId],
|
||||
queryFn: () => apiFetch(`/api/v1/tenancies/${tenancyId}`),
|
||||
});
|
||||
|
||||
const documentsForRes = useQuery<{ data: ReservationDoc[] }>({
|
||||
queryKey: ['documents', 'by-reservation', reservationId],
|
||||
const documentsForTenancy = useQuery<{ data: TenancyDoc[] }>({
|
||||
queryKey: ['documents', 'by-tenancy', tenancyId],
|
||||
queryFn: () =>
|
||||
apiFetch(
|
||||
`/api/v1/documents?documentType=reservation_agreement&signatureOnly=true&limit=10`,
|
||||
).then((res) => {
|
||||
const r = res as { data: ReservationDoc[] & Array<{ reservationId?: string }> };
|
||||
const r = res as { data: TenancyDoc[] & Array<{ tenancyId?: string }> };
|
||||
return {
|
||||
data: r.data.filter(
|
||||
(d: ReservationDoc & { reservationId?: string }) => d.reservationId === reservationId,
|
||||
(d: TenancyDoc & { tenancyId?: string }) => d.tenancyId === tenancyId,
|
||||
),
|
||||
} as { data: ReservationDoc[] };
|
||||
} as { data: TenancyDoc[] };
|
||||
}),
|
||||
});
|
||||
|
||||
useRealtimeInvalidation({
|
||||
'document:created': [['documents', 'by-reservation', reservationId]],
|
||||
'document:created': [['documents', 'by-tenancy', tenancyId]],
|
||||
'document:completed': [
|
||||
['documents', 'by-reservation', reservationId],
|
||||
['reservation', reservationId],
|
||||
['documents', 'by-tenancy', tenancyId],
|
||||
['tenancy', tenancyId],
|
||||
],
|
||||
'document:cancelled': [['documents', 'by-reservation', reservationId]],
|
||||
'document:cancelled': [['documents', 'by-tenancy', tenancyId]],
|
||||
});
|
||||
|
||||
if (reservation.isLoading) {
|
||||
if (tenancy.isLoading) {
|
||||
return <div className="h-32 animate-pulse rounded-md bg-muted/40" />;
|
||||
}
|
||||
|
||||
if (reservation.error || !reservation.data) {
|
||||
if (tenancy.error || !tenancy.data) {
|
||||
return (
|
||||
<PageHeader
|
||||
title="Reservation not found"
|
||||
title="Tenancy not found"
|
||||
actions={
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/${portSlug}/berths`}>
|
||||
@@ -167,8 +167,8 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail
|
||||
);
|
||||
}
|
||||
|
||||
const res = reservation.data.data;
|
||||
const docs = documentsForRes.data?.data ?? [];
|
||||
const res = tenancy.data.data;
|
||||
const docs = documentsForTenancy.data?.data ?? [];
|
||||
const activeAgreement = docs.find((d) => ['sent', 'partially_signed'].includes(d.status));
|
||||
const completedAgreement = docs.find((d) => ['completed', 'signed'].includes(d.status));
|
||||
|
||||
@@ -199,9 +199,7 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Signed contract attached to this reservation.
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Signed contract attached to this tenancy.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -254,13 +252,13 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail
|
||||
return (
|
||||
<EmptyState
|
||||
icon={<FileSignature className="h-7 w-7" aria-hidden />}
|
||||
title="No reservation agreement yet"
|
||||
title="No tenancy agreement yet"
|
||||
body="Generate an agreement for the parties to sign before activation."
|
||||
actions={
|
||||
<Button asChild>
|
||||
<Link
|
||||
href={
|
||||
`/${portSlug}/documents/new?reservationId=${reservationId}&documentType=reservation_agreement` as Route
|
||||
`/${portSlug}/documents/new?tenancyId=${tenancyId}&documentType=reservation_agreement` as Route
|
||||
}
|
||||
>
|
||||
Generate agreement
|
||||
@@ -274,12 +272,12 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<PageHeader
|
||||
eyebrow="Berth reservation"
|
||||
title={`Reservation #${res.id.slice(0, 8)}`}
|
||||
eyebrow="Berth tenancy"
|
||||
title={`Tenancy #${res.id.slice(0, 8)}`}
|
||||
description={`${res.tenureType.replace(/_/g, ' ')} · ${new Date(res.startDate).toLocaleDateString(undefined)}${res.endDate ? ` → ${new Date(res.endDate).toLocaleDateString(undefined)}` : ''}`}
|
||||
kpiLine={
|
||||
<>
|
||||
<StatusPill status={RESERVATION_PILL[res.status] ?? 'pending'} withDot>
|
||||
<StatusPill status={TENANCY_PILL[res.status] ?? 'pending'} withDot>
|
||||
{res.status}
|
||||
</StatusPill>
|
||||
{res.contractFileId ? <span>Contract attached</span> : <span>No contract</span>}
|
||||
@@ -290,7 +288,7 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail
|
||||
{res.status === 'active' && (
|
||||
<Button variant="outline" size="sm" onClick={() => setEndDialogOpen(true)}>
|
||||
<StopCircle className="mr-1.5 h-4 w-4" aria-hidden />
|
||||
End reservation
|
||||
End tenancy
|
||||
</Button>
|
||||
)}
|
||||
<Button asChild variant="outline">
|
||||
@@ -307,7 +305,7 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail
|
||||
<div className="flex flex-col gap-4">
|
||||
<section className="rounded-md border bg-white p-4">
|
||||
<h2 className="mb-2 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Reservation details
|
||||
Tenancy details
|
||||
</h2>
|
||||
<dl className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
@@ -352,8 +350,8 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EndReservationDialog
|
||||
reservationId={reservationId}
|
||||
<EndTenancyDialog
|
||||
tenancyId={tenancyId}
|
||||
open={endDialogOpen}
|
||||
onOpenChange={setEndDialogOpen}
|
||||
/>
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { EmptyState } from '@/components/shared/empty-state';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
|
||||
export interface ReservationRow {
|
||||
export interface TenancyRow {
|
||||
id: string;
|
||||
berthId: string;
|
||||
portId: string;
|
||||
@@ -30,8 +30,8 @@ export interface ReservationRow {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ReservationListProps {
|
||||
reservations: ReservationRow[];
|
||||
export interface TenancyListProps {
|
||||
tenancies: TenancyRow[];
|
||||
showBerth?: boolean;
|
||||
portSlug?: string;
|
||||
emptyMessage?: string;
|
||||
@@ -106,8 +106,8 @@ export function BerthLink({ berthId, portSlug }: { berthId: string; portSlug: st
|
||||
/**
|
||||
* Renders a status badge with appropriate color coding.
|
||||
*/
|
||||
function StatusBadge({ status }: { status: ReservationRow['status'] }) {
|
||||
const colorMap: Record<ReservationRow['status'], string> = {
|
||||
function StatusBadge({ status }: { status: TenancyRow['status'] }) {
|
||||
const colorMap: Record<TenancyRow['status'], string> = {
|
||||
pending: 'bg-gray-100 text-gray-800',
|
||||
active: 'bg-green-100 text-green-800',
|
||||
ended: 'bg-blue-100 text-blue-800',
|
||||
@@ -145,19 +145,17 @@ function formatDateRange(startDate: string, endDate: string | null): string {
|
||||
return `${start} → ${end}`;
|
||||
}
|
||||
|
||||
export function ReservationList({
|
||||
reservations,
|
||||
export function TenancyList({
|
||||
tenancies,
|
||||
showBerth = false,
|
||||
portSlug: portSlugProp,
|
||||
emptyMessage,
|
||||
}: ReservationListProps) {
|
||||
}: TenancyListProps) {
|
||||
const routeParams = useParams<{ portSlug: string }>();
|
||||
const portSlug = portSlugProp ?? routeParams?.portSlug ?? '';
|
||||
|
||||
if (reservations.length === 0) {
|
||||
return (
|
||||
<EmptyState title="No reservations" description={emptyMessage ?? 'No reservations yet.'} />
|
||||
);
|
||||
if (tenancies.length === 0) {
|
||||
return <EmptyState title="No tenancies" description={emptyMessage ?? 'No tenancies yet.'} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -175,7 +173,7 @@ export function ReservationList({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{reservations.map((r) => (
|
||||
{tenancies.map((r) => (
|
||||
<TableRow key={r.id}>
|
||||
{showBerth && (
|
||||
<TableCell>
|
||||
@@ -9,7 +9,7 @@ import { FieldHistoryProvider, FieldHistoryIcon } from '@/components/shared/fiel
|
||||
import { InlineTagEditor } from '@/components/shared/inline-tag-editor';
|
||||
import { NotesList } from '@/components/shared/notes-list';
|
||||
import { EntityActivityFeed } from '@/components/shared/entity-activity-feed';
|
||||
import { ReservationList, type ReservationRow } from '@/components/reservations/reservation-list';
|
||||
import { TenancyList, type TenancyRow } from '@/components/tenancies/tenancy-list';
|
||||
import { RemindersInline } from '@/components/reminders/reminders-inline';
|
||||
import { YachtOwnershipHistory } from '@/components/yachts/yacht-ownership-history';
|
||||
import { feetToMeters, metersToFeet } from '@/components/yachts/yacht-dimensions';
|
||||
@@ -334,23 +334,23 @@ function YachtInterestsTab({ yachtId }: { yachtId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function YachtReservationsTab({ yachtId }: { yachtId: string }) {
|
||||
function YachtTenanciesTab({ 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`),
|
||||
const { data, isLoading } = useQuery<{ data: TenancyRow[] }>({
|
||||
queryKey: ['tenancies', 'by-yacht', yachtId],
|
||||
queryFn: () => apiFetch(`/api/v1/tenancies?yachtId=${yachtId}&limit=50&order=desc`),
|
||||
});
|
||||
|
||||
if (isLoading) return <p className="text-sm text-muted-foreground">Loading…</p>;
|
||||
|
||||
return (
|
||||
<ReservationList
|
||||
reservations={data?.data ?? []}
|
||||
<TenancyList
|
||||
tenancies={data?.data ?? []}
|
||||
showBerth
|
||||
portSlug={portSlug}
|
||||
emptyMessage="No reservations for this yacht."
|
||||
emptyMessage="No tenancies for this yacht."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -373,9 +373,9 @@ export function getYachtTabs({ yachtId, currentUserId, yacht }: YachtTabsOptions
|
||||
content: <YachtInterestsTab yachtId={yachtId} />,
|
||||
},
|
||||
{
|
||||
id: 'reservations',
|
||||
label: 'Reservations',
|
||||
content: <YachtReservationsTab yachtId={yachtId} />,
|
||||
id: 'tenancies',
|
||||
label: 'Tenancies',
|
||||
content: <YachtTenanciesTab yachtId={yachtId} />,
|
||||
},
|
||||
{
|
||||
id: 'notes',
|
||||
|
||||
Reference in New Issue
Block a user