Skip to content

Commit 2a5b392

Browse files
authored
Merge pull request #2549 from bcgov/2540-property-maps-submission
2540 Property & Maps Submission
2 parents 4586c65 + cac00e2 commit 2a5b392

File tree

16 files changed

+1253
-2
lines changed

16 files changed

+1253
-2
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { ComplaintReferralComponent } from './details/complaint-referral/complai
1616
import { ComplaintReferralOverviewComponent } from './details/complaint-referral/overview/overview.component';
1717
import { ComplaintReferralSubmittersComponent } from './details/complaint-referral/submitters/submitters.component';
1818
import { AddSubmitterDialogComponent } from './details/complaint-referral/submitters/add-submitter-dialog/add-submitter-dialog.component';
19+
import { PropertyMapsComponent } from './details/property-maps/property-maps.component';
20+
import { ResponsiblePartiesDetailsComponent } from './details/responsible-parties/responsible-parties.component';
1921

2022
export const detailsRoutes: (Route & { icon?: string; menuTitle?: string })[] = [
2123
{
@@ -46,6 +48,40 @@ export const detailsRoutes: (Route & { icon?: string; menuTitle?: string })[] =
4648
},
4749
],
4850
},
51+
{
52+
path: 'property-maps',
53+
icon: 'location_on',
54+
menuTitle: 'Property & Maps',
55+
children: [
56+
{
57+
path: '',
58+
component: PropertyMapsComponent,
59+
data: { editing: null },
60+
},
61+
{
62+
path: 'edit',
63+
component: PropertyMapsComponent,
64+
data: { editing: 'property' },
65+
},
66+
],
67+
},
68+
{
69+
path: 'responsible-parties',
70+
icon: 'people',
71+
menuTitle: 'Responsible Parties',
72+
children: [
73+
{
74+
path: '',
75+
component: ResponsiblePartiesDetailsComponent,
76+
data: { editing: null },
77+
},
78+
{
79+
path: 'edit',
80+
component: ResponsiblePartiesDetailsComponent,
81+
data: { editing: 'parties' },
82+
},
83+
],
84+
},
4985
];
5086

5187
const routes: Routes = [
@@ -75,6 +111,8 @@ const routes: Routes = [
75111
ComplaintReferralOverviewComponent,
76112
ComplaintReferralSubmittersComponent,
77113
AddSubmitterDialogComponent,
114+
PropertyMapsComponent,
115+
ResponsiblePartiesDetailsComponent,
78116
],
79117
imports: [SharedModule.forRoot(), RouterModule.forChild(routes), MatMomentDateModule, CommonModule, SharedModule],
80118
})
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<ng-container *ngIf="editing === null">
2+
<section class="file-section">
3+
<h3>Property</h3>
4+
5+
<div class="property-overview">
6+
<div class="inset">
7+
<div class="field">
8+
<div class="field-heading">Civic Address</div>
9+
<ng-container *ngIf="file?.property?.civicAddress; else noData">
10+
{{ file?.property?.civicAddress }}
11+
</ng-container>
12+
</div>
13+
14+
<div class="field">
15+
<div class="field-heading">Legal Description</div>
16+
<ng-container *ngIf="file?.property?.legalDescription; else noData">
17+
{{ file?.property?.legalDescription }}
18+
</ng-container>
19+
</div>
20+
21+
<div class="field">
22+
<div class="field-heading">Local or First Nation Government</div>
23+
<ng-container *ngIf="getLocalGovernmentName(file?.property?.localGovernmentUuid); else noData">
24+
{{ getLocalGovernmentName(file?.property?.localGovernmentUuid) }}
25+
</ng-container>
26+
</div>
27+
28+
<div class="field">
29+
<div class="field-heading">Region</div>
30+
<ng-container *ngIf="file?.property?.regionCode; else noData">
31+
{{ file?.property?.regionCode }}
32+
</ng-container>
33+
</div>
34+
35+
<div class="field">
36+
<div class="field-heading">Latitude</div>
37+
<ng-container *ngIf="file?.property?.latitude !== undefined && file?.property?.latitude !== null; else noData">
38+
{{ file?.property?.latitude | number:'1.5-5' }}
39+
</ng-container>
40+
</div>
41+
42+
<div class="field">
43+
<div class="field-heading">Longitude</div>
44+
<ng-container *ngIf="file?.property?.longitude !== undefined && file?.property?.longitude !== null; else noData">
45+
{{ file?.property?.longitude | number:'1.5-5' }}
46+
</ng-container>
47+
</div>
48+
49+
<div class="field">
50+
<div class="field-heading">Ownership Type</div>
51+
<ng-container *ngIf="file?.property?.ownershipTypeCode; else noData">
52+
{{ file?.property?.ownershipTypeCode === 'SMPL' ? 'Fee Simple' : 'Crown' }}
53+
</ng-container>
54+
</div>
55+
56+
<div class="field" *ngIf="file?.property?.pid">
57+
<div class="field-heading">PID</div>
58+
{{ file?.property?.pid }}
59+
</div>
60+
61+
<div class="field" *ngIf="file?.property?.pin">
62+
<div class="field-heading">PIN</div>
63+
{{ file?.property?.pin }}
64+
</div>
65+
66+
<div class="field">
67+
<div class="field-heading">Area (ha)</div>
68+
<ng-container *ngIf="file?.property?.areaHectares !== undefined && file?.property?.areaHectares !== null; else noData">
69+
{{ file?.property?.areaHectares }}
70+
</ng-container>
71+
</div>
72+
73+
<div class="field">
74+
<div class="field-heading">% within ALR</div>
75+
<ng-container *ngIf="file?.property?.alrPercentage !== undefined && file?.property?.alrPercentage !== null; else noData">
76+
{{ file?.property?.alrPercentage }}%
77+
</ng-container>
78+
</div>
79+
80+
<div class="field full-width">
81+
<div class="field-heading">ALC History</div>
82+
<ng-container *ngIf="file?.property?.alcHistory; else noData">
83+
{{ file?.property?.alcHistory }}
84+
</ng-container>
85+
</div>
86+
</div>
87+
</div>
88+
89+
<div class="edit-section-button-container">
90+
<a mat-flat-button color="accent" [routerLink]="'edit'" class="edit-section-button">Edit Section</a>
91+
</div>
92+
</section>
93+
94+
<section class="file-section">
95+
<app-compliance-and-enforcement-documents
96+
#mapsDocumentsComponent
97+
[title]="'Maps'"
98+
[noDocumentsText]="'No maps'"
99+
[fileNumber]="fileNumber"
100+
[options]="mapsDocumentOptions"
101+
[section]="Section.MAPS"
102+
[addButtonText]="'+ Add Maps'"
103+
/>
104+
</section>
105+
</ng-container>
106+
107+
<ng-container *ngIf="editing === 'property'">
108+
<form [formGroup]="form">
109+
<section class="form-section">
110+
<h3>Property & Maps > Edit Property</h3>
111+
<app-compliance-and-enforcement-property
112+
#propertyComponent
113+
[property]="file?.property"
114+
[parentForm]="form"
115+
/>
116+
</section>
117+
118+
<div class="button-container">
119+
<a mat-stroked-button color="warn" [routerLink]="'..'">Cancel</a>
120+
<button mat-flat-button color="primary" [disabled]="form.invalid" (click)="saveProperty()">Save</button>
121+
</div>
122+
</form>
123+
</ng-container>
124+
125+
<ng-template #noData>
126+
<div class="no-data">No Data</div>
127+
</ng-template>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
}
37+
38+
a[mat-stroked-button] {
39+
font-size: 14px !important;
40+
font-weight: 700 !important;
41+
text-transform: uppercase;
42+
}
43+
44+
.no-data {
45+
color: colors.$grey;
46+
}
47+
48+
.inset {
49+
display: grid;
50+
grid-template-columns: 1fr 1fr;
51+
gap: 24px;
52+
53+
background-color: colors.$grey-light;
54+
padding: 24px;
55+
margin-top: 24px;
56+
}
57+
58+
.field {
59+
width: 100%;
60+
61+
&.full-width {
62+
grid-column: span 2;
63+
}
64+
}
65+
66+
.field-heading {
67+
font-weight: bold;
68+
margin-bottom: 5px;
69+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { ComplianceAndEnforcementService } from '../../../../services/compliance-and-enforcement/compliance-and-enforcement.service';
2+
import { ComplianceAndEnforcementPropertyService } from '../../../../services/compliance-and-enforcement/property/property.service';
3+
import { ApplicationLocalGovernmentService } from '../../../../services/application/application-local-government/application-local-government.service';
4+
import { createMock, DeepMocked } from '@golevelup/ts-jest';
5+
import { ComponentFixture, TestBed } from '@angular/core/testing';
6+
import { PropertyMapsComponent } from './property-maps.component';
7+
import { ActivatedRoute, Router } from '@angular/router';
8+
import { ToastService } from '../../../../services/toast/toast.service';
9+
import { firstValueFrom, of, Subject } from 'rxjs';
10+
import { ComplianceAndEnforcementDto } from '../../../../services/compliance-and-enforcement/compliance-and-enforcement.dto';
11+
import { ComplianceAndEnforcementPropertyDto } from '../../../../services/compliance-and-enforcement/property/property.dto';
12+
13+
describe('PropertyMapsComponent', () => {
14+
let component: PropertyMapsComponent;
15+
let fixture: ComponentFixture<PropertyMapsComponent>;
16+
let mockActivatedRoute: DeepMocked<ActivatedRoute>;
17+
let mockRouter: DeepMocked<Router>;
18+
let mockService: DeepMocked<ComplianceAndEnforcementService>;
19+
let mockPropertyService: DeepMocked<ComplianceAndEnforcementPropertyService>;
20+
let mockToastService: DeepMocked<ToastService>;
21+
let mockLocalGovernmentService: DeepMocked<ApplicationLocalGovernmentService>;
22+
23+
beforeEach(async () => {
24+
mockActivatedRoute = createMock<ActivatedRoute>();
25+
mockRouter = createMock<Router>();
26+
mockService = createMock<ComplianceAndEnforcementService>();
27+
mockPropertyService = createMock<ComplianceAndEnforcementPropertyService>();
28+
mockToastService = createMock<ToastService>();
29+
mockLocalGovernmentService = createMock<ApplicationLocalGovernmentService>();
30+
31+
TestBed.configureTestingModule({
32+
imports: [],
33+
declarations: [PropertyMapsComponent],
34+
providers: [
35+
{
36+
provide: ActivatedRoute,
37+
useValue: mockActivatedRoute,
38+
},
39+
{
40+
provide: Router,
41+
useValue: mockRouter,
42+
},
43+
{
44+
provide: ComplianceAndEnforcementService,
45+
useValue: mockService,
46+
},
47+
{
48+
provide: ComplianceAndEnforcementPropertyService,
49+
useValue: mockPropertyService,
50+
},
51+
{
52+
provide: ToastService,
53+
useValue: mockToastService,
54+
},
55+
{
56+
provide: ApplicationLocalGovernmentService,
57+
useValue: mockLocalGovernmentService,
58+
},
59+
],
60+
});
61+
62+
fixture = TestBed.createComponent(PropertyMapsComponent);
63+
component = fixture.componentInstance;
64+
});
65+
66+
it('should create', () => {
67+
expect(component).toBeTruthy();
68+
});
69+
70+
it('should set editing from route data on ngOnInit', () => {
71+
const dataSubject = new Subject<any>();
72+
mockActivatedRoute.data = dataSubject as any;
73+
component.ngOnInit();
74+
dataSubject.next({ editing: 'property' });
75+
expect(component.editing).toBe('property');
76+
});
77+
78+
it('should set file, fileNumber, and mapsDocumentOptions.fileId from service.$file', () => {
79+
const fileSubject = new Subject<any>();
80+
mockService.$file = fileSubject as any;
81+
component.ngOnInit();
82+
const file = { fileNumber: '123' };
83+
84+
fileSubject.next(file);
85+
86+
expect(component.file).toBe(file);
87+
expect(component.fileNumber).toBe('123');
88+
expect(component.mapsDocumentOptions.fileId).toBe('123');
89+
});
90+
91+
it('should show error toast and not call update if property uuid is missing on save', async () => {
92+
component.file = { property: undefined } as any;
93+
const toastSpy = jest.spyOn(mockToastService, 'showErrorToast');
94+
95+
await component.saveProperty();
96+
97+
expect(toastSpy).toHaveBeenCalledWith('Error loading property');
98+
expect(mockPropertyService.update).not.toHaveBeenCalled();
99+
});
100+
101+
it('should call update, show success toast, and navigate on successful save', async () => {
102+
component.file = { property: { uuid: 'property-uuid' } } as any;
103+
component.propertyComponent = {
104+
$changes: { getValue: () => ({ foo: 'bar' }) },
105+
} as any;
106+
mockPropertyService.update.mockReturnValue(of({} as ComplianceAndEnforcementPropertyDto));
107+
const toastSpy = jest.spyOn(mockToastService, 'showSuccessToast');
108+
const navSpy = jest.spyOn(mockRouter, 'navigate');
109+
110+
await component.saveProperty();
111+
112+
expect(mockPropertyService.update).toHaveBeenCalledWith('property-uuid', { foo: 'bar' });
113+
expect(toastSpy).toHaveBeenCalledWith('Property updated successfully');
114+
expect(navSpy).toHaveBeenCalled();
115+
});
116+
117+
it('should catch error and not throw if update fails', async () => {
118+
component.file = { property: { uuid: 'property-uuid' } } as any;
119+
component.propertyComponent = {
120+
$changes: { getValue: () => ({}) },
121+
} as any;
122+
mockPropertyService.update.mockReturnValue({
123+
toPromise: () => Promise.reject(new Error('fail')),
124+
subscribe: (_: any, error: any) => error(new Error('fail')),
125+
} as any);
126+
jest.spyOn({ firstValueFrom }, 'firstValueFrom').mockImplementation(() => Promise.reject(new Error('fail')));
127+
128+
await expect(component.saveProperty()).resolves.not.toThrow();
129+
});
130+
131+
it('should complete $destroy on ngOnDestroy', () => {
132+
const nextSpy = jest.spyOn(component.$destroy, 'next');
133+
const completeSpy = jest.spyOn(component.$destroy, 'complete');
134+
135+
component.ngOnDestroy();
136+
137+
expect(nextSpy).toHaveBeenCalled();
138+
expect(completeSpy).toHaveBeenCalled();
139+
});
140+
});

0 commit comments

Comments
 (0)