feat(WEB-505): implement waive penalties for loan repayment and reschedule (#2941)

Fix waive-penalties amount calc and align translations across locales

fix:test issue

Fix waive penalties recalculation, clean up subscription leaks, and align penalty translations
This commit is contained in:
Shubham Kumar
2025-12-28 16:09:08 -06:00
committed by GitHub
parent cb52f59dc3
commit 62c175ff15
21 changed files with 668 additions and 38 deletions
@@ -116,6 +116,42 @@
<input matInput formControlName="newInterestRate" />
</mat-form-field>
}
<mat-checkbox class="m-b-10" [formControl]="waivePenalties">
{{ 'labels.inputs.Waive Penalties' | translate }}
</mat-checkbox>
@if (waivePenalties.value) {
@if (penalties.length > 0) {
<div class="layout-row-wrap m-t-20">
<div class="flex-100 m-b-10">
<mat-checkbox [checked]="selectAllPenalties" (change)="toggleSelectAllPenalties()">
{{ 'labels.inputs.All' | translate }}
</mat-checkbox>
</div>
<div class="flex-100 layout-row-wrap">
@for (penalty of penalties; track penalty.id) {
<div class="penalty-item-container">
<mat-checkbox
[checked]="isPenaltySelected(penalty.id)"
(change)="togglePenaltySelection(penalty.id)"
></mat-checkbox>
<span class="penalty-label mat-body-strong">{{ 'labels.inputs.Penalty' | translate }}</span>
<span class="penalty-name">{{ getPenaltyDisplayKey(penalty) | translate }}</span>
<span class="penalty-date">{{ penalty.dueDate | dateFormat }}</span>
<span class="penalty-amount r-amount">{{
penalty.amountOutstanding || penalty.amount || 0 | formatNumber
}}</span>
</div>
}
</div>
</div>
} @else {
<div class="m-t-20 m-l-10">
<span class="mat-body-2">{{ 'labels.text.No penalties found' | translate }}</span>
</div>
}
}
</div>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
@@ -1,3 +1,45 @@
.container {
max-width: 37rem;
}
.penalty-item-container {
width: 100%;
max-width: 100%;
display: grid;
grid-template-columns: auto minmax(80px, 110px) minmax(140px, 100%) minmax(130px, 170px) minmax(90px, 120px);
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 12px 16px;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 4px;
box-sizing: border-box;
overflow: hidden;
}
.penalty-label {
min-width: 80px;
}
.penalty-name {
min-width: 120px;
}
.penalty-date {
color: rgb(0 0 0 / 70%);
white-space: nowrap;
}
.penalty-amount {
text-align: right;
white-space: nowrap;
}
:host-context(.dark-theme) {
.penalty-item-container {
background-color: #3a3a3a;
color: rgb(255 255 255 / 87%);
border-color: #4a4a4a;
}
}
@@ -1,4 +1,4 @@
import { Component, OnInit, Input, inject } from '@angular/core';
import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core';
import { LoansService } from 'app/loans/loans.service';
import {
UntypedFormBuilder,
@@ -13,7 +13,10 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { MatCheckbox } from '@angular/material/checkbox';
import { PenaltyManagementService } from 'app/loans/services/penalty-management.service';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'mifosx-loan-reschedule',
@@ -21,7 +24,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./loan-reschedule.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox
MatCheckbox,
FormatNumberPipe
]
})
export class LoanRescheduleComponent implements OnInit {
@@ -31,6 +35,8 @@ export class LoanRescheduleComponent implements OnInit {
private router = inject(Router);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private penaltyManagementService = inject(PenaltyManagementService);
private destroyRef = inject(DestroyRef);
@Input() dataObject: any;
loanId: any;
@@ -46,6 +52,14 @@ export class LoanRescheduleComponent implements OnInit {
introduceGracePeriods = new UntypedFormControl(false);
extendRepaymentPeriod = new UntypedFormControl(false);
adjustinterestrates = new UntypedFormControl(false);
waivePenalties = new UntypedFormControl(false);
/** Penalties list */
penalties: any[] = [];
/** Selected penalty IDs */
selectedPenalties: number[] = [];
/** Select all penalties checkbox */
selectAllPenalties = false;
/**
* @param {FormBuilder} formBuilder Form Builder.
@@ -62,6 +76,8 @@ export class LoanRescheduleComponent implements OnInit {
this.maxDate = this.settingsService.businessDate;
this.codes = this.dataObject.rescheduleReasons;
this.setRescheduleLoanForm();
this.loadPenalties();
this.setupWaivePenaltiesListener();
}
setRescheduleLoanForm() {
@@ -109,6 +125,12 @@ export class LoanRescheduleComponent implements OnInit {
locale
};
data.loanId = this.loanId;
// Add waived penalty charge IDs if penalties are being waived
if (this.waivePenalties.value && this.selectedPenalties.length > 0) {
data.chargeIds = this.selectedPenalties;
}
this.loanService.submitRescheduleData(data).subscribe((response: any) => {
// TODO: needs to be updated
// mentioned in Community App:
@@ -116,4 +138,74 @@ export class LoanRescheduleComponent implements OnInit {
this.router.navigate(['../../general'], { relativeTo: this.route });
});
}
/**
* Load penalties for the loan
* Penalties are charges calculated for installments in the payment schedule.
* Each penalty charge has a dueDate that corresponds to an installment due date.
*/
loadPenalties() {
this.penaltyManagementService.loadPenalties(this.loanId).subscribe({
next: (penalties: any[]) => {
this.penalties = penalties;
},
error: (error: any) => {
console.error('Error loading penalties:', error);
this.penalties = [];
}
});
}
/**
* Setup listener for waive penalties checkbox
* Following the pattern of using valueChanges.subscribe()
*/
setupWaivePenaltiesListener() {
this.waivePenalties.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value: boolean) => {
if (!value) {
// Reset selections when toggling off
this.selectedPenalties = [];
this.selectAllPenalties = false;
}
});
}
/**
* Toggle select all penalties
* Following the toggleSelects() pattern from loans-active-client-members
*/
toggleSelectAllPenalties() {
const result = this.penaltyManagementService.toggleSelectAllPenalties(this.selectAllPenalties, this.penalties);
this.selectAllPenalties = result.selectAllPenalties;
this.selectedPenalties = result.selectedPenalties;
}
/**
* Toggle individual penalty selection
* Following the toggleSelect() pattern from loans-active-client-members
*/
togglePenaltySelection(penaltyId: number) {
const result = this.penaltyManagementService.togglePenaltySelection(
penaltyId,
this.selectedPenalties,
this.penalties
);
this.selectedPenalties = result.selectedPenalties;
this.selectAllPenalties = result.selectAllPenalties;
}
/**
* Check if penalty is selected
*/
isPenaltySelected(penaltyId: number): boolean {
return this.penaltyManagementService.isPenaltySelected(penaltyId, this.selectedPenalties);
}
/**
* Get penalty display key or plain text for translation/output
* Normalizes common backend values (like MORA / labels.inputs.*) to translation keys
*/
getPenaltyDisplayKey(penalty: any): string {
return this.penaltyManagementService.getPenaltyDisplayKey(penalty);
}
}
@@ -89,16 +89,50 @@
</mat-select>
</mat-form-field>
<div class="flex-fill">
<span class="expandcollapsebutton m-l-10 m-t-40 flex-75" (click)="addPaymentDetails()">
<mat-slide-toggle>
<div [className]="showPaymentDetails ? 'enabled' : 'disabled'">
<span class="m-l-10">{{ 'labels.inputs.Show Payment Details' | translate }}</span>
</div>
</mat-slide-toggle>
</span>
<div class="flex-fill m-b-10">
<mat-slide-toggle [checked]="showPaymentDetails" (change)="addPaymentDetails()">
{{ 'labels.inputs.Show Payment Details' | translate }}
</mat-slide-toggle>
</div>
<div class="flex-fill m-b-10">
<mat-slide-toggle [checked]="waivePenalties" (change)="toggleWaivePenalties()">
{{ 'labels.inputs.Waive Penalties' | translate }}
</mat-slide-toggle>
</div>
@if (waivePenalties) {
@if (penalties.length > 0) {
<div class="layout-row-wrap m-t-20">
<div class="flex-100 m-b-10">
<mat-checkbox [checked]="selectAllPenalties" (change)="toggleSelectAllPenalties()">
{{ 'labels.inputs.All' | translate }}
</mat-checkbox>
</div>
<div class="flex-100 layout-row-wrap">
@for (penalty of penalties; track penalty.id) {
<div class="penalty-item-container">
<mat-checkbox
[checked]="isPenaltySelected(penalty.id)"
(change)="togglePenaltySelection(penalty.id)"
></mat-checkbox>
<span class="penalty-label mat-body-strong">{{ 'labels.inputs.Penalty' | translate }}</span>
<span class="penalty-name">{{ getPenaltyDisplayKey(penalty) | translate }}</span>
<span class="penalty-date">{{ penalty.dueDate | dateFormat }}</span>
<span class="penalty-amount r-amount">{{
penalty.amountOutstanding || penalty.amount || 0 | formatNumber
}}</span>
</div>
}
</div>
</div>
} @else {
<div class="m-t-20 m-l-10">
<span class="mat-body-2">{{ 'labels.text.No penalties found' | translate }}</span>
</div>
}
}
@if (showPaymentDetails) {
<mat-form-field>
<mat-label> {{ 'labels.inputs.Account' | translate }} #</mat-label>
@@ -13,3 +13,45 @@
.right-label {
padding-right: 25px !important;
}
.penalty-item-container {
width: 100%;
max-width: 100%;
display: grid;
grid-template-columns: auto minmax(80px, 110px) minmax(140px, 100%) minmax(130px, 170px) minmax(90px, 120px);
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 12px 16px;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 4px;
box-sizing: border-box;
overflow: hidden;
}
.penalty-label {
min-width: 80px;
}
.penalty-name {
min-width: 120px;
}
.penalty-date {
white-space: nowrap;
}
.penalty-amount {
text-align: right;
white-space: nowrap;
}
// Dark mode styles
:host-context(.dark-theme) {
.penalty-item-container {
background-color: #3a3a3a;
color: rgb(255 255 255 / 87%);
border-color: #4a4a4a;
}
}
@@ -8,8 +8,10 @@ import { LoansService } from 'app/loans/loans.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { Currency } from 'app/shared/models/general.model';
import { PenaltyManagementService } from 'app/loans/services/penalty-management.service';
import { InputAmountComponent } from '../../../../shared/input-amount/input-amount.component';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { MatCheckbox } from '@angular/material/checkbox';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -25,6 +27,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
InputAmountComponent,
MatSlideToggle,
MatCheckbox,
CdkTextareaAutosize,
FormatNumberPipe
]
@@ -36,6 +39,7 @@ export class MakeRepaymentComponent implements OnInit {
private router = inject(Router);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private penaltyManagementService = inject(PenaltyManagementService);
@Input() dataObject: any;
/** Loan Id */
@@ -44,6 +48,14 @@ export class MakeRepaymentComponent implements OnInit {
paymentTypes: any;
/** Show payment details */
showPaymentDetails = false;
/** Waive Penalties toggle */
waivePenalties = false;
/** Penalties list */
penalties: any[] = [];
/** Selected penalty IDs */
selectedPenalties: number[] = [];
/** Select all penalties checkbox */
selectAllPenalties = false;
/** Minimum Date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum Date allowed. */
@@ -79,6 +91,7 @@ export class MakeRepaymentComponent implements OnInit {
if (this.dataObject.currency) {
this.currency = this.dataObject.currency;
}
this.loadPenalties();
}
/**
@@ -97,22 +110,11 @@ export class MakeRepaymentComponent implements OnInit {
});
if (this.isCapitalizedIncome()) {
this.repaymentLoanForm.addControl(
'transactionAmount',
new UntypedFormControl('', [
Validators.required,
Validators.min(0.001),
Validators.max(this.dataObject.amount)
])
);
this.repaymentLoanForm.addControl('transactionAmount', new UntypedFormControl('', []));
this.updateTransactionAmountValidators(false);
} else {
this.repaymentLoanForm.addControl(
'transactionAmount',
new UntypedFormControl('', [
Validators.required,
Validators.min(0.001)
])
);
this.repaymentLoanForm.addControl('transactionAmount', new UntypedFormControl('', []));
this.updateTransactionAmountValidators(false);
}
if (this.isCapitalizedIncome() || this.isBuyDownFee()) {
this.repaymentLoanForm.addControl('classificationId', new UntypedFormControl(''));
@@ -169,6 +171,141 @@ export class MakeRepaymentComponent implements OnInit {
return code.includes('merchantissuedrefund') || code.includes('payoutrefund');
}
/**
* Load penalties for the loan
* Penalties are charges calculated for installments in the payment schedule.
* Each penalty charge has a dueDate that corresponds to an installment due date.
*/
loadPenalties() {
this.penaltyManagementService.loadPenalties(this.loanId).subscribe({
next: (penalties: any[]) => {
this.penalties = penalties;
},
error: (error: any) => {
console.error('Error loading penalties:', error);
this.penalties = [];
}
});
}
/**
* Toggle waive penalties
* Following the addPaymentDetails() pattern
*/
toggleWaivePenalties() {
this.waivePenalties = !this.waivePenalties;
if (!this.waivePenalties) {
// Reset selections when toggling off
this.selectedPenalties = [];
this.selectAllPenalties = false;
this.updateTransactionAmountValidators(false);
this.recalculateTransactionAmount();
} else {
this.recalculateTransactionAmount();
}
}
/**
* Toggle select all penalties
* Following the toggleSelects() pattern from loans-active-client-members
*/
toggleSelectAllPenalties() {
const result = this.penaltyManagementService.toggleSelectAllPenalties(this.selectAllPenalties, this.penalties);
this.selectAllPenalties = result.selectAllPenalties;
this.selectedPenalties = result.selectedPenalties;
this.recalculateTransactionAmount();
}
/**
* Toggle individual penalty selection
* Following the toggleSelect() pattern from loans-active-client-members
*/
togglePenaltySelection(penaltyId: number) {
const result = this.penaltyManagementService.togglePenaltySelection(
penaltyId,
this.selectedPenalties,
this.penalties
);
this.selectedPenalties = result.selectedPenalties;
this.selectAllPenalties = result.selectAllPenalties;
this.recalculateTransactionAmount();
}
/**
* Check if penalty is selected
*/
isPenaltySelected(penaltyId: number): boolean {
return this.penaltyManagementService.isPenaltySelected(penaltyId, this.selectedPenalties);
}
/**
* Get penalty display key or plain text for translation/output
* Normalizes common backend values (like MORA / labels.inputs.*) to translation keys
*/
getPenaltyDisplayKey(penalty: any): string {
return this.penaltyManagementService.getPenaltyDisplayKey(penalty);
}
/**
* Recalculate transaction amount when penalties are waived
*/
recalculateTransactionAmount() {
const currentAmountValue = this.repaymentLoanForm.value.transactionAmount;
const currentAmount =
currentAmountValue !== undefined && currentAmountValue !== null
? Number(currentAmountValue)
: Number(this.dataObject.amount ?? 0);
const baseAmount = isNaN(currentAmount) ? Number(this.dataObject.amount ?? 0) : currentAmount;
if (!this.waivePenalties || this.selectedPenalties.length === 0) {
// Reset to original amount if no penalties selected
this.repaymentLoanForm.patchValue(
{
transactionAmount: baseAmount
},
{ emitEvent: false }
);
return;
}
// Calculate total waived amount
let totalWaived = 0;
this.selectedPenalties.forEach((penaltyId: number) => {
const penalty = this.penalties.find((p: any) => p.id === penaltyId);
if (penalty) {
totalWaived += penalty.amountOutstanding || penalty.amount || 0;
}
});
// Calculate new transaction amount
const newAmount = Math.max(0, baseAmount - totalWaived);
// Allow zero when fully waived
this.updateTransactionAmountValidators(this.waivePenalties && newAmount === 0);
this.repaymentLoanForm.patchValue(
{
transactionAmount: newAmount
},
{ emitEvent: false }
);
}
/**
* Update transaction amount validators to allow or disallow zero
*/
private updateTransactionAmountValidators(allowZero: boolean) {
const validators = [
Validators.required,
Validators.min(allowZero ? 0 : 0.001)
];
if (this.isCapitalizedIncome()) {
validators.push(Validators.max(this.dataObject.amount));
}
this.repaymentLoanForm.controls.transactionAmount.setValidators(validators);
this.repaymentLoanForm.controls.transactionAmount.updateValueAndValidity({ emitEvent: false });
}
/** Submits the repayment form */
submit() {
const repaymentLoanFormData = this.repaymentLoanForm.value;
@@ -188,6 +325,12 @@ export class MakeRepaymentComponent implements OnInit {
data.interestRefundCalculation = false;
}
delete data.skipInterestRefund;
// Add waived penalty charge IDs if penalties are being waived
if (this.waivePenalties && this.selectedPenalties.length > 0) {
data.chargeIds = this.selectedPenalties;
}
this.loanService.submitLoanActionButton(this.loanId, data, this.command).subscribe((response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
+8
View File
@@ -528,6 +528,14 @@ export class LoansService {
return this.http.get(`/loans/${loanId}/guarantors/accounts/template`, { params: httpParams });
}
/**
* @param {string} loanId Loan Id
* @returns {Observable<any>} All charges for the loan
*/
getLoanCharges(loanId: string): Observable<any> {
return this.http.get(`/loans/${loanId}/charges`);
}
/**
* @param {string} accountId loans account Id
* @param {string} chargeId loans charge Id
@@ -0,0 +1,190 @@
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { LoansService } from '../loans.service';
import { Dates } from 'app/core/utils/dates';
/**
* Service for managing loan penalty charges
* Provides shared logic for loading, filtering, and managing penalties
*/
@Injectable({
providedIn: 'root'
})
export class PenaltyManagementService {
private loanService = inject(LoansService);
private dateUtils = inject(Dates);
/**
* Load penalties for the loan
* Penalties are charges calculated for installments in the payment schedule.
* Each penalty charge has a dueDate that corresponds to an installment due date.
* @param loanId The loan ID
* @returns Observable of filtered and sorted penalties
*/
loadPenalties(loanId: string): Observable<any[]> {
return this.loanService.getLoanCharges(loanId).pipe(
map((response: any) => {
// Handle different response structures - could be array directly or wrapped
let charges: any[] = [];
if (Array.isArray(response)) {
charges = response;
} else if (response && Array.isArray(response.pageItems)) {
charges = response.pageItems;
} else if (response && Array.isArray(response.content)) {
charges = response.content;
} else if (response && Array.isArray(response.charges)) {
charges = response.charges;
} else if (response && response.data && Array.isArray(response.data)) {
charges = response.data;
}
const filteredPenalties = this.filterPenaltyCharges(charges);
// Parse dates for proper display
// The dueDate on each penalty corresponds to the installment due date in the payment schedule
filteredPenalties.forEach((penalty: any) => {
if (penalty.dueDate) {
penalty.dueDate = this.dateUtils.parseDate(penalty.dueDate);
}
});
// Sort by due date to show penalties in chronological order
filteredPenalties.sort((a: any, b: any) => {
if (a.dueDate && b.dueDate) {
return a.dueDate.getTime() - b.dueDate.getTime();
}
return 0;
});
return filteredPenalties;
})
);
}
/**
* Filter penalty charges from all charges
* Returns only penalty charges that are:
* - Marked as penalties (penalty === true) or overdue charges
* - Not already waived or paid
* - Have outstanding amounts
* These penalties are calculated for specific installments in the payment schedule.
* @param charges Array of all charges
* @returns Array of filtered penalty charges
*/
filterPenaltyCharges(charges: any[]): any[] {
if (!charges || !Array.isArray(charges)) {
return [];
}
return charges.filter((charge: any) => {
if (!charge) {
return false;
}
// Check if it's a penalty charge
const isPenalty =
charge.penalty === true ||
charge.penalty === 'true' ||
(charge.chargeTimeType &&
(charge.chargeTimeType.value?.toLowerCase().includes('overdue') ||
charge.chargeTimeType.code?.toLowerCase().includes('overdue') ||
charge.chargeTimeType.id === 9)); // Overdue charge time type ID
if (!isPenalty) {
return false;
}
// Check if already waived or paid
if (charge.waived === true || charge.waived === 'true' || charge.paid === true || charge.paid === 'true') {
return false;
}
// Check if has outstanding amount
const outstanding =
charge.amountOutstanding || charge.amountOutstanding === 0 ? charge.amountOutstanding : charge.amount;
return outstanding > 0;
});
}
/**
* Toggle select all penalties
* @param selectAllPenalties Current state of select all checkbox
* @param penalties Array of all penalties
* @returns Object with updated selectAllPenalties state and selectedPenalties array
*/
toggleSelectAllPenalties(
selectAllPenalties: boolean,
penalties: any[]
): { selectAllPenalties: boolean; selectedPenalties: number[] } {
const newSelectAll = !selectAllPenalties;
return {
selectAllPenalties: newSelectAll,
selectedPenalties: newSelectAll ? penalties.map((penalty: any) => penalty.id) : []
};
}
/**
* Toggle individual penalty selection
* @param penaltyId The penalty ID to toggle
* @param selectedPenalties Current array of selected penalty IDs
* @param penalties Array of all penalties
* @returns Object with updated selectedPenalties array and selectAllPenalties state
*/
togglePenaltySelection(
penaltyId: number,
selectedPenalties: number[],
penalties: any[]
): { selectedPenalties: number[]; selectAllPenalties: boolean } {
const newSelectedPenalties = [...selectedPenalties];
const index = newSelectedPenalties.indexOf(penaltyId);
if (index > -1) {
newSelectedPenalties.splice(index, 1);
} else {
newSelectedPenalties.push(penaltyId);
}
// Update select all checkbox state
const len = penalties.length;
const selectAllPenalties = len === 0 ? false : newSelectedPenalties.length === len;
return {
selectedPenalties: newSelectedPenalties,
selectAllPenalties
};
}
/**
* Check if penalty is selected
* @param penaltyId The penalty ID to check
* @param selectedPenalties Array of selected penalty IDs
* @returns True if penalty is selected
*/
isPenaltySelected(penaltyId: number, selectedPenalties: number[]): boolean {
return selectedPenalties.includes(penaltyId);
}
/**
* Get penalty display key or plain text for translation/output
* Normalizes common backend values (like MORA / labels.inputs.*) to translation keys
* @param penalty The penalty object
* @returns Translation key or plain text
*/
getPenaltyDisplayKey(penalty: any): string {
if (!penalty) {
return 'labels.inputs.Overdue Fees';
}
const rawName = (penalty.name || penalty.chargeTimeType?.value || 'Overdue Fees').toString().trim();
const upperName = rawName.toUpperCase();
// Handle known aliases from backend
if (upperName === 'MORA' || upperName === 'OVERDUE') {
return 'labels.inputs.Overdue';
}
if (upperName === 'OVERDUE FEES') {
return 'labels.inputs.Overdue Fees';
}
// Pass through existing translation keys (labels.*)
if (rawName.startsWith('labels.')) {
return rawName;
}
// Fall back to the raw name (translate pipe will no-op if not a key)
return rawName;
}
}
+6 -1
View File
@@ -1106,6 +1106,7 @@
"Offices": "Kanceláře",
"Options": "Možnosti",
"Other Details": "Další detaily",
"Overdue": "Po splatnosti",
"Overdue Charges": "Poplatky po splatnosti",
"Password Preferences": "Předvolby hesla",
"Payment Type": "Způsob platby",
@@ -2358,6 +2359,7 @@
"Show Closed Centers": "Zobrazit uzavřená centra",
"Show Closed Groups": "Zobrazit uzavřené skupiny",
"Show Payment Details": "Zobrazit podrobnosti o platbě",
"Waive Penalties": "Zřeknutí se pokut",
"Slab": "Pásmo",
"Specific to Installment": "Specifické pro splátky",
"Staff": "Personál",
@@ -2583,7 +2585,9 @@
"Income from Buy down fees": "Příjmy z poplatků za odkup",
"Buy down fee Expense": "Náklady na poplatek za odkup",
"Skip Interest Refund Transaction Posting": "Přeskočit zaúčtování vrácení úroků",
"Available Disbursement Amount (with Over Applied)": "Dostupná částka k výplatě (s přeplatkem)"
"Available Disbursement Amount (with Over Applied)": "Dostupná částka k výplatě (s přeplatkem)",
"Overdue": "Po splatnosti",
"Overdue Fees": "Poplatky po splatnosti"
},
"links": {
"Community": "Společenství",
@@ -3167,6 +3171,7 @@
"No checker inbox data available for this search": "Pro toto vyhledávání nejsou k dispozici žádná data doručené pošty Checker.",
"No client was found": "Nebyl nalezen žádný klient",
"No data found": "Nenalezena žádná data",
"No penalties found": "Nebyly nalezeny žádné pokuty",
"NoDocuments": "Žádné dokumenty k dispozici.",
"NoFileSelected": "Nevybrán žádný soubor",
"NoNotesAvailable": "Žádné poznámky k dispozici",
+7 -4
View File
@@ -1107,6 +1107,7 @@
"Offices": "Büros",
"Options": "Optionen",
"Other Details": "Andere Details",
"Overdue": "Überfällig",
"Overdue Charges": "Überfällige Gebühren",
"Password Preferences": "Passworteinstellungen",
"Payment Type": "Zahlungsart",
@@ -2358,6 +2359,7 @@
"Show Closed Centers": "Geschlossene Zentren anzeigen",
"Show Closed Groups": "Geschlossene Gruppen anzeigen",
"Show Payment Details": "Zahlungsdetails anzeigen",
"Waive Penalties": "Auf Strafen verzichten",
"Slab": "Stufe",
"Specific to Installment": "Speziell für Ratenzahlung",
"Staff": "Personal",
@@ -2583,7 +2585,9 @@
"Income from Buy down fees": "Einnahmen aus Buy-Down-Gebühren",
"Buy down fee Expense": "Aufwendungen aus Buy-Down-Gebühren",
"Skip Interest Refund Transaction Posting": "Buchung der Zinsrückerstattung überspringen",
"Available Disbursement Amount (with Over Applied)": "Verfügbarer Auszahlungsbetrag (mit Überzahlung)"
"Available Disbursement Amount (with Over Applied)": "Verfügbarer Auszahlungsbetrag (mit Überzahlung)",
"Overdue": "Überfällig",
"Overdue Fees": "Überfällige Gebühren"
},
"links": {
"Community": "Gemeinschaft",
@@ -2799,8 +2803,7 @@
"All Recurring Deposits": "Alle wiederkehrenden Einzahlungen",
"All Savings": "Alle Einsparungen",
"Savings Account Details": "Sparkonto-Details",
"Create Savings Product": "Sparkonto anlegen",
"Create Savings Product": "Sparkonto anlegen",
"Create Savings Product": "Sparprodukt erstellen",
"Allocate Cash": "Bargeld zuweisen",
"Allows you to create new fixed deposit product": "Mit dieser Option können Sie ein neues Festgeldprodukt erstellen.",
"This option allows you to create new users in your organization": "Mit dieser Option können Sie neue Benutzer in Ihrer Organisation erstellen",
@@ -2907,7 +2910,6 @@
"Create Recurring Deposits Account": "Erstellen Sie ein Konto für wiederkehrende Einlagen",
"Create Report": "Bericht erstellen",
"Create SMS Campaign": "Erstellen Sie eine SMS-Kampagne",
"Create Savings Product": "Sparprodukt erstellen",
"Create Savings Account": "Sparkonto erstellen",
"Create Self Service User": "Erstellen Sie einen Self-Service-Benutzer",
"Create Share Product": "Share-Produkt erstellen",
@@ -3168,6 +3170,7 @@
"No checker inbox data available for this search": "Für diese Suche sind keine Checker-Posteingangsdaten verfügbar.",
"No client was found": "Es wurde kein Kunde gefunden",
"No data found": "Keine Daten gefunden",
"No penalties found": "Keine Strafen gefunden",
"NoDocuments": "Keine Dokumente verfügbar.",
"NoFileSelected": "Keine Datei ausgewählt",
"NoNotesAvailable": "Keine Notizen verfügbar",
+6 -3
View File
@@ -1112,6 +1112,7 @@
"Offices": "Offices",
"Options": "Options",
"Other Details": "Other Details",
"Overdue": "Overdue",
"Overdue Charges": "Overdue Charges",
"Password Preferences": "Password Preferences",
"Payment Type": "Payment Type",
@@ -2591,7 +2592,10 @@
"Income from Buy down fees": "Income from Buy down fees",
"Buy down fee Expense": "Buy down fee Expense",
"Skip Interest Refund Transaction Posting": "Skip Interest Refund Transaction Posting",
"Available Disbursement Amount (with Over Applied)": "Available Disbursement Amount (with Over Applied)"
"Available Disbursement Amount (with Over Applied)": "Available Disbursement Amount (with Over Applied)",
"Waive Penalties": "Waive Penalties",
"Overdue": "Overdue",
"Overdue Fees": "Overdue Fees"
},
"links": {
"Community": "Community",
@@ -3532,8 +3536,7 @@
"“Maker-Checker” principle requires every tasks": "The “Maker-Checker” principle requires every tasks to be completed by two people to reduce the chance of errors and misuse. One person initiates the process and the second completes it.",
"Buy Down Fees": "Buy Down Fees",
"Undo Write-off Description": "This action will reverse the write-off transaction and restore the loan to its previous active status. Please provide a note explaining the reason for this action.",
"UploadDocumentHint": "Upload a PDF or image to generate a preview.",
"Create Savings Product": "Create Savings Product"
"UploadDocumentHint": "Upload a PDF or image to generate a preview."
},
"auditTrail": {
"actions": {
+3
View File
@@ -1107,6 +1107,7 @@
"Offices": "Oficinas",
"Options": "Opciones",
"Other Details": "Otros detalles",
"Overdue": "Vencido",
"Overdue Charges": "Comisiones Moratorias",
"Password Preferences": "Preferencias de contraseña",
"Payment Type": "Tipo de pago",
@@ -2359,6 +2360,7 @@
"Show Closed Centers": "Mostrar Centros Cerrados",
"Show Closed Groups": "Mostrar grupos cerrados",
"Show Payment Details": "Mostrar detalles de pago",
"Waive Penalties": "Renunciar a Penalizaciones",
"Slab": "Tramo",
"Specific to Installment": "Específico para la cuota",
"Staff": "Asesor",
@@ -3170,6 +3172,7 @@
"No checker inbox data available for this search": "No hay datos de la bandeja de entrada del verificador disponibles para esta búsqueda.",
"No client was found": "No se encontró ningún cliente",
"No data found": "Datos no encontrados",
"No penalties found": "No se encontraron penalizaciones",
"NoDocuments": "No hay documentos disponibles.",
"NoFileSelected": "No se ha seleccionado ningún archivo",
"NoNotesAvailable": "No hay notas disponibles",
+6 -2
View File
@@ -1106,6 +1106,7 @@
"Offices": "Oficinas",
"Options": "Opciones",
"Other Details": "Otros detalles",
"Overdue": "Vencido",
"Overdue Charges": "Comisiones Moratorias",
"Password Preferences": "Preferencias de contraseña",
"Payment Type": "Tipo de pago",
@@ -2359,6 +2360,7 @@
"Show Closed Centers": "Mostrar Centros Cerrados",
"Show Closed Groups": "Mostrar grupos cerrados",
"Show Payment Details": "Mostrar detalles de pago",
"Waive Penalties": "Renunciar a Penalizaciones",
"Specific to Installment": "Específico para la cuota",
"Slab": "Tramo",
"Staff": "Asesor",
@@ -2585,7 +2587,9 @@
"Buy down fee Expense": "Gastos por comisiones de compra inicial",
"Skip Interest Refund Transaction Posting": "Omitir el registro de la devolución de intereses",
"Incorporation Date": "Fecha de Constitución",
"Available Disbursement Amount (with Over Applied)": "Monto disponible para desembolso (con sobreaplicación)"
"Available Disbursement Amount (with Over Applied)": "Monto disponible para desembolso (con sobreaplicación)",
"Overdue": "Vencido",
"Overdue Fees": "Cargos vencidos"
},
"links": {
"Community": "Comunidad",
@@ -2910,7 +2914,6 @@
"Create Recurring Deposits Account": "Crear cuenta de depósitos recurrentes",
"Create Report": "Crear reporte",
"Create SMS Campaign": "Crear campaña de SMS",
"Create Savings Product": "Crear producto de ahorro",
"Create Savings Account": "Crear Cuenta de Ahorros",
"Create Self Service User": "Crear usuario de autoservicio",
"Create Share Product": "Crear producto de Acciones",
@@ -3171,6 +3174,7 @@
"No checker inbox data available for this search": "No hay datos de la bandeja de entrada del verificador disponibles para esta búsqueda.",
"No client was found": "No se encontró ningún cliente",
"No data found": "Datos no encontrados",
"No penalties found": "No se encontraron penalizaciones",
"NoDocuments": "No hay documentos disponibles.",
"NoFileSelected": "No se ha seleccionado ningún archivo",
"NoNotesAvailable": "No hay notas disponibles",
+3
View File
@@ -1107,6 +1107,7 @@
"Offices": "Des bureaux",
"Options": "Possibilités",
"Other Details": "Autres détails",
"Overdue": "En retard",
"Overdue Charges": "Frais de retard",
"Password Preferences": "Préférences de mot de passe",
"Payment Type": "Type de paiement",
@@ -2358,6 +2359,7 @@
"Show Closed Centers": "Afficher les centres fermés",
"Show Closed Groups": "Afficher les groupes fermés",
"Show Payment Details": "Afficher les détails du paiement",
"Waive Penalties": "Renoncer aux pénalités",
"Slab": "Tranche",
"Specific to Installment": "Spécifique au versement",
"Staff": "Personnel",
@@ -3168,6 +3170,7 @@
"No checker inbox data available for this search": "Aucune donnée de boîte de réception du vérificateur disponible pour cette recherche.",
"No client was found": "Aucun client n'a été trouvé",
"No data found": "Aucune donnée disponible",
"No penalties found": "Aucune pénalité trouvée",
"NoDocuments": "Aucun document disponible.",
"NoFileSelected": "Aucun fichier sélectionné",
"NoNotesAvailable": "Aucune note disponible",
+3
View File
@@ -1107,6 +1107,7 @@
"Offices": "Uffici",
"Options": "Opzioni",
"Other Details": "Altri dettagli",
"Overdue": "Scaduto",
"Overdue Charges": "Addebiti scaduti",
"Password Preferences": "Preferenze password",
"Payment Type": "Modalità di pagamento",
@@ -2358,6 +2359,7 @@
"Show Closed Centers": "Mostra centri chiusi",
"Show Closed Groups": "Mostra gruppi chiusi",
"Show Payment Details": "Mostra i dettagli del pagamento",
"Waive Penalties": "Rinuncia alle penalità",
"Slab": "Fascia",
"Specific to Installment": "Specifico per la rata",
"Staff": "Personale",
@@ -3169,6 +3171,7 @@
"No checker inbox data available for this search": "Nessun dato della casella di posta disponibile per questa ricerca.",
"No client was found": "Nessun cliente trovato",
"No data found": "Nessun dato trovato",
"No penalties found": "Nessuna penalità trovata",
"NoDocuments": "Nessun documento disponibile.",
"NoFileSelected": "Nessun file selezionato",
"NoNotesAvailable": "Nessuna nota disponibile",
+3
View File
@@ -1107,6 +1107,7 @@
"Offices": "부엌",
"Options": "옵션",
"Other Details": "그 외 세부 사항",
"Overdue": "연체",
"Overdue Charges": "연체료",
"Password Preferences": "비밀번호 기본 설정",
"Payment Type": "결제 유형",
@@ -2359,6 +2360,7 @@
"Show Closed Centers": "닫힌 센터 표시",
"Show Closed Groups": "닫힌 그룹 표시",
"Show Payment Details": "결제 세부정보 표시",
"Waive Penalties": "패널티 면제",
"Slab": "구간",
"Specific to Installment": "할부에만 해당",
"Staff": "직원",
@@ -3169,6 +3171,7 @@
"No checker inbox data available for this search": "이 검색에 사용할 수 있는 검사기 받은 편지함 데이터가 없습니다.",
"No client was found": "클라이언트를 찾을 수 없습니다.",
"No data found": "데이터가 없습니다",
"No penalties found": "패널티가 없습니다",
"NoDocuments": "사용 가능한 문서가 없습니다.",
"NoFileSelected": "선택된 파일 없음",
"NoNotesAvailable": "사용 가능한 메모 없음",
+7 -3
View File
@@ -1107,6 +1107,7 @@
"Offices": "Biurai",
"Options": "Galimybės",
"Other Details": "Kitos detalės",
"Overdue": "Pradelstas",
"Overdue Charges": "Pradelsti mokesčiai",
"Password Preferences": "Slaptažodžio nuostatos",
"Payment Type": "Mokėjimo tipas",
@@ -2583,7 +2584,10 @@
"Income from Buy down fees": "Pajamos iš supirkimo mokesčių",
"Buy down fee Expense": "Supirkimo mokesčio išlaidos",
"Skip Interest Refund Transaction Posting": "Praleisti palūkanų grąžinimo įrašą",
"Available Disbursement Amount (with Over Applied)": "Galimas išmokėjimo kiekis (su pertekliumi)"
"Available Disbursement Amount (with Over Applied)": "Galimas išmokėjimo kiekis (su pertekliumi)",
"Waive Penalties": "Atsisakyti delspinigių",
"Overdue": "Pradelstas",
"Overdue Fees": "Delspinigiai"
},
"links": {
"Community": "bendruomenė",
@@ -2800,7 +2804,7 @@
"All Recurring Deposits": "Visi periodiniai indėliai",
"All Savings": "Visos santaupos",
"Savings Account Details": "Taupomosios sąskaitos informacija",
"Create Savings Product": "Sukurti taupymo produktą",
"Create Savings Product": "Sukurkite taupymo produktą",
"Allocate Cash": "Paskirstykite grynuosius pinigus",
"Allows you to create new fixed deposit product": "Ši parinktis leidžia sukurti naują terminuoto indėlio produktą.",
"This option allows you to create new users in your organization": "Ši parinktis leidžia sukurti naujų naudotojų jūsų organizacijoje",
@@ -2908,7 +2912,6 @@
"Create Recurring Deposits Account": "Sukurkite pasikartojančių indėlių sąskaitą",
"Create Report": "Sukurti ataskaitą",
"Create SMS Campaign": "Sukurkite SMS kampaniją",
"Create Savings Product": "Sukurkite taupymo produktą",
"Create Savings Account": "Sukurti taupomąją sąskaitą",
"Create Self Service User": "Sukurkite savitarnos vartotoją",
"Create Share Product": "Sukurkite bendrinimo produktą",
@@ -3169,6 +3172,7 @@
"No checker inbox data available for this search": "Nėra šios paieškos tikrinimo gautųjų duomenų.",
"No client was found": "Klientas nerastas",
"No data found": "Duomenų nerasta",
"No penalties found": "Delspinigiai nerasti",
"NoDocuments": "Nėra dokumentų.",
"NoFileSelected": "Failas nepasirinktas",
"NoNotesAvailable": "Nėra prieinamų pastabų",
+3
View File
@@ -1107,6 +1107,7 @@
"Offices": "Biroji",
"Options": "Iespējas",
"Other Details": "Citas detaļas",
"Overdue": "Nokavējums",
"Overdue Charges": "Nokavētas maksas",
"Password Preferences": "Paroles preferences",
"Payment Type": "Maksājuma veids",
@@ -2358,6 +2359,7 @@
"Show Closed Centers": "Rādīt slēgtos centrus",
"Show Closed Groups": "Rādīt slēgtās grupas",
"Show Payment Details": "Rādīt maksājuma informāciju",
"Waive Penalties": "Atteikties no sodiem",
"Slab": "Slānis",
"Specific to Installment": "Īpaši uz nomaksu",
"Staff": "Personāls",
@@ -3169,6 +3171,7 @@
"No checker inbox data available for this search": "Šai meklēšanai nav pieejami pārbaudītāja iesūtnes dati.",
"No client was found": "Klients netika atrasts",
"No data found": "Dati nav atrasti",
"No penalties found": "Sodi nav atrasti",
"NoDocuments": "Nav pieejamu dokumentu.",
"NoFileSelected": "Nav izvēlēts fails",
"NoNotesAvailable": "Nav pieejamas piezīmes",
+3
View File
@@ -1105,6 +1105,7 @@
"Offices": "कार्यालयहरू",
"Options": "विकल्पहरू",
"Other Details": "अन्य विवरणहरू",
"Overdue": "अतिदेय",
"Overdue Charges": "ओभरड्यू चार्जहरू",
"Password Preferences": "पासवर्ड प्राथमिकताहरू",
"Payment Type": "भुक्तानी प्रकार",
@@ -2356,6 +2357,7 @@
"Show Closed Centers": "बन्द केन्द्रहरू देखाउनुहोस्",
"Show Closed Groups": "बन्द समूहहरू देखाउनुहोस्",
"Show Payment Details": "भुक्तानी विवरणहरू देखाउनुहोस्",
"Waive Penalties": "दण्ड छुट",
"Specific to Installment": "किस्ताको लागि विशिष्ट",
"Slab": "स्तर",
"Staff": "कर्मचारी",
@@ -3166,6 +3168,7 @@
"No checker inbox data available for this search": "यस खोजको लागि कुनै परीक्षक इनबक्स डाटा उपलब्ध छैन।",
"No client was found": "कुनै ग्राहक भेटिएन",
"No data found": "कुनै डाटा फेला परेन",
"No penalties found": "कुनै दण्ड फेला परेन",
"NoDocuments": "कुनै कागजात उपलब्ध छैन।",
"NoFileSelected": "कुनै फाइल छानिएको छैन",
"NoNotesAvailable": "कुनै नोटहरू उपलब्ध छैनन्",
+3
View File
@@ -1107,6 +1107,7 @@
"Offices": "Escritórios",
"Options": "Opções",
"Other Details": "Outros detalhes",
"Overdue": "Vencido",
"Overdue Charges": "Cobranças vencidas",
"Password Preferences": "Preferências de senha",
"Payment Type": "Tipo de pagamento",
@@ -2358,6 +2359,7 @@
"Show Closed Centers": "Mostrar Centros Fechados",
"Show Closed Groups": "Mostrar grupos fechados",
"Show Payment Details": "Mostrar detalhes de pagamento",
"Waive Penalties": "Isenção de penalidades",
"Slab": "Faixa",
"Specific to Installment": "Específico para parcelamento",
"Staff": "Funcionários",
@@ -3169,6 +3171,7 @@
"No checker inbox data available for this search": "Não há dados da caixa de entrada do verificador disponíveis para esta pesquisa.",
"No client was found": "Nenhum cliente foi encontrado",
"No data found": "Nenhum dado encontrado",
"No penalties found": "Nenhuma penalidade encontrada",
"NoDocuments": "Nenhum documento disponível.",
"NoFileSelected": "Nenhum arquivo selecionado",
"NoNotesAvailable": "Nenhuma nota disponível",
+3
View File
@@ -1106,6 +1106,7 @@
"Offices": "Ofisi",
"Options": "Chaguo",
"Other Details": "Maelezo Mengine",
"Overdue": "Zilizochelewa",
"Overdue Charges": "Gharama Zilizopitwa na Wakati",
"Password Preferences": "Mapendeleo ya Nenosiri",
"Payment Type": "Aina ya malipo",
@@ -2356,6 +2357,7 @@
"Show Closed Centers": "Onyesha Vituo Vilivyofungwa",
"Show Closed Groups": "Onyesha Vikundi Vilivyofungwa",
"Show Payment Details": "Onyesha Maelezo ya Malipo",
"Waive Penalties": "Acha Adhabu",
"Slab": "Kiwango",
"Specific to Installment": "Maalum kwa Installment",
"Staff": "Wafanyakazi",
@@ -3166,6 +3168,7 @@
"No checker inbox data available for this search": "Hakuna data ya kikasha kikagua inayopatikana kwa utafutaji huu.",
"No client was found": "Hakuna mteja aliyepatikana",
"No data found": "Hakuna data iliyopatikana",
"No penalties found": "Hakuna adhabu zilizopatikana",
"NoDocuments": "Hakuna hati zinazopatikana.",
"NoFileSelected": "Hakuna faili iliyochaguliwa",
"NoNotesAvailable": "Hakuna noti zinazopatikana",