Files
alla-allaos-fullstack/src/features/crm/customers/components/customer-detail.tsx
phaichayon 8148850fda tsk-C
compleate
2026-06-15 13:20:39 +07:00

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>
);
}