dashboard added

This commit is contained in:
Aaditya Jaiswal 2026-04-09 15:39:13 +05:30
parent 34c488ae16
commit 876ec26e97
9 changed files with 334 additions and 23 deletions

View File

@ -1 +1 @@
import{a as s}from"./index-B_32yGxr.js";import"./radix-vendor-CLtqm-Ae.js";import"./charts-vendor-CmYZJIYl.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-DgwXkk2Y.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-HW_ujxKo.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
import{a as s}from"./index-BVr6jLdd.js";import"./radix-vendor-CLtqm-Ae.js";import"./charts-vendor-CmYZJIYl.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-DgwXkk2Y.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-HW_ujxKo.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -13,7 +13,7 @@
<!-- Preload essential fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="module" crossorigin src="/assets/index-B_32yGxr.js"></script>
<script type="module" crossorigin src="/assets/index-BVr6jLdd.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-CmYZJIYl.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CLtqm-Ae.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
@ -21,7 +21,7 @@
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-HW_ujxKo.js">
<link rel="stylesheet" crossorigin href="/assets/index-Dr_mmbQV.css">
<link rel="stylesheet" crossorigin href="/assets/index-f-qcsO2P.css">
</head>
<body>

View File

@ -341,6 +341,21 @@ export class Form16Controller {
}
}
/**
* GET /api/v1/form16/26as/dashboard
* RE only. Aggregated Form16A dashboard (collection/submission status + year/zone breakdown).
*/
async get26asDashboard(req: Request, res: Response): Promise<void> {
try {
const dashboard = await form16Service.getForm16DashboardData();
return ResponseHandler.success(res, dashboard, 'Form16A dashboard fetched');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('[Form16Controller] get26asDashboard error:', error);
return ResponseHandler.error(res, 'Failed to fetch Form16A dashboard', 500, errorMessage);
}
}
/**
* POST /api/v1/form16/26as
* RE only. Create a 26AS TDS entry.

View File

@ -213,6 +213,12 @@ router.post(
);
// 26AS (who can see: twentySixAsViewerEmails from admin config)
router.get(
'/26as/dashboard',
requireForm16ReOnly,
requireForm1626AsAccess,
asyncHandler(form16Controller.get26asDashboard.bind(form16Controller))
);
router.get(
'/26as',
requireForm16ReOnly,

View File

@ -874,8 +874,17 @@ export class AuthService {
throw new Error('Redirect URI is required');
}
const normalize = (s: string) => s.trim().replace(/\/+$/, '');
const providedRedirectUri = normalize(redirectUri);
const frontendBase = process.env.FRONTEND_URL ? normalize(process.env.FRONTEND_URL) : '';
const canonicalRedirectUri = frontendBase ? `${frontendBase}/login/callback` : providedRedirectUri;
const isSecureEnv = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'uat';
const effectiveRedirectUri = isSecureEnv ? canonicalRedirectUri : providedRedirectUri;
logger.info('Exchanging code with Okta', {
redirectUri,
redirectUri: effectiveRedirectUri,
providedRedirectUri,
canonicalRedirectUri,
codePrefix: code.substring(0, 10) + '...',
oktaDomain: ssoConfig.oktaDomain,
clientId: ssoConfig.oktaClientId,
@ -891,7 +900,7 @@ export class AuthService {
new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri, // Frontend URL (e.g., http://localhost:3000/login/callback)
redirect_uri: effectiveRedirectUri, // Must match authorize request redirect_uri exactly
client_id: ssoConfig.oktaClientId,
client_secret: ssoConfig.oktaClientSecret,
}),

View File

@ -3039,3 +3039,284 @@ export async function list26asUploadHistory(
});
return { rows: mapped, total: count };
}
export interface Form16DashboardKpi {
collectionPct: number;
pendingPct: number;
submittedPct: number;
submissionPendingPct: number;
}
export interface Form16DashboardOverall {
totalAmount: number;
submittedAmount: number;
pendingAmount: number;
totalDealers: number;
submittedDealerCount: number;
pendingDealerCount: number;
}
export interface Form16DashboardBreakdownRow {
label: string;
totalAmount: number;
dealerCount: number;
submittedAmount: number;
submittedDealerCount: number;
pendingAmount: number;
pendingDealerCount: number;
}
export interface Form16DashboardData {
kpi: Form16DashboardKpi;
overall: Form16DashboardOverall;
yearWise: Form16DashboardBreakdownRow[];
zoneWise: Form16DashboardBreakdownRow[];
}
/**
* Form16A dashboard for RE users.
* Uses real DB data:
* - dealer universe from active dealers
* - latest submission per dealer+FY+quarter
* - submitted/credited via form_16_credit_notes
* Zone mapping follows dealer region code prefix: N* -> North, S* -> South, E* -> East, W* -> West, C* -> Central.
*/
export async function getForm16DashboardData(): Promise<Form16DashboardData> {
const toNum = (v: unknown): number => {
const n = Number(v ?? 0);
return Number.isFinite(n) ? n : 0;
};
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 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
),
submitted_by_dealer AS (
SELECT
lb.dealer_code,
SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount
FROM latest_base lb
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 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);
const toPct = (part: number, whole: number): number => {
if (!whole || whole <= 0) return 0;
return Math.max(0, Math.min(100, Math.round((part / whole) * 100)));
};
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: r.label,
totalAmount: totalAmountRow,
dealerCount: dealerCountRow,
submittedAmount: submittedAmountRow,
submittedDealerCount: submittedDealerCountRow,
pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
};
});
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 {
label: r.label,
totalAmount: totalAmountRow,
dealerCount: dealerCountRow,
submittedAmount: submittedAmountRow,
submittedDealerCount: submittedDealerCountRow,
pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
};
});
return {
kpi: {
collectionPct: toPct(submittedDealerCount, totalDealers),
pendingPct: toPct(pendingDealerCount, totalDealers),
submittedPct: toPct(submittedAmount, totalAmount),
submissionPendingPct: toPct(pendingAmount, totalAmount),
},
overall: {
totalAmount,
submittedAmount,
pendingAmount,
totalDealers,
submittedDealerCount,
pendingDealerCount,
},
yearWise,
zoneWise,
};
}