243 lines
8.2 KiB
TypeScript
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} />
|
|
);
|
|
}
|