feat: updates for cashier, tds reports, rbi reserve and dashboard

This commit is contained in:
2026-10-07 10:13:28 +05:30
parent 5ab4e9eaf0
commit 6226682b36
50 changed files with 3765 additions and 1636 deletions
@@ -10,11 +10,11 @@ import { Component, OnInit, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import * as ExcelJS from 'exceljs';
import { BranchScopeService } from '../core/branch-scope.service';
import { FY_TAX_CONFIG, Quarter, QUARTER_LABEL } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
import { TdsDeductee, setTdsDeductees, tdsFys } from './tds-reports-data';
import { buildTdsInterestWorkbook } from './tds-interest-export';
import { TdsReportsService } from './tds-reports.service';
import {
NEAR_THRESHOLD_BAND,
@@ -26,6 +26,9 @@ import {
tdsInterestSummary
} from './tds-reports.compute';
/** The three answers the register gives: TDS taken, not taken because of a declaration, none due. */
type TdsTab = 'paying' | 'declared' | 'not-paying';
@Component({
selector: 'mifosx-tds-interest-report',
templateUrl: './tds-interest-report.component.html',
@@ -70,7 +73,7 @@ export class TdsInterestReportComponent implements OnInit {
/** Which customer's quarter-wise detail is open. */
expandedId: string | null = null;
exporting = false;
selectedTab: 'paying' | 'not-paying' = 'paying';
selectedTab: TdsTab = 'paying';
/** Rows per page; 0 shows the whole list. */
readonly pageSizeOptions = [
@@ -92,6 +95,7 @@ export class TdsInterestReportComponent implements OnInit {
private allRows: TdsInterestRow[] = [];
private paying: TdsInterestRow[] = [];
private notPaying: TdsInterestRow[] = [];
private declared: TdsInterestRow[] = [];
private summaryCacheKey = '';
private cachedSummary: TdsInterestSummary | null = null;
@@ -219,8 +223,28 @@ export class TdsInterestReportComponent implements OnInit {
if (!row.trackedAccounts) {
return { label: 'Not tracked — no FD/RD', tone: 'muted' };
}
if (row.hasDeclaration) {
const beforeForm = Math.round(row.schedule.totalTds - row.actualTds);
if (beforeForm > 0) {
return { label: `Form ${row.form} — ₹${beforeForm.toLocaleString('en-IN')} due from before the form, not yet deducted`, tone: 'warn' };
}
if (row.tdsSaved > 0) {
return { label: `Form ${row.form} — TDS not deducted`, tone: 'ok' };
}
return { label: `Form ${row.form} on record — below the limit so far`, tone: 'ok' };
}
if (row.schedule.totalTds > 0) {
return { label: 'Over the limit — TDS due', tone: 'warn' };
const gap = Math.round(row.schedule.totalTds - row.actualTds);
if (gap > 0) {
return { label: `Over the limit — ₹${gap.toLocaleString('en-IN')} not yet deducted`, tone: 'warn' };
}
if (gap < 0) {
return { label: `Deducted ₹${(-gap).toLocaleString('en-IN')} more than due — check`, tone: 'warn' };
}
return { label: 'Over the limit — TDS deducted', tone: 'ok' };
}
if (row.actualTds > 0) {
return { label: `₹${Math.round(row.actualTds).toLocaleString('en-IN')} deducted but none due — check`, tone: 'warn' };
}
if (row.hasDeclaration && row.crossed) {
return { label: 'Over the limit — 15G/15H, no TDS', tone: 'ok' };
@@ -247,8 +271,12 @@ export class TdsInterestReportComponent implements OnInit {
}
this.rowsCacheKey = key;
this.allRows = tdsInterestRows(this.filter);
this.paying = this.allRows.filter((r) => r.schedule.totalTds > 0);
this.notPaying = this.allRows.filter((r) => r.schedule.totalTds === 0);
// Three plain answers: whose TDS was taken, whose was not because they gave a declaration, and who
// owed none. A declarant whose tax was partly taken before the form is shown with the declarants,
// where both figures sit side by side.
this.declared = this.allRows.filter((r) => r.hasDeclaration);
this.paying = this.allRows.filter((r) => !r.hasDeclaration && (r.schedule.totalTds > 0 || r.actualTds > 0));
this.notPaying = this.allRows.filter((r) => !r.hasDeclaration && r.schedule.totalTds === 0 && r.actualTds === 0);
}
get rows(): TdsInterestRow[] {
@@ -266,6 +294,11 @@ export class TdsInterestReportComponent implements OnInit {
return this.notPaying;
}
get declaredRows(): TdsInterestRow[] {
this.ensureRows();
return this.declared;
}
get summary(): TdsInterestSummary {
const key = JSON.stringify(this.filter);
if (key !== this.summaryCacheKey || !this.cachedSummary) {
@@ -277,6 +310,9 @@ export class TdsInterestReportComponent implements OnInit {
/** Every row of the tab on screen, before paging. */
get visibleRows(): TdsInterestRow[] {
if (this.selectedTab === 'declared') {
return this.declaredRows;
}
return this.selectedTab === 'paying' ? this.payingRows : this.notPayingRows;
}
@@ -337,7 +373,7 @@ export class TdsInterestReportComponent implements OnInit {
this.expandedId = null;
}
selectTab(tab: 'paying' | 'not-paying'): void {
selectTab(tab: TdsTab): void {
this.selectedTab = tab;
this.onFilterChanged();
}
@@ -385,105 +421,17 @@ export class TdsInterestReportComponent implements OnInit {
async exportWorkbook(): Promise<void> {
this.exporting = true;
try {
const wb = new ExcelJS.Workbook();
const ws = wb.addWorksheet(`TDS Interest FY ${this.filter.fy}`);
ws.addRow([`Deposit Interest & TDS — FY ${this.filter.fy}`]);
ws.addRow([`Scope: ${this.scopeLabel}`]);
ws.addRow([]);
ws.addRow([
'Customer',
'CIF',
'PAN',
'Branch',
'Accounts tracked for TDS',
'Accounts not tracked',
'Status',
'Form',
'Band',
'Age',
'Q1',
'Q2',
'Q3',
'Q4',
'Interest (FD/RD)',
'Savings interest (exempt)',
'Threshold',
'Crossed in',
'Rate %',
'TDS payable',
'Declaration',
'UIN',
'TDS saved',
'Paid without deduction'
]);
for (const r of this.rows) {
ws.addRow([
r.deductee.name,
r.deductee.customerId,
r.deductee.pan || 'NOT ON RECORD',
r.branchCode,
this.trackedOf(r)
.map((a) => `${a.accountNo} (${a.type})`)
.join(', '),
this.untrackedOf(r)
.map((a) => `${a.accountNo} (${a.type})`)
.join(', '),
this.statusOf(r).label,
r.form === '121' ? 'Form 121' : `Form ${r.form}`,
r.band === 'SENIOR' ? 'Senior citizen' : 'General',
r.age,
r.quarters[1],
r.quarters[2],
r.quarters[3],
r.quarters[4],
r.tdsApplicableInterest,
r.exemptInterest,
r.threshold,
r.crossedInQuarter ? `Q${r.crossedInQuarter}` : '—',
r.rateApplied * 100,
r.schedule.totalTds,
r.hasDeclaration ? 'Yes' : 'No',
r.declarationUin ?? '',
r.tdsSaved,
r.paidWithoutDeduction
]);
}
const s = this.summary;
ws.addRow([]);
ws.addRow([
'TOTAL',
`${s.deductees} deductees`,
'',
'',
`${s.trackedCustomers} tracked`,
'',
'',
'',
'',
'',
s.byQuarter[0].interest,
s.byQuarter[1].interest,
s.byQuarter[2].interest,
s.byQuarter[3].interest,
s.applicableInterest,
s.exemptInterest,
'',
'',
'',
s.totalTds,
`${s.declarations} filed`,
'',
s.tdsSaved,
s.paidWithoutDeduction
]);
ws.getRow(1).font = { bold: true, size: 13 };
ws.getRow(4).font = { bold: true };
ws.columns.forEach((c) => (c.width = 18));
const wb = buildTdsInterestWorkbook({
fy: this.filter.fy,
scopeLabel: this.scopeLabel,
rules: this.rules,
statement: this.statute.statement,
// What the screen shows under the current filters, every tab included.
rows: this.rows,
declaredRows: this.declaredRows,
summary: this.summary,
statusOf: (row) => this.statusOf(row)
});
const buffer = await wb.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
@@ -491,7 +439,7 @@ export class TdsInterestReportComponent implements OnInit {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `TDS-Interest-FY${this.filter.fy}.xlsx`;
a.download = `TDS-Register-FY${this.filter.fy}.xlsx`;
a.click();
URL.revokeObjectURL(url);
} finally {