From db248a8037db69616e1a3d5180ba3eabdaec6f79 Mon Sep 17 00:00:00 2001 From: Karan Takalkar Date: Sat, 13 Jun 2020 01:54:44 +0530 Subject: [PATCH] feat: add view shares account page (#868) Fixes: #224 --- src/app/clients/clients-routing.module.ts | 4 + .../general-tab/general-tab.component.html | 2 +- .../share-account-view.resolver.ts | 32 +++++ .../charges-tab/charges-tab.component.html | 58 ++++++++ .../charges-tab/charges-tab.component.scss | 10 ++ .../charges-tab/charges-tab.component.spec.ts | 25 ++++ .../charges-tab/charges-tab.component.ts | 50 +++++++ .../dividends-tab.component.html | 38 ++++++ .../dividends-tab.component.scss | 10 ++ .../dividends-tab.component.spec.ts | 25 ++++ .../dividends-tab/dividends-tab.component.ts | 45 +++++++ .../shares-account-view.component-theme.scss | 16 +++ .../shares-account-view.component.html | 119 ++++++++++++++++ .../shares-account-view.component.scss | 53 ++++++++ .../shares-account-view.component.spec.ts | 25 ++++ .../shares-account-view.component.ts | 69 ++++++++++ .../shares-buttons.config.ts | 127 ++++++++++++++++++ .../transactions-tab.component.html | 48 +++++++ .../transactions-tab.component.scss | 10 ++ .../transactions-tab.component.spec.ts | 25 ++++ .../transactions-tab.component.ts | 47 +++++++ src/app/shares/shares-routing.module.ts | 58 ++++++++ src/app/shares/shares.module.ts | 35 +++++ src/app/shares/shares.service.spec.ts | 12 ++ src/app/shares/shares.service.ts | 26 ++++ .../images/shares_account_placeholder.png | Bin 0 -> 26399 bytes src/theme/custom/denim-yellowgreen.scss | 2 + src/theme/custom/pictonblue-yellowgreen.scss | 2 + 28 files changed, 972 insertions(+), 1 deletion(-) create mode 100644 src/app/shares/common-resolvers/share-account-view.resolver.ts create mode 100644 src/app/shares/shares-account-view/charges-tab/charges-tab.component.html create mode 100644 src/app/shares/shares-account-view/charges-tab/charges-tab.component.scss create mode 100644 src/app/shares/shares-account-view/charges-tab/charges-tab.component.spec.ts create mode 100644 src/app/shares/shares-account-view/charges-tab/charges-tab.component.ts create mode 100644 src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.html create mode 100644 src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.scss create mode 100644 src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.spec.ts create mode 100644 src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.ts create mode 100644 src/app/shares/shares-account-view/shares-account-view.component-theme.scss create mode 100644 src/app/shares/shares-account-view/shares-account-view.component.html create mode 100644 src/app/shares/shares-account-view/shares-account-view.component.scss create mode 100644 src/app/shares/shares-account-view/shares-account-view.component.spec.ts create mode 100644 src/app/shares/shares-account-view/shares-account-view.component.ts create mode 100644 src/app/shares/shares-account-view/shares-buttons.config.ts create mode 100644 src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.html create mode 100644 src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.scss create mode 100644 src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.spec.ts create mode 100644 src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts create mode 100644 src/app/shares/shares-routing.module.ts create mode 100644 src/app/shares/shares.module.ts create mode 100644 src/app/shares/shares.service.spec.ts create mode 100644 src/app/shares/shares.service.ts create mode 100644 src/assets/images/shares_account_placeholder.png diff --git a/src/app/clients/clients-routing.module.ts b/src/app/clients/clients-routing.module.ts index 9e3de6735..5da1a44a3 100644 --- a/src/app/clients/clients-routing.module.ts +++ b/src/app/clients/clients-routing.module.ts @@ -177,6 +177,10 @@ const routes: Routes = [ path: 'savingsaccounts', loadChildren: '../savings/savings.module#SavingsModule' }, + { + path: 'sharesaccounts', + loadChildren: '../shares/shares.module#SharesModule' + }, ] } ] diff --git a/src/app/clients/clients-view/general-tab/general-tab.component.html b/src/app/clients/clients-view/general-tab/general-tab.component.html index 62e427624..a9a7cf91e 100644 --- a/src/app/clients/clients-view/general-tab/general-tab.component.html +++ b/src/app/clients/clients-view/general-tab/general-tab.component.html @@ -318,7 +318,7 @@ - + diff --git a/src/app/shares/common-resolvers/share-account-view.resolver.ts b/src/app/shares/common-resolvers/share-account-view.resolver.ts new file mode 100644 index 000000000..67021b64a --- /dev/null +++ b/src/app/shares/common-resolvers/share-account-view.resolver.ts @@ -0,0 +1,32 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { SharesService } from '../shares.service'; + +/** + * Shares Account data resolver. + */ +@Injectable() +export class SharesAccountViewResolver implements Resolve { + + /** + * @param {SharesService} SharesService Shares service. + */ + constructor(private sharesService: SharesService) { } + + /** + * Returns the Shares Account data. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const shareAccountId = route.paramMap.get('shareAccountId'); + return this.sharesService.getSharesAccountData(shareAccountId); + } + +} diff --git a/src/app/shares/shares-account-view/charges-tab/charges-tab.component.html b/src/app/shares/shares-account-view/charges-tab/charges-tab.component.html new file mode 100644 index 000000000..51aec2e82 --- /dev/null +++ b/src/app/shares/shares-account-view/charges-tab/charges-tab.component.html @@ -0,0 +1,58 @@ +
+ +
+

All Charges

+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name {{ charge.name }} Fee/Penalty {{ charge.penalty === true ? 'Penalty' : 'Fee' }} Payment Due At {{ charge.chargeTimeType.value }} Calculation Type {{charge.chargeCalculationType.value}} Due {{charge.currency.displaySymbol}} {{charge.amount}} Paid {{charge.currency.displaySymbol}} {{charge.amountPaid}} Waived {{charge.currency.displaySymbol}} {{charge.amountWaived}} Outstanding {{charge.currency.displaySymbol}} {{charge.amountOutstanding}}
+ + + + diff --git a/src/app/shares/shares-account-view/charges-tab/charges-tab.component.scss b/src/app/shares/shares-account-view/charges-tab/charges-tab.component.scss new file mode 100644 index 000000000..be9328c80 --- /dev/null +++ b/src/app/shares/shares-account-view/charges-tab/charges-tab.component.scss @@ -0,0 +1,10 @@ +.tab-container { + padding: 1%; + margin: 1%; + h3 { + margin:1% auto; + } + table { + width: 100%; + } +} diff --git a/src/app/shares/shares-account-view/charges-tab/charges-tab.component.spec.ts b/src/app/shares/shares-account-view/charges-tab/charges-tab.component.spec.ts new file mode 100644 index 000000000..1ef118215 --- /dev/null +++ b/src/app/shares/shares-account-view/charges-tab/charges-tab.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChargesTabComponent } from './charges-tab.component'; + +describe('ChargesTabComponent', () => { + let component: ChargesTabComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ChargesTabComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChargesTabComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shares/shares-account-view/charges-tab/charges-tab.component.ts b/src/app/shares/shares-account-view/charges-tab/charges-tab.component.ts new file mode 100644 index 000000000..1201fd6a1 --- /dev/null +++ b/src/app/shares/shares-account-view/charges-tab/charges-tab.component.ts @@ -0,0 +1,50 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { MatTableDataSource } from '@angular/material'; +import { ActivatedRoute } from '@angular/router'; + +/** + * Charges Tab Component + */ +@Component({ + selector: 'mifosx-charges-tab', + templateUrl: './charges-tab.component.html', + styleUrls: ['./charges-tab.component.scss'] +}) +export class ChargesTabComponent implements OnInit { + + /** Shares Account Data */ + sharesAccountData: any; + /** Charges Data */ + chargesData: any[]; + /** Data source for charges table. */ + dataSource: MatTableDataSource; + /** Columns to be displayed in charges table. */ + displayedColumns: string[] = [ + 'name', + 'feeOrPenalty', + 'paymentDueAt', + 'calculationType', + 'due', + 'paid', + 'waived', + 'outstanding', + ]; + + /** + * Retrieves shares account aata from `resolve`. + * @param {ActivatedRoute} route Activated Route. + */ + constructor(private route: ActivatedRoute) { + this.route.parent.data.subscribe((data: { sharesAccountData: any }) => { + this.sharesAccountData = data.sharesAccountData; + this.chargesData = this.sharesAccountData.charges; + }); + } + + ngOnInit() { + const activeCharges = this.chargesData ? this.chargesData.filter(charge => charge.isActive) : []; + this.dataSource = new MatTableDataSource(activeCharges); + } + +} diff --git a/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.html b/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.html new file mode 100644 index 000000000..aa3d6faa2 --- /dev/null +++ b/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.html @@ -0,0 +1,38 @@ +
+ +
+

All Dividends

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Transaction Date {{dividend.postedDate | date}} Amount {{shareAccountData.currency.displaySymbol}} {{dividend.amount}} Transaction Reference {{dividend.savingsTransactionId}} status {{dividend.status.value}}
+ +
+ +
diff --git a/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.scss b/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.scss new file mode 100644 index 000000000..be9328c80 --- /dev/null +++ b/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.scss @@ -0,0 +1,10 @@ +.tab-container { + padding: 1%; + margin: 1%; + h3 { + margin:1% auto; + } + table { + width: 100%; + } +} diff --git a/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.spec.ts b/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.spec.ts new file mode 100644 index 000000000..0d82e4718 --- /dev/null +++ b/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DividendsTabComponent } from './dividends-tab.component'; + +describe('DividendsTabComponent', () => { + let component: DividendsTabComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DividendsTabComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DividendsTabComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.ts b/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.ts new file mode 100644 index 000000000..bea385a72 --- /dev/null +++ b/src/app/shares/shares-account-view/dividends-tab/dividends-tab.component.ts @@ -0,0 +1,45 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { MatTableDataSource } from '@angular/material'; +import { ActivatedRoute } from '@angular/router'; + +/** + * Dividends Tab Component. + */ +@Component({ + selector: 'mifosx-dividends-tab', + templateUrl: './dividends-tab.component.html', + styleUrls: ['./dividends-tab.component.scss'] +}) +export class DividendsTabComponent implements OnInit { + + /** Shares Account Data */ + shareAccountData: any; + /** Dividends Data */ + dividendsData: any; + /** Data source for dividends table. */ + dataSource: MatTableDataSource; + /** Columns to be displayed in dividends table. */ + displayedColumns: string[] = [ + 'transactionDate', + 'amount', + 'transactionReference', + 'status' + ]; + + /** + * Retrieves shares account data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + */ + constructor(private route: ActivatedRoute) { + this.route.parent.data.subscribe((data: { sharesAccountData: any }) => { + this.shareAccountData = data.sharesAccountData; + this.dividendsData = this.shareAccountData.dividends; + }); + } + + ngOnInit() { + this.dataSource = new MatTableDataSource(this.dividendsData); + } + +} diff --git a/src/app/shares/shares-account-view/shares-account-view.component-theme.scss b/src/app/shares/shares-account-view/shares-account-view.component-theme.scss new file mode 100644 index 000000000..9d6eb1052 --- /dev/null +++ b/src/app/shares/shares-account-view/shares-account-view.component-theme.scss @@ -0,0 +1,16 @@ +@import "~@angular/material/theming"; + +@mixin shares-account-view-component-theme($theme) { + $primary: map-get($theme, primary); + mifosx-shares-account-view { + .shares-account-card { + .header { + background-color: mat-color($primary, 500); + } + .table-headers { + background-color: mat-color($primary, 500); + color: mat-contrast($primary, 500); + } + } + } +} diff --git a/src/app/shares/shares-account-view/shares-account-view.component.html b/src/app/shares/shares-account-view/shares-account-view.component.html new file mode 100644 index 000000000..63652e1f0 --- /dev/null +++ b/src/app/shares/shares-account-view/shares-account-view.component.html @@ -0,0 +1,119 @@ + diff --git a/src/app/shares/shares-account-view/shares-account-view.component.scss b/src/app/shares/shares-account-view/shares-account-view.component.scss new file mode 100644 index 000000000..6f6c669de --- /dev/null +++ b/src/app/shares/shares-account-view/shares-account-view.component.scss @@ -0,0 +1,53 @@ +.shares-account-card { + margin: 0 auto; + max-width: 80rem; + width: 90%; + padding: 0; + .header { + padding: 1%; + .header-title-group { + .account-card-title { + color: white; + width: 90%; + p { + color: white; + } + } + } + .profile-image-container { + margin: 1%; + .profile-image { + object-fit: cover; + border-radius: 20px; + } + } + .account-actions { + align-self: flex-end; + margin: 0 1%; + i { + margin-bottom: 2px; + margin-right: 4px; + } + } + } + .navigation-tabs { + background-color: #f2f2f2; + overflow: auto; + } + .content { + .shares-account-tables { + padding: 1%; + margin: 1%; + .table-headers { + margin: 0; + padding: 6px; + } + td { + padding: 3px; + } + } + } + i, img:hover { + cursor: pointer; + } +} diff --git a/src/app/shares/shares-account-view/shares-account-view.component.spec.ts b/src/app/shares/shares-account-view/shares-account-view.component.spec.ts new file mode 100644 index 000000000..f65036d2e --- /dev/null +++ b/src/app/shares/shares-account-view/shares-account-view.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SharesAccountViewComponent } from './shares-account-view.component'; + +describe('SharesAccountViewComponent', () => { + let component: SharesAccountViewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SharesAccountViewComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SharesAccountViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shares/shares-account-view/shares-account-view.component.ts b/src/app/shares/shares-account-view/shares-account-view.component.ts new file mode 100644 index 000000000..621bab13e --- /dev/null +++ b/src/app/shares/shares-account-view/shares-account-view.component.ts @@ -0,0 +1,69 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +/** Custom Buttons Configuration */ +import { SharesButtonsConfiguration } from './shares-buttons.config'; + +/** + * Shares Account View + */ +@Component({ + selector: 'mifosx-shares-account-view', + templateUrl: './shares-account-view.component.html', + styleUrls: ['./shares-account-view.component.scss'] +}) +export class SharesAccountViewComponent implements OnInit { + + /** Shares Account Data */ + sharesAccountData: any; + /** Button Configurations */ + buttonConfig: SharesButtonsConfiguration; + + /** + * Fetches shares account data from `resolve` + * @param {ActivatedRoute} route Activated Route + */ + constructor(private route: ActivatedRoute) { + this.route.data.subscribe((data: { sharesAccountData: any }) => { + this.sharesAccountData = data.sharesAccountData; + }); + } + + ngOnInit() { + this.setConditionalButtons(); + } + + /** + * Adds options to button config. conditionaly. + */ + setConditionalButtons() { + const status = this.sharesAccountData.status.value; + this.buttonConfig = new SharesButtonsConfiguration(status); + if (this.sharesAccountData.charges) { + const charges: any[] = this.sharesAccountData.charges; + charges.forEach((charge: any) => { + if (charge.name === 'Annual fee - INR') { + this.buttonConfig.addOption({ + name: 'Apply Anuual Fees', + taskPermissionName: 'APPLYANNUALFEE_SAVINGSACCOUNT' + }); + } + }); + } + if (status === 'Active') { + const purchasedShares: any[] = this.sharesAccountData.purchasedShares; + let sharesPendingForApproval = false; + purchasedShares.forEach((share: any) => { + if (share.status.code === 'purchasedSharesStatusType.applied' && share.type.code === 'purchasedSharesType.purchased') { + sharesPendingForApproval = true; + } + }); + if (!sharesPendingForApproval) { + this.buttonConfig.removeButton('Approve Additional Shares'); + this.buttonConfig.removeButton('Reject Additional Shares'); + } + } + } + +} diff --git a/src/app/shares/shares-account-view/shares-buttons.config.ts b/src/app/shares/shares-account-view/shares-buttons.config.ts new file mode 100644 index 000000000..a20faa70f --- /dev/null +++ b/src/app/shares/shares-account-view/shares-buttons.config.ts @@ -0,0 +1,127 @@ +/** Shares Account Buttons Configuration */ +export class SharesButtonsConfiguration { + + optionArray: { + name: string, + taskPermissionName: string, + action?: Function + }[]; + + buttonsArray: { + name: string, + icon: string, + taskPermissionName: string, + action?: Function + }[]; + + constructor(status: string) { + this.setOptions(status); + this.setButtons(status); + } + + get singleButtons() { + return this.buttonsArray; + } + + get options() { + return this.optionArray; + } + + setButtons(status: string) { + switch (status) { + case 'Active': + this.buttonsArray = [ + { + name: 'Apply Additional Shares', + icon: 'fa fa-arrow-right', + taskPermissionName: 'APPLYADDITIONAL_SHAREACCOUNT' + }, + { + name: 'Approve Additional Shares', + icon: 'fa fa-arrow-right', + taskPermissionName: 'APPROVEADDITIONAL_SHAREACCOUNT' + }, + { + name: 'Reject Additional Shares', + icon: 'fa fa-arrow-right', + taskPermissionName: 'REJECTADDITIONAL_SHAREACCOUNT' + }, + { + name: 'Redeem Shares', + icon: 'fa fa-arrow-left', + taskPermissionName: 'WITHDRAW_SAVINGSACCOUNT' + } + ]; + break; + case 'Submitted and pending approval': + this.buttonsArray = [ + { + name: 'Modify Application', + icon: 'fa fa-pencil ', + taskPermissionName: 'UPDATE_SHAREACCOUNT' + }, + { + name: 'Approve', + icon: 'fa fa-check', + taskPermissionName: 'APPROVE_SHAREACCOUNT' + } + ]; + break; + case 'Approved': + this.buttonsArray = [ + { + name: 'Undo Approval', + icon: 'fa fa-undo', + taskPermissionName: 'APPROVALUNDO_SHAREACCOUNT' + }, + { + name: 'Activate', + icon: 'fa fa-check', + taskPermissionName: 'ACTIVATE_SHAREACCOUNT' + } + ]; + break; + default: + this.buttonsArray = []; + } + } + + setOptions(status: string) { + switch (status) { + case 'Active': + this.optionArray = [ + { + name: 'Close', + taskPermissionName: 'CLOSE_SHAREACCOUNT' + } + ]; + break; + case 'Submitted and pending approval': + this.optionArray = [ + { + name: 'Reject', + taskPermissionName: 'REJECT_SHAREACCOUNT' + }, + { + name: 'Delete', + taskPermissionName: 'DELETE_SHAREACCOUNT' + } + ]; + break; + case 'Approved': + default: + this.optionArray = []; + } + } + + addOption(option: {name: string, taskPermissionName: string}) { + this.optionArray.push(option); + } + + removeButton(name: string) { + const buttonNames = this.buttonsArray.map(entry => entry.name); + const index = buttonNames.indexOf(name); + this.buttonsArray.splice(index, 1); + } + +} diff --git a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.html b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.html new file mode 100644 index 000000000..c9286d4f6 --- /dev/null +++ b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.html @@ -0,0 +1,48 @@ +
+ +
+

All Transactions

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Transaction Date {{transaction.purchasedDate | date}} Transaction Type {{transaction.type.value}} {{transaction.type.value !== "Charge Payment" ? ('(' + transaction.status.value + ')') : ''}} Total Shares {{transaction.numberOfShares}} Purhcased/Redeemed Price {{shareAccountData.currency.displaySymbol}} {{transaction.purchasedPrice}} Charge Amount {{shareAccountData.currency.displaySymbol}} {{transaction.type.value === "Charge Payment" ? transaction.amount : transaction.chargeAmount}} Amount Recieved/Returned {{shareAccountData.currency.displaySymbol}} {{transaction.amount}}
+ +
+ +
diff --git a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.scss b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.scss new file mode 100644 index 000000000..be9328c80 --- /dev/null +++ b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.scss @@ -0,0 +1,10 @@ +.tab-container { + padding: 1%; + margin: 1%; + h3 { + margin:1% auto; + } + table { + width: 100%; + } +} diff --git a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.spec.ts b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.spec.ts new file mode 100644 index 000000000..a67fc2279 --- /dev/null +++ b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TransactionsTabComponent } from './transactions-tab.component'; + +describe('TransactionsTabComponent', () => { + let component: TransactionsTabComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TransactionsTabComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TransactionsTabComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts new file mode 100644 index 000000000..985a175f6 --- /dev/null +++ b/src/app/shares/shares-account-view/transactions-tab/transactions-tab.component.ts @@ -0,0 +1,47 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { MatTableDataSource } from '@angular/material'; +import { ActivatedRoute } from '@angular/router'; + +/** + * Transactions Tab Component. + */ +@Component({ + selector: 'mifosx-transactions-tab', + templateUrl: './transactions-tab.component.html', + styleUrls: ['./transactions-tab.component.scss'] +}) +export class TransactionsTabComponent implements OnInit { + + /** Shares Account Data */ + shareAccountData: any; + /** Transactions Data */ + transactionsData: any; + /** Data source for transactions table. */ + dataSource: MatTableDataSource; + /** Columns to be displayed in transactions table. */ + displayedColumns: string[] = [ + 'transactionDate', + 'transactionType', + 'totalShares', + 'purchasedOrRedeemedPrice', + 'chargeAmount', + 'amountRecievedOrReturned' + ]; + + /** + * Retrieves shares account data from `resolve`. + * @param {ActivatedRoute} route Activated Route. + */ + constructor(private route: ActivatedRoute) { + this.route.parent.data.subscribe((data: { sharesAccountData: any }) => { + this.shareAccountData = data.sharesAccountData; + this.transactionsData = this.shareAccountData.purchasedShares; + }); + } + + ngOnInit() { + this.dataSource = new MatTableDataSource(this.transactionsData); + } + +} diff --git a/src/app/shares/shares-routing.module.ts b/src/app/shares/shares-routing.module.ts new file mode 100644 index 000000000..69c9605b2 --- /dev/null +++ b/src/app/shares/shares-routing.module.ts @@ -0,0 +1,58 @@ +/** Angular Imports */ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +/** Translation Imports */ +import { extract } from '../core/i18n/i18n.service'; + +/** Custom Components */ +import { SharesAccountViewComponent } from './shares-account-view/shares-account-view.component'; +import { TransactionsTabComponent } from './shares-account-view/transactions-tab/transactions-tab.component'; +import { ChargesTabComponent } from './shares-account-view/charges-tab/charges-tab.component'; +import { DividendsTabComponent } from './shares-account-view/dividends-tab/dividends-tab.component'; + +/** Custom Resolvers */ +import { SharesAccountViewResolver } from './common-resolvers/share-account-view.resolver'; + +const routes: Routes = [ + { + path: '', + data: { title: extract('Shares'), breadcrumb: 'Shares', routeParamBreadcrumb: false }, + children: [ + { + path: ':shareAccountId', + data: { title: extract('Shares Account View'), routeParamBreadcrumb: 'shareAccountId' }, + component: SharesAccountViewComponent, + resolve: { + sharesAccountData: SharesAccountViewResolver + }, + children: [ + { + path: 'transactions', + component: TransactionsTabComponent, + data: { title: extract('Shares Account Transactions'), breadcrumb: 'Transactions', routeParamBreadcrumb: false } + }, + { + path: 'charges', + component: ChargesTabComponent, + data: { title: extract('Shares Account Charges'), breadcrumb: 'Charges', routeParamBreadcrumb: false } + }, + { + path: 'dividends', + component: DividendsTabComponent, + data: { title: extract('Shares Account Dividends'), breadcrumb: 'Dividends', routeParamBreadcrumb: false } + } + ] + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + SharesAccountViewResolver + ] +}) +export class SharesRoutingModule { } diff --git a/src/app/shares/shares.module.ts b/src/app/shares/shares.module.ts new file mode 100644 index 000000000..e8839b245 --- /dev/null +++ b/src/app/shares/shares.module.ts @@ -0,0 +1,35 @@ +/** Angular Imports */ +import { NgModule } from '@angular/core'; +import { DatePipe } from '@angular/common'; + +/** Custom Modules */ +import { SharesRoutingModule } from './shares-routing.module'; +import { SharedModule } from 'app/shared/shared.module'; +import { PipesModule } from '../pipes/pipes.module'; +import { DirectivesModule } from '../directives/directives.module'; +import { SharesAccountViewComponent } from './shares-account-view/shares-account-view.component'; +import { TransactionsTabComponent } from './shares-account-view/transactions-tab/transactions-tab.component'; +import { DividendsTabComponent } from './shares-account-view/dividends-tab/dividends-tab.component'; +import { ChargesTabComponent } from './shares-account-view/charges-tab/charges-tab.component'; + +/** + * Shares Module + * + * All components related to Shares functions should be declared here. + */ +@NgModule({ + imports: [ + SharedModule, + PipesModule, + DirectivesModule, + SharesRoutingModule + ], + declarations: [ + SharesAccountViewComponent, + TransactionsTabComponent, + DividendsTabComponent, + ChargesTabComponent + ], + providers: [DatePipe] +}) +export class SharesModule { } diff --git a/src/app/shares/shares.service.spec.ts b/src/app/shares/shares.service.spec.ts new file mode 100644 index 000000000..ee99018ae --- /dev/null +++ b/src/app/shares/shares.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { SharesService } from './shares.service'; + +describe('SharesService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: SharesService = TestBed.get(SharesService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shares/shares.service.ts b/src/app/shares/shares.service.ts new file mode 100644 index 000000000..1e267e0ab --- /dev/null +++ b/src/app/shares/shares.service.ts @@ -0,0 +1,26 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** + * Shares Service. + */ +@Injectable({ + providedIn: 'root' +}) +export class SharesService { + + constructor(private http: HttpClient) {} + + /** + * @param accountId Shares Account Id of account to get data for. + * @returns {Observable} Shares data. + */ + getSharesAccountData(accountId: string): Observable { + return this.http.get(`/accounts/share/${accountId}`); + } + +} diff --git a/src/assets/images/shares_account_placeholder.png b/src/assets/images/shares_account_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..66ac301515e3a20dc3c48bd6c7ccc41973c517cd GIT binary patch literal 26399 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