519 lines
16 KiB
TypeScript
519 lines
16 KiB
TypeScript
/**
|
|
* 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/.
|
|
*/
|
|
|
|
/** Angular Imports */
|
|
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
|
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
|
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
|
|
|
/** Custom Services. */
|
|
import { ClientsService } from 'app/clients/clients.service';
|
|
import {
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow,
|
|
MatFooterCellDef,
|
|
MatFooterCell,
|
|
MatFooterRowDef,
|
|
MatFooterRow
|
|
} from '@angular/material/table';
|
|
import { NgClass } from '@angular/common';
|
|
import { AccountNumberComponent } from '../../../shared/account-number/account-number.component';
|
|
import { LongTextComponent } from '../../../shared/long-text/long-text.component';
|
|
import { MatTooltip } from '@angular/material/tooltip';
|
|
import { StatusLookupPipe } from '../../../pipes/status-lookup.pipe';
|
|
import { AccountsFilterPipe } from '../../../pipes/accounts-filter.pipe';
|
|
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
|
import { FormatNumberPipe } from '../../../pipes/format-number.pipe';
|
|
import { CurrencyPipe } from '@angular/common';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
import { ReportsService } from 'app/reports/reports.service';
|
|
import { SettingsService } from 'app/settings/settings.service';
|
|
import { Subject } from 'rxjs';
|
|
import { takeUntil, catchError } from 'rxjs/operators';
|
|
import { AlertService } from 'app/core/alert/alert.service';
|
|
import { EMPTY } from 'rxjs';
|
|
|
|
/**
|
|
* General Tab component.
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-general-tab',
|
|
templateUrl: './general-tab.component.html',
|
|
styleUrls: ['./general-tab.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCellDef,
|
|
MatCell,
|
|
NgClass,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow,
|
|
MatFooterCellDef,
|
|
MatFooterCell,
|
|
MatFooterRowDef,
|
|
MatFooterRow,
|
|
AccountNumberComponent,
|
|
LongTextComponent,
|
|
MatTooltip,
|
|
StatusLookupPipe,
|
|
AccountsFilterPipe,
|
|
DateFormatPipe,
|
|
FormatNumberPipe,
|
|
CurrencyPipe
|
|
]
|
|
})
|
|
export class GeneralTabComponent implements OnDestroy {
|
|
private destroy$ = new Subject<void>();
|
|
private alertService = inject(AlertService);
|
|
private sanitizer = inject(DomSanitizer);
|
|
pdfUrl: SafeResourceUrl | null = null;
|
|
rawPdfUrl: string | null = null;
|
|
showPdf: boolean = false;
|
|
|
|
openLoanApplicationReport(event: MouseEvent, loanId: string) {
|
|
event.stopPropagation();
|
|
const tenantIdentifier = this.settingsService.tenantIdentifier || 'default';
|
|
let locale = this.settingsService.languageCode || 'en-US';
|
|
locale = locale.split('-')[0];
|
|
const dateFormat = this.settingsService.dateFormat || 'dd MMMM yyyy';
|
|
const formData = { R_loanId: loanId, 'output-type': 'PDF' };
|
|
this.reportsService
|
|
.getPentahoRunReportData('LoanApplicationReport', formData, tenantIdentifier, locale, dateFormat)
|
|
.pipe(
|
|
takeUntil(this.destroy$),
|
|
catchError((error): any => {
|
|
this.showPdf = false;
|
|
if (this.rawPdfUrl) {
|
|
URL.revokeObjectURL(this.rawPdfUrl);
|
|
this.rawPdfUrl = null;
|
|
}
|
|
this.pdfUrl = null;
|
|
this.alertService.alert({
|
|
type: 'error',
|
|
message: 'Failed to load Loan Application PDF report.'
|
|
});
|
|
return EMPTY;
|
|
})
|
|
)
|
|
.subscribe((res: any) => {
|
|
if (this.rawPdfUrl) {
|
|
URL.revokeObjectURL(this.rawPdfUrl);
|
|
this.rawPdfUrl = null;
|
|
this.pdfUrl = null;
|
|
}
|
|
const contentType = res.headers.get('Content-Type') || 'application/pdf';
|
|
const file = new Blob([res.body], { type: contentType });
|
|
this.rawPdfUrl = URL.createObjectURL(file);
|
|
this.pdfUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.rawPdfUrl);
|
|
this.showPdf = true;
|
|
});
|
|
}
|
|
|
|
closePdf() {
|
|
this.showPdf = false;
|
|
if (this.rawPdfUrl) {
|
|
URL.revokeObjectURL(this.rawPdfUrl);
|
|
this.rawPdfUrl = null;
|
|
}
|
|
this.pdfUrl = null;
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
if (this.rawPdfUrl) {
|
|
URL.revokeObjectURL(this.rawPdfUrl);
|
|
this.rawPdfUrl = null;
|
|
}
|
|
this.pdfUrl = null;
|
|
}
|
|
|
|
private route = inject(ActivatedRoute);
|
|
private clientService = inject(ClientsService);
|
|
private router = inject(Router);
|
|
private reportsService = inject(ReportsService);
|
|
private settingsService = inject(SettingsService);
|
|
|
|
/** Open Loan Accounts Columns */
|
|
openLoansColumns: string[] = [
|
|
'Account No',
|
|
'Loan Account',
|
|
'Original Loan',
|
|
'Loan Balance',
|
|
'Amount Paid',
|
|
'Type',
|
|
'Actions'
|
|
];
|
|
/** Closed Loan Accounts Columns */
|
|
closedLoansColumns: string[] = [
|
|
'Account No',
|
|
'Loan Account',
|
|
'Original Loan',
|
|
'Loan Balance',
|
|
'Amount Paid',
|
|
'Type',
|
|
'Closed Date',
|
|
'Actions'
|
|
];
|
|
/** Open Savings Accounts Columns */
|
|
openSavingsColumns: string[] = [
|
|
'Account No',
|
|
'Saving Account',
|
|
'Last Active',
|
|
'Balance',
|
|
'Actions'
|
|
];
|
|
/** Closed Savings Accounts Columns */
|
|
closedSavingsColumns: string[] = [
|
|
'Account No',
|
|
'Saving Account',
|
|
'Closed Date'
|
|
];
|
|
/** Closed Recurring Deposit Accounts Columns - with how each one ended, for tracking. */
|
|
recurringColumns: string[] = [
|
|
'Account No',
|
|
'Saving Account',
|
|
'Status',
|
|
'Last Active',
|
|
'Balance',
|
|
'Actions'
|
|
];
|
|
/** Open Shares Accounts Columns */
|
|
openSharesColumns: string[] = [
|
|
'Account No',
|
|
'Share Account',
|
|
'Approved Shares',
|
|
'Pending For Approval Shares',
|
|
'Actions'
|
|
];
|
|
/** Closed Shares Accounts Columns */
|
|
closedSharesColumns: string[] = [
|
|
'Account No',
|
|
'Share Account',
|
|
'Approved Shares',
|
|
'Pending For Approval Shares',
|
|
'Closed Date'
|
|
];
|
|
/** Upcoming Charges Columns */
|
|
upcomingChargesColumns: string[] = [
|
|
'Name',
|
|
'Due as of',
|
|
'Due',
|
|
'Paid',
|
|
'Waived',
|
|
'Outstanding',
|
|
'Actions'
|
|
];
|
|
/** Collaterals Column */
|
|
collateralsColumns: string[] = [
|
|
'ID',
|
|
'Name',
|
|
'Quantity',
|
|
'Total Value',
|
|
'Total Collateral Value'
|
|
];
|
|
|
|
/** Client Account Data */
|
|
clientAccountData: any;
|
|
/** Loan Accounts Data */
|
|
loanAccounts: any[] = [];
|
|
/** Savings Accounts Data (all: savings + FD + RD — filtered per section) */
|
|
savingAccounts: any[] = [];
|
|
|
|
get pureSavingsCount(): number {
|
|
return this.savingAccounts.filter((a: any) => a.depositType?.code === 'depositAccountType.savingsDeposit').length;
|
|
}
|
|
|
|
get fixedDepositCount(): number {
|
|
return this.fixedDepositAccountsForDisplay.filter(
|
|
(a: any) => a.depositType?.code === 'depositAccountType.fixedDeposit'
|
|
).length;
|
|
}
|
|
|
|
/**
|
|
* Accounts for the Fixed Deposit sections, with superseded renewal cycles folded away.
|
|
*
|
|
* An FD renewal closes one account and opens the next term as its own account row, so a renewed FD
|
|
* would otherwise appear twice on this page -- the matured term under Closed and the new term under
|
|
* Active -- reading as two unrelated deposits. A closed term is therefore dropped whenever a later
|
|
* cycle of the same FD (same `fdAccountNumber`) exists, leaving one row per FD that shows its
|
|
* current term. A chain that has genuinely ended keeps its final term, so it still lists as closed.
|
|
*
|
|
* Only FD rows are rewritten; savings, RD, current and share accounts pass through untouched.
|
|
*/
|
|
get fixedDepositAccountsForDisplay(): any[] {
|
|
// Memoised on the source array's identity. This getter is bound straight to two [dataSource] inputs, so
|
|
// returning a freshly built array on every change-detection pass would make the table re-bind and
|
|
// re-render its rows on every tick -- mouse moves included.
|
|
if (this.fdDisplayCacheSource !== this.savingAccounts) {
|
|
this.fdDisplayCacheSource = this.savingAccounts;
|
|
this.fdDisplayCache = this.foldSupersededFdCycles(this.savingAccounts);
|
|
}
|
|
return this.fdDisplayCache;
|
|
}
|
|
|
|
private fdDisplayCacheSource: any[] = null;
|
|
private fdDisplayCache: any[] = [];
|
|
|
|
private foldSupersededFdCycles(accounts: any[]): any[] {
|
|
const latestCycleByFd = new Map<string, number>();
|
|
for (const account of accounts) {
|
|
if (account?.depositType?.code !== 'depositAccountType.fixedDeposit') {
|
|
continue;
|
|
}
|
|
const fdNumber = account.fdAccountNumber ?? account.accountNo;
|
|
const cycle = account.renewalCycleNo ?? 0;
|
|
if (cycle > (latestCycleByFd.get(fdNumber) ?? -1)) {
|
|
latestCycleByFd.set(fdNumber, cycle);
|
|
}
|
|
}
|
|
|
|
return accounts.filter((account: any) => {
|
|
if (account?.depositType?.code !== 'depositAccountType.fixedDeposit') {
|
|
return true;
|
|
}
|
|
const fdNumber = account.fdAccountNumber ?? account.accountNo;
|
|
const cycle = account.renewalCycleNo ?? 0;
|
|
return cycle === latestCycleByFd.get(fdNumber);
|
|
});
|
|
}
|
|
|
|
get recurringDepositCount(): number {
|
|
return this.savingAccounts.filter((a: any) => a.depositType?.code === 'depositAccountType.recurringDeposit').length;
|
|
}
|
|
/** Current Accounts Data */
|
|
currentAccounts: any[] = [];
|
|
/** Shares Accounts Data */
|
|
shareAccounts: any[] = [];
|
|
/** Upcoming Charges Data */
|
|
upcomingCharges: any[] = [];
|
|
/** Performance History Data */
|
|
performanceHistory: {
|
|
loanCycle: number;
|
|
activeLoans: number;
|
|
lastLoanAmount: number;
|
|
activeSavings: number;
|
|
totalSavings: number;
|
|
} = {
|
|
loanCycle: 0,
|
|
activeLoans: 0,
|
|
lastLoanAmount: 0,
|
|
activeSavings: 0,
|
|
totalSavings: 0
|
|
};
|
|
/** Collaterals Data */
|
|
collaterals: any[] = [];
|
|
|
|
/** Show Closed Loan Accounts */
|
|
showClosedLoanAccounts = false;
|
|
/** Show Closed Saving Accounts */
|
|
showClosedSavingAccounts = false;
|
|
/** Show Closed Share Accounts */
|
|
showClosedShareAccounts = false;
|
|
/** Show Closed Reccuring Deposits Accounts */
|
|
showClosedRecurringAccounts = false;
|
|
/** Show Closed Fixed Deposits Accounts */
|
|
showClosedFixedAccounts = false;
|
|
|
|
/** Client Id */
|
|
clientid: any;
|
|
|
|
/**
|
|
* @param {ActivatedRoute} route Activated Route
|
|
* @param {ClientsService} clientService Clients Service
|
|
* @param {Router} router Router
|
|
*/
|
|
constructor() {
|
|
this.route.data.subscribe(
|
|
(data: { clientAccountsData: any; clientChargesData: any; clientSummary: any; clientCollateralData: any }) => {
|
|
this.clientAccountData = data.clientAccountsData;
|
|
this.savingAccounts = data.clientAccountsData?.savingsAccounts ?? [];
|
|
this.currentAccounts = data.clientAccountsData?.currentAccounts ?? [];
|
|
this.loanAccounts = data.clientAccountsData?.loanAccounts ?? [];
|
|
this.shareAccounts = data.clientAccountsData?.shareAccounts ?? [];
|
|
|
|
this.upcomingCharges = data.clientChargesData?.pageItems ?? [];
|
|
|
|
this.collaterals = data.clientCollateralData ?? [];
|
|
|
|
this.clientid = this.route.parent.snapshot.params['clientId'];
|
|
|
|
// Compute performance history from accounts data
|
|
this.computePerformanceHistory(data.clientAccountsData ?? { loanAccounts: [], savingsAccounts: [] });
|
|
}
|
|
);
|
|
}
|
|
|
|
private computePerformanceHistory(accountsData: any) {
|
|
// Loan Cycles: total number of loans
|
|
const allLoans = accountsData.loanAccounts || [];
|
|
this.performanceHistory.loanCycle = allLoans.length;
|
|
// Active Loans: status === 'Active'
|
|
this.performanceHistory.activeLoans = allLoans.filter((l: any) => {
|
|
if (!l.status) return false;
|
|
if (typeof l.status === 'string') {
|
|
return l.status.toLowerCase() === 'active';
|
|
}
|
|
if (typeof l.status === 'object' && l.status.value) {
|
|
return l.status.value.toLowerCase() === 'active';
|
|
}
|
|
return false;
|
|
}).length;
|
|
// Loan Amount: most recent loan by submittedOnDate
|
|
if (allLoans.length > 0) {
|
|
const sortedLoans = [...allLoans].sort(
|
|
(a, b) => new Date(b.submittedOnDate).getTime() - new Date(a.submittedOnDate).getTime()
|
|
);
|
|
this.performanceHistory.lastLoanAmount = sortedLoans[0].principal || 0;
|
|
} else {
|
|
this.performanceHistory.lastLoanAmount = 0;
|
|
}
|
|
// Active Savings: status === 'Active'
|
|
const allSavings = accountsData.savingsAccounts || [];
|
|
this.performanceHistory.activeSavings = allSavings.filter((s: any) => {
|
|
if (!s.status) return false;
|
|
if (typeof s.status === 'string') {
|
|
return s.status.toLowerCase() === 'active';
|
|
}
|
|
if (typeof s.status === 'object' && s.status.value) {
|
|
return s.status.value.toLowerCase() === 'active';
|
|
}
|
|
return false;
|
|
}).length;
|
|
// Total Savings: sum of balances
|
|
this.performanceHistory.totalSavings = allSavings.reduce((sum: number, s: any) => sum + (s.accountBalance || 0), 0);
|
|
}
|
|
|
|
/**
|
|
* Toggles Loan Accounts Overview
|
|
*/
|
|
toggleLoanAccountsOverview() {
|
|
this.showClosedLoanAccounts = !this.showClosedLoanAccounts;
|
|
}
|
|
|
|
/**
|
|
* Toggles Loan Accounts Overview
|
|
*/
|
|
toggleSavingAccountsOverview() {
|
|
this.showClosedSavingAccounts = !this.showClosedSavingAccounts;
|
|
}
|
|
|
|
/**
|
|
* Toggles Loan Accounts Overview
|
|
*/
|
|
toggleShareAccountsOverview() {
|
|
this.showClosedShareAccounts = !this.showClosedShareAccounts;
|
|
}
|
|
|
|
/**
|
|
* Toggles Reccuring Accounts Overview
|
|
*/
|
|
toggleRecurringAccountsOverview() {
|
|
this.showClosedRecurringAccounts = !this.showClosedRecurringAccounts;
|
|
}
|
|
|
|
/**
|
|
* Toggles Fixed Accounts Overview
|
|
*/
|
|
toggleFixedAccountsOverview() {
|
|
this.showClosedFixedAccounts = !this.showClosedFixedAccounts;
|
|
}
|
|
|
|
/**
|
|
* Waive Charge.
|
|
* @param chargeId Selected Charge Id.
|
|
* @param clientId Selected Client Id.
|
|
*/
|
|
waiveCharge(chargeId: string, clientId: string) {
|
|
const charge = { clientId: clientId.toString(), resourceType: chargeId };
|
|
this.clientService.waiveClientCharge(charge).subscribe(() => {
|
|
this.getChargeData(clientId);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get Charge Data.
|
|
* @param clientId Selected Client Id.
|
|
*/
|
|
getChargeData(clientId: string) {
|
|
this.clientService.getClientChargesData(clientId).subscribe((data: any) => {
|
|
this.upcomingCharges = data.pageItems;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stops the propagation to view pages.
|
|
* @param $event Mouse Event
|
|
*/
|
|
routeEdit($event: MouseEvent) {
|
|
$event.stopPropagation();
|
|
}
|
|
|
|
/**
|
|
* @param {any} loanId Loan Id
|
|
*/
|
|
routeTransferFund(loanId: any) {
|
|
const queryParams: any = { loanId: loanId, accountType: 'fromloans' };
|
|
this.router.navigate(
|
|
[
|
|
'../',
|
|
'loans-accounts',
|
|
loanId,
|
|
'transfer-funds',
|
|
'make-account-transfer'
|
|
],
|
|
{ relativeTo: this.route, queryParams: queryParams }
|
|
);
|
|
}
|
|
|
|
/** Where a recurring deposit stands - for one that has ended, how it ended. */
|
|
rdStatusLabel(account: any): string {
|
|
switch (account?.status?.code) {
|
|
case 'savingsAccountStatusType.active':
|
|
return 'Active';
|
|
case 'savingsAccountStatusType.matured':
|
|
return 'Matured (not closed)';
|
|
case 'savingsAccountStatusType.submitted.and.pending.approval':
|
|
return 'Pending Approval';
|
|
case 'savingsAccountStatusType.approved':
|
|
return 'Approved';
|
|
case 'savingsAccountStatusType.closed':
|
|
return 'Matured & Closed';
|
|
case 'savingsAccountStatusType.pre.mature.closure':
|
|
return 'Premature Closed';
|
|
case 'savingsAccountStatusType.rejected':
|
|
return 'Rejected';
|
|
case 'savingsAccountStatusType.withdrawn.by.applicant':
|
|
return 'Withdrawn by Client';
|
|
default:
|
|
return account?.status?.value ?? '';
|
|
}
|
|
}
|
|
|
|
viewAccountsLabel(closed: boolean): string {
|
|
if (closed) {
|
|
return 'labels.buttons.View Active Accounts';
|
|
} else {
|
|
return 'labels.buttons.View Closed Accounts';
|
|
}
|
|
}
|
|
}
|