Skip to content

Commit 2491f42

Browse files
authored
Merge pull request #1821 from bcgov/feature/ALCS-2117
Add 'Go to card' button for Commissioners
2 parents 6a56f5c + cd19e3b commit 2491f42

File tree

6 files changed

+125
-22
lines changed

6 files changed

+125
-22
lines changed

alcs-frontend/src/app/services/decision-meeting/decision-meeting.service.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
33
import { firstValueFrom } from 'rxjs';
44
import { environment } from '../../../environments/environment';
55
import { ToastService } from '../toast/toast.service';
6-
import { UpcomingMeetingBoardMapDto } from './decision-meeting.dto';
6+
import { UpcomingMeetingBoardMapDto, UpcomingMeetingDto } from './decision-meeting.dto';
77

88
@Injectable({
99
providedIn: 'root',
@@ -16,7 +16,16 @@ export class DecisionMeetingService {
1616
private toastService: ToastService,
1717
) {}
1818

19-
async fetch() {
19+
async fetch(fileNumber?: string) {
20+
if (fileNumber !== undefined) {
21+
try {
22+
const meetings = await firstValueFrom(this.http.get<UpcomingMeetingDto[]>(`${this.url}/${fileNumber}`));
23+
const record: UpcomingMeetingBoardMapDto = { all: meetings };
24+
return record;
25+
} catch (err) {
26+
this.toastService.showErrorToast('Failed to fetch scheduled discussions');
27+
}
28+
}
2029
try {
2130
return await firstValueFrom(this.http.get<UpcomingMeetingBoardMapDto>(`${this.url}/overview/meetings`));
2231
} catch (err) {

alcs-frontend/src/app/shared/details-header/details-header.component.html

+12
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ <h5 class="detail-heading">
3838
<mat-icon style="transform: scale(1.1)">arrow_right_alt</mat-icon>
3939
</div>
4040
</button>
41+
<button
42+
*ngIf="isCommissioner && hasMeetings"
43+
class="menu-item"
44+
mat-flat-button
45+
color="accent"
46+
(click)="onGoToSchedule(_application.fileNumber)"
47+
>
48+
<div class="center">
49+
Go to card
50+
<mat-icon style="transform: scale(1.1)">arrow_right_alt</mat-icon>
51+
</div>
52+
</button>
4153
<ng-container *ngIf="linkedCards.length > 1">
4254
<button class="menu-item center" mat-flat-button color="accent" [matMenuTriggerFor]="goToMenu">
4355
Go to card ▾

alcs-frontend/src/app/shared/details-header/details-header.component.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { HttpClientTestingModule } from '@angular/common/http/testing';
23
import { RouterTestingModule } from '@angular/router/testing';
34

45
import { DetailsHeaderComponent } from './details-header.component';
@@ -9,7 +10,7 @@ describe('DetailsHeaderComponent', () => {
910

1011
beforeEach(async () => {
1112
await TestBed.configureTestingModule({
12-
imports: [RouterTestingModule],
13+
imports: [RouterTestingModule, HttpClientTestingModule],
1314
declarations: [DetailsHeaderComponent],
1415
providers: [],
1516
}).compileComponents();

alcs-frontend/src/app/shared/details-header/details-header.component.ts

+70-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Component, Input } from '@angular/core';
1+
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
22
import { Router } from '@angular/router';
3-
import { Subject } from 'rxjs';
3+
import { combineLatestWith, Subject, takeUntil } from 'rxjs';
44
import { ApplicationTypeDto } from '../../services/application/application-code.dto';
55
import { ApplicationModificationDto } from '../../services/application/application-modification/application-modification.dto';
66
import { ApplicationReconsiderationDto } from '../../services/application/application-reconsideration/application-reconsideration.dto';
@@ -23,14 +23,18 @@ import {
2323
import { TimeTrackable } from '../time-tracker/time-tracker.component';
2424
import { ApplicationDetailService } from '../../services/application/application-detail.service';
2525
import { ApplicationSubmissionService } from '../../services/application/application-submission/application-submission.service';
26+
import { AuthenticationService, ROLES } from '../../services/authentication/authentication.service';
27+
import { BoardService } from '../../services/board/board.service';
28+
import { DecisionMeetingService } from '../../services/decision-meeting/decision-meeting.service';
29+
import { UpcomingMeetingBoardMapDto } from '../../services/decision-meeting/decision-meeting.dto';
2630

2731
@Component({
2832
selector: 'app-details-header[application]',
2933
templateUrl: './details-header.component.html',
3034
styleUrls: ['./details-header.component.scss'],
3135
})
32-
export class DetailsHeaderComponent {
33-
destroy = new Subject<void>();
36+
export class DetailsHeaderComponent implements OnInit, OnDestroy {
37+
$destroy = new Subject<void>();
3438

3539
@Input() heading = 'Title Here';
3640
@Input() days = 'Calendar Days';
@@ -96,6 +100,8 @@ export class DetailsHeaderComponent {
96100
this.currentStatus = DEFAULT_NO_STATUS;
97101
});
98102
}
103+
104+
this.$application.next(application);
99105
}
100106
}
101107

@@ -125,7 +131,55 @@ export class DetailsHeaderComponent {
125131
isNOI = false;
126132
currentStatus?: ApplicationSubmissionStatusPill;
127133

128-
constructor(private router: Router) {}
134+
isCommissioner: boolean = false;
135+
hasMeetings: boolean = false;
136+
137+
$meetingsByBoard = new Subject<UpcomingMeetingBoardMapDto>();
138+
$application = new Subject<ApplicationDto | CommissionerApplicationDto | NoticeOfIntentDto | NotificationDto>();
139+
140+
constructor(
141+
private router: Router,
142+
private authService: AuthenticationService,
143+
private boardService: BoardService,
144+
private meetingService: DecisionMeetingService,
145+
) {}
146+
147+
ngOnInit(): void {
148+
this.loadMeetings();
149+
150+
this.authService.$currentUser.pipe(takeUntil(this.$destroy)).subscribe((currentUser) => {
151+
this.isCommissioner =
152+
!!currentUser &&
153+
!!currentUser.client_roles &&
154+
currentUser.client_roles.length === 1 &&
155+
currentUser.client_roles.includes(ROLES.COMMISSIONER);
156+
});
157+
158+
this.boardService.$boards
159+
.pipe(combineLatestWith(this.$meetingsByBoard, this.$application))
160+
.pipe(takeUntil(this.$destroy))
161+
.subscribe(([boards, meetingsByBoard, application]) => {
162+
if (boards && meetingsByBoard && application) {
163+
const visibleBoardCodes = boards.filter((board) => board.showOnSchedule).map((board) => board.code);
164+
165+
const visibleBoardCodeMeetingPairs = Object.entries(meetingsByBoard).filter(([code, _]) =>
166+
visibleBoardCodes.includes(code),
167+
);
168+
169+
this.hasMeetings = visibleBoardCodeMeetingPairs.some(([_, meetings]) =>
170+
meetings.some((meeting) => meeting.fileNumber === application?.fileNumber),
171+
);
172+
}
173+
});
174+
}
175+
176+
async loadMeetings() {
177+
const meetingsByBoards = await this.meetingService.fetch();
178+
179+
if (meetingsByBoards !== undefined) {
180+
this.$meetingsByBoard.next(meetingsByBoards);
181+
}
182+
}
129183

130184
async onGoToCard(card: CardDto) {
131185
const boardCode = card.boardCode;
@@ -134,6 +188,12 @@ export class DetailsHeaderComponent {
134188
await this.router.navigateByUrl(`/board/${boardCode}?card=${cardUuid}&type=${cardTypeCode}`);
135189
}
136190

191+
async onGoToSchedule(fileNumber: string) {
192+
if (this.isCommissioner) {
193+
await this.router.navigateByUrl(`/home?file_number=${fileNumber}`);
194+
}
195+
}
196+
137197
async setupLinkedCards() {
138198
const application = this._application;
139199
const result = [];
@@ -168,4 +228,9 @@ export class DetailsHeaderComponent {
168228
await this.applicationSubmissionService?.update(this._application?.fileNumber, { applicant });
169229
}
170230
}
231+
232+
ngOnDestroy(): void {
233+
this.$destroy.next();
234+
this.$destroy.complete();
235+
}
171236
}

alcs-frontend/src/app/shared/meeting-overview/meeting-overview.component.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ToastService } from '../../services/toast/toast.service';
1010
import { AssigneeDto, UserDto } from '../../services/user/user.dto';
1111
import { UserService } from '../../services/user/user.service';
1212
import { CardType } from '../card/card.component';
13+
import { RouterTestingModule } from '@angular/router/testing';
1314

1415
import { MeetingOverviewComponent } from './meeting-overview.component';
1516

@@ -34,6 +35,7 @@ describe('MeetingOverviewComponent', () => {
3435
mockToastService = createMock();
3536

3637
await TestBed.configureTestingModule({
38+
imports: [RouterTestingModule],
3739
providers: [
3840
{
3941
provide: DecisionMeetingService,

alcs-frontend/src/app/shared/meeting-overview/meeting-overview.component.ts

+28-14
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Component, OnDestroy, OnInit } from '@angular/core';
22
import * as moment from 'moment';
3-
import { Subject, takeUntil } from 'rxjs';
3+
import { combineLatestWith, Subject, takeUntil } from 'rxjs';
44
import { ROLES } from '../../services/authentication/authentication.service';
55
import { BoardService, BoardWithFavourite } from '../../services/board/board.service';
66
import { UpcomingMeetingBoardMapDto, UpcomingMeetingDto } from '../../services/decision-meeting/decision-meeting.dto';
77
import { DecisionMeetingService } from '../../services/decision-meeting/decision-meeting.service';
88
import { ToastService } from '../../services/toast/toast.service';
99
import { UserService } from '../../services/user/user.service';
1010
import { CardType } from '../card/card.component';
11+
import { ActivatedRoute, Router } from '@angular/router';
1112

1213
type MeetingCollection = {
1314
meetingDate: number;
@@ -45,13 +46,22 @@ export class MeetingOverviewComponent implements OnInit, OnDestroy {
4546
private boardService: BoardService,
4647
private toastService: ToastService,
4748
private userService: UserService,
49+
private route: ActivatedRoute,
50+
private router: Router,
4851
) {}
4952

5053
ngOnInit(): void {
51-
this.boardService.$boards.pipe(takeUntil(this.destroy)).subscribe((boards) => {
52-
this.boards = boards;
53-
this.loadMeetings();
54-
});
54+
this.boardService.$boards
55+
.pipe(combineLatestWith(this.route.queryParams))
56+
.pipe(takeUntil(this.destroy))
57+
.subscribe(async ([boards, params]) => {
58+
this.boards = boards;
59+
await this.loadMeetings();
60+
61+
if (this.viewData.length > 0 && params['file_number'] !== undefined) {
62+
this.findAndExpandAll(params['file_number']);
63+
}
64+
});
5565

5666
this.userService.$userProfile.pipe(takeUntil(this.destroy)).subscribe((currentUser) => {
5767
if (currentUser) {
@@ -72,11 +82,11 @@ export class MeetingOverviewComponent implements OnInit, OnDestroy {
7282
const meetings = await this.meetingService.fetch();
7383
if (meetings) {
7484
this.meetings = meetings;
75-
this.populateViewData();
85+
await this.populateViewData();
7686
}
7787
}
7888

79-
private populateViewData() {
89+
private async populateViewData() {
8090
if (this.meetings && this.boards.length > 0) {
8191
this.viewData = this.boards
8292
.filter((board) => board.showOnSchedule)
@@ -146,26 +156,30 @@ export class MeetingOverviewComponent implements OnInit, OnDestroy {
146156
}
147157

148158
onSearch() {
159+
this.findAndExpandAll(this.searchText);
160+
}
161+
162+
findAndExpandAll(fileNumber: string) {
149163
let foundResult = false;
150164
this.viewData = this.viewData.map((board) => {
151165
board.isExpanded = false;
152166
board.pastMeetings = board.pastMeetings.map((meeting) => {
153-
const res = this.findAndExpand(meeting, board);
167+
const res = this.findAndExpand(meeting, board, fileNumber);
154168
if (res.isExpanded) {
155169
foundResult = true;
156170
}
157171
return res;
158172
});
159173

160174
if (board.nextMeeting) {
161-
const res = this.findAndExpand(board.nextMeeting, board);
175+
const res = this.findAndExpand(board.nextMeeting, board, fileNumber);
162176
if (res.isExpanded) {
163177
foundResult = true;
164178
}
165179
}
166180

167181
board.upcomingMeetings = board.upcomingMeetings.map((meeting) => {
168-
const res = this.findAndExpand(meeting, board);
182+
const res = this.findAndExpand(meeting, board, fileNumber);
169183
if (res.isExpanded) {
170184
foundResult = true;
171185
}
@@ -182,11 +196,11 @@ export class MeetingOverviewComponent implements OnInit, OnDestroy {
182196
}
183197
}
184198

185-
private findAndExpand(meeting: MeetingCollection, board: BoardWithDecisionMeetings) {
199+
private findAndExpand(meeting: MeetingCollection, board: BoardWithDecisionMeetings, fileNumber: string) {
186200
meeting.isExpanded = false;
187201
meeting.meetings = meeting.meetings.map((application) => {
188202
application.isHighlighted = false;
189-
if (application.fileNumber === this.searchText) {
203+
if (application.fileNumber === fileNumber) {
190204
meeting.isExpanded = true;
191205
board.isExpanded = true;
192206
application.isHighlighted = true;
@@ -243,10 +257,10 @@ export class MeetingOverviewComponent implements OnInit, OnDestroy {
243257
return el ? el.offsetWidth < el.scrollWidth : false;
244258
}
245259

246-
openMeetings(fileNumber: string, type: CardType) {
260+
async openMeetings(fileNumber: string, type: CardType) {
247261
this.clearHighlight();
248262
const target = type === CardType.PLAN ? 'planning-review' : 'application';
249263
const url = this.isCommissioner ? `/commissioner/${target}/${fileNumber}` : `/${target}/${fileNumber}/review`;
250-
window.open(url, '_blank');
264+
await this.router.navigateByUrl(url);
251265
}
252266
}

0 commit comments

Comments
 (0)