480 lines
16 KiB
TypeScript
480 lines
16 KiB
TypeScript
/** Angular Imports */
|
|
import { Component, OnInit } from '@angular/core';
|
|
import { ActivatedRoute, NavigationExtras, Router, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
|
|
/** Custom Services */
|
|
import { LoansService } from '../loans.service';
|
|
|
|
/** Custom Buttons Configuration */
|
|
import { LoansAccountButtonConfiguration } from './loan-accounts-button-config';
|
|
|
|
/** Dialog Components */
|
|
import { ConfirmationDialogComponent } from '../../shared/confirmation-dialog/confirmation-dialog.component';
|
|
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
|
import { LoanStatus } from '../models/loan-status.model';
|
|
import { Currency } from 'app/shared/models/general.model';
|
|
import { DelinquencyPausePeriod } from '../models/loan-account.model';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { LoanTransaction } from 'app/products/loan-products/models/loan-account.model';
|
|
import { OptionData } from 'app/shared/models/option-data.model';
|
|
import { MatCardHeader, MatCardTitleGroup, MatCardTitle } from '@angular/material/card';
|
|
import { SvgIconComponent } from '../../shared/svg-icon/svg-icon.component';
|
|
import { MatTooltip } from '@angular/material/tooltip';
|
|
import { NgClass, CurrencyPipe } from '@angular/common';
|
|
import { LongTextComponent } from '../../shared/long-text/long-text.component';
|
|
import { AccountNumberComponent } from '../../shared/account-number/account-number.component';
|
|
import { MatIconButton } from '@angular/material/button';
|
|
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
|
|
import { MatIcon } from '@angular/material/icon';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
|
|
import { StatusLookupPipe } from '../../pipes/status-lookup.pipe';
|
|
import { DateFormatPipe } from '../../pipes/date-format.pipe';
|
|
import { FormatNumberPipe } from '../../pipes/format-number.pipe';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
import { LoanProducts } from 'app/products/loan-products/loan-products';
|
|
|
|
@Component({
|
|
selector: 'mifosx-loans-view',
|
|
templateUrl: './loans-view.component.html',
|
|
styleUrls: ['./loans-view.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatCardHeader,
|
|
MatCardTitleGroup,
|
|
SvgIconComponent,
|
|
MatTooltip,
|
|
MatCardTitle,
|
|
NgClass,
|
|
LongTextComponent,
|
|
AccountNumberComponent,
|
|
MatIconButton,
|
|
MatMenuTrigger,
|
|
MatIcon,
|
|
FaIconComponent,
|
|
MatMenu,
|
|
MatMenuItem,
|
|
MatTabNav,
|
|
MatTabLink,
|
|
RouterLinkActive,
|
|
MatTabNavPanel,
|
|
RouterOutlet,
|
|
CurrencyPipe,
|
|
StatusLookupPipe,
|
|
DateFormatPipe,
|
|
FormatNumberPipe
|
|
]
|
|
})
|
|
export class LoansViewComponent implements OnInit {
|
|
/** Loan Details Data */
|
|
loanDetailsData: any;
|
|
/** Loan Datatables */
|
|
loanDatatables: any;
|
|
/** Recalculate Interest */
|
|
recalculateInterest: any;
|
|
/** loan Arrears Delinquency config value */
|
|
loanDisplayArrearsDelinquency: number;
|
|
/** Status */
|
|
status: string;
|
|
entityType: string;
|
|
/** Loan Id */
|
|
loanId: number;
|
|
/** Client Id */
|
|
clientId: number;
|
|
/** Button Configuration */
|
|
buttonConfig: LoansAccountButtonConfiguration;
|
|
/** Disburse Transaction number */
|
|
disburseTransactionNo = 0;
|
|
|
|
loanDelinquencyClassificationStyle = '';
|
|
loanStatus: LoanStatus;
|
|
loanSubStatus: OptionData | null = null;
|
|
currency: Currency;
|
|
loanReAged = false;
|
|
loanReAmortized = false;
|
|
|
|
constructor(
|
|
private route: ActivatedRoute,
|
|
private router: Router,
|
|
public loansService: LoansService,
|
|
private translateService: TranslateService,
|
|
public dialog: MatDialog
|
|
) {
|
|
this.route.data.subscribe(
|
|
(data: { loanDetailsData: any; loanDatatables: any; loanArrearsDelinquencyConfig: any }) => {
|
|
this.loanDetailsData = data.loanDetailsData;
|
|
this.loanDatatables = data.loanDatatables;
|
|
this.loanDisplayArrearsDelinquency = data.loanArrearsDelinquencyConfig.value || 0;
|
|
this.loanStatus = this.loanDetailsData.status;
|
|
this.loanSubStatus = this.loanDetailsData.subStatus === undefined ? null : this.loanDetailsData.subStatus;
|
|
this.currency = this.loanDetailsData.currency;
|
|
loansService.saveLoanDisbursementDetailsData(this.loanDetailsData.disbursementDetails);
|
|
if (this.loanStatus.active) {
|
|
this.loanDetailsData.transactions.forEach((lt: LoanTransaction) => {
|
|
if (!lt.manuallyReversed) {
|
|
if (lt.type.reAge) {
|
|
this.loanReAged = true;
|
|
} else if (lt.type.reAmortize) {
|
|
this.loanReAmortized = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
this.setConditionalButtons();
|
|
}
|
|
);
|
|
this.loanId = this.route.snapshot.params['loanId'];
|
|
this.clientId = this.loanDetailsData.clientId;
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.route.params.subscribe((params) => {
|
|
if (this.loanId != params['loanId']) {
|
|
this.loanId = params['loanId'];
|
|
this.reload();
|
|
}
|
|
});
|
|
this.recalculateInterest = this.loanDetailsData.recalculateInterest || true;
|
|
this.status = this.loanDetailsData.status.value;
|
|
this.loanStatus = this.loanDetailsData.status;
|
|
this.loanSubStatus = this.loanDetailsData.subStatus === undefined ? null : this.loanDetailsData.subStatus;
|
|
if (this.loanStatus.active && this.loanDetailsData.multiDisburseLoan) {
|
|
if (this.loanDetailsData && this.loanDetailsData.transactions) {
|
|
this.loanDetailsData.transactions.forEach((transaction: any) => {
|
|
if (transaction.type.disbursement) {
|
|
this.disburseTransactionNo++;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
this.setConditionalButtons();
|
|
if (this.router.url.includes('clients')) {
|
|
this.entityType = 'Client';
|
|
} else if (this.router.url.includes('groups')) {
|
|
this.entityType = 'Group';
|
|
} else if (this.router.url.includes('centers')) {
|
|
this.entityType = 'Center';
|
|
}
|
|
this.loanDelinquencyClassification();
|
|
}
|
|
|
|
// Defines the buttons based on the status of the loan account
|
|
setConditionalButtons() {
|
|
this.buttonConfig = new LoansAccountButtonConfiguration(this.status, this.loanSubStatus);
|
|
|
|
if (this.status === 'Submitted and pending approval') {
|
|
this.buttonConfig.addOption({
|
|
name: this.loanDetailsData.loanOfficerName ? 'Change Loan Officer' : 'Assign Loan Officer',
|
|
icon: 'user-tie',
|
|
taskPermissionName: 'DISBURSE_LOAN'
|
|
});
|
|
|
|
if (this.loanDetailsData.isVariableInstallmentsAllowed) {
|
|
this.buttonConfig.addOption({
|
|
name: 'Edit Repayment Schedule',
|
|
icon: 'edit',
|
|
taskPermissionName: 'ADJUST_REPAYMENT_SCHEDULE'
|
|
});
|
|
}
|
|
} else if (this.status === 'Approved') {
|
|
this.buttonConfig.addButton({
|
|
name: this.loanDetailsData.loanOfficerName ? 'Change Loan Officer' : 'Assign Loan Officer',
|
|
icon: 'user-tie',
|
|
taskPermissionName: 'DISBURSE_LOAN'
|
|
});
|
|
} else if (this.status === 'Active') {
|
|
if (this.loanDetailsData.enableBuyDownFee) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Buy Down Fee',
|
|
icon: 'plus',
|
|
taskPermissionName: 'BUYDOWNFEE_LOAN'
|
|
});
|
|
}
|
|
if (this.loanDetailsData.enableIncomeCapitalization) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Capitalized Income',
|
|
icon: 'coins',
|
|
taskPermissionName: 'CAPITALIZEDINCOME_LOAN'
|
|
});
|
|
}
|
|
|
|
if (this.loanDetailsData.canDisburse || this.loanDetailsData.multiDisburseLoan) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Disburse',
|
|
icon: 'hand-holding-usd',
|
|
taskPermissionName: 'DISBURSE_LOAN'
|
|
});
|
|
}
|
|
if (this.loanDetailsData.canDisburse) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Disburse to Savings',
|
|
icon: 'piggy-bank',
|
|
taskPermissionName: 'DISBURSETOSAVINGS_LOAN'
|
|
});
|
|
}
|
|
if (this.loanDetailsData.multiDisburseLoan && this.disburseTransactionNo > 1) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Undo Last Disbursal',
|
|
icon: 'undo',
|
|
taskPermissionName: 'DISBURSALLASTUNDO_LOAN'
|
|
});
|
|
}
|
|
if (this.recalculateInterest) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Add Interest Pause',
|
|
icon: 'calendar',
|
|
taskPermissionName: 'CREATE_INTEREST_PAUSE'
|
|
});
|
|
}
|
|
// loan officer not assigned to loan, below logic
|
|
// helps to display otherwise not
|
|
if (!this.loanDetailsData.loanOfficerName) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Assign Loan Officer',
|
|
icon: 'user-tie',
|
|
taskPermissionName: 'UPDATELOANOFFICER_LOAN'
|
|
});
|
|
}
|
|
|
|
if (this.recalculateInterest) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Prepay Loan',
|
|
icon: 'coins',
|
|
taskPermissionName: 'REPAYMENT_LOAN'
|
|
});
|
|
}
|
|
|
|
// Allow ChargeOff only If there loan is not already ChargeOff
|
|
if (!this.loanDetailsData.chargedOff) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Charge-Off',
|
|
icon: 'coins',
|
|
taskPermissionName: 'CHARGEOFF_LOAN'
|
|
});
|
|
} else {
|
|
this.buttonConfig.addButton({
|
|
name: 'Undo Charge-Off',
|
|
icon: 'undo',
|
|
taskPermissionName: 'UNDOCHARGEOFF_LOAN'
|
|
});
|
|
}
|
|
|
|
// Allow Re-Ageing only when there is not any Re-Age transaction
|
|
if (!this.loanReAged) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Re-Age',
|
|
icon: 'calendar',
|
|
taskPermissionName: 'REAGE_LOAN'
|
|
});
|
|
} else {
|
|
this.buttonConfig.addButton({
|
|
name: 'Undo Re-Age',
|
|
icon: 'undo',
|
|
taskPermissionName: 'UNDO_REAGE_LOAN'
|
|
});
|
|
}
|
|
|
|
if (!this.loanReAmortized) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Re-Amortize',
|
|
icon: 'calendar-alt',
|
|
taskPermissionName: 'REAMORTIZE_LOAN'
|
|
});
|
|
} else {
|
|
this.buttonConfig.addButton({
|
|
name: 'Undo Re-Amortize',
|
|
icon: 'undo',
|
|
taskPermissionName: 'UNDO_REAMORTIZE_LOAN'
|
|
});
|
|
}
|
|
} else if (this.status === 'Closed (obligations met)' || this.status === 'Overpaid') {
|
|
if (this.loanDetailsData.multiDisburseLoan) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Disburse',
|
|
icon: 'hand-holding-usd',
|
|
taskPermissionName: 'DISBURSE_LOAN'
|
|
});
|
|
}
|
|
if (LoanProducts.isAdvancedPaymentAllocationStrategy(this.loanDetailsData.transactionProcessingStrategyCode)) {
|
|
this.buttonConfig.addButton({
|
|
name: 'Reschedule',
|
|
icon: 'calendar',
|
|
taskPermissionName: 'CREATE_RESCHEDULELOAN'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
loanAction(actionName: string) {
|
|
switch (actionName) {
|
|
case 'Recover From Guarantor':
|
|
this.recoverFromGuarantor();
|
|
break;
|
|
case 'Delete':
|
|
this.deleteLoanAccount();
|
|
break;
|
|
case 'Modify Application':
|
|
this.router.navigate(['edit-loans-account'], { relativeTo: this.route });
|
|
break;
|
|
case 'Transfer Funds':
|
|
const queryParams: any = { loanId: this.loanId, accountType: 'fromloans' };
|
|
this.router.navigate(['transfer-funds/make-account-transfer'], {
|
|
relativeTo: this.route,
|
|
queryParams: queryParams
|
|
});
|
|
break;
|
|
case 'Undo Re-Age':
|
|
case 'Undo Re-Amortize':
|
|
case 'Undo Charge-Off':
|
|
this.undoLoanAction(actionName);
|
|
break;
|
|
default:
|
|
const navigationExtras: NavigationExtras = {
|
|
relativeTo: this.route,
|
|
state: {
|
|
data: this.loanDetailsData
|
|
}
|
|
};
|
|
this.router.navigate(
|
|
[
|
|
'actions',
|
|
actionName
|
|
],
|
|
navigationExtras
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recover from guarantor action
|
|
*/
|
|
private recoverFromGuarantor() {
|
|
const recoverFromGuarantorDialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
|
data: {
|
|
heading: this.translateService.instant('labels.heading.Recover from Guarantor'),
|
|
dialogContext: this.translateService.instant(
|
|
'labels.dialogContext.Are you sure you want recover from Guarantor'
|
|
),
|
|
type: 'Mild'
|
|
}
|
|
});
|
|
recoverFromGuarantorDialogRef.afterClosed().subscribe((response: any) => {
|
|
if (response.confirm) {
|
|
this.loansService.loanActionButtons(this.loanId, 'recoverGuarantees').subscribe(() => {
|
|
this.reload();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
loanDelinquencyClassification(): void {
|
|
this.loanDelinquencyClassificationStyle = '';
|
|
if (this.loanDetailsData.delinquent && this.loanDetailsData.delinquent.delinquencyPausePeriods) {
|
|
this.loanDetailsData.delinquent.delinquencyPausePeriods.some((period: DelinquencyPausePeriod) => {
|
|
if (period.active) {
|
|
this.loanDelinquencyClassificationStyle = 'fa fa-stop status-pending';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
undoLoanAction(actionName: string): void {
|
|
actionName = actionName.replace('Undo ', '');
|
|
const undoTransactionAccountDialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
|
data: {
|
|
heading: this.translateService.instant('labels.heading.Undo Transaction'),
|
|
dialogContext:
|
|
this.translateService.instant('labels.dialogContext.Are you sure you want undo the transaction type') +
|
|
' ' +
|
|
this.translateService.instant('labels.menus.' + actionName)
|
|
}
|
|
});
|
|
undoTransactionAccountDialogRef.afterClosed().subscribe((response: any) => {
|
|
if (response.confirm) {
|
|
let undoCommand: string = '';
|
|
switch (actionName) {
|
|
case 'Re-Age':
|
|
undoCommand = 'undoReAge';
|
|
break;
|
|
case 'Re-Amortize':
|
|
undoCommand = 'undoReAmortize';
|
|
break;
|
|
case 'Charge-Off':
|
|
undoCommand = 'undo-charge-off';
|
|
break;
|
|
}
|
|
this.loansService.executeLoansAccountTransactionsCommand(String(this.loanId), undoCommand, {}).subscribe(() => {
|
|
this.reload();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
iconLoanStatusColor() {
|
|
if (this.loanDetailsData.chargedOff) {
|
|
return 'loanStatusType.chargeoff';
|
|
}
|
|
if (this.isContractTermination(this.loanSubStatus)) {
|
|
return 'loanSubStatusType.contractTermination';
|
|
}
|
|
if (this.loanDetailsData.inArrears) {
|
|
return 'loanStatusType.activeOverdue';
|
|
}
|
|
return this.loanDetailsData.status.code;
|
|
}
|
|
|
|
loanStatusTooltip() {
|
|
if (this.loanDetailsData.chargedOff) {
|
|
return 'Chargeoff';
|
|
}
|
|
if (this.loanDetailsData.inArrears) {
|
|
return 'activeOverdue';
|
|
}
|
|
return this.loanDetailsData.status.code;
|
|
}
|
|
|
|
loanSubStatusTooltip() {
|
|
if (this.isContractTermination(this.loanSubStatus)) {
|
|
return 'contractTermination';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Delete loan Account
|
|
*/
|
|
private deleteLoanAccount() {
|
|
const deleteGuarantorDialogRef = this.dialog.open(DeleteDialogComponent, {
|
|
data: { deleteContext: `with loan id: ${this.loanId}` }
|
|
});
|
|
deleteGuarantorDialogRef.afterClosed().subscribe((response: any) => {
|
|
if (response.delete) {
|
|
this.loansService.deleteLoanAccount(this.loanId).subscribe(() => {
|
|
this.router.navigate(['../../'], { relativeTo: this.route });
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refetches data for the component
|
|
* TODO: Replace by a custom reload component instead of hard-coded back-routing.
|
|
*/
|
|
private reload() {
|
|
const clientId = this.clientId;
|
|
const url: string = this.router.url;
|
|
this.router
|
|
.navigateByUrl(`/clients/${clientId}/loans-accounts`, { skipLocationChange: true })
|
|
.then(() => this.router.navigate([url]));
|
|
}
|
|
|
|
private isContractTermination(substatus: OptionData): boolean {
|
|
if (substatus == null) {
|
|
return false;
|
|
}
|
|
return substatus.code === 'loanSubStatus.loanSubStatusType.contractTermination';
|
|
}
|
|
}
|