task-k.2
This commit is contained in:
242
src/features/crm/reports/components/aging-report-view.tsx
Normal file
242
src/features/crm/reports/components/aging-report-view.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
'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} />
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user