186 lines
6.0 KiB
TypeScript
186 lines
6.0 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||
|
|
import { Pencil, Archive, RotateCcw, Mail, Phone } from 'lucide-react';
|
||
|
|
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Badge } from '@/components/ui/badge';
|
||
|
|
import { TagBadge } from '@/components/shared/tag-badge';
|
||
|
|
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
|
||
|
|
import { ClientForm } from '@/components/clients/client-form';
|
||
|
|
import { apiFetch } from '@/lib/api/client';
|
||
|
|
|
||
|
|
interface ClientDetailHeaderProps {
|
||
|
|
client: {
|
||
|
|
id: string;
|
||
|
|
fullName: string;
|
||
|
|
companyName?: string | null;
|
||
|
|
nationality?: string | null;
|
||
|
|
isProxy?: boolean;
|
||
|
|
proxyType?: string | null;
|
||
|
|
actualOwnerName?: string | null;
|
||
|
|
yachtName?: string | null;
|
||
|
|
berthSizeDesired?: string | null;
|
||
|
|
preferredContactMethod?: string | null;
|
||
|
|
preferredLanguage?: string | null;
|
||
|
|
timezone?: string | null;
|
||
|
|
source?: string | null;
|
||
|
|
sourceDetails?: string | null;
|
||
|
|
archivedAt?: string | null;
|
||
|
|
contacts?: Array<{ channel: string; value: string; isPrimary: boolean; label?: string | null }>;
|
||
|
|
tags?: Array<{ id: string; name: string; color: string }>;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const SOURCE_LABELS: Record<string, string> = {
|
||
|
|
website: 'Website',
|
||
|
|
manual: 'Manual',
|
||
|
|
referral: 'Referral',
|
||
|
|
broker: 'Broker',
|
||
|
|
};
|
||
|
|
|
||
|
|
export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
|
||
|
|
const queryClient = useQueryClient();
|
||
|
|
const [editOpen, setEditOpen] = useState(false);
|
||
|
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
||
|
|
|
||
|
|
const isArchived = !!client.archivedAt;
|
||
|
|
|
||
|
|
const archiveMutation = useMutation({
|
||
|
|
mutationFn: () =>
|
||
|
|
apiFetch(`/api/v1/clients/${client.id}`, { method: 'DELETE' }),
|
||
|
|
onSuccess: () => {
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['clients', client.id] });
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||
|
|
setArchiveOpen(false);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const restoreMutation = useMutation({
|
||
|
|
mutationFn: () =>
|
||
|
|
apiFetch(`/api/v1/clients/${client.id}/restore`, { method: 'POST' }),
|
||
|
|
onSuccess: () => {
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['clients', client.id] });
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['clients'] });
|
||
|
|
setArchiveOpen(false);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const primaryContact = client.contacts?.find((c) => c.isPrimary);
|
||
|
|
const primaryEmail = client.contacts?.find((c) => c.channel === 'email' && c.isPrimary)
|
||
|
|
?? client.contacts?.find((c) => c.channel === 'email');
|
||
|
|
const primaryPhone = client.contacts?.find((c) => c.channel === 'phone' && c.isPrimary)
|
||
|
|
?? client.contacts?.find((c) => c.channel === 'phone');
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<div className="space-y-3">
|
||
|
|
<div className="flex items-start gap-3 flex-wrap">
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<div className="flex items-center gap-2 flex-wrap">
|
||
|
|
<h1 className="text-2xl font-bold text-foreground truncate">
|
||
|
|
{client.fullName}
|
||
|
|
</h1>
|
||
|
|
{isArchived && (
|
||
|
|
<Badge variant="secondary" className="text-xs">Archived</Badge>
|
||
|
|
)}
|
||
|
|
{client.isProxy && (
|
||
|
|
<Badge variant="outline" className="text-xs capitalize">
|
||
|
|
Proxy {client.proxyType ? `(${client.proxyType.replace('_', ' ')})` : ''}
|
||
|
|
</Badge>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{client.companyName && (
|
||
|
|
<p className="text-muted-foreground mt-0.5">{client.companyName}</p>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex items-center gap-4 mt-2 flex-wrap text-sm text-muted-foreground">
|
||
|
|
{client.source && (
|
||
|
|
<span>
|
||
|
|
Source:{' '}
|
||
|
|
<span className="text-foreground">
|
||
|
|
{SOURCE_LABELS[client.source] ?? client.source}
|
||
|
|
</span>
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
{primaryEmail && (
|
||
|
|
<span className="flex items-center gap-1">
|
||
|
|
<Mail className="h-3.5 w-3.5" />
|
||
|
|
{primaryEmail.value}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
{primaryPhone && (
|
||
|
|
<span className="flex items-center gap-1">
|
||
|
|
<Phone className="h-3.5 w-3.5" />
|
||
|
|
{primaryPhone.value}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{client.tags && client.tags.length > 0 && (
|
||
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
||
|
|
{client.tags.map((tag) => (
|
||
|
|
<TagBadge key={tag.id} name={tag.name} color={tag.color} />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Actions */}
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => setEditOpen(true)}
|
||
|
|
>
|
||
|
|
<Pencil className="mr-1.5 h-3.5 w-3.5" />
|
||
|
|
Edit
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant={isArchived ? 'outline' : 'outline'}
|
||
|
|
size="sm"
|
||
|
|
onClick={() => setArchiveOpen(true)}
|
||
|
|
>
|
||
|
|
{isArchived ? (
|
||
|
|
<>
|
||
|
|
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
||
|
|
Restore
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Archive className="mr-1.5 h-3.5 w-3.5" />
|
||
|
|
Archive
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<ClientForm
|
||
|
|
open={editOpen}
|
||
|
|
onOpenChange={setEditOpen}
|
||
|
|
client={client as any}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<ArchiveConfirmDialog
|
||
|
|
open={archiveOpen}
|
||
|
|
onOpenChange={setArchiveOpen}
|
||
|
|
entityName={client.fullName}
|
||
|
|
entityType="Client"
|
||
|
|
isArchived={isArchived}
|
||
|
|
onConfirm={() => {
|
||
|
|
if (isArchived) {
|
||
|
|
restoreMutation.mutate();
|
||
|
|
} else {
|
||
|
|
archiveMutation.mutate();
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
isLoading={archiveMutation.isPending || restoreMutation.isPending}
|
||
|
|
/>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|