Revert "add uploaded Form16 PDF APIs and bulk download support"
This reverts commit d25ffbaf7b.
This commit is contained in:
parent
d25ffbaf7b
commit
d01e248a35
978
package-lock.json
generated
978
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -39,7 +39,6 @@
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.7.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bullmq": "^5.63.0",
|
||||
@ -79,7 +78,6 @@
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/cors": "^2.8.17",
|
||||
|
||||
@ -199,58 +199,6 @@ export class Form16Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/form16/uploaded-form16-pdfs
|
||||
* RE only. List uploaded Form16A PDFs that are successfully submitted and have credit notes.
|
||||
*/
|
||||
async listUploadedForm16Pdfs(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await form16Service.listUploadedForm16Pdfs({
|
||||
search: req.query.search as string | undefined,
|
||||
dealerCode: req.query.dealerCode as string | undefined,
|
||||
dealerName: req.query.dealerName as string | undefined,
|
||||
financialYear: req.query.financialYear as string | undefined,
|
||||
assessmentYear: req.query.assessmentYear as string | undefined,
|
||||
quarter: req.query.quarter as string | undefined,
|
||||
uploadedFrom: req.query.uploadedFrom as string | undefined,
|
||||
uploadedTo: req.query.uploadedTo as string | undefined,
|
||||
zone: req.query.zone as string | undefined,
|
||||
region: req.query.region as string | undefined,
|
||||
limit: req.query.limit != null ? parseInt(String(req.query.limit), 10) : undefined,
|
||||
offset: req.query.offset != null ? parseInt(String(req.query.offset), 10) : undefined,
|
||||
});
|
||||
return ResponseHandler.success(res, result, 'Uploaded Form 16 PDFs fetched');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('[Form16Controller] listUploadedForm16Pdfs error:', error);
|
||||
return ResponseHandler.error(res, 'Failed to fetch uploaded Form 16 PDFs', 500, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/form16/uploaded-form16-pdfs/bulk-download-links
|
||||
* RE only. Return document links for selected submission ids (used by FE bulk download action).
|
||||
*/
|
||||
async getUploadedForm16PdfBulkDownloadLinks(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const idsRaw = Array.isArray((req.body as any)?.submissionIds) ? (req.body as any).submissionIds : [];
|
||||
const requested = idsRaw
|
||||
.map((v: unknown) => Number(v))
|
||||
.filter((n: number) => Number.isInteger(n) && n > 0);
|
||||
if (!requested.length) {
|
||||
return ResponseHandler.error(res, 'submissionIds is required', 400);
|
||||
}
|
||||
|
||||
const files = await form16Service.getUploadedForm16PdfLinksBySubmissionIds(requested);
|
||||
|
||||
return ResponseHandler.success(res, { files }, 'Bulk download links generated');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
logger.error('[Form16Controller] getUploadedForm16PdfBulkDownloadLinks error:', error);
|
||||
return ResponseHandler.error(res, 'Failed to prepare bulk download links', 500, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/form16/dealer/submissions
|
||||
* Dealer only. List Form 16 submissions for the authenticated dealer (pending/failed for Pending Submissions page).
|
||||
@ -400,11 +348,6 @@ export class Form16Controller {
|
||||
async get26asDashboard(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const dashboard = await form16Service.getForm16DashboardData();
|
||||
logger.info('[Form16Controller] 26AS dashboard served', {
|
||||
yearRows: dashboard.yearWise?.length || 0,
|
||||
zoneRows: dashboard.zoneWise?.length || 0,
|
||||
zoneHierarchy: dashboard.zoneHierarchy?.length || 0,
|
||||
});
|
||||
return ResponseHandler.success(res, dashboard, 'Form16A dashboard fetched');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
import { QueryInterface } from 'sequelize';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface: QueryInterface) => {
|
||||
// Speeds latest-submission window ordering per dealer+FY+quarter.
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_form16a_submissions_dashboard_latest
|
||||
ON form16a_submissions (
|
||||
dealer_code,
|
||||
financial_year,
|
||||
quarter,
|
||||
version DESC,
|
||||
submitted_date DESC,
|
||||
created_at DESC,
|
||||
id DESC
|
||||
);
|
||||
`);
|
||||
|
||||
// Speeds join between latest submissions and 26AS quarter snapshots.
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_form16a_submissions_tan_fy_quarter
|
||||
ON form16a_submissions (tan_number, financial_year, quarter);
|
||||
`);
|
||||
|
||||
// Speeds normalized active dealer lookup used by dashboard aggregation.
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_dealers_active_normalized_code_region
|
||||
ON dealers (
|
||||
(TRIM(COALESCE(NULLIF(sales_code, ''), NULLIF(dlrcode, '')))),
|
||||
region
|
||||
)
|
||||
WHERE is_active = true;
|
||||
`);
|
||||
},
|
||||
|
||||
down: async (queryInterface: QueryInterface) => {
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP INDEX IF EXISTS idx_dealers_active_normalized_code_region;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP INDEX IF EXISTS idx_form16a_submissions_tan_fy_quarter;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP INDEX IF EXISTS idx_form16a_submissions_dashboard_latest;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
@ -108,18 +108,6 @@ router.get(
|
||||
requireForm16SubmissionAccess,
|
||||
asyncHandler(form16Controller.listDebitNotes.bind(form16Controller))
|
||||
);
|
||||
router.get(
|
||||
'/uploaded-form16-pdfs',
|
||||
requireForm16ReOnly,
|
||||
requireForm16SubmissionAccess,
|
||||
asyncHandler(form16Controller.listUploadedForm16Pdfs.bind(form16Controller))
|
||||
);
|
||||
router.post(
|
||||
'/uploaded-form16-pdfs/bulk-download-links',
|
||||
requireForm16ReOnly,
|
||||
requireForm16SubmissionAccess,
|
||||
asyncHandler(form16Controller.getUploadedForm16PdfBulkDownloadLinks.bind(form16Controller))
|
||||
);
|
||||
router.get(
|
||||
'/debit-notes/:id/sap-response',
|
||||
requireForm16ReOnly,
|
||||
|
||||
@ -1836,21 +1836,11 @@ export async function listDealerPendingQuarters(userId: string) {
|
||||
`SELECT financial_year, quarter, MIN(created_at) AS min_created FROM tds_26as_entries GROUP BY financial_year, quarter`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
));
|
||||
const twentySixAsAmounts = (await sequelize.query<{ financial_year: string; quarter: string; total_amount: number | string }>(
|
||||
`SELECT financial_year, quarter, COALESCE(SUM(tax_deducted), 0) AS total_amount FROM tds_26as_entries GROUP BY financial_year, quarter`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
));
|
||||
const minDateByKey = new Map<string, Date>();
|
||||
for (const row of twentySixAsMinDates) {
|
||||
const key = `${row.financial_year}|${row.quarter}`;
|
||||
if (row.min_created) minDateByKey.set(key, new Date(row.min_created));
|
||||
}
|
||||
const amountByKey = new Map<string, number>();
|
||||
for (const row of twentySixAsAmounts) {
|
||||
const key = `${row.financial_year}|${row.quarter}`;
|
||||
const amount = Number(row.total_amount ?? 0);
|
||||
amountByKey.set(key, Number.isFinite(amount) ? amount : 0);
|
||||
}
|
||||
|
||||
const dealerSubmissions = await Form16aSubmission.findAll({
|
||||
where: { dealerCode },
|
||||
@ -1887,7 +1877,6 @@ export async function listDealerPendingQuarters(userId: string) {
|
||||
days_remaining: number | null;
|
||||
days_overdue: number | null;
|
||||
days_since_26as_uploaded: number | null;
|
||||
twenty_six_as_amount: number;
|
||||
}> = [];
|
||||
const now = new Date();
|
||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||
@ -1912,7 +1901,6 @@ export async function listDealerPendingQuarters(userId: string) {
|
||||
const daysSince26AsUploaded = twentySixAsMin
|
||||
? Math.floor((now.getTime() - twentySixAsMin.getTime()) / oneDayMs)
|
||||
: null;
|
||||
const twentySixAsAmount = amountByKey.get(key) ?? 0;
|
||||
result.push({
|
||||
financial_year: financialYear,
|
||||
quarter,
|
||||
@ -1925,7 +1913,6 @@ export async function listDealerPendingQuarters(userId: string) {
|
||||
days_remaining: daysRemaining,
|
||||
days_overdue: daysOverdue,
|
||||
days_since_26as_uploaded: daysSince26AsUploaded,
|
||||
twenty_six_as_amount: twentySixAsAmount,
|
||||
});
|
||||
}
|
||||
result.sort((a, b) => {
|
||||
@ -1938,282 +1925,6 @@ export async function listDealerPendingQuarters(userId: string) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface Form16UploadedPdfFilters {
|
||||
search?: string;
|
||||
dealerCode?: string;
|
||||
dealerName?: string;
|
||||
financialYear?: string;
|
||||
assessmentYear?: string;
|
||||
quarter?: string;
|
||||
uploadedFrom?: string;
|
||||
uploadedTo?: string;
|
||||
zone?: string;
|
||||
region?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface Form16UploadedPdfRow {
|
||||
submissionId: number;
|
||||
requestId: string;
|
||||
creditNoteNumber: string;
|
||||
dealerName: string;
|
||||
dealerCode: string;
|
||||
region: string;
|
||||
zone: string;
|
||||
financialYear: string;
|
||||
assessmentYear: string;
|
||||
quarter: string;
|
||||
amount: number;
|
||||
uploadedDate: string | null;
|
||||
documentUrl: string;
|
||||
}
|
||||
|
||||
type Form16UploadedPdfSqlRow = {
|
||||
submission_id: number;
|
||||
request_id: string;
|
||||
credit_note_number: string;
|
||||
dealer_name: string | null;
|
||||
dealer_code: string;
|
||||
region: string | null;
|
||||
zone: string | null;
|
||||
financial_year: string;
|
||||
assessment_year: string | null;
|
||||
quarter: string;
|
||||
amount: string | number;
|
||||
uploaded_date: Date | string | null;
|
||||
document_url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* RE only. List uploaded Form16A PDFs that were successfully processed and have credit notes.
|
||||
* Includes dealer + geography columns and supports filter/search for management views.
|
||||
*/
|
||||
export async function listUploadedForm16Pdfs(filters?: Form16UploadedPdfFilters): Promise<{
|
||||
rows: Form16UploadedPdfRow[];
|
||||
total: number;
|
||||
}> {
|
||||
const replacements: Record<string, unknown> = {
|
||||
limit: Math.min(Math.max(Number(filters?.limit || 50), 1), 500),
|
||||
offset: Math.max(Number(filters?.offset || 0), 0),
|
||||
};
|
||||
const where: string[] = [
|
||||
`s.validation_status = 'success'`,
|
||||
`COALESCE(TRIM(s.document_url), '') <> ''`,
|
||||
];
|
||||
|
||||
const search = String(filters?.search || '').trim().toLowerCase();
|
||||
if (search) {
|
||||
replacements.search = `%${search}%`;
|
||||
where.push(
|
||||
`(
|
||||
LOWER(COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code)) LIKE :search
|
||||
OR LOWER(COALESCE(s.dealer_code, '')) LIKE :search
|
||||
OR LOWER(COALESCE(s.financial_year, '')) LIKE :search
|
||||
OR LOWER(COALESCE(s.quarter, '')) LIKE :search
|
||||
OR LOWER(COALESCE(c.credit_note_number, '')) LIKE :search
|
||||
)`
|
||||
);
|
||||
}
|
||||
|
||||
const dealerCode = String(filters?.dealerCode || '').trim();
|
||||
if (dealerCode) {
|
||||
replacements.dealerCode = `%${dealerCode.toLowerCase()}%`;
|
||||
where.push(`LOWER(COALESCE(s.dealer_code, '')) LIKE :dealerCode`);
|
||||
}
|
||||
|
||||
const dealerName = String(filters?.dealerName || '').trim();
|
||||
if (dealerName) {
|
||||
replacements.dealerName = `%${dealerName.toLowerCase()}%`;
|
||||
where.push(`LOWER(COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code)) LIKE :dealerName`);
|
||||
}
|
||||
|
||||
const financialYear = normalizeFinancialYear(filters?.financialYear || '') || String(filters?.financialYear || '').trim();
|
||||
if (financialYear) {
|
||||
replacements.financialYear = financialYear;
|
||||
where.push(`s.financial_year = :financialYear`);
|
||||
}
|
||||
|
||||
const quarter = normalizeQuarter(filters?.quarter || '') || String(filters?.quarter || '').trim().toUpperCase();
|
||||
if (quarter) {
|
||||
replacements.quarter = quarter;
|
||||
where.push(`s.quarter = :quarter`);
|
||||
}
|
||||
|
||||
const region = String(filters?.region || '').trim();
|
||||
if (region) {
|
||||
replacements.region = `%${region.toLowerCase()}%`;
|
||||
where.push(`LOWER(COALESCE(d.region, 'UNKNOWN')) LIKE :region`);
|
||||
}
|
||||
|
||||
const zone = String(filters?.zone || '').trim();
|
||||
if (zone) {
|
||||
replacements.zone = zone.toUpperCase();
|
||||
where.push(
|
||||
`(CASE
|
||||
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) IN ('N','S','E','W','C')
|
||||
THEN UPPER(LEFT(d.region, 1))
|
||||
ELSE 'UNKNOWN'
|
||||
END) = :zone`
|
||||
);
|
||||
}
|
||||
|
||||
const uploadedFrom = String(filters?.uploadedFrom || '').trim();
|
||||
if (uploadedFrom) {
|
||||
replacements.uploadedFrom = uploadedFrom;
|
||||
where.push(`DATE(COALESCE(s.submitted_date, s.created_at)) >= :uploadedFrom`);
|
||||
}
|
||||
|
||||
const uploadedTo = String(filters?.uploadedTo || '').trim();
|
||||
if (uploadedTo) {
|
||||
replacements.uploadedTo = uploadedTo;
|
||||
where.push(`DATE(COALESCE(s.submitted_date, s.created_at)) <= :uploadedTo`);
|
||||
}
|
||||
|
||||
const assessmentYear = String(filters?.assessmentYear || '').trim();
|
||||
if (assessmentYear) {
|
||||
replacements.assessmentYear = assessmentYear.toLowerCase();
|
||||
where.push(
|
||||
`LOWER(
|
||||
CASE
|
||||
WHEN s.financial_year ~ '^[0-9]{4}-[0-9]{2}$'
|
||||
THEN ((SUBSTRING(s.financial_year, 1, 4)::int + 1)::text || '-' || LPAD((((SUBSTRING(s.financial_year, 6, 2)::int + 1) % 100)::text), 2, '0'))
|
||||
ELSE s.financial_year
|
||||
END
|
||||
) LIKE '%' || :assessmentYear || '%'`
|
||||
);
|
||||
}
|
||||
|
||||
const whereSql = where.length ? `WHERE ${where.join('\n AND ')}` : '';
|
||||
const baseFromSql = `
|
||||
FROM form16a_submissions s
|
||||
INNER JOIN form_16_credit_notes c ON c.submission_id = s.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
d.dealership,
|
||||
d.dealer_principal_name,
|
||||
d.region
|
||||
FROM dealers d
|
||||
WHERE d.is_active = true
|
||||
AND (
|
||||
COALESCE(d.sales_code, '') = COALESCE(s.dealer_code, '')
|
||||
OR COALESCE(d.dlrcode, '') = COALESCE(s.dealer_code, '')
|
||||
)
|
||||
ORDER BY CASE WHEN COALESCE(d.sales_code, '') = COALESCE(s.dealer_code, '') THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
) d ON true
|
||||
`;
|
||||
|
||||
const rows = await sequelize.query<Form16UploadedPdfSqlRow>(
|
||||
`
|
||||
SELECT
|
||||
s.id AS submission_id,
|
||||
s.request_id,
|
||||
c.credit_note_number,
|
||||
COALESCE(d.dealership, d.dealer_principal_name, s.dealer_code) AS dealer_name,
|
||||
s.dealer_code,
|
||||
COALESCE(d.region, 'UNKNOWN') AS region,
|
||||
CASE
|
||||
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'N' THEN 'North'
|
||||
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'S' THEN 'South'
|
||||
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'E' THEN 'East'
|
||||
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'W' THEN 'West'
|
||||
WHEN UPPER(LEFT(COALESCE(d.region, ''), 1)) = 'C' THEN 'Central'
|
||||
ELSE 'Unknown'
|
||||
END AS zone,
|
||||
s.financial_year,
|
||||
CASE
|
||||
WHEN s.financial_year ~ '^[0-9]{4}-[0-9]{2}$'
|
||||
THEN ((SUBSTRING(s.financial_year, 1, 4)::int + 1)::text || '-' || LPAD((((SUBSTRING(s.financial_year, 6, 2)::int + 1) % 100)::text), 2, '0'))
|
||||
ELSE s.financial_year
|
||||
END AS assessment_year,
|
||||
s.quarter,
|
||||
s.total_amount AS amount,
|
||||
COALESCE(s.submitted_date, s.created_at) AS uploaded_date,
|
||||
s.document_url
|
||||
${baseFromSql}
|
||||
${whereSql}
|
||||
ORDER BY COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
`,
|
||||
{ replacements, type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const countRows = await sequelize.query<{ total: string }>(
|
||||
`
|
||||
SELECT COUNT(1)::text AS total
|
||||
${baseFromSql}
|
||||
${whereSql}
|
||||
`,
|
||||
{ replacements, type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const total = Number(countRows?.[0]?.total || 0);
|
||||
return {
|
||||
rows: rows.map((r) => ({
|
||||
submissionId: Number(r.submission_id),
|
||||
requestId: r.request_id,
|
||||
creditNoteNumber: r.credit_note_number,
|
||||
dealerName: String(r.dealer_name || r.dealer_code || '—'),
|
||||
dealerCode: String(r.dealer_code || '—'),
|
||||
region: String(r.region || 'UNKNOWN'),
|
||||
zone: String(r.zone || 'Unknown'),
|
||||
financialYear: String(r.financial_year || ''),
|
||||
assessmentYear: String(r.assessment_year || r.financial_year || ''),
|
||||
quarter: String(r.quarter || ''),
|
||||
amount: Number(r.amount || 0),
|
||||
uploadedDate: r.uploaded_date ? new Date(r.uploaded_date).toISOString() : null,
|
||||
documentUrl: String(r.document_url || ''),
|
||||
})),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUploadedForm16PdfLinksBySubmissionIds(submissionIds: number[]): Promise<Array<{
|
||||
submissionId: number;
|
||||
creditNoteNumber: string;
|
||||
dealerCode: string;
|
||||
quarter: string;
|
||||
financialYear: string;
|
||||
documentUrl: string;
|
||||
}>> {
|
||||
const ids = (submissionIds || []).map((v) => Number(v)).filter((n) => Number.isInteger(n) && n > 0);
|
||||
if (!ids.length) return [];
|
||||
const rows = await sequelize.query<{
|
||||
submission_id: number;
|
||||
credit_note_number: string;
|
||||
dealer_code: string;
|
||||
quarter: string;
|
||||
financial_year: string;
|
||||
document_url: string;
|
||||
}>(
|
||||
`
|
||||
SELECT
|
||||
s.id AS submission_id,
|
||||
c.credit_note_number,
|
||||
s.dealer_code,
|
||||
s.quarter,
|
||||
s.financial_year,
|
||||
s.document_url
|
||||
FROM form16a_submissions s
|
||||
INNER JOIN form_16_credit_notes c ON c.submission_id = s.id
|
||||
WHERE s.id IN (:ids)
|
||||
AND s.validation_status = 'success'
|
||||
AND COALESCE(TRIM(s.document_url), '') <> ''
|
||||
ORDER BY s.id DESC
|
||||
`,
|
||||
{ replacements: { ids }, type: QueryTypes.SELECT }
|
||||
);
|
||||
return rows.map((r) => ({
|
||||
submissionId: Number(r.submission_id),
|
||||
creditNoteNumber: r.credit_note_number,
|
||||
dealerCode: r.dealer_code,
|
||||
quarter: r.quarter,
|
||||
financialYear: r.financial_year,
|
||||
documentUrl: r.document_url,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* RE only. Cancel a Form 16 submission: set submission status to cancelled and workflow to REJECTED.
|
||||
*/
|
||||
@ -3407,31 +3118,11 @@ export interface Form16DashboardBreakdownRow {
|
||||
pendingDealerCount: number;
|
||||
}
|
||||
|
||||
export interface Form16DashboardDealerRow {
|
||||
dealerCode: string;
|
||||
dealerName: string;
|
||||
regionId: string;
|
||||
totalAmount: number;
|
||||
submittedAmount: number;
|
||||
pendingAmount: number;
|
||||
}
|
||||
|
||||
export interface Form16DashboardZoneRegionNode extends Form16DashboardBreakdownRow {
|
||||
regionId: string;
|
||||
dealers: Form16DashboardDealerRow[];
|
||||
}
|
||||
|
||||
export interface Form16DashboardZoneNode extends Form16DashboardBreakdownRow {
|
||||
zoneCode: string;
|
||||
regions: Form16DashboardZoneRegionNode[];
|
||||
}
|
||||
|
||||
export interface Form16DashboardData {
|
||||
kpi: Form16DashboardKpi;
|
||||
overall: Form16DashboardOverall;
|
||||
yearWise: Form16DashboardBreakdownRow[];
|
||||
zoneWise: Form16DashboardBreakdownRow[];
|
||||
zoneHierarchy: Form16DashboardZoneNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3448,172 +3139,64 @@ export async function getForm16DashboardData(): Promise<Form16DashboardData> {
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
};
|
||||
|
||||
const zoneSortRank = (zoneCode: string): number => {
|
||||
const z = String(zoneCode || '').toUpperCase();
|
||||
if (z === 'N') return 1;
|
||||
if (z === 'C') return 2;
|
||||
if (z === 'W') return 3;
|
||||
if (z === 'E') return 4;
|
||||
if (z === 'S') return 5;
|
||||
return 99;
|
||||
};
|
||||
|
||||
const zoneName = (zoneCode: string): string => {
|
||||
const z = String(zoneCode || '').toUpperCase();
|
||||
if (z === 'N') return 'North';
|
||||
if (z === 'C') return 'Central';
|
||||
if (z === 'W') return 'West';
|
||||
if (z === 'E') return 'East';
|
||||
if (z === 'S') return 'South';
|
||||
return z || 'Unknown';
|
||||
};
|
||||
|
||||
const regionSortValue = (regionId: string): number => {
|
||||
const r = String(regionId || '').toUpperCase();
|
||||
const m = /^([A-Z]+)(\d+)$/.exec(r);
|
||||
if (!m) return Number.MAX_SAFE_INTEGER;
|
||||
return parseInt(m[2], 10);
|
||||
};
|
||||
|
||||
// Dealer master is the base universe for dashboard (include all active dealers).
|
||||
const dealerUniverseRaw = await sequelize.query<{
|
||||
dealer_code: string;
|
||||
dealer_name: string | null;
|
||||
region_id: string | null;
|
||||
}>(
|
||||
`
|
||||
SELECT DISTINCT
|
||||
TRIM(COALESCE(d.dlrcode, '')) AS dealer_code,
|
||||
COALESCE(NULLIF(TRIM(d.dealership), ''), TRIM(COALESCE(d.dlrcode, ''))) AS dealer_name,
|
||||
UPPER(TRIM(COALESCE(d.region, ''))) AS region_id
|
||||
FROM dealers d
|
||||
WHERE d.is_active = true
|
||||
AND TRIM(COALESCE(d.dlrcode, '')) <> ''
|
||||
`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const dealerUniverse = (dealerUniverseRaw || []).map((r) => ({
|
||||
dealerCode: String(r.dealer_code || '').trim(),
|
||||
dealerName: String(r.dealer_name || r.dealer_code || '').trim() || 'Unknown Dealer',
|
||||
regionId: String(r.region_id || '').trim().toUpperCase() || 'UNKNOWN',
|
||||
}));
|
||||
|
||||
const detailRowsRaw = await sequelize.query<{
|
||||
dealer_code: string;
|
||||
dealer_name: string | null;
|
||||
region_id: string | null;
|
||||
financial_year: string;
|
||||
quarter: string;
|
||||
const [overallRow] = await sequelize.query<{
|
||||
total_amount: number | string | null;
|
||||
submitted_amount: number | string | null;
|
||||
total_dealers: number | string | null;
|
||||
submitted_dealer_count: number | string | null;
|
||||
}>(
|
||||
`
|
||||
WITH dealer_meta AS (
|
||||
WITH active_dealers AS (
|
||||
SELECT DISTINCT
|
||||
TRIM(COALESCE(d.dlrcode, '')) AS dealer_code,
|
||||
COALESCE(NULLIF(TRIM(d.dealership), ''), TRIM(COALESCE(d.dlrcode, ''))) AS dealer_name,
|
||||
UPPER(TRIM(COALESCE(d.region, ''))) AS region_id
|
||||
TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code
|
||||
FROM dealers d
|
||||
WHERE d.is_active = true
|
||||
AND TRIM(COALESCE(d.dlrcode, '')) <> ''
|
||||
AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
|
||||
),
|
||||
latest_submissions AS (
|
||||
SELECT
|
||||
s.id,
|
||||
TRIM(COALESCE(s.dealer_code, '')) AS dealer_code,
|
||||
s.dealer_code,
|
||||
s.financial_year,
|
||||
s.quarter,
|
||||
s.tan_number,
|
||||
COALESCE(s.total_amount, 0)::numeric AS total_amount,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY s.dealer_code, s.financial_year, s.quarter
|
||||
ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
|
||||
) AS rn
|
||||
FROM form16a_submissions s
|
||||
WHERE TRIM(COALESCE(s.dealer_code, '')) <> ''
|
||||
INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
|
||||
),
|
||||
latest_base AS (
|
||||
SELECT id, dealer_code, financial_year, quarter, tan_number, total_amount
|
||||
SELECT id, dealer_code, financial_year, quarter, total_amount
|
||||
FROM latest_submissions
|
||||
WHERE rn = 1
|
||||
),
|
||||
latest_snapshots AS (
|
||||
SELECT tan_number, financial_year, quarter, aggregated_amount
|
||||
FROM (
|
||||
SELECT
|
||||
ss.tan_number,
|
||||
ss.financial_year,
|
||||
ss.quarter,
|
||||
COALESCE(ss.aggregated_amount, 0)::numeric AS aggregated_amount,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY ss.tan_number, ss.financial_year, ss.quarter
|
||||
ORDER BY ss.id DESC, ss.created_at DESC
|
||||
) AS rn
|
||||
FROM form_16_26as_quarter_snapshots ss
|
||||
) t
|
||||
WHERE t.rn = 1
|
||||
),
|
||||
credit_by_submission AS (
|
||||
SELECT cn.submission_id, SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount
|
||||
FROM form_16_credit_notes cn
|
||||
GROUP BY cn.submission_id
|
||||
)
|
||||
submitted_by_dealer AS (
|
||||
SELECT
|
||||
lb.dealer_code,
|
||||
COALESCE(dm.dealer_name, lb.dealer_code) AS dealer_name,
|
||||
COALESCE(dm.region_id, 'UNKNOWN') AS region_id,
|
||||
lb.financial_year,
|
||||
lb.quarter,
|
||||
COALESCE(ls.aggregated_amount, lb.total_amount, 0)::numeric AS total_amount,
|
||||
COALESCE(cbs.submitted_amount, 0)::numeric AS submitted_amount
|
||||
SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount
|
||||
FROM latest_base lb
|
||||
LEFT JOIN dealer_meta dm ON dm.dealer_code = lb.dealer_code
|
||||
LEFT JOIN latest_snapshots ls
|
||||
ON ls.tan_number = lb.tan_number
|
||||
AND ls.financial_year = lb.financial_year
|
||||
AND ls.quarter = lb.quarter
|
||||
LEFT JOIN credit_by_submission cbs ON cbs.submission_id = lb.id
|
||||
LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
|
||||
GROUP BY lb.dealer_code
|
||||
)
|
||||
SELECT
|
||||
COALESCE((SELECT SUM(lb.total_amount) FROM latest_base lb), 0) AS total_amount,
|
||||
COALESCE((SELECT SUM(sbd.submitted_amount) FROM submitted_by_dealer sbd), 0) AS submitted_amount,
|
||||
COALESCE((SELECT COUNT(*) FROM active_dealers), 0) AS total_dealers,
|
||||
COALESCE((
|
||||
SELECT COUNT(DISTINCT sbd.dealer_code)
|
||||
FROM submitted_by_dealer sbd
|
||||
WHERE sbd.submitted_amount > 0
|
||||
), 0) AS submitted_dealer_count
|
||||
`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const detailRows = (detailRowsRaw || []).map((r) => ({
|
||||
dealerCode: String(r.dealer_code || '').trim(),
|
||||
dealerName: String(r.dealer_name || r.dealer_code || '').trim() || 'Unknown Dealer',
|
||||
regionId: String(r.region_id || '').trim().toUpperCase() || 'UNKNOWN',
|
||||
financialYear: String(r.financial_year || '').trim(),
|
||||
quarter: String(r.quarter || '').trim(),
|
||||
totalAmount: Math.max(0, toNum(r.total_amount)),
|
||||
submittedAmount: Math.max(0, toNum(r.submitted_amount)),
|
||||
}));
|
||||
|
||||
const dealerAgg = new Map<string, { dealerName: string; regionId: string; totalAmount: number; submittedAmount: number }>();
|
||||
for (const d of dealerUniverse) {
|
||||
dealerAgg.set(d.dealerCode, {
|
||||
dealerName: d.dealerName,
|
||||
regionId: d.regionId,
|
||||
totalAmount: 0,
|
||||
submittedAmount: 0,
|
||||
});
|
||||
}
|
||||
for (const r of detailRows) {
|
||||
const key = r.dealerCode;
|
||||
const prev = dealerAgg.get(key) || {
|
||||
dealerName: r.dealerName || key,
|
||||
regionId: r.regionId || 'UNKNOWN',
|
||||
totalAmount: 0,
|
||||
submittedAmount: 0,
|
||||
};
|
||||
prev.totalAmount += r.totalAmount;
|
||||
prev.submittedAmount += r.submittedAmount;
|
||||
dealerAgg.set(key, prev);
|
||||
}
|
||||
|
||||
const totalAmount = Array.from(dealerAgg.values()).reduce((sum, r) => sum + r.totalAmount, 0);
|
||||
const submittedAmount = Array.from(dealerAgg.values()).reduce((sum, r) => sum + r.submittedAmount, 0);
|
||||
const totalDealers = dealerAgg.size;
|
||||
const submittedDealerCount = Array.from(dealerAgg.values()).filter((r) => r.submittedAmount > 0).length;
|
||||
const totalAmount = toNum(overallRow?.total_amount);
|
||||
const submittedAmount = toNum(overallRow?.submitted_amount);
|
||||
const totalDealers = Math.max(0, Math.trunc(toNum(overallRow?.total_dealers)));
|
||||
const submittedDealerCount = Math.max(0, Math.trunc(toNum(overallRow?.submitted_dealer_count)));
|
||||
const pendingDealerCount = Math.max(0, totalDealers - submittedDealerCount);
|
||||
const pendingAmount = Math.max(0, totalAmount - submittedAmount);
|
||||
|
||||
@ -3622,137 +3205,154 @@ export async function getForm16DashboardData(): Promise<Form16DashboardData> {
|
||||
return Math.max(0, Math.min(100, Math.round((part / whole) * 100)));
|
||||
};
|
||||
|
||||
const yearMap = new Map<string, { totalAmount: number; submittedAmount: number; dealers: Set<string>; submittedDealers: Set<string> }>();
|
||||
for (const r of detailRows) {
|
||||
const y = r.financialYear || 'Unknown';
|
||||
const ag = yearMap.get(y) || { totalAmount: 0, submittedAmount: 0, dealers: new Set<string>(), submittedDealers: new Set<string>() };
|
||||
ag.totalAmount += r.totalAmount;
|
||||
ag.submittedAmount += r.submittedAmount;
|
||||
ag.dealers.add(r.dealerCode);
|
||||
if (r.submittedAmount > 0) ag.submittedDealers.add(r.dealerCode);
|
||||
yearMap.set(y, ag);
|
||||
}
|
||||
const yearWise = Array.from(yearMap.entries())
|
||||
.sort((a, b) => b[0].localeCompare(a[0]))
|
||||
.map(([label, ag]) => {
|
||||
const dealerCount = ag.dealers.size;
|
||||
const submittedDealerCount = ag.submittedDealers.size;
|
||||
const yearRowsRaw = await sequelize.query<{
|
||||
label: string;
|
||||
total_amount: number | string | null;
|
||||
dealer_count: number | string | null;
|
||||
submitted_amount: number | string | null;
|
||||
submitted_dealer_count: number | string | null;
|
||||
}>(
|
||||
`
|
||||
WITH active_dealers AS (
|
||||
SELECT DISTINCT
|
||||
TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code
|
||||
FROM dealers d
|
||||
WHERE d.is_active = true
|
||||
AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
|
||||
),
|
||||
latest_submissions AS (
|
||||
SELECT
|
||||
s.id,
|
||||
s.dealer_code,
|
||||
s.financial_year,
|
||||
s.quarter,
|
||||
COALESCE(s.total_amount, 0)::numeric AS total_amount,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY s.dealer_code, s.financial_year, s.quarter
|
||||
ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
|
||||
) AS rn
|
||||
FROM form16a_submissions s
|
||||
INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
|
||||
),
|
||||
latest_base AS (
|
||||
SELECT id, dealer_code, financial_year, quarter, total_amount
|
||||
FROM latest_submissions
|
||||
WHERE rn = 1
|
||||
),
|
||||
by_year AS (
|
||||
SELECT
|
||||
lb.financial_year AS label,
|
||||
SUM(lb.total_amount)::numeric AS total_amount,
|
||||
COUNT(DISTINCT lb.dealer_code) AS dealer_count,
|
||||
SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount,
|
||||
COUNT(DISTINCT CASE WHEN COALESCE(cn.amount, 0) > 0 THEN lb.dealer_code END) AS submitted_dealer_count
|
||||
FROM latest_base lb
|
||||
LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
|
||||
GROUP BY lb.financial_year
|
||||
)
|
||||
SELECT * FROM by_year
|
||||
ORDER BY label DESC
|
||||
`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const zoneRowsRaw = await sequelize.query<{
|
||||
label: string;
|
||||
total_amount: number | string | null;
|
||||
dealer_count: number | string | null;
|
||||
submitted_amount: number | string | null;
|
||||
submitted_dealer_count: number | string | null;
|
||||
}>(
|
||||
`
|
||||
WITH active_dealers AS (
|
||||
SELECT DISTINCT
|
||||
TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code,
|
||||
CASE
|
||||
WHEN UPPER(COALESCE(d.region, '')) LIKE 'N%' THEN 'North'
|
||||
WHEN UPPER(COALESCE(d.region, '')) LIKE 'S%' THEN 'South'
|
||||
WHEN UPPER(COALESCE(d.region, '')) LIKE 'E%' THEN 'East'
|
||||
WHEN UPPER(COALESCE(d.region, '')) LIKE 'W%' THEN 'West'
|
||||
WHEN UPPER(COALESCE(d.region, '')) LIKE 'C%' THEN 'Central'
|
||||
ELSE 'Unknown'
|
||||
END AS zone
|
||||
FROM dealers d
|
||||
WHERE d.is_active = true
|
||||
AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
|
||||
),
|
||||
latest_submissions AS (
|
||||
SELECT
|
||||
s.id,
|
||||
s.dealer_code,
|
||||
COALESCE(s.total_amount, 0)::numeric AS total_amount,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY s.dealer_code, s.financial_year, s.quarter
|
||||
ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
|
||||
) AS rn
|
||||
FROM form16a_submissions s
|
||||
INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
|
||||
),
|
||||
latest_base AS (
|
||||
SELECT id, dealer_code, total_amount
|
||||
FROM latest_submissions
|
||||
WHERE rn = 1
|
||||
),
|
||||
by_zone AS (
|
||||
SELECT
|
||||
ad.zone AS label,
|
||||
SUM(COALESCE(lb.total_amount, 0))::numeric AS total_amount,
|
||||
COUNT(DISTINCT ad.dealer_code) AS dealer_count,
|
||||
SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount,
|
||||
COUNT(DISTINCT CASE WHEN COALESCE(cn.amount, 0) > 0 THEN ad.dealer_code END) AS submitted_dealer_count
|
||||
FROM active_dealers ad
|
||||
LEFT JOIN latest_base lb ON lb.dealer_code = ad.dealer_code
|
||||
LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
|
||||
GROUP BY ad.zone
|
||||
)
|
||||
SELECT * FROM by_zone
|
||||
ORDER BY CASE label
|
||||
WHEN 'North' THEN 1
|
||||
WHEN 'Central' THEN 2
|
||||
WHEN 'West' THEN 3
|
||||
WHEN 'East' THEN 4
|
||||
WHEN 'South' THEN 5
|
||||
ELSE 99
|
||||
END, label
|
||||
`,
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const yearWise = (yearRowsRaw || []).map((r) => {
|
||||
const totalAmountRow = toNum(r.total_amount);
|
||||
const submittedAmountRow = toNum(r.submitted_amount);
|
||||
const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count)));
|
||||
const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count)));
|
||||
return {
|
||||
label,
|
||||
totalAmount: ag.totalAmount,
|
||||
dealerCount,
|
||||
submittedAmount: ag.submittedAmount,
|
||||
submittedDealerCount,
|
||||
pendingAmount: Math.max(0, ag.totalAmount - ag.submittedAmount),
|
||||
pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount),
|
||||
label: r.label,
|
||||
totalAmount: totalAmountRow,
|
||||
dealerCount: dealerCountRow,
|
||||
submittedAmount: submittedAmountRow,
|
||||
submittedDealerCount: submittedDealerCountRow,
|
||||
pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
|
||||
pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
|
||||
};
|
||||
});
|
||||
|
||||
const dealerRegionMap = new Map<string, { dealerName: string; regionId: string; totalAmount: number; submittedAmount: number }>();
|
||||
for (const [dealerCode, v] of dealerAgg.entries()) {
|
||||
dealerRegionMap.set(dealerCode, {
|
||||
dealerName: v.dealerName,
|
||||
regionId: v.regionId,
|
||||
totalAmount: v.totalAmount,
|
||||
submittedAmount: v.submittedAmount,
|
||||
});
|
||||
}
|
||||
|
||||
const zoneMap = new Map<string, { totalAmount: number; submittedAmount: number; dealers: Set<string>; submittedDealers: Set<string>; regions: Map<string, Form16DashboardZoneRegionNode> }>();
|
||||
for (const [dealerCode, d] of dealerRegionMap.entries()) {
|
||||
const regionId = String(d.regionId || '').toUpperCase();
|
||||
const zoneCode = (/^[A-Z]+/.exec(regionId)?.[0] || 'UNKNOWN').toUpperCase();
|
||||
const zone = zoneMap.get(zoneCode) || {
|
||||
totalAmount: 0,
|
||||
submittedAmount: 0,
|
||||
dealers: new Set<string>(),
|
||||
submittedDealers: new Set<string>(),
|
||||
regions: new Map<string, Form16DashboardZoneRegionNode>(),
|
||||
};
|
||||
zone.totalAmount += d.totalAmount;
|
||||
zone.submittedAmount += d.submittedAmount;
|
||||
zone.dealers.add(dealerCode);
|
||||
if (d.submittedAmount > 0) zone.submittedDealers.add(dealerCode);
|
||||
|
||||
const regionNode = zone.regions.get(regionId) || {
|
||||
regionId,
|
||||
label: regionId,
|
||||
totalAmount: 0,
|
||||
dealerCount: 0,
|
||||
submittedAmount: 0,
|
||||
submittedDealerCount: 0,
|
||||
pendingAmount: 0,
|
||||
pendingDealerCount: 0,
|
||||
dealers: [],
|
||||
};
|
||||
regionNode.totalAmount += d.totalAmount;
|
||||
regionNode.submittedAmount += d.submittedAmount;
|
||||
regionNode.dealers.push({
|
||||
dealerCode,
|
||||
dealerName: d.dealerName || dealerCode,
|
||||
regionId,
|
||||
totalAmount: d.totalAmount,
|
||||
submittedAmount: d.submittedAmount,
|
||||
pendingAmount: Math.max(0, d.totalAmount - d.submittedAmount),
|
||||
});
|
||||
zone.regions.set(regionId, regionNode);
|
||||
zoneMap.set(zoneCode, zone);
|
||||
}
|
||||
|
||||
const zoneHierarchy: Form16DashboardZoneNode[] = Array.from(zoneMap.entries())
|
||||
.map(([zoneCode, z]) => {
|
||||
const regions = Array.from(z.regions.values())
|
||||
.map((r) => {
|
||||
const submittedDealerCount = r.dealers.filter((d) => d.submittedAmount > 0).length;
|
||||
const dealerCount = r.dealers.length;
|
||||
const dealers = [...r.dealers].sort((a, b) => a.dealerCode.localeCompare(b.dealerCode));
|
||||
const zoneWise = (zoneRowsRaw || []).map((r) => {
|
||||
const totalAmountRow = toNum(r.total_amount);
|
||||
const submittedAmountRow = toNum(r.submitted_amount);
|
||||
const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count)));
|
||||
const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count)));
|
||||
return {
|
||||
...r,
|
||||
dealers,
|
||||
dealerCount,
|
||||
submittedDealerCount,
|
||||
pendingAmount: Math.max(0, r.totalAmount - r.submittedAmount),
|
||||
pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount),
|
||||
label: r.label,
|
||||
totalAmount: totalAmountRow,
|
||||
dealerCount: dealerCountRow,
|
||||
submittedAmount: submittedAmountRow,
|
||||
submittedDealerCount: submittedDealerCountRow,
|
||||
pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
|
||||
pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const da = regionSortValue(a.regionId);
|
||||
const db = regionSortValue(b.regionId);
|
||||
if (da !== db) return da - db;
|
||||
return a.regionId.localeCompare(b.regionId);
|
||||
});
|
||||
|
||||
const dealerCount = z.dealers.size;
|
||||
const submittedDealerCount = z.submittedDealers.size;
|
||||
return {
|
||||
zoneCode,
|
||||
label: zoneName(zoneCode),
|
||||
totalAmount: z.totalAmount,
|
||||
dealerCount,
|
||||
submittedAmount: z.submittedAmount,
|
||||
submittedDealerCount,
|
||||
pendingAmount: Math.max(0, z.totalAmount - z.submittedAmount),
|
||||
pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount),
|
||||
regions,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const ra = zoneSortRank(a.zoneCode);
|
||||
const rb = zoneSortRank(b.zoneCode);
|
||||
if (ra !== rb) return ra - rb;
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
|
||||
const zoneWise = zoneHierarchy.map((z) => ({
|
||||
label: z.label,
|
||||
totalAmount: z.totalAmount,
|
||||
dealerCount: z.dealerCount,
|
||||
submittedAmount: z.submittedAmount,
|
||||
submittedDealerCount: z.submittedDealerCount,
|
||||
pendingAmount: z.pendingAmount,
|
||||
pendingDealerCount: z.pendingDealerCount,
|
||||
}));
|
||||
|
||||
return {
|
||||
kpi: {
|
||||
collectionPct: toPct(submittedDealerCount, totalDealers),
|
||||
@ -3770,6 +3370,5 @@ export async function getForm16DashboardData(): Promise<Form16DashboardData> {
|
||||
},
|
||||
yearWise,
|
||||
zoneWise,
|
||||
zoneHierarchy,
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user