Skip to content

Commit 9d308bc

Browse files
authored
2393 feat: C&E property component (#2455)
* feat: property component, FE only * small fix up * updated enum, cleaned up formatting, addressed error catching * backend set up and migrations * fixed flex items * draft saved work, automatic not functional as of yet * fixed save draft for property * fix: be tests * added line spacing * final change to allow blank values after user has provided input * fix: pid and pin now set instead of null - use http param instead of creating string for cleaner code - updated auto mapper for pid and pin
1 parent 6379204 commit 9d308bc

24 files changed

+1092
-17
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { OverviewComponent } from './overview/overview.component';
55
import { MatMomentDateModule } from '@angular/material-moment-adapter';
66
import { DraftComponent } from './draft/draft.component';
77
import { SubmitterComponent } from './submitter/submitter.component';
8+
import { PropertyComponent } from './property/property.component';
89

910
const routes: Routes = [
1011
{
@@ -14,7 +15,7 @@ const routes: Routes = [
1415
];
1516

1617
@NgModule({
17-
declarations: [DraftComponent, OverviewComponent, SubmitterComponent],
18+
declarations: [DraftComponent, OverviewComponent, SubmitterComponent, PropertyComponent],
1819
imports: [SharedModule.forRoot(), RouterModule.forChild(routes), MatMomentDateModule],
1920
})
2021
export class ComplianceAndEnforcementModule {}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ <h2>C&E File ID: {{ file?.fileNumber }}</h2>
1212
/>
1313
</section>
1414

15+
<section class="form-section">
16+
<app-compliance-and-enforcement-property
17+
#propertyComponent
18+
[parentForm]="form"
19+
[property]="property"
20+
/>
21+
</section>
22+
1523
<div class="button-container">
1624
<button type="button" mat-stroked-button color="primary" (click)="onSaveDraftClicked()">Save Draft</button>
1725
</div>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,25 @@ import {
1919
UpdateComplianceAndEnforcementSubmitterDto,
2020
} from '../../../services/compliance-and-enforcement/submitter/submitter.dto';
2121
import { ComplianceAndEnforcementSubmitterService } from '../../../services/compliance-and-enforcement/submitter/submitter.service';
22+
import { PropertyComponent } from '../property/property.component';
23+
import {
24+
CreateComplianceAndEnforcementPropertyDto,
25+
UpdateComplianceAndEnforcementPropertyDto,
26+
} from '../../../services/compliance-and-enforcement/property/property.dto';
27+
import { ComplianceAndEnforcementPropertyService } from '../../../services/compliance-and-enforcement/property/property.service';
2228

2329
describe('DraftComponent', () => {
2430
let component: DraftComponent;
2531
let fixture: ComponentFixture<DraftComponent>;
2632
let mockComplianceAndEnforcementService: DeepMocked<ComplianceAndEnforcementService>;
2733
let mockComplianceAndEnforcementSubmitterService: DeepMocked<ComplianceAndEnforcementSubmitterService>;
34+
let mockComplianceAndEnforcementPropertyService: DeepMocked<ComplianceAndEnforcementPropertyService>;
2835
let mockToastService: DeepMocked<ToastService>;
2936

3037
beforeEach(async () => {
3138
mockComplianceAndEnforcementService = createMock();
3239
mockComplianceAndEnforcementSubmitterService = createMock();
40+
mockComplianceAndEnforcementPropertyService = createMock();
3341
mockToastService = createMock();
3442

3543
await TestBed.configureTestingModule({
@@ -44,6 +52,10 @@ describe('DraftComponent', () => {
4452
provide: ComplianceAndEnforcementSubmitterService,
4553
useValue: mockComplianceAndEnforcementSubmitterService,
4654
},
55+
{
56+
provide: ComplianceAndEnforcementPropertyService,
57+
useValue: mockComplianceAndEnforcementPropertyService,
58+
},
4759
{
4860
provide: ToastService,
4961
useValue: mockToastService,

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

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { FormGroup } from '@angular/forms';
1212
import { SubmitterComponent } from '../submitter/submitter.component';
1313
import { ComplianceAndEnforcementSubmitterDto } from '../../../services/compliance-and-enforcement/submitter/submitter.dto';
1414
import { ComplianceAndEnforcementSubmitterService } from '../../../services/compliance-and-enforcement/submitter/submitter.service';
15+
import { PropertyComponent, cleanPropertyUpdate } from '../property/property.component';
16+
import { ComplianceAndEnforcementPropertyDto, UpdateComplianceAndEnforcementPropertyDto } from '../../../services/compliance-and-enforcement/property/property.dto';
17+
import { ComplianceAndEnforcementPropertyService } from '../../../services/compliance-and-enforcement/property/property.service';
1518

1619
@Component({
1720
selector: 'app-compliance-and-enforcement-draft',
@@ -25,15 +28,18 @@ export class DraftComponent implements OnInit, AfterViewInit, OnDestroy {
2528
file?: ComplianceAndEnforcementDto;
2629
initialSubmissionType?: InitialSubmissionType;
2730
submitter?: ComplianceAndEnforcementSubmitterDto;
31+
property?: ComplianceAndEnforcementPropertyDto;
2832

29-
form = new FormGroup({ overview: new FormGroup({}), submitter: new FormGroup({}) });
33+
form = new FormGroup({ overview: new FormGroup({}), submitter: new FormGroup({}), property: new FormGroup({}) });
3034

3135
@ViewChild(OverviewComponent) overviewComponent?: OverviewComponent;
3236
@ViewChild(SubmitterComponent) submitterComponent?: SubmitterComponent;
37+
@ViewChild(PropertyComponent) propertyComponent?: PropertyComponent;
3338

3439
constructor(
3540
private readonly complianceAndEnforcementService: ComplianceAndEnforcementService,
3641
private readonly complianceAndEnforcementSubmitterService: ComplianceAndEnforcementSubmitterService,
42+
private readonly complianceAndEnforcementPropertyService: ComplianceAndEnforcementPropertyService,
3743
private readonly route: ActivatedRoute,
3844
private readonly toastService: ToastService,
3945
) {}
@@ -47,7 +53,7 @@ export class DraftComponent implements OnInit, AfterViewInit, OnDestroy {
4753
}
4854

4955
ngAfterViewInit(): void {
50-
if (!this.overviewComponent || !this.submitterComponent) {
56+
if (!this.overviewComponent || !this.submitterComponent || !this.propertyComponent) {
5157
console.warn('Not all form sections component not initialized');
5258
return;
5359
}
@@ -99,36 +105,110 @@ export class DraftComponent implements OnInit, AfterViewInit, OnDestroy {
99105
.subscribe(() => {
100106
this.toastService.showSuccessToast('C&E submitter draft saved');
101107
});
108+
109+
this.propertyComponent.$changes
110+
.pipe(
111+
skip(1), // Skip the initial emission to prevent save on load
112+
debounceTime(1000),
113+
switchMap((property) => {
114+
// Only auto-save if there are meaningful changes (non-empty fields)
115+
const hasActualData = Object.values(cleanPropertyUpdate(property)).some(value =>
116+
value !== null && value !== undefined && value !== '' && value !== 0
117+
);
118+
119+
if (!hasActualData) {
120+
return EMPTY; // Prevents saving on empty that was occurring when starting a new file
121+
}
122+
123+
if (this.property?.uuid) {
124+
return this.complianceAndEnforcementPropertyService.update(this.property.uuid, property);
125+
} else if (this.file?.uuid) {
126+
return this.complianceAndEnforcementPropertyService.create({
127+
fileUuid: this.file.uuid,
128+
...cleanPropertyUpdate(property)
129+
});
130+
} else {
131+
return EMPTY;
132+
}
133+
}),
134+
tap((property) => {
135+
if (!this.property) {
136+
this.property = property;
137+
}
138+
}),
139+
catchError((error) => {
140+
console.error('Error saving C&E property draft', error);
141+
this.toastService.showErrorToast('Failed to save C&E property draft');
142+
return EMPTY;
143+
}),
144+
takeUntil(this.$destroy),
145+
)
146+
.subscribe(() => {
147+
this.toastService.showSuccessToast('C&E property draft saved');
148+
});
102149
}
103150

104151
async loadFile(fileNumber: string) {
105152
try {
106153
this.file = await this.complianceAndEnforcementService.fetchByFileNumber(fileNumber, true);
107154
this.submitter = this.file.submitters[0];
108155
this.initialSubmissionType = this.file.initialSubmissionType ?? undefined;
156+
157+
// Load property data
158+
if (this.file.uuid) {
159+
try {
160+
this.property = await this.complianceAndEnforcementPropertyService.fetchByFileUuid(this.file.uuid);
161+
if (this.propertyComponent && this.property) {
162+
this.propertyComponent.property = this.property;
163+
}
164+
} catch (error: any) {
165+
// Property might not exist yet (404 is expected)
166+
if (error.status !== 404) {
167+
console.error('Error loading property data', error);
168+
this.toastService.showErrorToast('Failed to load property data');
169+
}
170+
this.property = undefined;
171+
}
172+
}
109173
} catch (error) {
110174
console.error('Error loading C&E file', error);
111175
this.toastService.showErrorToast('Failed to load C&E file');
112176
}
113177
}
114178

115179
async onSaveDraftClicked() {
116-
if (!this.overviewComponent || !this.submitterComponent || !this.file?.uuid) {
180+
if (!this.overviewComponent || !this.submitterComponent || !this.propertyComponent || !this.file?.uuid) {
117181
return;
118182
}
119183

120184
const overviewUpdate = this.overviewComponent.$changes.getValue();
121185
const submitterUpdate = this.submitterComponent.$changes.getValue();
186+
const propertyUpdate = this.propertyComponent.$changes.getValue();
122187

123188
try {
124189
await firstValueFrom(this.complianceAndEnforcementService.update(this.file.uuid, overviewUpdate));
190+
125191
if (this.submitter?.uuid) {
126192
await firstValueFrom(
127193
this.complianceAndEnforcementSubmitterService.update(this.submitter.uuid, submitterUpdate),
128194
);
129195
} else {
130196
this.submitter = await firstValueFrom(this.complianceAndEnforcementSubmitterService.create(submitterUpdate));
131197
}
198+
199+
if (this.property?.uuid) {
200+
await firstValueFrom(
201+
this.complianceAndEnforcementPropertyService.update(this.property.uuid, propertyUpdate),
202+
);
203+
} else {
204+
this.property = await firstValueFrom(
205+
this.complianceAndEnforcementPropertyService.create({
206+
fileUuid: this.file.uuid,
207+
...cleanPropertyUpdate(propertyUpdate)
208+
}),
209+
);
210+
}
211+
132212
this.toastService.showSuccessToast('C&E file draft saved');
133213
} catch (error) {
134214
console.error('Error saving C&E file draft', error);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<form [formGroup]="form">
2+
<div class="header full-width">
3+
<h3>Property</h3>
4+
</div>
5+
6+
<mat-form-field class="full-width" appearance="outline">
7+
<mat-label>Civic Address</mat-label>
8+
<textarea matInput formControlName="civicAddress" rows="1"></textarea>
9+
</mat-form-field>
10+
11+
<mat-form-field class="full-width" appearance="outline">
12+
<mat-label>Legal Description</mat-label>
13+
<textarea matInput formControlName="legalDescription" rows="1"></textarea>
14+
</mat-form-field>
15+
16+
<div class="government-region-fields">
17+
<mat-form-field appearance="outline">
18+
<mat-label>Local or First Nation Government</mat-label>
19+
<mat-select formControlName="localGovernmentUuid" (selectionChange)="onLocalGovernmentChange($event)">
20+
<mat-option *ngFor="let lg of filteredLocalGovernments" [value]="lg.uuid">
21+
{{ lg.name }}
22+
</mat-option>
23+
</mat-select>
24+
</mat-form-field>
25+
26+
<mat-form-field appearance="outline">
27+
<mat-label>Region</mat-label>
28+
<mat-select formControlName="regionCode">
29+
<mat-option *ngFor="let region of regions" [value]="region.code">
30+
{{ region.label }}
31+
</mat-option>
32+
</mat-select>
33+
</mat-form-field>
34+
</div>
35+
36+
<div class="coordinate-fields">
37+
<div class="coordinate-field">
38+
<mat-form-field appearance="outline">
39+
<mat-label>Latitude</mat-label>
40+
<input type="number" matInput formControlName="latitude" step="0.000001" />
41+
</mat-form-field>
42+
<div class="coordinate-help">Valid Range is 48 to 61. Example: 49.55555</div>
43+
</div>
44+
45+
<div class="coordinate-field">
46+
<mat-form-field appearance="outline">
47+
<mat-label>Longitude</mat-label>
48+
<input type="number" matInput formControlName="longitude" step="0.000001" />
49+
</mat-form-field>
50+
<div class="coordinate-help">Valid Range is -140 to -113. Example: -153.44444</div>
51+
</div>
52+
</div>
53+
54+
<div class="ownership-type-section">
55+
<label class="ownership-type-label">Ownership Type</label>
56+
<mat-button-toggle-group formControlName="ownershipTypeCode" class="ownership-toggle-group">
57+
<mat-button-toggle value="SMPL">Fee Simple</mat-button-toggle>
58+
<mat-button-toggle value="CRWN">Crown</mat-button-toggle>
59+
</mat-button-toggle-group>
60+
</div>
61+
62+
<div class="pid-pin-section">
63+
<div class="pid-pin-fields">
64+
<mat-form-field appearance="outline">
65+
<mat-label>Select to Choose PID or PIN</mat-label>
66+
<mat-select formControlName="pidOrPin" (selectionChange)="onPidOrPinChange($event)">
67+
<mat-option value="PID">PID</mat-option>
68+
<mat-option value="PIN">PIN</mat-option>
69+
</mat-select>
70+
</mat-form-field>
71+
72+
<mat-form-field *ngIf="form.get('pidOrPin')?.value === 'PID'" appearance="outline">
73+
<mat-label>Enter PID</mat-label>
74+
<input matInput formControlName="pid" mask="000-000-000" placeholder="XXX-XXX-XXX" />
75+
</mat-form-field>
76+
77+
<mat-form-field *ngIf="form.get('pidOrPin')?.value === 'PIN'" appearance="outline">
78+
<mat-label>Enter PIN</mat-label>
79+
<input matInput formControlName="pin" />
80+
</mat-form-field>
81+
</div>
82+
</div>
83+
84+
<div class="area-fields">
85+
<mat-form-field appearance="outline">
86+
<mat-label>Area (ha)</mat-label>
87+
<input type="number" matInput formControlName="areaHectares" step="0.01" min="0" />
88+
</mat-form-field>
89+
90+
<mat-form-field appearance="outline">
91+
<mat-label>% within ALR</mat-label>
92+
<input type="number" matInput formControlName="alrPercentage" step="0.1" min="0" max="100" />
93+
<span matTextSuffix>%</span>
94+
</mat-form-field>
95+
</div>
96+
97+
<mat-form-field class="full-width" appearance="outline">
98+
<mat-label>ALC History</mat-label>
99+
<textarea matInput formControlName="alcHistory" rows="4" placeholder="Enter ALC history details..."></textarea>
100+
</mat-form-field>
101+
</form>

0 commit comments

Comments
 (0)