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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { FieldLabel } from '@/components/ui/field-label';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { import {
Dialog, Dialog,
@@ -297,7 +298,12 @@ function CustomFieldFormBody({ open, onOpenChange, field, onSuccess }: CustomFie
{/* Sort Order */} {/* Sort Order */}
<div className="space-y-1.5"> <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 <Input
id="cf-sort-order" id="cf-sort-order"
type="number" type="number"

View File

@@ -97,7 +97,7 @@ export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps)
<div className="space-y-8"> <div className="space-y-8">
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center justify-between"> <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)}> <Button size="sm" variant="outline" onClick={() => setEoiDialogOpen(true)}>
Generate EOI Generate EOI
</Button> </Button>

View File

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