Skip to content

Commit 1342076

Browse files
authored
2415 QA Create Header and Overview Tab from Submission (#2491)
* Fix status not setting - Move set status logic to backend - Update logic to allow setting status given different states - Allow setting status by file number or UUID * Always set closed date when opening file * Add address and owner name to details header - Pass property owner name into header - Determine property owner name in parent component when loading file - Allow filtering responsible parties by party type - Add address/name to header template * Use fake timers, so times line up * Add HTTP mock * Catch potentially missing parties Also caught property not being loaded with file
1 parent f5b7263 commit 1342076

File tree

13 files changed

+133
-33
lines changed

13 files changed

+133
-33
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="layout">
22
<div class="file">
3-
<app-compliance-and-enforcement-details-header [file]="file" />
3+
<app-compliance-and-enforcement-details-header [file]="file" [propertyOwnerName]="propertyOwnerName" />
44
<div class="content">
55
<div class="nav">
66
<div *ngFor="let route of detailsRoutes" class="nav-link" data-testid="details-nav-link">

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@ import { ComplianceAndEnforcementService } from '../../services/compliance-and-e
33
import { ActivatedRoute } from '@angular/router';
44
import { createMock, DeepMocked } from '@golevelup/ts-jest';
55
import { ComponentFixture, TestBed } from '@angular/core/testing';
6+
import { HttpClient } from '@angular/common/http';
67

78
describe('ComplianceAndEnforcementComponent', () => {
89
let component: ComplianceAndEnforcementComponent;
910
let fixture: ComponentFixture<ComplianceAndEnforcementComponent>;
1011
let mockService: DeepMocked<ComplianceAndEnforcementService>;
1112
let mockRoute: DeepMocked<ActivatedRoute>;
13+
let mockHttpClient: DeepMocked<HttpClient>;
1214

1315
beforeEach(async () => {
1416
mockService = createMock<ComplianceAndEnforcementService>();
1517
mockRoute = createMock<ActivatedRoute>();
18+
mockHttpClient = createMock();
1619

1720
await TestBed.configureTestingModule({
1821
imports: [],
@@ -26,6 +29,10 @@ describe('ComplianceAndEnforcementComponent', () => {
2629
provide: ActivatedRoute,
2730
useValue: mockRoute,
2831
},
32+
{
33+
provide: HttpClient,
34+
useValue: mockHttpClient,
35+
},
2936
],
3037
});
3138

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
FetchOptions,
99
} from '../../services/compliance-and-enforcement/compliance-and-enforcement.service';
1010
import { ToastService } from '../../services/toast/toast.service';
11+
import { ResponsiblePartiesService } from '../../services/compliance-and-enforcement/responsible-parties/responsible-parties.service';
12+
import { ResponsiblePartyType } from '../../services/compliance-and-enforcement/responsible-parties/responsible-parties.dto';
1113

1214
@Component({
1315
selector: 'app-compliance-and-enforcement',
@@ -21,11 +23,13 @@ export class ComplianceAndEnforcementComponent implements OnInit, OnDestroy {
2123

2224
fileNumber?: string;
2325
file?: ComplianceAndEnforcementDto;
26+
propertyOwnerName?: string;
2427

2528
constructor(
2629
private readonly route: ActivatedRoute,
2730
private readonly router: Router,
2831
private readonly service: ComplianceAndEnforcementService,
32+
private readonly responsiblePartyService: ResponsiblePartiesService,
2933
private readonly toastService: ToastService,
3034
) {}
3135

@@ -45,7 +49,7 @@ export class ComplianceAndEnforcementComponent implements OnInit, OnDestroy {
4549

4650
if (fileNumber) {
4751
this.fileNumber = fileNumber;
48-
this.loadFile(fileNumber, { withSubmitters: true });
52+
this.loadFile(fileNumber, { withSubmitters: true, withProperty: true });
4953
}
5054
});
5155
}
@@ -58,6 +62,15 @@ export class ComplianceAndEnforcementComponent implements OnInit, OnDestroy {
5862
async loadFile(fileNumber: string, options?: FetchOptions) {
5963
try {
6064
await this.service.loadFile(fileNumber, options);
65+
66+
if (this.file) {
67+
const parties = await this.responsiblePartyService.fetchByFileNumber(
68+
fileNumber,
69+
ResponsiblePartyType.PROPERTY_OWNER,
70+
);
71+
72+
this.propertyOwnerName = parties?.[0].organizationName || parties?.[0].individualName;
73+
}
6174
} catch (error) {
6275
console.error('Error loading file:', error);
6376
this.toastService.showErrorToast('Failed to load file');

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
<div class="first-row">
66
<div class="title">
77
<h5 class="detail-heading">
8-
<div class="file-number">{{ file?.fileNumber }}</div>
8+
<div class="file-number">
9+
{{ file?.fileNumber }} - {{ file?.property?.civicAddress }} - {{ propertyOwnerName }}
10+
</div>
911
</h5>
1012
</div>
1113
</div>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export class DetailsHeaderComponent implements OnDestroy {
3131
return this._file;
3232
}
3333

34+
@Input() propertyOwnerName?: string;
35+
3436
ngOnDestroy(): void {
3537
this.$destroy.next();
3638
this.$destroy.complete();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class DetailsOverviewComponent implements OnInit, OnDestroy {
4545
return;
4646
}
4747

48-
await this.service.setStatus(this.fileNumber, this.status.value);
48+
await this.service.setStatus(this.fileNumber, this.status.value, { idType: 'fileNumber' });
4949
this.service.loadFile(this.fileNumber, { withProperty: true });
5050

5151
this.endEdit();

alcs-frontend/src/app/services/compliance-and-enforcement/compliance-and-enforcement.service.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Injectable } from '@angular/core';
33
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
44
import { environment } from '../../../environments/environment';
55
import { ComplianceAndEnforcementDto, UpdateComplianceAndEnforcementDto } from './compliance-and-enforcement.dto';
6-
import moment from 'moment';
76

87
export enum Status {
98
OPEN = 'Open',
@@ -67,16 +66,18 @@ export class ComplianceAndEnforcementService {
6766
return this.http.patch<ComplianceAndEnforcementDto>(`${this.url}/${id}?idType=${options.idType}`, updateDto);
6867
}
6968

70-
async setStatus(fileNumber: string, status: Status) {
71-
const dto: UpdateComplianceAndEnforcementDto = {
72-
dateClosed: status === Status.CLOSED ? moment.now() : null,
73-
};
74-
75-
await firstValueFrom(this.update(fileNumber, dto, { idType: 'fileNumber' }));
69+
async setStatus(
70+
id: string,
71+
status: Status,
72+
options: { idType: string } = { idType: 'uuid' },
73+
): Promise<ComplianceAndEnforcementDto> {
74+
return await firstValueFrom(
75+
this.http.patch<ComplianceAndEnforcementDto>(`${this.url}/${id}/status?idType=${options.idType}`, status),
76+
);
7677
}
7778

7879
async delete(uuid: string): Promise<UpdateComplianceAndEnforcementDto> {
79-
return await firstValueFrom(this.http.delete<UpdateComplianceAndEnforcementDto>(`${this.url}/${uuid}`));
80+
return await firstValueFrom(this.http.delete<ComplianceAndEnforcementDto>(`${this.url}/${uuid}`));
8081
}
8182

8283
async submit(uuid: string): Promise<ComplianceAndEnforcementDto> {

alcs-frontend/src/app/services/compliance-and-enforcement/responsible-parties/responsible-parties.service.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HttpClient } from '@angular/common/http';
1+
import { HttpClient, HttpParams } from '@angular/common/http';
22
import { Injectable } from '@angular/core';
33
import { firstValueFrom, Observable } from 'rxjs';
44
import { environment } from '../../../../environments/environment';
@@ -7,6 +7,7 @@ import {
77
CreateResponsiblePartyDto,
88
UpdateResponsiblePartyDto,
99
FOIPPACategory,
10+
ResponsiblePartyType,
1011
} from './responsible-parties.dto';
1112
import { OwnerDto, SubmissionOwnersDto } from '../../../shared/document-upload-dialog/document-upload-dialog.dto';
1213

@@ -23,7 +24,7 @@ export class ResponsiblePartiesService {
2324
}
2425

2526
async fetchSubmission(fileNumber: string): Promise<SubmissionOwnersDto> {
26-
const responsibleParties = await this.fetchByFileNumber(fileNumber);
27+
const responsibleParties = await this.fetchByFileNumber(fileNumber, ResponsiblePartyType.PROPERTY_OWNER);
2728
const responsiblePartiesOwners: OwnerDto[] = responsibleParties.map((party) => ({
2829
...party,
2930
displayName: '',
@@ -39,10 +40,16 @@ export class ResponsiblePartiesService {
3940
};
4041
}
4142

42-
async fetchByFileNumber(fileNumber: string): Promise<ResponsiblePartyDto[]> {
43-
return await firstValueFrom(
44-
this.http.get<ResponsiblePartyDto[]>(`${this.url}/file/${fileNumber}?idType=fileNumber`),
45-
);
43+
async fetchByFileNumber(fileNumber: string, partyType?: ResponsiblePartyType): Promise<ResponsiblePartyDto[]> {
44+
let params = new HttpParams();
45+
46+
params = params.set('idType', 'fileNumber');
47+
48+
if (partyType) {
49+
params = params.set('partyType', partyType);
50+
}
51+
52+
return await firstValueFrom(this.http.get<ResponsiblePartyDto[]>(`${this.url}/file/${fileNumber}`, { params }));
4653
}
4754

4855
create(createDto: CreateResponsiblePartyDto): Observable<ResponsiblePartyDto> {

services/apps/alcs/src/alcs/compliance-and-enforcement/compliance-and-enforcement.controller.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as config from 'config';
44
import { RolesGuard } from '../../common/authorization/roles-guard.service';
55
import { UserRoles } from '../../common/authorization/roles.decorator';
66
import { AUTH_ROLE, ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles';
7-
import { ComplianceAndEnforcementService } from './compliance-and-enforcement.service';
7+
import { ComplianceAndEnforcementService, Status } from './compliance-and-enforcement.service';
88
import { ComplianceAndEnforcementDto, UpdateComplianceAndEnforcementDto } from './compliance-and-enforcement.dto';
99
import { DeleteResult } from 'typeorm';
1010

@@ -50,6 +50,16 @@ export class ComplianceAndEnforcementController {
5050
return await this.service.update(id, updateDto, { idType });
5151
}
5252

53+
@Patch('/:id/status')
54+
@UserRoles(AUTH_ROLE.ADMIN, AUTH_ROLE.C_AND_E)
55+
async setStatus(
56+
@Param('id') id: string,
57+
@Body() status: Status,
58+
@Query('idType') idType: string = 'uuid',
59+
): Promise<ComplianceAndEnforcementDto> {
60+
return await this.service.setStatus(id, status, { idType });
61+
}
62+
5363
@Post('/:id/submit')
5464
@UserRoles(AUTH_ROLE.ADMIN, AUTH_ROLE.C_AND_E)
5565
async submit(@Param('id') id: string): Promise<ComplianceAndEnforcementDto> {

services/apps/alcs/src/alcs/compliance-and-enforcement/compliance-and-enforcement.service.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable } from '@nestjs/common';
22
import { DeleteResult, In, Repository } from 'typeorm';
33
import { InjectRepository } from '@nestjs/typeorm';
4-
import { AllegedActivity, ComplianceAndEnforcement, InitialSubmissionType } from './compliance-and-enforcement.entity';
4+
import { ComplianceAndEnforcement } from './compliance-and-enforcement.entity';
55
import { ComplianceAndEnforcementDto, UpdateComplianceAndEnforcementDto } from './compliance-and-enforcement.dto';
66
import { InjectMapper } from 'automapper-nestjs';
77
import { Mapper } from 'automapper-core';
@@ -13,7 +13,15 @@ import {
1313
import { ComplianceAndEnforcementSubmitterService } from './submitter/submitter.service';
1414
import { ComplianceAndEnforcementPropertyService } from './property/property.service';
1515
import { ComplianceAndEnforcementDocument } from './document/document.entity';
16-
import { ComplianceAndEnforcementValidatorService, ValidatedComplianceAndEnforcement } from './compliance-and-enforcement-validator.service';
16+
import {
17+
ComplianceAndEnforcementValidatorService,
18+
ValidatedComplianceAndEnforcement,
19+
} from './compliance-and-enforcement-validator.service';
20+
21+
export enum Status {
22+
OPEN = 'Open',
23+
CLOSED = 'Closed',
24+
}
1725

1826
@Injectable()
1927
export class ComplianceAndEnforcementService {
@@ -120,6 +128,33 @@ export class ComplianceAndEnforcementService {
120128
return this.mapper.map(savedEntity, ComplianceAndEnforcement, ComplianceAndEnforcementDto);
121129
}
122130

131+
async setStatus(
132+
id: string,
133+
status: Status,
134+
options: { idType: string } = { idType: 'uuid' },
135+
): Promise<ComplianceAndEnforcementDto> {
136+
const entity = await this.repository.findOneBy({ [options.idType]: id });
137+
if (entity === null) {
138+
throw new ServiceConflictException('A C&E file with this UUID does not exist. Unable to update.');
139+
}
140+
141+
const updateDto: UpdateComplianceAndEnforcementDto = {};
142+
143+
if (status === Status.CLOSED) {
144+
updateDto.dateClosed = new Date().getTime();
145+
}
146+
147+
if (status === Status.OPEN) {
148+
updateDto.dateClosed = null;
149+
150+
if (!entity.dateOpened) {
151+
updateDto.dateOpened = new Date().getTime();
152+
}
153+
}
154+
155+
return this.update(id, updateDto, options);
156+
}
157+
123158
async delete(uuid: string): Promise<DeleteResult> {
124159
const manager: any = this.repository.manager as any;
125160

@@ -161,7 +196,8 @@ export class ComplianceAndEnforcementService {
161196
await manager.delete(Director, { uuid: In(directorUuids) });
162197
}
163198
if (parties.length > 0) {
164-
const Party = (await import('./responsible-parties/responsible-party.entity')).ComplianceAndEnforcementResponsibleParty;
199+
const Party = (await import('./responsible-parties/responsible-party.entity'))
200+
.ComplianceAndEnforcementResponsibleParty;
165201
await manager.delete(Party, { fileUuid: file.uuid });
166202
}
167203

@@ -195,14 +231,14 @@ export class ComplianceAndEnforcementService {
195231
// Get submitters, properties, and responsible parties
196232
const submitters = await this.submitterService.fetchByFileNumber(entity.fileNumber);
197233
const properties = await this.propertyService.fetchParcels(entity.fileNumber);
198-
234+
199235
// Get responsible parties with directors
200236
const responsibleParties = await this.repository.manager.find(
201237
(await import('./responsible-parties/responsible-party.entity')).ComplianceAndEnforcementResponsibleParty,
202-
{
203-
where: { fileUuid: uuid },
204-
relations: ['directors']
205-
}
238+
{
239+
where: { fileUuid: uuid },
240+
relations: ['directors'],
241+
},
206242
);
207243

208244
// Validate the submission
@@ -215,7 +251,7 @@ export class ComplianceAndEnforcementService {
215251

216252
if (validationResult.errors.length > 0) {
217253
// Create a detailed error message
218-
const errorMessages = validationResult.errors.map(error => error.message).join('; ');
254+
const errorMessages = validationResult.errors.map((error) => error.message).join('; ');
219255
throw new ServiceValidationException(`Validation failed: ${errorMessages}`);
220256
}
221257

0 commit comments

Comments
 (0)