feat(uat-batch-14): InterestDocumentsTab rename, custom-field tooltip, yacht Transfer surface

- InterestDocumentsTab section "Legal documents" renamed to
  "Signature documents" so its scope is unambiguous. The section
  holds Documenso envelopes (EOI / Reservation / Contract); generic
  legal uploads belong in Attachments below.
- Custom-field admin form's "Sort Order" label now uses the
  FieldLabel primitive with an explainer tooltip ("Lower numbers
  render first... use to pin frequently-edited fields to the top").
  First adoption of the FieldLabel primitive shipped in PR4.2.
- Yacht Ownership History tab gains a "Transfer ownership" button:
  in the populated state as a header CTA (perm-gated by yachts.edit),
  in the empty state as the EmptyState action. Reuses the existing
  YachtTransferDialog from the header. Closes the "no way to enter/
  change" UX gap without duplicating the transfer logic.
- Verified the existing row-owner rendering already uses OwnerLink,
  so the row-click affordance was already in place.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 18:18:29 +02:00
parent 610154395a
commit 552b966903
3 changed files with 86 additions and 45 deletions

View File

@@ -7,6 +7,7 @@ import { Plus, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { FieldLabel } from '@/components/ui/field-label';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
@@ -297,7 +298,12 @@ function CustomFieldFormBody({ open, onOpenChange, field, onSuccess }: CustomFie
{/* Sort Order */}
<div className="space-y-1.5">
<Label htmlFor="cf-sort-order">Sort Order</Label>
<FieldLabel
htmlFor="cf-sort-order"
tooltip="Lower numbers render first when the field set is shown on entity detail pages. Use to pin frequently-edited fields to the top."
>
Sort Order
</FieldLabel>
<Input
id="cf-sort-order"
type="number"

View File

@@ -97,7 +97,7 @@ export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps)
<div className="space-y-8">
<section className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-muted-foreground">Legal documents</h3>
<h3 className="text-sm font-medium text-muted-foreground">Signature documents</h3>
<Button size="sm" variant="outline" onClick={() => setEoiDialogOpen(true)}>
Generate EOI
</Button>

View File

@@ -1,9 +1,11 @@
'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { Loader2, ArrowLeftRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
@@ -15,6 +17,8 @@ import {
import { Badge } from '@/components/ui/badge';
import { EmptyState } from '@/components/shared/empty-state';
import { OwnerLink } from '@/components/yachts/yacht-detail-header';
import { YachtTransferDialog } from '@/components/yachts/yacht-transfer-dialog';
import { PermissionGate } from '@/components/shared/permission-gate';
import { apiFetch } from '@/lib/api/client';
interface OwnershipHistoryRow {
@@ -52,6 +56,7 @@ function formatDate(value: string | null): string {
export function YachtOwnershipHistory({ yachtId }: YachtOwnershipHistoryProps) {
const params = useParams<{ portSlug: string }>();
const portSlug = params?.portSlug ?? '';
const [transferOpen, setTransferOpen] = useState(false);
const { data, isLoading } = useQuery<OwnershipHistoryRow[]>({
queryKey: ['yachts', yachtId, 'ownership-history'],
@@ -61,6 +66,11 @@ export function YachtOwnershipHistory({ yachtId }: YachtOwnershipHistoryProps) {
),
});
const currentOwnerRow = data?.find((r) => r.endDate === null);
const currentOwner = currentOwnerRow
? { type: currentOwnerRow.ownerType, id: currentOwnerRow.ownerId }
: undefined;
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
@@ -71,53 +81,78 @@ export function YachtOwnershipHistory({ yachtId }: YachtOwnershipHistoryProps) {
if (!data || data.length === 0) {
return (
<EmptyState
title="No ownership history"
description="This yacht's ownership transfers will appear here."
/>
<>
<EmptyState
title="No ownership history"
description="This yacht's ownership transfers will appear here."
action={{ label: 'Transfer ownership', onClick: () => setTransferOpen(true) }}
/>
<YachtTransferDialog
open={transferOpen}
onOpenChange={setTransferOpen}
yachtId={yachtId}
currentOwner={currentOwner}
/>
</>
);
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Start Date</TableHead>
<TableHead>End Date</TableHead>
<TableHead>Owner</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Notes</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row) => (
<TableRow key={row.id}>
<TableCell>{formatDate(row.startDate)}</TableCell>
<TableCell>
{row.endDate ? (
formatDate(row.endDate)
) : (
<Badge variant="secondary" className="text-xs">
Current
</Badge>
)}
</TableCell>
<TableCell>
<OwnerLink portSlug={portSlug} type={row.ownerType} id={row.ownerId} />
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{row.transferReason
? (REASON_LABELS[row.transferReason] ?? row.transferReason)
: '-'}
</TableCell>
<TableCell className="text-sm text-muted-foreground max-w-[320px] truncate">
{row.transferNotes ?? '-'}
</TableCell>
<div className="space-y-3">
<div className="flex items-center justify-end">
<PermissionGate resource="yachts" action="edit">
<Button size="sm" onClick={() => setTransferOpen(true)} className="gap-1.5">
<ArrowLeftRight className="size-4" aria-hidden />
Transfer ownership
</Button>
</PermissionGate>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Start Date</TableHead>
<TableHead>End Date</TableHead>
<TableHead>Owner</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Notes</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{data.map((row) => (
<TableRow key={row.id}>
<TableCell>{formatDate(row.startDate)}</TableCell>
<TableCell>
{row.endDate ? (
formatDate(row.endDate)
) : (
<Badge variant="secondary" className="text-xs">
Current
</Badge>
)}
</TableCell>
<TableCell>
<OwnerLink portSlug={portSlug} type={row.ownerType} id={row.ownerId} />
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{row.transferReason
? (REASON_LABELS[row.transferReason] ?? row.transferReason)
: '-'}
</TableCell>
<TableCell className="text-sm text-muted-foreground max-w-[320px] truncate">
{row.transferNotes ?? '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<YachtTransferDialog
open={transferOpen}
onOpenChange={setTransferOpen}
yachtId={yachtId}
currentOwner={currentOwner}
/>
</div>
);
}