From 62c175ff15b22b8a63c07d5de67ac557766793c3 Mon Sep 17 00:00:00 2001 From: Shubham Kumar Date: Mon, 29 Dec 2025 03:39:08 +0530 Subject: [PATCH] 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 --- .../loan-reschedule.component.html | 36 ++++ .../loan-reschedule.component.scss | 42 ++++ .../loan-reschedule.component.ts | 96 ++++++++- .../make-repayment.component.html | 50 ++++- .../make-repayment.component.scss | 42 ++++ .../make-repayment.component.ts | 173 ++++++++++++++-- src/app/loans/loans.service.ts | 8 + .../services/penalty-management.service.ts | 190 ++++++++++++++++++ src/assets/translations/cs-CS.json | 7 +- src/assets/translations/de-DE.json | 11 +- src/assets/translations/en-US.json | 9 +- src/assets/translations/es-CL.json | 3 + src/assets/translations/es-MX.json | 8 +- src/assets/translations/fr-FR.json | 3 + src/assets/translations/it-IT.json | 3 + src/assets/translations/ko-KO.json | 3 + src/assets/translations/lt-LT.json | 10 +- src/assets/translations/lv-LV.json | 3 + src/assets/translations/ne-NE.json | 3 + src/assets/translations/pt-PT.json | 3 + src/assets/translations/sw-SW.json | 3 + 21 files changed, 668 insertions(+), 38 deletions(-) create mode 100644 src/app/loans/services/penalty-management.service.ts diff --git a/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.html b/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.html index 5f2a5b26f..2db07c96d 100644 --- a/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.html +++ b/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.html @@ -116,6 +116,42 @@ } + + + {{ 'labels.inputs.Waive Penalties' | translate }} + + + @if (waivePenalties.value) { + @if (penalties.length > 0) { +
+
+ + {{ 'labels.inputs.All' | translate }} + +
+
+ @for (penalty of penalties; track penalty.id) { +
+ + {{ 'labels.inputs.Penalty' | translate }} + {{ getPenaltyDisplayKey(penalty) | translate }} + {{ penalty.dueDate | dateFormat }} + {{ + penalty.amountOutstanding || penalty.amount || 0 | formatNumber + }} +
+ } +
+
+ } @else { +
+ {{ 'labels.text.No penalties found' | translate }} +
+ } + } diff --git a/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.scss b/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.scss index d774f68da..3ea6fac85 100644 --- a/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.scss +++ b/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.scss @@ -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; + } +} diff --git a/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.ts b/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.ts index 622f2de8b..b7e2c443a 100644 --- a/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.ts +++ b/src/app/loans/loans-view/loan-account-actions/loan-reschedule/loan-reschedule.component.ts @@ -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); + } } diff --git a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.html b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.html index 7db7238b4..02dd0980e 100644 --- a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.html +++ b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.html @@ -89,16 +89,50 @@ -
- - -
- {{ 'labels.inputs.Show Payment Details' | translate }} -
-
-
+
+ + {{ 'labels.inputs.Show Payment Details' | translate }} +
+
+ + {{ 'labels.inputs.Waive Penalties' | translate }} + +
+ + @if (waivePenalties) { + @if (penalties.length > 0) { +
+
+ + {{ 'labels.inputs.All' | translate }} + +
+
+ @for (penalty of penalties; track penalty.id) { +
+ + {{ 'labels.inputs.Penalty' | translate }} + {{ getPenaltyDisplayKey(penalty) | translate }} + {{ penalty.dueDate | dateFormat }} + {{ + penalty.amountOutstanding || penalty.amount || 0 | formatNumber + }} +
+ } +
+
+ } @else { +
+ {{ 'labels.text.No penalties found' | translate }} +
+ } + } + @if (showPaymentDetails) { {{ 'labels.inputs.Account' | translate }} # diff --git a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.scss b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.scss index 1fcb1ebc0..65e6ce3a2 100644 --- a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.scss +++ b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.scss @@ -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; + } +} diff --git a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.ts b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.ts index 035ddfd12..a2d7364ab 100644 --- a/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.ts +++ b/src/app/loans/loans-view/loan-account-actions/make-repayment/make-repayment.component.ts @@ -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 }); }); diff --git a/src/app/loans/loans.service.ts b/src/app/loans/loans.service.ts index 94bd6b3cc..b1d490eb6 100644 --- a/src/app/loans/loans.service.ts +++ b/src/app/loans/loans.service.ts @@ -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} All charges for the loan + */ + getLoanCharges(loanId: string): Observable { + return this.http.get(`/loans/${loanId}/charges`); + } + /** * @param {string} accountId loans account Id * @param {string} chargeId loans charge Id diff --git a/src/app/loans/services/penalty-management.service.ts b/src/app/loans/services/penalty-management.service.ts new file mode 100644 index 000000000..a0339935d --- /dev/null +++ b/src/app/loans/services/penalty-management.service.ts @@ -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 { + 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; + } +} diff --git a/src/assets/translations/cs-CS.json b/src/assets/translations/cs-CS.json index 9db58c58e..eae0af8e8 100644 --- a/src/assets/translations/cs-CS.json +++ b/src/assets/translations/cs-CS.json @@ -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", diff --git a/src/assets/translations/de-DE.json b/src/assets/translations/de-DE.json index ab4ea9757..6557a7b78 100644 --- a/src/assets/translations/de-DE.json +++ b/src/assets/translations/de-DE.json @@ -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", diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index eb1564614..c780fef60 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -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": { diff --git a/src/assets/translations/es-CL.json b/src/assets/translations/es-CL.json index abff160ef..aafb38333 100644 --- a/src/assets/translations/es-CL.json +++ b/src/assets/translations/es-CL.json @@ -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", diff --git a/src/assets/translations/es-MX.json b/src/assets/translations/es-MX.json index 3cec26be5..6605ec3ca 100644 --- a/src/assets/translations/es-MX.json +++ b/src/assets/translations/es-MX.json @@ -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", diff --git a/src/assets/translations/fr-FR.json b/src/assets/translations/fr-FR.json index dc6fb00fc..5a4398945 100644 --- a/src/assets/translations/fr-FR.json +++ b/src/assets/translations/fr-FR.json @@ -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", diff --git a/src/assets/translations/it-IT.json b/src/assets/translations/it-IT.json index bc4ff06fe..210326953 100644 --- a/src/assets/translations/it-IT.json +++ b/src/assets/translations/it-IT.json @@ -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", diff --git a/src/assets/translations/ko-KO.json b/src/assets/translations/ko-KO.json index b3de37211..1d264c143 100644 --- a/src/assets/translations/ko-KO.json +++ b/src/assets/translations/ko-KO.json @@ -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": "사용 가능한 메모 없음", diff --git a/src/assets/translations/lt-LT.json b/src/assets/translations/lt-LT.json index 9ed449d59..cf04cce55 100644 --- a/src/assets/translations/lt-LT.json +++ b/src/assets/translations/lt-LT.json @@ -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ų", diff --git a/src/assets/translations/lv-LV.json b/src/assets/translations/lv-LV.json index 3f6075d74..ad929f3a9 100644 --- a/src/assets/translations/lv-LV.json +++ b/src/assets/translations/lv-LV.json @@ -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", diff --git a/src/assets/translations/ne-NE.json b/src/assets/translations/ne-NE.json index 653d6fddc..7f89de209 100644 --- a/src/assets/translations/ne-NE.json +++ b/src/assets/translations/ne-NE.json @@ -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": "कुनै नोटहरू उपलब्ध छैनन्", diff --git a/src/assets/translations/pt-PT.json b/src/assets/translations/pt-PT.json index ea4240cf1..c9800d9c7 100644 --- a/src/assets/translations/pt-PT.json +++ b/src/assets/translations/pt-PT.json @@ -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", diff --git a/src/assets/translations/sw-SW.json b/src/assets/translations/sw-SW.json index 99fed7f59..713da39c8 100644 --- a/src/assets/translations/sw-SW.json +++ b/src/assets/translations/sw-SW.json @@ -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",