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",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"archiver": "^7.0.1",
|
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bullmq": "^5.63.0",
|
"bullmq": "^5.63.0",
|
||||||
@ -79,7 +78,6 @@
|
|||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/archiver": "^7.0.0",
|
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/cookie-parser": "^1.4.10",
|
"@types/cookie-parser": "^1.4.10",
|
||||||
"@types/cors": "^2.8.17",
|
"@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
|
* GET /api/v1/form16/dealer/submissions
|
||||||
* Dealer only. List Form 16 submissions for the authenticated dealer (pending/failed for Pending Submissions page).
|
* 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> {
|
async get26asDashboard(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const dashboard = await form16Service.getForm16DashboardData();
|
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');
|
return ResponseHandler.success(res, dashboard, 'Form16A dashboard fetched');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown 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,
|
requireForm16SubmissionAccess,
|
||||||
asyncHandler(form16Controller.listDebitNotes.bind(form16Controller))
|
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(
|
router.get(
|
||||||
'/debit-notes/:id/sap-response',
|
'/debit-notes/:id/sap-response',
|
||||||
requireForm16ReOnly,
|
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`,
|
`SELECT financial_year, quarter, MIN(created_at) AS min_created FROM tds_26as_entries GROUP BY financial_year, quarter`,
|
||||||
{ type: QueryTypes.SELECT }
|
{ 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>();
|
const minDateByKey = new Map<string, Date>();
|
||||||
for (const row of twentySixAsMinDates) {
|
for (const row of twentySixAsMinDates) {
|
||||||
const key = `${row.financial_year}|${row.quarter}`;
|
const key = `${row.financial_year}|${row.quarter}`;
|
||||||
if (row.min_created) minDateByKey.set(key, new Date(row.min_created));
|
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({
|
const dealerSubmissions = await Form16aSubmission.findAll({
|
||||||
where: { dealerCode },
|
where: { dealerCode },
|
||||||
@ -1887,7 +1877,6 @@ export async function listDealerPendingQuarters(userId: string) {
|
|||||||
days_remaining: number | null;
|
days_remaining: number | null;
|
||||||
days_overdue: number | null;
|
days_overdue: number | null;
|
||||||
days_since_26as_uploaded: number | null;
|
days_since_26as_uploaded: number | null;
|
||||||
twenty_six_as_amount: number;
|
|
||||||
}> = [];
|
}> = [];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||||
@ -1912,7 +1901,6 @@ export async function listDealerPendingQuarters(userId: string) {
|
|||||||
const daysSince26AsUploaded = twentySixAsMin
|
const daysSince26AsUploaded = twentySixAsMin
|
||||||
? Math.floor((now.getTime() - twentySixAsMin.getTime()) / oneDayMs)
|
? Math.floor((now.getTime() - twentySixAsMin.getTime()) / oneDayMs)
|
||||||
: null;
|
: null;
|
||||||
const twentySixAsAmount = amountByKey.get(key) ?? 0;
|
|
||||||
result.push({
|
result.push({
|
||||||
financial_year: financialYear,
|
financial_year: financialYear,
|
||||||
quarter,
|
quarter,
|
||||||
@ -1925,7 +1913,6 @@ export async function listDealerPendingQuarters(userId: string) {
|
|||||||
days_remaining: daysRemaining,
|
days_remaining: daysRemaining,
|
||||||
days_overdue: daysOverdue,
|
days_overdue: daysOverdue,
|
||||||
days_since_26as_uploaded: daysSince26AsUploaded,
|
days_since_26as_uploaded: daysSince26AsUploaded,
|
||||||
twenty_six_as_amount: twentySixAsAmount,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
result.sort((a, b) => {
|
result.sort((a, b) => {
|
||||||
@ -1938,282 +1925,6 @@ export async function listDealerPendingQuarters(userId: string) {
|
|||||||
return result;
|
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.
|
* 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;
|
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 {
|
export interface Form16DashboardData {
|
||||||
kpi: Form16DashboardKpi;
|
kpi: Form16DashboardKpi;
|
||||||
overall: Form16DashboardOverall;
|
overall: Form16DashboardOverall;
|
||||||
yearWise: Form16DashboardBreakdownRow[];
|
yearWise: Form16DashboardBreakdownRow[];
|
||||||
zoneWise: Form16DashboardBreakdownRow[];
|
zoneWise: Form16DashboardBreakdownRow[];
|
||||||
zoneHierarchy: Form16DashboardZoneNode[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3448,172 +3139,64 @@ export async function getForm16DashboardData(): Promise<Form16DashboardData> {
|
|||||||
return Number.isFinite(n) ? n : 0;
|
return Number.isFinite(n) ? n : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const zoneSortRank = (zoneCode: string): number => {
|
const [overallRow] = await sequelize.query<{
|
||||||
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;
|
|
||||||
total_amount: number | string | null;
|
total_amount: number | string | null;
|
||||||
submitted_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
|
SELECT DISTINCT
|
||||||
TRIM(COALESCE(d.dlrcode, '')) AS dealer_code,
|
TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(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
|
FROM dealers d
|
||||||
WHERE d.is_active = true
|
WHERE d.is_active = true
|
||||||
AND TRIM(COALESCE(d.dlrcode, '')) <> ''
|
AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
|
||||||
),
|
),
|
||||||
latest_submissions AS (
|
latest_submissions AS (
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
TRIM(COALESCE(s.dealer_code, '')) AS dealer_code,
|
s.dealer_code,
|
||||||
s.financial_year,
|
s.financial_year,
|
||||||
s.quarter,
|
s.quarter,
|
||||||
s.tan_number,
|
|
||||||
COALESCE(s.total_amount, 0)::numeric AS total_amount,
|
COALESCE(s.total_amount, 0)::numeric AS total_amount,
|
||||||
ROW_NUMBER() OVER (
|
ROW_NUMBER() OVER (
|
||||||
PARTITION BY s.dealer_code, s.financial_year, s.quarter
|
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
|
ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
|
||||||
) AS rn
|
) AS rn
|
||||||
FROM form16a_submissions s
|
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 (
|
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
|
FROM latest_submissions
|
||||||
WHERE rn = 1
|
WHERE rn = 1
|
||||||
),
|
),
|
||||||
latest_snapshots AS (
|
submitted_by_dealer 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
|
|
||||||
)
|
|
||||||
SELECT
|
SELECT
|
||||||
lb.dealer_code,
|
lb.dealer_code,
|
||||||
COALESCE(dm.dealer_name, lb.dealer_code) AS dealer_name,
|
SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount
|
||||||
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
|
|
||||||
FROM latest_base lb
|
FROM latest_base lb
|
||||||
LEFT JOIN dealer_meta dm ON dm.dealer_code = lb.dealer_code
|
LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
|
||||||
LEFT JOIN latest_snapshots ls
|
GROUP BY lb.dealer_code
|
||||||
ON ls.tan_number = lb.tan_number
|
)
|
||||||
AND ls.financial_year = lb.financial_year
|
SELECT
|
||||||
AND ls.quarter = lb.quarter
|
COALESCE((SELECT SUM(lb.total_amount) FROM latest_base lb), 0) AS total_amount,
|
||||||
LEFT JOIN credit_by_submission cbs ON cbs.submission_id = lb.id
|
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 }
|
{ type: QueryTypes.SELECT }
|
||||||
);
|
);
|
||||||
|
|
||||||
const detailRows = (detailRowsRaw || []).map((r) => ({
|
const totalAmount = toNum(overallRow?.total_amount);
|
||||||
dealerCode: String(r.dealer_code || '').trim(),
|
const submittedAmount = toNum(overallRow?.submitted_amount);
|
||||||
dealerName: String(r.dealer_name || r.dealer_code || '').trim() || 'Unknown Dealer',
|
const totalDealers = Math.max(0, Math.trunc(toNum(overallRow?.total_dealers)));
|
||||||
regionId: String(r.region_id || '').trim().toUpperCase() || 'UNKNOWN',
|
const submittedDealerCount = Math.max(0, Math.trunc(toNum(overallRow?.submitted_dealer_count)));
|
||||||
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 pendingDealerCount = Math.max(0, totalDealers - submittedDealerCount);
|
const pendingDealerCount = Math.max(0, totalDealers - submittedDealerCount);
|
||||||
const pendingAmount = Math.max(0, totalAmount - submittedAmount);
|
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)));
|
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> }>();
|
const yearRowsRaw = await sequelize.query<{
|
||||||
for (const r of detailRows) {
|
label: string;
|
||||||
const y = r.financialYear || 'Unknown';
|
total_amount: number | string | null;
|
||||||
const ag = yearMap.get(y) || { totalAmount: 0, submittedAmount: 0, dealers: new Set<string>(), submittedDealers: new Set<string>() };
|
dealer_count: number | string | null;
|
||||||
ag.totalAmount += r.totalAmount;
|
submitted_amount: number | string | null;
|
||||||
ag.submittedAmount += r.submittedAmount;
|
submitted_dealer_count: number | string | null;
|
||||||
ag.dealers.add(r.dealerCode);
|
}>(
|
||||||
if (r.submittedAmount > 0) ag.submittedDealers.add(r.dealerCode);
|
`
|
||||||
yearMap.set(y, ag);
|
WITH active_dealers AS (
|
||||||
}
|
SELECT DISTINCT
|
||||||
const yearWise = Array.from(yearMap.entries())
|
TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code
|
||||||
.sort((a, b) => b[0].localeCompare(a[0]))
|
FROM dealers d
|
||||||
.map(([label, ag]) => {
|
WHERE d.is_active = true
|
||||||
const dealerCount = ag.dealers.size;
|
AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
|
||||||
const submittedDealerCount = ag.submittedDealers.size;
|
),
|
||||||
|
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 {
|
return {
|
||||||
label,
|
label: r.label,
|
||||||
totalAmount: ag.totalAmount,
|
totalAmount: totalAmountRow,
|
||||||
dealerCount,
|
dealerCount: dealerCountRow,
|
||||||
submittedAmount: ag.submittedAmount,
|
submittedAmount: submittedAmountRow,
|
||||||
submittedDealerCount,
|
submittedDealerCount: submittedDealerCountRow,
|
||||||
pendingAmount: Math.max(0, ag.totalAmount - ag.submittedAmount),
|
pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
|
||||||
pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount),
|
pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const dealerRegionMap = new Map<string, { dealerName: string; regionId: string; totalAmount: number; submittedAmount: number }>();
|
const zoneWise = (zoneRowsRaw || []).map((r) => {
|
||||||
for (const [dealerCode, v] of dealerAgg.entries()) {
|
const totalAmountRow = toNum(r.total_amount);
|
||||||
dealerRegionMap.set(dealerCode, {
|
const submittedAmountRow = toNum(r.submitted_amount);
|
||||||
dealerName: v.dealerName,
|
const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count)));
|
||||||
regionId: v.regionId,
|
const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count)));
|
||||||
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));
|
|
||||||
return {
|
return {
|
||||||
...r,
|
label: r.label,
|
||||||
dealers,
|
totalAmount: totalAmountRow,
|
||||||
dealerCount,
|
dealerCount: dealerCountRow,
|
||||||
submittedDealerCount,
|
submittedAmount: submittedAmountRow,
|
||||||
pendingAmount: Math.max(0, r.totalAmount - r.submittedAmount),
|
submittedDealerCount: submittedDealerCountRow,
|
||||||
pendingDealerCount: Math.max(0, dealerCount - submittedDealerCount),
|
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 {
|
return {
|
||||||
kpi: {
|
kpi: {
|
||||||
collectionPct: toPct(submittedDealerCount, totalDealers),
|
collectionPct: toPct(submittedDealerCount, totalDealers),
|
||||||
@ -3770,6 +3370,5 @@ export async function getForm16DashboardData(): Promise<Form16DashboardData> {
|
|||||||
},
|
},
|
||||||
yearWise,
|
yearWise,
|
||||||
zoneWise,
|
zoneWise,
|
||||||
zoneHierarchy,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user