Fix Loan product Payment Allocation (#1855)

Co-authored-by: Jose Alberto Hernandez <alberto@blue-daemon.local>
This commit is contained in:
José Alberto Hernández
2023-08-29 20:29:54 -06:00
committed by GitHub
co-authored by Jose Alberto Hernandez
parent 80936a3ea9
commit 32a9db1f6d
15 changed files with 164 additions and 125 deletions
@@ -108,7 +108,7 @@
</mat-menu>
<mat-menu #accountApplications="matMenu">
<button mat-menu-item *mifosxHasPermission="'CREATE_LOANACCOUNT'"
<button mat-menu-item *mifosxHasPermission="'CREATE_LOAN'"
[routerLink]="['loans-accounts', 'create']">New Loan Account</button>
<button mat-menu-item *mifosxHasPermission="'CREATE_SAVINGSACCOUNT'"
[routerLink]="['savings-accounts', 'create']">New Savings Account</button>
@@ -69,7 +69,7 @@
<button mat-menu-item routerLink="/clients">{{ 'labels.menus.Clients' | translate }}</button>
<button mat-menu-item routerLink="/groups">{{ 'labels.menus.Groups' | translate }}</button>
<button mat-menu-item routerLink="/centers">{{ 'labels.menus.Centers' | translate }}</button>
<span fxHide.lg>
<span fxHide.lt-lg="true">
<button mat-menu-item [routerLink]="['/accounting']">{{ 'labels.menus.Accounting' | translate }}</button>
<button mat-menu-item [matMenuTriggerFor]="reportsMenu">{{ 'labels.menus.Reports' | translate }}</button>
<button mat-menu-item [matMenuTriggerFor]="adminMenu">{{ 'labels.menus.Admin' | translate }}</button>
@@ -89,11 +89,11 @@
</mat-menu>
<mat-menu #adminMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item [routerLink]="['/users']">{{ 'labels.menus.Users' | translate }}</button>
<button mat-menu-item (click)="navigateMenu('/appusers')">{{ 'labels.menus.Users' | translate }}</button>
<button mat-menu-item [routerLink]="['/organization']">{{ 'labels.menus.Organization' | translate }}</button>
<button mat-menu-item [routerLink]="['/system']">{{ 'labels.menus.System' | translate }}</button>
<button mat-menu-item [routerLink]="['/products']">{{ 'labels.menus.Products' | translate }}</button>
<button mat-menu-item [routerLink]="['/templates']">{{ 'labels.menus.Templates' | translate }}</button>
<button mat-menu-item routerLink="/templates">{{ 'labels.menus.Templates' | translate }}</button>
</mat-menu>
<mat-menu #selfServiceMenu="matMenu" [overlapTrigger]="false">
@@ -1,5 +1,5 @@
/** Angular Imports */
import { Component, OnInit, Input, EventEmitter, Output, ViewChild, AfterViewInit, ElementRef, TemplateRef } from '@angular/core';
import { Component, OnInit, Input, EventEmitter, Output, ViewChild, AfterViewInit, ElementRef, TemplateRef, AfterContentChecked, ChangeDetectorRef } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSidenav } from '@angular/material/sidenav';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
@@ -25,7 +25,7 @@ import { ConfigurationWizardComponent } from '../../../configuration-wizard/conf
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss']
})
export class ToolbarComponent implements OnInit, AfterViewInit {
export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChecked {
/* Reference of institution */
@ViewChild('institution') institution: ElementRef<any>;
@@ -64,7 +64,8 @@ export class ToolbarComponent implements OnInit, AfterViewInit {
private authenticationService: AuthenticationService,
private popoverService: PopoverService,
private configurationWizardService: ConfigurationWizardService,
private dialog: MatDialog) { }
private dialog: MatDialog,
private changeDetector: ChangeDetectorRef) { }
/**
* Subscribes to breakpoint for handset.
@@ -77,6 +78,10 @@ export class ToolbarComponent implements OnInit, AfterViewInit {
});
}
ngAfterContentChecked(): void {
this.changeDetector.detectChanges();
}
/**
* Toggles the current state of sidenav.
*/
@@ -187,4 +192,10 @@ export class ToolbarComponent implements OnInit, AfterViewInit {
});
}
}
navigateMenu(routePath: string): void {
console.log(routePath);
this.router.navigate([routePath]);
}
}
@@ -63,9 +63,10 @@
<ng-template matStepLabel>PAYMENT ALLOCATION</ng-template>
<mifosx-loan-product-payment-strategy-step
<mifosx-loan-product-payment-strategy-step *ngFor="let transactionType of transactionTypes"
[loanProductsTemplate]="loanProductsTemplate"
(paymentAllocation)="setPaymentAllocation($event)"
(transactionType)="transactionType"
>
</mifosx-loan-product-payment-strategy-step>
@@ -13,7 +13,7 @@ import { LoanProductAccountingStepComponent } from '../loan-product-stepper/loan
/** Custom Services */
import { ProductsService } from 'app/products/products.service';
import { LoanProducts } from '../loan-products';
import { PaymentAllocation } from '../loan-product-stepper/loan-product-payment-strategy-step/loan-product-payment-strategy-step.component';
import { PaymentAllocation, PaymentAllocationTransactionType, PaymentAllocationTransactionTypes, PaymentAllocationTypes } from '../loan-product-stepper/loan-product-payment-strategy-step/payment-allocation-model';
@Component({
selector: 'mifosx-create-loan-product',
@@ -35,6 +35,9 @@ export class CreateLoanProductComponent implements OnInit {
isAdvancedPaymentStrategy = false;
paymentAllocation: PaymentAllocation[] = [];
transactionTypes: PaymentAllocationTransactionType[] = [
PaymentAllocationTransactionTypes.DEFAULT_TRANSACTION
];
/**
* @param {ActivatedRoute} route Activated Route.
@@ -63,10 +63,11 @@
<ng-template matStepLabel>PAYMENT ALLOCATION</ng-template>
<mifosx-loan-product-payment-strategy-step
<mifosx-loan-product-payment-strategy-step *ngFor="let transactionType of transactionTypes"
[loanProductsTemplate]="loanProductsTemplate"
(paymentAllocation)="setPaymentAllocation($event)"
(paymentAllocationChange)="paymentAllocationChanged($event)"
(transactionType)="transactionType"
>
</mifosx-loan-product-payment-strategy-step>
@@ -14,7 +14,7 @@ import { LoanProductAccountingStepComponent } from '../loan-product-stepper/loan
import { ProductsService } from 'app/products/products.service';
import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
import { LoanProducts } from '../loan-products';
import { PaymentAllocation } from '../loan-product-stepper/loan-product-payment-strategy-step/loan-product-payment-strategy-step.component';
import { PaymentAllocation, PaymentAllocationTransactionType, PaymentAllocationTransactionTypes } from '../loan-product-stepper/loan-product-payment-strategy-step/payment-allocation-model';
@Component({
selector: 'mifosx-edit-loan-product',
@@ -37,6 +37,9 @@ export class EditLoanProductComponent implements OnInit {
isAdvancedPaymentStrategy = false;
wasPaymentAllocationChanged = false;
paymentAllocation: PaymentAllocation[] = [];
transactionTypes: PaymentAllocationTransactionType[] = [
PaymentAllocationTransactionTypes.DEFAULT_TRANSACTION
];
/**
* @param {ActivatedRoute} route Activated Route.
@@ -1,32 +1,55 @@
<div class="container space-top" fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<div fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="33%">
<mat-label>Future Installment Allocation Rule</mat-label>
<mat-select [formControl]="futureInstallmentAllocationRule">
<mat-option *ngFor="let installmentAllocation of installmentAllocationOptions" [value]="installmentAllocation.code">
{{ installmentAllocation.name | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="33%">
<mat-label>Transaction Type</mat-label>
<mat-select [formControl]="transactionTypeCtrl" disabled>
<mat-option *ngFor="let transactionTypeOption of transactionTypeOptions" [value]="transactionTypeOption.code">
{{ transactionTypeOption.name | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<div class="mat-elevation-z8 container">
<div class="mat-elevation-z8 container m-t-20">
<table mat-table [dataSource]="transactionTypesData" #table cdkDropList [cdkDropListData]="transactionTypesData"
(cdkDropListDropped)="dropTable($event)">
<table mat-table [dataSource]="paymentAllocationsData" #table cdkDropList
[cdkDropListData]="paymentAllocationsData" (cdkDropListDropped)="dropTable($event)"
matTooltip="You can drag and drop the rows to set a Payment Allocations order">
<ng-container matColumnDef="order">
<th mat-header-cell *matHeaderCellDef> Order </th>
<td mat-cell *matCellDef="let transactionType; let rowIndex = index"> {{ (rowIndex + 1) }} </td>
</ng-container>
<ng-container matColumnDef="order">
<th mat-header-cell *matHeaderCellDef> Order </th>
<td mat-cell *matCellDef="let paymentAllocation; let rowIndex = index"> {{ (rowIndex + 1) }} </td>
</ng-container>
<ng-container matColumnDef="transactionName">
<th mat-header-cell *matHeaderCellDef> Transaction Type </th>
<td mat-cell *matCellDef="let transactionType"> {{ transactionType.name }} </td>
</ng-container>
<ng-container matColumnDef="paymentAllocation">
<th mat-header-cell *matHeaderCellDef> Payment Allocation </th>
<td mat-cell *matCellDef="let paymentAllocation"> {{ paymentAllocation.name }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row cdkDrag [cdkDragData]="row" *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row cdkDrag [cdkDragData]="row" *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<mat-form-field fxFlex="33%" class="m-t-20">
<mat-label>Future Installment Allocation Rule</mat-label>
<mat-select [formControl]="futureInstallmentAllocationRule">
<mat-option *ngFor="let installmentAllocation of installmentAllocationOptions"
[value]="installmentAllocation.code">
{{ installmentAllocation.name | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlexFill fxLayout="row" class="margin-t" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
Previous
</button>
<button mat-raised-button matStepperNext>
Next
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
@@ -3,23 +3,7 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angu
import { UntypedFormControl, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatTable } from '@angular/material/table';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
interface PaymentCode {
name: string;
code: string;
}
interface PaymentAllocationOrder {
paymentAllocationRule: string;
order: number;
}
export interface PaymentAllocation {
transactionType: string;
paymentAllocationOrder: PaymentAllocationOrder[];
futureInstallmentAllocationRule: string;
}
import { FutureInstallmentAllocationRules, PaymentAllocation, PaymentAllocationOrder, PaymentAllocationTransactionType, PaymentAllocationTransactionTypes, PaymentAllocationTypes, PaymentCode } from './payment-allocation-model';
@Component({
selector: 'mifosx-loan-product-payment-strategy-step',
@@ -29,112 +13,64 @@ export interface PaymentAllocation {
export class LoanProductPaymentStrategyStepComponent implements OnInit {
@Input() loanProductsTemplate: any;
@Input() transactionType: PaymentAllocationTransactionType;
@Output() paymentAllocation = new EventEmitter<PaymentAllocation[]>();
@Output() paymentAllocationChange = new EventEmitter<boolean>();
transactionTypesData: PaymentCode[] = [];
transactionTypeOptions: PaymentCode[] = [];
paymentAllocationsData: PaymentCode[] = [];
installmentAllocationOptions: PaymentCode[] = [];
transactionTypeOptions: PaymentCode[] = [];
transactionType = new UntypedFormControl('');
transactionTypeCtrl = new UntypedFormControl('');
futureInstallmentAllocationRule = new UntypedFormControl('', Validators.required);
@ViewChild('table') table: MatTable<any>;
/** Columns to be displayed in the table. */
displayedColumns: string[] = ['order', 'transactionName'];
displayedColumns: string[] = ['order', 'paymentAllocation'];
constructor(private dialog: MatDialog) { }
constructor() { }
ngOnInit(): void {
this.transactionTypesData = [];
this.paymentAllocationsData = [];
this.setOptions();
this.sendPaymentAllocation();
}
setOptions(): void {
this.transactionTypeOptions = [
{ name: 'Past due penalty', code: 'PAST_DUE_PENALTY' },
{ name: 'Past due fee', code: 'PAST_DUE_FEE' },
{ name: 'Past due principal', code: 'PAST_DUE_PRINCIPAL' },
{ name: 'Past due interest', code: 'PAST_DUE_INTEREST' },
{ name: 'Due penalty', code: 'DUE_PENALTY' },
{ name: 'Due fee', code: 'DUE_FEE' },
{ name: 'Due principal', code: 'DUE_PRINCIPAL' },
{ name: 'Due interest', code: 'DUE_INTEREST' },
{ name: 'In advance penalty', code: 'IN_ADVANCE_PENALTY' },
{ name: 'In advance fee', code: 'IN_ADVANCE_FEE' },
{ name: 'In advance principal', code: 'IN_ADVANCE_PRINCIPAL' },
{ name: 'In advance interest', code: 'IN_ADVANCE_INTEREST' }
];
this.transactionTypesData = this.transactionTypeOptions;
this.installmentAllocationOptions = [
{ name: 'Next Installment', code: 'NEXT_INSTALLMENT' },
{ name: 'Last Installment', code: 'LAST_INSTALLMENT' },
{ name: 'Reamortization', code: 'REAMORTIZATION' }
];
this.futureInstallmentAllocationRule.patchValue('NEXT_INSTALLMENT');
}
addTransactionType(): void {
this.transactionTypeOptions.forEach((txType: PaymentCode) => {
if (txType.code === this.transactionType.value) {
this.transactionTypesData = [
...this.transactionTypesData,
txType
];
this.sendPaymentAllocation();
this.removePaymentCode(txType);
this.resetTransactionType();
}
});
}
removePaymentCode(txType: PaymentCode): void {
this.transactionTypeOptions = this.transactionTypeOptions.filter(({ code }) => code !== txType.code);
}
resetTransactionType(): void {
this.transactionType.patchValue('');
this.transactionType.markAsUntouched();
}
removeTransactionType(index: number) {
const transactionTypeName = this.transactionTypesData[index].name;
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: transactionTypeName }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.transactionTypesData.splice(index, 1);
this.transactionTypesData = this.transactionTypesData.concat([]);
this.transactionTypesData = [...this.transactionTypesData];
this.sendPaymentAllocation();
}
});
this.paymentAllocationsData = PaymentAllocationTypes.options;
this.transactionTypeOptions = PaymentAllocationTransactionTypes.options;
this.transactionTypeCtrl.patchValue(this.transactionTypeOptions[0].code);
if (this.transactionType) {
this.transactionTypeCtrl.patchValue(this.transactionType.code);
}
this.installmentAllocationOptions = FutureInstallmentAllocationRules.options;
this.futureInstallmentAllocationRule.patchValue(this.installmentAllocationOptions[0].code);
}
dropTable(event: CdkDragDrop<any[]>) {
const prevIndex = this.transactionTypesData.findIndex((d: any) => d === event.item.data);
moveItemInArray(this.transactionTypesData, prevIndex, event.currentIndex);
this.transactionTypesData = [...this.transactionTypesData];
const prevIndex = this.paymentAllocationsData.findIndex((d: any) => d === event.item.data);
moveItemInArray(this.paymentAllocationsData, prevIndex, event.currentIndex);
this.paymentAllocationsData = [...this.paymentAllocationsData];
this.table.renderRows();
this.sendPaymentAllocation();
this.paymentAllocationChange.emit(true);
}
sendPaymentAllocation(): void {
let transactionTypeVal = PaymentAllocationTransactionTypes.DEFAULT_TRANSACTION.code;
if (this.transactionType) {
transactionTypeVal = this.transactionType.code;
}
const paymentAllocationOrder: PaymentAllocationOrder[] = [];
this.transactionTypesData.forEach((txType: PaymentCode, index: number) => {
this.paymentAllocationsData.forEach((txType: PaymentCode, index: number) => {
paymentAllocationOrder.push({ 'paymentAllocationRule': txType.code, order: (index + 1) });
});
const paymentAllocation: PaymentAllocation[] = [];
paymentAllocation.push({
'futureInstallmentAllocationRule': this.futureInstallmentAllocationRule.value,
'transactionType': 'DEFAULT',
'transactionType': transactionTypeVal,
'paymentAllocationOrder': paymentAllocationOrder
});
@@ -0,0 +1,53 @@
export interface PaymentCode {
name: string;
code: string;
}
export interface PaymentAllocationOrder {
paymentAllocationRule: string;
order: number;
}
export interface PaymentAllocation {
transactionType: string;
paymentAllocationOrder: PaymentAllocationOrder[];
futureInstallmentAllocationRule: string;
}
export interface PaymentAllocationTransactionType {
name: string;
code: string;
}
export class PaymentAllocationTransactionTypes {
public static DEFAULT_TRANSACTION: PaymentAllocationTransactionType = { name: 'Default', code: 'DEFAULT' };
public static options: PaymentCode[] = [
this.DEFAULT_TRANSACTION
];
}
export class PaymentAllocationTypes {
public static options: PaymentCode[] = [
{ name: 'Past due penalty', code: 'PAST_DUE_PENALTY' },
{ name: 'Past due fee', code: 'PAST_DUE_FEE' },
{ name: 'Past due principal', code: 'PAST_DUE_PRINCIPAL' },
{ name: 'Past due interest', code: 'PAST_DUE_INTEREST' },
{ name: 'Due penalty', code: 'DUE_PENALTY' },
{ name: 'Due fee', code: 'DUE_FEE' },
{ name: 'Due principal', code: 'DUE_PRINCIPAL' },
{ name: 'Due interest', code: 'DUE_INTEREST' },
{ name: 'In advance penalty', code: 'IN_ADVANCE_PENALTY' },
{ name: 'In advance fee', code: 'IN_ADVANCE_FEE' },
{ name: 'In advance principal', code: 'IN_ADVANCE_PRINCIPAL' },
{ name: 'In advance interest', code: 'IN_ADVANCE_INTEREST' }
];
}
export class FutureInstallmentAllocationRules {
public static options: PaymentCode[] = [
{ name: 'Next Installment', code: 'NEXT_INSTALLMENT' },
{ name: 'Last Installment', code: 'LAST_INSTALLMENT' },
{ name: 'Reamortization', code: 'REAMORTIZATION' }
];
}
@@ -37,7 +37,7 @@
</ng-container>
<ng-container *ngIf="i > 0">
<th mat-header-cell class="right" *matHeaderCellDef> {{datatableColumn}} </th>
<th mat-header-cell class="right" *matHeaderCellDef> {{ getInputName(datatableColumn) }} </th>
<td mat-cell class="right" *matCellDef="let data" [ngClass]="isToDelete(data)">
{{ formatValue(data, datatableColumn) }}
</td>
@@ -256,4 +256,8 @@ export class DatatableMultiRowComponent implements OnInit, OnDestroy, OnChanges
return '';
}
getInputName(attr: string): string {
return this.datatables.getName(attr);
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ import { UserResolver } from './user.resolver';
const routes: Routes = [
Route.withShell([
{
path: 'users',
path: 'appusers',
data: { title: extract('Users'), breadcrumb: 'Users' },
children: [
{
+1 -1
View File
@@ -22,7 +22,7 @@ export class UsersService {
* @returns {Observable<any>} Users data
*/
getUsers(): Observable<any> {
return this.http.get('/users');
return this.http.get(`/users`);
}
/**
+4
View File
@@ -88,3 +88,7 @@ $error-log: #ffa726;
color: $status-inactive;
text-align: center;
}
.cdk-drag-placeholder {
background: $peter-river;
}