feat: interest posting
This commit is contained in:
@@ -13,12 +13,12 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
|
||||
import { BranchScopeService } from '../core/branch-scope.service';
|
||||
import { GL_BRANCHES, glBranchName } from '../gl-reports/gl-reports-data';
|
||||
import { FY_TAX_CONFIG, Quarter, QUARTER_LABEL } from '../letter-mgmt/certificates/form-15gh/tds-15gh.compute';
|
||||
import { setTdsDeductees, tdsFys } from './tds-reports-data';
|
||||
import { TdsDeductee, setTdsDeductees, tdsFys } from './tds-reports-data';
|
||||
import { TdsReportsService } from './tds-reports.service';
|
||||
import {
|
||||
NEAR_THRESHOLD_BAND,
|
||||
TdsAccountLine,
|
||||
TdsInterestRow,
|
||||
TdsInterestSummary,
|
||||
TdsReportFilter,
|
||||
@@ -40,7 +40,12 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
private scope = inject(BranchScopeService);
|
||||
private tdsService = inject(TdsReportsService);
|
||||
|
||||
readonly branches = GL_BRANCHES.filter((b) => b.code !== 'HO');
|
||||
/**
|
||||
* Office names the loaded customers belong to, for Head Office's branch selector. Read off the
|
||||
* data rather than a fixed list, which named branches this bank does not have and left out the
|
||||
* ones it does.
|
||||
*/
|
||||
branches: string[] = [];
|
||||
/** Filled by the route resolver from Organization › TDS Configuration. */
|
||||
readonly financialYears = tdsFys();
|
||||
readonly quarterLabel = QUARTER_LABEL;
|
||||
@@ -92,16 +97,26 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
|
||||
loading = false;
|
||||
loadError = '';
|
||||
/** The "how to read this page" panel. */
|
||||
showHelp = true;
|
||||
|
||||
readonly accountTypeLabel: Record<TdsAccountLine['type'], string> = {
|
||||
FD: 'Fixed deposit',
|
||||
RD: 'Recurring deposit',
|
||||
SB: 'Savings',
|
||||
OTHER: 'Other'
|
||||
};
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.financialYears.length) {
|
||||
this.loadError = 'No financial year is configured for TDS. Set one up under Organization › TDS Configuration.';
|
||||
return;
|
||||
}
|
||||
// A branch login sees only its own book and cannot widen the scope.
|
||||
const own = this.scope.ownBranchCode;
|
||||
if (own) {
|
||||
this.filter.branchCode = own;
|
||||
// A branch login sees only its own book and cannot widen the scope. The platform labels each
|
||||
// customer with their office's name, so the login's office name is the filter — the GL branch
|
||||
// code list does not know every office and turned a branch login into an empty report.
|
||||
if (!this.scope.isHeadOffice) {
|
||||
this.filter.branchCode = this.scope.officeName;
|
||||
}
|
||||
this.loadDeductees();
|
||||
}
|
||||
@@ -121,6 +136,7 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
next: (deductees) => {
|
||||
this.loading = false;
|
||||
setTdsDeductees(deductees);
|
||||
this.branches = this.branchesOf(deductees);
|
||||
this.invalidateCaches();
|
||||
},
|
||||
error: (err: any) => {
|
||||
@@ -138,6 +154,11 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
* only the summary left the KPIs counting the new data while the tables kept the empty list
|
||||
* cached on the first render, before the load returned.
|
||||
*/
|
||||
private branchesOf(deductees: TdsDeductee[]): string[] {
|
||||
const names = new Set(deductees.map((d) => d.branchCode).filter((n) => !!n));
|
||||
return [...names].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
private invalidateCaches(): void {
|
||||
this.rowsCacheKey = '';
|
||||
this.summaryCacheKey = '';
|
||||
@@ -168,12 +189,54 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
/** The limits and rates for the selected year, for the help panel. */
|
||||
get rules(): { general: number; senior: number; rate: number; rateNoPan: number } | null {
|
||||
const cfg = FY_TAX_CONFIG[this.filter.fy];
|
||||
if (!cfg) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
general: cfg.thresholdGeneral,
|
||||
senior: cfg.thresholdSenior,
|
||||
rate: cfg.rate * 100,
|
||||
rateNoPan: cfg.rateNoPan * 100
|
||||
};
|
||||
}
|
||||
|
||||
get canSelectBranch(): boolean {
|
||||
return this.scope.canSelectBranch;
|
||||
}
|
||||
|
||||
get scopeLabel(): string {
|
||||
return this.filter.branchCode ? glBranchName(this.filter.branchCode) : 'All branches';
|
||||
return this.filter.branchCode ? `${this.filter.branchCode} branch` : 'All branches';
|
||||
}
|
||||
|
||||
/**
|
||||
* One plain-language status per customer, so a reader does not have to work it out from the
|
||||
* threshold, crossed and saved columns together.
|
||||
*/
|
||||
statusOf(row: TdsInterestRow): { label: string; tone: 'muted' | 'warn' | 'near' | 'ok' } {
|
||||
if (!row.trackedAccounts) {
|
||||
return { label: 'Not tracked — no FD/RD', tone: 'muted' };
|
||||
}
|
||||
if (row.schedule.totalTds > 0) {
|
||||
return { label: 'Over the limit — TDS due', tone: 'warn' };
|
||||
}
|
||||
if (row.hasDeclaration && row.crossed) {
|
||||
return { label: 'Over the limit — 15G/15H, no TDS', tone: 'ok' };
|
||||
}
|
||||
if (row.nearThreshold) {
|
||||
return { label: `Near the limit — ₹${Math.round(row.headroom).toLocaleString('en-IN')} left`, tone: 'near' };
|
||||
}
|
||||
return { label: 'Below the limit', tone: 'ok' };
|
||||
}
|
||||
|
||||
trackedOf(row: TdsInterestRow): TdsAccountLine[] {
|
||||
return row.accounts.filter((a) => a.tracked);
|
||||
}
|
||||
|
||||
untrackedOf(row: TdsInterestRow): TdsAccountLine[] {
|
||||
return row.accounts.filter((a) => !a.tracked);
|
||||
}
|
||||
|
||||
/** Recomputes the book only when the filter actually changed. Side-effect free. */
|
||||
@@ -315,10 +378,6 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
return this.expandedId === row.deductee.customerId;
|
||||
}
|
||||
|
||||
branchName(code: string): string {
|
||||
return glBranchName(code);
|
||||
}
|
||||
|
||||
trackRow(_: number, row: TdsInterestRow): string {
|
||||
return row.deductee.customerId;
|
||||
}
|
||||
@@ -337,6 +396,9 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
'CIF',
|
||||
'PAN',
|
||||
'Branch',
|
||||
'Accounts tracked for TDS',
|
||||
'Accounts not tracked',
|
||||
'Status',
|
||||
'Form',
|
||||
'Band',
|
||||
'Age',
|
||||
@@ -361,7 +423,14 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
r.deductee.name,
|
||||
r.deductee.customerId,
|
||||
r.deductee.pan || 'NOT ON RECORD',
|
||||
this.branchName(r.branchCode),
|
||||
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,
|
||||
@@ -389,6 +458,9 @@ export class TdsInterestReportComponent implements OnInit {
|
||||
`${s.deductees} deductees`,
|
||||
'',
|
||||
'',
|
||||
`${s.trackedCustomers} tracked`,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
|
||||
Reference in New Issue
Block a user