From 595b68b7307dae8125c3bbb68b1111ecd51f9074 Mon Sep 17 00:00:00 2001 From: Lucas Li <35748253+yzlucas@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:18:50 -0700 Subject: [PATCH 1/2] Handle Impact of Project Type Change on Evaluation Criteria Scores (#985) --- .../confirmation-dialog.component.ts | 2 +- .../edit-project.component.spec.ts | 6 +- .../edit-project/edit-project.component.ts | 6 + .../project-details.component.html | 5 +- .../project-details.component.spec.ts | 145 +++++++++++++++++- .../project-details.component.ts | 89 ++++++++++- .../project-fiscals.component.ts | 7 +- .../src/app/services/project-services.spec.ts | 33 ++++ .../src/app/services/project-services.ts | 19 +++ .../main/angular/src/app/utils/constants.ts | 4 +- 10 files changed, 303 insertions(+), 13 deletions(-) diff --git a/client/wfprev-war/src/main/angular/src/app/components/confirmation-dialog/confirmation-dialog.component.ts b/client/wfprev-war/src/main/angular/src/app/components/confirmation-dialog/confirmation-dialog.component.ts index 34fec8772..658a2d282 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/confirmation-dialog/confirmation-dialog.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/confirmation-dialog/confirmation-dialog.component.ts @@ -34,7 +34,7 @@ export class ConfirmationDialogComponent { } get isDeleteDialog(): boolean { - return this.dialogUsage.startsWith('delete-'); + return this.dialogUsage.startsWith('delete-') || this.dialogUsage.startsWith('change-'); } constructor( diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.spec.ts b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.spec.ts index 52bf63936..80c01dd89 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.spec.ts @@ -196,7 +196,8 @@ describe('EditProjectComponent', () => { const mockInstance = { focusedFiscalId: null, loadProjectFiscals: jasmine.createSpy('loadProjectFiscals'), - getFirstFiscalGuid: jasmine.createSpy('getFirstFiscalGuid').and.returnValue('guid-123') + getFirstFiscalGuid: jasmine.createSpy('getFirstFiscalGuid').and.returnValue('guid-123'), + fiscalsUpdated: { subscribe: jasmine.createSpy('subscribe') } }; const mockComponentRef = { instance: mockInstance } as any; @@ -485,7 +486,8 @@ describe('EditProjectComponent (extended coverage)', () => { it('should initialize ProjectFiscalsComponent in loadFiscalComponent', async () => { const mockInstance = { focusedFiscalId: null, - loadProjectFiscals: jasmine.createSpy('loadProjectFiscals') + loadProjectFiscals: jasmine.createSpy('loadProjectFiscals'), + fiscalsUpdated: { subscribe: jasmine.createSpy('subscribe') } }; const mockComponentRef = { instance: mockInstance }; diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.ts b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.ts index ca39b2dc1..65fe1cc6b 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.ts @@ -142,6 +142,9 @@ export class EditProjectComponent implements CanComponentDeactivate, OnInit { this.projectFiscalsComponentRef = this.fiscalsContainer.createComponent(ProjectFiscalsComponent); this.projectFiscalsComponentRef.instance.focusedFiscalId = this.focusedFiscalId; this.projectFiscalsComponentRef.instance.loadProjectFiscals(); + this.projectFiscalsComponentRef.instance.fiscalsUpdated.subscribe(() => { + this.projectDetailsComponent.reloadFiscals(); + }); } ); } else if (event.index === 0) { @@ -186,6 +189,9 @@ export class EditProjectComponent implements CanComponentDeactivate, OnInit { this.projectFiscalsComponentRef = this.fiscalsContainer.createComponent(ProjectFiscalsComponent); this.projectFiscalsComponentRef.instance.focusedFiscalId = this.focusedFiscalId; this.projectFiscalsComponentRef.instance.loadProjectFiscals(); + this.projectFiscalsComponentRef.instance.fiscalsUpdated.subscribe(() => { + this.projectDetailsComponent.reloadFiscals(); + }); }); } return Promise.resolve(); diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.html b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.html index f16cf8eff..9f70785f6 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.html +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.html @@ -24,7 +24,10 @@ [options]="projectTypeCode" [optionValueField]="'projectTypeCode'" [optionLabelField]="'description'" - [tooltip]="getCodeDescription(CodeTableKeys.PROJECT_TYPE_CODE)" + [tooltip]="projectTypeLocked + ? 'Project Type cannot be changed after any Fiscal Activity has been Endorsed and Approved' + : getCodeDescription(CodeTableKeys.PROJECT_TYPE_CODE)" + [disabled]="projectTypeLocked" id="projectTypeCode" placeholder="Select" /> diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.spec.ts b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.spec.ts index 6acdd37be..2d6e04bba 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.spec.ts @@ -14,6 +14,7 @@ import { ActivatedRoute } from '@angular/router'; import { formatLatLong } from 'src/app/utils/tools'; import { CodeTableKeys } from 'src/app/utils/constants'; import * as toolUtils from 'src/app/utils/tools' +import { EvaluationCriteriaSummaryModel, ProjectFiscal } from 'src/app/components/models'; const mockApplicationConfig = { application: { baseUrl: 'http://test.com', @@ -76,7 +77,8 @@ describe('ProjectDetailsComponent', () => { 'getProjectByProjectGuid', 'getProjectFiscalsByProjectGuid', 'getFiscalActivities', - 'getActivityBoundaries' + 'getActivityBoundaries', + 'deleteEvaluationCriteriaSummary' ]); mockSnackbar = jasmine.createSpyObj('MatSnackBar', ['open']); @@ -1184,4 +1186,145 @@ describe('ProjectDetailsComponent', () => { }); }); + describe('onSave projectType change confirmation', () => { + let dialogSpy: jasmine.SpyObj; + + beforeEach(() => { + dialogSpy = jasmine.createSpyObj('MatDialog', ['open']); + component['dialog'] = dialogSpy; + + component.originalFormValues = { projectTypeCode: 'OLD_TYPE' }; + component.detailsForm.patchValue({ projectTypeCode: 'NEW_TYPE' }); + + component.evaluationCriteriaComponent = { + evaluationCriteriaSummary: { + evaluationCriteriaSummaryGuid: 'guid-123' + } + } as any; + + mockProjectService.deleteEvaluationCriteriaSummary.and.returnValue(of({})); + }); + + it('should open confirmation dialog when projectType changes and evaluationCriteriaSummary exists', fakeAsync(() => { + const afterClosedSpy = jasmine.createSpy().and.returnValue(of(true)); + dialogSpy.open.and.returnValue({ afterClosed: afterClosedSpy }); + + spyOn(component, 'onSave').and.callThrough(); + + component.onSave(); + tick(); + + expect(dialogSpy.open).toHaveBeenCalledWith(jasmine.any(Function), jasmine.objectContaining({ + data: jasmine.objectContaining({ indicator: 'change-project-type' }) + })); + expect(afterClosedSpy).toHaveBeenCalled(); + expect(mockProjectService.deleteEvaluationCriteriaSummary) + .toHaveBeenCalledWith(component.projectGuid, 'guid-123'); + expect(component.onSave).toHaveBeenCalledTimes(2); // recursive call after delete + })); + + it('should log warning and skip delete if summaryGuid is missing', fakeAsync(() => { + component.evaluationCriteriaComponent = { + evaluationCriteriaSummary: {} as EvaluationCriteriaSummaryModel + } as any; + const afterClosedSpy = jasmine.createSpy().and.returnValue(of(true)); + dialogSpy.open.and.returnValue({ afterClosed: afterClosedSpy }); + + const consoleSpy = spyOn(console, 'warn'); + + component.onSave(); + tick(); + + expect(consoleSpy).toHaveBeenCalledWith('No evaluationCriteriaSummaryGuid found, skipping delete.'); + expect(mockProjectService.deleteEvaluationCriteriaSummary).not.toHaveBeenCalled(); + })); + + it('should log when user cancels the dialog', fakeAsync(() => { + const afterClosedSpy = jasmine.createSpy().and.returnValue(of(false)); + dialogSpy.open.and.returnValue({ afterClosed: afterClosedSpy }); + + const consoleSpy = spyOn(console, 'log'); + + component.onSave(); + tick(); + + expect(consoleSpy).toHaveBeenCalledWith('User canceled project type change, save aborted.'); + expect(mockProjectService.deleteEvaluationCriteriaSummary).not.toHaveBeenCalled(); + })); + + it('should log error if deleteEvaluationCriteriaSummary fails', fakeAsync(() => { + const afterClosedSpy = jasmine.createSpy().and.returnValue(of(true)); + dialogSpy.open.and.returnValue({ afterClosed: afterClosedSpy }); + mockProjectService.deleteEvaluationCriteriaSummary.and.returnValue( + throwError(() => new Error('delete failed')) + ); + + const consoleErrorSpy = spyOn(console, 'error'); + + component.onSave(); + tick(); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to delete evaluation criteria summary', + jasmine.any(Error) + ); + })); + }); + + + describe('hasApprovedFiscals Method', () => { + it('should return true if any fiscal has a locked status', () => { + const fiscals = [ + { planFiscalStatusCode: { planFiscalStatusCode: 'PREPARED' } }, + { planFiscalStatusCode: { planFiscalStatusCode: 'DRAFT' } } + ] as ProjectFiscal[]; + + expect(component.hasApprovedFiscals(fiscals)).toBeTrue(); + }); + + it('should return false if no fiscals have a locked status', () => { + const fiscals = [ + { planFiscalStatusCode: { planFiscalStatusCode: 'DRAFT' } }, + { planFiscalStatusCode: { planFiscalStatusCode: '' } } + ] as ProjectFiscal[]; + + expect(component.hasApprovedFiscals(fiscals)).toBeFalse(); + }); + + }); + + describe('reloadFiscals Method', () => { + it('should call getProjectFiscalsByProjectGuid and pass response to handleFiscalsResponse', () => { + const mockData = { _embedded: { projectFiscals: [] } }; + spyOn(component as any, 'handleFiscalsResponse'); + mockProjectService.getProjectFiscalsByProjectGuid.and.returnValue(of(mockData)); + + component.projectGuid = 'test-guid'; + component.reloadFiscals(); + + expect(mockProjectService.getProjectFiscalsByProjectGuid).toHaveBeenCalledWith('test-guid'); + expect((component as any).handleFiscalsResponse).toHaveBeenCalledWith(mockData); + }); + + it('should not call service if projectGuid is empty', () => { + component.projectGuid = ''; + component.reloadFiscals(); + + expect(mockProjectService.getProjectFiscalsByProjectGuid).not.toHaveBeenCalled(); + }); + + it('should log error when service call fails', () => { + spyOn(console, 'error'); + mockProjectService.getProjectFiscalsByProjectGuid.and.returnValue( + throwError(() => new Error('Service error')) + ); + + component.projectGuid = 'test-guid'; + component.reloadFiscals(); + + expect(console.error).toHaveBeenCalledWith('Error reloading fiscals:', jasmine.any(Error)); + }); + }); + + }); diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.ts b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.ts index 6da322c84..80dc5eb64 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.ts @@ -1,6 +1,6 @@ import { TextFieldModule } from '@angular/cdk/text-field'; import { CommonModule } from '@angular/common'; -import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { MatExpansionModule } from '@angular/material/expansion'; @@ -14,7 +14,7 @@ import { FiscalYearProjectsComponent } from 'src/app/components/edit-project/pro import { ProjectFilesComponent } from 'src/app/components/edit-project/project-details/project-files/project-files.component'; import { CodeTableServices } from 'src/app/services/code-table-services'; import { ProjectService } from 'src/app/services/project-services'; -import { CodeTableKeys, Messages, FiscalYearColors, ModalTitles, ModalMessages, WildfireOrgUnitTypeCodes, CodeTableNames } from 'src/app/utils/constants'; +import { CodeTableKeys, Messages, FiscalYearColors, ModalTitles, ModalMessages, WildfireOrgUnitTypeCodes, CodeTableNames, FiscalStatuses } from 'src/app/utils/constants'; import { formatLatLong, getBluePinIcon, @@ -24,7 +24,7 @@ import { getUtcIsoTimestamp } from 'src/app/utils/tools'; import { ExpansionIndicatorComponent } from '../../shared/expansion-indicator/expansion-indicator.component'; -import { BcParksSectionCodeModel, ForestDistrictCodeModel } from 'src/app/components/models'; +import { BcParksSectionCodeModel, ForestDistrictCodeModel, ProjectFiscal } from 'src/app/components/models'; import { SelectFieldComponent } from 'src/app/components/shared/select-field/select-field.component'; import { InputFieldComponent } from 'src/app/components/shared/input-field/input-field.component'; import { EvaluationCriteriaComponent } from 'src/app/components/edit-project/project-details/evaluation-criteria/evaluation-criteria.component'; @@ -42,8 +42,9 @@ import { TextareaComponent } from 'src/app/components/shared/textarea/textarea.c }) export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild(FiscalYearProjectsComponent) fiscalYearProjectsComponent!: FiscalYearProjectsComponent; - @Output() projectNameChange = new EventEmitter(); + @ViewChild(EvaluationCriteriaComponent) evaluationCriteriaComponent!: EvaluationCriteriaComponent; @ViewChild('mapHost') mapHost!: ElementRef; + @Output() projectNameChange = new EventEmitter(); private map: L.Map | undefined; private readonly activityBoundaryGroup: L.LayerGroup = L.layerGroup(); @@ -80,6 +81,7 @@ export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy allActivities: any[] = []; allActivityBoundaries: any[] = []; readonly PROJECT_DESC_MAX = 4000; + projectTypeLocked = false; constructor( private readonly fb: FormBuilder, @@ -88,7 +90,8 @@ export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy private readonly codeTableService: CodeTableServices, public snackbarService: MatSnackBar, public dialog: MatDialog, - public tokenService: TokenService, + public tokenService: TokenService, + public cd: ChangeDetectorRef ) { } ngOnInit(): void { @@ -391,6 +394,52 @@ export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy } onSave(): void { + const projectTypeOriginalCode = this.originalFormValues.projectTypeCode?.projectTypeCode ?? this.originalFormValues.projectTypeCode; + const projectTypeCurrentCode = this.detailsForm.get('projectTypeCode')?.value?.projectTypeCode ?? this.detailsForm.get('projectTypeCode')?.value; + if ((projectTypeOriginalCode !== projectTypeCurrentCode) && + this.evaluationCriteriaComponent?.evaluationCriteriaSummary) { + // evaluation criteria attached to this project + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + indicator: 'change-project-type', + title: ModalTitles.CHANGE_PROJECT_TYPE, + message: ModalMessages.CONFIRM_DELETE_EVALUACTION_CRITERIA + }, + width: '600px', + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + const summaryGuid = + this.evaluationCriteriaComponent?.evaluationCriteriaSummary?.evaluationCriteriaSummaryGuid; + + if (!summaryGuid) { + console.warn('No evaluationCriteriaSummaryGuid found, skipping delete.'); + return; + } + + this.projectService.deleteEvaluationCriteriaSummary(this.projectGuid, summaryGuid) + .subscribe({ + next: () => { + console.log('Evaluation criteria deleted successfully'); + if (this.evaluationCriteriaComponent) { + this.evaluationCriteriaComponent.evaluationCriteriaSummary = null; + } + this.onSave(); + }, + error: (err) => { + console.error('Failed to delete evaluation criteria summary', err); + } + }); + } else { + console.log('User canceled project type change, save aborted.'); + } + }); + + return; + } + + if (this.detailsForm.valid) { const updatedProject = { ...this.projectDetail, @@ -410,6 +459,7 @@ export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy : this.projectDetail.primaryObjectiveTypeCode?.objectiveTypeCode }, }; + const secondaryObjectiveValue = this.detailsForm.get('secondaryObjectiveTypeCode')?.value; updatedProject.secondaryObjectiveTypeCode = secondaryObjectiveValue ? { objectiveTypeCode: secondaryObjectiveValue } @@ -627,7 +677,6 @@ export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy getAllActivitiesBoundaries(): void { if (!this.projectGuid) return; - this.projectService.getProjectFiscalsByProjectGuid(this.projectGuid).subscribe(data => this.handleFiscalsResponse(data) ); @@ -638,6 +687,16 @@ export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy (a: { fiscalYear: number }, b: { fiscalYear: number }) => a.fiscalYear - b.fiscalYear ); + this.projectTypeLocked = this.hasApprovedFiscals(this.projectFiscals); + const projectTypeControl = this.detailsForm.get('projectTypeCode'); + if (this.projectTypeLocked) { + projectTypeControl?.disable({ emitEvent: false }); + } else { + projectTypeControl?.enable({ emitEvent: false }); + } + + this.cd.detectChanges(); + const activityRequests = this.projectFiscals.map(fiscal => this.projectService.getFiscalActivities(this.projectGuid, fiscal.projectPlanFiscalGuid).pipe( map(response => this.mapFiscalActivities(response, fiscal)) @@ -649,6 +708,7 @@ export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy ); } + private mapFiscalActivities(response: any, fiscal: any): any[] { const activities = response?._embedded?.activities ?? []; return activities.map((activity: any) => ({ @@ -841,4 +901,21 @@ export class ProjectDetailsComponent implements OnInit, AfterViewInit, OnDestroy }); } + hasApprovedFiscals(fiscals: ProjectFiscal[]): boolean { + const LOCKED_STATUSES = ['PREPARED', 'IN_PROGRESS', 'COMPLETE', 'CANCELLED', 'IN_PROG', 'ACTIVE']; + // include all statuses that should lock type change + return fiscals?.some(fiscal => + LOCKED_STATUSES.includes(fiscal.planFiscalStatusCode?.planFiscalStatusCode ?? '') + ); + } + + reloadFiscals(): void { + if (!this.projectGuid) return; + this.projectService.getProjectFiscalsByProjectGuid(this.projectGuid).subscribe({ + next: (data) => { + this.handleFiscalsResponse(data); + }, + error: (err) => console.error('Error reloading fiscals:', err) + }); + } } diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-fiscals/project-fiscals.component.ts b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-fiscals/project-fiscals.component.ts index 8d92ea77a..1745e6a3d 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-fiscals/project-fiscals.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-fiscals/project-fiscals.component.ts @@ -1,5 +1,5 @@ import { CommonModule, CurrencyPipe } from '@angular/common'; -import { ChangeDetectorRef, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; @@ -64,6 +64,7 @@ export class ProjectFiscalsComponent implements OnInit, CanComponentDeactivate { @Input() focusedFiscalId: string | null = null; @ViewChild(ActivitiesComponent) activitiesComponent!: ActivitiesComponent; @ViewChild('fiscalMapRef') fiscalMapComponent!: FiscalMapComponent; + @Output() fiscalsUpdated = new EventEmitter(); currentUser: string = ''; currentIdir: string = ''; projectGuid = ''; @@ -451,6 +452,7 @@ export class ProjectFiscalsComponent implements OnInit, CanComponentDeactivate { { duration: 5000, panelClass: 'snackbar-success' }, ); this.loadProjectFiscals(true); + this.fiscalsUpdated.emit(); }, error: () => { this.snackbarService.open( @@ -527,6 +529,7 @@ export class ProjectFiscalsComponent implements OnInit, CanComponentDeactivate { { duration: 5000, panelClass: 'snackbar-success' } ); this.loadProjectFiscals(true); + this.fiscalsUpdated.emit(); }, error: () => { this.snackbarService.open( @@ -711,6 +714,7 @@ export class ProjectFiscalsComponent implements OnInit, CanComponentDeactivate { } else { this.showSnackbar(this.messages.projectFiscalUpdatedSuccess); this.loadProjectFiscals(true); + this.fiscalsUpdated.emit(); } } @@ -731,6 +735,7 @@ export class ProjectFiscalsComponent implements OnInit, CanComponentDeactivate { next: () => { this.showSnackbar(this.messages.projectFiscalUpdatedSuccess); this.loadProjectFiscals(true); + this.fiscalsUpdated.emit(); }, error: () => this.showSnackbar(this.messages.projectFiscalUpdatedFailure, false) }); diff --git a/client/wfprev-war/src/main/angular/src/app/services/project-services.spec.ts b/client/wfprev-war/src/main/angular/src/app/services/project-services.spec.ts index e18a23837..f564f9ee6 100644 --- a/client/wfprev-war/src/main/angular/src/app/services/project-services.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/services/project-services.spec.ts @@ -937,4 +937,37 @@ describe('ProjectService', () => { req.flush(errorBlob, { status: 500, statusText: 'Server Error' }); }); + it('should delete evaluation criteria summary', () => { + const projectGuid = 'project-1'; + const summaryGuid = 'summary-1'; + + service.deleteEvaluationCriteriaSummary(projectGuid, summaryGuid).subscribe((result) => { + expect(result).toBeTruthy(); + }); + + const req = httpMock.expectOne( + `http://mock-api.com/wfprev-api/projects/${projectGuid}/evaluationCriteriaSummary/${summaryGuid}` + ); + expect(req.request.method).toBe('DELETE'); + expect(req.request.headers.get('Authorization')).toBe('Bearer mock-token'); + req.flush({}); + }); + + it('should handle error when deleting evaluation criteria summary', () => { + const projectGuid = 'project-1'; + const summaryGuid = 'summary-1'; + + service.deleteEvaluationCriteriaSummary(projectGuid, summaryGuid).subscribe({ + next: () => fail('Should have failed'), + error: (err) => { + expect(err.message).toBe('Failed to delete evaluation criteria'); + } + }); + + const req = httpMock.expectOne( + `http://mock-api.com/wfprev-api/projects/${projectGuid}/evaluationCriteriaSummary/${summaryGuid}` + ); + req.flush('Error', { status: 500, statusText: 'Server Error' }); + }); + }); diff --git a/client/wfprev-war/src/main/angular/src/app/services/project-services.ts b/client/wfprev-war/src/main/angular/src/app/services/project-services.ts index 83b5b6157..21701dde6 100644 --- a/client/wfprev-war/src/main/angular/src/app/services/project-services.ts +++ b/client/wfprev-war/src/main/angular/src/app/services/project-services.ts @@ -541,6 +541,25 @@ export class ProjectService { ); } + + deleteEvaluationCriteriaSummary(projectGuid: string, EvaluationCriteriaSummaryGuid: string): Observable { + const baseUrl = `${this.appConfigService.getConfig().rest['wfprev']}/wfprev-api/projects`; + const url = `${baseUrl}/${projectGuid}/evaluationCriteriaSummary/${EvaluationCriteriaSummaryGuid}`; + + return this.httpClient.delete(url, { + headers: { + Authorization: `Bearer ${this.tokenService.getOauthToken()}`, + } + }).pipe( + map((response: any) => response), + catchError((error) => { + console.error("Error delete evaluation criteria", error); + return throwError(() => new Error("Failed to delete evaluation criteria")); + }) + ); + } + + downloadProjects(body: ReportRequest): Observable { const url = `${this.appConfigService.getConfig().rest['wfprev']}/wfprev-api/reports`; diff --git a/client/wfprev-war/src/main/angular/src/app/utils/constants.ts b/client/wfprev-war/src/main/angular/src/app/utils/constants.ts index 9a54c4549..38d05b45d 100644 --- a/client/wfprev-war/src/main/angular/src/app/utils/constants.ts +++ b/client/wfprev-war/src/main/angular/src/app/utils/constants.ts @@ -161,7 +161,8 @@ export const ModalTitles = { DELETE_ATTACHMENT_TITLE: 'Delete Attachment', DELETE_FISCAL_YEAR_TITLE: 'Delete Fiscal Activity', DELETE_ACTIVITY_TITLE: 'Delete Activity', - DUPLICATE_FOUND_TITLE: 'Duplicate Found' + DUPLICATE_FOUND_TITLE: 'Duplicate Found', + CHANGE_PROJECT_TYPE: 'Change Project Type' }; export const ModalMessages = { @@ -169,6 +170,7 @@ export const ModalMessages = { DUPLICATE_FOUND_MESSAGE: 'This Project already exists: ', CONFIRM_UNSAVE_MESSAGE: 'Are you sure you want to leave this page? The changes you made will not be saved.', DELETE_ATTACHMENT_MESSAGE: 'Are you sure you want to delete this file? ', + CONFIRM_DELETE_EVALUACTION_CRITERIA: 'Are you sure you want to change the Project Type? This action cannot be reversed and will immediately delete any existing Evaluation Criteria values.' } export const EndorsementCode = { From f048e7f52cc6fbc1e662bd9ad44a65b6cb9c8441 Mon Sep 17 00:00:00 2001 From: Lucas Li <35748253+yzlucas@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:11:50 -0700 Subject: [PATCH 2/2] code review change (#989) --- .../confirmation-dialog/confirmation-dialog.component.html | 6 +++--- .../confirmation-dialog.component.spec.ts | 6 +++--- .../confirmation-dialog/confirmation-dialog.component.ts | 2 +- .../project-details/project-details.component.html | 2 +- .../wfprev-war/src/main/angular/src/app/utils/constants.ts | 3 ++- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/client/wfprev-war/src/main/angular/src/app/components/confirmation-dialog/confirmation-dialog.component.html b/client/wfprev-war/src/main/angular/src/app/components/confirmation-dialog/confirmation-dialog.component.html index a45642437..77c742fd9 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/confirmation-dialog/confirmation-dialog.component.html +++ b/client/wfprev-war/src/main/angular/src/app/components/confirmation-dialog/confirmation-dialog.component.html @@ -1,6 +1,6 @@
- @@ -11,11 +11,11 @@