feat: add individual collection sheet (#1204)

Fixes: #255
This commit is contained in:
Muskan Khedia
2020-08-15 09:56:03 +05:30
committed by GitHub
parent 048a3dd951
commit 9861182b7d
11 changed files with 739 additions and 0 deletions
+2
View File
@@ -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],
@@ -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 { }
+22
View File
@@ -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 { }
@@ -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();
});
});
@@ -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<any> {
return this.http.get(`/offices`);
}
/**
* Returns the loan officer(staffs) for the particular office
* @param {officeId} OfficeId any
*/
getStaffs(officeId: any): Observable<any> {
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<any> {
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<any> {
const httpParams = new HttpParams().set('command', 'saveCollectionSheet');
return this.http.post(`/collectionsheet`, data, { params: httpParams });
}
}
@@ -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<Object> {
/**
* @param {CollectionsService} CollectionsService Collections service.
*/
constructor(private collectionsService: CollectionsService) { }
/**
* Returns the Collection Office Data.
* @returns {Observable<any>}
*/
resolve(): Observable<any> {
return this.collectionsService.getOffices();
}
}
@@ -0,0 +1,176 @@
<mat-card class="container" *ngIf="!isCollapsed">
<div class="alert" *ngIf="noData">
<div class="message">
<i class="fa fa-exclamation-circle alert-check"></i>
No repayments and disbursal are available for selected client and meeting date
</div>
</div>
<form [formGroup]="collectionSheetForm" (ngSubmit)="previewCollectionSheet()">
<div fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column" fxLayoutAlign.gt-sm="start center">
<mat-form-field fxFlex="98%">
<mat-label>Branch Office</mat-label>
<mat-select formControlName="officeId" required>
<mat-option *ngFor="let office of officesData" [value]="office.id">
{{ office.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="collectionSheetForm.controls.officeId.hasError('required')">
Branch Office is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="98%">
<mat-label>Repayment date</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="transactionDatePicker" required
formControlName="transactionDate" required>
<mat-datepicker-toggle matSuffix [for]="transactionDatePicker"></mat-datepicker-toggle>
<mat-datepicker #transactionDatePicker></mat-datepicker>
<mat-error *ngIf="collectionSheetForm.controls.transactionDate.hasError('required')">
Repayment Date is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="98%">
<mat-label>Staff</mat-label>
<mat-select formControlName="staffId">
<mat-option *ngFor="let loanOfficer of loanOfficerData" [value]="loanOfficer.id">
{{ loanOfficer.displayName }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-card-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="5px">
<button type="button" mat-raised-button [routerLink]="['../']">Cancel</button>
<button mat-raised-button color="primary" [disabled]="!collectionSheetForm.valid">
<fa-icon icon="search"></fa-icon>&nbsp; Collection Sheet
</button>
</mat-card-actions>
</form>
</mat-card>
<div class="showCollectionSheet">
<div class="container" *ngIf="isCollapsed">
<mat-card>
<div class="m-b-20">
<button mat-raised-button color="primary" (click)="isCollapsed = false">
Parameters
</button>
</div>
<ng-container *ngIf="loansDataSource">
<h2 class="mat-h2">Due Collections</h2>
<table class="mat-elevation-z1" mat-table [dataSource]="loansDataSource" matSort>
<ng-container matColumnDef="loanAccount">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Loan Account# </th>
<td mat-cell *matCellDef="let loan"> {{ loan.accountId }}</td>
</ng-container>
<ng-container matColumnDef="productName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Product Name </th>
<td mat-cell *matCellDef="let loan"> {{ loan.productShortName }}({{ loan.productId }}) </td>
</ng-container>
<ng-container matColumnDef="clientName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Client Name </th>
<td mat-cell *matCellDef="let loan"> {{ loan.clientName }}({{ loan.clientId }}) </td>
</ng-container>
<ng-container matColumnDef="totalDue">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Total Due </th>
<td mat-cell *matCellDef="let loan"> {{ loan.totalDue }} </td>
</ng-container>
<ng-container matColumnDef="charges">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Charges </th>
<td mat-cell *matCellDef="let loan"> {{ loan.feeDue }} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Actions </th>
<td mat-cell *matCellDef="let loan; let i = index">
<button mat-raised-button color="primary" (click)="showAndStorePaymentDetails('loans', loan, i)">Add Payment</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="loansDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: loansDisplayedColumns;"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
</ng-container>
<ng-container *ngIf="savingsDataSource">
<h2 class="mat-h2">Due Savings Collections</h2>
<table class="mat-elevation-z1" mat-table [dataSource]="savingsDataSource" matSort>
<ng-container matColumnDef="depositAccount">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Deposit Account </th>
<td mat-cell *matCellDef="let saving"> {{ saving.depositAccountType }}</td>
</ng-container>
<ng-container matColumnDef="savingsAccountNo">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Savings Account # </th>
<td mat-cell *matCellDef="let saving"> {{ saving.accountId }} </td>
</ng-container>
<ng-container matColumnDef="productName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Product Name </th>
<td mat-cell *matCellDef="let saving"> {{ saving.productName }}({{ saving.productId }}) </td>
</ng-container>
<ng-container matColumnDef="clientName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Client Name </th>
<td mat-cell *matCellDef="let saving"> {{ saving.clientName }}({{ saving.clientId }}) </td>
</ng-container>
<ng-container matColumnDef="totalDue">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Total Due </th>
<td mat-cell *matCellDef="let saving"> {{ saving.dueAmount }} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Actions </th>
<td mat-cell *matCellDef="let saving; let j = index">
<button mat-raised-button color="primary" (click)="showAndStorePaymentDetails('savings', saving, j)">Add Payment</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="savingsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: savingsDisplayedColumns;"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
</ng-container>
<mat-card-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="5px">
<button type="button" mat-raised-button [routerLink]="['../']">Cancel</button>
<button mat-raised-button color="primary" (click)="submit()">Submit</button>
</mat-card-actions>
</mat-card>
</div>
</div>
@@ -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;
}
}
@@ -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<IndividualCollectionSheetComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ IndividualCollectionSheetComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IndividualCollectionSheetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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<any>;
/** Data source for savings table. */
savingsDataSource: MatTableDataSource<any>;
/** 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]));
}
}
@@ -73,6 +73,12 @@
</mat-icon>
<a matLine>Navigation</a>
</mat-list-item>
<mat-list-item [routerLink]="['/collections/individual-collection-sheet']">
<mat-icon matListIcon>
<i class="fa fa-tasks "></i>
</mat-icon>
<a matLine>Individual Collection Sheet</a>
</mat-list-item>
<mat-list-item [routerLink]="['/notifications']">
<mat-icon matListIcon>
<fa-icon icon="bell" size="sm"></fa-icon>