Skip to content

Commit 6c79768

Browse files
authored
Merge pull request #641 from bcgov/aps-registration
DSS-812: APS user registration - FE
2 parents fed5cf1 + 15a267e commit 6c79768

File tree

11 files changed

+329
-8
lines changed

11 files changed

+329
-8
lines changed

frontend/src/app/app.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { UserDetailsComponent } from './features/components/user-management/user
2525
import { ExportListingsComponent } from './features/components/export-listings/export-listings.component';
2626
import { UploadBusinessLicenseComponent } from './features/components/upload-business-license/upload-business-license.component';
2727
import { AggregatedListingsTableComponent } from './features/components/listings-table/aggregated-listings-table/aggregated-listings-table.component';
28+
import { AddApsUserComponent } from './features/components/user-management/add-aps-user/add-aps-user.component';
2829

2930
export const routes: Routes = [
3031
{
@@ -113,6 +114,12 @@ export const routes: Routes = [
113114
component: UserDetailsComponent,
114115
data: { permissions: [user_read] }
115116
},
117+
{
118+
path: 'add-aps-user',
119+
canActivate: [approvedUserGuard, activeUserGuard, hasPermissionsGuard, areTermsAceptedGuard],
120+
component: AddApsUserComponent,
121+
data: { permissions: [user_read, user_write] }
122+
},
116123
{
117124
path: 'roles',
118125
canActivate: [approvedUserGuard, activeUserGuard, hasPermissionsGuard, areTermsAceptedGuard],
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface ApsUser {
2+
displayNm: string,
3+
isEnabled: boolean,
4+
representedByOrganizationId: number,
5+
roleCds: Array<string>,
6+
}

frontend/src/app/common/services/user-data.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { environment } from '../../../environments/environment';
55
import { DropdownOption } from '../models/dropdown-option';
66
import { User, UserDetails } from '../models/user';
77
import { Router } from '@angular/router';
8+
import { ApsUser } from '../models/aps-user';
89

910
@Injectable({
1011
providedIn: 'root'
@@ -44,6 +45,10 @@ export class UserDataService {
4445
return this.httpClient.put<UserDetails>(`${environment.API_HOST}/users/${data.userIdentityId}`, data)
4546
}
4647

48+
createApsUser(user: ApsUser): Observable<any> {
49+
return this.httpClient.post(`${environment.API_HOST}/users/aps`, user)
50+
}
51+
4752
updateIsEnabled(userIdentityId: number, isEnabled: boolean, updDtm: string): Observable<void> {
4853
return this.httpClient.put<void>(`${environment.API_HOST}/users/updateisenabled`, { userIdentityId, isEnabled, updDtm });
4954
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<h2>Add APS User Information</h2>
2+
3+
<div class="card">
4+
<div class="header">User Details, Status, Roles and Permissions</div>
5+
<div class="content">
6+
<div class="value-block">
7+
<div class="label"><strong>Client ID</strong> </div>
8+
<div class="value">
9+
<input type="text" placeholder="Enter Client ID" pInputText class="width320px"
10+
[(ngModel)]="user.displayNm">
11+
</div>
12+
</div>
13+
<div class="value-block">
14+
<div class="label"><strong>Organization Type</strong> </div>
15+
<p-dropdown class="full-width-text-field" appendTo="body" [options]="orgTypes" [(ngModel)]="selectedOrgType"
16+
placeholder="Please Select" (onChange)="onOrgTypeChanged($event)">
17+
</p-dropdown>
18+
</div>
19+
<div class="value-block">
20+
<div class="label"><strong>Organization Name</strong> </div>
21+
<p-dropdown [filter]="true" class="full-width-text-field" appendTo="body" [options]="organizations"
22+
[(ngModel)]="selectedOrg" placeholder="Please Select">
23+
</p-dropdown>
24+
</div>
25+
<div class="value-block">
26+
<div class="label"><strong>Current Status</strong> </div>
27+
<div class="status-checkboxes">
28+
<div class="radio">
29+
<p-radioButton name="active-radio" value="Active" [(ngModel)]="state"
30+
inputId="active-radio"></p-radioButton>
31+
<label for="active-radio" class="ml-2">Active</label>
32+
</div>
33+
34+
<div class="radio">
35+
<p-radioButton name="disabled-radio" value="Disabled" [(ngModel)]="state"
36+
inputId="disabled-radio"></p-radioButton>
37+
<label for="disabled-radio" class="ml-2">Disabled</label>
38+
</div>
39+
</div>
40+
</div>
41+
<div class="value-block">
42+
<div class="label"><strong>User Role(s) and Permission(s)</strong> </div>
43+
<div class="tags-block">
44+
<p-multiSelect placeholder="Please Select" [options]="roles" class="width320px" [maxSelectedLabels]="2"
45+
[(ngModel)]="user.roleCds" appendTo="body"
46+
(onChange)="onSelectedRolesChanged($event)"></p-multiSelect>
47+
<br>
48+
<a class="link-to-roles" [routerLink]="'/roles'" target="_blank">View All Roles and Permissions</a>
49+
<div class="tags-content">
50+
<p-tag [style]="{'background-color':'#D8EAFD', 'color':'#2D2D2D'}" *ngFor="let tag of tags"
51+
[value]="tag"></p-tag>
52+
</div>
53+
</div>
54+
55+
</div>
56+
</div>
57+
</div>
58+
59+
<div class="actions">
60+
<button pButton [disabled]="!user.displayNm || !selectedOrg ||!user.roleCds.length" (click)="onSave()">Save</button>
61+
<button pButton class="outline-btn" (click)="onCancel()">Cancel</button>
62+
</div>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
:host {
2+
width: 100%;
3+
flex: 0 0 100%;
4+
padding: 0 24px 12px 24px;
5+
background-color: #FFFFFF;
6+
7+
.card {
8+
margin-top: 24px;
9+
width: 100%;
10+
height: auto;
11+
border-radius: 4px;
12+
box-shadow: 0px 3.2px 7.2px 0px rgba(0, 0, 0, 0.23);
13+
14+
.header {
15+
width: 100%;
16+
height: 50px;
17+
background-color: #F6F9FC;
18+
padding: 12px;
19+
padding-left: 20px;
20+
font-size: large;
21+
font-family: 'BcSans-bold';
22+
}
23+
24+
.content {
25+
width: 100%;
26+
height: calc(100% - 50px);
27+
overflow: auto;
28+
padding: 20px;
29+
display: flex;
30+
flex-direction: row;
31+
gap: 24px;
32+
flex-wrap: wrap;
33+
}
34+
}
35+
36+
.status-checkboxes {
37+
display: flex;
38+
gap: 12px;
39+
padding-top: 12px;
40+
}
41+
42+
.value-block {
43+
width: 20rem;
44+
display: flex;
45+
flex-direction: column;
46+
gap: 10px;
47+
48+
.tags-block {
49+
width: 320px;
50+
51+
.link-to-roles {
52+
font-size: small;
53+
}
54+
55+
.tags-content {
56+
display: flex;
57+
flex-wrap: wrap;
58+
gap: 4px;
59+
}
60+
}
61+
}
62+
63+
.actions {
64+
padding: 24px 0;
65+
66+
button {
67+
&:not(:last-of-type) {
68+
margin-right: 12px;
69+
}
70+
}
71+
}
72+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { AddApsUserComponent } from './add-aps-user.component';
4+
5+
xdescribe('AddApsUserComponent', () => {
6+
let component: AddApsUserComponent;
7+
let fixture: ComponentFixture<AddApsUserComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [AddApsUserComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(AddApsUserComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { CommonModule } from '@angular/common';
2+
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
3+
import { FormsModule } from '@angular/forms';
4+
import { Router, RouterModule } from '@angular/router';
5+
import { ButtonModule } from 'primeng/button';
6+
import { DropdownModule } from 'primeng/dropdown';
7+
import { MultiSelectModule } from 'primeng/multiselect';
8+
import { RadioButtonModule } from 'primeng/radiobutton';
9+
import { TagModule } from 'primeng/tag';
10+
import { DropdownOptionSelectable, DropdownOption } from '../../../../common/models/dropdown-option';
11+
import { ErrorHandlingService } from '../../../../common/services/error-handling.service';
12+
import { GlobalLoaderService } from '../../../../common/services/global-loader.service';
13+
import { RequestAccessService } from '../../../../common/services/request-access.service';
14+
import { RolesService } from '../../../../common/services/roles.service';
15+
import { UserDataService } from '../../../../common/services/user-data.service';
16+
import { forkJoin } from 'rxjs';
17+
import { ApsUser } from '../../../../common/models/aps-user';
18+
import { InputTextModule } from 'primeng/inputtext';
19+
20+
@Component({
21+
selector: 'app-add-aps-user',
22+
standalone: true,
23+
imports: [
24+
CommonModule,
25+
TagModule,
26+
ButtonModule,
27+
DropdownModule,
28+
FormsModule,
29+
RouterModule,
30+
MultiSelectModule,
31+
RadioButtonModule,
32+
InputTextModule,
33+
],
34+
templateUrl: './add-aps-user.component.html',
35+
styleUrl: './add-aps-user.component.scss'
36+
})
37+
export class AddApsUserComponent implements OnInit {
38+
user: ApsUser = { displayNm: '', representedByOrganizationId: 0, isEnabled: true, roleCds: [], }
39+
40+
roles = new Array<DropdownOptionSelectable>();
41+
tags = new Array<string>();
42+
orgTypes = new Array<DropdownOption>();
43+
organizationsRaw = new Array<DropdownOption>();
44+
organizations = new Array<DropdownOption>();
45+
46+
state: 'Active' | 'Disabled' = 'Active';
47+
selectedOrg = 0;
48+
selectedOrgType = '';
49+
50+
constructor(
51+
private userService: UserDataService,
52+
private rolesService: RolesService,
53+
private errorService: ErrorHandlingService,
54+
private requestAccessService: RequestAccessService,
55+
private router: Router,
56+
private loaderService: GlobalLoaderService,
57+
private cd: ChangeDetectorRef,
58+
) { }
59+
60+
ngOnInit(): void {
61+
this.init();
62+
}
63+
64+
onCancel(): void {
65+
this.router.navigate(['/user-management']);
66+
}
67+
68+
onOrgTypeChanged(orgType: any): void {
69+
this.organizations = this.organizationsRaw.filter(x =>
70+
(x as any)['organizationType'] === orgType.value);
71+
}
72+
73+
onSelectedRolesChanged(_: any): void {
74+
this.roles = this.roles.map((role) => ({ ...role, selected: this.user.roleCds.some(x => role.value === x) }));
75+
this.tags = this.roles.filter(x => x.selected).map(x => x.label);
76+
77+
this.cd.detectChanges();
78+
}
79+
80+
onSave(): void {
81+
this.loaderService.loadingStart();
82+
this.user.representedByOrganizationId = this.selectedOrg;
83+
this.user.isEnabled = this.state === 'Active'
84+
this.userService.createApsUser(this.user).subscribe({
85+
next: (response: any) => {
86+
this.errorService.showSuccess('APS user created successfully')
87+
// NOTE: Next line routes user to the User Management page
88+
this.router.navigateByUrl('/user-management');
89+
// NOTE: get User ID from the response and redirect to the user details page:
90+
// this.router.navigateByUrl(`/user/${response.userId}`);
91+
},
92+
complete: () => {
93+
this.cd.detectChanges();
94+
this.loaderService.loadingEnd();
95+
},
96+
});
97+
}
98+
99+
private init(): void {
100+
this.loaderService.loadingStart();
101+
102+
forkJoin([
103+
this.rolesService.getRoles(),
104+
this.requestAccessService.getOrganizationTypes(),
105+
this.requestAccessService.getOrganizations(),
106+
]).subscribe({
107+
next: ([roles, orgTypes, organizations]) => {
108+
this.orgTypes = orgTypes.sort((a, b) => this.sortHandler(a.label, b.label));
109+
this.organizations = organizations.sort((a, b) => this.sortHandler(a.label, b.label));
110+
this.organizationsRaw = organizations.sort((a, b) => this.sortHandler(a.label, b.label));
111+
this.tags = this.roles.filter(x => x.selected).map(x => x.label);
112+
this.roles = roles.map((role) => ({ label: role.userRoleNm, value: role.userRoleCd, selected: false }));
113+
this.cd.detectChanges();
114+
},
115+
complete: () => {
116+
this.loaderService.loadingEnd();
117+
},
118+
});
119+
}
120+
121+
private sortHandler(a: string, b: string): number {
122+
if (a > b) return 1;
123+
if (a < b) return -1;
124+
return 0;
125+
}
126+
}

frontend/src/app/features/components/user-management/user-management.component.html

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<div class="title extra-space-bottom">User Management</div>
1+
<div class="title extra-space-bottom"><span class="title-text">User Management</span> <button pButton
2+
icon="pi pi-user-plus" (click)="onAddApsUserClicked()">&nbsp;Add APS User</button> </div>
23
<div class="table-card-container">
34
<div class="search-filter-container extra-space-bottom">
45
<div class="form-row">
@@ -101,12 +102,12 @@
101102
<ng-template pTemplate="body" let-accessRequest let-index="rowIndex">
102103
<tr id="row-{{index}}">
103104
<td>{{ accessRequest.accessRequestDtm | dateFormat }}</td>
104-
<td>{{ accessRequest.identityProviderNm }}</td>
105-
<td>{{ accessRequest.givenNm }}</td>
106-
<td>{{ accessRequest.familyNm }}</td>
107-
<td>{{ accessRequest.organizationType }}</td>
108-
<td>{{ accessRequest.emailAddressDsc }}</td>
109-
<td>{{ accessRequest.organizationNm }}</td>
105+
<td>{{ accessRequest.identityProviderNm || '-'}}</td>
106+
<td>{{ accessRequest.givenNm || '-' }}</td>
107+
<td>{{ accessRequest.familyNm || '-'}}</td>
108+
<td>{{ accessRequest.organizationType || '-'}}</td>
109+
<td>{{ accessRequest.emailAddressDsc || '-'}}</td>
110+
<td>{{ accessRequest.organizationNm || '-'}}</td>
110111
<td>
111112
<ng-container [ngSwitch]="accessRequest.accessRequestStatusCd">
112113
<ng-container *ngSwitchCase="'Approved'">

frontend/src/app/features/components/user-management/user-management.component.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
width: 100%;
33
height: 100%;
44
padding: 22px;
5+
padding-top: 12px;
56
background-color: #FFFFFF;
67

78
.bold-text {
@@ -12,6 +13,9 @@
1213
font-size: 24px;
1314
font-family: 'BcSans-bold';
1415
margin-bottom: 14px;
16+
display: flex;
17+
justify-content: space-between;
18+
align-items: center;
1519
}
1620

1721
.table-card-container {

0 commit comments

Comments
 (0)