Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
209 lines
5.9 KiB
TypeScript
209 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import type { DetailTab } from '@/components/shared/detail-layout';
|
|
import { NotesList } from '@/components/shared/notes-list';
|
|
|
|
interface ClientTabsOptions {
|
|
clientId: string;
|
|
currentUserId?: string;
|
|
client: {
|
|
fullName: string;
|
|
companyName?: string | null;
|
|
nationality?: string | null;
|
|
isProxy?: boolean;
|
|
proxyType?: string | null;
|
|
actualOwnerName?: string | null;
|
|
yachtName?: string | null;
|
|
yachtLengthFt?: string | null;
|
|
yachtWidthFt?: string | null;
|
|
yachtDraftFt?: string | null;
|
|
berthSizeDesired?: string | null;
|
|
preferredContactMethod?: string | null;
|
|
preferredLanguage?: string | null;
|
|
timezone?: string | null;
|
|
source?: string | null;
|
|
sourceDetails?: string | null;
|
|
contacts?: Array<{
|
|
id: string;
|
|
channel: string;
|
|
value: string;
|
|
label?: string | null;
|
|
isPrimary: boolean;
|
|
}>;
|
|
};
|
|
}
|
|
|
|
function InfoRow({ label, value }: { label: string; value?: string | null }) {
|
|
if (!value) return null;
|
|
return (
|
|
<div className="flex gap-2 py-1.5 border-b last:border-0">
|
|
<dt className="w-40 shrink-0 text-sm text-muted-foreground">{label}</dt>
|
|
<dd className="text-sm">{value}</dd>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OverviewTab({ client }: { client: ClientTabsOptions['client'] }) {
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Personal Info */}
|
|
<div className="space-y-1">
|
|
<h3 className="text-sm font-medium mb-2">Personal Information</h3>
|
|
<dl>
|
|
<InfoRow label="Full Name" value={client.fullName} />
|
|
<InfoRow label="Company" value={client.companyName} />
|
|
<InfoRow label="Nationality" value={client.nationality} />
|
|
<InfoRow label="Preferred Language" value={client.preferredLanguage} />
|
|
<InfoRow label="Timezone" value={client.timezone} />
|
|
<InfoRow
|
|
label="Preferred Contact"
|
|
value={client.preferredContactMethod}
|
|
/>
|
|
</dl>
|
|
</div>
|
|
|
|
{/* Contacts */}
|
|
<div className="space-y-1">
|
|
<h3 className="text-sm font-medium mb-2">Contact Details</h3>
|
|
{client.contacts && client.contacts.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{client.contacts.map((c) => (
|
|
<div
|
|
key={c.id}
|
|
className="flex items-center gap-2 p-2 rounded-lg border bg-muted/30 text-sm"
|
|
>
|
|
<span className="capitalize text-muted-foreground w-20 shrink-0">
|
|
{c.channel}
|
|
</span>
|
|
<span className="flex-1">{c.value}</span>
|
|
{c.label && (
|
|
<span className="text-xs text-muted-foreground capitalize">
|
|
{c.label}
|
|
</span>
|
|
)}
|
|
{c.isPrimary && (
|
|
<span className="text-xs font-medium text-primary">Primary</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">No contacts added</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Yacht Details */}
|
|
{(client.yachtName ||
|
|
client.yachtLengthFt ||
|
|
client.berthSizeDesired) && (
|
|
<div className="space-y-1">
|
|
<h3 className="text-sm font-medium mb-2">Yacht Details</h3>
|
|
<dl>
|
|
<InfoRow label="Yacht Name" value={client.yachtName} />
|
|
<InfoRow
|
|
label="Length"
|
|
value={
|
|
client.yachtLengthFt
|
|
? `${client.yachtLengthFt} ft`
|
|
: undefined
|
|
}
|
|
/>
|
|
<InfoRow
|
|
label="Width"
|
|
value={
|
|
client.yachtWidthFt ? `${client.yachtWidthFt} ft` : undefined
|
|
}
|
|
/>
|
|
<InfoRow
|
|
label="Draft"
|
|
value={
|
|
client.yachtDraftFt
|
|
? `${client.yachtDraftFt} ft`
|
|
: undefined
|
|
}
|
|
/>
|
|
<InfoRow label="Berth Size Desired" value={client.berthSizeDesired} />
|
|
</dl>
|
|
</div>
|
|
)}
|
|
|
|
{/* Source */}
|
|
{(client.source || client.sourceDetails) && (
|
|
<div className="space-y-1">
|
|
<h3 className="text-sm font-medium mb-2">Source</h3>
|
|
<dl>
|
|
<InfoRow label="Source" value={client.source} />
|
|
<InfoRow label="Source Details" value={client.sourceDetails} />
|
|
</dl>
|
|
</div>
|
|
)}
|
|
|
|
{/* Proxy Info */}
|
|
{client.isProxy && (
|
|
<div className="space-y-1">
|
|
<h3 className="text-sm font-medium mb-2">Proxy Information</h3>
|
|
<dl>
|
|
<InfoRow
|
|
label="Proxy Type"
|
|
value={client.proxyType?.replace('_', ' ')}
|
|
/>
|
|
<InfoRow label="Actual Owner" value={client.actualOwnerName} />
|
|
</dl>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function getClientTabs({
|
|
clientId,
|
|
currentUserId,
|
|
client,
|
|
}: ClientTabsOptions): DetailTab[] {
|
|
return [
|
|
{
|
|
id: 'overview',
|
|
label: 'Overview',
|
|
content: <OverviewTab client={client} />,
|
|
},
|
|
{
|
|
id: 'interests',
|
|
label: 'Interests',
|
|
content: (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p>Interests will appear here once created.</p>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'notes',
|
|
label: 'Notes',
|
|
content: (
|
|
<NotesList
|
|
entityType="clients"
|
|
entityId={clientId}
|
|
currentUserId={currentUserId}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'files',
|
|
label: 'Files',
|
|
content: (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p>File attachments coming soon.</p>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'activity',
|
|
label: 'Activity',
|
|
content: (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p>Activity log coming soon.</p>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
}
|