dashboard added
This commit is contained in:
parent
34c488ae16
commit
876ec26e97
@ -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
1
build/assets/index-f-qcsO2P.css
Normal file
1
build/assets/index-f-qcsO2P.css
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user