256 lines
11 KiB
TypeScript
256 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useMemo, useState } from 'react';
|
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
import { Icons } from '@/components/icons';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { customerByIdOptions } from '../api/queries';
|
|
import type { CustomerReferenceData } from '../api/types';
|
|
import { CustomerFormSheet } from './customer-form-sheet';
|
|
import { CustomerContactsTab } from './customer-contacts-tab';
|
|
import { CustomerStatusBadge } from './customer-status-badge';
|
|
|
|
function FieldItem({ label, value }: { label: string; value: string | null | undefined }) {
|
|
return (
|
|
<div className='space-y-1'>
|
|
<div className='text-muted-foreground text-xs'>{label}</div>
|
|
<div className='text-sm'>{value || '-'}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function CustomerDetail({
|
|
customerId,
|
|
referenceData,
|
|
canUpdate,
|
|
canManageContacts
|
|
}: {
|
|
customerId: string;
|
|
referenceData: CustomerReferenceData;
|
|
canUpdate: boolean;
|
|
canManageContacts: {
|
|
create: boolean;
|
|
update: boolean;
|
|
delete: boolean;
|
|
};
|
|
}) {
|
|
const { data } = useSuspenseQuery(customerByIdOptions(customerId));
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
const branchMap = useMemo(
|
|
() => new Map(referenceData.branches.map((item) => [item.id, item.name])),
|
|
[referenceData]
|
|
);
|
|
const typeMap = useMemo(
|
|
() => new Map(referenceData.customerTypes.map((item) => [item.id, item])),
|
|
[referenceData]
|
|
);
|
|
const statusMap = useMemo(
|
|
() => new Map(referenceData.customerStatuses.map((item) => [item.id, item])),
|
|
[referenceData]
|
|
);
|
|
const leadMap = useMemo(
|
|
() => new Map(referenceData.leadChannels.map((item) => [item.id, item.label])),
|
|
[referenceData]
|
|
);
|
|
const groupMap = useMemo(
|
|
() => new Map(referenceData.customerGroups.map((item) => [item.id, item.label])),
|
|
[referenceData]
|
|
);
|
|
const customer = data.customer;
|
|
const status = statusMap.get(customer.customerStatus);
|
|
const type = typeMap.get(customer.customerType);
|
|
|
|
return (
|
|
<div className='space-y-6'>
|
|
<div className='flex flex-col gap-4 rounded-xl border p-5 lg:flex-row lg:items-start lg:justify-between'>
|
|
<div className='space-y-3'>
|
|
<div className='flex items-center gap-2'>
|
|
<Button asChild variant='ghost' size='icon'>
|
|
<Link href='/dashboard/crm/customers'>
|
|
<Icons.chevronLeft className='h-4 w-4' />
|
|
</Link>
|
|
</Button>
|
|
<span className='text-muted-foreground font-mono text-sm'>{customer.code}</span>
|
|
<CustomerStatusBadge code={status?.code} label={status?.label ?? 'Unknown'} />
|
|
{type ? <Badge variant='outline'>{type.label}</Badge> : null}
|
|
{customer.branchId ? (
|
|
<Badge variant='secondary'>
|
|
{branchMap.get(customer.branchId) ?? 'Unknown branch'}
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
<div>
|
|
<h2 className='text-2xl font-semibold'>{customer.name}</h2>
|
|
<p className='text-muted-foreground text-sm'>
|
|
{customer.abbr || customer.email || customer.phone || 'Customer detail record'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className='flex flex-wrap gap-2'>
|
|
{canUpdate ? (
|
|
<Button variant='outline' onClick={() => setEditOpen(true)}>
|
|
<Icons.edit className='mr-2 h-4 w-4' /> Edit Customer
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className='grid gap-6 lg:grid-cols-3'>
|
|
<div className='space-y-6 lg:col-span-2'>
|
|
<Card>
|
|
<CardContent className='pt-6'>
|
|
<Tabs defaultValue='overview' className='gap-4'>
|
|
<TabsList>
|
|
<TabsTrigger value='overview'>Overview</TabsTrigger>
|
|
<TabsTrigger value='contacts'>Contacts</TabsTrigger>
|
|
<TabsTrigger value='activity'>Activity</TabsTrigger>
|
|
<TabsTrigger value='related'>Related Documents</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value='overview'>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Overview</CardTitle>
|
|
<CardDescription>
|
|
Core account data, location, and CRM classification.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
|
<FieldItem label='Tax ID' value={customer.taxId} />
|
|
<FieldItem label='Phone' value={customer.phone} />
|
|
<FieldItem label='Email' value={customer.email} />
|
|
<FieldItem label='Website' value={customer.website} />
|
|
<FieldItem
|
|
label='Branch'
|
|
value={customer.branchId ? branchMap.get(customer.branchId) : null}
|
|
/>
|
|
<FieldItem
|
|
label='Lead Channel'
|
|
value={leadMap.get(customer.leadChannel ?? '')}
|
|
/>
|
|
<FieldItem
|
|
label='Customer Group'
|
|
value={groupMap.get(customer.customerGroup ?? '')}
|
|
/>
|
|
<FieldItem label='Active' value={customer.isActive ? 'Yes' : 'No'} />
|
|
<div className='md:col-span-2 xl:col-span-4'>
|
|
<FieldItem
|
|
label='Address'
|
|
value={
|
|
[
|
|
customer.address,
|
|
customer.subDistrict,
|
|
customer.district,
|
|
customer.province,
|
|
customer.postalCode,
|
|
customer.country
|
|
]
|
|
.filter(Boolean)
|
|
.join(', ') || null
|
|
}
|
|
/>
|
|
</div>
|
|
<div className='md:col-span-2 xl:col-span-4'>
|
|
<FieldItem label='Notes' value={customer.notes} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
<TabsContent value='contacts'>
|
|
<CustomerContactsTab
|
|
customerId={customerId}
|
|
canCreate={canManageContacts.create}
|
|
canUpdate={canManageContacts.update}
|
|
canDelete={canManageContacts.delete}
|
|
/>
|
|
</TabsContent>
|
|
<TabsContent value='activity'>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Activity</CardTitle>
|
|
<CardDescription>
|
|
Audit log entries captured from customer mutations.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='space-y-4'>
|
|
{!data.activity.length ? (
|
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
|
No activity recorded yet.
|
|
</div>
|
|
) : (
|
|
data.activity.map((item, index) => (
|
|
<div key={item.id} className='space-y-4'>
|
|
<div className='flex items-start gap-3'>
|
|
<div className='bg-primary mt-2 h-2 w-2 rounded-full' />
|
|
<div className='space-y-1'>
|
|
<div className='flex items-center gap-2'>
|
|
<Badge variant='outline' className='capitalize'>
|
|
{item.action}
|
|
</Badge>
|
|
<span className='text-sm font-medium'>
|
|
{item.actorName ?? item.userId}
|
|
</span>
|
|
</div>
|
|
<div className='text-muted-foreground text-xs'>
|
|
{new Date(item.createdAt).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{index < data.activity.length - 1 ? <Separator /> : null}
|
|
</div>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
<TabsContent value='related'>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Related Documents</CardTitle>
|
|
<CardDescription>
|
|
Task C stops at customer and contact scope, so downstream CRM documents stay
|
|
intentionally deferred.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center text-sm'>
|
|
Enquiry, quotation, and approval links will land here in Task D and later.
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className='space-y-6'>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Record Snapshot</CardTitle>
|
|
<CardDescription>Operational metadata for this customer row.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className='space-y-4'>
|
|
<FieldItem label='Created By' value={customer.createdBy} />
|
|
<FieldItem label='Updated By' value={customer.updatedBy} />
|
|
<FieldItem label='Created At' value={new Date(customer.createdAt).toLocaleString()} />
|
|
<FieldItem label='Updated At' value={new Date(customer.updatedAt).toLocaleString()} />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<CustomerFormSheet
|
|
customer={customer}
|
|
open={editOpen}
|
|
onOpenChange={setEditOpen}
|
|
referenceData={referenceData}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|