WEB-642: Add Loan Originators management#3095
Conversation
|
Note
|
| Cohort / File(s) | Summary |
|---|---|
Organization - Loan Originators UI & Resolvers src/app/organization/loan-originators/loan-originators.component.*, src/app/organization/loan-originators/create-loan-originator/*, src/app/organization/loan-originators/edit-loan-originator/*, src/app/organization/loan-originators/view-loan-originator/*, src/app/organization/loan-originators/*.resolver.ts |
Adds standalone components for list/create/edit/view, template resolvers, table UI, forms with validation, delete flow and template data resolvers. |
Organization Service API src/app/organization/organization.service.ts |
Adds CRUD endpoints and template endpoint for loan originators: getLoanOriginators, getLoanOriginatorsTemplate, getLoanOriginator, createLoanOriginator, updateLoanOriginator, deleteLoanOriginator. |
Loan Account — Attach/Detach Originator src/app/loans/loans-view/loan-account-actions/attach-originator/*, src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.*, src/app/loans/common-resolvers/loan-action-button.resolver.ts, src/app/loans/loans-view/loan-accounts-button-config.ts |
Adds AttachOriginatorComponent (template, styles, logic), integrates "Attach Loan Originator" action into actions map and button configuration, and extends loan-action-button resolver to return originators via OrganizationService. |
Loans Service src/app/loans/loans.service.ts |
Adds attachLoanOriginator and dettachLoanOriginator methods invoking POST/DELETE endpoints for loan originator association. |
Loan Originators Tab (Loan details) src/app/loans/loans-view/loan-originators-tab/* |
Updates columns to use nested CodeValue fields (originatorType/channelType), adds actions column with detach confirmation dialog, dettachLoanOriginator method and navigation refresh. |
Data Model src/app/loans/models/loan-account.model.ts |
Replaces numeric originatorTypeId/channelTypeId with originatorType: CodeValue and channelType: CodeValue; adds CodeValue import. |
Routing & Navigation src/app/organization/organization-routing.module.ts, src/app/organization/organization.component.html, src/app/organization/organization.component.ts |
Registers manage-loan-originators routes and resolvers, adds menu entry guarded by permission, and adjusts arrowBooleans length. |
Icons & Styling src/app/shared/icons.module.ts, src/app/organization/loan-originators/**/*.scss, src/app/loans/loans-view/loan-account-actions/attach-originator/*.scss |
Registers faUnlink icon and adds SCSS files (container width, table/utility styles). |
Translations src/assets/translations/*.json |
Adds translation keys for Manage Loan Originators, Loan Originator, Attach Loan Originator, descriptive note and status labels across multiple locales. |
Small UI change src/app/loans/loans-view/loan-account-actions/approve-loan/approve-loan.component.html |
Removes the Transaction Amount input field (approvedLoanAmount) from the approve loan form. |
Sequence Diagram(s)
sequenceDiagram
participant User as User (Browser)
participant Router as Router/Resolver
participant OrgSvc as OrganizationService
participant UI as AttachOriginatorComponent
participant LoansSvc as LoansService
participant API as Backend API
User->>Router: Navigate to Attach Originator view
Router->>OrgSvc: resolve() -> getLoanOriginators()/getLoanOriginatorsTemplate()
OrgSvc-->>Router: originators list
Router-->>UI: route data (loanOriginators)
User->>UI: select originator & submit
UI->>LoansSvc: attachLoanOriginator(loanId, originatorId)
LoansSvc->>API: POST /loans/{loanId}/originators/{originatorId}
API-->>LoansSvc: 200 OK
LoansSvc-->>UI: success
UI->>Router: navigate to ../../general
Router-->>User: updated loan view
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
- WEB-352: Re-Amortization Interest Handling configuration #2714: Modifies the same
loan-action-button.resolver.tsby adding conditional branches for loan action buttons. - WEB-611: Add charge option menu loan overpaid #3027: Adds action buttons to the loan accounts button configuration similar to this PR.
- WEB-642: Add Originators tab on loan details view #3064: Touches loan originators feature and loan details UI, related to the originators tab and resolvers.
Suggested reviewers
- IOhacker
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The PR title clearly and concisely summarizes the main change: adding loan originators management functionality. It directly aligns with the changeset, which includes new components, services, and UI for creating, viewing, editing, deleting, and attaching loan originators. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing touches
- 📝 Generate docstrings
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 6
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts (1)
8-114:⚠️ Potential issue | 🟡 MinorGuard route param access and clean up route subscriptions.
Line 76 directly accesses
this.route.parent.parent.snapshot.paramMapwithout optional chaining (line 77 inconsistently uses it), which can throw if the hierarchy changes. Both subscriptions in the constructor (lines 78–83) lacktakeUntilDestroyed, violating clean observable patterns. Additionally,loanIdandclientIdare used indettachLoanOriginator()(line 100) andreload()(line 113) without null guards, and nested properties are accessed without null coalescing.Suggested fixes
-import { Component, inject } from '@angular/core'; +import { Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ export class LoanOriginatorsTabComponent { private route = inject(ActivatedRoute); private router = inject(Router); private loansService = inject(LoansService); private translateService = inject(TranslateService); private dialog = inject(MatDialog); + private destroyRef = inject(DestroyRef); @@ constructor() { - this.clientId = this.route.parent.parent.snapshot.paramMap.get('clientId'); - this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId'); - this.route.parent.parent.data.subscribe((data: { loanDetailsData: any }) => { - this.loanStatus = data.loanDetailsData.status; - }); - this.route.parent.data.subscribe((data: { loanOriginatorsData: any }) => { - this.loanOriginatorsData = data.loanOriginatorsData.originators; - }); + const parent = this.route.parent?.parent; + this.clientId = parent?.snapshot.paramMap.get('clientId'); + this.loanId = parent?.snapshot.paramMap.get('loanId'); + + parent?.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { loanDetailsData: any }) => { + this.loanStatus = data.loanDetailsData?.status ?? null; + }); + this.route.parent?.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { loanOriginatorsData: any }) => { + this.loanOriginatorsData = data.loanOriginatorsData?.originators ?? []; + }); } @@ dettachLoanOriginator(loanOriginator: LoanOriginator): void { + if (!this.loanId) { + return; + } const dettachCodeDialogRef = this.dialog.open(ConfirmationDialogComponent, { @@ private reload() { + if (!this.clientId) { + return; + } const url: string = this.router.url; this.router .navigateByUrl(`/clients/${this.clientId}/loans-accounts`, { skipLocationChange: true }) .then(() => this.router.navigate([url])); }
🤖 Fix all issues with AI agents
In
`@src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts`:
- Around line 75-79: The submit method uses loanId without validating it and
does not use takeUntilDestroyed for subscription cleanup or check form validity;
update submit() in attach-originator.component.ts to first ensure
attachLoanOriginatorForm.valid and that this.loanId is non-null (return early
and surface an error if not), then call
this.loanService.attachLoanOriginator(this.loanId,
approveLoanFormData.originatorId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(...)
to navigate on success and handle errors in the error handler (e.g., log or set
form error); import takeUntilDestroyed from `@angular/core/rxjs-interop` and
inject DestroyRef as destroyRef to enable this pattern.
In
`@src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.html`:
- Around line 105-112: The Submit button in edit-loan-originator.component.html
uses the wrong permission token ('CREATE_LOAN_ORIGINATOR'); update the
permission check on the <button> that uses *mifosxHasPermission to use the
edit/update permission 'UPDATE_LOAN_ORIGINATOR' instead, keeping the rest of the
template (mat-raised-button, color="primary",
[disabled]="!loanOriginatorForm.valid") unchanged so the permission directive
grants access only for update operations.
In
`@src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts`:
- Around line 71-83: In createLoanOriginatorForm replace the externalId initial
value to use the existing record (loanOriginatorsData.externalId) instead of
loanOriginatorsTemplateData.externalId so the edit form is prefilled with the
originator's value; also tighten the name Validators.pattern to only allow
letters at start (e.g. use /^[A-Za-z].*/ instead of '(^[A-z]).*') so non-letter
ASCII chars are excluded—update the loanOriginatorForm definition where
externalId and name validators are set and keep Validators.required intact.
- Around line 103-111: The edit form currently deletes externalId before sending
updates in submit() while the form marks externalId as required and editable;
fix by choosing one of two consistent approaches: (A) If externalId should not
be changed on edit, remove the required validator and make the FormControl
readOnly/disabled in the loanOriginatorForm setup (the FormGroup where
externalId is declared) and keep the delete statement so the payload omits it;
or (B) If externalId is editable on update, remove the delete
loanOriginatorFormData.externalId line in submit() so
organizationService.updateLoanOriginator(this.loanOriginatorsData.id, data)
includes externalId; update any template bindings accordingly (e.g., disabling
the input when choosing option A). Ensure the chosen change is applied to the
loanOriginatorForm initialization and submit() logic for consistency.
In `@src/app/organization/organization.component.html`:
- Around line 326-349: The arrowBooleans array is initialized too short (only
indices 0–17) but the template references arrowBooleans[18]; update the
initialization of the arrowBooleans variable in organization.component.ts (where
it’s currently set as new Array(18).fill(false)) to allocate 19 entries (new
Array(19).fill(false)) so index 18 is valid, then run a quick build/test to
ensure no other out-of-bounds indices exist.
In `@src/app/organization/organization.service.ts`:
- Around line 852-875: The three methods use inconsistent types for
originatorId: getLoanOriginator(originatorId: string) but
updateLoanOriginator(originatorId: number) and
deleteLoanOriginator(originatorId: number); change updateLoanOriginator and
deleteLoanOriginator signatures to accept originatorId: string (matching
getLoanOriginator) so route params and template literals are typed correctly,
and update any callers to pass strings (or coerce to string before calling) to
avoid type mismatches.
🟡 Minor comments (15)
src/assets/translations/de-DE.json-2683-2683 (1)
2683-2683:⚠️ Potential issue | 🟡 MinorUse consistent terminology for “Loan Originator”.
Line 2683 uses “Darlehensgeber” (lender) while Line 1981 uses “Kreditvermittler”. Consider aligning to avoid UX confusion.
💡 Suggested fix
- "Attach Loan Originator": "Darlehensgeber hinzufügen", + "Attach Loan Originator": "Kreditvermittler hinzufügen",src/assets/translations/fr-FR.json-1115-1115 (1)
1115-1115:⚠️ Potential issue | 🟡 MinorUnify French terminology for “Loan Originator”.
The new strings mix “courtiers en prêts”, “Agent de prêt”, and “initiateur de crédit”. This is inconsistent and risks confusion with “Loan Officer”. Please standardize to a single term across menu/labels/text.
💡 Suggested alignment (example)
- "Loan Originator": "Agent de prêt", + "Loan Originator": "Courtier en prêts", @@ - "Attach Loan Originator": "Attacher l'initiateur de crédit", + "Attach Loan Originator": "Attacher le courtier en prêts",Also applies to: 1981-1981, 2681-2681, 3188-3188
src/assets/translations/ne-NE.json-1979-1979 (1)
1979-1979:⚠️ Potential issue | 🟡 MinorInconsistent terminology for "Loan Originator" translation.
This line translates "Loan Originator" as "ऋण प्रवर्तक" (prevertak), but all other related translations use "ऋण सुरुवातकर्ता" (suruwatkarta):
- Line 1091: "Loan Originators" → "ऋण सुरुवातकर्ताहरू"
- Line 1113: "Manage Loan Originators" → "ऋण सुरुवातकर्ताहरू व्यवस्थापन गर्नुहोस्"
- Line 2679: "Attach Loan Originator" → "ऋण सुरुवातकर्ता संलग्न गर्नुहोस्"
For consistency across the UI, consider using "ऋण सुरुवातकर्ता" here as well.
🌐 Proposed fix for terminology consistency
- "Loan Originator": "ऋण प्रवर्तक", + "Loan Originator": "ऋण सुरुवातकर्ता",src/assets/translations/es-MX.json-1981-1981 (1)
1981-1981:⚠️ Potential issue | 🟡 MinorPrefer singular “crédito” for the singular label.
“Loan Originator” is singular, but the translation uses plural “Créditos”. Consider using singular and sentence case for natural Spanish.
Suggested update
- "Loan Originator": "Originador de Créditos", + "Loan Originator": "Originador de crédito", ... - "Attach Loan Originator": "Adjuntar Originador de Créditos", + "Attach Loan Originator": "Adjuntar originador de crédito",Also applies to: 2685-2685
src/assets/translations/es-MX.json-1114-1114 (1)
1114-1114:⚠️ Potential issue | 🟡 MinorUnify “préstamos” vs “créditos” for consistency.
These two strings use “préstamos” while most es-MX UI strings use “Crédito”. Consider aligning the terminology.
Suggested update
- "Manage Loan Originators": "Administrar originadores de préstamos", + "Manage Loan Originators": "Administrar originadores de créditos", ... - "Loan Originators are associated with loan accounts": "Los originadores de préstamos están asociados con cuentas de préstamos", + "Loan Originators are associated with loan accounts": "Los originadores de créditos están asociados con cuentas de crédito",Also applies to: 3193-3193
src/assets/translations/es-CL.json-1115-1115 (1)
1115-1115:⚠️ Potential issue | 🟡 MinorUnify “préstamos” vs “créditos” terminology for Loan Originator strings.
Right now the feature mixes “originadores de préstamos” with “Originador de Créditos,” which reads inconsistent in the same UI flow. Please pick one term and apply it across these keys (and ideally align with the existing “Loan Originators” label).
💡 Suggested alignment (toward “préstamos”)
- "Loan Originator": "Originador de Créditos", + "Loan Originator": "Originador de préstamos", @@ - "Attach Loan Originator": "Adjuntar Originador de Créditos", + "Attach Loan Originator": "Adjuntar originador de préstamos",Also applies to: 1981-1981, 2682-2682, 3190-3190
src/app/loans/loans.service.ts-661-669 (1)
661-669:⚠️ Potential issue | 🟡 MinorRemove the empty body argument from
.delete()to match codebase patterns, and clarify intent for.post().The second parameter to
HttpClient.delete()is an options object, not a body. Passing an empty{}is ineffective. However, instead of using{ body: {} }, align with the established codebase pattern: remove the second argument entirely (all other.delete()calls in the service have no options argument).Additionally,
attachLoanOriginatorpassing an empty body to.post()is inconsistent with all other POST calls in loans.service.ts, which pass actual data. Clarify whether these endpoints truly require no payload or if data is missing.Suggested changes
attachLoanOriginator(loanId: string, originatorId: string): Observable<any> { - const emptyBody = {}; - return this.http.post(`/loans/${loanId}/originators/${originatorId}`, emptyBody); + return this.http.post(`/loans/${loanId}/originators/${originatorId}`, {}); } dettachLoanOriginator(loanId: string, originatorId: string): Observable<any> { - const emptyBody = {}; - return this.http.delete(`/loans/${loanId}/originators/${originatorId}`, emptyBody); + return this.http.delete(`/loans/${loanId}/originators/${originatorId}`); }src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.html-37-48 (1)
37-48:⚠️ Potential issue | 🟡 MinorGuard nested CodeValue access against nulls.
IforiginatorType/channelTypeis missing, the template will throw. Consider safe navigation with a fallback.Suggested fix
- {{ item.originatorType.name }} + {{ item.originatorType?.name || '-' }} ... - {{ item.channelType.name }} + {{ item.channelType?.name || '-' }}src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts-49-57 (1)
49-57:⚠️ Potential issue | 🟡 MinorGuard against undefined data and remove debug logging.
dataObject.forEachwill throw if the input is missing; also remove theconsole.log.Suggested fix
- console.log(this.dataObject); - this.loanOriginators = []; - this.dataObject.forEach((loanOriginator: LoanOriginator) => { - if (loanOriginator.status === 'ACTIVE') { - this.loanOriginators.push(loanOriginator); - } - }); + const source = Array.isArray(this.dataObject) ? this.dataObject : []; + this.loanOriginators = source.filter((loanOriginator) => loanOriginator.status === 'ACTIVE');src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.html-49-65 (1)
49-65:⚠️ Potential issue | 🟡 MinorAlign tooltip/title text for the detach action.
The title says “Dettach” while the tooltip says “Delete.” Use one consistent label (and spelling) to avoid UX confusion.Suggested fix
- title="{{ 'tooltips.Dettach' | translate }}" mat-icon-button - matTooltip="{{ 'tooltips.Delete' | translate }}" + matTooltip="{{ 'tooltips.Dettach' | translate }}" *mifosxHasPermission="'DETTACH_LOAN_ORIGINATOR'"src/app/organization/loan-originators/loan-originators.component.html-87-87 (1)
87-87:⚠️ Potential issue | 🟡 MinorStop event propagation on delete button click.
The delete button is inside a clickable row with
[routerLink]. Clicking delete will also trigger the row navigation. Add$event.stopPropagation()to prevent this.🔧 Suggested fix
- (click)="deleteLoanOriginator(loanOriginator)" + (click)="deleteLoanOriginator(loanOriginator); $event.stopPropagation()"src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.ts-100-108 (1)
100-108:⚠️ Potential issue | 🟡 MinorAdd error handling for the create operation.
The
submit()method callscreateLoanOriginatorwithout handling potential errors. Failed API calls will silently fail without user feedback.🔧 Suggested fix
submit() { const loanOriginatorFormData = this.loanOriginatorForm.value; const data = { ...loanOriginatorFormData }; - this.organizationService.createLoanOriginator(data).subscribe((response: any) => { - this.router.navigate(['../'], { relativeTo: this.route }); - }); + this.organizationService.createLoanOriginator(data).subscribe({ + next: () => { + this.router.navigate(['../'], { relativeTo: this.route }); + }, + error: (error) => { + // Handle error - show notification to user + console.error('Failed to create loan originator:', error); + } + }); }src/app/organization/loan-originators/loan-originators.component.ts-134-140 (1)
134-140:⚠️ Potential issue | 🟡 MinorAdd null check for dialog response and error handling for delete operation.
If the dialog is dismissed (e.g., clicking outside),
responsemay be undefined, causingresponse.deleteto throw. Also, the delete operation lacks error handling.🔧 Suggested fix
deleteCodeDialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { + if (response?.delete) { this.organizationService.deleteLoanOriginator(loanOriginator.id).subscribe(() => { this.router.navigate(['/organization/manage-loan-originators']); + }, (error) => { + // Error handling - could show a snackbar/toast notification + console.error('Failed to delete loan originator:', error); }); } });src/app/organization/loan-originators/loan-originators.component.html-67-75 (1)
67-75:⚠️ Potential issue | 🟡 MinorAdd safe navigation for nested property access.
Accessing
loanOriginator.originatorType.nameandloanOriginator.channelType.namewill throw if these nested objects are null/undefined. Use optional chaining for defensive access.🛡️ Suggested fix
<ng-container matColumnDef="originatorType"> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Originator Type' | translate }}</th> - <td mat-cell *matCellDef="let loanOriginator">{{ loanOriginator.originatorType.name }}</td> + <td mat-cell *matCellDef="let loanOriginator">{{ loanOriginator.originatorType?.name }}</td> </ng-container> <ng-container matColumnDef="channelType"> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Channel Type' | translate }}</th> - <td mat-cell *matCellDef="let loanOriginator">{{ loanOriginator.channelType.name }}</td> + <td mat-cell *matCellDef="let loanOriginator">{{ loanOriginator.channelType?.name }}</td> </ng-container>src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.ts-74-80 (1)
74-80:⚠️ Potential issue | 🟡 MinorName pattern excludes international characters.
The pattern
(^[A-z]).*only allows names starting with ASCII letters A-Z or a-z. This excludes valid names with accented characters (e.g., "Ángel", "Müller", "Øystein"). Consider using a more inclusive Unicode pattern.🔧 Suggested fix
name: [ '', [ Validators.required, - Validators.pattern('(^[A-z]).*') + Validators.pattern('^[\\p{L}].*', 'u') ] ],Note: Angular's pattern validator may need adjustment for Unicode support. Alternatively, use
^[a-zA-ZÀ-ÿ].*for common Latin characters or create a custom validator.
🧹 Nitpick comments (14)
src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.html (1)
15-21: Use a stable track expression for the originator loop.Tracking by object identity can cause unnecessary DOM churn if the array is rehydrated; track by
idinstead.♻️ Suggested change
- `@for` (loanOriginator of loanOriginators; track loanOriginator) { + `@for` (loanOriginator of loanOriginators; track loanOriginator.id) {As per coding guidelines, for Angular code: verify component separation, trackBy on *ngFor, strict type safety, and clean observable patterns.
src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.ts (1)
28-41: Prefer a typed observable + async pipe to keep strict typing and avoid manual subscriptions.
This eliminates the uninitialized field and aligns with cleaner observable patterns.♻️ Suggested refactor
-import { Component, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; +import { map } from 'rxjs'; ... export class ViewLoanOriginatorComponent { private route = inject(ActivatedRoute); - - /** Employee data. */ - loanOriginatorData: LoanOriginator; - - /** - * Retrieves the Loan Originator data from `resolve`. - * `@param` {ActivatedRoute} route Activated Route. - */ - constructor() { - this.route.data.subscribe((data: { loanOriginatorData: LoanOriginator }) => { - this.loanOriginatorData = data.loanOriginatorData; - }); - } + /** Loan originator data. */ + readonly loanOriginatorData$ = this.route.data.pipe( + map((data: { loanOriginatorData: LoanOriginator }) => data.loanOriginatorData) + ); }As per coding guidelines, verify strict type safety and clean observable patterns.
src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html (1)
16-59: If you adopt the async refactor, wrap the view in an@ifguard to avoid undefined dereferences.✅ Template adjustment example
-<div class="container"> +@if (loanOriginatorData$ | async; as loanOriginatorData) { +<div class="container"> <mat-card> <mat-card-content> ... </mat-card-content> </mat-card> </div> +}As per coding guidelines, ensure clean observable patterns and strict type safety in templates.
src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.html (1)
65-89: Track option rows by stable ids to avoid unnecessary re-renders.♻️ Suggested tweak
-@for (originatorType of originatorTypeOptions; track originatorType) { +@for (originatorType of originatorTypeOptions; track originatorType.id) { <mat-option [value]="originatorType.id"> {{ originatorType.name }} </mat-option> } ... -@for (channelTypeI of channelTypeOptions; track channelTypeI) { +@for (channelTypeI of channelTypeOptions; track channelTypeI.id) { <mat-option [value]="channelTypeI.id"> {{ channelTypeI.name }} </mat-option> }As per coding guidelines, ensure trackBy usage on iteration blocks.
src/app/organization/loan-originators/loan-originators-template.resolver.ts (1)
25-31: Create aLoanOriginatorsTemplateinterface and use it instead ofObservable<any>.No template model currently exists in the codebase. The template data is accessed in consuming components (edit-loan-originator, create-loan-originator) with known properties like
statusOptions,originatorTypeOptions,channelTypeOptions, andexternalId. Define a concrete interface for this shape and update the resolver andOrganizationService.getLoanOriginatorsTemplate()to returnObservable<LoanOriginatorsTemplate>to enforce strict type safety as required by the coding guidelines for Angular code.src/app/organization/loan-originators/loan-originators.resolver.ts (1)
30-37: Consider adding stricter type safety for the resolver return type.The resolver returns
Observable<any>, which loses type information. Consider using a union type or generic to preserve type safety.♻️ Suggested type improvement
- resolve(route: ActivatedRouteSnapshot): Observable<any> { + resolve(route: ActivatedRouteSnapshot): Observable<LoanOriginator | LoanOriginator[]> { const originatorId = route.paramMap.get('id'); if (originatorId) { return this.organizationService.getLoanOriginator(originatorId); } else { return this.organizationService.getLoanOriginators(); } }This requires importing
LoanOriginatorfromapp/loans/models/loan-account.model.src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.html (2)
68-72: Usetrack originatorType.idinstead of tracking by object reference.Tracking by object reference can cause unnecessary re-renders when the array is replaced with new objects that have the same data. Track by the unique
idproperty instead.♻️ Suggested fix
- `@for` (originatorType of originatorTypeOptions; track originatorType) { + `@for` (originatorType of originatorTypeOptions; track originatorType.id) {As per coding guidelines: "For Angular code: verify component separation, trackBy on *ngFor, strict type safety, and clean observable patterns."
85-89: Usetrack channelTypeI.idinstead of tracking by object reference.Same issue as originatorType - track by the unique
idproperty for optimal change detection.♻️ Suggested fix
- `@for` (channelTypeI of channelTypeOptions; track channelTypeI) { + `@for` (channelType of channelTypeOptions; track channelType.id) { <mat-option [value]="channelTypeI.id"> {{ channelTypeI.name }} </mat-option>Also consider renaming
channelTypeItochannelTypefor consistency with the other loops.As per coding guidelines: "For Angular code: verify component separation, trackBy on *ngFor, strict type safety, and clean observable patterns."
src/app/organization/loan-originators/loan-originators.component.html (1)
22-23: Add type assertion or null check for event target.
$event.targetcould be null, and accessing.valuewithout type assertion may cause issues. Consider using a template reference variable or type assertion.♻️ Suggested fix using template reference
- <input matInput (keyup)="applyFilter($event.target.value)" /> + <input matInput `#filterInput` (keyup)="applyFilter(filterInput.value)" />src/app/organization/loan-originators/loan-originators.component.ts (2)
93-96: Remove unused ViewChild references if not needed.
tableLoanOriginatorsandtemplateTableLoanOriginatorsViewChild references appear to be unused in the component. Consider removing them to reduce code clutter.
66-126: Consider adding a trackBy function for table performance.The mat-table iterates over
dataSourcebut notrackByis configured. For better change detection performance, especially with larger datasets, add a trackBy function.♻️ Suggested implementation
Add this method to the component:
trackByOriginatorId(index: number, loanOriginator: LoanOriginator): number { return loanOriginator.id; }Then in the template, use
[trackBy]="trackByOriginatorId"on the mat-table (note: mat-table doesn't directly support trackBy, but the underlying MatTableDataSource handles this internally - consider this optional).As per coding guidelines: "For Angular code: verify component separation, trackBy on *ngFor, strict type safety, and clean observable patterns."
src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.ts (1)
44-47: Remove unused ViewChild references.
createLoanOriginatorFormRefandtemplateCreateLoanOriginatorFormappear to be unused. Consider removing them to keep the component clean.src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts (2)
11-44: Prefer typed reactive forms and a typed template-data shape.Untyped forms plus
anyundermine strict type safety, which the Angular guideline calls out. A small typed form/interface will make this component safer and self-documenting.♻️ Example typed-form refactor
-import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms'; - loanOriginatorForm: UntypedFormGroup; - loanOriginatorsTemplateData: any; + loanOriginatorForm: FormGroup<{ + externalId: FormControl<string>; + name: FormControl<string>; + status: FormControl<string>; + originatorTypeId: FormControl<number>; + channelTypeId: FormControl<number>; + }>; + loanOriginatorsTemplateData: { + externalId?: string; + statusOptions: string[]; + originatorTypeOptions: CodeValue[]; + channelTypeOptions: CodeValue[]; + };
51-66: Move the route-data subscription tongOnInitand bind it to the destroy lifecycle.This keeps constructor side‑effect‑free and aligns with clean observable patterns.
♻️ Example lifecycle-safe subscription
-import { Component, OnInit, TemplateRef, ElementRef, ViewChild, inject } from '@angular/core'; +import { Component, OnInit, TemplateRef, ElementRef, ViewChild, inject, DestroyRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; private route = inject(ActivatedRoute); private router = inject(Router); + private destroyRef = inject(DestroyRef); - constructor() { - this.route.data.subscribe((data: { loanOriginatorData: LoanOriginator; loanOriginatorsTemplateData: any }) => { - this.loanOriginatorsData = data.loanOriginatorData; - this.loanOriginatorsTemplateData = data.loanOriginatorsTemplateData; - this.statusOptions = data.loanOriginatorsTemplateData.statusOptions; - this.originatorTypeOptions = data.loanOriginatorsTemplateData.originatorTypeOptions; - this.channelTypeOptions = data.loanOriginatorsTemplateData.channelTypeOptions; - }); - } + constructor() {} ngOnInit() { + this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { loanOriginatorData: LoanOriginator; loanOriginatorsTemplateData: any }) => { + this.loanOriginatorsData = data.loanOriginatorData; + this.loanOriginatorsTemplateData = data.loanOriginatorsTemplateData; + this.statusOptions = data.loanOriginatorsTemplateData.statusOptions; + this.originatorTypeOptions = data.loanOriginatorsTemplateData.originatorTypeOptions; + this.channelTypeOptions = data.loanOriginatorsTemplateData.channelTypeOptions; + }); this.createLoanOriginatorForm(); }
| submit() { | ||
| const approveLoanFormData = this.attachLoanOriginatorForm.value; | ||
| this.loanService.attachLoanOriginator(this.loanId, approveLoanFormData.originatorId).subscribe((response: any) => { | ||
| this.router.navigate(['../../general'], { relativeTo: this.route }); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts | head -100Repository: openMF/web-app
Length of output: 2998
🏁 Script executed:
rg -n "takeUntilDestroyed|take\(1\)|first\(" src/app -g '*.ts' | head -30Repository: openMF/web-app
Length of output: 4364
Use takeUntilDestroyed for cleanup and validate loanId before submission.
The loanId is typed as string | null but used without validation. Additionally, add form validity check and error handling. Per codebase convention, use takeUntilDestroyed(this.destroyRef) instead of take(1) for proper lifecycle management.
Suggested fix
submit() {
+ if (!this.loanId || this.attachLoanOriginatorForm.invalid) {
+ return;
+ }
const approveLoanFormData = this.attachLoanOriginatorForm.value;
this.loanService
.attachLoanOriginator(this.loanId, approveLoanFormData.originatorId)
- .subscribe((response: any) => {
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ next: () => {
this.router.navigate(['../../general'], { relativeTo: this.route });
- });
+ },
+ error: () => {
+ // TODO: surface error to user
+ }
+ });
}Import takeUntilDestroyed from @angular/core/rxjs-interop and inject DestroyRef.
📝 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.
| submit() { | |
| const approveLoanFormData = this.attachLoanOriginatorForm.value; | |
| this.loanService.attachLoanOriginator(this.loanId, approveLoanFormData.originatorId).subscribe((response: any) => { | |
| this.router.navigate(['../../general'], { relativeTo: this.route }); | |
| }); | |
| submit() { | |
| if (!this.loanId || this.attachLoanOriginatorForm.invalid) { | |
| return; | |
| } | |
| const approveLoanFormData = this.attachLoanOriginatorForm.value; | |
| this.loanService | |
| .attachLoanOriginator(this.loanId, approveLoanFormData.originatorId) | |
| .pipe(takeUntilDestroyed(this.destroyRef)) | |
| .subscribe({ | |
| next: () => { | |
| this.router.navigate(['../../general'], { relativeTo: this.route }); | |
| }, | |
| error: () => { | |
| // TODO: surface error to user | |
| } | |
| }); | |
| } |
🤖 Prompt for AI Agents
In
`@src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts`
around lines 75 - 79, The submit method uses loanId without validating it and
does not use takeUntilDestroyed for subscription cleanup or check form validity;
update submit() in attach-originator.component.ts to first ensure
attachLoanOriginatorForm.valid and that this.loanId is non-null (return early
and surface an error if not), then call
this.loanService.attachLoanOriginator(this.loanId,
approveLoanFormData.originatorId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(...)
to navigate on success and handle errors in the error handler (e.g., log or set
form error); import takeUntilDestroyed from `@angular/core/rxjs-interop` and
inject DestroyRef as destroyRef to enable this pattern.
src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.html
Show resolved
Hide resolved
| createLoanOriginatorForm() { | ||
| this.loanOriginatorForm = this.formBuilder.group({ | ||
| externalId: [ | ||
| this.loanOriginatorsTemplateData.externalId, | ||
| Validators.required | ||
| ], | ||
| name: [ | ||
| this.loanOriginatorsData.name, | ||
| [ | ||
| Validators.required, | ||
| Validators.pattern('(^[A-z]).*') | ||
| ] | ||
| ], |
There was a problem hiding this comment.
Prefill externalId from the originator record (and tighten the name regex).
In an edit flow, Line 74 should use the existing record value; using template data can blank or reset the field. Also, [A-z] includes non-letters (e.g., [ and \).
🐛 Suggested fix
externalId: [
- this.loanOriginatorsTemplateData.externalId,
+ this.loanOriginatorsData.externalId,
Validators.required
],
name: [
this.loanOriginatorsData.name,
[
Validators.required,
- Validators.pattern('(^[A-z]).*')
+ Validators.pattern('^[A-Za-z].*')
]
],🤖 Prompt for AI Agents
In
`@src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts`
around lines 71 - 83, In createLoanOriginatorForm replace the externalId initial
value to use the existing record (loanOriginatorsData.externalId) instead of
loanOriginatorsTemplateData.externalId so the edit form is prefilled with the
originator's value; also tighten the name Validators.pattern to only allow
letters at start (e.g. use /^[A-Za-z].*/ instead of '(^[A-z]).*') so non-letter
ASCII chars are excluded—update the loanOriginatorForm definition where
externalId and name validators are set and keep Validators.required intact.
src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts
Show resolved
Hide resolved
| <div #manageLoanOriginators> | ||
| <mat-list-item *mifosxHasPermission="'READ_LOAN_ORIGINATOR'"> | ||
| <div class="menu-list-item-content"> | ||
| <div class="menu-left-section" [routerLink]="['manage-loan-originators']"> | ||
| <mat-icon matListIcon> | ||
| <fa-icon icon="edit" size="sm"></fa-icon> | ||
| </mat-icon> | ||
| <div matLine> | ||
| {{ 'labels.heading.Manage Loan Originators' | translate }} | ||
| @if (arrowBooleans[18]) { | ||
| <p matLine [routerLink]="['manage-loan-originators']" class="menu-explanation"> | ||
| {{ 'labels.text.Loan Originators are associated with loan accounts' | translate }} | ||
| </p> | ||
| } | ||
| </div> | ||
| </div> | ||
| <div class="menu-right-section"> | ||
| @if (!arrowBooleans[18]) { | ||
| <fa-icon (click)="arrowBooleansToggle(18)" icon="arrow-down" size="md"></fa-icon> | ||
| } | ||
| @if (arrowBooleans[18]) { | ||
| <fa-icon (click)="arrowBooleansToggle(18)" icon="arrow-up" size="md"></fa-icon> | ||
| } | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate the component TypeScript file and search for arrowBooleans
fd -a "organization.component.ts" | head -5Repository: openMF/web-app
Length of output: 123
🏁 Script executed:
#!/bin/bash
# Search for arrowBooleans initialization and usage patterns
rg -n "arrowBooleans" --type ts -C 5 src/app/organization/Repository: openMF/web-app
Length of output: 3589
🏁 Script executed:
#!/bin/bash
# Also check for any array initialization that might set the size
rg -n "new Array|arrowBooleans\s*=" --type ts src/app/organization/Repository: openMF/web-app
Length of output: 393
Expand arrowBooleans array to support index 18.
The array at line 70 of organization.component.ts is initialized with only 18 elements (new Array(18).fill(false)), which creates indices 0–17. However, the new menu item uses arrowBooleans[18] at lines 341 and 344, which is out of bounds. Change the initialization to new Array(19).fill(false) to support index 18.
🤖 Prompt for AI Agents
In `@src/app/organization/organization.component.html` around lines 326 - 349, The
arrowBooleans array is initialized too short (only indices 0–17) but the
template references arrowBooleans[18]; update the initialization of the
arrowBooleans variable in organization.component.ts (where it’s currently set as
new Array(18).fill(false)) to allocate 19 entries (new Array(19).fill(false)) so
index 18 is valid, then run a quick build/test to ensure no other out-of-bounds
indices exist.
| getLoanOriginator(originatorId: string): Observable<any> { | ||
| return this.http.get(`/loan-originators/${originatorId}`); | ||
| } | ||
|
|
||
| /** | ||
| * @returns {Observable<any>} | ||
| */ | ||
| createLoanOriginator(payload: any): Observable<any> { | ||
| return this.http.post(`/loan-originators`, payload); | ||
| } | ||
|
|
||
| /** | ||
| * @returns {Observable<any>} | ||
| */ | ||
| updateLoanOriginator(originatorId: number, payload: any): Observable<any> { | ||
| return this.http.put(`/loan-originators/${originatorId}`, payload); | ||
| } | ||
|
|
||
| /** | ||
| * @returns {Observable<any>} | ||
| */ | ||
| deleteLoanOriginator(originatorId: number): Observable<any> { | ||
| return this.http.delete(`/loan-originators/${originatorId}`); | ||
| } |
There was a problem hiding this comment.
Inconsistent parameter types for originatorId across methods.
getLoanOriginator accepts string, while updateLoanOriginator and deleteLoanOriginator accept number. Route parameters are strings, so calling update/delete with a route param will cause type mismatch. Standardize to string or use template literal coercion consistently.
🔧 Suggested fix - standardize to string
/**
* `@returns` {Observable<any>}
*/
- updateLoanOriginator(originatorId: number, payload: any): Observable<any> {
+ updateLoanOriginator(originatorId: string | number, payload: any): Observable<any> {
return this.http.put(`/loan-originators/${originatorId}`, payload);
}
/**
* `@returns` {Observable<any>}
*/
- deleteLoanOriginator(originatorId: number): Observable<any> {
+ deleteLoanOriginator(originatorId: string | number): Observable<any> {
return this.http.delete(`/loan-originators/${originatorId}`);
}📝 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.
| getLoanOriginator(originatorId: string): Observable<any> { | |
| return this.http.get(`/loan-originators/${originatorId}`); | |
| } | |
| /** | |
| * @returns {Observable<any>} | |
| */ | |
| createLoanOriginator(payload: any): Observable<any> { | |
| return this.http.post(`/loan-originators`, payload); | |
| } | |
| /** | |
| * @returns {Observable<any>} | |
| */ | |
| updateLoanOriginator(originatorId: number, payload: any): Observable<any> { | |
| return this.http.put(`/loan-originators/${originatorId}`, payload); | |
| } | |
| /** | |
| * @returns {Observable<any>} | |
| */ | |
| deleteLoanOriginator(originatorId: number): Observable<any> { | |
| return this.http.delete(`/loan-originators/${originatorId}`); | |
| } | |
| getLoanOriginator(originatorId: string): Observable<any> { | |
| return this.http.get(`/loan-originators/${originatorId}`); | |
| } | |
| /** | |
| * `@returns` {Observable<any>} | |
| */ | |
| createLoanOriginator(payload: any): Observable<any> { | |
| return this.http.post(`/loan-originators`, payload); | |
| } | |
| /** | |
| * `@returns` {Observable<any>} | |
| */ | |
| updateLoanOriginator(originatorId: string | number, payload: any): Observable<any> { | |
| return this.http.put(`/loan-originators/${originatorId}`, payload); | |
| } | |
| /** | |
| * `@returns` {Observable<any>} | |
| */ | |
| deleteLoanOriginator(originatorId: string | number): Observable<any> { | |
| return this.http.delete(`/loan-originators/${originatorId}`); | |
| } |
🤖 Prompt for AI Agents
In `@src/app/organization/organization.service.ts` around lines 852 - 875, The
three methods use inconsistent types for originatorId:
getLoanOriginator(originatorId: string) but updateLoanOriginator(originatorId:
number) and deleteLoanOriginator(originatorId: number); change
updateLoanOriginator and deleteLoanOriginator signatures to accept originatorId:
string (matching getLoanOriginator) so route params and template literals are
typed correctly, and update any callers to pass strings (or coerce to string
before calling) to avoid type mismatches.
163f75b to
1b9eb51
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/loans/models/loan-account.model.ts (1)
176-183:⚠️ Potential issue | 🔴 CriticalCritical: Form components not updated to match interface changes.
The
LoanOriginatorinterface correctly updatedoriginatorTypeIdandchannelTypeIdfrom primitive numbers toCodeValueobjects. However, the form components inedit-loan-originatorandcreate-loan-originatorstill use the old field names (originatorTypeId,channelTypeId) in their form groups. While the edit component correctly extracts.idfrom the newCodeValueobjects, the form submission will fail because the field names don't match the updated interface structure. Both form components must be updated to use the new field names or properly map the form data during submission.
🤖 Fix all issues with AI agents
In
`@src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts`:
- Around line 98-104: The afterClosed subscription on dettachCodeDialogRef can
receive undefined when the dialog is dismissed, so update the handler in
loan-originators-tab.component (the dettachCodeDialogRef.afterClosed().subscribe
callback) to first check that response is truthy (e.g., if (response &&
response.confirm)) before accessing response.confirm, and only call
this.loansService.dettachLoanOriginator(this.loanId,
String(loanOriginator.id)).subscribe(...) and this.reload() when that check
passes; keep the existing dettachLoanOriginator and reload calls intact and
scoped inside the guarded branch.
In `@src/app/organization/loan-originators/loan-originators.component.ts`:
- Around line 134-140: The dialog close handler may receive undefined if the
user dismisses the dialog, so update the deleteCodeDialogRef.afterClosed()
subscription to guard against a null/undefined response before accessing
response.delete; specifically, in the callback that currently calls
this.organizationService.deleteLoanOriginator(loanOriginator.id) and
this.router.navigate(...), check that response exists (e.g., response &&
response.delete or response?.delete) and only invoke
deleteLoanOriginator(loanOriginator.id) when that condition is true to avoid a
runtime TypeError.
In `@src/assets/translations/es-MX.json`:
- Line 1114: Two translation entries use "préstamos" instead of the locale's
consistent "Crédito": update the value for the key "Manage Loan Originators"
(currently "Administrar originadores de préstamos") and the other affected key
at the same locale (line referenced 3193) to replace "préstamos" with "Crédito"
(e.g., "Administrar originadores de Crédito") so the terminology matches
existing strings like "Cuentas de Crédito".
In `@src/assets/translations/fr-FR.json`:
- Line 1115: Replace mixed French terms for "Loan Originator" with a single
consistent term across the translations: use "Initiateur de prêt" (plural
"Initiateurs de prêt") and update the value for the key "Manage Loan
Originators" to "Gérer les initiateurs de prêt", then find and replace all other
occurrences (the other related strings mentioned) so every related key/value
uses "Initiateur(s) de prêt" consistently instead of "courtiers en prêts",
"Agent de prêt", or "initiateur de crédit".
In `@src/assets/translations/pt-PT.json`:
- Line 1980: The translation value for the "Loan Originator" key uses sentence
case ("Originador de empréstimo") but should match title case like the action
label; update the value for the "Loan Originator" key to "Originador de
Empréstimo" (and scan nearby related keys to ensure consistent title‑case
styling across similar labels such as other role/action keys).
- Line 1114: The translation for "Manage Loan Originators" uses "Originadores de
Empréstimos" while the existing key "Loan Originators" (key: "Loan Originators")
uses "Agentes de Crédito"; pick one term and make them consistent by updating
the "Manage Loan Originators" value (and any other related keys/descriptive
sentences referencing loan originators) to use the chosen term (e.g., change
"Manage Loan Originators" translation to "Gerir Agentes de Crédito" if you
standardize on "Agentes de Crédito"), ensuring pluralization and verb form match
other entries.
🧹 Nitpick comments (8)
src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html (1)
48-58: Add null-safe navigation for nested properties.Accessing
originatorType.nameandchannelType.namewithout optional chaining will throw a runtime error if these objects are null or undefined. Use Angular's safe navigation operator for defensive coding.♻️ Proposed fix
<div class="flex-50"> - {{ loanOriginatorData.originatorType.name }} + {{ loanOriginatorData.originatorType?.name }} </div> <div class="flex-50 mat-body-strong"> {{ 'labels.inputs.Channel Type' | translate }} </div> <div class="flex-50"> - {{ loanOriginatorData.channelType.name }} + {{ loanOriginatorData.channelType?.name }} </div>As per coding guidelines, Angular code should maintain strict type safety.
src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts (3)
86-86: Typo: "dettach" should be "detach".The method name
dettachLoanOriginatorcontains a spelling error. This also appears to propagate to the service method name on line 100.✏️ Proposed fix
- dettachLoanOriginator(loanOriginator: LoanOriginator): void { - const dettachCodeDialogRef = this.dialog.open(ConfirmationDialogComponent, { + detachLoanOriginator(loanOriginator: LoanOriginator): void { + const detachCodeDialogRef = this.dialog.open(ConfirmationDialogComponent, {Also update line 98:
- dettachCodeDialogRef.afterClosed().subscribe((response: any) => { + detachCodeDialogRef.afterClosed().subscribe((response: any) => {
78-83: Subscriptions in constructor are not cleaned up.The subscriptions to
route.parent.parent.dataandroute.parent.dataare not unsubscribed when the component is destroyed. While route data observables often complete when navigation occurs, explicitly managing these subscriptions or usingtakeUntilDestroyed()ensures proper cleanup.♻️ Proposed fix using takeUntilDestroyed
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export class LoanOriginatorsTabComponent { + private destroyRef = inject(DestroyRef); // ... constructor() { this.clientId = this.route.parent.parent.snapshot.paramMap.get('clientId'); this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId'); - this.route.parent.parent.data.subscribe((data: { loanDetailsData: any }) => { + this.route.parent.parent.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { loanDetailsData: any }) => { this.loanStatus = data.loanDetailsData.status; }); - this.route.parent.data.subscribe((data: { loanOriginatorsData: any }) => { + this.route.parent.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { loanOriginatorsData: any }) => { this.loanOriginatorsData = data.loanOriginatorsData.originators; }); }
76-77: Inconsistent null-safety on parent route access.Line 76 accesses
this.route.parent.parent.snapshotwithout optional chaining, while line 77 usesthis.route.parent?.parent?.snapshot. This inconsistency could lead to runtime errors if the route hierarchy is different than expected.♻️ Proposed fix for consistency
- this.clientId = this.route.parent.parent.snapshot.paramMap.get('clientId'); - this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId'); + this.clientId = this.route.parent?.parent?.snapshot.paramMap.get('clientId') ?? null; + this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId') ?? null;src/app/organization/loan-originators/loan-originators.component.ts (2)
86-86: Use strict typing forMatTableDataSource.Replace
anywithLoanOriginatorto maintain type safety consistent with theloanOriginatorsDataproperty.♻️ Suggested fix
/** Data source for Loan Originators table. */ - dataSource: MatTableDataSource<any>; + dataSource: MatTableDataSource<LoanOriginator>;As per coding guidelines: "For Angular code: verify ... strict type safety."
98-102: Unsubscribe from route data observable to avoid memory leaks.The subscription in the constructor should be cleaned up when the component is destroyed. Consider using
takeUntilDestroyed()or storing the subscription and unsubscribing inngOnDestroy.♻️ Suggested fix using `takeUntilDestroyed()`
+import { Component, OnInit, TemplateRef, ElementRef, ViewChild, inject, DestroyRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; ... export class LoanOriginatorsComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); + private destroyRef = inject(DestroyRef); ... constructor() { - this.route.data.subscribe((data: { loanOriginatorsData: LoanOriginator[] }) => { + this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { loanOriginatorsData: LoanOriginator[] }) => { this.loanOriginatorsData = data.loanOriginatorsData; }); }As per coding guidelines: "For Angular code: verify ... clean observable patterns."
src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts (2)
41-41: Add proper typing forloanOriginatorsTemplateData.Using
anyreduces type safety. Consider defining an interface for the template data structure.♻️ Suggested improvement
+interface LoanOriginatorTemplateData { + externalId?: string; + statusOptions: string[]; + originatorTypeOptions: CodeValue[]; + channelTypeOptions: CodeValue[]; +} + export class EditLoanOriginatorComponent implements OnInit { ... - loanOriginatorsTemplateData: any; + loanOriginatorsTemplateData: LoanOriginatorTemplateData;As per coding guidelines: "For Angular code: verify ... strict type safety."
51-59: Unsubscribe from route data observable to avoid memory leaks.Same pattern as the list component—consider using
takeUntilDestroyed()for clean observable management.♻️ Suggested fix
+import { Component, OnInit, TemplateRef, ElementRef, ViewChild, inject, DestroyRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; ... export class EditLoanOriginatorComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private organizationService = inject(OrganizationService); private route = inject(ActivatedRoute); private router = inject(Router); + private destroyRef = inject(DestroyRef); ... constructor() { - this.route.data.subscribe((data: { loanOriginatorData: LoanOriginator; loanOriginatorsTemplateData: any }) => { + this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { loanOriginatorData: LoanOriginator; loanOriginatorsTemplateData: any }) => { this.loanOriginatorsData = data.loanOriginatorData; ... }); }As per coding guidelines: "For Angular code: verify ... clean observable patterns."
| dettachCodeDialogRef.afterClosed().subscribe((response: any) => { | ||
| if (response.confirm) { | ||
| this.loansService.dettachLoanOriginator(this.loanId, String(loanOriginator.id)).subscribe((response) => { | ||
| this.reload(); | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Unsafe access to response.confirm when dialog is dismissed.
If the user dismisses the dialog by clicking outside or pressing Escape, response may be undefined, causing a runtime error when accessing response.confirm.
🐛 Proposed fix
dettachCodeDialogRef.afterClosed().subscribe((response: any) => {
- if (response.confirm) {
+ if (response?.confirm) {
this.loansService.dettachLoanOriginator(this.loanId, String(loanOriginator.id)).subscribe((response) => {
this.reload();
});
}
});📝 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.
| dettachCodeDialogRef.afterClosed().subscribe((response: any) => { | |
| if (response.confirm) { | |
| this.loansService.dettachLoanOriginator(this.loanId, String(loanOriginator.id)).subscribe((response) => { | |
| this.reload(); | |
| }); | |
| } | |
| }); | |
| dettachCodeDialogRef.afterClosed().subscribe((response: any) => { | |
| if (response?.confirm) { | |
| this.loansService.dettachLoanOriginator(this.loanId, String(loanOriginator.id)).subscribe((response) => { | |
| this.reload(); | |
| }); | |
| } | |
| }); |
🤖 Prompt for AI Agents
In
`@src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts`
around lines 98 - 104, The afterClosed subscription on dettachCodeDialogRef can
receive undefined when the dialog is dismissed, so update the handler in
loan-originators-tab.component (the dettachCodeDialogRef.afterClosed().subscribe
callback) to first check that response is truthy (e.g., if (response &&
response.confirm)) before accessing response.confirm, and only call
this.loansService.dettachLoanOriginator(this.loanId,
String(loanOriginator.id)).subscribe(...) and this.reload() when that check
passes; keep the existing dettachLoanOriginator and reload calls intact and
scoped inside the guarded branch.
| deleteCodeDialogRef.afterClosed().subscribe((response: any) => { | ||
| if (response.delete) { | ||
| this.organizationService.deleteLoanOriginator(loanOriginator.id).subscribe(() => { | ||
| this.router.navigate(['/organization/manage-loan-originators']); | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Add null check for dialog response to prevent runtime error.
If the user dismisses the dialog by clicking outside or pressing Escape, response may be undefined, causing response.delete to throw a TypeError.
🐛 Suggested fix
deleteCodeDialogRef.afterClosed().subscribe((response: any) => {
- if (response.delete) {
+ if (response?.delete) {
this.organizationService.deleteLoanOriginator(loanOriginator.id).subscribe(() => {
this.router.navigate(['/organization/manage-loan-originators']);
});
}
});🤖 Prompt for AI Agents
In `@src/app/organization/loan-originators/loan-originators.component.ts` around
lines 134 - 140, The dialog close handler may receive undefined if the user
dismisses the dialog, so update the deleteCodeDialogRef.afterClosed()
subscription to guard against a null/undefined response before accessing
response.delete; specifically, in the callback that currently calls
this.organizationService.deleteLoanOriginator(loanOriginator.id) and
this.router.navigate(...), check that response exists (e.g., response &&
response.delete or response?.delete) and only invoke
deleteLoanOriginator(loanOriginator.id) when that condition is true to avoid a
runtime TypeError.
| "Manage Holidays": "Administrar festivos", | ||
| "Manage Hooks": "Administrar ganchos", | ||
| "Manage Jobs": "Administrar procesos", | ||
| "Manage Loan Originators": "Administrar originadores de préstamos", |
There was a problem hiding this comment.
Align “Loan” terminology with existing “Crédito” usage.
Line 1114 and Line 3193 use “préstamos”, while the rest of this locale consistently uses “Crédito” (e.g., “Cuentas de Crédito”). Please standardize to avoid mixed terminology.
Suggested diff
- "Manage Loan Originators": "Administrar originadores de préstamos",
+ "Manage Loan Originators": "Administrar originadores de créditos",
...
- "Loan Originators are associated with loan accounts": "Los originadores de préstamos están asociados con cuentas de préstamos",
+ "Loan Originators are associated with loan accounts": "Los originadores de créditos están asociados con cuentas de crédito",Also applies to: 3193-3193
🤖 Prompt for AI Agents
In `@src/assets/translations/es-MX.json` at line 1114, Two translation entries use
"préstamos" instead of the locale's consistent "Crédito": update the value for
the key "Manage Loan Originators" (currently "Administrar originadores de
préstamos") and the other affected key at the same locale (line referenced 3193)
to replace "préstamos" with "Crédito" (e.g., "Administrar originadores de
Crédito") so the terminology matches existing strings like "Cuentas de Crédito".
| "Manage Holidays": "Gérer les jours fériés", | ||
| "Manage Hooks": "Gérer les hooks", | ||
| "Manage Jobs": "Gérer les travaux", | ||
| "Manage Loan Originators": "Gérer les courtiers en prêts", |
There was a problem hiding this comment.
Standardize the French term for “Loan Originator” to avoid user confusion.
These entries mix “courtiers en prêts”, “Agent de prêt”, and “initiateur de crédit”, which is inconsistent and overlaps with “Loan Officer.” Please choose one distinct term and use it consistently across all related strings.
✍️ Example consistency update (pick the final term you prefer)
- "Manage Loan Originators": "Gérer les courtiers en prêts",
+ "Manage Loan Originators": "Gérer les originateurs de prêts",
- "Loan Originator": "Agent de prêt",
+ "Loan Originator": "Originateur de prêt",
- "Attach Loan Originator": "Attacher l'initiateur de crédit",
+ "Attach Loan Originator": "Attacher l'originateur de prêt",
- "Loan Originators are associated with loan accounts": "Les courtiers en prêts sont associés aux comptes de prêt.",
+ "Loan Originators are associated with loan accounts": "Les originateurs de prêts sont associés aux comptes de prêt."Also applies to: 1981-1981, 2681-2681, 3188-3188
🤖 Prompt for AI Agents
In `@src/assets/translations/fr-FR.json` at line 1115, Replace mixed French terms
for "Loan Originator" with a single consistent term across the translations: use
"Initiateur de prêt" (plural "Initiateurs de prêt") and update the value for the
key "Manage Loan Originators" to "Gérer les initiateurs de prêt", then find and
replace all other occurrences (the other related strings mentioned) so every
related key/value uses "Initiateur(s) de prêt" consistently instead of
"courtiers en prêts", "Agent de prêt", or "initiateur de crédit".
| "Manage Holidays": "Gerenciar feriados", | ||
| "Manage Hooks": "Gerenciar ganchos", | ||
| "Manage Jobs": "Gerenciar trabalhos", | ||
| "Manage Loan Originators": "Gerenciar Originadores de Empréstimos", |
There was a problem hiding this comment.
Standardize terminology for “Loan Originator(s)”.
This new label uses “Originadores de Empréstimos”, but the existing key “Loan Originators” (Line 1092) is translated as “Agentes de Crédito”. Please choose one term and align all related keys (including this new one and the descriptive sentence) to avoid inconsistent UI wording.
🤖 Prompt for AI Agents
In `@src/assets/translations/pt-PT.json` at line 1114, The translation for "Manage
Loan Originators" uses "Originadores de Empréstimos" while the existing key
"Loan Originators" (key: "Loan Originators") uses "Agentes de Crédito"; pick one
term and make them consistent by updating the "Manage Loan Originators" value
(and any other related keys/descriptive sentences referencing loan originators)
to use the chosen term (e.g., change "Manage Loan Originators" translation to
"Gerir Agentes de Crédito" if you standardize on "Agentes de Crédito"), ensuring
pluralization and verb form match other entries.
| "Loan Documents": "Documentos de empréstimo", | ||
| "Loan Id": "ID do empréstimo", | ||
| "Loan Officer": "Oficial de Empréstimo", | ||
| "Loan Originator": "Originador de empréstimo", |
There was a problem hiding this comment.
Match title‑case styling for the label.
Here “empréstimo” is lower‑case, while the action label uses title case (“Originador de Empréstimo”). Consider aligning for UI consistency.
✅ Suggested adjustment
- "Loan Originator": "Originador de empréstimo",
+ "Loan Originator": "Originador de Empréstimo",📝 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.
| "Loan Originator": "Originador de empréstimo", | |
| "Loan Originator": "Originador de Empréstimo", |
🤖 Prompt for AI Agents
In `@src/assets/translations/pt-PT.json` at line 1980, The translation value for
the "Loan Originator" key uses sentence case ("Originador de empréstimo") but
should match title case like the action label; update the value for the "Loan
Originator" key to "Originador de Empréstimo" (and scan nearby related keys to
ensure consistent title‑case styling across similar labels such as other
role/action keys).
1b9eb51 to
5dcbdf5
Compare
There was a problem hiding this comment.
Actionable comments posted: 16
🤖 Fix all issues with AI agents
In
`@src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts`:
- Line 51: Remove the debug console.log statement that prints this.dataObject in
attach-originator.component.ts; locate the console.log(this.dataObject) call
(inside the AttachOriginatorComponent or its initialization method) and delete
it, or replace it with a proper structured logger call if runtime logging is
required (e.g., use the application's Logger service and ensure sensitive data
in dataObject is not logged).
In
`@src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.html`:
- Around line 52-60: The button shown when loanStatus.pendingApproval has
inconsistent tooltip text—title uses "Dettach" and matTooltip uses "Delete"—so
remove one tooltip attribute and standardize the text: fix the spelling to
"Detach" and use the same translate key for the remaining tooltip (e.g., replace
title and matTooltip with a single matTooltip="{{ 'tooltips.Detach' | translate
}}" on the button that also keeps the
*mifosxHasPermission="'DETTACH_LOAN_ORIGINATOR'" attribute). Ensure only one
tooltip attribute remains and that it references the consistent translate key.
In
`@src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts`:
- Line 86: The method name dettachLoanOriginator is misspelled and the inline
comment contains "fot" — rename dettachLoanOriginator to detachLoanOriginator in
the component and update all callers/usages (including
LoansService.dettachLoanOriginator if it exists) to the new name to keep
consistency, and fix the comment text "fot" to "for"; search for
"dettachLoanOriginator" across the codebase and refactor both definition and
references to avoid breakage.
- Around line 110-115: The reload() method can navigate to
`/clients/null/loans-accounts` when this.clientId is null; add a guard in the
reload() function to check that this.clientId is non-null/undefined (and
non-empty if desired) before calling
router.navigateByUrl(`/clients/${this.clientId}/loans-accounts`, ...); if
clientId is missing, either skip the navigation or use a safe fallback route
(e.g., the parent clients list), and ensure subsequent router.navigate([url]) is
only invoked when the first navigation succeeded.
- Around line 75-83: The constructor uses this.route.parent.parent without
null-safety causing potential crashes; update it to consistently guard
route.parent and route.parent.parent (e.g., read const grandParent =
this.route.parent?.parent and const parent = this.route.parent) and then use
grandParent?.snapshot?.paramMap.get('clientId') and
grandParent?.snapshot?.paramMap.get('loanId') or bail out if grandParent is
undefined; likewise subscribe only when parent or grandParent exists (use
parent?.data.subscribe(...) and grandParent?.data.subscribe(...) or an if-check)
so that clientId, loanId, loanStatus and loanOriginatorsData assignments and
subscriptions (refer to constructor, loanStatus, loanOriginatorsData, clientId,
loanId) are guarded against null parents.
In `@src/app/loans/loans.service.ts`:
- Around line 661-669: The DELETE call in dettachLoanOriginator currently passes
an unnecessary empty options/body object; remove the emptyBody constant and call
this.http.delete(`/loans/${loanId}/originators/${originatorId}`) with no second
argument so it matches other methods like deleteGuarantor and deleteLoanNote in
the service (update the dettachLoanOriginator function accordingly).
In
`@src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.ts`:
- Around line 74-79: The Validators.pattern on the form control "name" is using
the incorrect regex '(^[A-z]).*' which matches non-letter ASCII characters;
update the pattern used in the Validators.pattern call in
create-loan-originator.component.ts (the "name" control definition) to use
'^[A-Za-z].*' (or equivalent) so it only allows names that start with an ASCII
letter; keep the existing Validators.required and overall validator array
intact.
- Line 10: Remove the unused ViewChild property named
templateCreateLoanOriginatorForm from the component: delete the declaration
decorated with `@ViewChild`('templateCreateLoanOriginatorForm') and its associated
property (templateCreateLoanOriginatorForm: TemplateRef<any> | undefined) so the
component no longer contains an unused member; keep the other ViewChild,
ElementRef and TemplateRef usages/imports intact since they are referenced
elsewhere.
In
`@src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts`:
- Around line 20-21: The file-level JSDoc comment is a leftover saying "Create
Loan Originator component" but this file implements the Edit component; update
the top comment to correctly describe the component (e.g., "Edit Loan Originator
component" or remove the incorrect doc comment) so it matches the
EditLoanOriginatorComponent (or the exported class/component name) in
edit-loan-originator.component.ts.
In
`@src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.ts`:
- Around line 19-27: The `@Component` decorator for ViewLoanOriginatorComponent
declares an imports array (FaIconComponent and STANDALONE_SHARED_IMPORTS) which
only works for standalone components; add standalone: true to the component
metadata so Angular treats ViewLoanOriginatorComponent as standalone and the
imports are applied correctly (update the `@Component` config for the class
declared in view-loan-originator.component.ts).
- Around line 28-41: The component currently subscribes to this.route.data in
the constructor without teardown and declares loanOriginatorData uninitialized;
change loanOriginatorData's type to LoanOriginator | null and initialize it to
null, then pipe the route.data subscription through takeUntilDestroyed() (e.g.,
this.route.data.pipe(takeUntilDestroyed()).subscribe(...)) so the subscription
is automatically cleaned up; update the constructor subscription code using
takeUntilDestroyed() and adjust any template usage to handle the nullable
loanOriginatorData.
In `@src/assets/translations/cs-CS.json`:
- Line 2682: The Czech translations use both "půjčka"/"půjček" and
"úvěr"/"úvěrů" for "Loan" which is inconsistent; pick one term (recommend
"úvěr") and update the translation values for the affected keys such as "Attach
Loan Originator" (currently "Připojit poskytovatele půjčky"), the occurrences
translated as "Poskytovatel úvěrů" and "Poskytovatelé půjček" so they all use
the same form (e.g., "Připojit poskytovatele úvěru", "Poskytovatel úvěrů",
"Poskytovatelé úvěrů"), then run a quick grep across the translations file to
ensure no other "půjčka"/"půjček" remain for loan-originator related keys.
In `@src/assets/translations/ko-KO.json`:
- Line 1982: The translation for the key "Loan Originator" currently duplicates
"Loan Officer" ("대출 담당자"); update the "Loan Originator" value to a distinct
Korean term (e.g., "대출 개시자" or "대출 발행기관") to reflect that originators are
organizational entities, and apply the same change for every occurrence of the
"Loan Originator" key in this file (also present near the other occurrences
noted: the entries at/from lines referenced as 1115, 2682, and 3189) so all
instances consistently use the new distinct translation while leaving "Loan
Officer" ("대출 담당자") unchanged.
In `@src/assets/translations/lv-LV.json`:
- Around line 3773-3775: Update the Latvian translation value for the "INACTIVE"
key from "NEAKTIVS" to "NEAKTĪVS" so it matches the diacritic used in "AKTĪVS";
locate the "INACTIVE" JSON entry and replace the string, ensuring the file
remains UTF-8 encoded to preserve the Ī character.
In `@src/assets/translations/ne-NE.json`:
- Line 1979: Replace the inconsistent Nepali translation for the "Loan
Originator" key in the translations file: change the value "ऋण प्रवर्तक" to
match the other originator-related keys by using "ऋण सुरुवातकर्ता" so the UI
uses the same Nepali term for "Originator" across all keys (i.e., update the
translation value for the "Loan Originator" string to "ऋण सुरुवातकर्ता").
- Around line 3770-3773: Remove the unused tooltip translation keys "ACTIVE",
"PENDING", and "INACTIVE" from the tooltips section in the ne-NE JSON (the
entries with keys ACTIVE, PENDING, INACTIVE); delete those three key/value
lines, then run a quick repo-wide search for usages of tooltips.ACTIVE,
tooltips.PENDING, and tooltips.INACTIVE to confirm they aren't referenced and,
if you have an i18n extraction/lint step, run it to ensure no missing-key errors
are introduced.
🧹 Nitpick comments (7)
src/assets/translations/cs-CS.json (1)
3772-3775: Status enum translations placed in the wrong section.
ACTIVE,PENDING, andINACTIVEare status labels, not tooltip explanations. Thetooltipssection contains descriptive/explanatory text (e.g., "A borrower loan counter (cycle) ..."). These status values would be better placed inlabels.catalogs(which already contains similar enum-like values) orlabels.statusfor consistency with the file's organizational convention.Also note that
"Create Interest Refund"on Line 3772 appears unrelated to the loan originator feature—verify this was intentionally included in this PR.src/app/organization/organization.service.ts (1)
838-874: Type Loan Originator endpoints with domain models.Returning
Observable<any>(and acceptinganypayloads) weakens compile-time checks for new APIs. Consider using theLoanOriginatormodel (and DTOs/Partial types) for stricter typing. As per coding guidelines, verify component separation, trackBy on *ngFor, strict type safety, and clean observable patterns.♻️ Suggested refactor
+import { LoanOriginator } from 'app/loans/models/loan-account.model'; ... - getLoanOriginators(): Observable<any> { + getLoanOriginators(): Observable<LoanOriginator[]> { return this.http.get('/loan-originators'); } ... - getLoanOriginator(originatorId: string): Observable<any> { + getLoanOriginator(originatorId: string): Observable<LoanOriginator> { return this.http.get(`/loan-originators/${originatorId}`); } ... - createLoanOriginator(payload: any): Observable<any> { + createLoanOriginator(payload: Partial<LoanOriginator>): Observable<LoanOriginator> { return this.http.post(`/loan-originators`, payload); } ... - updateLoanOriginator(originatorId: number, payload: any): Observable<any> { + updateLoanOriginator(originatorId: number, payload: Partial<LoanOriginator>): Observable<LoanOriginator> { return this.http.put(`/loan-originators/${originatorId}`, payload); } ... - deleteLoanOriginator(originatorId: number): Observable<any> { + deleteLoanOriginator(originatorId: number): Observable<void> { return this.http.delete(`/loan-originators/${originatorId}`); }src/app/organization/loan-originators/loan-originators.component.ts (1)
87-128: Strongly type the table data source.Use
MatTableDataSource<LoanOriginator>to keep filtering/sorting and column bindings type-safe. As per coding guidelines, verify component separation, trackBy on *ngFor, strict type safety, and clean observable patterns.♻️ Suggested refactor
- dataSource: MatTableDataSource<any>; + dataSource: MatTableDataSource<LoanOriginator>; ... - this.dataSource = new MatTableDataSource(this.loanOriginatorsData); + this.dataSource = new MatTableDataSource<LoanOriginator>(this.loanOriginatorsData);src/app/organization/loan-originators/loan-originators.resolver.ts (1)
22-36: Tighten resolver typing (avoidObservable<any>).Consider implementing
Resolve<LoanOriginator | LoanOriginator[]>and returning a typed observable to align with strict typing.As per coding guidelines, "For Angular code: verify component separation, trackBy on *ngFor, strict type safety, and clean observable patterns."🔧 Example adjustment
-import { Injectable, inject } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; +import { LoanOriginator } from 'app/loans/models/loan-account.model'; ... -import { ActivatedRouteSnapshot } from '@angular/router'; ... -export class LoanOriginatorsResolver { +export class LoanOriginatorsResolver implements Resolve<LoanOriginator | LoanOriginator[]> { private organizationService = inject(OrganizationService); ... - resolve(route: ActivatedRouteSnapshot): Observable<any> { + resolve(route: ActivatedRouteSnapshot): Observable<LoanOriginator | LoanOriginator[]> { const originatorId = route.paramMap.get('id'); if (originatorId) { return this.organizationService.getLoanOriginator(originatorId); } else { return this.organizationService.getLoanOriginators(); } } }src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts (1)
49-57: Guard againstdataObjectbeing null/undefined and preferArray.filter.If the parent component doesn't pass
dataObjector the resolver data is missing,this.dataObject.forEach(...)will throw a runtime error. Additionally,.filter()is more idiomatic than manual push.♻️ Proposed fix
ngOnInit() { this.setAttachLoanOriginatorForm(); - console.log(this.dataObject); - this.loanOriginators = []; - this.dataObject.forEach((loanOriginator: LoanOriginator) => { - if (loanOriginator.status === 'ACTIVE') { - this.loanOriginators.push(loanOriginator); - } - }); + this.loanOriginators = (this.dataObject ?? []).filter( + (lo: LoanOriginator) => lo.status === 'ACTIVE' + ); }src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.ts (1)
98-106: No error handling oncreateLoanOriginatorsubscription.If the API call fails, the user receives no feedback. Consider adding an error handler or using a global error interceptor pattern.
src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts (1)
103-106: Thedelete loanOriginatorFormData.externalIdis a no-op — disabled controls are excluded fromFormGroup.value.Since
externalIdis disabled (Line 76),this.loanOriginatorForm.valuealready omits it. The delete statement is redundant. Not harmful, but can be cleaned up for clarity.
|
|
||
| ngOnInit() { | ||
| this.setAttachLoanOriginatorForm(); | ||
| console.log(this.dataObject); |
There was a problem hiding this comment.
Remove console.log debug statement.
This debug log should not ship to production.
🧹 Proposed fix
ngOnInit() {
this.setAttachLoanOriginatorForm();
- console.log(this.dataObject);
this.loanOriginators = [];📝 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.
| console.log(this.dataObject); | |
| ngOnInit() { | |
| this.setAttachLoanOriginatorForm(); | |
| this.loanOriginators = []; |
🤖 Prompt for AI Agents
In
`@src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts`
at line 51, Remove the debug console.log statement that prints this.dataObject
in attach-originator.component.ts; locate the console.log(this.dataObject) call
(inside the AttachOriginatorComponent or its initialization method) and delete
it, or replace it with a proper structured logger call if runtime logging is
required (e.g., use the application's Logger service and ensure sensitive data
in dataObject is not logged).
| @if (loanStatus.pendingApproval) { | ||
| <button | ||
| type="button" | ||
| color="warn" | ||
| title="{{ 'tooltips.Dettach' | translate }}" | ||
| mat-icon-button | ||
| matTooltip="{{ 'tooltips.Delete' | translate }}" | ||
| *mifosxHasPermission="'DETTACH_LOAN_ORIGINATOR'" | ||
| matTooltipPosition="above" |
There was a problem hiding this comment.
Use a single tooltip message for the detach action.
title shows “Dettach” while matTooltip shows “Delete,” which is confusing for a detach action. Keep one, consistent tooltip string.
💡 Suggested fix
- title="{{ 'tooltips.Dettach' | translate }}"
- matTooltip="{{ 'tooltips.Delete' | translate }}"
+ matTooltip="{{ 'tooltips.Dettach' | translate }}"📝 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.
| @if (loanStatus.pendingApproval) { | |
| <button | |
| type="button" | |
| color="warn" | |
| title="{{ 'tooltips.Dettach' | translate }}" | |
| mat-icon-button | |
| matTooltip="{{ 'tooltips.Delete' | translate }}" | |
| *mifosxHasPermission="'DETTACH_LOAN_ORIGINATOR'" | |
| matTooltipPosition="above" | |
| `@if` (loanStatus.pendingApproval) { | |
| <button | |
| type="button" | |
| color="warn" | |
| mat-icon-button | |
| matTooltip="{{ 'tooltips.Dettach' | translate }}" | |
| *mifosxHasPermission="'DETTACH_LOAN_ORIGINATOR'" | |
| matTooltipPosition="above" |
🤖 Prompt for AI Agents
In
`@src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.html`
around lines 52 - 60, The button shown when loanStatus.pendingApproval has
inconsistent tooltip text—title uses "Dettach" and matTooltip uses "Delete"—so
remove one tooltip attribute and standardize the text: fix the spelling to
"Detach" and use the same translate key for the remaining tooltip (e.g., replace
title and matTooltip with a single matTooltip="{{ 'tooltips.Detach' | translate
}}" on the button that also keeps the
*mifosxHasPermission="'DETTACH_LOAN_ORIGINATOR'" attribute). Ensure only one
tooltip attribute remains and that it references the consistent translate key.
| constructor() { | ||
| const loanIdParam = this.route.parent?.parent?.snapshot.paramMap.get('loanId'); | ||
| this.loanId = loanIdParam ? Number(loanIdParam) : null; | ||
| this.clientId = this.route.parent.parent.snapshot.paramMap.get('clientId'); | ||
| this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId'); | ||
| this.route.parent.parent.data.subscribe((data: { loanDetailsData: any }) => { | ||
| this.loanStatus = data.loanDetailsData.status; | ||
| }); | ||
| this.route.parent.data.subscribe((data: { loanOriginatorsData: any }) => { | ||
| this.loanOriginatorsData = data.loanOriginatorsData.originators; | ||
| }); |
There was a problem hiding this comment.
Inconsistent null safety on route.parent access.
Line 76 accesses this.route.parent.parent.snapshot without optional chaining, while Line 77 uses this.route.parent?.parent?.snapshot. Line 78 also lacks the guard. If parent is ever null, Line 76 and 78 will throw.
🐛 Proposed fix
- this.clientId = this.route.parent.parent.snapshot.paramMap.get('clientId');
- this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId');
- this.route.parent.parent.data.subscribe((data: { loanDetailsData: any }) => {
+ this.clientId = this.route.parent?.parent?.snapshot.paramMap.get('clientId') ?? null;
+ this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId') ?? null;
+ this.route.parent?.parent?.data.subscribe((data: { loanDetailsData: any }) => {
this.loanStatus = data.loanDetailsData.status;
});📝 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.
| constructor() { | |
| const loanIdParam = this.route.parent?.parent?.snapshot.paramMap.get('loanId'); | |
| this.loanId = loanIdParam ? Number(loanIdParam) : null; | |
| this.clientId = this.route.parent.parent.snapshot.paramMap.get('clientId'); | |
| this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId'); | |
| this.route.parent.parent.data.subscribe((data: { loanDetailsData: any }) => { | |
| this.loanStatus = data.loanDetailsData.status; | |
| }); | |
| this.route.parent.data.subscribe((data: { loanOriginatorsData: any }) => { | |
| this.loanOriginatorsData = data.loanOriginatorsData.originators; | |
| }); | |
| constructor() { | |
| this.clientId = this.route.parent?.parent?.snapshot.paramMap.get('clientId') ?? null; | |
| this.loanId = this.route.parent?.parent?.snapshot.paramMap.get('loanId') ?? null; | |
| this.route.parent?.parent?.data.subscribe((data: { loanDetailsData: any }) => { | |
| this.loanStatus = data.loanDetailsData.status; | |
| }); | |
| this.route.parent.data.subscribe((data: { loanOriginatorsData: any }) => { | |
| this.loanOriginatorsData = data.loanOriginatorsData.originators; | |
| }); |
🤖 Prompt for AI Agents
In
`@src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts`
around lines 75 - 83, The constructor uses this.route.parent.parent without
null-safety causing potential crashes; update it to consistently guard
route.parent and route.parent.parent (e.g., read const grandParent =
this.route.parent?.parent and const parent = this.route.parent) and then use
grandParent?.snapshot?.paramMap.get('clientId') and
grandParent?.snapshot?.paramMap.get('loanId') or bail out if grandParent is
undefined; likewise subscribe only when parent or grandParent exists (use
parent?.data.subscribe(...) and grandParent?.data.subscribe(...) or an if-check)
so that clientId, loanId, loanStatus and loanOriginatorsData assignments and
subscriptions (refer to constructor, loanStatus, loanOriginatorsData, clientId,
loanId) are guarded against null parents.
| }); | ||
| } | ||
|
|
||
| dettachLoanOriginator(loanOriginator: LoanOriginator): void { |
There was a problem hiding this comment.
Typos: dettach → detach, fot → for.
"dettach" is misspelled in the method name (Line 86) and the comment on Line 108 has "fot" instead of "for". The method name typo may need to stay consistent with the LoansService.dettachLoanOriginator method name — if so, consider fixing the service method name too.
#!/bin/bash
# Check if the typo "dettach" is in the service definition as well
rg -n "dettach" --type=ts -C2Also applies to: 108-108
🤖 Prompt for AI Agents
In
`@src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts`
at line 86, The method name dettachLoanOriginator is misspelled and the inline
comment contains "fot" — rename dettachLoanOriginator to detachLoanOriginator in
the component and update all callers/usages (including
LoansService.dettachLoanOriginator if it exists) to the new name to keep
consistency, and fix the comment text "fot" to "for"; search for
"dettachLoanOriginator" across the codebase and refactor both definition and
references to avoid breakage.
| private reload() { | ||
| const url: string = this.router.url; | ||
| this.router | ||
| .navigateByUrl(`/clients/${this.clientId}/loans-accounts`, { skipLocationChange: true }) | ||
| .then(() => this.router.navigate([url])); | ||
| } |
There was a problem hiding this comment.
reload() will produce a broken URL if clientId is null.
If clientId was not resolved from the route, the navigation will go to /clients/null/loans-accounts. Add a guard.
🛡️ Proposed fix
private reload() {
+ if (!this.clientId) {
+ return;
+ }
const url: string = this.router.url;
this.router
.navigateByUrl(`/clients/${this.clientId}/loans-accounts`, { skipLocationChange: true })
.then(() => this.router.navigate([url]));
}📝 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.
| private reload() { | |
| const url: string = this.router.url; | |
| this.router | |
| .navigateByUrl(`/clients/${this.clientId}/loans-accounts`, { skipLocationChange: true }) | |
| .then(() => this.router.navigate([url])); | |
| } | |
| private reload() { | |
| if (!this.clientId) { | |
| return; | |
| } | |
| const url: string = this.router.url; | |
| this.router | |
| .navigateByUrl(`/clients/${this.clientId}/loans-accounts`, { skipLocationChange: true }) | |
| .then(() => this.router.navigate([url])); | |
| } |
🤖 Prompt for AI Agents
In
`@src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts`
around lines 110 - 115, The reload() method can navigate to
`/clients/null/loans-accounts` when this.clientId is null; add a guard in the
reload() function to check that this.clientId is non-null/undefined (and
non-empty if desired) before calling
router.navigateByUrl(`/clients/${this.clientId}/loans-accounts`, ...); if
clientId is missing, either skip the navigation or use a safe fallback route
(e.g., the parent clients list), and ensure subsequent router.navigate([url]) is
only invoked when the first navigation succeeded.
| "Application Menu": "Nabídka aplikací", | ||
| "Approve": "Schvalovat", | ||
| "Assign Loan Officer": "Přiřadit úvěrového úředníka", | ||
| "Attach Loan Originator": "Připojit poskytovatele půjčky", |
There was a problem hiding this comment.
Inconsistent Czech terminology for "Loan" across the new translations.
Lines 1114 and 1980 translate "Loan" as "úvěr" (e.g., "Poskytovatel úvěrů"), while Line 2682 uses "půjčky" ("Připojit poskytovatele půjčky") and Line 3190 uses "půjček" ("Poskytovatelé půjček"). Consider unifying on a single term—either "úvěr" or "půjčka"—for all loan originator translations to avoid confusing Czech-speaking users.
Also applies to: 3190-3190
🤖 Prompt for AI Agents
In `@src/assets/translations/cs-CS.json` at line 2682, The Czech translations use
both "půjčka"/"půjček" and "úvěr"/"úvěrů" for "Loan" which is inconsistent; pick
one term (recommend "úvěr") and update the translation values for the affected
keys such as "Attach Loan Originator" (currently "Připojit poskytovatele
půjčky"), the occurrences translated as "Poskytovatel úvěrů" and "Poskytovatelé
půjček" so they all use the same form (e.g., "Připojit poskytovatele úvěru",
"Poskytovatel úvěrů", "Poskytovatelé úvěrů"), then run a quick grep across the
translations file to ensure no other "půjčka"/"půjček" remain for
loan-originator related keys.
| "Loan Documents": "대출 서류", | ||
| "Loan Id": "대출 ID", | ||
| "Loan Officer": "대출 담당자", | ||
| "Loan Originator": "대출 담당자", |
There was a problem hiding this comment.
"Loan Originator" and "Loan Officer" share the same Korean translation "대출 담당자".
On line 1981, "Loan Officer" is already translated as "대출 담당자". Using the identical translation for "Loan Originator" will confuse Korean-speaking users, especially since the PR screenshot shows originators are organizations (e.g., "Langosh Group") with types like AFFILIATE/PLATFORM—fundamentally different from a loan officer (a person).
Consider a distinct term such as "대출 개시자" (loan initiator/originator) or "대출 발행기관" (loan-issuing entity) to differentiate the two concepts. This same change should propagate to lines 1115, 2682, and 3189.
🤖 Prompt for AI Agents
In `@src/assets/translations/ko-KO.json` at line 1982, The translation for the key
"Loan Originator" currently duplicates "Loan Officer" ("대출 담당자"); update the
"Loan Originator" value to a distinct Korean term (e.g., "대출 개시자" or "대출 발행기관")
to reflect that originators are organizational entities, and apply the same
change for every occurrence of the "Loan Originator" key in this file (also
present near the other occurrences noted: the entries at/from lines referenced
as 1115, 2682, and 3189) so all instances consistently use the new distinct
translation while leaving "Loan Officer" ("대출 담당자") unchanged.
| "ACTIVE": "AKTĪVS", | ||
| "PENDING": "NEAPSTRĀDĀTS", | ||
| "INACTIVE": "NEAKTIVS" |
There was a problem hiding this comment.
Fix diacritic in Latvian translation for INACTIVE.
“NEAKTIVS” is missing the long ī, while “AKTĪVS” uses it. Align for consistency.
✏️ Suggested fix
- "INACTIVE": "NEAKTIVS"
+ "INACTIVE": "NEAKTĪVS"📝 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.
| "ACTIVE": "AKTĪVS", | |
| "PENDING": "NEAPSTRĀDĀTS", | |
| "INACTIVE": "NEAKTIVS" | |
| "ACTIVE": "AKTĪVS", | |
| "PENDING": "NEAPSTRĀDĀTS", | |
| "INACTIVE": "NEAKTĪVS" |
🤖 Prompt for AI Agents
In `@src/assets/translations/lv-LV.json` around lines 3773 - 3775, Update the
Latvian translation value for the "INACTIVE" key from "NEAKTIVS" to "NEAKTĪVS"
so it matches the diacritic used in "AKTĪVS"; locate the "INACTIVE" JSON entry
and replace the string, ensuring the file remains UTF-8 encoded to preserve the
Ī character.
| "Loan Documents": "ऋण कागजातहरू", | ||
| "Loan Id": "ऋण आईडी", | ||
| "Loan Officer": "ऋण अधिकारी", | ||
| "Loan Originator": "ऋण प्रवर्तक", |
There was a problem hiding this comment.
Inconsistent Nepali translation for "Originator".
Line 1979 translates "Loan Originator" as "ऋण प्रवर्तक", while all other originator-related keys (Lines 1091, 1113, 2679, 3187) consistently use "सुरुवातकर्ता". This inconsistency will surface in the UI — a user could see two different Nepali words for the same concept on different screens.
Proposed fix: align with the other keys
- "Loan Originator": "ऋण प्रवर्तक",
+ "Loan Originator": "ऋण सुरुवातकर्ता",📝 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.
| "Loan Originator": "ऋण प्रवर्तक", | |
| "Loan Originator": "ऋण सुरुवातकर्ता", |
🤖 Prompt for AI Agents
In `@src/assets/translations/ne-NE.json` at line 1979, Replace the inconsistent
Nepali translation for the "Loan Originator" key in the translations file:
change the value "ऋण प्रवर्तक" to match the other originator-related keys by
using "ऋण सुरुवातकर्ता" so the UI uses the same Nepali term for "Originator"
across all keys (i.e., update the translation value for the "Loan Originator"
string to "ऋण सुरुवातकर्ता").
| "Create Interest Refund": "ब्याज फिर्ता सिर्जना गर्नुहोस्", | ||
| "ACTIVE": "सक्रिय", | ||
| "PENDING": "पेन्डिङ", | ||
| "INACTIVE": "निष्क्रिय" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for references to tooltips.ACTIVE, tooltips.PENDING, tooltips.INACTIVE in component code
echo "=== Searching for tooltips.ACTIVE references ==="
rg "tooltips\s*\.\s*ACTIVE|'tooltips\.ACTIVE'|\"tooltips\.ACTIVE\"" --type=ts --type=tsx --type=js --type=jsx -C2 2>/dev/null | head -50
echo ""
echo "=== Searching for tooltips.PENDING references ==="
rg "tooltips\s*\.\s*PENDING|'tooltips\.PENDING'|\"tooltips\.PENDING\"" --type=ts --type=tsx --type=js --type=jsx -C2 2>/dev/null | head -50
echo ""
echo "=== Searching for tooltips.INACTIVE references ==="
rg "tooltips\s*\.\s*INACTIVE|'tooltips\.INACTIVE'|\"tooltips\.INACTIVE\"" --type=ts --type=tsx --type=js --type=jsx -C2 2>/dev/null | head -50
echo ""
echo "=== Broader search: translate() with ACTIVE/PENDING/INACTIVE in tooltips context ==="
rg "translate.*tooltips" --type=ts --type=tsx --type=js --type=jsx -C1 2>/dev/null | grep -E "(ACTIVE|PENDING|INACTIVE)" | head -20Repository: openMF/web-app
Length of output: 295
🏁 Script executed:
#!/bin/bash
# Check if these keys are used anywhere at all
echo "=== Searching for any translate() calls with 'ACTIVE' ==="
rg "translate.*ACTIVE" --type=ts --type=tsx --type=js --type=jsx -C1 2>/dev/null | head -30
echo ""
echo "=== Searching for any translate() calls with 'PENDING' ==="
rg "translate.*PENDING" --type=ts --type=tsx --type=js --type=jsx -C1 2>/dev/null | head -30
echo ""
echo "=== Examine the JSON structure around lines 3770-3773 ==="
sed -n '3750,3780p' src/assets/translations/ne-NE.json | cat -n
echo ""
echo "=== Check if these keys exist in the default English translation ==="
rg -n "ACTIVE|PENDING|INACTIVE" src/assets/translations/en.json | head -20Repository: openMF/web-app
Length of output: 2579
🏁 Script executed:
#!/bin/bash
# Find the English translation file
echo "=== List translation files ==="
ls -la src/assets/translations/ | head -20
echo ""
echo "=== Search for ACTIVE/PENDING/INACTIVE in all translation files ==="
rg -l "ACTIVE|PENDING|INACTIVE" src/assets/translations/ | head -10
echo ""
echo "=== Check if these keys are in the English translation and where ==="
for file in src/assets/translations/en*.json; do
if [ -f "$file" ]; then
echo "=== File: $(basename $file) ==="
rg -n "\"(ACTIVE|PENDING|INACTIVE)\"" "$file" -A 0 -B 2 | head -30
fi
doneRepository: openMF/web-app
Length of output: 1844
🏁 Script executed:
#!/bin/bash
# More thorough search for any usage of these keys
echo "=== Search for 'ACTIVE' in component code (any context) ==="
rg -n "ACTIVE" --type=ts --type=tsx --type=js --type=jsx src/ -i 2>/dev/null | grep -v "node_modules" | head -20
echo ""
echo "=== Check what section these keys should logically belong to ==="
echo "Looking for other status definitions in en-US.json:"
rg -n "labels\.status|labels\.catalogs" src/assets/translations/en-US.json | head -10
echo ""
echo "=== Get context around the tooltips section in en-US.json ==="
grep -n "\"tooltips\"" src/assets/translations/en-US.json | head -3Repository: openMF/web-app
Length of output: 319
Remove unused status constants "ACTIVE" / "PENDING" / "INACTIVE" from tooltips section.
These translation keys (lines 3770–3773) are not referenced anywhere in the codebase—no component uses tooltips.ACTIVE, tooltips.PENDING, or tooltips.INACTIVE. The same keys exist identically in en-US.json and across multiple language files, suggesting they were added but never integrated. If they are not actively used, they should be removed to keep the translation file clean and maintainable.
🤖 Prompt for AI Agents
In `@src/assets/translations/ne-NE.json` around lines 3770 - 3773, Remove the
unused tooltip translation keys "ACTIVE", "PENDING", and "INACTIVE" from the
tooltips section in the ne-NE JSON (the entries with keys ACTIVE, PENDING,
INACTIVE); delete those three key/value lines, then run a quick repo-wide search
for usages of tooltips.ACTIVE, tooltips.PENDING, and tooltips.INACTIVE to
confirm they aren't referenced and, if you have an i18n extraction/lint step,
run it to ensure no missing-key errors are introduced.
Description
Add Loan Originators management to Create / Modify / Delete originator details
WEB-642
Screenshots, if any
Screen.Recording.2026-02-05.at.5.45.25.p.m.mov
Checklist
Please make sure these boxes are checked before submitting your pull request - thanks!
If you have multiple commits please combine them into one commit by squashing them.
Read and understood the contribution guidelines at
web-app/.github/CONTRIBUTING.md.Summary by CodeRabbit
New Features
Improvements
Localization