Skip to content

Commit 333cea1

Browse files
authored
Merge pull request #2487 from bcgov/2416-create-complaint-referral-tab-from-submission
2416 create complaint referral tab from submission
2 parents 14c5e95 + 4170e69 commit 333cea1

19 files changed

+569
-29
lines changed

alcs-frontend/angular.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
"budgets": [
5151
{
5252
"type": "initial",
53-
"maximumWarning": "500kb",
54-
"maximumError": "2mb"
53+
"maximumWarning": "2mb",
54+
"maximumError": "3mb"
5555
},
5656
{
5757
"type": "anyComponentStyle",

alcs-frontend/src/app/features/compliance-and-enforcement/compliance-and-enforcement.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('ComplianceAndEnforcementComponent', () => {
4040

4141
it('should call service.loadFile with correct arguments in loadFile()', async () => {
4242
await component.loadFile('456');
43-
expect(mockService.loadFile).toHaveBeenCalledWith('456', { withProperty: true });
43+
expect(mockService.loadFile).toHaveBeenCalledWith('456', undefined);
4444
});
4545

4646
it('should complete destroy subject on ngOnDestroy', () => {

alcs-frontend/src/app/features/compliance-and-enforcement/compliance-and-enforcement.component.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { Component, OnDestroy, OnInit } from '@angular/core';
2-
import { ActivatedRoute } from '@angular/router';
2+
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
33
import { Subject, takeUntil } from 'rxjs';
44
import { detailsRoutes } from './compliance-and-enforcement.module';
55
import { ComplianceAndEnforcementDto } from '../../services/compliance-and-enforcement/compliance-and-enforcement.dto';
6-
import { ComplianceAndEnforcementService } from '../../services/compliance-and-enforcement/compliance-and-enforcement.service';
6+
import {
7+
ComplianceAndEnforcementService,
8+
FetchOptions,
9+
} from '../../services/compliance-and-enforcement/compliance-and-enforcement.service';
710
import { ToastService } from '../../services/toast/toast.service';
811

912
@Component({
@@ -21,6 +24,7 @@ export class ComplianceAndEnforcementComponent implements OnInit, OnDestroy {
2124

2225
constructor(
2326
private readonly route: ActivatedRoute,
27+
private readonly router: Router,
2428
private readonly service: ComplianceAndEnforcementService,
2529
private readonly toastService: ToastService,
2630
) {}
@@ -30,12 +34,18 @@ export class ComplianceAndEnforcementComponent implements OnInit, OnDestroy {
3034
this.file = file ?? undefined;
3135
});
3236

37+
this.router.events.pipe(takeUntil(this.$destroy)).subscribe((event) => {
38+
if (event instanceof NavigationEnd && this.fileNumber) {
39+
this.loadFile(this.fileNumber, { withSubmitters: true });
40+
}
41+
});
42+
3343
this.route.params.pipe(takeUntil(this.$destroy)).subscribe(async (params) => {
3444
const { fileNumber } = params;
3545

3646
if (fileNumber) {
3747
this.fileNumber = fileNumber;
38-
this.loadFile(fileNumber);
48+
this.loadFile(fileNumber, { withSubmitters: true });
3949
}
4050
});
4151
}
@@ -45,9 +55,9 @@ export class ComplianceAndEnforcementComponent implements OnInit, OnDestroy {
4555
this.$destroy.complete();
4656
}
4757

48-
async loadFile(fileNumber: string) {
58+
async loadFile(fileNumber: string, options?: FetchOptions) {
4959
try {
50-
await this.service.loadFile(fileNumber, { withProperty: true });
60+
await this.service.loadFile(fileNumber, options);
5161
} catch (error) {
5262
console.error('Error loading file:', error);
5363
this.toastService.showErrorToast('Failed to load file');

alcs-frontend/src/app/features/compliance-and-enforcement/compliance-and-enforcement.module.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { DetailsOverviewComponent } from './details/overview/details-overview.co
1212
import { ComplianceAndEnforcementComponent } from './compliance-and-enforcement.component';
1313
import { CommonModule } from '@angular/common';
1414
import { DetailsHeaderComponent } from './details/header/details-header.component';
15+
import { ComplaintReferralComponent } from './details/complaint-referral/complaint-referral.component';
16+
import { ComplaintReferralOverviewComponent } from './details/complaint-referral/overview/overview.component';
17+
import { ComplaintReferralSubmittersComponent } from './details/complaint-referral/submitters/submitters.component';
1518

1619
export const detailsRoutes: (Route & { icon?: string; menuTitle?: string })[] = [
1720
{
@@ -20,6 +23,23 @@ export const detailsRoutes: (Route & { icon?: string; menuTitle?: string })[] =
2023
icon: 'summarize',
2124
menuTitle: 'Overview',
2225
},
26+
{
27+
path: 'complaint-referral',
28+
icon: 'edit_note',
29+
menuTitle: 'Complaint / Referral',
30+
children: [
31+
{
32+
path: '',
33+
component: ComplaintReferralComponent,
34+
data: { editing: null },
35+
},
36+
{
37+
path: 'overview/edit',
38+
component: ComplaintReferralComponent,
39+
data: { editing: 'overview' },
40+
},
41+
],
42+
},
2343
];
2444

2545
const routes: Routes = [
@@ -45,6 +65,9 @@ const routes: Routes = [
4565
ComplianceAndEnforcementComponent,
4666
DetailsHeaderComponent,
4767
DetailsOverviewComponent,
68+
ComplaintReferralComponent,
69+
ComplaintReferralOverviewComponent,
70+
ComplaintReferralSubmittersComponent,
4871
],
4972
imports: [SharedModule.forRoot(), RouterModule.forChild(routes), MatMomentDateModule, CommonModule, SharedModule],
5073
})
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<ng-container *ngIf="editing === null">
2+
<section class="file-section">
3+
<app-compliance-and-enforcement-complaint-referral-overview [file]="file" />
4+
5+
<div class="edit-section-button-container">
6+
<a mat-flat-button color="accent" [routerLink]="'overview/edit'" class="edit-section-button">Edit Section</a>
7+
</div>
8+
</section>
9+
10+
<section class="file-section">
11+
<app-compliance-and-enforcement-complaint-referral-submitters [file]="file" />
12+
</section>
13+
14+
<section class="file-section">
15+
<app-compliance-and-enforcement-documents
16+
#submissionDocumentsComponent
17+
[title]="'Complaint/Referral Submission'"
18+
[fileNumber]="fileNumber"
19+
[options]="submissionDocumentOptions"
20+
[section]="Section.SUBMISSION"
21+
/>
22+
</section>
23+
</ng-container>
24+
25+
<ng-container *ngIf="editing === 'overview'">
26+
<form [formGroup]="form">
27+
<section class="form-section">
28+
<app-compliance-and-enforcement-overview #overviewComponent [parentForm]="form" [file]="file" />
29+
</section>
30+
31+
<div class="button-container">
32+
<a mat-flat-button [routerLink]="'../..'">Cancel</a>
33+
<button mat-flat-button color="primary" (click)="save()">Save</button>
34+
</div>
35+
</form>
36+
</ng-container>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@use '../../../../../styles/colors';
2+
3+
h5 {
4+
margin: 16px 0 !important;
5+
}
6+
7+
section {
8+
margin: 32px 0;
9+
}
10+
11+
section.file-section {
12+
display: flex;
13+
flex-direction: column;
14+
gap: 24px;
15+
padding: 24px;
16+
border-radius: 4px;
17+
box-shadow: 0 2px 8px 1px rgba(0, 0, 0, 0.25);
18+
margin: 32px 0;
19+
}
20+
21+
.button-container {
22+
display: flex;
23+
justify-content: space-between;
24+
margin-top: 24px;
25+
}
26+
27+
.edit-section-button-container {
28+
display: flex;
29+
justify-content: center;
30+
}
31+
32+
.edit-section-button {
33+
font-size: 14px !important;
34+
font-weight: 700 !important;
35+
text-transform: uppercase;
36+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { ComplianceAndEnforcementService } from '../../../../services/compliance-and-enforcement/compliance-and-enforcement.service';
2+
import { createMock, DeepMocked } from '@golevelup/ts-jest';
3+
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
import { ComplaintReferralComponent } from './complaint-referral.component';
5+
import { ActivatedRoute, Router } from '@angular/router';
6+
import { ToastService } from '../../../../services/toast/toast.service';
7+
import { firstValueFrom, of, Subject } from 'rxjs';
8+
import { ComplianceAndEnforcementDto } from '../../../../services/compliance-and-enforcement/compliance-and-enforcement.dto';
9+
10+
describe('ComplaintReferralComponent', () => {
11+
let component: ComplaintReferralComponent;
12+
let fixture: ComponentFixture<ComplaintReferralComponent>;
13+
let mockActivatedRoute: DeepMocked<ActivatedRoute>;
14+
let mockRouter: DeepMocked<Router>;
15+
let mockService: DeepMocked<ComplianceAndEnforcementService>;
16+
let mockToastService: DeepMocked<ToastService>;
17+
18+
beforeEach(async () => {
19+
mockActivatedRoute = createMock<ActivatedRoute>();
20+
mockRouter = createMock<Router>();
21+
mockService = createMock<ComplianceAndEnforcementService>();
22+
mockToastService = createMock<ToastService>();
23+
24+
TestBed.configureTestingModule({
25+
imports: [],
26+
declarations: [ComplaintReferralComponent],
27+
providers: [
28+
{
29+
provide: ActivatedRoute,
30+
useValue: mockActivatedRoute,
31+
},
32+
{
33+
provide: Router,
34+
useValue: mockRouter,
35+
},
36+
{
37+
provide: ComplianceAndEnforcementService,
38+
useValue: mockService,
39+
},
40+
{
41+
provide: ToastService,
42+
useValue: mockToastService,
43+
},
44+
],
45+
});
46+
47+
fixture = TestBed.createComponent(ComplaintReferralComponent);
48+
component = fixture.componentInstance;
49+
});
50+
51+
it('should create', () => {
52+
expect(component).toBeTruthy();
53+
});
54+
55+
it('should set editing from route data on ngOnInit', () => {
56+
const dataSubject = new Subject<any>();
57+
mockActivatedRoute.data = dataSubject as any;
58+
component.ngOnInit();
59+
dataSubject.next({ editing: 'overview' });
60+
expect(component.editing).toBe('overview');
61+
});
62+
63+
it('should set file, fileNumber, and submissionDocumentOptions.fileId from service.$file', () => {
64+
const fileSubject = new Subject<any>();
65+
mockService.$file = fileSubject as any;
66+
component.ngOnInit();
67+
const file = { fileNumber: '123' };
68+
69+
fileSubject.next(file);
70+
71+
expect(component.file).toBe(file);
72+
expect(component.fileNumber).toBe('123');
73+
expect(component.submissionDocumentOptions.fileId).toBe('123');
74+
});
75+
76+
it('should show error toast and not call update if fileNumber is missing on save', async () => {
77+
component.fileNumber = undefined;
78+
const toastSpy = jest.spyOn(mockToastService, 'showErrorToast');
79+
80+
await component.save();
81+
82+
expect(toastSpy).toHaveBeenCalledWith('Error loading file');
83+
expect(mockService.update).not.toHaveBeenCalled();
84+
});
85+
86+
it('should call update, show success toast, and navigate on successful save', async () => {
87+
component.fileNumber = '456';
88+
component.overviewComponent = {
89+
$changes: { getValue: () => ({ foo: 'bar' }) },
90+
} as any;
91+
mockService.update.mockReturnValue(of({} as ComplianceAndEnforcementDto));
92+
const toastSpy = jest.spyOn(mockToastService, 'showSuccessToast');
93+
const navSpy = jest.spyOn(mockRouter, 'navigate');
94+
95+
await component.save();
96+
97+
expect(mockService.update).toHaveBeenCalledWith('456', { foo: 'bar' }, { idType: 'fileNumber' });
98+
expect(toastSpy).toHaveBeenCalledWith('File updated successfully');
99+
expect(navSpy).toHaveBeenCalled();
100+
});
101+
102+
it('should catch error and not throw if update fails', async () => {
103+
component.fileNumber = '789';
104+
component.overviewComponent = {
105+
$changes: { getValue: () => ({}) },
106+
} as any;
107+
mockService.update.mockReturnValue({
108+
toPromise: () => Promise.reject(new Error('fail')),
109+
subscribe: (_: any, error: any) => error(new Error('fail')),
110+
} as any);
111+
jest.spyOn({ firstValueFrom }, 'firstValueFrom').mockImplementation(() => Promise.reject(new Error('fail')));
112+
113+
await expect(component.save()).resolves.not.toThrow();
114+
});
115+
116+
it('should complete $destroy on ngOnDestroy', () => {
117+
const nextSpy = jest.spyOn(component.$destroy, 'next');
118+
const completeSpy = jest.spyOn(component.$destroy, 'complete');
119+
120+
component.ngOnDestroy();
121+
122+
expect(nextSpy).toHaveBeenCalled();
123+
expect(completeSpy).toHaveBeenCalled();
124+
});
125+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
2+
import { FormGroup } from '@angular/forms';
3+
import { firstValueFrom, Subject, takeUntil } from 'rxjs';
4+
import { ComplianceAndEnforcementService } from '../../../../services/compliance-and-enforcement/compliance-and-enforcement.service';
5+
import {
6+
ComplianceAndEnforcementDto,
7+
UpdateComplianceAndEnforcementDto,
8+
} from '../../../../services/compliance-and-enforcement/compliance-and-enforcement.dto';
9+
import { Section } from '../../../../services/compliance-and-enforcement/documents/document.service';
10+
import { ActivatedRoute, Router } from '@angular/router';
11+
import { submissionDocumentOptions } from '../../draft/draft.component';
12+
import { OverviewComponent } from '../../overview/overview.component';
13+
import { ToastService } from '../../../../services/toast/toast.service';
14+
15+
@Component({
16+
selector: 'app-complaint-referral',
17+
templateUrl: './complaint-referral.component.html',
18+
styleUrls: ['./complaint-referral.component.scss'],
19+
})
20+
export class ComplaintReferralComponent implements OnInit, OnDestroy {
21+
$destroy = new Subject<void>();
22+
23+
Section = Section;
24+
25+
@ViewChild(OverviewComponent) overviewComponent?: OverviewComponent;
26+
27+
fileNumber?: string;
28+
file?: ComplianceAndEnforcementDto;
29+
30+
form = new FormGroup({ overview: new FormGroup({}), submitter: new FormGroup({}), property: new FormGroup({}) });
31+
32+
editing: string | null = null;
33+
34+
submissionDocumentOptions = submissionDocumentOptions;
35+
36+
constructor(
37+
private readonly route: ActivatedRoute,
38+
private readonly router: Router,
39+
private readonly service: ComplianceAndEnforcementService,
40+
private readonly toastService: ToastService,
41+
) {}
42+
43+
ngOnInit(): void {
44+
this.route.data.pipe(takeUntil(this.$destroy)).subscribe(async (data) => {
45+
this.editing = data['editing'];
46+
});
47+
48+
this.service.$file.pipe(takeUntil(this.$destroy)).subscribe((file) => {
49+
if (file) {
50+
this.file = file;
51+
this.fileNumber = file.fileNumber;
52+
this.submissionDocumentOptions.fileId = file.fileNumber;
53+
}
54+
});
55+
}
56+
57+
async save() {
58+
const updateDto: UpdateComplianceAndEnforcementDto = this.overviewComponent?.$changes.getValue() ?? {};
59+
60+
if (!this.fileNumber) {
61+
console.error('Error loading file by file number. File number not defined.');
62+
this.toastService.showErrorToast('Error loading file');
63+
return;
64+
}
65+
66+
try {
67+
await firstValueFrom(this.service.update(this.fileNumber, updateDto, { idType: 'fileNumber' }));
68+
this.toastService.showSuccessToast('File updated successfully');
69+
this.router.navigate(['../..'], { relativeTo: this.route });
70+
} catch (error) {
71+
console.error('Error updating file:', error);
72+
this.toastService.showErrorToast('Error loading file');
73+
}
74+
}
75+
76+
async ngOnDestroy() {
77+
this.$destroy.next();
78+
this.$destroy.complete();
79+
}
80+
}

0 commit comments

Comments
 (0)