From ccc775dc6656caaf71eddef7095fbf1452a94e1e Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 May 2026 15:09:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(tenancies-p2):=20rename=20berth=5Freservat?= =?UTF-8?q?ions=20=E2=86=92=20berth=5Ftenancies=20(schema=20+=20perms=20+?= =?UTF-8?q?=20UI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../berth-reservations/[id]/page.tsx | 10 -- .../[portSlug]/berth-reservations/page.tsx | 5 - .../[portSlug]/tenancies/[id]/page.tsx | 10 ++ .../(dashboard)/[portSlug]/tenancies/page.tsx | 5 + src/app/(portal)/portal/dashboard/page.tsx | 8 +- .../page.tsx | 20 +-- .../api/v1/berths/[id]/reservations/route.ts | 6 - .../{reservations => tenancies}/handlers.ts | 12 +- src/app/api/v1/berths/[id]/tenancies/route.ts | 6 + src/app/api/v1/clients/[id]/archive/route.ts | 4 +- .../clients/bulk-archive-preflight/route.ts | 6 +- src/app/api/v1/clients/bulk/route.ts | 4 +- .../[id]/handlers.ts | 19 +- .../[id]/route.ts | 4 +- .../handlers.ts | 10 +- .../route.ts | 2 +- src/components/admin/roles/role-form.tsx | 4 +- .../admin/users/user-permission-matrix.tsx | 4 +- src/components/berths/berth-tabs.tsx | 8 +- ...ations-tab.tsx => berth-tenancies-tab.tsx} | 46 ++--- .../clients/bulk-archive-wizard.tsx | 6 +- src/components/clients/client-detail.tsx | 8 +- src/components/clients/client-tabs.tsx | 14 +- ...tions-tab.tsx => client-tenancies-tab.tsx} | 49 +++-- .../clients/smart-archive-dialog.tsx | 45 ++--- src/components/dashboard/activity-feed.tsx | 2 +- .../documents/create-document-wizard.tsx | 4 +- src/components/documents/document-detail.tsx | 8 +- src/components/interests/interest-detail.tsx | 2 +- src/components/portal/portal-nav.tsx | 2 +- .../berth-reserve-dialog.tsx | 20 +-- .../tenancies-list-page.tsx} | 24 +-- .../tenancy-detail.tsx} | 86 +++++---- .../tenancy-list.tsx} | 24 ++- src/components/yachts/yacht-tabs.tsx | 22 +-- .../0085_rename_reservations_to_tenancies.sql | 129 +++++++++++++ src/lib/db/schema/documents.ts | 6 +- src/lib/db/schema/index.ts | 4 +- src/lib/db/schema/relations.ts | 32 ++-- .../schema/{reservations.ts => tenancies.ts} | 34 ++-- src/lib/db/schema/users.ts | 5 +- src/lib/db/seed-data.ts | 10 +- src/lib/db/seed-permissions.ts | 12 +- src/lib/db/seed-synthetic-data.ts | 6 +- src/lib/db/seed.ts | 2 +- src/lib/services/alert-rules.ts | 22 +-- ....service.ts => berth-tenancies.service.ts} | 170 +++++++++--------- .../client-archive-dossier.service.ts | 44 ++--- src/lib/services/client-archive.service.ts | 34 ++-- .../services/client-hard-delete.service.ts | 4 +- src/lib/services/client-merge.service.ts | 14 +- src/lib/services/clients.service.ts | 12 +- src/lib/services/dashboard.service.ts | 14 +- src/lib/services/documents.service.ts | 35 ++-- src/lib/services/gdpr-bundle-builder.ts | 6 +- src/lib/services/interests.service.ts | 14 +- src/lib/services/portal.service.ts | 48 ++--- .../services/reservation-agreement-context.ts | 10 +- src/lib/services/tenancies-module.service.ts | 10 +- src/lib/services/webhook-event-map.ts | 16 +- src/lib/socket/events.ts | 10 +- src/lib/validators/documents.ts | 7 +- .../{reservations.ts => tenancies.ts} | 12 +- .../02-client-smart-archive.spec.ts | 2 +- tests/global-setup.ts | 2 +- tests/helpers/factories.ts | 16 +- tests/integration/alerts-engine.test.ts | 10 +- ...t.test.ts => berth-tenancies-list.test.ts} | 52 +++--- ...reservations.test.ts => tenancies.test.ts} | 120 ++++++------- tests/integration/factories-smoke.test.ts | 6 +- tests/integration/permission-matrix.test.ts | 12 +- tests/integration/schema-constraints.test.ts | 8 +- ...ty.test.ts => tenancy-exclusivity.test.ts} | 13 +- ...ations.test.ts => berth-tenancies.test.ts} | 38 ++-- tests/unit/services/portal.test.ts | 32 ++-- tests/unit/validators.test.ts | 2 +- tests/unit/webhook-event-map.test.ts | 16 +- 77 files changed, 818 insertions(+), 742 deletions(-) delete mode 100644 src/app/(dashboard)/[portSlug]/berth-reservations/[id]/page.tsx delete mode 100644 src/app/(dashboard)/[portSlug]/berth-reservations/page.tsx create mode 100644 src/app/(dashboard)/[portSlug]/tenancies/[id]/page.tsx create mode 100644 src/app/(dashboard)/[portSlug]/tenancies/page.tsx rename src/app/(portal)/portal/{my-reservations => my-tenancies}/page.tsx (81%) delete mode 100644 src/app/api/v1/berths/[id]/reservations/route.ts rename src/app/api/v1/berths/[id]/{reservations => tenancies}/handlers.ts (80%) create mode 100644 src/app/api/v1/berths/[id]/tenancies/route.ts rename src/app/api/v1/{berth-reservations => tenancies}/[id]/handlers.ts (85%) rename src/app/api/v1/{berth-reservations => tenancies}/[id]/route.ts (68%) rename src/app/api/v1/{berth-reservations => tenancies}/handlers.ts (71%) rename src/app/api/v1/{berth-reservations => tenancies}/route.ts (56%) rename src/components/berths/{berth-reservations-tab.tsx => berth-tenancies-tab.tsx} (54%) rename src/components/clients/{client-reservations-tab.tsx => client-tenancies-tab.tsx} (64%) rename src/components/{reservations => tenancies}/berth-reserve-dialog.tsx (93%) rename src/components/{reservations/berth-reservations-list.tsx => tenancies/tenancies-list-page.tsx} (61%) rename src/components/{reservations/reservation-detail.tsx => tenancies/tenancy-detail.tsx} (81%) rename src/components/{reservations/reservation-list.tsx => tenancies/tenancy-list.tsx} (92%) create mode 100644 src/lib/db/migrations/0085_rename_reservations_to_tenancies.sql rename src/lib/db/schema/{reservations.ts => tenancies.ts} (66%) rename src/lib/services/{berth-reservations.service.ts => berth-tenancies.service.ts} (69%) rename src/lib/validators/{reservations.ts => tenancies.ts} (72%) rename tests/integration/api/{berth-reservations-list.test.ts => berth-tenancies-list.test.ts} (58%) rename tests/integration/api/{reservations.test.ts => tenancies.test.ts} (78%) rename tests/integration/{reservation-exclusivity.test.ts => tenancy-exclusivity.test.ts} (88%) rename tests/unit/services/{berth-reservations.test.ts => berth-tenancies.test.ts} (93%) diff --git a/src/app/(dashboard)/[portSlug]/berth-reservations/[id]/page.tsx b/src/app/(dashboard)/[portSlug]/berth-reservations/[id]/page.tsx deleted file mode 100644 index ad5a0024..00000000 --- a/src/app/(dashboard)/[portSlug]/berth-reservations/[id]/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ReservationDetail } from '@/components/reservations/reservation-detail'; - -interface PageProps { - params: Promise<{ portSlug: string; id: string }>; -} - -export default async function ReservationDetailPage({ params }: PageProps) { - const { portSlug, id } = await params; - return ; -} diff --git a/src/app/(dashboard)/[portSlug]/berth-reservations/page.tsx b/src/app/(dashboard)/[portSlug]/berth-reservations/page.tsx deleted file mode 100644 index 26acaa8b..00000000 --- a/src/app/(dashboard)/[portSlug]/berth-reservations/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { BerthReservationsList } from '@/components/reservations/berth-reservations-list'; - -export default function BerthReservationsPage() { - return ; -} diff --git a/src/app/(dashboard)/[portSlug]/tenancies/[id]/page.tsx b/src/app/(dashboard)/[portSlug]/tenancies/[id]/page.tsx new file mode 100644 index 00000000..bca8aff5 --- /dev/null +++ b/src/app/(dashboard)/[portSlug]/tenancies/[id]/page.tsx @@ -0,0 +1,10 @@ +import { TenancyDetail } from '@/components/tenancies/tenancy-detail'; + +interface PageProps { + params: Promise<{ portSlug: string; id: string }>; +} + +export default async function TenancyDetailPage({ params }: PageProps) { + const { portSlug, id } = await params; + return ; +} diff --git a/src/app/(dashboard)/[portSlug]/tenancies/page.tsx b/src/app/(dashboard)/[portSlug]/tenancies/page.tsx new file mode 100644 index 00000000..974e00d7 --- /dev/null +++ b/src/app/(dashboard)/[portSlug]/tenancies/page.tsx @@ -0,0 +1,5 @@ +import { TenanciesListPage } from '@/components/tenancies/tenancies-list-page'; + +export default function BerthTenanciesPage() { + return ; +} diff --git a/src/app/(portal)/portal/dashboard/page.tsx b/src/app/(portal)/portal/dashboard/page.tsx index bdc999f8..9579a7d7 100644 --- a/src/app/(portal)/portal/dashboard/page.tsx +++ b/src/app/(portal)/portal/dashboard/page.tsx @@ -59,11 +59,11 @@ export default async function PortalDashboardPage() { route). Hidden until a memberships page ships. The count is still available in the underlying dashboard data when needed. */} diff --git a/src/app/(portal)/portal/my-reservations/page.tsx b/src/app/(portal)/portal/my-tenancies/page.tsx similarity index 81% rename from src/app/(portal)/portal/my-reservations/page.tsx rename to src/app/(portal)/portal/my-tenancies/page.tsx index 6b643b51..13823ff2 100644 --- a/src/app/(portal)/portal/my-reservations/page.tsx +++ b/src/app/(portal)/portal/my-tenancies/page.tsx @@ -3,10 +3,10 @@ import { CalendarCheck } from 'lucide-react'; import type { Metadata } from 'next'; import { getPortalSession } from '@/lib/portal/auth'; -import { getPortalUserReservations } from '@/lib/services/portal.service'; +import { getPortalUserTenancies } from '@/lib/services/portal.service'; import { Badge } from '@/components/ui/badge'; -export const metadata: Metadata = { title: 'My Reservations' }; +export const metadata: Metadata = { title: 'My Tenancies' }; const STATUS_COLORS: Record = { pending: 'secondary', @@ -29,30 +29,30 @@ function formatDate(d: Date | string): string { }); } -export default async function PortalMyReservationsPage() { +export default async function PortalMyTenanciesPage() { const session = await getPortalSession(); if (!session) redirect('/portal/login'); - const reservations = await getPortalUserReservations(session.clientId, session.portId); + const tenancies = await getPortalUserTenancies(session.clientId, session.portId); return (
-

My Reservations

-

Your current and pending berth reservations

+

My Tenancies

+

Your current and pending berth tenancies

- {reservations.length === 0 ? ( + {tenancies.length === 0 ? (
-

No active reservations

+

No active tenancies

- Contact your port representative to discuss reservations. + Contact your port representative to discuss tenancies.

) : (
- {reservations.map((r) => ( + {tenancies.map((r) => (
diff --git a/src/app/api/v1/berths/[id]/reservations/route.ts b/src/app/api/v1/berths/[id]/reservations/route.ts deleted file mode 100644 index 4db6df8b..00000000 --- a/src/app/api/v1/berths/[id]/reservations/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { withAuth, withPermission } from '@/lib/api/helpers'; - -import { listHandler, createHandler } from './handlers'; - -export const GET = withAuth(withPermission('reservations', 'view', listHandler)); -export const POST = withAuth(withPermission('reservations', 'create', createHandler)); diff --git a/src/app/api/v1/berths/[id]/reservations/handlers.ts b/src/app/api/v1/berths/[id]/tenancies/handlers.ts similarity index 80% rename from src/app/api/v1/berths/[id]/reservations/handlers.ts rename to src/app/api/v1/berths/[id]/tenancies/handlers.ts index 0b4bbd92..7302ef9b 100644 --- a/src/app/api/v1/berths/[id]/reservations/handlers.ts +++ b/src/app/api/v1/berths/[id]/tenancies/handlers.ts @@ -6,8 +6,8 @@ import { parseBody, parseQuery } from '@/lib/api/route-helpers'; import { db } from '@/lib/db'; import { berths } from '@/lib/db/schema/berths'; import { NotFoundError, errorResponse } from '@/lib/errors'; -import { createPending, listReservations } from '@/lib/services/berth-reservations.service'; -import { createPendingSchema, listReservationsSchema } from '@/lib/validators/reservations'; +import { createPending, listTenancies } from '@/lib/services/berth-tenancies.service'; +import { createPendingSchema, listTenanciesSchema } from '@/lib/validators/tenancies'; // URL berthId is authoritative; make body berthId optional (ignored anyway). const createPendingBodySchema = createPendingSchema @@ -24,8 +24,8 @@ async function assertBerthInPort(berthId: string, portId: string): Promise export const listHandler: RouteHandler = async (req, ctx, params) => { try { await assertBerthInPort(params.id!, ctx.portId); - const query = parseQuery(req, listReservationsSchema); - const result = await listReservations(ctx.portId, { ...query, berthId: params.id! }); + const query = parseQuery(req, listTenanciesSchema); + const result = await listTenancies(ctx.portId, { ...query, berthId: params.id! }); const { page, limit } = query; const totalPages = Math.ceil(result.total / limit); return NextResponse.json({ @@ -48,7 +48,7 @@ export const createHandler: RouteHandler = async (req, ctx, params) => { try { await assertBerthInPort(params.id!, ctx.portId); const body = await parseBody(req, createPendingBodySchema); - const reservation = await createPending( + const tenancy = await createPending( ctx.portId, { ...body, berthId: params.id! }, { @@ -58,7 +58,7 @@ export const createHandler: RouteHandler = async (req, ctx, params) => { userAgent: ctx.userAgent, }, ); - return NextResponse.json({ data: reservation }, { status: 201 }); + return NextResponse.json({ data: tenancy }, { status: 201 }); } catch (error) { return errorResponse(error); } diff --git a/src/app/api/v1/berths/[id]/tenancies/route.ts b/src/app/api/v1/berths/[id]/tenancies/route.ts new file mode 100644 index 00000000..07d9fca1 --- /dev/null +++ b/src/app/api/v1/berths/[id]/tenancies/route.ts @@ -0,0 +1,6 @@ +import { withAuth, withPermission } from '@/lib/api/helpers'; + +import { listHandler, createHandler } from './handlers'; + +export const GET = withAuth(withPermission('tenancies', 'view', listHandler)); +export const POST = withAuth(withPermission('tenancies', 'manage', createHandler)); diff --git a/src/app/api/v1/clients/[id]/archive/route.ts b/src/app/api/v1/clients/[id]/archive/route.ts index ae9dcc01..b5cd36df 100644 --- a/src/app/api/v1/clients/[id]/archive/route.ts +++ b/src/app/api/v1/clients/[id]/archive/route.ts @@ -35,10 +35,10 @@ const decisionsSchema = z.object({ }), ) .default([]), - reservationDecisions: z + tenancyDecisions: z .array( z.object({ - reservationId: z.string().min(1), + tenancyId: z.string().min(1), action: z.enum(['cancel', 'transfer']), transferToClientId: z.string().min(1).optional(), }), diff --git a/src/app/api/v1/clients/bulk-archive-preflight/route.ts b/src/app/api/v1/clients/bulk-archive-preflight/route.ts index b06fdbe3..1e3da7b2 100644 --- a/src/app/api/v1/clients/bulk-archive-preflight/route.ts +++ b/src/app/api/v1/clients/bulk-archive-preflight/route.ts @@ -16,7 +16,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 }; } /** @@ -43,7 +43,7 @@ export const POST = withAuth( summary: { berths: d.berths.length, yachts: d.yachts.length, - reservations: d.reservations.length, + tenancies: d.tenancies.length, signedDocs: d.documents.filter( (doc) => doc.status === 'completed' || doc.status === 'signed', ).length, @@ -60,7 +60,7 @@ export const POST = withAuth( stakeLevel: 'low', highStakesStage: null, blockers: ['Could not load dossier - client may have been removed'], - summary: { berths: 0, yachts: 0, reservations: 0, signedDocs: 0 }, + summary: { berths: 0, yachts: 0, tenancies: 0, signedDocs: 0 }, }); } } diff --git a/src/app/api/v1/clients/bulk/route.ts b/src/app/api/v1/clients/bulk/route.ts index 1cbbf539..4344aead 100644 --- a/src/app/api/v1/clients/bulk/route.ts +++ b/src/app/api/v1/clients/bulk/route.ts @@ -135,8 +135,8 @@ export const POST = withAuth(async (req, ctx) => { acknowledgedSignedDocuments: hasSignedDocs, berthDecisions, yachtDecisions: dossier.yachts.map((y) => ({ yachtId: y.yachtId, action: 'retain' })), - reservationDecisions: dossier.reservations.map((r) => ({ - reservationId: r.reservationId, + tenancyDecisions: dossier.tenancies.map((r) => ({ + tenancyId: r.tenancyId, action: 'cancel', })), invoiceDecisions: dossier.invoices.map((i) => ({ diff --git a/src/app/api/v1/berth-reservations/[id]/handlers.ts b/src/app/api/v1/tenancies/[id]/handlers.ts similarity index 85% rename from src/app/api/v1/berth-reservations/[id]/handlers.ts rename to src/app/api/v1/tenancies/[id]/handlers.ts index 46d51a0a..571fbb7c 100644 --- a/src/app/api/v1/berth-reservations/[id]/handlers.ts +++ b/src/app/api/v1/tenancies/[id]/handlers.ts @@ -5,12 +5,7 @@ import { type RouteHandler } from '@/lib/api/helpers'; import { parseBody } from '@/lib/api/route-helpers'; import { requirePermission } from '@/lib/auth/permissions'; import { errorResponse } from '@/lib/errors'; -import { - activate, - cancel, - endReservation, - getById, -} from '@/lib/services/berth-reservations.service'; +import { activate, cancel, endTenancy, getById } from '@/lib/services/berth-tenancies.service'; // ─── PATCH body schema (action-based discriminated union) ──────────────────── @@ -35,8 +30,8 @@ const patchBodySchema = z.discriminatedUnion('action', [ export const getHandler: RouteHandler = async (_req, ctx, params) => { try { - const reservation = await getById(params.id!, ctx.portId); - return NextResponse.json({ data: reservation }); + const tenancy = await getById(params.id!, ctx.portId); + return NextResponse.json({ data: tenancy }); } catch (error) { return errorResponse(error); } @@ -53,7 +48,7 @@ export const patchHandler: RouteHandler = async (req, ctx, params) => { }; if (body.action === 'activate') { - requirePermission(ctx, 'reservations', 'activate'); + requirePermission(ctx, 'tenancies', 'manage'); const result = await activate( params.id!, ctx.portId, @@ -68,8 +63,8 @@ export const patchHandler: RouteHandler = async (req, ctx, params) => { if (body.action === 'end') { // `end` is lifecycle progression; same privilege as activate. - requirePermission(ctx, 'reservations', 'activate'); - const result = await endReservation( + requirePermission(ctx, 'tenancies', 'manage'); + const result = await endTenancy( params.id!, ctx.portId, { endDate: body.endDate, notes: body.notes }, @@ -79,7 +74,7 @@ export const patchHandler: RouteHandler = async (req, ctx, params) => { } // action === 'cancel' - requirePermission(ctx, 'reservations', 'cancel'); + requirePermission(ctx, 'tenancies', 'cancel'); const result = await cancel(params.id!, ctx.portId, { reason: body.reason }, meta); return NextResponse.json({ data: result }); } catch (error) { diff --git a/src/app/api/v1/berth-reservations/[id]/route.ts b/src/app/api/v1/tenancies/[id]/route.ts similarity index 68% rename from src/app/api/v1/berth-reservations/[id]/route.ts rename to src/app/api/v1/tenancies/[id]/route.ts index 32120f48..6d8b1c22 100644 --- a/src/app/api/v1/berth-reservations/[id]/route.ts +++ b/src/app/api/v1/tenancies/[id]/route.ts @@ -2,9 +2,9 @@ import { withAuth, withPermission } from '@/lib/api/helpers'; import { getHandler, patchHandler, deleteHandler } from './handlers'; -export const GET = withAuth(withPermission('reservations', 'view', getHandler)); +export const GET = withAuth(withPermission('tenancies', 'view', getHandler)); // PATCH cannot use `withPermission` wrapper - the required permission depends // on the `action` field in the body. `requirePermission` is called inside the // handler after the body is parsed. export const PATCH = withAuth(patchHandler); -export const DELETE = withAuth(withPermission('reservations', 'cancel', deleteHandler)); +export const DELETE = withAuth(withPermission('tenancies', 'cancel', deleteHandler)); diff --git a/src/app/api/v1/berth-reservations/handlers.ts b/src/app/api/v1/tenancies/handlers.ts similarity index 71% rename from src/app/api/v1/berth-reservations/handlers.ts rename to src/app/api/v1/tenancies/handlers.ts index 947cbe2e..52669993 100644 --- a/src/app/api/v1/berth-reservations/handlers.ts +++ b/src/app/api/v1/tenancies/handlers.ts @@ -3,19 +3,19 @@ import { NextResponse } from 'next/server'; import type { AuthContext } from '@/lib/api/helpers'; import { parseQuery } from '@/lib/api/route-helpers'; import { errorResponse } from '@/lib/errors'; -import { listReservations } from '@/lib/services/berth-reservations.service'; -import { listReservationsSchema } from '@/lib/validators/reservations'; +import { listTenancies } from '@/lib/services/berth-tenancies.service'; +import { listTenanciesSchema } from '@/lib/validators/tenancies'; /** - * Port-scoped global list of reservations across all berths. Inner handler + * Port-scoped global list of tenancies across all berths. Inner handler * lives here so it can be invoked directly from integration tests without * the `withAuth(withPermission(...))` wrappers (matches the convention * used throughout `src/app/api/v1/*`). */ export async function listHandler(req: Request, ctx: AuthContext): Promise { try { - const query = parseQuery(req as never, listReservationsSchema); - const result = await listReservations(ctx.portId, query); + const query = parseQuery(req as never, listTenanciesSchema); + const result = await listTenancies(ctx.portId, query); const { page, limit } = query; const totalPages = Math.ceil(result.total / limit); return NextResponse.json({ diff --git a/src/app/api/v1/berth-reservations/route.ts b/src/app/api/v1/tenancies/route.ts similarity index 56% rename from src/app/api/v1/berth-reservations/route.ts rename to src/app/api/v1/tenancies/route.ts index 2497266c..773118b8 100644 --- a/src/app/api/v1/berth-reservations/route.ts +++ b/src/app/api/v1/tenancies/route.ts @@ -1,4 +1,4 @@ import { withAuth, withPermission } from '@/lib/api/helpers'; import { listHandler } from './handlers'; -export const GET = withAuth(withPermission('reservations', 'view', listHandler)); +export const GET = withAuth(withPermission('tenancies', 'view', listHandler)); diff --git a/src/components/admin/roles/role-form.tsx b/src/components/admin/roles/role-form.tsx index 0b200a7b..361f1422 100644 --- a/src/components/admin/roles/role-form.tsx +++ b/src/components/admin/roles/role-form.tsx @@ -82,7 +82,7 @@ const DEFAULT_PERMISSIONS: Record> = { 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 = { yachts: 'Yachts', companies: 'Companies', memberships: 'Company Memberships', - reservations: 'Reservations', + tenancies: 'Tenancies', admin: 'Administration', residential_clients: 'Residential Clients', residential_interests: 'Residential Interests', diff --git a/src/components/admin/users/user-permission-matrix.tsx b/src/components/admin/users/user-permission-matrix.tsx index 2fe5ca32..e3ddeacf 100644 --- a/src/components/admin/users/user-permission-matrix.tsx +++ b/src/components/admin/users/user-permission-matrix.tsx @@ -46,7 +46,7 @@ const GROUP_LABELS: Record = { 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 = { 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', diff --git a/src/components/berths/berth-tabs.tsx b/src/components/berths/berth-tabs.tsx index 4a2e6967..ccdf3a1c 100644 --- a/src/components/berths/berth-tabs.tsx +++ b/src/components/berths/berth-tabs.tsx @@ -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: , }, { - id: 'reservations', - label: 'Reservations', - content: , + id: 'tenancies', + label: 'Tenancies', + content: , }, { id: 'spec', diff --git a/src/components/berths/berth-reservations-tab.tsx b/src/components/berths/berth-tenancies-tab.tsx similarity index 54% rename from src/components/berths/berth-reservations-tab.tsx rename to src/components/berths/berth-tenancies-tab.tsx index 91c9a63f..c8cb33fa 100644 --- a/src/components/berths/berth-reservations-tab.tsx +++ b/src/components/berths/berth-tenancies-tab.tsx @@ -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 (
-

Reservations

- +

Tenancies

+
- {/* Active reservation card */} + {/* Active tenancy card */} - Active reservation + Active tenancy {active ? ( - + ) : ( -

No active reservation.

+

No active tenancy.

)}
@@ -77,9 +77,9 @@ export function BerthReservationsTab({ berthId }: BerthReservationsTabProps) { {isLoading ? (

Loading…

) : history.length === 0 ? ( - + ) : ( - + )} diff --git a/src/components/clients/bulk-archive-wizard.tsx b/src/components/clients/bulk-archive-wizard.tsx index 814a170e..54ab030c 100644 --- a/src/components/clients/bulk-archive-wizard.tsx +++ b/src/components/clients/bulk-archive-wizard.tsx @@ -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)` : ''} diff --git a/src/components/clients/client-detail.tsx b/src/components/clients/client-detail.tsx index 37270ab3..50e10e5b 100644 --- a/src/components/clients/client-detail.tsx +++ b/src/components/clients/client-detail.tsx @@ -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) { diff --git a/src/components/clients/client-tabs.tsx b/src/components/clients/client-tabs.tsx index dccd672e..0e41ed7d 100644 --- a/src/components/clients/client-tabs.tsx +++ b/src/components/clients/client-tabs.tsx @@ -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: , }, { - id: 'reservations', - label: 'Reservations', - badge: client.activeReservations.length, - content: ( - - ), + id: 'tenancies', + label: 'Tenancies', + badge: client.activeTenancies.length, + content: , }, { id: 'addresses', diff --git a/src/components/clients/client-reservations-tab.tsx b/src/components/clients/client-tenancies-tab.tsx similarity index 64% rename from src/components/clients/client-reservations-tab.tsx rename to src/components/clients/client-tenancies-tab.tsx index d852f984..1811d9ef 100644 --- a/src/components/clients/client-reservations-tab.tsx +++ b/src/components/clients/client-tenancies-tab.tsx @@ -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( - `/api/v1/berth-reservations?clientId=${encodeURIComponent(clientId)}&status=ended&pageSize=50`, + apiFetch( + `/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( - `/api/v1/berth-reservations?clientId=${encodeURIComponent(clientId)}&status=cancelled&pageSize=50`, + apiFetch( + `/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({
-

Active reservations

+

Active tenancies

-
@@ -100,15 +97,15 @@ export function ClientReservationsTab({ isHistoryLoading ? (

Loading…

) : ( - ) ) : (

- Click “Show history” to load ended and cancelled reservations. + Click “Show history” to load ended and cancelled tenancies.

)}
diff --git a/src/components/clients/smart-archive-dialog.tsx b/src/components/clients/smart-archive-dialog.tsx index b73de3e7..cd98aea2 100644 --- a/src/components/clients/smart-archive-dialog.tsx +++ b/src/components/clients/smart-archive-dialog.tsx @@ -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 - >(() => + const [tenancyDecisions, setTenancyDecisions] = useState>(() => 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>(() => @@ -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({ )} - {/* Reservations */} - {dossier.reservations.length > 0 && ( + {/* Tenancies */} + {dossier.tenancies.length > 0 && ( - Active reservations ( - {dossier.reservations.length}) + Active tenancies ( + {dossier.tenancies.length}) - {dossier.reservations.map((r) => ( -
+ {dossier.tenancies.map((r) => ( +
Berth {r.mooringNumber}
))} diff --git a/src/components/dashboard/activity-feed.tsx b/src/components/dashboard/activity-feed.tsx index 912c68f1..261c9e32 100644 --- a/src/components/dashboard/activity-feed.tsx +++ b/src/components/dashboard/activity-feed.tsx @@ -58,7 +58,7 @@ function humanizeFieldName(name: string): string { const ENTITY_TYPE_LABELS: Record = { 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', diff --git a/src/components/documents/create-document-wizard.tsx b/src/components/documents/create-document-wizard.tsx index 826b444d..07c72778 100644 --- a/src/components/documents/create-document-wizard.tsx +++ b/src/components/documents/create-document-wizard.tsx @@ -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) { setSubjectId(e.target.value)} - placeholder="Reservation id" + placeholder="Tenancy id" /> )}
diff --git a/src/components/documents/document-detail.tsx b/src/components/documents/document-detail.tsx index a4f898ec..d0bbf2cb 100644 --- a/src/components/documents/document-detail.tsx +++ b/src/components/documents/document-detail.tsx @@ -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, }); } diff --git a/src/components/interests/interest-detail.tsx b/src/components/interests/interest-detail.tsx index 4e5acead..a00864a3 100644 --- a/src/components/interests/interest-detail.tsx +++ b/src/components/interests/interest-detail.tsx @@ -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; diff --git a/src/components/portal/portal-nav.tsx b/src/components/portal/portal-nav.tsx index 0c62e6fb..531d0494 100644 --- a/src/components/portal/portal-nav.tsx +++ b/src/components/portal/portal-nav.tsx @@ -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 }, diff --git a/src/components/reservations/berth-reserve-dialog.tsx b/src/components/tenancies/berth-reserve-dialog.tsx similarity index 93% rename from src/components/reservations/berth-reserve-dialog.tsx rename to src/components/tenancies/berth-reserve-dialog.tsx index ae9d783f..ce405208 100644 --- a/src/components/reservations/berth-reserve-dialog.tsx +++ b/src/components/tenancies/berth-reserve-dialog.tsx @@ -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); diff --git a/src/components/reservations/berth-reservations-list.tsx b/src/components/tenancies/tenancies-list-page.tsx similarity index 61% rename from src/components/reservations/berth-reservations-list.tsx rename to src/components/tenancies/tenancies-list-page.tsx index e9668e25..b208730d 100644 --- a/src/components/reservations/berth-reservations-list.tsx +++ b/src/components/tenancies/tenancies-list-page.tsx @@ -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({ - queryKey: ['berth-reservations', 'list'], - queryFn: () => apiFetch('/api/v1/berth-reservations?page=1&limit=100&order=desc'), + const { data, isLoading } = useQuery({ + queryKey: ['tenancies', 'list'], + queryFn: () => apiFetch('/api/v1/tenancies?page=1&limit=100&order=desc'), }); return (
) : ( - )}
diff --git a/src/components/reservations/reservation-detail.tsx b/src/components/tenancies/tenancy-detail.tsx similarity index 81% rename from src/components/reservations/reservation-detail.tsx rename to src/components/tenancies/tenancy-detail.tsx index af470e88..9eb0936b 100644 --- a/src/components/reservations/reservation-detail.tsx +++ b/src/components/tenancies/tenancy-detail.tsx @@ -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 = { +const TENANCY_PILL: Record = { 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 - End reservation + End tenancy
@@ -103,7 +103,7 @@ function EndReservationDialog({ reservationId, open, onOpenChange }: EndReservat Cancel @@ -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
; } - if (reservation.error || !reservation.data) { + if (tenancy.error || !tenancy.data) { return ( @@ -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
-

- Signed contract attached to this reservation. -

+

Signed contract attached to this tenancy.

); } @@ -254,13 +252,13 @@ export function ReservationDetail({ reservationId, portSlug }: ReservationDetail return ( } - title="No reservation agreement yet" + title="No tenancy agreement yet" body="Generate an agreement for the parties to sign before activation." actions={ )}