Files
alla-allaos-fullstack/src/features/crm/reports/components/aging-report-view.tsx
phaichayon 99a4087099 task-k.2
2026-06-22 21:26:43 +07:00

243 lines
8.2 KiB
TypeScript

'use client';
import { useMemo } from 'react';
import { parseAsString, useQueryStates } from 'nuqs';
import { useSuspenseQuery } from '@tanstack/react-query';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import type {
CrmEnquiryAgingReportResponse,
CrmLeadAgingReportResponse
} from '../api/types';
import {
crmEnquiryAgingReportQueryOptions,
crmLeadAgingReportQueryOptions,
crmReportFiltersQueryOptions
} from '../api/queries';
import { ReportFilterBar } from './report-filter-bar';
function formatNumber(value: number) {
return new Intl.NumberFormat('en-US').format(value);
}
function AgingReportLayout({
variant,
canExport,
data
}: {
variant: 'lead' | 'enquiry';
canExport: boolean;
data: CrmLeadAgingReportResponse | CrmEnquiryAgingReportResponse;
}) {
const { data: filterData } = useSuspenseQuery(crmReportFiltersQueryOptions());
const summaryCards =
variant === 'lead' && 'leadCount' in data.summary
? [
['Lead Count', formatNumber(data.summary.leadCount)],
['Average Aging', `${data.summary.averageAgingDays} days`],
['Maximum Aging', `${data.summary.maximumAgingDays} days`]
]
: [
[
'Open Enquiries',
formatNumber('openEnquiries' in data.summary ? data.summary.openEnquiries : 0)
],
['Average Aging', `${data.summary.averageAgingDays} days`],
['Maximum Aging', `${data.summary.maximumAgingDays} days`]
];
return (
<div className='space-y-6'>
<ReportFilterBar
filterMetadata={filterData.filters}
filterKeys={
variant === 'lead'
? ['dateFrom', 'dateTo', 'branch', 'productType', 'customerOwner', 'leadSource']
: ['dateFrom', 'dateTo', 'branch', 'productType', 'sales']
}
canExport={canExport}
reportCode={variant === 'lead' ? 'lead_aging' : 'enquiry_aging'}
pageLinks={[
{ href: '/dashboard/crm/reports/pipeline', label: 'Pipeline Suite' },
{ href: '/dashboard/crm/reports/lead-aging', label: 'Lead Aging', active: variant === 'lead' },
{ href: '/dashboard/crm/reports/enquiry-aging', label: 'Enquiry Aging', active: variant === 'enquiry' }
]}
/>
<div className='grid gap-4 md:grid-cols-3'>
{summaryCards.map(([label, value]) => (
<Card key={String(label)}>
<CardHeader className='pb-2'>
<CardDescription>{label}</CardDescription>
</CardHeader>
<CardContent>
<div className='text-2xl font-semibold'>{value}</div>
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle>{variant === 'lead' ? 'Lead Aging Buckets' : 'Enquiry Aging Buckets'}</CardTitle>
<CardDescription>Bucketed view to spot stagnant records quickly.</CardDescription>
</CardHeader>
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
{data.buckets.map((bucket) => (
<div key={bucket.key} className='rounded-lg border p-4'>
<div className='text-muted-foreground text-sm'>{bucket.label}</div>
<div className='mt-2 text-2xl font-semibold'>{formatNumber(bucket.count)}</div>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{variant === 'lead' ? 'Lead Drill-down' : 'Enquiry Drill-down'}</CardTitle>
<CardDescription>
{variant === 'lead'
? 'Open leads sorted from oldest to newest.'
: 'Open enquiries with latest follow-up visibility.'}
</CardDescription>
</CardHeader>
<CardContent>
<div className='overflow-x-auto rounded-lg border'>
<Table>
<TableHeader>
<TableRow>
{variant === 'lead' ? (
<>
<TableHead>Lead Number</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Assigned User</TableHead>
<TableHead>Created Date</TableHead>
<TableHead>Aging Days</TableHead>
</>
) : (
<>
<TableHead>Enquiry No</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Salesman</TableHead>
<TableHead>Last Follow-Up</TableHead>
<TableHead>Aging Days</TableHead>
</>
)}
</TableRow>
</TableHeader>
<TableBody>
{data.rows.length ? (
data.rows.map((row) => (
<TableRow key={'leadId' in row ? row.leadId : row.enquiryId}>
{'leadId' in row ? (
<>
<TableCell>{row.leadCode}</TableCell>
<TableCell>{row.customerName}</TableCell>
<TableCell>{row.assignedUserName ?? '-'}</TableCell>
<TableCell>{new Date(row.createdDate).toLocaleDateString('en-CA')}</TableCell>
<TableCell>{formatNumber(row.agingDays)}</TableCell>
</>
) : (
<>
<TableCell>{row.enquiryCode}</TableCell>
<TableCell>{row.customerName}</TableCell>
<TableCell>{row.salesmanName ?? '-'}</TableCell>
<TableCell>
{row.lastFollowupDate
? new Date(row.lastFollowupDate).toLocaleDateString('en-CA')
: '-'}
</TableCell>
<TableCell>{formatNumber(row.agingDays)}</TableCell>
</>
)}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className='text-muted-foreground text-center'>
No records found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
}
function LeadAgingReportInner({ canExport }: { canExport: boolean }) {
const [params] = useQueryStates({
dateFrom: parseAsString,
dateTo: parseAsString,
branch: parseAsString,
productType: parseAsString,
customerOwner: parseAsString,
leadSource: parseAsString
});
const filters = useMemo(
() => ({
dateFrom: params.dateFrom,
dateTo: params.dateTo,
branch: params.branch,
productType: params.productType,
sales: null,
customerOwner: params.customerOwner,
leadSource: params.leadSource
}),
[params]
);
const { data } = useSuspenseQuery(crmLeadAgingReportQueryOptions(filters));
return <AgingReportLayout variant='lead' canExport={canExport} data={data} />;
}
function EnquiryAgingReportInner({ canExport }: { canExport: boolean }) {
const [params] = useQueryStates({
dateFrom: parseAsString,
dateTo: parseAsString,
branch: parseAsString,
productType: parseAsString,
sales: parseAsString
});
const filters = useMemo(
() => ({
dateFrom: params.dateFrom,
dateTo: params.dateTo,
branch: params.branch,
productType: params.productType,
sales: params.sales,
customerOwner: null,
leadSource: null
}),
[params]
);
const { data } = useSuspenseQuery(crmEnquiryAgingReportQueryOptions(filters));
return <AgingReportLayout variant='enquiry' canExport={canExport} data={data} />;
}
export function AgingReportView({
variant,
canExport
}: {
variant: 'lead' | 'enquiry';
canExport: boolean;
}) {
return variant === 'lead' ? (
<LeadAgingReportInner canExport={canExport} />
) : (
<EnquiryAgingReportInner canExport={canExport} />
);
}