feat(tenancies-p6-followup): generic create dialog + edit dialog + self-FKs

- Migration 0086: berth_tenancies.previous_tenancy_id +
  transferred_from_tenancy_id self-FKs + partial indexes. Per
  docs/tenancies-design.md these chain renewal / transfer successors
  to predecessors for fixed-term and seasonal lineage. Schema mirrored
  in tenancies.ts with AnyPgColumn typed-import.
- POST /api/v1/tenancies (generic create): accepts berthId in the
  body so client + yacht tab entry points don't have to bounce through
  /api/v1/berths/[id]/tenancies. Same createPending service helper.
- TenancyCreateDialog: <TenancyCreateDialog open clientId? yachtId?
  berthId? /> with all three pickers; pre-fills the carrier from the
  parent entity. POSTs to /api/v1/tenancies; "Create" and
  "Create and activate" CTAs both wire to the new endpoint.
- Mounted on ClientTenanciesTab + YachtTenanciesTab behind
  <PermissionGate resource="tenancies" action="manage"> so reps can
  mint tenancies directly from those tabs without bouncing through
  the berth page.
- TenancyEditDialog: edit metadata only (start/end dates, tenure type,
  notes) via the new action='update' branch on the [id] PATCH route.
  Status transitions stay on activate/end/cancel. Wired into the
  tenancy detail page header. Outer wrapper unmounts on close so the
  form re-initialises from current row data without setState-in-effect.
- updateTenancy service helper + PATCH action='update' branch added.
  Audit-logged + emits berth_tenancy:activated to invalidate detail
  query caches.

Renew + Transfer dialogs deferred — both need lineage UX decisions
(tenure-aware mutate-in-place vs new-row spawn; client/yacht swap
semantics) and the self-FK columns this commit lands are the
underpinning. Next sub-task.

Verified: tsc clean, 1493/1493 vitest, migration applied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 17:10:06 +02:00
parent c4450dd852
commit 911b51a669
12 changed files with 689 additions and 13 deletions

View File

@@ -3,7 +3,11 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Plus } from 'lucide-react';
import { TenancyList, type TenancyRow } from '@/components/tenancies/tenancy-list';
import { TenancyCreateDialog } from '@/components/tenancies/tenancy-create-dialog';
import { PermissionGate } from '@/components/shared/permission-gate';
import { Button } from '@/components/ui/button';
import { apiFetch } from '@/lib/api/client';
@@ -26,6 +30,7 @@ interface TenancyListResponse {
export function ClientTenanciesTab({ clientId, activeTenancies }: ClientTenanciesTabProps) {
const [showHistory, setShowHistory] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const activeRows: TenancyRow[] = activeTenancies.map((r) => ({
id: r.id,
@@ -73,6 +78,12 @@ export function ClientTenanciesTab({ clientId, activeTenancies }: ClientTenancie
<div>
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">Active tenancies</h3>
<PermissionGate resource="tenancies" action="manage">
<Button size="sm" onClick={() => setCreateOpen(true)}>
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
Create tenancy
</Button>
</PermissionGate>
</div>
<TenancyList
tenancies={activeRows}
@@ -109,6 +120,8 @@ export function ClientTenanciesTab({ clientId, activeTenancies }: ClientTenancie
</p>
)}
</div>
<TenancyCreateDialog open={createOpen} onOpenChange={setCreateOpen} clientId={clientId} />
</div>
);
}