diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 84e16dfb3..67bce1a3a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -35,6 +35,7 @@ import { TemplatesModule } from './templates/templates.module'; import { UsersModule } from './users/users.module'; import { ReportsModule } from './reports/reports.module'; import { NotificationsModule } from './notifications/notifications.module'; +import { CollectionsModule } from './collections/collections.module'; /** Main Routing Module */ import { AppRoutingModule } from './app-routing.module'; @@ -68,6 +69,7 @@ import { AppRoutingModule } from './app-routing.module'; TemplatesModule, UsersModule, NotificationsModule, + CollectionsModule, AppRoutingModule, ], declarations: [WebAppComponent, NotFoundComponent], diff --git a/src/app/collections/collections-routing.module.ts b/src/app/collections/collections-routing.module.ts new file mode 100644 index 000000000..84841176c --- /dev/null +++ b/src/app/collections/collections-routing.module.ts @@ -0,0 +1,39 @@ +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 { IndividualCollectionSheetComponent } from './individual-collection-sheet/individual-collection-sheet.component'; + +/** Custom Resolvers */ +import { GetOfficesResolver } from './individual-collection-sheet/get-offices.resolver'; + +const routes: Routes = [ + Route.withShell([ + { + path: 'collections', + children: [ + { + path: 'individual-collection-sheet', + data: { title: extract('Individual Collection Sheet'), breadcrumb: 'Individual Collection Sheet', routeParamBreadcrumb: false }, + component: IndividualCollectionSheetComponent, + resolve: { + officesData: GetOfficesResolver + } + }, + ] + } + ]) +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + providers: [GetOfficesResolver], + exports: [RouterModule] +}) +export class CollectionsRoutingModule { } diff --git a/src/app/collections/collections.module.ts b/src/app/collections/collections.module.ts new file mode 100644 index 000000000..340932e66 --- /dev/null +++ b/src/app/collections/collections.module.ts @@ -0,0 +1,22 @@ +/** Angular Imports */ +import { NgModule } from '@angular/core'; + +/** Custom Modules */ +import { SharedModule } from '../shared/shared.module'; +import { PipesModule } from '../pipes/pipes.module'; +import { CollectionsRoutingModule } from './collections-routing.module'; + +/** Custom Components */ +import { IndividualCollectionSheetComponent } from './individual-collection-sheet/individual-collection-sheet.component'; + +@NgModule({ + imports: [ + SharedModule, + PipesModule, + CollectionsRoutingModule + ], + declarations: [ + IndividualCollectionSheetComponent + ] +}) +export class CollectionsModule { } diff --git a/src/app/collections/collections.service.spec.ts b/src/app/collections/collections.service.spec.ts new file mode 100644 index 000000000..383ec356e --- /dev/null +++ b/src/app/collections/collections.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { CollectionsService } from './collections.service'; + +describe('CollectionsService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: CollectionsService = TestBed.get(CollectionsService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/collections/collections.service.ts b/src/app/collections/collections.service.ts new file mode 100644 index 000000000..642ca7990 --- /dev/null +++ b/src/app/collections/collections.service.ts @@ -0,0 +1,54 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** + * Collections Service + */ +@Injectable({ + providedIn: 'root' +}) +export class CollectionsService { + + constructor(private http: HttpClient) { } + + /** + * Returns all the offices + */ + getOffices(): Observable { + return this.http.get(`/offices`); + } + + /** + * Returns the loan officer(staffs) for the particular office + * @param {officeId} OfficeId any + */ + getStaffs(officeId: any): Observable { + const httpParams = new HttpParams() + .set('officeId', officeId) + .set('status', 'all'); + return this.http.get(`/staff`, { params: httpParams }); + } + + /** + * Retrieves the Collection Sheet Data + * @param {data} data any + */ + retrieveCollectionSheetData(data: any): Observable { + const httpParams = new HttpParams().set('command', 'generateCollectionSheet'); + return this.http.post(`/collectionsheet`, data, { params: httpParams }); + } + + /** + * Executes the Save Collection Sheet Data + * @param {data} data any + */ + executeSaveCollectionSheet(data: any): Observable { + const httpParams = new HttpParams().set('command', 'saveCollectionSheet'); + return this.http.post(`/collectionsheet`, data, { params: httpParams }); + } + +} diff --git a/src/app/collections/individual-collection-sheet/get-offices.resolver.ts b/src/app/collections/individual-collection-sheet/get-offices.resolver.ts new file mode 100644 index 000000000..dd5e6d841 --- /dev/null +++ b/src/app/collections/individual-collection-sheet/get-offices.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 { CollectionsService } from '../collections.service'; + +/** + * Returns all the offices data. + */ +@Injectable() +export class GetOfficesResolver implements Resolve { + + /** + * @param {CollectionsService} CollectionsService Collections service. + */ + constructor(private collectionsService: CollectionsService) { } + + /** + * Returns the Collection Office Data. + * @returns {Observable} + */ + resolve(): Observable { + return this.collectionsService.getOffices(); + } + +} diff --git a/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.html b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.html new file mode 100644 index 000000000..f36520529 --- /dev/null +++ b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.html @@ -0,0 +1,176 @@ + + +
+ +
+ + No repayments and disbursal are available for selected client and meeting date +
+ +
+ +
+ +
+ + + Branch Office + + + {{ office.name }} + + + + Branch Office is required + + + + + Repayment date + + + + + Repayment Date is required + + + + + Staff + + + {{ loanOfficer.displayName }} + + + + +
+ + + + + + +
+ +
+ +
+ +
+ + + +
+ +
+ + + +

Due Collections

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Loan Account# {{ loan.accountId }} Product Name {{ loan.productShortName }}({{ loan.productId }}) Client Name {{ loan.clientName }}({{ loan.clientId }}) Total Due {{ loan.totalDue }} Charges {{ loan.feeDue }} Actions + +
+ + + +
+ + + +

Due Savings Collections

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Deposit Account {{ saving.depositAccountType }} Savings Account # {{ saving.accountId }} Product Name {{ saving.productName }}({{ saving.productId }}) Client Name {{ saving.clientName }}({{ saving.clientId }}) Total Due {{ saving.dueAmount }} Actions + +
+ + + +
+ + + + + + +
+ +
+ +
diff --git a/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.scss b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.scss new file mode 100644 index 000000000..fb5d3842b --- /dev/null +++ b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.scss @@ -0,0 +1,42 @@ +.container { + + max-width: 37em; + + .margin-v { + margin: 1em 0; + } + + .margin-b { + margin: 0 0 1em 0; + } + + .margin-t { + margin-top: 1em; + } + +} + +.showCollectionSheet { + .container { + max-width: 73rem; + } +} + +.alert { + background-color: rgb(253, 236, 234); + 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; + + .message { + padding: 8px 0; + } + .alert-check { + color:#f44336; + margin-right: 2px; + } +} diff --git a/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.spec.ts b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.spec.ts new file mode 100644 index 000000000..0937bdb6a --- /dev/null +++ b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IndividualCollectionSheetComponent } from './individual-collection-sheet.component'; + +describe('IndividualCollectionSheetComponent', () => { + let component: IndividualCollectionSheetComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ IndividualCollectionSheetComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(IndividualCollectionSheetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.ts b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.ts new file mode 100644 index 000000000..57bfac3d4 --- /dev/null +++ b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.ts @@ -0,0 +1,331 @@ +/** Angular Imports */ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatTableDataSource, MatPaginator, MatSort, MatDialog } from '@angular/material'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DatePipe } from '@angular/common'; + +/** Services Import */ +import { CollectionsService } from '../collections.service'; + +/** Custom Dialogs */ +import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; + +/** Custom Models */ +import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; +import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; +import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base'; + +/** + * Individual Collection Sheet + */ +@Component({ + selector: 'mifosx-individual-collection-sheet', + templateUrl: './individual-collection-sheet.component.html', + styleUrls: ['./individual-collection-sheet.component.scss'] +}) +export class IndividualCollectionSheetComponent implements OnInit { + + /** Offices Data */ + officesData: any; + /** Loan Officer Data */ + loanOfficerData: any = []; + /** Loans Data */ + loansData: any = []; + /** Savings Data */ + savingsData: any = []; + /** Minimum Date allowed. */ + minDate = new Date(2000, 0, 1); + /** Maximum Date allowed. */ + maxDate = new Date(); + /** Collection Sheet form. */ + collectionSheetForm: FormGroup; + /** Toggles b/w form and table */ + isCollapsed = false; + /** Collections Sheet Data */ + collectionSheetData: any; + /** checks and stores the local storage values */ + Success: boolean; + /** Bulk Disbursement Transactions Data */ + bulkDisbursementTransactionsData = {}; + /** Bulk Repayment Transactions Data */ + bulkRepaymentTransactions: any[] = []; + /** Bulk Savings Due Data */ + bulkSavingsDueTransactions: any[] = []; + /** Checks if there is no client data in the response */ + noData = false; + + /** Columns to be displayed in loans table. */ + loansDisplayedColumns: string[] = ['loanAccount', 'productName', 'clientName', 'totalDue', 'charges', 'actions']; + /** Columns to be displayed in savings table. */ + savingsDisplayedColumns: string[] = ['depositAccount', 'savingsAccountNo', 'productName', 'clientName', 'totalDue', 'actions']; + + /** Data source for loans table. */ + loansDataSource: MatTableDataSource; + /** Data source for savings table. */ + savingsDataSource: MatTableDataSource; + + + /** Paginator for table. */ + @ViewChild(MatPaginator, { read: true }) paginator: MatPaginator; + /** Sorter for table. */ + @ViewChild(MatSort, { read: true }) sort: MatSort; + + /** + * Retrieves the offices data from `resolve`. + * @param {FormBuilder} formBuilder Form Builder. + * @param {OrganizationService} collectionsService Organization Service. + * @param {Route} route Route. + * @param {DatePipe} datePipe Date Pipe to format date. + * @param {Dialog} dialog Dialog component. + * @param {Router} router Router for navigation. + */ + constructor(private formBuilder: FormBuilder, + private collectionsService: CollectionsService, + private route: ActivatedRoute, + private datePipe: DatePipe, + public dialog: MatDialog, + private router: Router, ) { + this.route.data.subscribe((data: { officesData: any }) => { + this.officesData = data.officesData; + }); + } + + ngOnInit() { + if (localStorage.getItem('Success')) { + localStorage.removeItem('Success'); + this.Success = true; + setTimeout(() => { this.Success = false; }, 3000); + } + this.createCollectionSheetForm(); + this.buildDependencies(); + } + + /** + * Creates the Individual Collection Sheet Form + */ + createCollectionSheetForm() { + this.collectionSheetForm = this.formBuilder.group({ + 'officeId': ['', Validators.required], + 'transactionDate': [new Date(), Validators.required], + 'staffId': [''] + }); + } + + /** + * Checks for the office id value change + */ + buildDependencies() { + this.collectionSheetForm.get('officeId').valueChanges.subscribe((value: any) => { + this.collectionsService.getStaffs(value).subscribe((response: any) => { + this.loanOfficerData = response; + }); + }); + } + + /** + * Initializes the data source, paginator and sorter for loans and savings table. + * @param {any} data + */ + organizeData(data: any) { + data.clients.forEach((client: any) => { + if (client.loans) { + client.loans.forEach((loan: any) => { + const loanData = { + ...loan, + clientName: client.clientName, + clientId: client.clientId + }; + this.loansData.push(loanData); + }); + } + if (client.savings) { + client.savings.forEach((saving: any) => { + const savingData = { + ...saving, + clientName: client.clientName, + clientId: client.clientId + }; + this.savingsData.push(savingData); + }); + } + }); + if (this.loansData.length > 0) { + this.loansDataSource = new MatTableDataSource(this.loansData); + this.loansDataSource.paginator = this.paginator; + this.loansDataSource.sort = this.sort; + } + if (this.savingsData.length > 0) { + this.savingsDataSource = new MatTableDataSource(this.savingsData); + this.savingsDataSource.paginator = this.paginator; + this.savingsDataSource.sort = this.sort; + } + } + + /** + * Gets Loan Total Due Amount + */ + getLoanTotalDueAmount (loan: any) { + let principalInterestDue = loan.totalDue; + let chargesDue = loan.chargesDue; + if (isNaN(principalInterestDue)) { + principalInterestDue = 0; + } + if (isNaN(chargesDue)) { + chargesDue = 0; + } + return Math.ceil((Number(principalInterestDue) + Number(chargesDue)) * 100) / 100; + } + + /** + * Shows the payment dialog box and stores the return data + */ + showAndStorePaymentDetails(type: any, selectedData: any, index: number) { + const formfields: FormfieldBase[] = [ + new SelectBase({ + controlName: 'paymentTypeId', + label: 'Payment Type', + options: { label: 'name', value: 'id', data: this.collectionSheetData.paymentTypeOptions }, + required: false + }), + new InputBase({ + controlName: 'accountNumber', + label: 'Account #', + type: 'number', + required: false + }), + new InputBase({ + controlName: 'checkNumber', + label: 'Cheque #', + type: 'number', + required: false + }), + new InputBase({ + controlName: 'routingCode', + label: 'Routing Code', + type: 'text', + required: false + }), + new InputBase({ + controlName: 'receiptNumber', + label: 'Receipt #', + type: 'number', + required: false + }), + new InputBase({ + controlName: 'bankNumber', + label: 'Bank #', + type: 'number', + required: false + }), + ]; + const data = { + title: `Payment for ${type === 'loans' ? 'Loan' : 'Saving'} Id ${type === 'loans' ? selectedData.loanId : selectedData.savingsId}`, + layout: { addButtonText: 'Confirm' }, + formfields: formfields + }; + const showPaymentDetailsDialogRef = this.dialog.open(FormDialogComponent, { data }); + showPaymentDetailsDialogRef.afterClosed().subscribe((response: any) => { + if (response.data) { + if (type === 'loans') { + const totalDue = this.getLoanTotalDueAmount(selectedData); + const loanTransaction = { + loanId: selectedData.loanId, + transactionAmount: totalDue + }; + if (response.data.value.paymentTypeId !== '') { + loanTransaction['paymentTypeId'] = response.data.value.paymentTypeId; + loanTransaction['accountNumber'] = response.data.value.accountNumber; + loanTransaction['checkNumber'] = response.data.value.checkNumber; + loanTransaction['routingCode'] = response.data.value.routingCode; + loanTransaction['receiptNumber'] = response.data.value.receiptNumber; + loanTransaction['bankNumber'] = response.data.value.bankNumber; + } + this.bulkRepaymentTransactions.push(loanTransaction); + } else { + let dueAmount = selectedData.dueAmount; + if (isNaN(dueAmount)) { + dueAmount = 0; + } + const savingsTransaction = { + savingsId: selectedData.savingsId, + transactionAmount: dueAmount, + depositAccountType: selectedData.depositAccountType === 'Saving Deposit' ? 100 : (selectedData.depositAccountType === 'Recurring Deposit' ? 300 : 400) + }; + if (response.data.paymentTypeId !== '') { + savingsTransaction['paymentTypeId'] = response.data.paymentTypeId; + savingsTransaction['accountNumber'] = response.data.accountNumber; + savingsTransaction['checkNumber'] = response.data.checkNumber; + savingsTransaction['routingCode'] = response.data.routingCode; + savingsTransaction['receiptNumber'] = response.data.receiptNumber; + savingsTransaction['bankNumber'] = response.data.bankNumber; + } + if (savingsTransaction.transactionAmount > 0) { + this.bulkSavingsDueTransactions.push(savingsTransaction); + } + } + } + }); + } + + /** + * Searches collection sheet data + */ + previewCollectionSheet() { + // TODO: Update once language and date settings are setup + const locale = 'en'; + const dateFormat = 'dd MMMM yyyy'; + const collectionSheet = { + ...this.collectionSheetForm.value, + 'transactionDate': this.datePipe.transform(this.collectionSheetForm.value.transactionDate, dateFormat), + dateFormat, + locale + }; + if (collectionSheet.staffId === '') { + delete collectionSheet.staffId; + } + this.collectionsService.retrieveCollectionSheetData(collectionSheet).subscribe((response: any) => { + if (response.clients.length > 0) { + this.collectionSheetData = response; + this.organizeData(response); + this.isCollapsed = true; + } else { + this.noData = true; + setTimeout(() => { this.noData = false; }, 3000); + } + }); + } + + /** + * Submit the data with all the payments data + */ + submit() { + // TODO: Update once language and date settings are setup + const locale = 'en'; + const dateFormat = 'dd MMMM yyyy'; + this.bulkDisbursementTransactionsData['bulkRepaymentTransactions'] = this.bulkRepaymentTransactions; + this.bulkDisbursementTransactionsData['bulkSavingsDueTransactions'] = this.bulkSavingsDueTransactions; + const finalSubmitData = { + dateFormat, + locale, + actualDisbursementDate: this.datePipe.transform(this.collectionSheetForm.value.transactionDate, dateFormat), + transactionDate: this.datePipe.transform(this.collectionSheetForm.value.transactionDate, dateFormat), + bulkDisbursementTransactions: this.bulkDisbursementTransactionsData + }; + this.collectionsService.executeSaveCollectionSheet(finalSubmitData).subscribe(() => { + this.reload(); + localStorage.setItem('Success', '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(`/collections`, { skipLocationChange: true }) + .then(() => this.router.navigate([url])); + } + +} diff --git a/src/app/core/shell/sidenav/sidenav.component.html b/src/app/core/shell/sidenav/sidenav.component.html index 4464ae862..d029508c9 100644 --- a/src/app/core/shell/sidenav/sidenav.component.html +++ b/src/app/core/shell/sidenav/sidenav.component.html @@ -73,6 +73,12 @@ Navigation + + + + + Individual Collection Sheet +