From 432d16d41f36396675a90bed37bdfe596d7a1c53 Mon Sep 17 00:00:00 2001 From: Muskan Khedia <36980003+muskankhedia@users.noreply.github.com> Date: Sun, 23 Aug 2020 15:38:48 +0530 Subject: [PATCH] feat: add checker inbox and tasks (#1218) - add checker inbox - add client approval - add loan disbursal - add loan approval - add reschedule loan Fixes: #252, #1217 --- src/app/app.module.ts | 2 + .../centers-view/centers-view.component.ts | 2 +- .../core/shell/sidenav/sidenav.component.html | 6 + .../loans/loans-view/loans-view.component.ts | 2 +- src/app/pipes/accounts-filter.pipe.ts | 5 + .../checker-inbox.component.html | 139 ++++++++++++ .../checker-inbox.component.scss | 42 ++++ .../checker-inbox.component.spec.ts | 25 +++ .../checker-inbox/checker-inbox.component.ts | 206 ++++++++++++++++++ .../client-approval.component.html | 71 ++++++ .../client-approval.component.scss | 35 +++ .../client-approval.component.spec.ts | 25 +++ .../client-approval.component.ts | 159 ++++++++++++++ .../loan-approval.component.html | 78 +++++++ .../loan-approval.component.scss | 35 +++ .../loan-approval.component.spec.ts | 25 +++ .../loan-approval/loan-approval.component.ts | 192 ++++++++++++++++ .../loan-disbursal.component.html | 65 ++++++ .../loan-disbursal.component.scss | 35 +++ .../loan-disbursal.component.spec.ts | 25 +++ .../loan-disbursal.component.ts | 140 ++++++++++++ .../reschedule-loan.component.html | 71 ++++++ .../reschedule-loan.component.scss | 35 +++ .../reschedule-loan.component.spec.ts | 25 +++ .../reschedule-loan.component.ts | 132 +++++++++++ .../checker-inbox-and-tasks.component.html | 36 +++ .../checker-inbox-and-tasks.component.scss | 0 .../checker-inbox-and-tasks.component.spec.ts | 25 +++ .../checker-inbox-and-tasks.component.ts | 16 ++ .../getGroupedClientsData.resolver.ts | 30 +++ .../common-resolvers/getLoans.resolver.ts | 30 +++ .../common-resolvers/getOffices.resolver.ts | 30 +++ .../getRescheduleLoans.resolver.ts | 30 +++ .../getmakercheckers.resolver.ts | 30 +++ .../makerCheckerTemplate.resolver.ts | 30 +++ src/app/tasks/tasks-routing.module.ts | 94 ++++++++ src/app/tasks/tasks.module.ts | 38 ++++ src/app/tasks/tasks.service.spec.ts | 12 + src/app/tasks/tasks.service.ts | 105 +++++++++ 39 files changed, 2081 insertions(+), 2 deletions(-) create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.html create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.scss create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.spec.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.html create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.scss create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.spec.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.html create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.scss create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.spec.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.html create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.scss create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.spec.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.html create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.scss create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.spec.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html create mode 100644 src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.scss create mode 100644 src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.spec.ts create mode 100644 src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.ts create mode 100644 src/app/tasks/common-resolvers/getGroupedClientsData.resolver.ts create mode 100644 src/app/tasks/common-resolvers/getLoans.resolver.ts create mode 100644 src/app/tasks/common-resolvers/getOffices.resolver.ts create mode 100644 src/app/tasks/common-resolvers/getRescheduleLoans.resolver.ts create mode 100644 src/app/tasks/common-resolvers/getmakercheckers.resolver.ts create mode 100644 src/app/tasks/common-resolvers/makerCheckerTemplate.resolver.ts create mode 100644 src/app/tasks/tasks-routing.module.ts create mode 100644 src/app/tasks/tasks.module.ts create mode 100644 src/app/tasks/tasks.service.spec.ts create mode 100644 src/app/tasks/tasks.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index fc9b51db8..f34c2826d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -37,6 +37,7 @@ import { ReportsModule } from './reports/reports.module'; import { NotificationsModule } from './notifications/notifications.module'; import { CollectionsModule } from './collections/collections.module'; import { ProfileModule } from './profile/profile.module'; +import { TasksModule } from './tasks/tasks.module'; /** Main Routing Module */ import { AppRoutingModule } from './app-routing.module'; @@ -73,6 +74,7 @@ import { AppRoutingModule } from './app-routing.module'; UsersModule, NotificationsModule, CollectionsModule, + TasksModule, AppRoutingModule, ], declarations: [WebAppComponent, NotFoundComponent], diff --git a/src/app/centers/centers-view/centers-view.component.ts b/src/app/centers/centers-view/centers-view.component.ts index aca2ff965..a8e721000 100644 --- a/src/app/centers/centers-view/centers-view.component.ts +++ b/src/app/centers/centers-view/centers-view.component.ts @@ -95,7 +95,7 @@ export class CentersViewComponent implements OnInit { */ private centersUnassignStaff() { const unAssignStaffDialogRef = this.dialog.open(ConfirmationDialogComponent, { - data: { heading: 'Unassign Staff', dialogContext: 'Are you sure you want Unassign Staff ?' } + data: { heading: 'Unassign Staff', dialogContext: 'Are you sure you want Unassign Staff' } }); unAssignStaffDialogRef.afterClosed().subscribe((response: { confirm: any }) => { if (response.confirm) { diff --git a/src/app/core/shell/sidenav/sidenav.component.html b/src/app/core/shell/sidenav/sidenav.component.html index 42ed00c23..77105dc48 100644 --- a/src/app/core/shell/sidenav/sidenav.component.html +++ b/src/app/core/shell/sidenav/sidenav.component.html @@ -63,6 +63,12 @@ Navigation + + + + + Checker Inbox and Tasks + diff --git a/src/app/loans/loans-view/loans-view.component.ts b/src/app/loans/loans-view/loans-view.component.ts index 17c90f9b6..0949f020a 100644 --- a/src/app/loans/loans-view/loans-view.component.ts +++ b/src/app/loans/loans-view/loans-view.component.ts @@ -145,7 +145,7 @@ export class LoansViewComponent implements OnInit { */ private recoverFromGuarantor() { const recoverFromGuarantorDialogRef = this.dialog.open(ConfirmationDialogComponent, { - data: { heading: 'Recover from Guarantor', dialogContext: 'Are you sure you want recover from Guarantor ?', type: 'Mild' } + data: { heading: 'Recover from Guarantor', dialogContext: 'Are you sure you want recover from Guarantor', type: 'Mild' } }); recoverFromGuarantorDialogRef.afterClosed().subscribe((response: any) => { if (response.confirm) { diff --git a/src/app/pipes/accounts-filter.pipe.ts b/src/app/pipes/accounts-filter.pipe.ts index 665228f5a..2280db00e 100644 --- a/src/app/pipes/accounts-filter.pipe.ts +++ b/src/app/pipes/accounts-filter.pipe.ts @@ -78,6 +78,11 @@ export class AccountsFilterPipe implements PipeTransform { return accounts; } } + if (type === 'clientApproval') { + accounts = accounts.filter((account: any) => { + return (account.active === false && account.status.value === 'Pending'); + }); + } return accounts; } } diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.html new file mode 100644 index 000000000..e4f357358 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.html @@ -0,0 +1,139 @@ +
+ +
+ +
+ + + +
+
+ +
+ +
+ + + From Date + + + + + + + To Date + + + + + + + Action + + + {{ action }} + + + + + + Entity + + + {{ entity }} + + + + + + Resource ID + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + ID {{ makerChecker.id }} Made on Date {{ makerChecker.madeOnDate | date }} Status {{ makerChecker.processingResult }} User {{ makerChecker.maker }} Action {{ makerChecker.actionName }} Entity {{ makerChecker.entityName }}
+ +
+ +
+ + No checker inbox data available for this search. +
+ +
+ +
+ +
+ +
+ + No checker inbox data available for this account. +
+ +
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.scss new file mode 100644 index 000000000..f41ef3b17 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.scss @@ -0,0 +1,42 @@ +.tab-container { + padding: 1%; + margin: 1%; + + .mat-raised-button.mat-success { + color: #fff; + background-color: #008000ab; + } + .mat-raised-button.mat-reject { + color: #fff; + background-color: #FFCA00; + } + #search-button { + height: 2.5rem; + margin-top: 1rem; + } + .view-details{ + cursor: pointer; + } +} + +.alert { + background-color: #E8F4FD; + padding: 6px 16px; + font-size: 0.875rem; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: 400; + line-height: 1.43; + border-radius: 4px; + letter-spacing: 0.01071em; + margin: 10px; + + .message { + padding: 8px 0; + font-size: 18px; + } + .alert-check { + color:#359FF4; + margin-right: 2px; + font-size: 1.4rem; + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.spec.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.spec.ts new file mode 100644 index 000000000..9f29ef9c0 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CheckerInboxComponent } from './checker-inbox.component'; + +describe('CheckerInboxComponent', () => { + let component: CheckerInboxComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CheckerInboxComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CheckerInboxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.ts new file mode 100644 index 000000000..7ce4817a2 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.ts @@ -0,0 +1,206 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SelectionModel } from '@angular/cdk/collections'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DatePipe } from '@angular/common'; +import { MatTableDataSource } from '@angular/material/table'; +import { MatDialog } from '@angular/material/dialog'; + +/** Custom Services */ +import { TasksService } from '../../tasks.service'; +import { SettingsService } from 'app/settings/settings.service'; + +/** Dialog Components */ +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; + +@Component({ + selector: 'mifosx-checker-inbox', + templateUrl: './checker-inbox.component.html', + styleUrls: ['./checker-inbox.component.scss'] +}) +export class CheckerInboxComponent implements OnInit { + + /** Data to be displayed */ + searchData: any; + /** Maker Checker Template */ + makerCheckerTemplate: any; + /** Checks if there is any data based on the search */ + noSearchedData = false; + /** Checks if there is any checker data */ + checkerData = false; + /** Maker Checker Search Form */ + makerCheckerSearchForm: FormGroup; + /** Minimum date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum date allowed. */ + maxDate = new Date(2100, 0, 1); + /** DataSource */ + dataSource: MatTableDataSource; + /** Selecting rows from table */ + selection: SelectionModel; + /** Displayed Column in table */ + displayedColumns: string[] = ['select', 'id', 'madeOnDate', 'status', 'user', 'action', 'entity']; + + /** + * Retrieves the maker checker data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + * @param {Dialog} dialog MatDialog. + * @param {DatePipe} datePipe Date Pipe. + * @param {router} router Router. + * @param {SettingsService} settingsService Settings Service. + * @param {TasksService} tasksService Tasks Service. + * @param {FormBuilder} formBuilder Form Builder. + */ + constructor(private route: ActivatedRoute, + private dialog: MatDialog, + private datePipe: DatePipe, + private router: Router, + private tasksService: TasksService, + private settingsService: SettingsService, + private formBuilder: FormBuilder) { + this.route.data.subscribe((data: { makerCheckerResource: any, makerCheckerTemplate: any }) => { + this.searchData = data.makerCheckerResource; + if (this.searchData.length > 0) { + this.checkerData = true; + } + this.makerCheckerTemplate = data.makerCheckerTemplate; + this.dataSource = new MatTableDataSource(this.searchData); + this.selection = new SelectionModel(true, []); + }); + } + + ngOnInit() { + this.createMakerCheckerSearchForm(); + } + + /** + * Creates the standing instruction form. + */ + createMakerCheckerSearchForm() { + this.makerCheckerSearchForm = this.formBuilder.group({ + 'makerDateTimeFrom': [''], + 'makerDateTimeto': [''], + 'actionName': [''], + 'entityName': [''], + 'resourceId': [''] + }); + } + + search() { + const dateFormat = this.settingsService.dateFormat; + const makerCheckerSearchParams = { + ...this.makerCheckerSearchForm.value, + makerDateTimeFrom: this.datePipe.transform(this.makerCheckerSearchForm.value.makerDateTimeFrom, dateFormat), + makerDateTimeto: this.datePipe.transform(this.makerCheckerSearchForm.value.makerDateTimeto, dateFormat) + }; + this.tasksService.getMakerCheckerData(makerCheckerSearchParams).subscribe((response: any) => { + this.searchData = response; + if (this.searchData.length === 0) { + this.noSearchedData = true; + } else { + this.noSearchedData = false; + } + this.dataSource = new MatTableDataSource(this.searchData); + this.selection = new SelectionModel(true, []); + }); + } + + /** Whether the number of selected elements matches the total number of rows. */ + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.dataSource.data.length; + return numSelected === numRows; + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + masterToggle() { + this.isAllSelected() ? + this.selection.clear() : + this.dataSource.data.forEach((row: any) => this.selection.select(row)); + } + + /** The label for the checkbox on the passed row */ + checkboxLabel(row?: any): string { + if (!row) { + return `${this.isAllSelected() ? 'select' : 'deselect'} all`; + } + return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`; + } + + approveChecker() { + const approveCheckerDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { heading: 'Approve Checker', dialogContext: 'Are you sure you want to approve checker' } + }); + approveCheckerDialogRef.afterClosed().subscribe((response: { confirm: any }) => { + if (response.confirm) { + this.bulkCheckerApproveorReject('approve'); + } + }); + } + + rejectChecker() { + const rejectCheckerDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { heading: 'Reject Checker', dialogContext: 'Are you sure you want to reject checker' } + }); + rejectCheckerDialogRef.afterClosed().subscribe((response: { confirm: any }) => { + if (response.confirm) { + this.bulkCheckerApproveorReject('reject'); + } + }); + } + + deleteChecker() { + const deleteCheckerDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { heading: 'Delete Checker', dialogContext: 'Are you sure you want to delete checker' } + }); + deleteCheckerDialogRef.afterClosed().subscribe((response: { confirm: any }) => { + if (response.confirm) { + this.bulkDeleteChecker(); + } + }); + } + + bulkCheckerApproveorReject(action: any) { + const selectedAccounts = this.selection.selected.length; + const listSelectedAccounts = this.selection.selected; + let approvedAccounts = 0; + listSelectedAccounts.forEach((element: any) => { + this.tasksService.executeMakerCheckerAction(element.id, action).subscribe((response: any) => { + approvedAccounts++; + if (selectedAccounts === approvedAccounts) { + this.reload(); + } + }); + }); + } + + bulkDeleteChecker() { + const selectedAccounts = this.selection.selected.length; + const listSelectedAccounts = this.selection.selected; + let approvedAccounts = 0; + listSelectedAccounts.forEach((element: any) => { + this.tasksService.deleteMakerChecker(element.id).subscribe((response: any) => { + approvedAccounts++; + if (selectedAccounts === approvedAccounts) { + this.reload(); + } + }); + }); + } + + applyFilter(filterValue: string = '') { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + /** + * Refetches data for the component + * TODO: Replace by a custom reload component instead of hard-coded back-routing. + */ + reload() { + const url: string = this.router.url; + this.router.navigateByUrl(`/checker-inbox-and-tasks`, { skipLocationChange: true }) + .then(() => this.router.navigate([url])); + } + +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.html new file mode 100644 index 000000000..84d195dd4 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.html @@ -0,0 +1,71 @@ +
+ +
+ +
+ +
+
+ + + +

{{client.key}}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name {{ element.displayName }} AccountNumber {{ element.accountNo }} Staff {{ element.staffName }}
+ +
+ +
+ +
+ +
+ +
+ + No pending loans for approval. +
+ +
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.scss new file mode 100644 index 000000000..a93c99f9a --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.scss @@ -0,0 +1,35 @@ +.tab-container { + padding: 1%; + margin: 1%; + + .mat-raised-button.mat-success { + color: #fff; + background-color: #008000ab; + } + + .view-details{ + cursor: pointer; + } +} + +.alert { + background-color: #E8F4FD; + padding: 6px 16px; + font-size: 0.875rem; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: 400; + line-height: 1.43; + border-radius: 4px; + letter-spacing: 0.01071em; + margin: 10px; + + .message { + padding: 8px 0; + font-size: 18px; + } + .alert-check { + color:#359FF4; + margin-right: 2px; + font-size: 1.4rem; + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.spec.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.spec.ts new file mode 100644 index 000000000..cf516c6e0 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClientApprovalComponent } from './client-approval.component'; + +describe('ClientApprovalComponent', () => { + let component: ClientApprovalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ClientApprovalComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClientApprovalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.ts new file mode 100644 index 000000000..171c87cc1 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.ts @@ -0,0 +1,159 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SelectionModel } from '@angular/cdk/collections'; +import * as _ from 'lodash'; +import { DatePipe } from '@angular/common'; +import { MatTableDataSource } from '@angular/material/table'; +import { MatDialog } from '@angular/material/dialog'; + +/** Dialog Imports */ +import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; +import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; +import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicker-base'; + +/** Custom Services */ +import { TasksService } from '../../tasks.service'; +import { SettingsService } from 'app/settings/settings.service'; + +@Component({ + selector: 'mifosx-client-approval', + templateUrl: './client-approval.component.html', + styleUrls: ['./client-approval.component.scss'] +}) +export class ClientApprovalComponent { + + /** Grouped Clients Data */ + groupedClients: any; + /** Checks to show the data */ + showData = false; + /** Batch Requests */ + batchRequests: any[]; + /** Datasource */ + dataSource: MatTableDataSource; + /** Row Selection Data */ + selection: SelectionModel; + /** Displayed Columns */ + displayedColumns: string[] = ['select', 'name', 'accountNumber', 'staff']; + + /** + * Retrieves the grouped client data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + * @param {Dialog} dialog MatDialog. + * @param {DatePipe} datePipe Date Pipe. + * @param {router} router Router. + * @param {SettingsService} settingsService Settings Service. + * @param {TasksService} tasksService Tasks Service. + */ + constructor(private route: ActivatedRoute, + private dialog: MatDialog, + private datePipe: DatePipe, + private router: Router, + private settingsService: SettingsService, + private tasksService: TasksService) { + this.route.data.subscribe((data: { groupedClientData: any }) => { + this.groupedClients = _.groupBy(data.groupedClientData.pageItems, 'officeName'); + if (Object.keys(this.groupedClients).length) { + this.showData = true; + } + this.dataSource = new MatTableDataSource(data.groupedClientData.pageItems); + this.selection = new SelectionModel(true, []); + }); + } + + /** Whether the number of selected elements matches the total number of rows. */ + isAllSelected(dataSource2: any) { + if (dataSource2) { + const numSelected = this.selection.selected; + return _.difference(dataSource2, numSelected).length === 0; + } + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + masterToggle(dataSource3: any) { + if (dataSource3) { + this.isAllSelected(dataSource3) ? + dataSource3.forEach((row: any) => this.selection.deselect(row)) : + dataSource3.forEach((row: any) => this.selection.select(row)); + } + } + + /** The label for the checkbox on the passed row */ + checkboxLabel(row?: any): string { + if (!row) { + return `${this.isAllSelected(row) ? 'select' : 'deselect'} all`; + } + return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`; + } + + approveClients() { + const formfields: FormfieldBase[] = [ + new DatepickerBase({ + controlName: 'actDate', + label: 'Date', + value: new Date(), + type: 'datetime-local', + required: true + }), + ]; + const data = { + title: 'Enter Clients Activation Date', + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const clientApprovalDialogRef = this.dialog.open(FormDialogComponent, { data }); + clientApprovalDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + this.bulkClientApproval(response.data); + } + }); + } + + bulkClientApproval(submittedData: any) { + const dateFormat = this.settingsService.dateFormat; + const activationDate = this.datePipe.transform(submittedData.value.actDate, dateFormat); + const locale = this.settingsService.language.code; + const formData = { + dateFormat, + activationDate, + locale + }; + const selectedAccounts = this.selection.selected.length; + const listSelectedAccounts = this.selection.selected; + let activatedAccounts = 0; + this.batchRequests = []; + let reqId = 1; + listSelectedAccounts.forEach((element: any) => { + const url = 'clients/' + element.id + '?command=activate'; + const bodyData = JSON.stringify(formData); + const batchData = { requestId: reqId++, relativeUrl: url, method: 'POST', body: bodyData }; + this.batchRequests.push(batchData); + }); + this.tasksService.submitBatchData(this.batchRequests).subscribe((response: any) => { + response.forEach((responseEle: any) => { + if (responseEle.statusCode = '200') { + activatedAccounts++; + responseEle.body = JSON.parse(responseEle.body); + if (selectedAccounts === activatedAccounts) { + this.reload(); + } + } + }); + }); + } + + applyFilter(filterValue: string = '') { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + /** + * Refetches data for the component + * TODO: Replace by a custom reload component instead of hard-coded back-routing. + */ + reload() { + const url: string = this.router.url; + this.router.navigateByUrl(`/checker-inbox-and-tasks`, { skipLocationChange: true }) + .then(() => this.router.navigate([url])); + } + +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.html new file mode 100644 index 000000000..84a09d6b4 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.html @@ -0,0 +1,78 @@ +
+ +
+ +
+ +
+
+ + + +

{{ office.name }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Client + {{ loan.clientName }} Loan + {{ loan.loanProductName}}({{loan.accountNo}}) Amount {{ loan.principal | number}} Loan Purpose {{ loan.loanPurposeName }}
+ +
+ +
+ +
+ +
+ +
+ + No pending loans for approval. +
+ +
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.scss new file mode 100644 index 000000000..a93c99f9a --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.scss @@ -0,0 +1,35 @@ +.tab-container { + padding: 1%; + margin: 1%; + + .mat-raised-button.mat-success { + color: #fff; + background-color: #008000ab; + } + + .view-details{ + cursor: pointer; + } +} + +.alert { + background-color: #E8F4FD; + padding: 6px 16px; + font-size: 0.875rem; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: 400; + line-height: 1.43; + border-radius: 4px; + letter-spacing: 0.01071em; + margin: 10px; + + .message { + padding: 8px 0; + font-size: 18px; + } + .alert-check { + color:#359FF4; + margin-right: 2px; + font-size: 1.4rem; + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.spec.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.spec.ts new file mode 100644 index 000000000..903bac954 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoanApprovalComponent } from './loan-approval.component'; + +describe('LoanApprovalComponent', () => { + let component: LoanApprovalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanApprovalComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanApprovalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.ts new file mode 100644 index 000000000..ee16d9149 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.ts @@ -0,0 +1,192 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SelectionModel } from '@angular/cdk/collections'; +import * as _ from 'lodash'; +import { DatePipe } from '@angular/common'; +import { MatTableDataSource } from '@angular/material/table'; +import { MatDialog } from '@angular/material/dialog'; + +/** Dialog Imports */ +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; + +/** Custom Services */ +import { TasksService } from '../../tasks.service'; +import { SettingsService } from 'app/settings/settings.service'; + +@Component({ + selector: 'mifosx-loan-approval', + templateUrl: './loan-approval.component.html', + styleUrls: ['./loan-approval.component.scss'] +}) +export class LoanApprovalComponent { + // settingsService: SettingsService; + /** Offices Data */ + offices: any; + /** Loans Data */ + loans: any; + /** Checks whether to show data or not */ + showData = false; + /** Data source for loans approval table. */ + dataSource: MatTableDataSource; + /** Row Selection Data */ + selection: SelectionModel; + /** Map data */ + idToNodeMap = {}; + /** Grouped Office Data */ + officesArray: any[]; + /** List of Requests */ + batchRequests: any[]; + /** Displayed Columns */ + displayedColumns: string[] = ['select', 'clientName', 'loan', 'amount', 'loanPurpose']; + + /** + * Retrieves the offices and loans data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + * @param {Dialog} dialog MatDialog. + * @param {DatePipe} datePipe Date Pipe. + * @param {router} router Router. + * @param {SettingsService} settingsService Settings Service. + * @param {TasksService} tasksService Tasks Service. + */ + constructor(private route: ActivatedRoute, + private dialog: MatDialog, + private datePipe: DatePipe, + private router: Router, + private settingsService: SettingsService, + private tasksService: TasksService) { + this.route.data.subscribe((data: { officesData: any, loansData: any }) => { + this.offices = data.officesData; + this.loans = data.loansData.pageItems; + this.setOfficeData(); + }); + } + + /** Group Office Data */ + setOfficeData() { + this.offices.forEach((officeEle: any) => { + officeEle.loans = []; + this.idToNodeMap[officeEle.id] = officeEle; + }); + this.loans.forEach((loanEle: any) => { + if (loanEle.status.pendingApproval) { + let tempOffice = {}; + if (loanEle.clientOfficeId) { + tempOffice = this.idToNodeMap[loanEle.clientOfficeId]; + tempOffice['loans'].push(loanEle); + } else { + if (loanEle.group) { + tempOffice = this.idToNodeMap[loanEle.group.officeId]; + tempOffice['loans'].push(loanEle); + } + } + } + }); + const finalArray: any[] = []; + this.offices.forEach((officeEle: any) => { + if (officeEle.loans && officeEle.loans.length > 0) { + this.showData = true; + finalArray.push(officeEle); + } + }); + this.officesArray = finalArray; + this.dataSource = new MatTableDataSource(this.officesArray); + this.selection = new SelectionModel(true, []); + } + + /** Whether the number of selected elements matches the total number of rows. */ + isAllSelected(dataSource2: any) { + if (dataSource2) { + const numSelected = this.selection.selected; + return _.difference(dataSource2, numSelected).length === 0; + } + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + masterToggle(dataSource3: any) { + if (dataSource3) { + this.isAllSelected(dataSource3) ? + dataSource3.forEach((row: any) => this.selection.deselect(row)) : + dataSource3.forEach((row: any) => this.selection.select(row)); + } + } + + /** The label for the checkbox on the passed row */ + checkboxLabel(row?: any): string { + if (!row) { + return `${this.isAllSelected(row) ? 'select' : 'deselect'} all`; + } + return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`; + } + + approveLoan() { + const approveLoanDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { heading: 'Approve Loan', dialogContext: 'Are you sure you want to Approve Loan' } + }); + approveLoanDialogRef.afterClosed().subscribe((response: { confirm: any }) => { + if (response.confirm) { + this.bulkLoanApproval(); + } + }); + } + + bulkLoanApproval() { + const dateFormat = this.settingsService.dateFormat; + const approvedOnDate = this.datePipe.transform(new Date(), dateFormat); + const locale = this.settingsService.language.code; + const formData = { + dateFormat, + approvedOnDate, + locale + }; + const selectedAccounts = this.selection.selected.length; + const listSelectedAccounts = this.selection.selected; + let approvedAccounts = 0; + this.batchRequests = []; + let reqId = 1; + listSelectedAccounts.forEach((element: any) => { + const url = 'loans/' + element.id + '?command=approve'; + const bodyData = JSON.stringify(formData); + const batchData = { requestId: reqId++, relativeUrl: url, method: 'POST', body: bodyData }; + this.batchRequests.push(batchData); + }); + this.tasksService.submitBatchData(this.batchRequests).subscribe((response: any) => { + response.forEach((responseEle: any) => { + if (responseEle.statusCode = '200') { + approvedAccounts++; + responseEle.body = JSON.parse(responseEle.body); + if (selectedAccounts === approvedAccounts) { + this.loanResource(); + } + } + }); + this.reload(); + }); + } + + applyFilter(filterValue: string = '') { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + loanResource() { + this.tasksService.getAllLoans().subscribe((response: any) => { + this.loans = response.pageItems; + this.loans = this.loans.filter((account: any) => { + return (account.status.waitingForDisbursal === true); + }); + this.dataSource = new MatTableDataSource(this.loans); + this.selection = new SelectionModel(true, []); + }); + } + + /** + * Refetches data for the component + * TODO: Replace by a custom reload component instead of hard-coded back-routing. + */ + reload() { + const url: string = this.router.url; + this.router.navigateByUrl(`/checker-inbox-and-tasks`, { skipLocationChange: true }) + .then(() => this.router.navigate([url])); + } + +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.html new file mode 100644 index 000000000..05f1dc27b --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.html @@ -0,0 +1,65 @@ +
+ +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Client Name {{loan.clientName}} Loan Account# {{loan.accountNo}} Loan Product {{loan.loanProductName}} Principal {{loan.principal | number}}
+ +
+ +
+ +
+ + No pending loan available for disbursal. +
+ +
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.scss new file mode 100644 index 000000000..a93c99f9a --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.scss @@ -0,0 +1,35 @@ +.tab-container { + padding: 1%; + margin: 1%; + + .mat-raised-button.mat-success { + color: #fff; + background-color: #008000ab; + } + + .view-details{ + cursor: pointer; + } +} + +.alert { + background-color: #E8F4FD; + padding: 6px 16px; + font-size: 0.875rem; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: 400; + line-height: 1.43; + border-radius: 4px; + letter-spacing: 0.01071em; + margin: 10px; + + .message { + padding: 8px 0; + font-size: 18px; + } + .alert-check { + color:#359FF4; + margin-right: 2px; + font-size: 1.4rem; + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.spec.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.spec.ts new file mode 100644 index 000000000..1a39d232f --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoanDisbursalComponent } from './loan-disbursal.component'; + +describe('LoanDisbursalComponent', () => { + let component: LoanDisbursalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoanDisbursalComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoanDisbursalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.ts new file mode 100644 index 000000000..5fa5c5de9 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.ts @@ -0,0 +1,140 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SelectionModel } from '@angular/cdk/collections'; +import { DatePipe } from '@angular/common'; +import { MatTableDataSource } from '@angular/material/table'; +import { MatDialog } from '@angular/material/dialog'; + +/** Dialog Imports */ +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; + +/** Custom Services */ +import { TasksService } from '../../tasks.service'; +import { SettingsService } from 'app/settings/settings.service'; + +@Component({ + selector: 'mifosx-loan-disbursal', + templateUrl: './loan-disbursal.component.html', + styleUrls: ['./loan-disbursal.component.scss'] +}) +export class LoanDisbursalComponent { + + /** Loans Data */ + loans: any; + /** Batch Requests */ + batchRequests: any[]; + /** Datasource for loans disbursal table */ + dataSource: MatTableDataSource; + /** Row Selection */ + selection: SelectionModel; + /** Displayed Columns for loan disbursal data */ + displayedColumns: string[] = ['select', 'client', 'loanAccountNumber', 'loanProduct', 'principal']; + + /** + * Retrieves the loans data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + * @param {Dialog} dialog MatDialog. + * @param {DatePipe} datePipe Date Pipe. + * @param {router} router Router. + * @param {SettingsService} settingsService Settings Service. + * @param {TasksService} tasksService Tasks Service. + */ + constructor(private route: ActivatedRoute, + private dialog: MatDialog, + private datePipe: DatePipe, + private settingsService: SettingsService, + private tasksService: TasksService) { + this.route.data.subscribe((data: { loansData: any }) => { + this.loans = data.loansData.pageItems; + this.loans = this.loans.filter((account: any) => { + return (account.status.waitingForDisbursal === true); + }); + this.dataSource = new MatTableDataSource(this.loans); + this.selection = new SelectionModel(true, []); + }); + } + + /** Whether the number of selected elements matches the total number of rows. */ + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.dataSource.data.length; + return numSelected === numRows; + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + masterToggle() { + this.isAllSelected() ? + this.selection.clear() : + this.dataSource.data.forEach((row: any) => this.selection.select(row)); + } + + /** The label for the checkbox on the passed row */ + checkboxLabel(row?: any): string { + if (!row) { + return `${this.isAllSelected() ? 'select' : 'deselect'} all`; + } + return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`; + } + + disburseLoan() { + const disburseLoanDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { heading: 'Loan Disbursal', dialogContext: 'Are you sure you want to Disburse Loan' } + }); + disburseLoanDialogRef.afterClosed().subscribe((response: { confirm: any }) => { + if (response.confirm) { + this.bulkLoanDisbursal(); + } + }); + } + + bulkLoanDisbursal() { + const dateFormat = this.settingsService.dateFormat; + const approvedOnDate = this.datePipe.transform(new Date(), dateFormat); + const locale = this.settingsService.language.code; + const formData = { + dateFormat, + approvedOnDate, + locale + }; + const selectedAccounts = this.selection.selected.length; + const listSelectedAccounts = this.selection.selected; + let approvedAccounts = 0; + this.batchRequests = []; + let reqId = 1; + listSelectedAccounts.forEach((element: any) => { + const url = 'loans/' + element.id + '?command=disburse'; + const bodyData = JSON.stringify(formData); + const batchData = { requestId: reqId++, relativeUrl: url, method: 'POST', body: bodyData }; + this.batchRequests.push(batchData); + }); + this.tasksService.submitBatchData(this.batchRequests).subscribe((response: any) => { + console.log(response); + response.forEach((responseEle: any) => { + if (responseEle.statusCode = '200') { + approvedAccounts++; + responseEle.body = JSON.parse(responseEle.body); + if (selectedAccounts === approvedAccounts) { + this.loanResource(); + } + } + }); + }); + } + + loanResource() { + this.tasksService.getAllLoans().subscribe((response: any) => { + this.loans = response.pageItems; + this.loans = this.loans.filter((account: any) => { + return (account.status.waitingForDisbursal === true); + }); + this.dataSource = new MatTableDataSource(this.loans); + this.selection = new SelectionModel(true, []); + }); + } + + applyFilter(filterValue: string = '') { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.html new file mode 100644 index 000000000..bcbe239bb --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.html @@ -0,0 +1,71 @@ +
+ +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Client {{loan.clientName}} Reschedule Request# {{loan.id}} Loan Account# {{loan.loanAccountNumber}} Reschedule Form {{loan.rescheduleFromDate | date}} Reschedule Reason {{loan.rescheduleReasonCodeValue.name}}
+ +
+ +
+ +
+ + No pending rescheduled loan available for this account. +
+ +
\ No newline at end of file diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.scss new file mode 100644 index 000000000..a93c99f9a --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.scss @@ -0,0 +1,35 @@ +.tab-container { + padding: 1%; + margin: 1%; + + .mat-raised-button.mat-success { + color: #fff; + background-color: #008000ab; + } + + .view-details{ + cursor: pointer; + } +} + +.alert { + background-color: #E8F4FD; + padding: 6px 16px; + font-size: 0.875rem; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: 400; + line-height: 1.43; + border-radius: 4px; + letter-spacing: 0.01071em; + margin: 10px; + + .message { + padding: 8px 0; + font-size: 18px; + } + .alert-check { + color:#359FF4; + margin-right: 2px; + font-size: 1.4rem; + } +} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.spec.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.spec.ts new file mode 100644 index 000000000..c7163876f --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RescheduleLoanComponent } from './reschedule-loan.component'; + +describe('RescheduleLoanComponent', () => { + let component: RescheduleLoanComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RescheduleLoanComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RescheduleLoanComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.ts b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.ts new file mode 100644 index 000000000..c9d266818 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.ts @@ -0,0 +1,132 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SelectionModel } from '@angular/cdk/collections'; +import { DatePipe } from '@angular/common'; +import * as _ from 'lodash'; +import { MatTableDataSource } from '@angular/material/table'; +import { MatDialog } from '@angular/material/dialog'; + +/** Dialog Imports */ +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; + +/** Custom Services */ +import { TasksService } from '../../tasks.service'; +import { SettingsRoutingModule } from 'app/settings/settings-routing.module'; +import { SettingsService } from 'app/settings/settings.service'; + +@Component({ + selector: 'mifosx-reschedule-loan', + templateUrl: './reschedule-loan.component.html', + styleUrls: ['./reschedule-loan.component.scss'] +}) +export class RescheduleLoanComponent implements OnInit { + + /** Loans Data */ + loans: any; + /** Datasource */ + dataSource: MatTableDataSource; + /** Rows Selection Data */ + selection: SelectionModel; + /** Batch Requests */ + batchRequests: any[]; + /** Displayed Columns */ + displayedColumns: string[] = ['select', 'client', 'rescheduleRequestNo', 'loanAccountNo', 'rescheduleForm', 'rescheduleReason']; + + /** + * Retrieves the reschedule loan data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + * @param {Dialog} dialog MatDialog. + * @param {DatePipe} datePipe Date Pipe. + * @param {router} router Router. + * @param {SettingsService} settingsService Settings Service. + * @param {TasksService} tasksService Tasks Service. + */ + constructor(private route: ActivatedRoute, + private dialog: MatDialog, + private datePipe: DatePipe, + private router: Router, + private settingsService: SettingsService, + private tasksService: TasksService) { + this.route.data.subscribe((data: { recheduleLoansData: any }) => { + this.loans = data.recheduleLoansData; + this.dataSource = new MatTableDataSource(this.loans); + this.selection = new SelectionModel(true, []); + }); + } + + ngOnInit() { + } + + /** Whether the number of selected elements matches the total number of rows. */ + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.dataSource.data.length; + return numSelected === numRows; + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + masterToggle() { + this.isAllSelected() ? + this.selection.clear() : + this.dataSource.data.forEach((row: any) => this.selection.select(row)); + } + + /** The label for the checkbox on the passed row */ + checkboxLabel(row?: any): string { + if (!row) { + return `${this.isAllSelected() ? 'select' : 'deselect'} all`; + } + return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.position + 1}`; + } + + approveBulkLoanReschedule() { + const rescheduleLoanDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { heading: 'Reschedule Loan', dialogContext: 'Are you sure you want to Reschedule Loan' } + }); + rescheduleLoanDialogRef.afterClosed().subscribe((response: { confirm: any }) => { + if (response.confirm) { + this.bulkLoanRescheduleApproval(); + } + }); + } + + bulkLoanRescheduleApproval() { + const dateFormat = this.settingsService.dateFormat; + const approvedOnDate = this.datePipe.transform(new Date(), dateFormat); + const locale = this.settingsService.language.code; + const formData = { + dateFormat, + approvedOnDate, + locale + }; + const listSelectedAccounts = this.selection.selected; + this.batchRequests = []; + let reqId = 1; + listSelectedAccounts.forEach((element: any) => { + const url = 'rescheduleloans/' + element.id + '?command=approve'; + const bodyData = JSON.stringify(formData); + const batchData = { requestId: reqId++, relativeUrl: url, method: 'POST', body: bodyData }; + this.batchRequests.push(batchData); + }); + this.tasksService.submitBatchData(this.batchRequests).subscribe((response: any) => { + console.log(response); + this.reload(); + }); + } + + applyFilter(filterValue: string = '') { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + /** + * Refetches data fot the component + * TODO: Replace by a custom reload component instead of hard-coded back-routing. + */ + reload() { + const url: string = this.router.url; + this.router.navigateByUrl(`/checker-inbox-and-tasks`, { skipLocationChange: true }) + .then(() => this.router.navigate([url])); + } + +} diff --git a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html new file mode 100644 index 000000000..100e45fc3 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html @@ -0,0 +1,36 @@ + diff --git a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.scss b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.spec.ts b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.spec.ts new file mode 100644 index 000000000..9eaeb8c83 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CheckerInboxAndTasksComponent } from './checker-inbox-and-tasks.component'; + +describe('CheckerInboxAndTasksComponent', () => { + let component: CheckerInboxAndTasksComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CheckerInboxAndTasksComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CheckerInboxAndTasksComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.ts b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.ts new file mode 100644 index 000000000..39651b689 --- /dev/null +++ b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.ts @@ -0,0 +1,16 @@ +/** Angular Imports */ +import { Component } from '@angular/core'; + +/** + * Checker Inbox and Tasks Component + */ +@Component({ + selector: 'mifosx-checker-inbox-and-tasks', + templateUrl: './checker-inbox-and-tasks.component.html', + styleUrls: ['./checker-inbox-and-tasks.component.scss'] +}) +export class CheckerInboxAndTasksComponent { + + constructor() { } + +} diff --git a/src/app/tasks/common-resolvers/getGroupedClientsData.resolver.ts b/src/app/tasks/common-resolvers/getGroupedClientsData.resolver.ts new file mode 100644 index 000000000..6f892b169 --- /dev/null +++ b/src/app/tasks/common-resolvers/getGroupedClientsData.resolver.ts @@ -0,0 +1,30 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { TasksService } from '../tasks.service'; + +/** + * Grouped Client Data data resolver. + */ +@Injectable() +export class GetGroupedClientsData implements Resolve { + + /** + * @param {TasksService} tasksService Tasks service. + */ + constructor(private tasksService: TasksService) {} + + /** + * Returns the grouped client data. + * @returns {Observable} + */ + resolve(): Observable { + return this.tasksService.getGroupedClientsData(); + } + +} diff --git a/src/app/tasks/common-resolvers/getLoans.resolver.ts b/src/app/tasks/common-resolvers/getLoans.resolver.ts new file mode 100644 index 000000000..6b1074e32 --- /dev/null +++ b/src/app/tasks/common-resolvers/getLoans.resolver.ts @@ -0,0 +1,30 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { TasksService } from '../tasks.service'; + +/** + * Loans data resolver. + */ +@Injectable() +export class GetLoans implements Resolve { + + /** + * @param {TasksService} tasksService Tasks service. + */ + constructor(private tasksService: TasksService) { } + + /** + * Returns all the loans data. + * @returns {Observable} + */ + resolve(): Observable { + return this.tasksService.getAllLoans(); + } + +} diff --git a/src/app/tasks/common-resolvers/getOffices.resolver.ts b/src/app/tasks/common-resolvers/getOffices.resolver.ts new file mode 100644 index 000000000..bc80d8d93 --- /dev/null +++ b/src/app/tasks/common-resolvers/getOffices.resolver.ts @@ -0,0 +1,30 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { TasksService } from '../tasks.service'; + +/** + * Offices data resolver. + */ +@Injectable() +export class GetOffices implements Resolve { + + /** + * @param {TasksService} tasksService Tasks service. + */ + constructor(private tasksService: TasksService) { } + + /** + * Returns the offices data. + * @returns {Observable} + */ + resolve(): Observable { + return this.tasksService.getAllOffices(); + } + +} diff --git a/src/app/tasks/common-resolvers/getRescheduleLoans.resolver.ts b/src/app/tasks/common-resolvers/getRescheduleLoans.resolver.ts new file mode 100644 index 000000000..39c275913 --- /dev/null +++ b/src/app/tasks/common-resolvers/getRescheduleLoans.resolver.ts @@ -0,0 +1,30 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { TasksService } from '../tasks.service'; + +/** + * Pending Reschedule Loans data resolver. + */ +@Injectable() +export class GetRescheduleLoans implements Resolve { + + /** + * @param {TasksService} tasksService Tasks service. + */ + constructor(private tasksService: TasksService) { } + + /** + * Returns the pending reschedule data. + * @returns {Observable} + */ + resolve(): Observable { + return this.tasksService.getPendingRescheduleLoans(); + } + +} diff --git a/src/app/tasks/common-resolvers/getmakercheckers.resolver.ts b/src/app/tasks/common-resolvers/getmakercheckers.resolver.ts new file mode 100644 index 000000000..16c4531fa --- /dev/null +++ b/src/app/tasks/common-resolvers/getmakercheckers.resolver.ts @@ -0,0 +1,30 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { TasksService } from '../tasks.service'; + +/** + * Maker Checker Data data resolver. + */ +@Injectable() +export class GetMakerCheckers implements Resolve { + + /** + * @param {TasksService} tasksService Tasks service. + */ + constructor(private tasksService: TasksService) {} + + /** + * Returns the maker checker data. + * @returns {Observable} + */ + resolve(): Observable { + return this.tasksService.getMakerCheckerData(); + } + +} diff --git a/src/app/tasks/common-resolvers/makerCheckerTemplate.resolver.ts b/src/app/tasks/common-resolvers/makerCheckerTemplate.resolver.ts new file mode 100644 index 000000000..685e6e379 --- /dev/null +++ b/src/app/tasks/common-resolvers/makerCheckerTemplate.resolver.ts @@ -0,0 +1,30 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { TasksService } from '../tasks.service'; + +/** + * Maker Checker Template resolver. + */ +@Injectable() +export class MakerCheckerTemplate implements Resolve { + + /** + * @param {TasksService} tasksService Tasks service. + */ + constructor(private tasksService: TasksService) { } + + /** + * Returns the maker checker template data. + * @returns {Observable} + */ + resolve(): Observable { + return this.tasksService.getMakerCheckerTemplate(); + } + +} diff --git a/src/app/tasks/tasks-routing.module.ts b/src/app/tasks/tasks-routing.module.ts new file mode 100644 index 000000000..1e0fcf573 --- /dev/null +++ b/src/app/tasks/tasks-routing.module.ts @@ -0,0 +1,94 @@ +/** Angular Imports */ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +/** Routing Imports */ +import { Route } from '../core/route/route.service'; + +/** Translation Imports */ +import { extract } from '../core/i18n/i18n.service'; + +/** Custom Components */ +import { CheckerInboxAndTasksComponent } from './checker-inbox-and-tasks/checker-inbox-and-tasks.component'; +import { CheckerInboxComponent } from './checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component'; +import { ClientApprovalComponent } from './checker-inbox-and-tasks-tabs/client-approval/client-approval.component'; +import { LoanApprovalComponent } from './checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component'; +import { LoanDisbursalComponent } from './checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component'; +import { RescheduleLoanComponent } from './checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component'; + +/** Custom Resolvers */ +import { GetMakerCheckers } from './common-resolvers/getmakercheckers.resolver'; +import { GetGroupedClientsData } from './common-resolvers/getGroupedClientsData.resolver'; +import { GetOffices } from './common-resolvers/getOffices.resolver'; +import { GetLoans } from './common-resolvers/getLoans.resolver'; +import { GetRescheduleLoans } from './common-resolvers/getRescheduleLoans.resolver'; +import { MakerCheckerTemplate } from './common-resolvers/makerCheckerTemplate.resolver'; + +/** Tasks Routes */ +const routes: Routes = [ + Route.withShell([ + { + path: 'checker-inbox-and-tasks', + component: CheckerInboxAndTasksComponent, + data: { title: extract('Checker Inbox & Tasks'), breadcrumb: 'Checker Inbox & Tasks' }, + children: [ + { + path: 'checker-inbox', + component: CheckerInboxComponent, + data: { title: extract('Checker Inbox') }, + resolve: { + makerCheckerResource: GetMakerCheckers, + makerCheckerTemplate: MakerCheckerTemplate + }, + }, + { + path: 'client-approval', + component: ClientApprovalComponent, + data: { title: extract('Client Approval') }, + resolve: { + groupedClientData: GetGroupedClientsData + }, + }, + { + path: 'loan-approval', + component: LoanApprovalComponent, + data: { title: extract('Laon Approval') }, + resolve: { + officesData: GetOffices, + loansData: GetLoans + }, + }, + { + path: 'loan-disbursal', + component: LoanDisbursalComponent, + data: { title: extract('Loan Disbursal') }, + resolve: { + loansData: GetLoans + } + }, + { + path: 'reschedule-loan', + component: RescheduleLoanComponent, + data: { title: extract('Reschedule Loan') }, + resolve: { + recheduleLoansData: GetRescheduleLoans + } + } + ] + } + ]) +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + GetMakerCheckers, + GetGroupedClientsData, + GetOffices, + GetLoans, + GetRescheduleLoans, + MakerCheckerTemplate + ] +}) +export class TasksRoutingModule { } diff --git a/src/app/tasks/tasks.module.ts b/src/app/tasks/tasks.module.ts new file mode 100644 index 000000000..f6dc90b8f --- /dev/null +++ b/src/app/tasks/tasks.module.ts @@ -0,0 +1,38 @@ +import { NgModule } from '@angular/core'; +import { DatePipe } from '@angular/common'; + +/** Custom Modules */ +import { SharedModule } from 'app/shared/shared.module'; +import { PipesModule } from '../pipes/pipes.module'; +import { TasksRoutingModule } from './tasks-routing.module'; +import { DirectivesModule } from '../directives/directives.module'; + +/** Custom Components */ +import { CheckerInboxAndTasksComponent } from './checker-inbox-and-tasks/checker-inbox-and-tasks.component'; +import { CheckerInboxComponent } from './checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component'; +import { ClientApprovalComponent } from './checker-inbox-and-tasks-tabs/client-approval/client-approval.component'; +import { LoanApprovalComponent } from './checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component'; +import { LoanDisbursalComponent } from './checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component'; +import { RescheduleLoanComponent } from './checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component'; + +/** + * Tasks Module + */ +@NgModule({ + imports: [ + SharedModule, + TasksRoutingModule, + DirectivesModule, + PipesModule + ], + declarations: [ + CheckerInboxAndTasksComponent, + CheckerInboxComponent, + ClientApprovalComponent, + LoanApprovalComponent, + LoanDisbursalComponent, + RescheduleLoanComponent + ], + providers: [DatePipe] +}) +export class TasksModule { } diff --git a/src/app/tasks/tasks.service.spec.ts b/src/app/tasks/tasks.service.spec.ts new file mode 100644 index 000000000..02fd407b7 --- /dev/null +++ b/src/app/tasks/tasks.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { TasksService } from './tasks.service'; + +describe('TasksService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: TasksService = TestBed.get(TasksService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/tasks.service.ts b/src/app/tasks/tasks.service.ts new file mode 100644 index 000000000..6cecb42e6 --- /dev/null +++ b/src/app/tasks/tasks.service.ts @@ -0,0 +1,105 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** + * Tasks Service + */ +@Injectable({ + providedIn: 'root' +}) +export class TasksService { + + /** + * @param {HttpClient} http Http Client to send requests. + */ + constructor(private http: HttpClient) { } + + /** + * Get Maker Checker Data + * @param {searchData} SearchData search the maker checker data. + */ + getMakerCheckerData(searchData?: any): Observable { + let httpParams = new HttpParams(); + if (searchData) { + const propNames = Object.getOwnPropertyNames(searchData); + for (let i = 0; i < propNames.length; i++) { + const propName = propNames[i]; + if (!(searchData[propName] === '' || searchData[propName] === undefined || searchData[propName] === null)) { + httpParams = httpParams.set(propName, searchData[propName]); + } + } + } + return this.http.get('/makercheckers', { params: httpParams }); + } + + /** + * Get Maker Checker Template + */ + getMakerCheckerTemplate(): Observable { + return this.http.get('/makercheckers'); + } + + /** + * Get Grouped Clients Data + */ + getGroupedClientsData(): Observable { + const httpParams = new HttpParams().set('limit', '1000') + .set('sqlSearch', 'c.status_enum=100'); + return this.http.get('/clients', { params: httpParams}); + } + + /** + * Get all Offices Data + */ + getAllOffices(): Observable { + return this.http.get('/offices'); + } + + /** + * Get all Loans Data + */ + getAllLoans(): Observable { + const httpParams = new HttpParams().set('limit', '1000') + .set('sqlSearch', 'l.loan_status_id in (100,200)'); + return this.http.get('/loans', { params: httpParams }); + } + + /** + * Get Pending Rescheduled Loans + */ + getPendingRescheduleLoans(): Observable { + const httpParams = new HttpParams().set('command', 'pending'); + return this.http.get('/rescheduleloans', { params: httpParams }); + } + + /** + * Submit data in batches. + * @param {data} Data to be submitted + */ + submitBatchData(data: any): Observable { + return this.http.post('/batches', data); + } + + /** + * Execute Maker Checker Approve and Reject Action. + * @param {makerCheckerId} MakerCheckerId + * @param {command} Command + */ + executeMakerCheckerAction(makerCheckerId: any, command: any): Observable { + const httpParams = new HttpParams().set('command', command); + return this.http.post(`/makercheckers/${makerCheckerId}`, {}, { params: httpParams }); + } + + /** + * Execute Maker Checker Delete Action + * @param {makerCheckerId} MakerCheckerId + */ + deleteMakerChecker(makerCheckerId: any): Observable { + return this.http.delete(`/makercheckers/${makerCheckerId}`); + } + +}