Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
@if (createStandingInstructionsForm.controls.name.hasError('required')) {
<mat-error>
{{ 'labels.inputs.name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.inputs.required' | translate }}</strong>
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
Expand All @@ -39,7 +39,7 @@
@if (createStandingInstructionsForm.controls.transferType.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Transfer Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.inputs.required' | translate }}</strong>
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { DELINQUENCY_BUCKET_TYPE, DelinquencyBucketType } from './models/delinquency-models';

export abstract class DelinquencyBucketBaseComponent {
protected route = inject(ActivatedRoute);

delinquencyBucketType = new BehaviorSubject<DelinquencyBucketType>(DELINQUENCY_BUCKET_TYPE.REGULAR);

constructor() {
this.initialize(this.route.snapshot.queryParamMap.get('bucketType') || 'regular');
}

initialize(productType: string): void {
if (productType === 'regular') {
this.delinquencyBucketType.next(DELINQUENCY_BUCKET_TYPE.REGULAR);
} else if (productType === 'workingcapital') {
this.delinquencyBucketType.next(DELINQUENCY_BUCKET_TYPE.WORKING_CAPITAL);
}
}

get isWorkingCapitalBucket(): boolean {
return DELINQUENCY_BUCKET_TYPE.WORKING_CAPITAL === this.delinquencyBucketType.value;
}

get isRegularBucket(): boolean {
return DELINQUENCY_BUCKET_TYPE.REGULAR === this.delinquencyBucketType.value;
}

bucketTypeLabel(bucketType: number): string {
if (bucketType === 1) {
return 'Regular';
}
if (bucketType === 2) {
return 'Working Capital';
}
Comment on lines +39 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Translate these bucket labels instead of hardcoding English.

Regular and Working Capital are rendered text, so they bypass the locale catalog and will not be picked up by translation extraction in this form.

As per coding guidelines: Use proper i18n variables from @ngx-translate/core for all user-facing strings instead of hardcoded text.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/products/manage-delinquency-buckets/delinquency-base.component.ts`
around lines 39 - 45, bucketTypeLabel currently returns hardcoded English
strings; update it to use ngx-translate so labels are extracted and localized:
inject TranslateService into the component and replace the literal returns in
bucketTypeLabel(bucketType: number) with calls to TranslateService (e.g.
this.translate.instant('products.bucketType.regular') and
this.translate.instant('products.bucketType.workingCapital')), adding those
translation keys to the locale files; keep the same numeric checks (bucketType
=== 1 / === 2) and default behavior unchanged.

return '';
}

bucketType(bucketType: number): string {
if (bucketType === 1) {
return 'regular';
}
if (bucketType === 2) {
return 'workingcapital';
}
return '';
}
Comment on lines +39 to +57
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use DelinquencyBucketType strings in these helpers.

src/app/products/manage-delinquency-buckets/models/delinquency-models.ts (Lines 9-14) defines bucket types as 'regular' | 'workingcapital', and src/app/products/manage-delinquency-buckets/delinquency-bucket/delinquency-bucket.component.html (Line 54) passes row.bucketType straight into this code. With the current 1/2 checks, working-capital rows resolve to '', so edit/view falls back to the regular flow and then hits the regular-only ranges path in src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.ts (Lines 101-112) / src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.ts (Lines 37-46).

🛠️ Minimal fix
-  bucketTypeLabel(bucketType: number): string {
-    if (bucketType === 1) {
+  bucketTypeLabel(bucketType: DelinquencyBucketType): string {
+    if (bucketType === DELINQUENCY_BUCKET_TYPE.REGULAR) {
       return 'Regular';
     }
-    if (bucketType === 2) {
+    if (bucketType === DELINQUENCY_BUCKET_TYPE.WORKING_CAPITAL) {
       return 'Working Capital';
     }
     return '';
   }
 
-  bucketType(bucketType: number): string {
-    if (bucketType === 1) {
-      return 'regular';
-    }
-    if (bucketType === 2) {
-      return 'workingcapital';
-    }
-    return '';
+  bucketType(bucketType: DelinquencyBucketType): DelinquencyBucketType {
+    return bucketType;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/products/manage-delinquency-buckets/delinquency-base.component.ts`
around lines 39 - 57, The helpers bucketTypeLabel and bucketType currently check
for numeric values (1/2) but the model uses string bucket types ('regular' |
'workingcapital') and row.bucketType passes those strings; update these
functions to accept and handle the string values by returning 'Regular' for
'regular' and 'Working Capital' for 'workingcapital' (and returning
'regular'/'workingcapital' respectively in bucketType), and keep optional
backward-compatibility by also handling numeric 1/2 if needed so working-capital
rows no longer resolve to ''. Use the existing function names bucketTypeLabel
and bucketType to locate and modify the logic accordingly.


getCatalogLabel(inputText: string): string {
const datas = inputText.split('.');
return this.camalize(datas[1]);
}

camalize(word: string) {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
Comment on lines +59 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard getCatalogLabel() before camelizing.

This helper unconditionally reads the second token from split('.'). Any plain label or unexpected enum code from the new WC option data will call camalize(undefined) and break render-time translation.

🛡️ Minimal fix
  getCatalogLabel(inputText: string): string {
-    const datas = inputText.split('.');
-    return this.camalize(datas[1]);
+    const [, catalogValue] = inputText.split('.');
+    return this.camalize(catalogValue ?? inputText);
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getCatalogLabel(inputText: string): string {
const datas = inputText.split('.');
return this.camalize(datas[1]);
}
camalize(word: string) {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
getCatalogLabel(inputText: string): string {
const [, catalogValue] = inputText.split('.');
return this.camalize(catalogValue ?? inputText);
}
camalize(word: string) {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/products/manage-delinquency-buckets/delinquency-base.component.ts`
around lines 59 - 65, getCatalogLabel currently assumes inputText.split('.') has
a second token and calls camalize(datas[1]) which can be undefined; update
getCatalogLabel to guard the split result (check that datas.length > 1 and
datas[1] is truthy) before calling camalize, and return a safe fallback (e.g.,
the original inputText or an empty string) when the second token is missing or
falsy; keep using the existing camalize(word) helper for valid tokens so
rendering won't break on unexpected enum/label formats.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,76 @@
</mat-error>
}
</mat-form-field>
@if (isWorkingCapitalBucket) {
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Delinquency Payment Rule' | translate }}</h3>
<mat-form-field class="flex-100">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input matInput type="number" required formControlName="frequency" min="1" step="1" />
@if (bucketForm.controls.frequency.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Frequency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (bucketForm.controls.frequency.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Frequency' | translate }} {{ 'labels.commons.must be' | translate }}
{{ 'labels.commons.a positive number' | translate }}
</mat-error>
}
</mat-form-field>

<mat-form-field class="flex-100">
<mat-label>{{ 'labels.inputs.Frequency Type' | translate }}</mat-label>
<mat-select required formControlName="frequencyType">
@for (frequencyType of frequencyTypeOptions; track frequencyType) {
<mat-option [value]="frequencyType.id">
{{ getCatalogLabel(frequencyType.name) | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
@if (bucketForm.controls.frequencyType.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Frequency Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>

<mat-form-field class="flex-100">
<mat-label>{{ 'labels.inputs.Minimum Payment' | translate }}</mat-label>
<input matInput type="number" required formControlName="minimumPayment" min="0.01" />
@if (bucketForm.controls.minimumPayment.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Minimum Payment' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (bucketForm.controls.minimumPayment.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Minimum Payment' | translate }} {{ 'labels.commons.must be' | translate }}
{{ 'labels.commons.a positive number' | translate }}
</mat-error>
}
Comment on lines +69 to +74
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

minimumPayment is checking the wrong validation error key.

The control uses Validators.min(0.01), but the template checks hasError('pattern'), so invalid min values won’t show the intended validation message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.html`
around lines 118 - 120, The template is checking the wrong validation key:
update the mat-error condition for bucketForm.controls.minimumPayment from
hasError('pattern') to hasError('min') so the Validators.min(0.01) failure shows
the intended message; locate the minimumPayment control usage in the
create-bucket.component.html and replace the error key accordingly (keep the
same error text).

</mat-form-field>

<mat-form-field class="flex-100">
<mat-label>{{ 'labels.inputs.Minimum Payment Type' | translate }}</mat-label>
<mat-select required formControlName="minimumPaymentType">
@for (minimumPaymentType of minimumPaymentOptions; track minimumPaymentType) {
<mat-option [value]="minimumPaymentType.id">
{{ getCatalogLabel(minimumPaymentType.name) | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
@if (bucketForm.controls.minimumPaymentType.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Minimum Payment Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}

<h3 class="mat-h3 flex-40">{{ 'labels.heading.Delinquency Ranges' | translate }}</h3>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
*/

import { Component, OnInit, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { ProductsService } from 'app/products/products.service';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
import { MatButton, MatIconButton } from '@angular/material/button';
import { MatIconButton } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import {
MatTable,
Expand All @@ -33,6 +33,8 @@ import {
import { MatTooltip } from '@angular/material/tooltip';
import { FindPipe } from '../../../../pipes/find.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { DelinquencyBucketBaseComponent } from '../../delinquency-base.component';
import { EnumOptionData } from 'app/shared/models/option-data.model';

@Component({
selector: 'mifosx-create-bucket',
Expand All @@ -56,11 +58,10 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
FindPipe
]
})
export class CreateBucketComponent implements OnInit {
export class CreateBucketComponent extends DelinquencyBucketBaseComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private productsService = inject(ProductsService);
private router = inject(Router);
private route = inject(ActivatedRoute);
dialog = inject(MatDialog);
private translateService = inject(TranslateService);

Expand All @@ -74,6 +75,9 @@ export class CreateBucketComponent implements OnInit {
delinquencyRangesData: any;
delinquencyRangesIds: any;

frequencyTypeOptions: EnumOptionData[] = [];
minimumPaymentOptions: EnumOptionData[] = [];

/** Delinquency Range Displayed Columns */
displayedColumns: string[] = [
'classification',
Expand All @@ -83,12 +87,23 @@ export class CreateBucketComponent implements OnInit {
];

constructor() {
this.route.data.subscribe((data: { delinquencyRanges: any }) => {
this.delinquencyRangesData = data.delinquencyRanges;
this.delinquencyRangesData = this.delinquencyRangesData.sort(
(objA: { minimumAgeDays: number }, objB: { minimumAgeDays: number }) =>
objA.minimumAgeDays - objB.minimumAgeDays
);
super();
this.route.data.subscribe((data: { delinquencyBucketsTemplateData: any }) => {
if (this.isRegularBucket) {
this.delinquencyRangesData = data.delinquencyBucketsTemplateData;
this.delinquencyRangesData = this.delinquencyRangesData.sort(
(objA: { minimumAgeDays: number }, objB: { minimumAgeDays: number }) =>
objA.minimumAgeDays - objB.minimumAgeDays
);
} else if (this.isWorkingCapitalBucket) {
this.delinquencyRangesData = data.delinquencyBucketsTemplateData.rangesOptions;
this.delinquencyRangesData = this.delinquencyRangesData.sort(
(objA: { minimumAgeDays: number }, objB: { minimumAgeDays: number }) =>
objA.minimumAgeDays - objB.minimumAgeDays
);
this.frequencyTypeOptions = data.delinquencyBucketsTemplateData.frequencyTypeOptions;
this.minimumPaymentOptions = data.delinquencyBucketsTemplateData.minimumPaymentOptions;
}
});
}

Expand All @@ -102,12 +117,44 @@ export class CreateBucketComponent implements OnInit {
* Creates the Delinquency Bucket form
*/
setupForm(): void {
this.bucketForm = this.formBuilder.group({
name: [
'',
Validators.required
]
});
if (this.isRegularBucket) {
this.bucketForm = this.formBuilder.group({
name: [
'',
Validators.required
]
});
} else if (this.isWorkingCapitalBucket) {
this.bucketForm = this.formBuilder.group({
name: [
'',
Validators.required
],
frequency: [
'',
[
Validators.pattern('^(0*[1-9][0-9]*)$'),
Validators.min(1),
Validators.required
]
],
frequencyType: [
'',
[Validators.required]
],
minimumPayment: [
'',
[
Validators.required,
Validators.min(0.01)
]
],
minimumPaymentType: [
'',
[Validators.required]
]
});
}
}

/**
Expand Down Expand Up @@ -158,30 +205,46 @@ export class CreateBucketComponent implements OnInit {
});
}

/**
* Submits the Delinquency Bucket form and creates the Delinquency Bucket,
* if successful redirects to Delinquency Buckets.
*/
submit() {
get payloadData() {
const bucketType: number = this.isRegularBucket ? 1 : 2;
const ranges: any = [];
this.rangesDataSource.forEach((item: any) => {
ranges.push(item.rangeId);
});
if (ranges.length > 0) {
const data = {
if (this.isRegularBucket) {
return {
bucketType: bucketType,
...this.bucketForm.value,
ranges: ranges
};

this.productsService.createDelinquencyBucket(data).subscribe((response: any) => {
this.router.navigate(
[
'../',
response.resourceId
],
{ relativeTo: this.route }
);
});
} else if (this.isWorkingCapitalBucket) {
const payload = this.bucketForm.value;
const bucketName = payload['name'];
return {
bucketType: bucketType,
name: bucketName,
minimumPaymentPeriodAndRule: payload,
ranges: ranges
};
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Submits the Delinquency Bucket form and creates the Delinquency Bucket,
* if successful redirects to Delinquency Buckets.
*/
submit() {
this.productsService.createDelinquencyBucket(this.payloadData).subscribe((response: any) => {
this.router.navigate(
[
'../',
response.resourceId
],
{
queryParams: { bucketType: this.delinquencyBucketType.value },
relativeTo: this.route
}
);
});
}
}
Loading
Loading