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:
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user