feat(ui): yacht transfer dialog with atomic ownership change

Replaces the Task 5.3 stub with a real YachtTransferDialog backed by
OwnerPicker, a date input, reason select, and notes textarea. Submits to
POST /api/v1/yachts/{id}/transfer, invalidates yacht + ownership-history
queries on success, and surfaces API errors (same-owner 400, cross-tenant
404, no-permission 403) as form-level messages. Transfer button is now
gated by PermissionGate resource="yachts" action="transfer".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-24 13:47:26 +02:00
parent f64a52b995
commit 508518b6c8
2 changed files with 221 additions and 41 deletions

View File

@@ -9,16 +9,10 @@ import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
import { PermissionGate } from '@/components/shared/permission-gate';
import { YachtForm } from '@/components/yachts/yacht-form';
import { YachtTransferDialog } from '@/components/yachts/yacht-transfer-dialog';
import { apiFetch } from '@/lib/api/client';
interface YachtDetailHeaderYacht {
@@ -174,15 +168,17 @@ export function YachtDetailHeader({ yacht }: YachtDetailHeaderProps) {
<Pencil className="mr-1.5 h-3.5 w-3.5" />
Edit
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setTransferOpen(true)}
disabled={isArchived}
>
<ArrowRightLeft className="mr-1.5 h-3.5 w-3.5" />
Transfer
</Button>
<PermissionGate resource="yachts" action="transfer">
<Button
variant="outline"
size="sm"
onClick={() => setTransferOpen(true)}
disabled={isArchived}
>
<ArrowRightLeft className="mr-1.5 h-3.5 w-3.5" />
Transfer
</Button>
</PermissionGate>
<Button
variant="outline"
size="sm"
@@ -234,30 +230,12 @@ export function YachtDetailHeader({ yacht }: YachtDetailHeaderProps) {
isLoading={archiveMutation.isPending}
/>
{/* TODO(Task 5.5): Replace with real YachtTransferDialog component. */}
<Dialog open={transferOpen} onOpenChange={setTransferOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Transfer Ownership</DialogTitle>
<DialogDescription>
The yacht ownership transfer flow will be implemented in Task 5.5.
</DialogDescription>
</DialogHeader>
<div className="py-2 text-sm text-muted-foreground">
This stub will be replaced with a form that lets you pick a new owner, effective date,
reason, and notes then calls{' '}
<code className="rounded bg-muted px-1 text-xs">
POST /api/v1/yachts/{'{id}'}/transfer
</code>
.
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setTransferOpen(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<YachtTransferDialog
open={transferOpen}
onOpenChange={setTransferOpen}
yachtId={yacht.id}
currentOwner={{ type: yacht.currentOwnerType, id: yacht.currentOwnerId }}
/>
</>
);
}