feat: interest posting
This commit is contained in:
@@ -11,11 +11,14 @@ import { FormsModule } from '@angular/forms';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { exportWorkbook } from '../cashier/cashier-export';
|
||||
import {
|
||||
InterestPostingDue,
|
||||
InterestPostingReport,
|
||||
InterestPostingReportDetail,
|
||||
InterestPostingReportService
|
||||
InterestPostingReportService,
|
||||
InterestPreviewRow,
|
||||
ReportScope
|
||||
} from './interest-posting-report.service';
|
||||
|
||||
/**
|
||||
@@ -51,15 +54,48 @@ export class InterestPostingReportComponent implements OnInit {
|
||||
/** Products with interest due and no report yet — the noon job ran before they had anything due, or failed. */
|
||||
due: InterestPostingDue[] = [];
|
||||
|
||||
/** 'own' — the user's own branch; 'others' — every other branch, a head-office-only tab. */
|
||||
tab: ReportScope = 'own';
|
||||
isHeadOffice = false;
|
||||
myBranch = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.postingDate = this.businessDateIso();
|
||||
this.reportService.context().subscribe({
|
||||
next: (ctx) => {
|
||||
this.isHeadOffice = !!ctx?.headOffice;
|
||||
this.myBranch = ctx?.officeName ?? '';
|
||||
},
|
||||
error: () => (this.isHeadOffice = false)
|
||||
});
|
||||
this.load();
|
||||
}
|
||||
|
||||
switchTab(tab: ReportScope): void {
|
||||
if (this.tab === tab) {
|
||||
return;
|
||||
}
|
||||
this.tab = tab;
|
||||
this.selected = null;
|
||||
this.notice = '';
|
||||
this.load();
|
||||
}
|
||||
|
||||
/** Acting for another branch is the head office's exception, so it is spelt out before it happens. */
|
||||
private confirmOnBehalf(officeName: string, action: string): boolean {
|
||||
if (this.tab !== 'others') {
|
||||
return true;
|
||||
}
|
||||
return confirm(
|
||||
`${officeName} has not opened today. You are about to ${action} its interest posting report on its behalf, ` +
|
||||
'from the head office. Continue?'
|
||||
);
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.errorMessage = '';
|
||||
this.reportService.list(this.postingDate || undefined).subscribe({
|
||||
this.reportService.list(this.postingDate || undefined, undefined, undefined, this.tab).subscribe({
|
||||
next: (reports) => {
|
||||
this.reports = reports ?? [];
|
||||
this.loading = false;
|
||||
@@ -75,13 +111,16 @@ export class InterestPostingReportComponent implements OnInit {
|
||||
this.errorMessage = this.messageOf(err, 'Could not load the interest posting reports.');
|
||||
}
|
||||
});
|
||||
this.reportService.dueWithoutReport(this.postingDate || undefined).subscribe({
|
||||
this.reportService.dueWithoutReport(this.postingDate || undefined, this.tab).subscribe({
|
||||
next: (due) => (this.due = due ?? []),
|
||||
error: () => (this.due = [])
|
||||
});
|
||||
}
|
||||
|
||||
generate(item: InterestPostingDue): void {
|
||||
if (!this.confirmOnBehalf(item.officeName, 'generate')) {
|
||||
return;
|
||||
}
|
||||
this.busy = true;
|
||||
this.notice = '';
|
||||
this.errorMessage = '';
|
||||
@@ -111,7 +150,8 @@ export class InterestPostingReportComponent implements OnInit {
|
||||
if (
|
||||
!confirm(
|
||||
`Regenerate the interest posting report for ${report.productName} — ${report.officeName}? It will need approving again.`
|
||||
)
|
||||
) ||
|
||||
!this.confirmOnBehalf(report.officeName, 'regenerate')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -138,7 +178,8 @@ export class InterestPostingReportComponent implements OnInit {
|
||||
!confirm(
|
||||
`Approve interest of ₹${report.totalInterest} across ${report.accountCount} account(s) for ${report.productName}? ` +
|
||||
'It will be posted when the branch closes the day.'
|
||||
)
|
||||
) ||
|
||||
!this.confirmOnBehalf(report.officeName, 'approve')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -177,8 +218,165 @@ export class InterestPostingReportComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
/** Pending reports that block this user's own day close; another branch's pending reports block only that branch. */
|
||||
get pendingCount(): number {
|
||||
return this.reports.filter((r) => r.status === 'GENERATED').length;
|
||||
return this.tab === 'own' ? this.reports.filter((r) => r.status === 'GENERATED').length : 0;
|
||||
}
|
||||
|
||||
// ── Preview: what each account has earned so far, as if posted today. View only. ──
|
||||
|
||||
showPreview = false;
|
||||
previewLoading = false;
|
||||
previewRows: InterestPreviewRow[] = [];
|
||||
previewAsOf = '';
|
||||
|
||||
openPreview(): void {
|
||||
this.showPreview = true;
|
||||
this.previewLoading = true;
|
||||
this.errorMessage = '';
|
||||
this.reportService.preview().subscribe({
|
||||
next: (rows) => {
|
||||
this.previewRows = rows ?? [];
|
||||
this.previewAsOf = this.previewRows[0]?.asOf ?? this.businessDateIso();
|
||||
this.previewLoading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.previewLoading = false;
|
||||
this.previewRows = [];
|
||||
this.errorMessage = this.messageOf(err, 'Could not load the interest preview.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closePreview(): void {
|
||||
this.showPreview = false;
|
||||
}
|
||||
|
||||
get previewTotal(): number {
|
||||
return this.previewRows.reduce((sum, r) => sum + Number(r.interest || 0), 0);
|
||||
}
|
||||
|
||||
/** Totals by branch and product, for the summary above the account list and the export. */
|
||||
get previewGroups(): {
|
||||
officeName: string;
|
||||
productName: string;
|
||||
postingDate: string;
|
||||
accounts: number;
|
||||
interest: number;
|
||||
}[] {
|
||||
const map = new Map<
|
||||
string,
|
||||
{ officeName: string; productName: string; postingDate: string; accounts: number; interest: number }
|
||||
>();
|
||||
this.previewRows.forEach((r) => {
|
||||
const key = `${r.officeName}|${r.productName}`;
|
||||
const g = map.get(key) ?? {
|
||||
officeName: r.officeName,
|
||||
productName: r.productName,
|
||||
postingDate: r.postingDate,
|
||||
accounts: 0,
|
||||
interest: 0
|
||||
};
|
||||
g.accounts += 1;
|
||||
g.interest += Number(r.interest || 0);
|
||||
map.set(key, g);
|
||||
});
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
exportPreview(): void {
|
||||
const stamp = `Interest earned to ${this.previewAsOf} — preview, not posted`;
|
||||
exportWorkbook(
|
||||
[
|
||||
{
|
||||
sheetName: 'By account',
|
||||
title: 'Savings Interest Preview',
|
||||
subtitle: stamp,
|
||||
headers: [
|
||||
'Branch',
|
||||
'Product',
|
||||
'Account No',
|
||||
'Customer',
|
||||
'Balance',
|
||||
'Rate %',
|
||||
'Cycle Start',
|
||||
'Posting Date',
|
||||
'Interest So Far'
|
||||
],
|
||||
widths: [
|
||||
18,
|
||||
22,
|
||||
20,
|
||||
26,
|
||||
16,
|
||||
8,
|
||||
12,
|
||||
12,
|
||||
16
|
||||
],
|
||||
rows: [
|
||||
...this.previewRows.map((r) => [
|
||||
r.officeName,
|
||||
r.productName,
|
||||
r.accountNo,
|
||||
r.clientName,
|
||||
Number(r.balance || 0),
|
||||
Number(r.rate || 0),
|
||||
r.cycleStart,
|
||||
r.postingDate,
|
||||
Number(r.interest || 0)
|
||||
]),
|
||||
[
|
||||
'TOTAL',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
this.previewTotal
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
sheetName: 'By branch & product',
|
||||
title: 'Savings Interest Preview — Totals',
|
||||
subtitle: stamp,
|
||||
headers: [
|
||||
'Branch',
|
||||
'Product',
|
||||
'Posting Date',
|
||||
'Accounts',
|
||||
'Interest So Far'
|
||||
],
|
||||
widths: [
|
||||
18,
|
||||
22,
|
||||
12,
|
||||
10,
|
||||
16
|
||||
],
|
||||
rows: [
|
||||
...this.previewGroups.map((g) => [
|
||||
g.officeName,
|
||||
g.productName,
|
||||
g.postingDate,
|
||||
g.accounts,
|
||||
g.interest
|
||||
]),
|
||||
[
|
||||
'TOTAL',
|
||||
'',
|
||||
'',
|
||||
this.previewRows.length,
|
||||
this.previewTotal
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
`savings-interest-preview-${this.previewAsOf}`
|
||||
);
|
||||
}
|
||||
|
||||
private businessDateIso(): string {
|
||||
|
||||
Reference in New Issue
Block a user