159 lines
4.6 KiB
TypeScript
159 lines
4.6 KiB
TypeScript
|
|
import { Badge, DataTable, DocumentShell, KeyValueGrid, Section } from '@/lib/pdf/brand-kit';
|
||
|
|
|
||
|
|
export interface ClientContact {
|
||
|
|
channel: string;
|
||
|
|
value: string;
|
||
|
|
label?: string | null;
|
||
|
|
isPrimary: boolean | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface YachtRow {
|
||
|
|
name: string;
|
||
|
|
lengthFt: string | null;
|
||
|
|
widthFt: string | null;
|
||
|
|
draftFt: string | null;
|
||
|
|
lengthM: string | null;
|
||
|
|
widthM: string | null;
|
||
|
|
draftM: string | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface InterestRow {
|
||
|
|
id: string;
|
||
|
|
pipelineStage: string | null;
|
||
|
|
leadCategory: string | null;
|
||
|
|
berthMooringNumber: string | null;
|
||
|
|
createdAt: Date | string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ActivityRow {
|
||
|
|
action: string;
|
||
|
|
entityType: string;
|
||
|
|
fieldChanged?: string | null;
|
||
|
|
createdAt: Date | string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ClientSummaryPdfProps {
|
||
|
|
portName: string;
|
||
|
|
logoBuffer: Buffer | null;
|
||
|
|
client: {
|
||
|
|
fullName: string | null;
|
||
|
|
nationality?: string | null;
|
||
|
|
source?: string | null;
|
||
|
|
createdAt: Date | string;
|
||
|
|
};
|
||
|
|
contacts: ClientContact[];
|
||
|
|
yachts: YachtRow[];
|
||
|
|
interests: InterestRow[];
|
||
|
|
activity: ActivityRow[];
|
||
|
|
}
|
||
|
|
|
||
|
|
function fmtDate(d: Date | string | null | undefined): string {
|
||
|
|
if (!d) return '—';
|
||
|
|
return new Date(d).toISOString().slice(0, 10);
|
||
|
|
}
|
||
|
|
|
||
|
|
function yachtDims(y: YachtRow): string {
|
||
|
|
const parts = [
|
||
|
|
y.lengthFt ? `${y.lengthFt}ft` : y.lengthM ? `${y.lengthM}m` : null,
|
||
|
|
y.widthFt ? `${y.widthFt}ft beam` : null,
|
||
|
|
y.draftFt ? `${y.draftFt}ft draft` : null,
|
||
|
|
].filter(Boolean);
|
||
|
|
return parts.length ? parts.join(' · ') : '—';
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ClientSummaryPdf({
|
||
|
|
portName,
|
||
|
|
logoBuffer,
|
||
|
|
client,
|
||
|
|
contacts,
|
||
|
|
yachts,
|
||
|
|
interests,
|
||
|
|
activity,
|
||
|
|
}: ClientSummaryPdfProps) {
|
||
|
|
const docMeta = `${client.fullName ?? 'Client'} · ${contacts.length} contact${
|
||
|
|
contacts.length === 1 ? '' : 's'
|
||
|
|
} · ${yachts.length} yacht${yachts.length === 1 ? '' : 's'} · ${interests.length} interest${
|
||
|
|
interests.length === 1 ? '' : 's'
|
||
|
|
}`;
|
||
|
|
return (
|
||
|
|
<DocumentShell
|
||
|
|
portName={portName}
|
||
|
|
docTitle="Client Summary"
|
||
|
|
docMeta={docMeta}
|
||
|
|
logoBuffer={logoBuffer}
|
||
|
|
>
|
||
|
|
<Section title="Client">
|
||
|
|
<KeyValueGrid
|
||
|
|
rows={[
|
||
|
|
{ label: 'Full name', value: client.fullName ?? '—' },
|
||
|
|
{ label: 'Nationality', value: client.nationality ?? '—' },
|
||
|
|
{ label: 'Source', value: client.source ?? '—' },
|
||
|
|
{ label: 'Added', value: fmtDate(client.createdAt) },
|
||
|
|
]}
|
||
|
|
/>
|
||
|
|
</Section>
|
||
|
|
|
||
|
|
<Section
|
||
|
|
title="Contacts"
|
||
|
|
subtitle={`${contacts.length} channel${contacts.length === 1 ? '' : 's'} on file.`}
|
||
|
|
>
|
||
|
|
<DataTable<ClientContact>
|
||
|
|
columns={[
|
||
|
|
{ header: 'Channel', flex: 1, render: (c) => c.channel },
|
||
|
|
{
|
||
|
|
header: 'Value',
|
||
|
|
flex: 3,
|
||
|
|
render: (c) => `${c.value}${c.label ? ` (${c.label})` : ''}`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
header: 'Primary',
|
||
|
|
flex: 1,
|
||
|
|
align: 'center',
|
||
|
|
render: (c) => (c.isPrimary ? <Badge text="Yes" tone="success" /> : '—'),
|
||
|
|
},
|
||
|
|
]}
|
||
|
|
rows={contacts}
|
||
|
|
emptyMessage="No contacts on file."
|
||
|
|
/>
|
||
|
|
</Section>
|
||
|
|
|
||
|
|
<Section title="Linked yachts">
|
||
|
|
<DataTable<YachtRow>
|
||
|
|
columns={[
|
||
|
|
{ header: 'Name', flex: 3, render: (y) => y.name },
|
||
|
|
{ header: 'Dimensions', flex: 4, render: (y) => yachtDims(y) },
|
||
|
|
]}
|
||
|
|
rows={yachts}
|
||
|
|
emptyMessage="No yachts linked to this client."
|
||
|
|
/>
|
||
|
|
</Section>
|
||
|
|
|
||
|
|
<Section title="Pipeline interests">
|
||
|
|
<DataTable<InterestRow>
|
||
|
|
columns={[
|
||
|
|
{ header: 'Stage', flex: 2, render: (i) => i.pipelineStage ?? 'open' },
|
||
|
|
{ header: 'Berth', flex: 1, render: (i) => i.berthMooringNumber ?? '—' },
|
||
|
|
{ header: 'Category', flex: 2, render: (i) => i.leadCategory ?? '—' },
|
||
|
|
{ header: 'Created', flex: 1.5, render: (i) => fmtDate(i.createdAt) },
|
||
|
|
]}
|
||
|
|
rows={interests}
|
||
|
|
emptyMessage="No pipeline interests on file."
|
||
|
|
/>
|
||
|
|
</Section>
|
||
|
|
|
||
|
|
<Section title="Recent activity" subtitle={`Last ${activity.length} events.`}>
|
||
|
|
<DataTable<ActivityRow>
|
||
|
|
columns={[
|
||
|
|
{ header: 'When', flex: 1.5, render: (a) => fmtDate(a.createdAt) },
|
||
|
|
{ header: 'Action', flex: 1.5, render: (a) => a.action },
|
||
|
|
{ header: 'Entity', flex: 1.5, render: (a) => a.entityType },
|
||
|
|
{ header: 'Field', flex: 1.5, render: (a) => a.fieldChanged ?? '—' },
|
||
|
|
]}
|
||
|
|
rows={activity}
|
||
|
|
emptyMessage="No recent activity."
|
||
|
|
/>
|
||
|
|
</Section>
|
||
|
|
</DocumentShell>
|
||
|
|
);
|
||
|
|
}
|