/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 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 { SettingsService } from 'app/settings/settings.service'; import { exportWorkbook } from '../cashier/cashier-export'; import { InterestPostingReportService, InterestPostingSummaryRow } from '../interest-posting-report/interest-posting-report.service'; /** * The head office's view of savings interest across branches: for each posting date, branch and * product, how many customers were paid and how much, and whether it has been approved and posted. * Nothing is approved here — each branch approves its own — so it is a report and an Excel export. */ @Component({ selector: 'mifosx-interest-posting-summary', templateUrl: './interest-posting-summary.component.html', styleUrls: ['../interest-posting-report/interest-posting-report.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FormsModule, FaIconComponent ] }) export class InterestPostingSummaryComponent implements OnInit { private reportService = inject(InterestPostingReportService); private settingsService = inject(SettingsService); fromDate = ''; toDate = ''; productId: number | null = null; loading = false; errorMessage = ''; rows: InterestPostingSummaryRow[] = []; ngOnInit(): void { const today = this.businessDateIso(); this.toDate = today; this.fromDate = `${today.slice(0, 7)}-01`; this.load(); } load(): void { this.loading = true; this.errorMessage = ''; this.reportService.summary(this.fromDate || undefined, this.toDate || undefined).subscribe({ next: (rows) => { this.rows = rows ?? []; this.loading = false; }, error: (err) => { this.loading = false; this.rows = []; this.errorMessage = err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.defaultUserMessage ?? err?.error?.message ?? 'Could not load the interest summary.'; } }); } /** Products present in the loaded rows, for the filter. */ get products(): { id: number; name: string }[] { const seen = new Map(); this.rows.forEach((r) => seen.set(r.productId, r.productName)); return [...seen.entries()].map( ([ id, name ]) => ({ id, name }) ); } get shown(): InterestPostingSummaryRow[] { return this.productId ? this.rows.filter((r) => r.productId === this.productId) : this.rows; } get totalCustomers(): number { return this.shown.reduce((sum, r) => sum + (r.accountCount || 0), 0); } get totalInterest(): number { return this.shown.reduce((sum, r) => sum + Number(r.totalInterest || 0), 0); } get postedInterest(): number { return this.shown.filter((r) => r.status === 'POSTED').reduce((sum, r) => sum + Number(r.totalInterest || 0), 0); } get pendingCount(): number { return this.shown.filter((r) => r.status !== 'POSTED').length; } /** Branch totals, for the second table and the export. */ get byBranch(): { officeName: string; customers: number; interest: number; posted: number; reports: number }[] { const map = new Map< string, { officeName: string; customers: number; interest: number; posted: number; reports: number } >(); this.shown.forEach((r) => { const key = r.officeName || '—'; const row = map.get(key) ?? { officeName: key, customers: 0, interest: 0, posted: 0, reports: 0 }; row.customers += r.accountCount || 0; row.interest += Number(r.totalInterest || 0); row.posted += r.status === 'POSTED' ? Number(r.totalInterest || 0) : 0; row.reports += 1; map.set(key, row); }); return [...map.values()].sort((a, b) => a.officeName.localeCompare(b.officeName)); } statusClass(status: string): string { return ( { GENERATED: 'ipr-badge--pending', APPROVED: 'ipr-badge--approved', POSTED: 'ipr-badge--posted' }[status] ?? 'ipr-badge--muted' ); } exportExcel(): void { const period = `Posting dates ${this.fromDate || '…'} to ${this.toDate || '…'}`; const product = this.productId ? this.products.find((p) => p.id === this.productId)?.name : 'All products'; exportWorkbook( [ { sheetName: 'By report', title: 'Savings Interest — Branch-wise Posting', subtitle: `${period} · ${product}`, headers: [ 'Posting Date', 'Branch', 'Product', 'Customers', 'Interest', 'Status', 'Approved By', 'Approved At', 'Posted At' ], widths: [ 14, 22, 24, 12, 16, 12, 20, 20, 20 ], rows: [ ...this.shown.map((r) => [ r.postingDate, r.officeName, r.productName, r.accountCount, Number(r.totalInterest || 0), r.status, (r.approvedBy || '') + (r.approvedOnBehalf ? ' (HO)' : ''), r.approvedAt || '', r.postedAt || '' ]), [ 'TOTAL', '', '', this.totalCustomers, this.totalInterest, '', '', '', '' ] ] }, { sheetName: 'By branch', title: 'Savings Interest — Branch Totals', subtitle: `${period} · ${product}`, headers: [ 'Branch', 'Reports', 'Customers', 'Interest', 'Posted', 'Not Yet Posted' ], widths: [ 22, 10, 12, 16, 16, 16 ], rows: [ ...this.byBranch.map((b) => [ b.officeName, b.reports, b.customers, b.interest, b.posted, b.interest - b.posted ]), [ 'TOTAL', this.shown.length, this.totalCustomers, this.totalInterest, this.postedInterest, this.totalInterest - this.postedInterest ] ] } ], `savings-interest-branch-summary-${this.fromDate}-to-${this.toDate}` ); } private businessDateIso(): string { const businessDate = this.settingsService.businessDate as any; const parsed = businessDate ? new Date(businessDate) : new Date(); if (isNaN(parsed.getTime())) { return new Date().toISOString().slice(0, 10); } const month = String(parsed.getMonth() + 1).padStart(2, '0'); const day = String(parsed.getDate()).padStart(2, '0'); return `${parsed.getFullYear()}-${month}-${day}`; } }