diff --git a/src/app/loans/common-resolvers/loan-action-button.resolver.ts b/src/app/loans/common-resolvers/loan-action-button.resolver.ts index 0d1ae081c..e67a7edfd 100644 --- a/src/app/loans/common-resolvers/loan-action-button.resolver.ts +++ b/src/app/loans/common-resolvers/loan-action-button.resolver.ts @@ -15,6 +15,7 @@ import { Observable } from 'rxjs'; /** Custom Services */ import { LoansService } from '../loans.service'; +import { OrganizationService } from 'app/organization/organization.service'; /** * Loans notes data resolver. @@ -22,6 +23,7 @@ import { LoansService } from '../loans.service'; @Injectable() export class LoanActionButtonResolver { private loansService = inject(LoansService); + private organizationService = inject(OrganizationService); /** * Returns the Loans Notes Data. @@ -88,6 +90,8 @@ export class LoanActionButtonResolver { return this.loansService.getLoanActionTemplate(loanId, 'reAge'); } else if (loanActionButton === 'Re-Amortize') { return this.loansService.getLoanActionTemplate(loanId, 'reAmortization'); + } else if (loanActionButton === 'Attach Loan Originator') { + return this.organizationService.getLoanOriginators(); } else { return undefined; } diff --git a/src/app/loans/loans-view/loan-account-actions/approve-loan/approve-loan.component.html b/src/app/loans/loans-view/loan-account-actions/approve-loan/approve-loan.component.html index 78f0c721f..b9cac1b3e 100644 --- a/src/app/loans/loans-view/loan-account-actions/approve-loan/approve-loan.component.html +++ b/src/app/loans/loans-view/loan-account-actions/approve-loan/approve-loan.component.html @@ -56,17 +56,6 @@ - - {{ 'labels.inputs.Transaction Amount' | translate }} - - @if (approveLoanForm.controls.approvedLoanAmount.hasError('required')) { - - {{ 'labels.inputs.Transaction Amount' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - {{ 'labels.inputs.Note' | translate }} diff --git a/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.html b/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.html new file mode 100644 index 000000000..e91c57e82 --- /dev/null +++ b/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.html @@ -0,0 +1,42 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Loan Originator' | translate }} + + @for (loanOriginator of loanOriginators; track loanOriginator) { + + {{ loanOriginator.name }} + + } + + +
+ + + + + +
+
+
+
diff --git a/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.scss b/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.scss @@ -0,0 +1,11 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts b/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts new file mode 100644 index 000000000..76977edfd --- /dev/null +++ b/src/app/loans/loans-view/loan-account-actions/attach-originator/attach-originator.component.ts @@ -0,0 +1,81 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports. */ +import { Component, Input, OnInit, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +/** Custom Services. */ +import { LoansService } from 'app/loans/loans.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { LoanOriginator } from 'app/loans/models/loan-account.model'; + +/** + * Attach Loan Originator component. + */ +@Component({ + selector: 'mifosx-attach-originator', + templateUrl: './attach-originator.component.html', + styleUrl: './attach-originator.component.scss', + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class AttachOriginatorComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private route = inject(ActivatedRoute); + private loanService = inject(LoansService); + private router = inject(Router); + + /** Attach Loan Originator Loan form. */ + attachLoanOriginatorForm: UntypedFormGroup; + /** Loan data. */ + loanOriginators: LoanOriginator[] = []; + @Input() dataObject: any; + + /** Loan Id */ + loanId: string | null = null; + + constructor() { + this.loanId = this.route.snapshot.params['loanId']; + } + + ngOnInit() { + this.setAttachLoanOriginatorForm(); + console.log(this.dataObject); + this.loanOriginators = []; + this.dataObject.forEach((loanOriginator: LoanOriginator) => { + if (loanOriginator.status === 'ACTIVE') { + this.loanOriginators.push(loanOriginator); + } + }); + } + + /** + * Set Attach Loan Originator form. + */ + setAttachLoanOriginatorForm() { + this.attachLoanOriginatorForm = this.formBuilder.group({ + originatorId: [ + '', + Validators.required + ] + }); + } + + /** + * Submits Approve form. + */ + submit() { + const approveLoanFormData = this.attachLoanOriginatorForm.value; + this.loanService.attachLoanOriginator(this.loanId, approveLoanFormData.originatorId).subscribe((response: any) => { + this.router.navigate(['../../general'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.html b/src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.html index 2532bc139..13ae992c8 100644 --- a/src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.html +++ b/src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.html @@ -114,3 +114,6 @@ @if (actions['Undo Write-off']) { } +@if (actions['Attach Loan Originator']) { + +} diff --git a/src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.ts b/src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.ts index b8a9db1d9..6c4158ec9 100644 --- a/src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.ts +++ b/src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.ts @@ -40,6 +40,7 @@ import { LoanReamortizeComponent } from './loan-reamortize/loan-reamortize.compo import { AddInterestPauseComponent } from './add-interest-pause/add-interest-pause.component'; import { UndoWriteOffComponent } from './undo-write-off/undo-write-off.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { AttachOriginatorComponent } from './attach-originator/attach-originator.component'; /** * Loan Account Actions component. @@ -79,7 +80,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; LoanReagingComponent, LoanReamortizeComponent, AddInterestPauseComponent, - UndoWriteOffComponent + UndoWriteOffComponent, + AttachOriginatorComponent ] }) export class LoanAccountActionsComponent { @@ -130,6 +132,7 @@ export class LoanAccountActionsComponent { 'Contract Termination': boolean; 'Buy Down Fee': boolean; 'Undo Write-off': boolean; + 'Attach Loan Originator': boolean; } = { Close: false, 'Undo Approval': false, @@ -169,7 +172,8 @@ export class LoanAccountActionsComponent { 'Capitalized Income': false, 'Contract Termination': false, 'Buy Down Fee': false, - 'Undo Write-off': false + 'Undo Write-off': false, + 'Attach Loan Originator': false }; actionButtonData: any; diff --git a/src/app/loans/loans-view/loan-accounts-button-config.ts b/src/app/loans/loans-view/loan-accounts-button-config.ts index 18493eb2b..7f317201b 100644 --- a/src/app/loans/loans-view/loan-accounts-button-config.ts +++ b/src/app/loans/loans-view/loan-accounts-button-config.ts @@ -90,6 +90,11 @@ export class LoansAccountButtonConfiguration { name: 'Reject', icon: 'times', taskPermissionName: 'REJECT_LOAN' + }, + { + name: 'Attach Loan Originator', + icon: 'edit', + taskPermissionName: 'ATTACH_LOAN_ORIGINATOR' } ]; break; diff --git a/src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.html b/src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.html index 1790ef2c8..6389ccbeb 100644 --- a/src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.html +++ b/src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.html @@ -34,16 +34,35 @@ {{ item.status }} - + {{ 'labels.inputs.Originator Type' | translate }} - {{ item.originatorTypeId }} + {{ item.originatorType.name }} - + {{ 'labels.inputs.Channel Type' | translate }} - {{ item.channelTypeId }} + {{ item.channelType.name }} + + + + {{ 'labels.inputs.Actions' | translate }} + + @if (loanStatus.pendingApproval) { + + } diff --git a/src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts b/src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts index 8b1965df2..7646ce890 100644 --- a/src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts +++ b/src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts @@ -6,6 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { Component, inject } from '@angular/core'; +import { MatIconButton } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; import { MatCell, MatCellDef, @@ -18,8 +20,13 @@ import { MatRowDef, MatTable } from '@angular/material/table'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { TranslateService } from '@ngx-translate/core'; +import { LoansService } from 'app/loans/loans.service'; import { LoanOriginator } from 'app/loans/models/loan-account.model'; +import { LoanStatus } from 'app/loans/models/loan-status.model'; +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -37,29 +44,73 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatHeaderRowDef, MatHeaderRow, MatRowDef, - MatRow + MatRow, + MatIconButton, + FaIconComponent ] }) export class LoanOriginatorsTabComponent { private route = inject(ActivatedRoute); + private router = inject(Router); + private loansService = inject(LoansService); + private translateService = inject(TranslateService); + private dialog = inject(MatDialog); loanOriginatorsData: LoanOriginator[] = []; - loanId: number | null = null; + loanId: string | null = null; + clientId: string | null = null; + + loanStatus: LoanStatus | null = null; loanoriginatorsColumns: string[] = [ 'id', 'externalId', 'name', 'status', - 'originatorTypeId', - 'channelTypeId' + 'originatorType', + 'channelType', + 'actions' ]; 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; }); } + + dettachLoanOriginator(loanOriginator: LoanOriginator): void { + const dettachCodeDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + heading: this.translateService.instant('labels.heading.Loan Originators'), + dialogContext: + this.translateService.instant('labels.buttons.Delete') + + ' ' + + this.translateService.instant('labels.inputs.Loan Originator') + + ' ' + + loanOriginator.name + } + }); + dettachCodeDialogRef.afterClosed().subscribe((response: any) => { + if (response.confirm) { + this.loansService.dettachLoanOriginator(this.loanId, String(loanOriginator.id)).subscribe((response) => { + this.reload(); + }); + } + }); + } + + /** + * Refetches data fot the component + */ + private reload() { + const url: string = this.router.url; + this.router + .navigateByUrl(`/clients/${this.clientId}/loans-accounts`, { skipLocationChange: true }) + .then(() => this.router.navigate([url])); + } } diff --git a/src/app/loans/loans.service.ts b/src/app/loans/loans.service.ts index b9748520d..d2640894e 100644 --- a/src/app/loans/loans.service.ts +++ b/src/app/loans/loans.service.ts @@ -658,6 +658,16 @@ export class LoansService { return this.http.post('/loans?command=calculateLoanSchedule', payload); } + attachLoanOriginator(loanId: string, originatorId: string): Observable { + const emptyBody = {}; + return this.http.post(`/loans/${loanId}/originators/${originatorId}`, emptyBody); + } + + dettachLoanOriginator(loanId: string, originatorId: string): Observable { + const emptyBody = {}; + return this.http.delete(`/loans/${loanId}/originators/${originatorId}`, emptyBody); + } + /** * @param loansAccount Loan account data used for the request * @param loansAccountTemplate Loan account template for getting product default values diff --git a/src/app/loans/models/loan-account.model.ts b/src/app/loans/models/loan-account.model.ts index b186183f8..b5072c372 100644 --- a/src/app/loans/models/loan-account.model.ts +++ b/src/app/loans/models/loan-account.model.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Currency } from 'app/shared/models/general.model'; +import { CodeValue, Currency } from 'app/shared/models/general.model'; export interface DelinquencyRange { id: number; @@ -178,6 +178,6 @@ export interface LoanOriginator { externalId: string; name: string; status: string; - originatorTypeId: number; - channelTypeId: number; + originatorType: CodeValue; + channelType: CodeValue; } diff --git a/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.html b/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.html new file mode 100644 index 000000000..7d756e1ec --- /dev/null +++ b/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.html @@ -0,0 +1,116 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Name' | translate }} + + @if (loanOriginatorForm.controls.name.hasError('required')) { + + {{ 'labels.inputs.Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + @if (loanOriginatorForm.controls.name.hasError('pattern')) { + + {{ 'labels.inputs.Name' | translate }} {{ 'labels.inputs.cannot' | translate }} + {{ 'labels.inputs.begin with a special character or number' | translate }} + + } + + + + {{ 'labels.inputs.External Id' | translate }} + + @if (loanOriginatorForm.controls.externalId.hasError('required')) { + + {{ 'labels.inputs.External Id' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + @if (loanOriginatorForm.controls.externalId.hasError('pattern')) { + + {{ 'labels.inputs.External Id' | translate }} {{ 'labels.inputs.cannot' | translate }} + {{ 'labels.inputs.begin with a special character or number' | translate }} + + } + + + + {{ 'labels.inputs.Status' | translate }} + + @for (status of statusOptions; track status) { + + {{ status }} + + } + + @if (loanOriginatorForm.controls.status.hasError('required')) { + + {{ 'labels.inputs.Status' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Originator Type' | translate }} + + @for (originatorType of originatorTypeOptions; track originatorType) { + + {{ originatorType.name }} + + } + + @if (loanOriginatorForm.controls.originatorTypeId.hasError('required')) { + + {{ 'labels.inputs.Originator Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Channel Type' | translate }} + + @for (channelTypeI of channelTypeOptions; track channelTypeI) { + + {{ channelTypeI.name }} + + } + + @if (loanOriginatorForm.controls.channelTypeId.hasError('required')) { + + {{ 'labels.inputs.Channel Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + +
+
+ + + + + +
+
+
diff --git a/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.scss b/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.scss @@ -0,0 +1,11 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.ts b/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.ts new file mode 100644 index 000000000..e316c0c69 --- /dev/null +++ b/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.ts @@ -0,0 +1,109 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, TemplateRef, ElementRef, ViewChild, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; + +/** Custom Services */ +import { OrganizationService } from '../../organization.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { CodeValue } from 'app/shared/models/general.model'; + +/** + * Create Loan Originator component. + */ +@Component({ + selector: 'mifosx-create-loan-originator', + templateUrl: './create-loan-originator.component.html', + styleUrl: './create-loan-originator.component.scss', + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class CreateLoanOriginatorComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private organizationService = inject(OrganizationService); + private route = inject(ActivatedRoute); + private router = inject(Router); + + /** Loan Originator form. */ + loanOriginatorForm: UntypedFormGroup; + /** Form data. */ + loanOriginatorsTemplateData: any; + statusOptions: string[] = []; + originatorTypeOptions: CodeValue[] = []; + channelTypeOptions: CodeValue[] = []; + + /* Reference of Loan Originator form */ + @ViewChild('createLoanOriginatorFormRef') createLoanOriginatorFormRef: ElementRef; + /* Template for popover on Loan Originator form */ + @ViewChild('templateCreateLoanOriginatorForm') templateCreateLoanOriginatorForm: TemplateRef; + + constructor() { + this.route.data.subscribe((data: { loanOriginatorsTemplateData: any }) => { + this.loanOriginatorsTemplateData = data.loanOriginatorsTemplateData; + this.statusOptions = data.loanOriginatorsTemplateData.statusOptions; + this.originatorTypeOptions = data.loanOriginatorsTemplateData.originatorTypeOptions; + this.channelTypeOptions = data.loanOriginatorsTemplateData.channelTypeOptions; + }); + } + + /** + * Creates the Loan Originator form. + */ + ngOnInit() { + this.createLoanOriginatorForm(); + } + + /** + * Creates the Loan Originator form. + */ + createLoanOriginatorForm() { + this.loanOriginatorForm = this.formBuilder.group({ + externalId: [ + this.loanOriginatorsTemplateData.externalId, + Validators.required + ], + name: [ + '', + [ + Validators.required, + Validators.pattern('(^[A-z]).*') + ] + ], + status: [ + '', + Validators.required + ], + originatorTypeId: [ + '', + Validators.required + ], + channelTypeId: [ + '', + Validators.required + ] + }); + } + + /** + * Submits the Loan Originator form and creates Loan Originator, + * if successful redirects to Loan Originators. + */ + submit() { + const loanOriginatorFormData = this.loanOriginatorForm.value; + const data = { + ...loanOriginatorFormData + }; + this.organizationService.createLoanOriginator(data).subscribe((response: any) => { + this.router.navigate(['../'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.html b/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.html new file mode 100644 index 000000000..151a9cbb8 --- /dev/null +++ b/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.html @@ -0,0 +1,116 @@ + + +
+ +
+ +
+ + {{ 'labels.inputs.Name' | translate }} + + @if (loanOriginatorForm.controls.name.hasError('required')) { + + {{ 'labels.inputs.Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + @if (loanOriginatorForm.controls.name.hasError('pattern')) { + + {{ 'labels.inputs.Name' | translate }} {{ 'labels.inputs.cannot' | translate }} + {{ 'labels.inputs.begin with a special character or number' | translate }} + + } + + + + {{ 'labels.inputs.External Id' | translate }} + + @if (loanOriginatorForm.controls.externalId.hasError('required')) { + + {{ 'labels.inputs.External Id' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + @if (loanOriginatorForm.controls.externalId.hasError('pattern')) { + + {{ 'labels.inputs.External Id' | translate }} {{ 'labels.inputs.cannot' | translate }} + {{ 'labels.inputs.begin with a special character or number' | translate }} + + } + + + + {{ 'labels.inputs.Status' | translate }} + + @for (status of statusOptions; track status) { + + {{ status }} + + } + + @if (loanOriginatorForm.controls.status.hasError('required')) { + + {{ 'labels.inputs.Status' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Originator Type' | translate }} + + @for (originatorType of originatorTypeOptions; track originatorType) { + + {{ originatorType.name }} + + } + + @if (loanOriginatorForm.controls.originatorTypeId.hasError('required')) { + + {{ 'labels.inputs.Originator Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + + + + {{ 'labels.inputs.Channel Type' | translate }} + + @for (channelTypeI of channelTypeOptions; track channelTypeI) { + + {{ channelTypeI.name }} + + } + + @if (loanOriginatorForm.controls.channelTypeId.hasError('required')) { + + {{ 'labels.inputs.Channel Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + +
+
+ + + + + +
+
+
diff --git a/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.scss b/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.scss new file mode 100644 index 000000000..7c3f1555b --- /dev/null +++ b/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.scss @@ -0,0 +1,11 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.container { + max-width: 37rem; +} diff --git a/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts b/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts new file mode 100644 index 000000000..c27f72a11 --- /dev/null +++ b/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.ts @@ -0,0 +1,115 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, TemplateRef, ElementRef, ViewChild, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; + +/** Custom Services */ +import { OrganizationService } from '../../organization.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { CodeValue } from 'app/shared/models/general.model'; +import { LoanOriginator } from 'app/loans/models/loan-account.model'; + +/** + * Create Loan Originator component. + */ +@Component({ + selector: 'mifosx-edit-loan-originator', + templateUrl: './edit-loan-originator.component.html', + styleUrl: './edit-loan-originator.component.scss', + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class EditLoanOriginatorComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private organizationService = inject(OrganizationService); + private route = inject(ActivatedRoute); + private router = inject(Router); + + /** Loan Originator form. */ + loanOriginatorForm: UntypedFormGroup; + /** Form data. */ + loanOriginatorsData: LoanOriginator; + loanOriginatorsTemplateData: any; + statusOptions: string[] = []; + originatorTypeOptions: CodeValue[] = []; + channelTypeOptions: CodeValue[] = []; + + /* Reference of Loan Originator form */ + @ViewChild('editLoanOriginatorFormRef') editLoanOriginatorFormRef: ElementRef; + /* Template for popover on Loan Originator form */ + @ViewChild('templateCreateLoanOriginatorForm') templateCreateLoanOriginatorForm: TemplateRef; + + 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; + }); + } + + /** + * Creates the Loan Originator form. + */ + ngOnInit() { + this.createLoanOriginatorForm(); + } + + /** + * Creates the Loan Originator form. + */ + createLoanOriginatorForm() { + this.loanOriginatorForm = this.formBuilder.group({ + externalId: [ + { + value: this.loanOriginatorsTemplateData.externalId, + disabled: true + } + ], + name: [ + this.loanOriginatorsData.name, + [ + Validators.required, + Validators.pattern('^[A-Za-z].*') + ] + ], + status: [ + this.loanOriginatorsData.status, + Validators.required + ], + originatorTypeId: [ + this.loanOriginatorsData.originatorType.id, + Validators.required + ], + channelTypeId: [ + this.loanOriginatorsData.channelType.id, + Validators.required + ] + }); + } + + /** + * Submits the Loan Originator form and creates Loan Originator, + * if successful redirects to Loan Originators. + */ + submit() { + const loanOriginatorFormData = this.loanOriginatorForm.value; + delete loanOriginatorFormData.externalId; + const data = { + ...loanOriginatorFormData + }; + this.organizationService.updateLoanOriginator(this.loanOriginatorsData.id, data).subscribe((response: any) => { + this.router.navigate(['../..'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/organization/loan-originators/loan-originators-template.resolver.ts b/src/app/organization/loan-originators/loan-originators-template.resolver.ts new file mode 100644 index 000000000..2db30a917 --- /dev/null +++ b/src/app/organization/loan-originators/loan-originators-template.resolver.ts @@ -0,0 +1,32 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Injectable, inject } from '@angular/core'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { OrganizationService } from '../organization.service'; + +/** + * Loan Originators data resolver. + */ +@Injectable() +export class LoanOriginatorsTemplateResolver { + private organizationService = inject(OrganizationService); + + /** + * Returns the Loan Originators data. + * @returns {Observable} + */ + resolve(): Observable { + return this.organizationService.getLoanOriginatorsTemplate(); + } +} diff --git a/src/app/organization/loan-originators/loan-originators.component.html b/src/app/organization/loan-originators/loan-originators.component.html new file mode 100644 index 000000000..ef1049ee3 --- /dev/null +++ b/src/app/organization/loan-originators/loan-originators.component.html @@ -0,0 +1,99 @@ + + +
+
+ +
+
+ +
+
+ + {{ 'labels.inputs.Filter' | translate }} + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Id' | translate }}{{ loanOriginator.id }}{{ 'labels.inputs.Name' | translate }}{{ loanOriginator.name }}{{ 'labels.inputs.External Id' | translate }}{{ loanOriginator.externalId }}{{ 'labels.inputs.Status' | translate }} +
+ @if (loanOriginator.status === 'ACTIVE') { + + } + @if (loanOriginator.status !== 'ACTIVE') { + + } +
+
{{ 'labels.inputs.Originator Type' | translate }}{{ loanOriginator.originatorType.name }}{{ 'labels.inputs.Channel Type' | translate }}{{ loanOriginator.channelType.name }}{{ 'labels.inputs.Actions' | translate }} + +
+ + +
+
diff --git a/src/app/organization/loan-originators/loan-originators.component.scss b/src/app/organization/loan-originators/loan-originators.component.scss new file mode 100644 index 000000000..dbecec6e7 --- /dev/null +++ b/src/app/organization/loan-originators/loan-originators.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +table { + width: 100%; + + .select-row:hover { + cursor: pointer; + } +} + +.true { + color: #32cd32; +} + +.false { + color: #f44366; +} diff --git a/src/app/organization/loan-originators/loan-originators.component.ts b/src/app/organization/loan-originators/loan-originators.component.ts new file mode 100644 index 000000000..56963e6e2 --- /dev/null +++ b/src/app/organization/loan-originators/loan-originators.component.ts @@ -0,0 +1,142 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, TemplateRef, ElementRef, ViewChild, inject } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { + MatTableDataSource, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { ActivatedRoute, Router } from '@angular/router'; + +/** Custom Services */ +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatTooltip } from '@angular/material/tooltip'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { LoanOriginator } from 'app/loans/models/loan-account.model'; +import { OrganizationService } from '../organization.service'; +import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatIconButton } from '@angular/material/button'; + +/** + * Loan Originators component. + */ +@Component({ + selector: 'mifosx-loan-originators', + templateUrl: './loan-originators.component.html', + styleUrl: './loan-originators.component.scss', + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatTable, + MatSort, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatSortHeader, + MatCellDef, + MatCell, + MatTooltip, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatPaginator, + MatIconButton + ] +}) +export class LoanOriginatorsComponent implements OnInit { + private route = inject(ActivatedRoute); + private router = inject(Router); + private organizationService = inject(OrganizationService); + private translateService = inject(TranslateService); + private dialog = inject(MatDialog); + + /** Loan Originators data. */ + loanOriginatorsData: LoanOriginator[] = []; + /** Columns to be displayed in Loan Originators table. */ + displayedColumns: string[] = [ + 'id', + 'name', + 'externalId', + 'status', + 'originatorType', + 'channelType', + 'actions' + ]; + /** Data source for Loan Originators table. */ + dataSource: MatTableDataSource; + + /** Paginator for Loan Originators table. */ + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + /** Sorter for Loan Originators table. */ + @ViewChild(MatSort, { static: true }) sort: MatSort; + + /* Reference of Loan Originators table */ + @ViewChild('tableLoanOriginators') tableLoanOriginators: ElementRef; + /* Template for popover on Loan Originators table */ + @ViewChild('templateTableLoanOriginators') templateTableLoanOriginators: TemplateRef; + + constructor() { + this.route.data.subscribe((data: { loanOriginatorsData: LoanOriginator[] }) => { + this.loanOriginatorsData = data.loanOriginatorsData; + }); + } + + /** + * Filters data in Loan Originators table based on passed value. + * @param {string} filterValue Value to filter data. + */ + applyFilter(filterValue: string) { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + /** + * Sets the Loan Originators table. + */ + ngOnInit() { + this.setLoanOriginators(); + } + + /** + * Initializes the data source, paginator and sorter for Loan Originators table. + */ + setLoanOriginators() { + this.dataSource = new MatTableDataSource(this.loanOriginatorsData); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + deleteLoanOriginator(loanOriginator: LoanOriginator): void { + const deleteCodeDialogRef = this.dialog.open(DeleteDialogComponent, { + data: { + deleteContext: this.translateService.instant('labels.inputs.Loan Originator') + ' ' + loanOriginator.name + } + }); + deleteCodeDialogRef.afterClosed().subscribe((response: any) => { + if (response.delete) { + this.organizationService.deleteLoanOriginator(loanOriginator.id).subscribe(() => { + this.router.navigate(['/organization/manage-loan-originators']); + }); + } + }); + } +} diff --git a/src/app/organization/loan-originators/loan-originators.resolver.ts b/src/app/organization/loan-originators/loan-originators.resolver.ts new file mode 100644 index 000000000..3f503baf0 --- /dev/null +++ b/src/app/organization/loan-originators/loan-originators.resolver.ts @@ -0,0 +1,38 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Injectable, inject } from '@angular/core'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { OrganizationService } from '../organization.service'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** + * Loan Originators data resolver. + */ +@Injectable() +export class LoanOriginatorsResolver { + private organizationService = inject(OrganizationService); + + /** + * Returns the Loan Originators data. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const originatorId = route.paramMap.get('id'); + if (originatorId) { + return this.organizationService.getLoanOriginator(originatorId); + } else { + return this.organizationService.getLoanOriginators(); + } + } +} diff --git a/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html new file mode 100644 index 000000000..054db83b6 --- /dev/null +++ b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html @@ -0,0 +1,62 @@ + + +
+ +
+ +
+ + +
+
+ {{ 'labels.inputs.Name' | translate }} +
+ +
+ {{ loanOriginatorData.name }} +
+ +
+ {{ 'labels.inputs.External Id' | translate }} +
+ +
+ {{ loanOriginatorData.externalId }} +
+ +
+ {{ 'labels.inputs.Status' | translate }} +
+ +
+ {{ loanOriginatorData.status }} +
+ +
+ {{ 'labels.inputs.Originator Type' | translate }} +
+ +
+ {{ loanOriginatorData.originatorType.name }} +
+ +
+ {{ 'labels.inputs.Channel Type' | translate }} +
+ +
+ {{ loanOriginatorData.channelType.name }} +
+
+
+
+
diff --git a/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.scss b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.scss new file mode 100644 index 000000000..e4166fc69 --- /dev/null +++ b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.scss @@ -0,0 +1,18 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.container { + max-width: 37rem; + + .content { + div { + margin: 1rem 0; + word-wrap: break-word; + } + } +} diff --git a/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.ts b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.ts new file mode 100644 index 000000000..10b6c9444 --- /dev/null +++ b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.ts @@ -0,0 +1,43 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { LoanOriginator } from 'app/loans/models/loan-account.model'; + +/** + * View Employee Component. + */ +@Component({ + selector: 'mifosx-view-loan-originator', + templateUrl: './view-loan-originator.component.html', + styleUrl: './view-loan-originator.component.scss', + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +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; + }); + } +} diff --git a/src/app/organization/organization-routing.module.ts b/src/app/organization/organization-routing.module.ts index 75009ca56..97152f764 100644 --- a/src/app/organization/organization-routing.module.ts +++ b/src/app/organization/organization-routing.module.ts @@ -111,6 +111,12 @@ import { ViewFundComponent } from './manage-funds/view-fund/view-fund.component' import { EditFundComponent } from './manage-funds/edit-fund/edit-fund.component'; import { CreateFundComponent } from './manage-funds/create-fund/create-fund.component'; import { InvestorsComponent } from './investors/investors.component'; +import { LoanOriginatorsComponent } from './loan-originators/loan-originators.component'; +import { LoanOriginatorsResolver } from './loan-originators/loan-originators.resolver'; +import { ViewLoanOriginatorComponent } from './loan-originators/view-loan-originator/view-loan-originator.component'; +import { EditLoanOriginatorComponent } from './loan-originators/edit-loan-originator/edit-loan-originator.component'; +import { CreateLoanOriginatorComponent } from './loan-originators/create-loan-originator/create-loan-originator.component'; +import { LoanOriginatorsTemplateResolver } from './loan-originators/loan-originators-template.resolver'; /** Organization Routes */ const routes: Routes = [ @@ -592,6 +598,50 @@ const routes: Routes = [ workingDays: WorkingDaysResolver } }, + { + path: 'manage-loan-originators', + data: { title: 'Manage Loan Originators', breadcrumb: 'Manage Loan Originators' }, + children: [ + { + path: '', + component: LoanOriginatorsComponent, + resolve: { + loanOriginatorsData: LoanOriginatorsResolver + } + }, + { + path: 'create', + component: CreateLoanOriginatorComponent, + data: { title: 'Create Loan Originator', breadcrumb: 'Create' }, + resolve: { + loanOriginatorsTemplateData: LoanOriginatorsTemplateResolver + } + }, + { + path: ':id', + data: { routeParamBreadcrumb: 'id', addBreadcrumbLink: false }, + children: [ + { + path: '', + component: ViewLoanOriginatorComponent, + data: { title: 'View Loan Originator', breadcrumb: 'View', routeParamBreadcrumb: false }, + resolve: { + loanOriginatorData: LoanOriginatorsResolver + } + }, + { + path: 'edit', + component: EditLoanOriginatorComponent, + data: { title: 'Edit Loan Originator', breadcrumb: 'Edit', routeParamBreadcrumb: false }, + resolve: { + loanOriginatorData: LoanOriginatorsResolver, + loanOriginatorsTemplateData: LoanOriginatorsTemplateResolver + } + } + ] + } + ] + }, { path: 'manage-funds', data: { title: 'Manage Funds', breadcrumb: 'Manage Funds' }, @@ -753,7 +803,9 @@ const routes: Routes = [ LoanProvisioningCriteriaTemplateResolver, LoanProvisioningCriteriaAndTemplateResolver, StandingInstructionsTemplateResolver, - AdvanceSearchTemplateResolver + AdvanceSearchTemplateResolver, + LoanOriginatorsResolver, + LoanOriginatorsTemplateResolver ] }) export class OrganizationRoutingModule {} diff --git a/src/app/organization/organization.component.html b/src/app/organization/organization.component.html index 3af786c81..35f04c74b 100644 --- a/src/app/organization/organization.component.html +++ b/src/app/organization/organization.component.html @@ -323,6 +323,34 @@ +
+ + + +
+