tsk-C
compleate
This commit is contained in:
255
src/features/crm/customers/components/customer-detail.tsx
Normal file
255
src/features/crm/customers/components/customer-detail.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user