diff --git a/src/app/groups/common-resolvers/group-account.resolver.ts b/src/app/groups/common-resolvers/group-account.resolver.ts new file mode 100644 index 000000000..d7951bf2a --- /dev/null +++ b/src/app/groups/common-resolvers/group-account.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 { GroupsService } from '../groups.service'; + +/** + * Group Accounts data resolver. + */ +@Injectable() +export class GroupAccountsResolver implements Resolve { + + /** + * @param {GroupsService} GroupsService Groups service. + */ + constructor(private groupsService: GroupsService) { } + + /** + * Returns the Group's Acccounts data. + * @param {ActivatedRouteSnapshot} route Route Snapshot. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const groupId = route.parent.paramMap.get('groupId'); + return this.groupsService.getGroupAccountsData(groupId); + } + +} diff --git a/src/app/groups/common-resolvers/group-client-members.resolver.ts b/src/app/groups/common-resolvers/group-client-members.resolver.ts new file mode 100644 index 000000000..a0b4e5717 --- /dev/null +++ b/src/app/groups/common-resolvers/group-client-members.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 { GroupsService } from '../groups.service'; + +/** + * Groups Client Members data resolver. + */ +@Injectable() +export class GroupClientMembersResolver implements Resolve { + + /** + * @param {GroupsService} GroupsService Groups service. + */ + constructor(private groupsService: GroupsService) { } + + /** + * Returns the Group's Client Members data. + * @param {ActivatedRouteSnapshot} route Route Snapshot. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const groupId = route.parent.paramMap.get('groupId'); + return this.groupsService.getGroupClientMembers(groupId); + } + +} diff --git a/src/app/groups/common-resolvers/group-summary.resolver.ts b/src/app/groups/common-resolvers/group-summary.resolver.ts new file mode 100644 index 000000000..fd58f5516 --- /dev/null +++ b/src/app/groups/common-resolvers/group-summary.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 { GroupsService } from '../groups.service'; + +/** + * Group Summary resolver. + */ +@Injectable() +export class GroupSummaryResolver implements Resolve { + + /** + * @param {GroupsService} GroupsService Groups service. + */ + constructor(private groupsService: GroupsService) { } + + /** + * Returns the Group Summary data. + * @param {ActivatedRouteSnapshot} route Route Snapshot. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const groupId = route.parent.paramMap.get('groupId'); + return this.groupsService.getGroupSummary(groupId); + } + +} diff --git a/src/app/groups/common-resolvers/group-view.resolver.ts b/src/app/groups/common-resolvers/group-view.resolver.ts new file mode 100644 index 000000000..cc2a049ad --- /dev/null +++ b/src/app/groups/common-resolvers/group-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 { GroupsService } from '../groups.service'; + +/** + * Groups data resolver. + */ +@Injectable() +export class GroupViewResolver implements Resolve { + + /** + * @param {GroupsService} GroupsService Groups service. + */ + constructor(private groupsService: GroupsService) { } + + /** + * Returns the Groups data. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const groupId = route.paramMap.get('groupId'); + return this.groupsService.getGroupData(groupId); + } + +} diff --git a/src/app/groups/groups-routing.module.ts b/src/app/groups/groups-routing.module.ts index cb6a40976..52c88b383 100644 --- a/src/app/groups/groups-routing.module.ts +++ b/src/app/groups/groups-routing.module.ts @@ -10,14 +10,48 @@ import { extract } from '../core/i18n/i18n.service'; /** Custom Imports */ import { GroupsComponent } from './groups.component'; +import { GroupsViewComponent } from './groups-view/groups-view.component'; +import { GeneralTabComponent } from './groups-view/general-tab/general-tab.component'; + +/** Custom Resolvers */ +import { GroupViewResolver } from './common-resolvers/group-view.resolver'; +import { GroupAccountsResolver } from './common-resolvers/group-account.resolver'; +import { GroupSummaryResolver } from './common-resolvers/group-summary.resolver'; +import { GroupClientMembersResolver } from './common-resolvers/group-client-members.resolver'; /** Groups Routes */ const routes: Routes = [ Route.withShell([ { path: 'groups', - component: GroupsComponent, - data: { title: extract('Groups'), breadcrumb: 'Groups' } + data: { title: extract('Groups'), breadcrumb: 'Groups', routeParamBreadcrumb: false }, + children: [ + { + path: '', + component: GroupsComponent, + }, + { + path: ':groupId', + component: GroupsViewComponent, + data: { title: extract('View Group'), routeParamBreadcrumb: 'groupId' }, + resolve: { + groupViewData: GroupViewResolver, + + }, + children: [ + { + path: 'general', + component: GeneralTabComponent, + data: { title: extract('General'), breadcrumb: 'General', routeParamBreadcrumb: false }, + resolve: { + groupAccountsData: GroupAccountsResolver, + groupClientMembers: GroupClientMembersResolver, + groupSummary: GroupSummaryResolver + } + } + ] + } + ] } ]) ]; @@ -30,6 +64,9 @@ const routes: Routes = [ @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], - providers: [] + providers: [GroupViewResolver, + GroupAccountsResolver, + GroupSummaryResolver, + GroupClientMembersResolver] }) export class GroupsRoutingModule { } diff --git a/src/app/groups/groups-view/general-tab/general-tab.component.html b/src/app/groups/groups-view/general-tab/general-tab.component.html new file mode 100644 index 000000000..a97716937 --- /dev/null +++ b/src/app/groups/groups-view/general-tab/general-tab.component.html @@ -0,0 +1,289 @@ +
+ +

Group Details

+ +
+

+ Active Client Loans: {{groupSummary?.activeClientLoans}}
+ Active Client Borrowers: {{groupSummary?.activeClientBorrowers}}
+ Active Overdue Client Loans: {{groupSummary?.overdueClientLoans}}
+

+

+ Active Group Loans: {{groupSummary?.activeGroupLoans}}
+ Active Group Borrowers: {{groupSummary?.activeGroupBorrowers}}
+ Active Overdue Group Loans: {{groupSummary?.overdueGroupLoans}}
+

+

+ Active Clients: {{groupSummary?.activeClients}}
+

+
+ + +
+ +

Client Members

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + + {{element.displayName}} + Account No. {{element.accountNo}} Office {{element.officeName}} JLG Loan Application +
+ +
+
+ +
+ +
+ +
+
+

Loan Accounts

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Account No. + {{element.accountNo}} + Loan Account {{element.productName}} Original Loan {{element.originalLoan}} Loan Balance {{element.loanBalance}} Amount Paid {{element.amountPaid}} Type + + Actions + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Account No. + {{element.accountNo}} + Loan Account {{element.productName}} Original Loan {{element.lastActiveTransactionDate | date}} Loan Balance {{element.loanBalance}} Amount Paid {{element.accountBalance}} Type + + Closed Date {{element.timeline.closedOnDate | date}}
+ +
+ +
+ +
+
+

Saving Accounts

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Account No. + {{element.accountNo}} Saving Account {{element.productName}} Last Active {{element.lastActiveTransactionDate | date}} Balance {{element.accountBalance}} Actions + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
Account No. + {{element.accountNo}} Saving Account {{element.productName}} Closed Date {{element.timeline.closedOnDate | date}}
+ +
+ +
diff --git a/src/app/groups/groups-view/general-tab/general-tab.component.scss b/src/app/groups/groups-view/general-tab/general-tab.component.scss new file mode 100644 index 000000000..8bb6e3775 --- /dev/null +++ b/src/app/groups/groups-view/general-tab/general-tab.component.scss @@ -0,0 +1,33 @@ +.tab-container { + padding: 1%; + margin: 1%; + h3 { + margin:1% auto; + } + .group-details-container { + border: 1px solid; + padding: 1%; + margin-bottom: 30px; + } + .table-header { + .action-button{ + margin-left: auto; + } + } + table { + width: 100%; + .account-action-button{ + min-width: 26px; + padding: 0 6px; + margin: 4px; + line-height: 25px; + } + tr.select-row:hover { + cursor: pointer; + } + margin-bottom: 30px; + } + i:hover { + cursor: pointer; + } +} diff --git a/src/app/groups/groups-view/general-tab/general-tab.component.spec.ts b/src/app/groups/groups-view/general-tab/general-tab.component.spec.ts new file mode 100644 index 000000000..68ff6503e --- /dev/null +++ b/src/app/groups/groups-view/general-tab/general-tab.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GeneralTabComponent } from './general-tab.component'; + +describe('GeneralTabComponent', () => { + let component: GeneralTabComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GeneralTabComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GeneralTabComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/groups/groups-view/general-tab/general-tab.component.ts b/src/app/groups/groups-view/general-tab/general-tab.component.ts new file mode 100644 index 000000000..eff250e82 --- /dev/null +++ b/src/app/groups/groups-view/general-tab/general-tab.component.ts @@ -0,0 +1,68 @@ +/** Angular Imports */ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +/** + * Groups View General Tab Component. + */ +@Component({ + selector: 'mifosx-general-tab', + templateUrl: './general-tab.component.html', + styleUrls: ['./general-tab.component.scss'] +}) +export class GeneralTabComponent { + + /** Group's all accounts data */ + groupAccountData: any; + /** Group's loan accounts data */ + loanAccounts: any; + /** Group's savings accounts data */ + savingAccounts: any; + /** Group Summary */ + groupSummary: any; + /** Group's Client Members */ + groupClientMembers: any; + /** Columns to be Displayed for client members table */ + clientMemberColumns: string[] = ['Name', 'Account No', 'Office', 'JLG Loan Application']; + /** Columns to be displayed for open loan accounts table */ + openLoansColumns: string[] = ['Account No', 'Loan Account', 'Original Loan', 'Loan Balance', 'Amount Paid', 'Type', 'Actions']; + /** Columns to be displayed for closed loan accounts table */ + closedLoansColumns: string[] = ['Account No', 'Loan Account', 'Original Loan', 'Loan Balance', 'Amount Paid', 'Type', 'Closed Date']; + /** Columns to be displayed for open savings accounts table */ + openSavingsColumns: string[] = ['Account No', 'Saving Account', 'Last Active', 'Balance', 'Actions']; + /** Columns to be displayed for closed accounts table */ + closedSavingsColumns: string[] = ['Account No', 'Saving Account', 'Closed Date']; + /** Boolean for toggling loan accounts table */ + showClosedLoanAccounts = false; + /** Boolean for toggling savings accounts table */ + showClosedSavingAccounts = false; + + /** + * Fetches group's related data from `resolve` + * @param {ActivatedRoute} route Activated Route. + */ + constructor(private route: ActivatedRoute) { + this.route.data.subscribe((data: { groupAccountsData: any, groupClientMembers: any, groupSummary: any }) => { + this.groupAccountData = data.groupAccountsData; + this.savingAccounts = data.groupAccountsData.savingsAccounts; + this.loanAccounts = data.groupAccountsData.loanAccounts; + this.groupSummary = data.groupSummary[0]; + this.groupClientMembers = data.groupClientMembers; + }); + } + + /** + * Toggles loan accounts table. + */ + toggleLoanAccountsOverview() { + this.showClosedLoanAccounts = !this.showClosedLoanAccounts; + } + + /** + * Toggles savings account table. + */ + toggleSavingAccountsOverview() { + this.showClosedSavingAccounts = !this.showClosedSavingAccounts; + } + +} diff --git a/src/app/groups/groups-view/groups-view.component-theme.scss b/src/app/groups/groups-view/groups-view.component-theme.scss new file mode 100644 index 000000000..676d5bc09 --- /dev/null +++ b/src/app/groups/groups-view/groups-view.component-theme.scss @@ -0,0 +1,12 @@ +@import "~@angular/material/theming"; + +@mixin groups-view-component-theme($theme) { + $primary: map-get($theme, primary); + mifosx-groups-view { + .group-card { + .header { + background-color: mat-color($primary, 500); + } + } + } +} diff --git a/src/app/groups/groups-view/groups-view.component.html b/src/app/groups/groups-view/groups-view.component.html new file mode 100644 index 000000000..625635bf3 --- /dev/null +++ b/src/app/groups/groups-view/groups-view.component.html @@ -0,0 +1,109 @@ + + + + + + +
+
+ +
+
+ +
+ +

+ + Group Name : {{groupViewData.name}} +

+
+ +

+ Group #: {{groupViewData.id}} | Center Name: {{groupViewData.centerName}} | Staff: {{groupViewData.staffName}}
+ Office Name: {{groupViewData.officeName}}
+ Activation Date : {{(groupViewData.activationDate)?(groupViewData.activationDate|date) :'Not Activated'}}
+ + Closure Date: {{groupViewData.timeline.closedOnDate | date}} + +

+
+
+ +
+
+

+ Next Meeting on: {{groupViewData.collectionMeetingCalendar?.recentEligibleMeetingDate | date}} +
+ Meeting Frequency: {{groupViewData.collectionMeetingCalendar?.frequency.value | lowercase}} +

+
+ +
+

+ Next Meeting on: Unassigned +
+ Meeting Frequency: N/A +

+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
diff --git a/src/app/groups/groups-view/groups-view.component.scss b/src/app/groups/groups-view/groups-view.component.scss new file mode 100644 index 000000000..b3bd5c5cb --- /dev/null +++ b/src/app/groups/groups-view/groups-view.component.scss @@ -0,0 +1,51 @@ +.group-card { + margin: 0 auto; + max-width: 80rem; + width: 90%; + padding: 0; + .header { + padding: 1%; + .header-title-group { + .group-card-title { + color: white; + width: 90%; + p { + color: white; + } + } + } + .profile-image-container { + margin: 1%; + .profile-image { + object-fit: cover; + border-radius: 20px; + } + } + .group-meeting { + align-self: flex-end; + width: 300px; + height: 140px; + div, ng-template > div { + background-color: #0a6aac; + p { + color: white; + padding-inline: 10px; + } + } + } + .group-actions { + align-self: flex-end; + margin: 0 1%; + i { + margin-bottom: 2px; + margin-right: 4px; + } + } + } + .navigation-tabs { + overflow: auto; + } + i:hover { + cursor: pointer; + } +} diff --git a/src/app/groups/groups-view/groups-view.component.spec.ts b/src/app/groups/groups-view/groups-view.component.spec.ts new file mode 100644 index 000000000..9d47bb4b8 --- /dev/null +++ b/src/app/groups/groups-view/groups-view.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupsViewComponent } from './groups-view.component'; + +describe('GroupsViewComponent', () => { + let component: GroupsViewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GroupsViewComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupsViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/groups/groups-view/groups-view.component.ts b/src/app/groups/groups-view/groups-view.component.ts new file mode 100644 index 000000000..00d0555b7 --- /dev/null +++ b/src/app/groups/groups-view/groups-view.component.ts @@ -0,0 +1,28 @@ +/** Angular Imports */ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +/** + * Groups View Component. + */ +@Component({ + selector: 'mifosx-groups-view', + templateUrl: './groups-view.component.html', + styleUrls: ['./groups-view.component.scss'] +}) +export class GroupsViewComponent { + + /** Group view data */ + groupViewData: any; + + /** + * Fetches group data from `resolve` + * @param {ActivatedRoute} route Activated Route + */ + constructor(private route: ActivatedRoute) { + this.route.data.subscribe((data: { groupViewData: any }) => { + this.groupViewData = data.groupViewData; + }); + } + +} diff --git a/src/app/groups/groups.component.html b/src/app/groups/groups.component.html index 39f69fc76..ad044b7fc 100644 --- a/src/app/groups/groups.component.html +++ b/src/app/groups/groups.component.html @@ -45,7 +45,7 @@ - + diff --git a/src/app/groups/groups.component.scss b/src/app/groups/groups.component.scss index b917b27e6..f738e24d4 100644 --- a/src/app/groups/groups.component.scss +++ b/src/app/groups/groups.component.scss @@ -2,9 +2,14 @@ .action-button{ margin-left: auto; } - .search-button{ min-width: 30px; margin: 4px; } } + +table { + tr.select-row:hover { + cursor: pointer; + } +} diff --git a/src/app/groups/groups.module.ts b/src/app/groups/groups.module.ts index c191dd9ce..799648f06 100644 --- a/src/app/groups/groups.module.ts +++ b/src/app/groups/groups.module.ts @@ -6,9 +6,12 @@ import { DatePipe } from '@angular/common'; import { GroupsRoutingModule } from './groups-routing.module'; import { SharedModule } from 'app/shared/shared.module'; import { PipesModule } from '../pipes/pipes.module'; +import { DirectivesModule } from '../directives/directives.module'; /** Custom Components */ import { GroupsComponent } from './groups.component'; +import { GroupsViewComponent } from './groups-view/groups-view.component'; +import { GeneralTabComponent } from './groups-view/general-tab/general-tab.component'; /** * Groups Module @@ -18,11 +21,14 @@ import { GroupsComponent } from './groups.component'; @NgModule({ imports: [ SharedModule, - GroupsRoutingModule, - PipesModule + PipesModule, + DirectivesModule, + GroupsRoutingModule ], declarations: [ - GroupsComponent + GroupsComponent, + GroupsViewComponent, + GeneralTabComponent ], providers: [DatePipe] }) diff --git a/src/app/groups/groups.service.ts b/src/app/groups/groups.service.ts index 2705c9f69..8dfa71bcb 100644 --- a/src/app/groups/groups.service.ts +++ b/src/app/groups/groups.service.ts @@ -4,6 +4,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; /** rxjs Imports */ import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; /** * Groups service. @@ -51,4 +52,41 @@ export class GroupsService { return this.http.get('/groups', { params: httpParams } ); } + /** + * @param groupId Group Id of group to get data for. + * @returns {Observable} Group data. + */ + getGroupData(groupId: string): Observable { + const httpParams = new HttpParams().set('associations', 'all'); + return this.http.get(`/groups/${groupId}`, { params: httpParams }); + } + + /** + * @param groupId Group Id of group to get data for. + * @returns {Observable} Group Summary data. + */ + getGroupSummary(groupId: string): Observable { + const httpParams = new HttpParams().set('R_groupId', groupId) + .set('genericResultSet', 'false'); + return this.http.get(`/runreports/GroupSummaryCounts`, { params: httpParams }); + } + + /** + * @param groupId Group Id of group to get data for. + * @returns {Observable} Group Client Members data. + */ + getGroupClientMembers(groupId: string): Observable { + const httpParams = new HttpParams().set('associations', 'clientMembers'); + return this.http.get(`/groups/${groupId}`, { params: httpParams }) + .pipe(map((group: any) => group.clientMembers)); + } + + /** + * @param groupId Group Id of group to get data for. + * @returns {Observable} Group Accounts data. + */ + getGroupAccountsData(groupId: string): Observable { + return this.http.get(`/groups/${groupId}/accounts`); + } + } diff --git a/src/assets/images/group_placeholder.png b/src/assets/images/group_placeholder.png new file mode 100644 index 000000000..2e053cba8 Binary files /dev/null and b/src/assets/images/group_placeholder.png differ diff --git a/src/theme/custom/denim-yellowgreen.scss b/src/theme/custom/denim-yellowgreen.scss index bef500396..e6b71cfd9 100644 --- a/src/theme/custom/denim-yellowgreen.scss +++ b/src/theme/custom/denim-yellowgreen.scss @@ -1,6 +1,8 @@ @import '~@angular/material/theming'; // Plus imports for other components in your app. @import '../../app/clients/clients-view/clients-view.component-theme.scss'; +@import '../../app/groups/groups-view/groups-view.component-theme.scss'; + // Custom material palette $mat-denim: ( 100: #5ba2ec, @@ -46,3 +48,4 @@ $mifosx-app-theme: mat-light-theme($mifosx-app-primary, $mifosx-app-accent, $mif // that you are using. @include angular-material-theme($mifosx-app-theme); @include clients-view-component-theme($mifosx-app-theme); +@include groups-view-component-theme($mifosx-app-theme); diff --git a/src/theme/custom/pictonblue-yellowgreen.scss b/src/theme/custom/pictonblue-yellowgreen.scss index 9e36753e9..88e1ffe65 100644 --- a/src/theme/custom/pictonblue-yellowgreen.scss +++ b/src/theme/custom/pictonblue-yellowgreen.scss @@ -1,6 +1,8 @@ @import '~@angular/material/theming'; // Plus imports for other components in your app. @import '../../app/clients/clients-view/clients-view.component-theme.scss'; +@import '../../app/groups/groups-view/groups-view.component-theme.scss'; + // Custom material palette $mat-picton-blue: ( 100: #6ce0ff, @@ -45,4 +47,5 @@ $mifosx-app-theme: mat-light-theme($mifosx-app-primary, $mifosx-app-accent, $mif // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include angular-material-theme($mifosx-app-theme); -@include clients-view-component-theme($mifosx-app-theme); \ No newline at end of file +@include clients-view-component-theme($mifosx-app-theme); +@include groups-view-component-theme($mifosx-app-theme);