update loan product code

This commit is contained in:
Nitesh Yadav
2026-05-13 10:35:41 +05:30
parent 045d07e068
commit 6516d6c95d
21 changed files with 1841 additions and 2234 deletions
@@ -15,22 +15,6 @@
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
<span class="flex-60">{{ loanProduct.shortName }}</span>
</div>
@if (loanProduct.externalId) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.External Id' | translate }}:</span>
<span class="flex-60">{{ loanProduct.externalId }}</span>
</div>
}
@if (loanProduct.fundId) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Fund' | translate }}:</span>
<span class="flex-60">{{ loanProduct.fundName }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Include in Customer Loan Counter' | translate }}:</span>
<span class="flex-60">{{ loanProduct.includeInBorrowerCycle | yesNo }}</span>
</div>
@if (loanProduct.startDate) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Start Date' | translate }}:</span>
@@ -63,18 +47,6 @@
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ loanProduct.digitsAfterDecimal ?? loanProduct.currency.decimalPlaces }}</span>
</div>
@if (loanProduct.inMultiplesOf || loanProduct.currency?.inMultiplesOf) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
<span class="flex-60">{{ loanProduct.inMultiplesOf ?? loanProduct.currency.inMultiplesOf }}</span>
</div>
}
@if (loanProduct.installmentAmountInMultiplesOf) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Installment in multiples of' | translate }}:</span>
<span class="flex-60">{{ loanProduct.installmentAmountInMultiplesOf }}</span>
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
@@ -414,24 +386,6 @@
</span>
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Down Payments' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Enable Down Payments' | translate }}:</span>
<span class="flex-60">{{ loanProduct.enableDownPayment | yesNo }}</span>
</div>
@if (loanProduct.enableDownPayment) {
<div class="flex-fill layout-row-wrap responsive-column">
<span class="flex-40">{{ 'labels.inputs.Disbursed Amount Percentage Down Payment' | translate }} (%):</span>
<span class="flex-60">{{ loanProduct.disbursedAmountPercentageForDownPayment }} %</span>
</div>
}
@if (loanProduct.enableDownPayment) {
<div class="flex-fill layout-row-wrap responsive-column">
<span class="flex-40">{{ 'labels.inputs.Enable Auto Repayment for Down Payment' | translate }}:</span>
<span class="flex-60">{{ loanProduct.enableAutoRepaymentForDownPayment | yesNo }}</span>
</div>
}
@if (isAdvancedPaymentAllocation) {
<div class="flex-100 layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Loan Charge-off behaviour' | translate }}</h3>
@@ -737,55 +691,6 @@
}
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Loan Tranche Details' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Enable Multiple Disbursals' | translate }}:</span>
<span class="flex-60">{{ loanProduct.multiDisburseLoan | yesNo }}</span>
</div>
@if (loanProduct.multiDisburseLoan) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Tranche count' | translate }}:</span>
<span class="flex-60">{{ loanProduct.maxTrancheCount }}</span>
</div>
@if (loanProduct.outstandingLoanBalance) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum allowed outstanding balance' | translate }}:</span>
<span class="flex-60">{{ loanProduct.outstandingLoanBalance }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Disallow Expected Disbursements' | translate }}:</span>
<span class="flex-60">{{ loanProduct.disallowExpectedDisbursements | yesNo }}</span>
</div>
@if (loanProduct.loanScheduleType && loanProduct.loanScheduleType.code === 'PROGRESSIVE') {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Allow full term for each tranche' | translate }}:</span>
<span class="flex-60">{{ loanProduct.allowFullTermForTranche | yesNo }}</span>
</div>
}
}
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Event Settings' | translate }}</h3>
<div class="flex-100">
<span class="flex-40"
>{{
'labels.inputs.Use the Global Configurations values to the Repayment Event (notifications)' | translate
}}:</span
>
<span class="flex-60">{{ useDueForRepaymentsConfigurations | yesNo }}</span>
</div>
@if (loanProduct.dueDaysForRepaymentEvent) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Due days for repayment event' | translate }}:</span>
<span class="flex-60">{{ loanProduct.dueDaysForRepaymentEvent | number }}</span>
</div>
}
@if (loanProduct.overDueDaysForRepaymentEvent) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.OverDue days for repayment event' | translate }}:</span>
<span class="flex-60">{{ loanProduct.overDueDaysForRepaymentEvent | number }}</span>
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Configurable Terms and Settings' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
@@ -84,7 +84,6 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
@Input() action: string;
@Input() loanProduct: LoanProduct;
@Input() loanProductsTemplate: any | null;
@Input() useDueForRepaymentsConfigurations: boolean;
@Input() paymentAllocations: PaymentAllocation | null;
@Input() creditAllocations: CreditAllocation | null;
@Input() supportedInterestRefundTypes: StringEnumOptionData[] | null;
@@ -304,10 +304,6 @@ export class CreateLoanProductComponent implements OnInit {
submit() {
const loanProduct = this.loanProducts.buildPayload(this.loanProduct, this.itemsByDefault);
if (loanProduct['useDueForRepaymentsConfigurations'] === true) {
loanProduct['dueDaysForRepaymentEvent'] = null;
loanProduct['overDueDaysForRepaymentEvent'] = null;
}
if (this.isAdvancedPaymentStrategy) {
loanProduct['supportedInterestRefundTypes'] = this.mapStringEnumOptionToIdList(
loanProduct['supportedInterestRefundTypes']
@@ -316,7 +312,6 @@ export class CreateLoanProductComponent implements OnInit {
delete loanProduct['supportedInterestRefundTypes'];
delete loanProduct['daysInYearCustomStrategy'];
}
delete loanProduct['useDueForRepaymentsConfigurations'];
this.productsService.createLoanProduct(loanProduct).subscribe((response: any) => {
this.router.navigate(
@@ -353,10 +353,6 @@ export class EditLoanProductComponent implements OnInit {
submit() {
const loanProduct = this.loanProducts.buildPayload(this.loanProduct, this.itemsByDefault);
if (loanProduct['useDueForRepaymentsConfigurations']) {
loanProduct['dueDaysForRepaymentEvent'] = null;
loanProduct['overDueDaysForRepaymentEvent'] = null;
}
if (this.isAdvancedPaymentStrategy) {
loanProduct['supportedInterestRefundTypes'] = this.mapStringEnumOptionToIdList(
loanProduct['supportedInterestRefundTypes']
@@ -365,7 +361,6 @@ export class EditLoanProductComponent implements OnInit {
delete loanProduct['supportedInterestRefundTypes'];
delete loanProduct['daysInYearCustomStrategy'];
}
delete loanProduct['useDueForRepaymentsConfigurations'];
this.productsService.updateLoanProduct(this.loanProductAndTemplate.id, loanProduct).subscribe((response: any) => {
this.router.navigate(
@@ -7,542 +7,407 @@
-->
<form [formGroup]="loanProductAccountingForm">
<!-- Accounting Rule Section -->
<div class="lpa-section">
<div class="lpa-section-header">
<i class="fas fa-book lpa-section-icon"></i>
<h3>{{ 'labels.heading.Accounting Rule' | translate }}</h3>
<div class="lpa-section__header">
<span class="lpa-section__icon">
<i class="fas fa-book"></i>
</span>
<h3>Accounting</h3>
</div>
<div class="lpa-section-body">
<div class="lpa-full-width">
<mat-radio-group class="lpa-radio-group" formControlName="accountingRule">
@for (accountingRule of accountingRuleData; track accountingRule; let i = $index) {
<mat-radio-button [value]="i + 1">
{{ 'labels.accounting.' + accountingRule | translate }}
</mat-radio-button>
}
</mat-radio-group>
<div class="lpa-section__body">
<!-- Accounting Rule Section -->
<div class="lpa-group">
<div class="lpa-group__header">
<h4>Accounting Rule</h4>
</div>
<div class="lpa-radio-group">
<mat-radio-button [value]="2" [checked]="true" [disabled]="true">Cash</mat-radio-button>
</div>
@if (isAccrualAccounting()) {
<div class="lpa-checkbox-row">
<mat-checkbox formControlName="enableAccrualActivityPosting">{{
'labels.inputs.Enable Accrual Activity Posting on Installment Due Date' | translate
}}</mat-checkbox>
</div>
}
</div>
@if (isAccountingAccrualBased) {
<div class="lpa-full-width lpa-checkbox-row">
<mat-checkbox formControlName="enableAccrualActivityPosting">{{
'labels.inputs.Enable Accrual Activity Posting on Installment Due Date' | translate
}}</mat-checkbox>
<!-- Main Accounting Sections -->
@for (section of accountingSectionConfigs; track section.key) {
<div class="lpa-group">
<div class="lpa-group__header">
<h4>{{ section.title }}</h4>
<button
type="button"
class="lpa-add-other-btn"
(click)="addOtherGlMapping(section.otherMappingsControlName)"
>
+ Add Other
</button>
</div>
<div class="lpa-list">
@for (row of section.rows; track row.key) {
@if (
(!row.accrualOnly || isAccrualAccounting()) &&
(!row.deferredOnly || isDeferredOnly()) &&
(!row.deferredCapitalizationOnly || isDeferredCapitalizationOnly()) &&
(!row.deferredBuyDownOnly || isDeferredBuyDownOnly()) &&
(!row.deferredBuyDownMerchantOnly || isDeferredBuyDownMerchantOnly())
) {
<div class="lpa-row">
<div class="lpa-row__meta">
<span
class="lpa-pill"
[class.lpa-pill--green]="section.pillClass === 'green'"
[class.lpa-pill--amber]="section.pillClass === 'amber'"
[class.lpa-pill--blue]="section.pillClass === 'blue'"
>
{{ section.pillLabel }}
</span>
<span class="lpa-row__label">{{ row.label }}</span>
</div>
<mifosx-gl-account-selector
class="lpa-row__input"
[inputFormControl]="loanProductAccountingForm.get(row.controlName)"
[glAccountList]="row.options"
[required]="row.required"
[inputLabel]="row.inputLabel"
></mifosx-gl-account-selector>
</div>
}
}
</div>
<!-- Other GL Mappings -->
<div class="lpa-list" [formArrayName]="section.otherMappingsControlName">
@for (
otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls;
track $index;
let i = $index
) {
<div class="lpa-other-row-wrap">
<div class="lpa-row lpa-row--other" [formGroupName]="i">
<div class="lpa-row__meta lpa-row__meta--other">
<span
class="lpa-pill"
[class.lpa-pill--green]="section.pillClass === 'green'"
[class.lpa-pill--amber]="section.pillClass === 'amber'"
[class.lpa-pill--blue]="section.pillClass === 'blue'"
>
{{ section.pillLabel }}
</span>
<input
class="lpa-row__name lpa-row__name--other-account"
formControlName="accountName"
placeholder="Enter account name..."
type="text"
/>
</div>
<div class="lpa-row__actions lpa-row__actions--other">
<mat-form-field class="lpa-row__other-field" appearance="outline" subscriptSizing="dynamic">
<mat-label>GL Code</mat-label>
<mat-select
[formControl]="getOtherGlMappingControl(section.otherMappingsControlName, i, 'glAccountId')"
>
@for (glAccount of section.otherAccountOptions; track glAccount.id) {
<mat-option [value]="glAccount.id">
{{ '(' + glAccount.glCode + ') ' + glAccount.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<button
type="button"
class="lpa-remove-btn lpa-remove-btn--side"
(click)="removeOtherGlMapping(section.otherMappingsControlName, i)"
>
<i class="fas fa-times"></i>
</button>
</div>
}
</div>
</div>
}
<!-- Advanced Accounting Rules Section -->
@if (loanProductAccountingForm.value.accountingRule >= 2 && loanProductAccountingForm.value.accountingRule <= 4) {
<div class="lpa-group">
<div class="lpa-group__header">
<h4>Advanced Accounting Rules</h4>
</div>
<div class="lpa-checkbox-row">
<mat-checkbox formControlName="advancedAccountingRules">{{
'labels.heading.Advanced Accounting Rules' | translate
}}</mat-checkbox>
</div>
@if (loanProductAccountingForm.value.advancedAccountingRules) {
<!-- Payment Fund Sources -->
<div class="lpa-subsection">
<div class="lpa-subsection-header">
<h4>{{ 'labels.heading.Configure Fund Sources for Payment Channels' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="lpa-add-btn"
(click)="add('PaymentFundSource', paymentChannelToFundSourceMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (paymentChannelToFundSourceMappings.value.length !== 0) {
<div class="lpa-table-wrapper">
<table mat-table [dataSource]="paymentChannelToFundSourceMappings.value">
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.fundSourceAccountId | find: assetAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('PaymentFundSource', paymentChannelToFundSourceMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="delete('PaymentFundSource', paymentChannelToFundSourceMappings, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Fees to Income -->
<div class="lpa-subsection">
<div class="lpa-subsection-header">
<h4>{{ 'labels.heading.Map Fees to Specific Income Accounts' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="lpa-add-btn"
(click)="add('FeesIncome', feeToIncomeAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (feeToIncomeAccountMappings.value.length !== 0) {
<div class="lpa-table-wrapper">
<table mat-table [dataSource]="feeToIncomeAccountMappings.value">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fees' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.chargeId | find: chargeData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.incomeAccountId | find: incomeAndLiabilityAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('FeesIncome', feeToIncomeAccountMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="delete('FeesIncome', feeToIncomeAccountMappings, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Penalties to Income -->
<div class="lpa-subsection">
<div class="lpa-subsection-header">
<h4>{{ 'labels.heading.Map Penalties to Specific Income Accounts' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="lpa-add-btn"
(click)="add('PenaltyIncome', penaltyToIncomeAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (penaltyToIncomeAccountMappings.value.length !== 0) {
<div class="lpa-table-wrapper">
<table mat-table [dataSource]="penaltyToIncomeAccountMappings.value">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.chargeId | find: penaltyData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('PenaltyIncome', penaltyToIncomeAccountMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="delete('PenaltyIncome', penaltyToIncomeAccountMappings, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Charge-off Reasons to Expense -->
<div class="lpa-subsection">
<div class="lpa-subsection-header">
<h4>{{ 'labels.heading.Map Charge-off reasons to Expense accounts' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="lpa-add-btn"
[disabled]="!allowAddChargeOffReasonExpense"
(click)="add('ChargeOffReasonExpense', chargeOffReasonToExpenseAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (chargeOffReasonToExpenseAccountMappings.value.length !== 0) {
<div class="lpa-table-wrapper">
<table mat-table [dataSource]="chargeOffReasonToExpenseAccountMappings.value">
<ng-container matColumnDef="chargeOffReasonCodeValueId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Charge-off reason' | translate }}</th>
<td mat-cell *matCellDef="let chargeOffReasonExpense">
{{
chargeOffReasonExpense.chargeOffReasonCodeValueId
| find: chargeOffReasonOptions : 'id' : 'name'
}}
</td>
</ng-container>
<ng-container matColumnDef="expenseAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Expense Account' | translate }}</th>
<td mat-cell *matCellDef="let chargeOffReasonExpense">
{{ chargeOffReasonExpense.expenseAccountId | find: expenseAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chargeOffReasonExpense; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('ChargeOffReasonExpense', chargeOffReasonToExpenseAccountMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="delete('ChargeOffReasonExpense', chargeOffReasonToExpenseAccountMappings, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargeOffReasonExpenseDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargeOffReasonExpenseDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Advanced Mapping Rule Components -->
<mifosx-advanced-accounting-mapping-rule
class="lpa-full-width"
[textField]="'Classification'"
[formType]="'BuydownFeeClassificationToIncome'"
[formArray]="buydownfeeClassificationToIncomeAccountMappings"
[textHeading]="'Buydown Fee classifications to Income accounts'"
[incomeAccountData]="incomeAccountData"
[accountingMappingOptions]="buydownFeeClassificationOptions"
(formChangeEvent)="formChangeEvent($event)"
></mifosx-advanced-accounting-mapping-rule>
<mifosx-advanced-accounting-mapping-rule
class="lpa-full-width"
[textField]="'Classification'"
[formType]="'CapitalizedIncomeClassificationToIncome'"
[formArray]="capitalizedIncomeClassificationToIncomeAccountMappings"
[textHeading]="'Capitalized Income classifications to Income accounts'"
[incomeAccountData]="incomeAccountData"
[accountingMappingOptions]="capitalizedIncomeClassificationOptions"
(formChangeEvent)="formChangeEvent($event)"
></mifosx-advanced-accounting-mapping-rule>
<mifosx-advanced-accounting-mapping-rule
class="lpa-full-width"
[textField]="'WriteOff Reason'"
[formType]="'WriteOffReasonToExpense'"
[formArray]="writeOffReasonsToExpenseMappings"
[textHeading]="'WriteOff reasons to Expense accounts'"
[expenseAccountData]="expenseAccountData"
[accountingMappingOptions]="writeOffReasonOptions"
(formChangeEvent)="formChangeEvent($event)"
></mifosx-advanced-accounting-mapping-rule>
}
</div>
}
</div>
</div>
@if (loanProductAccountingForm.value.accountingRule >= 2 && loanProductAccountingForm.value.accountingRule <= 4) {
<!-- Assets / Liabilities Section -->
<div class="lpa-section">
<div class="lpa-section-header">
<i class="fas fa-university lpa-section-icon"></i>
<h3>{{ 'labels.heading.Assets' | translate }} / {{ 'labels.heading.Liabilities' | translate }}</h3>
</div>
<div class="lpa-section-body lpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.fundSourceAccountId"
[glAccountList]="assetAndLiabilityAccountData"
[required]="true"
[inputLabel]="'Fund source'"
>
</mifosx-gl-account-selector>
</div>
</div>
<!-- Assets Section -->
<div class="lpa-section">
<div class="lpa-section-header">
<i class="fas fa-university lpa-section-icon"></i>
<h3>{{ 'labels.heading.Assets' | translate }}</h3>
</div>
<div class="lpa-section-body lpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.loanPortfolioAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Loan portfolio'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.transfersInSuspenseAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Transfer in suspense'"
>
</mifosx-gl-account-selector>
@if (
loanProductAccountingForm.value.accountingRule === 3 || loanProductAccountingForm.value.accountingRule === 4
) {
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.receivableInterestAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Interest Receivable'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.receivableFeeAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Fees Receivable'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.receivablePenaltyAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Penalties Receivable'"
>
</mifosx-gl-account-selector>
}
</div>
</div>
<!-- Income Section -->
<div class="lpa-section">
<div class="lpa-section-header">
<i class="fas fa-chart-line lpa-section-icon"></i>
<h3>{{ 'labels.heading.Income' | translate }}</h3>
</div>
<div class="lpa-section-body lpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.interestOnLoanAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from Interest'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromFeeAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from fees'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromPenaltyAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from penalties'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromRecoveryAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from Recovery Repayments'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromChargeOffInterestAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from ChargeOff Interest'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromChargeOffFeesAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from ChargeOff Fees'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromChargeOffPenaltyAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from ChargeOff Penalty'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromGoodwillCreditInterestAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from Goodwill Credit Interest'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromGoodwillCreditFeesAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from Goodwill Credit Fees'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromGoodwillCreditPenaltyAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from Goodwill Credit Penalty'"
>
</mifosx-gl-account-selector>
@if (deferredIncomeRecognition?.capitalizedIncome?.enableIncomeCapitalization) {
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromCapitalizationAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income capitalization'"
>
</mifosx-gl-account-selector>
}
@if (deferredIncomeRecognition?.buyDownFee?.enableBuyDownFee) {
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.incomeFromBuyDownAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from Buy down fees'"
>
</mifosx-gl-account-selector>
}
</div>
</div>
<!-- Expenses Section -->
<div class="lpa-section">
<div class="lpa-section-header">
<i class="fas fa-money-bill-wave lpa-section-icon"></i>
<h3>{{ 'labels.heading.Expenses' | translate }}</h3>
</div>
<div class="lpa-section-body lpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.writeOffAccountId"
[glAccountList]="expenseAccountData"
[required]="true"
[inputLabel]="'Losses written off'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.goodwillCreditAccountId"
[glAccountList]="expenseAccountData"
[required]="true"
[inputLabel]="'Expenses from Goodwill Credit'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.chargeOffExpenseAccountId"
[glAccountList]="expenseAccountData"
[required]="true"
[inputLabel]="'ChargeOff Expense'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.chargeOffFraudExpenseAccountId"
[glAccountList]="expenseAccountData"
[required]="true"
[inputLabel]="'ChargeOff Fraud Expense'"
>
</mifosx-gl-account-selector>
@if (
deferredIncomeRecognition?.buyDownFee?.enableBuyDownFee &&
deferredIncomeRecognition?.buyDownFee?.merchantBuyDownFee
) {
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.buyDownExpenseAccountId"
[glAccountList]="expenseAccountData"
[required]="true"
[inputLabel]="'Buy down fee Expense'"
>
</mifosx-gl-account-selector>
}
</div>
</div>
<!-- Liabilities Section -->
<div class="lpa-section">
<div class="lpa-section-header">
<i class="fas fa-file-invoice-dollar lpa-section-icon"></i>
<h3>{{ 'labels.heading.Liabilities' | translate }}</h3>
</div>
<div class="lpa-section-body lpa-grid">
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.overpaymentLiabilityAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Over payment liability'"
>
</mifosx-gl-account-selector>
@if (
deferredIncomeRecognition?.capitalizedIncome?.enableIncomeCapitalization ||
deferredIncomeRecognition?.buyDownFee?.enableBuyDownFee
) {
<mifosx-gl-account-selector
[inputFormControl]="loanProductAccountingForm.controls.deferredIncomeLiabilityAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Deferred income'"
>
</mifosx-gl-account-selector>
}
</div>
</div>
<!-- Advanced Accounting Rules Section -->
<div class="lpa-section">
<div class="lpa-section-header">
<i class="fas fa-cogs lpa-section-icon"></i>
<h3>{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
</div>
<div class="lpa-section-body">
<div class="lpa-full-width lpa-checkbox-row">
<mat-checkbox formControlName="advancedAccountingRules">{{
'labels.heading.Advanced Accounting Rules' | translate
}}</mat-checkbox>
</div>
</div>
@if (loanProductAccountingForm.value.advancedAccountingRules) {
<!-- Payment Fund Sources -->
<div class="lpa-subsection">
<div class="lpa-subsection-header">
<h4>{{ 'labels.heading.Configure Fund Sources for Payment Channels' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="lpa-add-btn"
(click)="add('PaymentFundSource', paymentChannelToFundSourceMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (paymentChannelToFundSourceMappings.value.length !== 0) {
<div class="lpa-table-wrapper">
<table mat-table [dataSource]="paymentChannelToFundSourceMappings.value">
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.fundSourceAccountId | find: assetAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('PaymentFundSource', paymentChannelToFundSourceMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="delete('PaymentFundSource', paymentChannelToFundSourceMappings, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Fees to Income -->
<div class="lpa-subsection">
<div class="lpa-subsection-header">
<h4>{{ 'labels.heading.Map Fees to Specific Income Accounts' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="lpa-add-btn"
(click)="add('FeesIncome', feeToIncomeAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (feeToIncomeAccountMappings.value.length !== 0) {
<div class="lpa-table-wrapper">
<table mat-table [dataSource]="feeToIncomeAccountMappings.value">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fees' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.chargeId | find: chargeData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.incomeAccountId | find: incomeAndLiabilityAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome; let i = index">
<button mat-icon-button color="primary" (click)="edit('FeesIncome', feeToIncomeAccountMappings, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete('FeesIncome', feeToIncomeAccountMappings, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Penalties to Income -->
<div class="lpa-subsection">
<div class="lpa-subsection-header">
<h4>{{ 'labels.heading.Map Penalties to Specific Income Accounts' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="lpa-add-btn"
(click)="add('PenaltyIncome', penaltyToIncomeAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (penaltyToIncomeAccountMappings.value.length !== 0) {
<div class="lpa-table-wrapper">
<table mat-table [dataSource]="penaltyToIncomeAccountMappings.value">
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.chargeId | find: penaltyData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('PenaltyIncome', penaltyToIncomeAccountMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="delete('PenaltyIncome', penaltyToIncomeAccountMappings, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Charge-off Reasons to Expense -->
<div class="lpa-subsection">
<div class="lpa-subsection-header">
<h4>{{ 'labels.heading.Map Charge-off reasons to Expense accounts' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="lpa-add-btn"
[disabled]="!allowAddChargeOffReasonExpense"
(click)="add('ChargeOffReasonExpense', chargeOffReasonToExpenseAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (chargeOffReasonToExpenseAccountMappings.value.length !== 0) {
<div class="lpa-table-wrapper">
<table mat-table [dataSource]="chargeOffReasonToExpenseAccountMappings.value">
<ng-container matColumnDef="chargeOffReasonCodeValueId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Charge-off reason' | translate }}</th>
<td mat-cell *matCellDef="let chargeOffReasonExpense">
{{
chargeOffReasonExpense.chargeOffReasonCodeValueId | find: chargeOffReasonOptions : 'id' : 'name'
}}
</td>
</ng-container>
<ng-container matColumnDef="expenseAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Expense Account' | translate }}</th>
<td mat-cell *matCellDef="let chargeOffReasonExpense">
{{ chargeOffReasonExpense.expenseAccountId | find: expenseAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chargeOffReasonExpense; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('ChargeOffReasonExpense', chargeOffReasonToExpenseAccountMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="delete('ChargeOffReasonExpense', chargeOffReasonToExpenseAccountMappings, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargeOffReasonExpenseDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargeOffReasonExpenseDisplayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Advanced Mapping Rule Components -->
<mifosx-advanced-accounting-mapping-rule
class="lpa-full-width"
[textField]="'Classification'"
[formType]="'BuydownFeeClassificationToIncome'"
[formArray]="buydownfeeClassificationToIncomeAccountMappings"
[textHeading]="'Buydown Fee classifications to Income accounts'"
[incomeAccountData]="incomeAccountData"
[accountingMappingOptions]="buydownFeeClassificationOptions"
(formChangeEvent)="formChangeEvent($event)"
></mifosx-advanced-accounting-mapping-rule>
<mifosx-advanced-accounting-mapping-rule
class="lpa-full-width"
[textField]="'Classification'"
[formType]="'CapitalizedIncomeClassificationToIncome'"
[formArray]="capitalizedIncomeClassificationToIncomeAccountMappings"
[textHeading]="'Capitalized Income classifications to Income accounts'"
[incomeAccountData]="incomeAccountData"
[accountingMappingOptions]="capitalizedIncomeClassificationOptions"
(formChangeEvent)="formChangeEvent($event)"
></mifosx-advanced-accounting-mapping-rule>
<mifosx-advanced-accounting-mapping-rule
class="lpa-full-width"
[textField]="'WriteOff Reason'"
[formType]="'WriteOffReasonToExpense'"
[formArray]="writeOffReasonsToExpenseMappings"
[textHeading]="'WriteOff reasons to Expense accounts'"
[expenseAccountData]="expenseAccountData"
[accountingMappingOptions]="writeOffReasonOptions"
(formChangeEvent)="formChangeEvent($event)"
></mifosx-advanced-accounting-mapping-rule>
}
</div>
}
</form>
@@ -6,124 +6,328 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// Section cards
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #6b7280;
$gray-700: #334155;
$gray-900: #202124;
$primary: #1353a0;
.lpa-section {
border: 1px solid #e2e8f0;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
background: #fff;
& + & {
margin-top: 16px;
}
}
// Section header
.lpa-section-header {
.lpa-section__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
background: $gray-50;
border-bottom: 1px solid $gray-200;
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
margin: 0;
color: $gray-900;
}
}
.lpa-section-icon {
color: #0ea5e9;
font-size: 15px;
}
// Section body
.lpa-section-body {
padding: 16px 24px;
}
// 2-column grid
.lpa-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
}
// Full width utility
.lpa-full-width {
grid-column: 1 / -1;
}
// Radio group
.lpa-radio-group {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
// Checkbox row
.lpa-checkbox-row {
display: flex;
.lpa-section__icon {
display: inline-flex;
align-items: center;
padding: 4px 0;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
color: #2563eb;
font-size: 13px;
}
// Subsections inside advanced rules
.lpa-subsection {
padding: 16px 24px;
border-top: 1px solid #e8eaed;
.lpa-section__body {
padding: 16px 24px 20px;
}
.lpa-subsection-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.lpa-group {
& + & {
margin-top: 20px;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin: 0 0 12px;
}
h4 {
font-size: 13px;
font-weight: 600;
color: #202124;
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #94a3b8;
}
}
// Add buttons
.lpa-add-btn {
height: 32px;
border-radius: 8px;
.lpa-add-other-btn {
border: 0;
background: transparent;
color: #2563eb;
font-size: 12px;
font-weight: 600;
font-weight: 700;
padding: 0;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
}
// Table wrapper
.lpa-table-wrapper {
.lpa-list {
display: flex;
flex-direction: column;
gap: 10px;
& + & {
margin-top: 12px;
}
}
.lpa-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 10px 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
@media (width <= 900px) {
flex-direction: column;
align-items: stretch;
}
&:has(mifosx-gl-account-selector) {
align-items: stretch;
}
}
.lpa-row--other {
border-style: dashed;
border-color: #cbd5e1;
background: #f8fafc;
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
align-items: center;
padding: 12px 16px;
@media (width <= 1100px) {
grid-template-columns: 1fr;
}
}
.lpa-other-row-wrap {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
@media (width <= 1100px) {
grid-template-columns: 1fr;
gap: 8px;
}
}
.lpa-row__meta {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1 1 auto;
&--other {
min-width: 0;
flex: 1 1 auto;
align-self: stretch;
}
}
.lpa-row__label {
font-size: 14px;
font-weight: 600;
color: $gray-700;
}
.lpa-row__input {
flex: 0 0 22rem;
max-width: 22rem;
min-width: 18rem;
@media (width <= 900px) {
width: 100%;
max-width: 100%;
min-width: 0;
}
:is(mifosx-gl-account-selector) {
width: 100%;
display: block;
:is(.layout-column) {
width: 100%;
margin-right: 0 !important;
}
:is(mat-form-field) {
width: 100%;
}
}
}
.lpa-row__actions {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
&--other {
justify-content: flex-end;
align-self: stretch;
width: 100%;
@media (width <= 1100px) {
justify-content: flex-start;
}
}
}
.lpa-row__name {
width: 100%;
min-width: 0;
max-width: 24rem;
height: 40px;
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 0 12px;
font-size: 14px;
background: #fff;
@media (width <= 900px) {
max-width: 100%;
}
&--other-account {
max-width: 100%;
flex: 1 1 auto;
height: 48px;
}
}
.lpa-row__other-field {
width: 100%;
min-width: 0;
:is(mat-form-field) {
width: 100%;
}
:is(.mat-mdc-form-field-required-marker) {
display: none;
}
}
.lpa-remove-btn {
border: 0;
background: transparent;
color: #ef4444;
font-size: 18px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
&--side {
place-self: center end;
margin-right: 4px;
@media (width <= 1100px) {
justify-self: start;
margin-right: 0;
}
}
}
.lpa-pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
&--green {
background: #dcfce7;
color: #16a34a;
}
&--amber {
background: #fef3c7;
color: #d97706;
}
&--blue {
background: #dbeafe;
color: #2563eb;
}
}
// Advanced Accounting Subsection styles
.lpa-subsection {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid $border;
&-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
h4 {
margin: 0;
color: $gray-700;
font-size: 14px;
}
}
}
.lpa-table-wrapper {
border: 1px solid $border;
border-radius: 8px;
overflow: hidden;
margin-top: 8px;
table {
width: 100%;
}
}
// Responsive
@media (width <= 600px) {
.lpa-grid {
grid-template-columns: 1fr;
}
.lpa-checkbox-row {
margin-top: 12px;
}
.lpa-radio-group {
flex-direction: column;
}
.lpa-subsection-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.lpa-radio-group {
display: flex;
gap: 16px;
}
@@ -57,61 +57,4 @@
</mat-form-field>
</div>
</div>
<div class="lpc-section">
<div class="lpc-section__header">
<i class="fas fa-calculator"></i>
<h3>{{ 'labels.inputs.Multiples' | translate }}</h3>
</div>
<div class="lpc-grid">
<div class="lpc-field--full lpc-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="setMultiples">
{{ 'labels.inputs.Set the multiples of the loan and its installment' | translate }}
</mat-checkbox>
</div>
@if (loanProductCurrencyForm.value.setMultiples) {
<mat-form-field appearance="outline" class="lpc-field">
<mat-label>{{ 'labels.inputs.Currency in multiples of' | translate }}</mat-label>
<input
type="number"
min="1"
matInput
matTooltip="{{ 'tooltips.Enter multiples of currency value' | translate }}"
formControlName="inMultiplesOf"
required
/>
@if (loanProductCurrencyForm.get('inMultiplesOf')?.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Currency in multiples of' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (loanProductCurrencyForm.get('inMultiplesOf')?.hasError('min')) {
<mat-error>
{{ 'labels.inputs.Currency in multiples of' | translate }}
{{ 'labels.commons.Minimum Value must be' | translate }} <strong>1</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="lpc-field">
<mat-label>{{ 'labels.inputs.Installment in multiples of' | translate }}</mat-label>
<input type="number" min="1" matInput formControlName="installmentAmountInMultiplesOf" required />
@if (loanProductCurrencyForm.get('installmentAmountInMultiplesOf')?.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Installment in multiples of' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (loanProductCurrencyForm.get('installmentAmountInMultiplesOf')?.hasError('min')) {
<mat-error>
{{ 'labels.inputs.Installment in multiples of' | translate }}
{{ 'labels.commons.Minimum Value must be' | translate }} <strong>1</strong>
</mat-error>
}
</mat-form-field>
}
</div>
</div>
</form>
@@ -9,8 +9,6 @@
import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -19,9 +17,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./loan-product-currency-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
MatCheckbox,
FaIconComponent
MatTooltip
]
})
export class LoanProductCurrencyStepComponent implements OnInit {
@@ -46,69 +42,8 @@ export class LoanProductCurrencyStepComponent implements OnInit {
this.loanProductsTemplate.currency.decimalPlaces === undefined ||
this.loanProductsTemplate.currency.decimalPlaces === null
? ''
: this.loanProductsTemplate.currency.decimalPlaces,
setMultiples: !!(
(this.loanProductsTemplate.currency.inMultiplesOf && this.loanProductsTemplate.currency.inMultiplesOf !== 0) ||
(this.loanProductsTemplate.installmentAmountInMultiplesOf &&
this.loanProductsTemplate.installmentAmountInMultiplesOf !== 0)
),
inMultiplesOf:
this.loanProductsTemplate.currency.inMultiplesOf === 0 ||
this.loanProductsTemplate.currency.inMultiplesOf === undefined ||
this.loanProductsTemplate.currency.inMultiplesOf === null
? ''
: this.loanProductsTemplate.currency.inMultiplesOf,
installmentAmountInMultiplesOf:
this.loanProductsTemplate.installmentAmountInMultiplesOf === 0 ||
this.loanProductsTemplate.installmentAmountInMultiplesOf === undefined ||
this.loanProductsTemplate.installmentAmountInMultiplesOf === null
? ''
: this.loanProductsTemplate.installmentAmountInMultiplesOf
: this.loanProductsTemplate.currency.decimalPlaces
});
this.setupConditionalValidation();
}
setupConditionalValidation() {
this.loanProductCurrencyForm.get('setMultiples')?.valueChanges.subscribe((setMultiples: boolean) => {
const inMultiplesOfControl = this.loanProductCurrencyForm.get('inMultiplesOf');
const installmentControl = this.loanProductCurrencyForm.get('installmentAmountInMultiplesOf');
if (setMultiples) {
inMultiplesOfControl?.setValidators([
Validators.required,
Validators.min(1)
]);
installmentControl?.setValidators([
Validators.required,
Validators.min(1)
]);
} else {
inMultiplesOfControl?.clearValidators();
installmentControl?.clearValidators();
inMultiplesOfControl?.setValue('');
installmentControl?.setValue('');
}
inMultiplesOfControl?.updateValueAndValidity();
installmentControl?.updateValueAndValidity();
});
const initialSetMultiples = this.loanProductCurrencyForm.get('setMultiples')?.value;
if (initialSetMultiples) {
const inMultiplesOfControl = this.loanProductCurrencyForm.get('inMultiplesOf');
const installmentControl = this.loanProductCurrencyForm.get('installmentAmountInMultiplesOf');
inMultiplesOfControl?.setValidators([
Validators.required,
Validators.min(1)
]);
installmentControl?.setValidators([
Validators.required,
Validators.min(1)
]);
inMultiplesOfControl?.updateValueAndValidity();
installmentControl?.updateValueAndValidity();
}
}
createLoanProductCurrencyForm() {
@@ -123,10 +58,7 @@ export class LoanProductCurrencyStepComponent implements OnInit {
Validators.required,
Validators.min(0)
]
],
setMultiples: [false],
inMultiplesOf: [''],
installmentAmountInMultiplesOf: ['']
]
});
}
@@ -136,21 +68,6 @@ export class LoanProductCurrencyStepComponent implements OnInit {
currencyCode: formValue.currencyCode,
digitsAfterDecimal: formValue.digitsAfterDecimal
};
if (formValue.setMultiples) {
if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) {
result.inMultiplesOf = formValue.inMultiplesOf;
}
if (
formValue.installmentAmountInMultiplesOf !== '' &&
formValue.installmentAmountInMultiplesOf !== null &&
formValue.installmentAmountInMultiplesOf !== undefined
) {
result.installmentAmountInMultiplesOf = formValue.installmentAmountInMultiplesOf;
}
}
return result;
}
}
@@ -41,36 +41,6 @@
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>{{ 'labels.inputs.External Id' | translate }}</mat-label>
<input
matInput
formControlName="externalId"
matTooltip="{{ 'tooltips.External Id is a unique identifier' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>{{ 'labels.inputs.Fund' | translate }}</mat-label>
<mat-select matTooltip="{{ 'tooltips.Loan products may be assigned' | translate }}" formControlName="fundId">
@for (fund of fundData; track fund) {
<mat-option [value]="fund.id">
{{ fund.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="lpd-field lpd-checkbox-row">
<mat-checkbox
labelPosition="before"
matTooltip="{{ 'tooltips.A borrower loan counter (cycle)' | translate }}"
formControlName="includeInBorrowerCycle"
>
{{ 'labels.inputs.Include in Customer Loan Counter' | translate }}
</mat-checkbox>
</div>
</div>
</div>
@@ -14,9 +14,7 @@ import { Dates } from 'app/core/utils/dates';
/** Custom Services */
import { SettingsService } from 'app/settings/settings.service';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -26,9 +24,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
MatCheckbox,
CdkTextareaAutosize,
FaIconComponent
CdkTextareaAutosize
]
})
export class LoanProductDetailsStepComponent implements OnInit {
@@ -40,8 +36,6 @@ export class LoanProductDetailsStepComponent implements OnInit {
loanProductDetailsForm: UntypedFormGroup;
fundData: any;
minDate = new Date(2000, 0, 1);
maxDate = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
@@ -56,17 +50,12 @@ export class LoanProductDetailsStepComponent implements OnInit {
}
ngOnInit() {
this.fundData = this.loanProductsTemplate.fundOptions;
this.loanProductDetailsForm.patchValue({
name: this.loanProductsTemplate.name,
shortName: this.loanProductsTemplate.shortName,
description: this.loanProductsTemplate.description,
externalId: this.loanProductsTemplate.externalId,
fundId: this.loanProductsTemplate.fundId,
startDate: this.loanProductsTemplate.startDate && new Date(this.loanProductsTemplate.startDate),
closeDate: this.loanProductsTemplate.closeDate && new Date(this.loanProductsTemplate.closeDate),
includeInBorrowerCycle: this.loanProductsTemplate.includeInBorrowerCycle
closeDate: this.loanProductsTemplate.closeDate && new Date(this.loanProductsTemplate.closeDate)
});
}
@@ -81,11 +70,8 @@ export class LoanProductDetailsStepComponent implements OnInit {
Validators.required
],
description: [''],
externalId: [''],
fundId: [''],
startDate: [''],
closeDate: [''],
includeInBorrowerCycle: [false]
closeDate: ['']
});
}
@@ -10,7 +10,6 @@
[action]="'preview'"
[loanProduct]="loanProduct"
[loanProductsTemplate]="loanProductsTemplate"
[useDueForRepaymentsConfigurations]="loanProduct.useDueForRepaymentsConfigurations"
[paymentAllocations]="loanProduct.paymentAllocation"
[creditAllocations]="loanProduct.creditAllocation"
[supportedInterestRefundTypes]="loanProduct.supportedInterestRefundTypes"
@@ -235,147 +235,528 @@
</div>
</div>
<!-- Loan Tranche Details -->
<!-- Cheque Book Settings -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-layer-group lps-section__icon"></i>
<h3 class="lps-section__title">{{ 'labels.inputs.Loan Tranche Details' | translate }}</h3>
<i class="fas fa-money-check lps-section__icon"></i>
<h3 class="lps-section__title">Cheque Book Settings</h3>
</div>
<div class="lps-section__body">
<div class="lps-checkbox-row lps-full-width">
<mat-checkbox
labelPosition="before"
formControlName="multiDisburseLoan"
matTooltip="{{ 'tooltips.Leave this checkbox unchecked' | translate }}"
>
{{ 'labels.inputs.Enable Multiple Disbursals' | translate }}
</mat-checkbox>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Cheque Book Issue Enabled</mat-label>
<mat-select formControlName="chequeBookIssueEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
@if (loanProductSettingsForm.value.multiDisburseLoan) {
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Maximum Tranche count' | translate }}</mat-label>
<input
type="number"
matInput
matTooltip="{{ 'tooltips.Maximum number of disbursements' | translate }}"
formControlName="maxTrancheCount"
required
/>
@if (loanProductSettingsForm.controls.maxTrancheCount.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Maximum Tranche count' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Maximum allowed outstanding balance' | translate }}</mat-label>
<input
type="number"
matInput
matTooltip="{{ 'tooltips.Maximum outstanding loan account balance' | translate }}"
formControlName="outstandingLoanBalance"
/>
</mat-form-field>
<div class="lps-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="disallowExpectedDisbursements">
{{ 'labels.inputs.Disallow Expected Disbursements' | translate }}
</mat-checkbox>
</div>
@if (loanProductSettingsForm.value.loanScheduleType === 'PROGRESSIVE') {
<div class="lps-checkbox-row">
<mat-checkbox
labelPosition="before"
formControlName="allowFullTermForTranche"
matTooltip="{{ 'tooltips.Allow full term length for each tranche disbursement' | translate }}"
>
{{ 'labels.inputs.Allow full term for each tranche' | translate }}
</mat-checkbox>
</div>
}
}
<mat-form-field appearance="outline">
<mat-label>Cheque Validation Required</mat-label>
<mat-select formControlName="chequeValidationRequired">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is First Cheque Book Free</mat-label>
<mat-select formControlName="isFirstChequeBookFree">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Cheque Printing Account Type</mat-label>
<mat-select formControlName="chequePrintingAccountType">
<mat-option value="SAVINGS_BANK">Savings Bank</mat-option>
<mat-option value="CURRENT_ACCOUNT">Current Account</mat-option>
<mat-option value="CASH_CREDIT">Cash Credit</mat-option>
<mat-option value="OVERDRAFT">Overdraft</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow Withdrawal Slip Usage</mat-label>
<mat-select formControlName="allowWithdrawalSlipUsage">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Maintain Cheque Book Stock Registry</mat-label>
<mat-select formControlName="maintainChequeBookStockRegistry">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Free Cheque Books Per Year</mat-label>
<input type="number" matInput formControlName="freeChequeBooksPerYear" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow Credit By Cheque Number</mat-label>
<mat-select formControlName="allowCreditByChequeNumber">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Credit Cheque Validation Required</mat-label>
<mat-select formControlName="creditChequeValidationRequired">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Grace Days For Stock Statement</mat-label>
<input type="number" matInput formControlName="graceDaysForStockStatement" />
</mat-form-field>
</div>
</div>
<!-- Down Payment -->
<!-- Additional Product Settings -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-hand-holding-usd lps-section__icon"></i>
<h3 class="lps-section__title">{{ 'labels.heading.Down Payment' | translate }}</h3>
<i class="fas fa-cogs lps-section__icon"></i>
<h3 class="lps-section__title">Additional Product Settings</h3>
</div>
<div class="lps-section__body">
<div class="lps-checkbox-row lps-full-width">
<mat-checkbox
labelPosition="before"
formControlName="enableDownPayment"
matTooltip="{{ 'tooltips.Leave this checkbox unchecked' | translate }}"
>
{{ 'labels.inputs.Enable Down Payment' | translate }}
</mat-checkbox>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline">
<mat-label>Account Type Active For Transactions</mat-label>
<mat-select formControlName="accountTypeActiveForTransactions">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
@if (loanProductSettingsForm.value.enableDownPayment) {
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Disbursed Amount Percentage Down Payment' | translate }} (%)</mat-label>
<input
type="number"
matInput
matTooltip="{{ 'tooltips.The percentage of disbursements amount for Down Payment' | translate }}"
formControlName="disbursedAmountPercentageForDownPayment"
required
[min]="0"
[max]="100"
/>
@if (loanProductSettingsForm.controls.disbursedAmountPercentageForDownPayment.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Disbursed Amount Percentage Down Payment' | translate }}
{{ 'labels.commons.is' | translate }} <strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (loanProductSettingsForm.controls.disbursedAmountPercentageForDownPayment.errors?.range) {
<mat-error>
{{ 'labels.inputs.Disbursed Amount Percentage Down Payment' | translate }}
{{ 'labels.commons.is' | translate }} {{ 'labels.text.out' | translate }}
<strong>{{ 'labels.text.percentage range' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<div class="lps-checkbox-row">
<mat-checkbox
labelPosition="before"
formControlName="enableAutoRepaymentForDownPayment"
matTooltip="{{ 'tooltips.Enable or disable the auto repayment of down-payment.' | translate }}"
<mat-form-field appearance="outline">
<mat-label>Sub Type Activity Enabled</mat-label>
<mat-select formControlName="subTypeActivityEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Account Opening Charges</mat-label>
<input type="number" matInput formControlName="accountOpeningCharges" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Maximum Gold Loan Limit</mat-label>
<input type="number" matInput formControlName="maximumGoldLoanLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Last Account Number Opened</mat-label>
<input type="text" matInput formControlName="lastAccountNumberOpened" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Customer Termination Allowed</mat-label>
<mat-select formControlName="customerTerminationAllowed">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Charges For Cash Credit Above Limit</mat-label>
<input type="number" matInput formControlName="chargesForCashCreditAboveLimit" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Death Interest GL Account</mat-label>
<input type="text" matInput formControlName="deathInterestGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Starting Ledger Number</mat-label>
<input type="number" matInput formControlName="startingLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Ending Ledger Number</mat-label>
<input type="number" matInput formControlName="endingLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Current Ledger Number</mat-label>
<input type="number" matInput formControlName="currentLedgerNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Remittance Account Number</mat-label>
<input type="text" matInput formControlName="remittanceAccountNumber" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Is Non Resident Indian Account</mat-label>
<mat-select formControlName="isNonResidentIndianAccount">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Debit Clearing GL Account</mat-label>
<input type="text" matInput formControlName="debitClearingGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Missed Call Alert Service Active</mat-label>
<mat-select formControlName="missedCallAlertServiceActive">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Standing Instruction Charge GL Account</mat-label>
<input type="text" matInput formControlName="standingInstructionChargeGLAccount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Standing Instruction Debit Charge Skip Option</mat-label>
<mat-select formControlName="standingInstructionDebitChargeSkipOption">
<mat-option value="SKIP_ALWAYS">Skip Always</mat-option>
<mat-option value="SKIP_ON_FIRST_FAILURE">Skip on First Failure</mat-option>
<mat-option value="NEVER_SKIP">Never Skip</mat-option>
<mat-option value="RETRY_AFTER_24_HOURS">Retry after 24 Hours</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Opening Balance Multiplier For Remittance Charges</mat-label>
<input type="number" matInput formControlName="openingBalanceMultiplierForRemittanceCharges" />
</mat-form-field>
</div>
</div>
<!-- Interest Related Parameters (Page 3) -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-percent lps-section__icon"></i>
<h3 class="lps-section__title">Interest Related Parameters</h3>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline" class="lps-full-width">
<mat-label>Drawing Power And Limit Generation Procedure</mat-label>
<mat-select formControlName="drawingPowerAndLimitGenerationProcedure">
<mat-option value="NORMAL D.P. ENTRY IS CONSIDER">Normal D.P. Entry Is Considered</mat-option>
<mat-option value="AGAINST F.D. INTEREST WITH CERTIFICATE WISE INTEREST RATE"
>Against F.D. Interest with Certificate Wise Interest Rate</mat-option
>
{{ 'labels.inputs.Enable Auto Repayment for Down Payment' | translate }}
</mat-checkbox>
</div>
}
<mat-option value="BASED ON ASSET VALUE">Based on Asset Value</mat-option>
<mat-option value="MONTHLY STOCK STATEMENT BASIS">Monthly Stock Statement Basis</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lps-full-width">
<mat-label>Advances Interest Calculation Method</mat-label>
<mat-select formControlName="advancesInterestCalculationMethod">
<mat-option value="DEBIT CLOSING BALANCE CONSIDER AS PRODUCT"
>Debit Closing Balance Considered as Product</mat-option
>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Against Interest Calculation Enabled</mat-label>
<mat-select formControlName="againstInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Day Wise Against Clearing Interest Rounding</mat-label>
<mat-select formControlName="dayWiseAgainstClearingInterestRounding">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Interest Calculation One Fourth Method</mat-label>
<mat-select formControlName="interestCalculationOneFourthMethod">
<mat-option value="0">0 (Standard)</mat-option>
<mat-option value="1">1 (Custom A)</mat-option>
<mat-option value="2">2 (Custom B)</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lps-full-width">
<mat-label>Against Clearing Interest Procedure Type</mat-label>
<mat-select formControlName="againstClearingInterestProcedureType">
<mat-option value="NORMAL AGAINST CLEARING INTEREST">Normal Against Clearing Interest</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Consider Debit Closing Amount For Against Clearing</mat-label>
<mat-select formControlName="considerDebitClosingAmountForAgainstClearing">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Interest Rounding Method</mat-label>
<mat-select formControlName="interestRoundingMethod">
<mat-option value="All Int. Round">All Int. Round</mat-option>
<mat-option value="Nearest Rupee">Nearest Rupee</mat-option>
<mat-option value="Round Up">Round Up</mat-option>
<mat-option value="Round Down">Round Down</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Penalty Interest Calculation Enabled</mat-label>
<mat-select formControlName="penaltyInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Penalty Interest Calculation Method</mat-label>
<mat-select formControlName="penaltyInterestCalculationMethod">
<mat-option value="Daywise ( 36500 )">Daywise ( 36500 )</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lps-full-width">
<mat-label>Penalty Interest Procedure Number</mat-label>
<mat-select formControlName="penaltyInterestProcedureNumber">
<mat-option value="ON DB., PENALTY PRODUCT IS CLOSING BALANCE"
>On Debit, Penalty Product is Closing Balance</mat-option
>
<mat-option value="LIMIT IS CONSIDER FOR PENALTY PRODUCT">Limit is Considered for Penalty Product</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Overdrawn Interest Calculation Method</mat-label>
<mat-select formControlName="overdrawnInterestCalculationMethod">
<mat-option value="OVERDRAWN = BALANCE - LIMIT">Overdrawn = Balance - Limit</mat-option>
<mat-option value="OVERDRAWN = BALANCE - D.P.">Overdrawn = Balance - D.P.</mat-option>
<mat-option value="DON'T CALCULATE">Don't Calculate</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Calculate Overdrawn Interest After Renewal</mat-label>
<mat-select formControlName="calculateOverdrawnInterestAfterRenewal">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Interest On Credit Balance</mat-label>
<mat-select formControlName="interestOnCreditBalance">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Penalty Interest Date Different Than Regular Interest</mat-label>
<mat-select formControlName="penaltyInterestDateDifferentThanRegularInterest">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Fixed Deposit Overdraft Certificate Sort Order</mat-label>
<mat-select formControlName="fixedDepositOverdraftCertificateSortOrder">
<mat-option value="ORDER ON INTEREST RATE">Order on Interest Rate</mat-option>
<mat-option value="ORDER ON D.P.">Order on Drawing Power</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Use Document Submission Date For Interest</mat-label>
<mat-select formControlName="useDocumentSubmissionDateForInterest">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Loan Management System Renewal Penalty Active</mat-label>
<mat-select formControlName="loanManagementSystemRenewalPenaltyActive">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Minimum Normal Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumNormalInterestAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Minimum Interest Calculation Method</mat-label>
<mat-select formControlName="minimumInterestCalculationMethod">
<mat-option value="Flat Amount">Flat Amount</mat-option>
<mat-option value="Percentage of Balance">Percentage of Balance</mat-option>
<mat-option value="Tier Based">Tier Based</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Minimum Against Clearing Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumAgainstClearingInterestAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Limit Increase Upto Interest Amount In Interest Calculation</mat-label>
<mat-select formControlName="limitIncreaseUptoInterestAmountInInterestCalculation">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Minimum Penalty Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumPenaltyInterestAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Days For Limit Increase Upto Interest Amount</mat-label>
<input type="number" matInput formControlName="daysForLimitIncreaseUptoInterestAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Additional Interest Rate For Fixed Deposit Overdraft</mat-label>
<input type="number" matInput formControlName="additionalInterestRateForFixedDepositOverdraft" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Consider DD/Payorder In Against Clearing Interest Calculation</mat-label>
<mat-select formControlName="considerDdPayorderInAgainstClearingInterestCalculation">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Grace Days After Renewal Date</mat-label>
<input type="number" matInput formControlName="graceDaysAfterRenewDate" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Apply Undebited Interest Concept Enabled</mat-label>
<mat-select formControlName="applyUndebitedInterestConceptEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="lps-full-width">
<mat-label>Interest Register Print Format Type</mat-label>
<mat-select formControlName="interestRegisterPrintFormatType">
<mat-option value="INTEREST AMOUNT WITH OPENING BALANCE">Interest Amount with Opening Balance</mat-option>
<mat-option value="INTEREST ONLY">Interest Only</mat-option>
<mat-option value="FULL LEDGER FORMAT">Full Ledger Format</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Normal Grace Days After Overdue</mat-label>
<input type="number" matInput formControlName="normalGraceDaysAfterOverdue" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow Against Clearing Entry</mat-label>
<mat-select formControlName="allowAgainstClearingEntry">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow No Balance Entry</mat-label>
<mat-select formControlName="allowNoBalanceEntry">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Fixed Deposit Overdraft Penalty Interest Combined With Normal Interest</mat-label>
<mat-select formControlName="fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Allow No Balance Entry With Password</mat-label>
<mat-select formControlName="allowNoBalanceEntryWithPassword">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Undebited Simple Interest Account Wise</mat-label>
<mat-select formControlName="undebitedSimpleInterestAccountWise">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Daily Interest Calculation Enabled</mat-label>
<mat-select formControlName="dailyInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Daily Interest Calculation Start Date</mat-label>
<input type="text" matInput formControlName="dailyInterestCalculationStartDate" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Non Performing Asset Penal Interest Rate Update</mat-label>
<mat-select formControlName="nonPerformingAssetPenalInterestRateUpdate">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Non Performing Asset Penal Interest Rate</mat-label>
<input type="number" matInput formControlName="nonPerformingAssetPenalInterestRate" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Fixed Deposit Overdraft Penal Interest Rate Update In Branch Process</mat-label>
<mat-select formControlName="fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Add Penal Interest Rate</mat-label>
<input type="number" matInput formControlName="addPenalInterestRate" />
</mat-form-field>
</div>
</div>
<!-- Loan Charge-off behaviour (only for advanced) -->
@if (isAdvancedTransactionProcessingStrategy) {
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-ban lps-section__icon"></i>
<h3 class="lps-section__title">{{ 'labels.inputs.Loan Charge-off behaviour' | translate }}</h3>
</div>
<div class="lps-section__body">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Loan Charge-off behaviour' | translate }}</mat-label>
<mat-select formControlName="chargeOffBehaviour" required>
@for (chargeOffBehaviour of chargeOffBehaviourData; track chargeOffBehaviour) {
<mat-option [value]="chargeOffBehaviour.id">
{{ chargeOffBehaviour.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
}
<!-- Moratorium & Grace -->
<div class="lps-section">
<div class="lps-section__header">
@@ -807,54 +1188,6 @@
</div>
</div>
<!-- Event Settings -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-bell lps-section__icon"></i>
<h3 class="lps-section__title">
{{ 'labels.heading.Event Settings' | translate }}
<i
class="fas fa-question lps-question-icon"
matTooltip="{{ 'tooltips.Setting for event notifications to be sent' | translate }}"
></i>
</h3>
</div>
<div class="lps-section__body">
<div class="lps-checkbox-row lps-full-width">
<mat-checkbox
labelPosition="before"
formControlName="useDueForRepaymentsConfigurations"
matTooltip="{{ 'tooltips.Use or not the Global Configurations' | translate }}"
>
{{ 'labels.inputs.Use the Global Configurations values to the Repayment Event (notifications)' | translate }}
</mat-checkbox>
</div>
@if (!loanProductSettingsForm.value.useDueForRepaymentsConfigurations) {
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Due days for repayment event' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
matTooltip="{{ 'tooltips.Maximum outstanding loan account balance' | translate }}"
formControlName="dueDaysForRepaymentEvent"
/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.OverDue days for repayment event' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
matTooltip="{{ 'tooltips.Maximum outstanding loan account balance' | translate }}"
formControlName="overDueDaysForRepaymentEvent"
/>
</mat-form-field>
}
</div>
</div>
<!-- Configurable Terms -->
<div class="lps-section">
<div class="lps-section__header">
@@ -57,6 +57,18 @@
grid-template-columns: 1fr;
}
}
&__body.lps-grid-4 {
grid-template-columns: repeat(4, 1fr);
@media (width <= 1100px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
}
.lps-full-width {
@@ -15,7 +15,6 @@ import {
ReactiveFormsModule
} from '@angular/forms';
import { LoanProducts } from '../../loan-products';
import { rangeValidator } from 'app/shared/validators/percentage.validator';
import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
import { CodeName, OptionData, StringEnumOptionData } from 'app/shared/models/option-data.model';
import { ProcessingStrategyService } from '../../services/processing-strategy.service';
@@ -29,21 +28,20 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
selector: 'mifosx-loan-product-settings-step',
templateUrl: './loan-product-settings-step.component.html',
styleUrls: ['./loan-product-settings-step.component.scss'],
standalone: true,
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTooltip,
MatCheckbox,
MatIconButton,
FaIconComponent
FaIconComponent,
ReactiveFormsModule
]
})
export class LoanProductSettingsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private processingStrategyService = inject(ProcessingStrategyService);
DAYS_BEFORE_REPAYMENT_IS_DUE = LoanProducts.DAYS_BEFORE_REPAYMENT_IS_DUE;
DAYS_AFTER_REPAYMENT_IS_OVERDUE = LoanProducts.DAYS_AFTER_REPAYMENT_IS_OVERDUE;
@Input() toEdit: boolean;
@Input() loanProductsTemplate: any;
@Input() isLinkedToFloatingInterestRates: UntypedFormControl;
@@ -71,7 +69,6 @@ export class LoanProductSettingsStepComponent implements OnInit {
loanScheduleProcessingTypeData: OptionData[] = [];
isAdvancedTransactionProcessingStrategy = false;
advancedTransactionProcessingStrategyDisabled = true;
useDueForRepaymentsConfigurations = false;
rescheduleStrategyTypeDisabled = false;
chargeOffBehaviourData: StringEnumOptionData[] = [];
daysInYearCustomStrategyOptions: OptionData[] = [];
@@ -150,12 +147,10 @@ export class LoanProductSettingsStepComponent implements OnInit {
maxTrancheCount: this.loanProductsTemplate.maxTrancheCount,
outstandingLoanBalance: this.loanProductsTemplate.outstandingLoanBalance,
allowFullTermForTranche: this.loanProductsTemplate.allowFullTermForTranche,
enableDownPayment: this.loanProductsTemplate.enableDownPayment,
enableInstallmentLevelDelinquency: this.loanProductsTemplate.enableInstallmentLevelDelinquency,
allowCompoundingOnEod: this.loanProductsTemplate.allowCompoundingOnEod,
isCompoundingToBePostedAsTransaction: this.loanProductsTemplate.isCompoundingToBePostedAsTransaction,
loanScheduleType: this.loanProductsTemplate.loanScheduleType.code,
useDueForRepaymentsConfigurations: this.loanProductsTemplate.useDueForRepaymentsConfigurations,
allowAccrualPostingInArrears: this.loanProductsTemplate.allowAccrualPostingInArrears,
chargeOffBehaviour: this.loanProductsTemplate.chargeOffBehaviour.id
});
@@ -166,23 +161,6 @@ export class LoanProductSettingsStepComponent implements OnInit {
this.processingStrategyService.initialize(this.isAdvancedTransactionProcessingStrategy);
this.validateAdvancedPaymentStrategyControls();
if (
this.loanProductsTemplate.dueDaysForRepaymentEvent != null &&
this.loanProductsTemplate.overDueDaysForRepaymentEvent != null
) {
this.loanProductSettingsForm.patchValue({
useDueForRepaymentsConfigurations: false,
dueDaysForRepaymentEvent: this.loanProductsTemplate.dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent: this.loanProductsTemplate.overDueDaysForRepaymentEvent
});
} else {
this.loanProductSettingsForm.patchValue({
useDueForRepaymentsConfigurations: true,
dueDaysForRepaymentEvent: null,
overDueDaysForRepaymentEvent: null
});
}
if (this.loanProductsTemplate.delinquencyBucket) {
this.loanProductSettingsForm.patchValue({
delinquencyBucketId:
@@ -190,13 +168,6 @@ export class LoanProductSettingsStepComponent implements OnInit {
});
}
if (this.loanProductsTemplate.enableDownPayment) {
this.loanProductSettingsForm.patchValue({
disbursedAmountPercentageForDownPayment: this.loanProductsTemplate.disbursedAmountPercentageForDownPayment || 0,
enableAutoRepaymentForDownPayment: this.loanProductsTemplate.enableAutoRepaymentForDownPayment || false
});
}
if (this.loanProductsTemplate.isInterestRecalculationEnabled) {
this.loanProductSettingsForm.patchValue({
preClosureInterestCalculationStrategy:
@@ -355,24 +326,89 @@ export class LoanProductSettingsStepComponent implements OnInit {
graceOnArrearsAgeing: [true]
}),
delinquencyBucketId: [''],
enableDownPayment: [false],
enableInstallmentLevelDelinquency: [false],
allowCompoundingOnEod: [false],
isCompoundingToBePostedAsTransaction: [false],
useDueForRepaymentsConfigurations: [false],
dueDaysForRepaymentEvent: [
'',
[Validators.min(0)]
],
overDueDaysForRepaymentEvent: [
'',
[Validators.min(0)]
],
loanScheduleType: [
LoanProducts.LOAN_SCHEDULE_TYPE_CUMULATIVE,
Validators.required
],
allowAccrualPostingInArrears: [false]
allowAccrualPostingInArrears: [false],
// Cheque Book Settings
chequeBookIssueEnabled: ['NO'],
chequeValidationRequired: ['NO'],
isFirstChequeBookFree: ['NO'],
chequePrintingAccountType: [''],
allowWithdrawalSlipUsage: ['NO'],
maintainChequeBookStockRegistry: ['NO'],
freeChequeBooksPerYear: [''],
allowCreditByChequeNumber: ['NO'],
creditChequeValidationRequired: ['NO'],
graceDaysForStockStatement: [''],
// Additional Product Settings
accountTypeActiveForTransactions: ['YES'],
subTypeActivityEnabled: ['NO'],
accountOpeningCharges: [''],
maximumGoldLoanLimit: [''],
lastAccountNumberOpened: [''],
customerTerminationAllowed: ['NO'],
chargesForCashCreditAboveLimit: [''],
deathInterestGLAccount: [''],
startingLedgerNumber: [''],
endingLedgerNumber: [''],
currentLedgerNumber: [''],
remittanceAccountNumber: [''],
isNonResidentIndianAccount: ['NO'],
debitClearingGLAccount: [''],
missedCallAlertServiceActive: ['NO'],
standingInstructionChargeGLAccount: [''],
standingInstructionDebitChargeSkipOption: [''],
openingBalanceMultiplierForRemittanceCharges: [''],
// Interest Related Parameters
drawingPowerAndLimitGenerationProcedure: ['NORMAL D.P. ENTRY IS CONSIDER'],
advancesInterestCalculationMethod: ['DEBIT CLOSING BALANCE CONSIDER AS PRODUCT'],
againstInterestCalculationEnabled: ['NO'],
dayWiseAgainstClearingInterestRounding: ['YES'],
interestCalculationOneFourthMethod: ['0'],
againstClearingInterestProcedureType: ['NORMAL AGAINST CLEARING INTEREST'],
considerDebitClosingAmountForAgainstClearing: ['NO'],
interestRoundingMethod: [''],
penaltyInterestCalculationEnabled: ['NO'],
penaltyInterestCalculationMethod: [''],
penaltyInterestProcedureNumber: [''],
overdrawnInterestCalculationMethod: [''],
calculateOverdrawnInterestAfterRenewal: ['NO'],
interestOnCreditBalance: ['NO'],
penaltyInterestDateDifferentThanRegularInterest: ['YES'],
fixedDepositOverdraftCertificateSortOrder: ['ORDER ON INTEREST RATE'],
useDocumentSubmissionDateForInterest: ['NO'],
loanManagementSystemRenewalPenaltyActive: [''],
minimumNormalInterestAmount: [''],
minimumInterestCalculationMethod: [''],
minimumAgainstClearingInterestAmount: [''],
limitIncreaseUptoInterestAmountInInterestCalculation: ['NO'],
minimumPenaltyInterestAmount: [''],
daysForLimitIncreaseUptoInterestAmount: [''],
additionalInterestRateForFixedDepositOverdraft: [''],
considerDdPayorderInAgainstClearingInterestCalculation: ['YES'],
graceDaysAfterRenewDate: [''],
applyUndebitedInterestConceptEnabled: ['YES'],
interestRegisterPrintFormatType: ['INTEREST AMOUNT WITH OPENING BALANCE'],
normalGraceDaysAfterOverdue: [''],
allowAgainstClearingEntry: [''],
allowNoBalanceEntry: ['YES'],
fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest: ['NO'],
allowNoBalanceEntryWithPassword: ['NO'],
undebitedSimpleInterestAccountWise: ['NO'],
dailyInterestCalculationEnabled: ['YES'],
dailyInterestCalculationStartDate: ['01/02/2023'],
nonPerformingAssetPenalInterestRateUpdate: ['YES'],
nonPerformingAssetPenalInterestRate: [''],
fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess: ['YES'],
addPenalInterestRate: ['']
});
}
@@ -595,22 +631,6 @@ export class LoanProductSettingsStepComponent implements OnInit {
}
});
this.loanProductSettingsForm.get('enableDownPayment').valueChanges.subscribe((enableDownPayment) => {
if (enableDownPayment) {
this.loanProductSettingsForm.addControl(
'disbursedAmountPercentageForDownPayment',
new UntypedFormControl(0, [
Validators.required,
rangeValidator(0, 100)
])
);
this.loanProductSettingsForm.addControl('enableAutoRepaymentForDownPayment', new UntypedFormControl(false, []));
} else {
this.loanProductSettingsForm.removeControl('disbursedAmountPercentageForDownPayment');
this.loanProductSettingsForm.removeControl('enableAutoRepaymentForDownPayment');
}
});
this.loanProductSettingsForm
.get('transactionProcessingStrategyCode')
.valueChanges.subscribe((transactionProcessingStrategyCode: string) => {
@@ -650,22 +670,6 @@ export class LoanProductSettingsStepComponent implements OnInit {
}
});
this.loanProductSettingsForm
.get('useDueForRepaymentsConfigurations')
.valueChanges.subscribe((useDueForRepaymentsConfigurations: boolean) => {
if (useDueForRepaymentsConfigurations) {
this.loanProductSettingsForm.patchValue({
dueDaysForRepaymentEvent: null,
overDueDaysForRepaymentEvent: null
});
} else {
this.loanProductSettingsForm.patchValue({
dueDaysForRepaymentEvent: this.getGlobalConfigValue(LoanProducts.DAYS_BEFORE_REPAYMENT_IS_DUE),
overDueDaysForRepaymentEvent: this.getGlobalConfigValue(LoanProducts.DAYS_AFTER_REPAYMENT_IS_OVERDUE)
});
}
});
this.loanProductSettingsForm.get('loanScheduleType').valueChanges.subscribe((loanScheduleType: string) => {
this.transactionProcessingStrategyData = [];
if (loanScheduleType === LoanProducts.LOAN_SCHEDULE_TYPE_CUMULATIVE) {
@@ -771,10 +775,6 @@ export class LoanProductSettingsStepComponent implements OnInit {
get loanProductSettings() {
const productSettings = this.loanProductSettingsForm.value;
if (this.loanProductSettingsForm.value.useDueForRepaymentsConfigurations) {
productSettings['dueDaysForRepaymentEvent'] = null;
productSettings['overDueDaysForRepaymentEvent'] = null;
}
if (productSettings['delinquencyBucketId'] === '') {
productSettings['delinquencyBucketId'] = null;
}
@@ -18,43 +18,6 @@
></i>
</div>
<div class="lpt-grid">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Minimum' | translate }}</mat-label>
<input type="number" matInput formControlName="minPrincipal" [min]="1" step="0.01" />
<mat-error *ngIf="loanProductTermsForm.get('minPrincipal')?.hasError('min')">
{{ 'labels.commons.Minimum Value must be' | translate }}
<strong>{{ loanProductTermsForm.get('minPrincipal')?.errors?.['min']?.min || 1 }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Default' | translate }}</mat-label>
<input type="number" matInput formControlName="principal" required [min]="1" step="0.01" />
<mat-error
*ngIf="
loanProductTermsForm.get('principal')?.hasError('required') &&
(loanProductTermsForm.get('principal')?.touched || loanProductTermsForm.get('principal')?.dirty)
"
>
{{ 'labels.catalogs.Default' | translate }} {{ 'labels.inputs.Principal' | translate }}
{{ 'labels.commons.is required' | translate }}
</mat-error>
<mat-error *ngIf="loanProductTermsForm.get('principal')?.hasError('min')">
{{ 'labels.catalogs.Default' | translate }} {{ 'labels.inputs.Principal' | translate }}
{{ 'labels.commons.is' | translate }}
<strong>{{ loanProductTermsForm.get('principal')?.errors?.['min']?.min || 1 }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Maximum' | translate }}</mat-label>
<input type="number" matInput formControlName="maxPrincipal" [min]="1" step="0.01" />
<mat-error *ngIf="loanProductTermsForm.get('maxPrincipal')?.hasError('min')">
{{ 'labels.commons.Minimum Value must be' | translate }}
<strong>{{ loanProductTermsForm.get('maxPrincipal')?.errors?.['min']?.min || 1 }}</strong>
</mat-error>
</mat-form-field>
<div class="lpt-checkbox-row lpt-full-width">
<mat-checkbox labelPosition="before" formControlName="allowApprovedDisbursedAmountsOverApplied">
{{ 'labels.inputs.Allow approval / disbursal above loan applied amount' | translate }}
@@ -119,57 +82,6 @@
</div>
</div>
<!-- Repayments Section -->
<div class="lpt-section">
<div class="lpt-section-header">
<i class="fas fa-calendar-alt lpt-section-icon"></i>
<h3>{{ 'labels.inputs.Number of repayments' | translate }}</h3>
<i
class="fas fa-question-circle lpt-help-icon"
matTooltip="{{
'tooltips.Fields are used to define the minimum, default, and maximum number of repayments' | translate
}}"
></i>
</div>
<div class="lpt-grid lpt-grid-3">
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Minimum' | translate }}</mat-label>
<input type="number" matInput formControlName="minNumberOfRepayments" [min]="1" />
<mat-error>
{{ 'labels.commons.Minimum Value must be' | translate }}
<strong>1</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Default' | translate }}</mat-label>
<input type="number" matInput formControlName="numberOfRepayments" required [min]="1" />
<mat-error>
{{ 'labels.catalogs.Default' | translate }} {{ 'labels.inputs.Number of repayments' | translate }}
{{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Maximum' | translate }}</mat-label>
<input type="number" matInput formControlName="maxNumberOfRepayments" [min]="1" />
<mat-error>
{{ 'labels.commons.Maximum Value must be' | translate }}
<strong>1</strong>
{{ 'labels.commons.and must be greater than' | translate }}
<strong>{{ 'labels.inputs.Minimum' | translate }}</strong>
</mat-error>
</mat-form-field>
<div class="lpt-checkbox-row lpt-full-width">
<mat-checkbox labelPosition="before" formControlName="interestRecognitionOnDisbursementDate">
{{ 'labels.inputs.Is interest recognition on disbursement date?' | translate }}
</mat-checkbox>
</div>
</div>
</div>
<!-- Interest Rate Section -->
<div class="lpt-section">
<div class="lpt-section-header">
@@ -352,250 +264,6 @@
}
</div>
<!-- Loan Cycle Variations Section -->
<div class="lpt-section">
<div class="lpt-section-header">
<i class="fas fa-sync lpt-section-icon"></i>
<h3>{{ 'labels.inputs.Variations' | translate }}</h3>
</div>
<div class="lpt-grid">
<div class="lpt-checkbox-row lpt-full-width">
<mat-checkbox labelPosition="before" formControlName="useBorrowerCycle">
{{ 'labels.inputs.Terms vary based on loan cycle' | translate }}
</mat-checkbox>
</div>
</div>
@if (loanProductTermsForm.value.useBorrowerCycle) {
<!-- Principal by loan cycle -->
<div class="lpt-subsection">
<div class="lpt-subsection-header">
<h4 class="lpt-subsection-title">{{ 'labels.inputs.Principal by loan cycle' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
(click)="addVariationsForBorrowerCycle('Principal', principalVariationsForBorrowerCycle)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (principalVariationsForBorrowerCycle.value.length) {
<div class="lpt-table-wrapper">
<table mat-table [dataSource]="principalVariationsForBorrowerCycle.value">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find: valueConditionTypeData : 'id' : 'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Loan Cycle' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Minimum' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Default' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Maximum' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let variation; let i = index">
<button
mat-icon-button
color="primary"
(click)="editVariationsForBorrowerCycle('Principal', principalVariationsForBorrowerCycle, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="deleteVariationsForBorrowerCycle(principalVariationsForBorrowerCycle, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Number of repayments by loan cycle -->
<div class="lpt-subsection">
<div class="lpt-subsection-header">
<h4 class="lpt-subsection-title">{{ 'labels.inputs.Number of repayments by loan cycle' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
(click)="addVariationsForBorrowerCycle('NumberOfRepayments', numberOfRepaymentVariationsForBorrowerCycle)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (numberOfRepaymentVariationsForBorrowerCycle.value.length) {
<div class="lpt-table-wrapper">
<table mat-table [dataSource]="numberOfRepaymentVariationsForBorrowerCycle.value">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find: valueConditionTypeData : 'id' : 'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Loan Cycle' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Minimum' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Default' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Maximum' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let variation; let i = index">
<button
mat-icon-button
color="primary"
(click)="
editVariationsForBorrowerCycle(
'NumberOfRepayments',
numberOfRepaymentVariationsForBorrowerCycle,
i
)
"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="deleteVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
</div>
<!-- Nominal interest rate by loan cycle -->
<div class="lpt-subsection">
<div class="lpt-subsection-header">
<h4 class="lpt-subsection-title">{{ 'labels.inputs.Nominal interest rate by loan cycle' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
(click)="addVariationsForBorrowerCycle('NominalInterestRate', interestRateVariationsForBorrowerCycle)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (interestRateVariationsForBorrowerCycle.value.length) {
<div class="lpt-table-wrapper">
<table mat-table [dataSource]="interestRateVariationsForBorrowerCycle.value">
<ng-container matColumnDef="valueConditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.valueConditionType | find: valueConditionTypeData : 'id' : 'value' }}
</td>
</ng-container>
<ng-container matColumnDef="borrowerCycleNumber">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Loan Cycle' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.borrowerCycleNumber }}
</td>
</ng-container>
<ng-container matColumnDef="minValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Minimum' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.minValue }}
</td>
</ng-container>
<ng-container matColumnDef="defaultValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Default' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.defaultValue }}
</td>
</ng-container>
<ng-container matColumnDef="maxValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Maximum' | translate }}</th>
<td mat-cell *matCellDef="let variation">
{{ variation.maxValue }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let element; let i = index">
<button
mat-icon-button
color="primary"
(click)="
editVariationsForBorrowerCycle('NominalInterestRate', interestRateVariationsForBorrowerCycle, i)
"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="deleteVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle, i)"
>
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
</div>
}
</div>
<!-- Repayment Schedule Section -->
<div class="lpt-section">
<div class="lpt-section-header">
@@ -11,36 +11,14 @@ import {
UntypedFormGroup,
UntypedFormBuilder,
Validators,
UntypedFormArray,
UntypedFormControl,
ReactiveFormsModule
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
import { ProcessingStrategyService } from '../../services/processing-strategy.service';
import { TranslateService } from '@ngx-translate/core';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatButton, MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { FindPipe } from '../../../../pipes/find.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -53,24 +31,12 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTooltip,
MatCheckbox,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatIconButton,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FindPipe
]
})
export class LoanProductTermsStepComponent implements OnInit, OnChanges {
private formBuilder = inject(UntypedFormBuilder);
private processingStrategyService = inject(ProcessingStrategyService);
private dialog = inject(MatDialog);
private translateService = inject(TranslateService);
@Input() loanProductsTemplate: any;
@@ -80,30 +46,20 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
/** Zero Interest control. */
zeroInterest = new UntypedFormControl(false);
valueConditionTypeData: any;
isAdvancedTransactionProcessingStrategy = false;
floatingRateData: any;
interestRateFrequencyTypeData: any;
overAppliedCalculationTypeData: any;
repaymentFrequencyTypeData: any;
repaymentStartDateTypeOptions: any;
displayedColumns: string[] = [
'valueConditionType',
'borrowerCycleNumber',
'minValue',
'defaultValue',
'maxValue',
'actions'
];
isAdvancedTransactionProcessingStrategy = false;
constructor() {
this.createLoanProductTermsForm();
this.setConditionalControls();
}
ngOnInit() {
this.valueConditionTypeData = this.loanProductsTemplate.valueConditionTypeOptions;
this.floatingRateData = this.loanProductsTemplate.floatingRateOptions;
this.interestRateFrequencyTypeData = this.loanProductsTemplate.interestRateFrequencyTypeOptions;
this.repaymentFrequencyTypeData = this.loanProductsTemplate.repaymentFrequencyTypeOptions;
@@ -114,12 +70,6 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
];
this.loanProductTermsForm.patchValue({
minPrincipal: this.loanProductsTemplate.minPrincipal,
principal: this.loanProductsTemplate.principal,
maxPrincipal: this.loanProductsTemplate.maxPrincipal,
minNumberOfRepayments: this.loanProductsTemplate.minNumberOfRepayments,
numberOfRepayments: this.loanProductsTemplate.numberOfRepayments,
maxNumberOfRepayments: this.loanProductsTemplate.maxNumberOfRepayments,
isLinkedToFloatingInterestRates: this.loanProductsTemplate.isLinkedToFloatingInterestRates,
minInterestRatePerPeriod: this.loanProductsTemplate.minInterestRatePerPeriod,
interestRatePerPeriod: this.loanProductsTemplate.interestRatePerPeriod,
@@ -132,13 +82,11 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
minDifferentialLendingRate: this.loanProductsTemplate.minDifferentialLendingRate,
defaultDifferentialLendingRate: this.loanProductsTemplate.defaultDifferentialLendingRate,
maxDifferentialLendingRate: this.loanProductsTemplate.maxDifferentialLendingRate,
useBorrowerCycle: this.loanProductsTemplate.useBorrowerCycle,
repaymentEvery: this.loanProductsTemplate.repaymentEvery,
repaymentFrequencyType: this.loanProductsTemplate.repaymentFrequencyType.id,
minimumDaysBetweenDisbursalAndFirstRepayment:
this.loanProductsTemplate.minimumDaysBetweenDisbursalAndFirstRepayment,
repaymentStartDateType: this.loanProductsTemplate.repaymentStartDateType.id || 1,
interestRecognitionOnDisbursementDate: this.loanProductsTemplate.interestRecognitionOnDisbursementDate || false
repaymentStartDateType: this.loanProductsTemplate.repaymentStartDateType.id || 1
});
if (this.loanProductsTemplate.allowApprovedDisbursedAmountsOverApplied) {
@@ -148,34 +96,6 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
});
}
this.loanProductTermsForm.setControl(
'principalVariationsForBorrowerCycle',
this.formBuilder.array(
this.loanProductsTemplate.principalVariationsForBorrowerCycle.map((variation: any) => ({
...variation,
valueConditionType: variation.valueConditionType.id
}))
)
);
this.loanProductTermsForm.setControl(
'numberOfRepaymentVariationsForBorrowerCycle',
this.formBuilder.array(
this.loanProductsTemplate.numberOfRepaymentVariationsForBorrowerCycle.map((variation: any) => ({
...variation,
valueConditionType: variation.valueConditionType.id
}))
)
);
this.loanProductTermsForm.setControl(
'interestRateVariationsForBorrowerCycle',
this.formBuilder.array(
this.loanProductsTemplate.interestRateVariationsForBorrowerCycle.map((variation: any) => ({
...variation,
valueConditionType: variation.valueConditionType.id
}))
)
);
this.zeroInterest.patchValue(
this.loanProductsTemplate.minInterestRatePerPeriod === 0 &&
this.loanProductsTemplate.interestRatePerPeriod === 0 &&
@@ -190,45 +110,6 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
createLoanProductTermsForm() {
this.loanProductTermsForm = this.formBuilder.group({
useBorrowerCycle: [false],
minPrincipal: [
'',
[
Validators.min(1)
]
],
principal: [
'',
[
Validators.required,
Validators.min(1)
]
],
maxPrincipal: [
'',
[
Validators.min(1)
]
],
minNumberOfRepayments: [
'',
[
Validators.pattern('^[1-9]\\d*$')
]
],
numberOfRepayments: [
'',
[
Validators.required,
Validators.pattern('^[1-9]\\d*$')
]
],
maxNumberOfRepayments: [
'',
[
Validators.pattern('^[1-9]\\d*$')
]
],
isLinkedToFloatingInterestRates: [false],
allowApprovedDisbursedAmountsOverApplied: [false],
overAppliedCalculationType: [{ value: null, disabled: true }],
@@ -275,8 +156,7 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
[]
],
repaymentStartDateType: [1],
fixedLength: [null],
interestRecognitionOnDisbursementDate: [false]
fixedLength: [null]
});
}
@@ -350,18 +230,6 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
}
});
this.loanProductTermsForm.get('useBorrowerCycle').valueChanges.subscribe((useBorrowerCycle) => {
if (useBorrowerCycle) {
this.loanProductTermsForm.addControl('principalVariationsForBorrowerCycle', this.formBuilder.array([]));
this.loanProductTermsForm.addControl('numberOfRepaymentVariationsForBorrowerCycle', this.formBuilder.array([]));
this.loanProductTermsForm.addControl('interestRateVariationsForBorrowerCycle', this.formBuilder.array([]));
} else {
this.loanProductTermsForm.removeControl('principalVariationsForBorrowerCycle');
this.loanProductTermsForm.removeControl('numberOfRepaymentVariationsForBorrowerCycle');
this.loanProductTermsForm.removeControl('interestRateVariationsForBorrowerCycle');
}
});
this.zeroInterest.valueChanges.subscribe((zeroInterest) => {
if (zeroInterest) {
this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(0);
@@ -388,129 +256,6 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
});
}
get principalVariationsForBorrowerCycle(): UntypedFormArray {
return this.loanProductTermsForm.get('principalVariationsForBorrowerCycle') as UntypedFormArray;
}
get numberOfRepaymentVariationsForBorrowerCycle(): UntypedFormArray {
return this.loanProductTermsForm.get('numberOfRepaymentVariationsForBorrowerCycle') as UntypedFormArray;
}
get interestRateVariationsForBorrowerCycle(): UntypedFormArray {
return this.loanProductTermsForm.get('interestRateVariationsForBorrowerCycle') as UntypedFormArray;
}
setLoanProductTermsFormDirty() {
if (this.loanProductTermsForm.pristine) {
this.loanProductTermsForm.markAsDirty();
}
}
addVariationsForBorrowerCycle(formType: string, variationsForBorrowerCycleFormArray: UntypedFormArray) {
const data = this.getData(formType);
const addVariationsForBorrowerCycleDialogRef = this.dialog.open(FormDialogComponent, { data });
addVariationsForBorrowerCycleDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
variationsForBorrowerCycleFormArray.push(response.data);
this.setLoanProductTermsFormDirty();
}
});
}
editVariationsForBorrowerCycle(
formType: string,
variationsForBorrowerCycleFormArray: UntypedFormArray,
index: number
) {
const data = {
...this.getData(formType, variationsForBorrowerCycleFormArray.at(index).value),
layout: { addButtonText: 'Edit' }
};
const addVariationsForBorrowerCycleDialogRef = this.dialog.open(FormDialogComponent, { data });
addVariationsForBorrowerCycleDialogRef.afterClosed().subscribe((response: any) => {
if (response.data) {
variationsForBorrowerCycleFormArray.at(index).patchValue(response.data.value);
this.setLoanProductTermsFormDirty();
}
});
}
deleteVariationsForBorrowerCycle(variationsForBorrowerCycleFormArray: UntypedFormArray, index: number) {
const deleteVariationsForBorrowerCycleDialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
deleteVariationsForBorrowerCycleDialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
variationsForBorrowerCycleFormArray.removeAt(index);
this.setLoanProductTermsFormDirty();
}
});
}
getData(formType: string, values?: any) {
switch (formType) {
case 'Principal':
return {
title: this.translateService.instant('labels.heading.Principal by loan cycle'),
formfields: this.getFormfields(values)
};
case 'NumberOfRepayments':
return {
title: this.translateService.instant('labels.heading.Number of Repayments by loan cycle'),
formfields: this.getFormfields(values)
};
case 'NominalInterestRate':
return {
title: this.translateService.instant('labels.heading.Nominal Interest Rate by loan cycle'),
formfields: this.getFormfields(values)
};
}
}
getFormfields(values?: any) {
const formfields: FormfieldBase[] = [
new SelectBase({
controlName: 'valueConditionType',
label: this.translateService.instant('labels.inputs.Condition'),
value: values ? values.valueConditionType : this.valueConditionTypeData[0].id,
options: { label: 'value', value: 'id', data: this.valueConditionTypeData },
required: true,
order: 1
}),
new InputBase({
controlName: 'borrowerCycleNumber',
label: this.translateService.instant('labels.inputs.Loan Cycle'),
value: values ? values.borrowerCycleNumber : undefined,
type: 'number',
required: true,
order: 2
}),
new InputBase({
controlName: 'minValue',
label: this.translateService.instant('labels.inputs.Minimum'),
value: values ? values.minValue : undefined,
type: 'number',
order: 3
}),
new InputBase({
controlName: 'defaultValue',
label: this.translateService.instant('labels.inputs.Default'),
value: values ? values.defaultValue : undefined,
type: 'number',
required: true,
order: 4
}),
new InputBase({
controlName: 'maxValue',
label: this.translateService.instant('labels.inputs.Maximum'),
value: values ? values.maxValue : undefined,
type: 'number',
order: 5
})
];
return formfields;
}
get loanProductTerms() {
const formValue = this.loanProductTermsForm.getRawValue();
// Normalize decimal separators: convert comma to dot for backend compatibility
@@ -8,7 +8,6 @@
import { Injectable, inject } from '@angular/core';
import { SettingsService } from 'app/settings/settings.service';
import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
@Injectable({
providedIn: 'root'
@@ -24,46 +23,15 @@ export class LoanProducts {
public static ADVANCED_PAYMENT_ALLOCATION_STRATEGY = 'advanced-payment-allocation-strategy';
public static DAYS_BEFORE_REPAYMENT_IS_DUE = 'days-before-repayment-is-due';
public static DAYS_AFTER_REPAYMENT_IS_OVERDUE = 'days-after-repayment-is-overdue';
globalConfigurations: string[] = [
LoanProducts.DAYS_BEFORE_REPAYMENT_IS_DUE,
LoanProducts.DAYS_AFTER_REPAYMENT_IS_OVERDUE
];
propertyNames: string[] = [
'dueDaysForRepaymentEvent',
'overDueDaysForRepaymentEvent'
];
public static isAdvancedPaymentAllocationStrategy(code: string): boolean {
return code === this.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
}
public setItemsByDefault(configurations: any) {
const itemsByDefault: GlobalConfiguration[] = [];
configurations.globalConfiguration.forEach((config: GlobalConfiguration) => {
if (this.globalConfigurations.includes(config.name)) {
itemsByDefault.push(config);
}
});
return itemsByDefault;
public setItemsByDefault(configurations: any): any[] {
return [];
}
public updateLoanProductDefaults(loanProductTemplate: any, isForUpdate: boolean) {
const itemsByDefault: GlobalConfiguration[] = loanProductTemplate['itemsByDefault'];
itemsByDefault.forEach((config: GlobalConfiguration) => {
const propertyName = this.resolvePropertyName(config.name);
if (propertyName !== '') {
if (isForUpdate) {
if (!loanProductTemplate[propertyName] || loanProductTemplate[propertyName] === '') {
loanProductTemplate[propertyName] = config.value;
}
} else {
loanProductTemplate[propertyName] = config.value;
}
}
});
return loanProductTemplate;
}
@@ -86,34 +54,14 @@ export class LoanProducts {
loanProduct.allowPartialPeriodInterestCalculation = loanProduct.allowPartialPeriodInterestCalculation;
delete loanProduct.allowPartialPeriodInterestCalculation;
// Set Default values If they were not set
itemsByDefault.forEach((config: GlobalConfiguration) => {
const propertyName = this.resolvePropertyName(config.name);
if (propertyName !== '') {
if (!loanProduct[propertyName] || loanProduct[propertyName] === '') {
loanProduct[propertyName] = config.value;
}
}
});
return loanProduct;
}
public isItemByDefault(propertyName: string): boolean {
return this.propertyNames.includes(propertyName);
return false;
}
public isGlobalConfigurations(propertyName: string): boolean {
return this.globalConfigurations.includes(propertyName);
}
private resolvePropertyName(configName: string): string {
if (this.globalConfigurations.includes(configName)) {
const idx = this.globalConfigurations.indexOf(configName);
if (idx > -1) {
return this.propertyNames[idx];
}
}
return '';
return false;
}
}
@@ -26,17 +26,13 @@ export interface LoanProduct {
id: number;
name: string;
shortName: string;
includeInBorrowerCycle: boolean;
useBorrowerCycle: boolean;
status: string;
currency: Currency;
currencyCode?: string;
fundId?: number;
fundName?: string;
startDate?: string;
closeDate?: string;
description?: string;
installmentAmountInMultiplesOf?: number;
principal: number;
minPrincipal?: number;
maxPrincipal?: number;
@@ -123,8 +119,6 @@ export interface LoanProduct {
graceOnInterestPayment?: number;
graceOnInterestCharged?: number;
inArrearsTolerance?: number;
dueDaysForRepaymentEvent: number;
overDueDaysForRepaymentEvent: number;
enableDownPayment: boolean;
enableAutoRepaymentForDownPayment: boolean;
enableInstallmentLevelDelinquency: boolean;
@@ -23,7 +23,6 @@
<mifosx-loan-product-summary
[action]="'view'"
[loanProduct]="loanProduct"
[useDueForRepaymentsConfigurations]="useDueForRepaymentsConfigurations"
[paymentAllocations]="loanProduct.paymentAllocation"
[creditAllocations]="loanProduct.creditAllocation"
[supportedInterestRefundTypes]="loanProduct.supportedInterestRefundTypes"
@@ -39,13 +39,10 @@ export class GeneralTabComponent implements OnInit {
private translateService = inject(TranslateService);
loanProduct: LoanProduct;
useDueForRepaymentsConfigurations = false;
constructor() {
this.route.data.subscribe((data: { loanProduct: any }) => {
this.loanProduct = data.loanProduct;
this.useDueForRepaymentsConfigurations =
!this.loanProduct.dueDaysForRepaymentEvent && !this.loanProduct.overDueDaysForRepaymentEvent;
});
}