From 5895792631533da440ac2b1b722bf1437fba869d Mon Sep 17 00:00:00 2001 From: Lucas Li <35748253+yzlucas@users.noreply.github.com> Date: Mon, 6 Jan 2025 09:38:03 -0800 Subject: [PATCH] Api hookup (#395) (#396) --- .../main/angular/src/app/app.component.html | 8 - .../main/angular/src/app/app.component.scss | 27 -- .../angular/src/app/app.component.spec.ts | 113 ++++----- .../src/main/angular/src/app/app.component.ts | 9 - .../create-new-project-dialog.component.html | 10 +- ...reate-new-project-dialog.component.spec.ts | 237 +++++++++++------- .../create-new-project-dialog.component.ts | 146 ++++++++--- .../projects-list.component.html | 31 +-- .../projects-list.component.scss | 28 +++ .../projects-list.component.spec.ts | 231 ++++++++--------- .../projects-list/projects-list.component.ts | 199 +++++++-------- .../src/app/components/list/list.component.ts | 1 - .../src/app/components/map/map.component.scss | 3 +- .../app/components/map/map.component.spec.ts | 112 ++++++--- .../src/app/components/map/map.component.ts | 2 +- .../main/angular/src/app/components/models.ts | 36 +++ .../resizable-panel.component.spec.ts | 124 ++++++--- .../src/app/services/code-table-services.ts | 46 ++++ .../src/app/services/project-services.ts | 50 ++++ .../angular/src/assets/data/appConfig.json | 6 +- .../src/assets/data/appConfig.local.json | 8 +- .../src/assets/data/checktoken-user.json | 118 +++++++++ 22 files changed, 972 insertions(+), 573 deletions(-) create mode 100644 client/wfprev-war/src/main/angular/src/app/components/models.ts create mode 100644 client/wfprev-war/src/main/angular/src/app/services/code-table-services.ts create mode 100644 client/wfprev-war/src/main/angular/src/app/services/project-services.ts create mode 100644 client/wfprev-war/src/main/angular/src/assets/data/checktoken-user.json diff --git a/client/wfprev-war/src/main/angular/src/app/app.component.html b/client/wfprev-war/src/main/angular/src/app/app.component.html index 70451db4f..80c99ab3c 100644 --- a/client/wfprev-war/src/main/angular/src/app/app.component.html +++ b/client/wfprev-war/src/main/angular/src/app/app.component.html @@ -1,13 +1,5 @@
-
- -
@@ -38,28 +38,28 @@
diff --git a/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.spec.ts b/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.spec.ts index ef50a94e4..347718275 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.spec.ts @@ -1,28 +1,54 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { CreateNewProjectDialogComponent } from './create-new-project-dialog.component'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Messages } from 'src/app/utils/messages'; - +import { ProjectService } from 'src/app/services/project-services'; +import { CodeTableServices } from 'src/app/services/code-table-services'; +import { MatSnackBar } from '@angular/material/snack-bar'; describe('CreateNewProjectDialogComponent', () => { let component: CreateNewProjectDialogComponent; let fixture: ComponentFixture; let mockDialog: jasmine.SpyObj; let mockDialogRef: jasmine.SpyObj>; + let mockProjectService: jasmine.SpyObj; + let mockCodeTableService: jasmine.SpyObj; + let mockSnackbarService: jasmine.SpyObj; beforeEach(async () => { mockDialog = jasmine.createSpyObj('MatDialog', ['open']); mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['close']); + mockProjectService = jasmine.createSpyObj('ProjectService', ['createProject']); + mockCodeTableService = jasmine.createSpyObj('CodeTableServices', ['fetchCodeTable']); + mockSnackbarService = jasmine.createSpyObj('MatSnackBar', ['open']); + + mockCodeTableService.fetchCodeTable.and.callFake((name: string) => { + switch (name) { + case 'programAreaCodes': + return of({ _embedded: { programArea: [{ name: 'Program Area 1' }] } }); + case 'forestRegionCodes': + return of({ _embedded: { forestRegion: [{ name: 'Forest Region 1' }] } }); + case 'bcParksRegionCodes': + return of({ _embedded: { bcParksRegionCode: [{ name: 'BC Parks Region 1' }] } }); + case 'bcParksSectionCodes': + return of({ _embedded: { bcParksSectionCode: [{ parentOrgUnitId: 1, name: 'BC Section 1' }] } }); + default: + return of({ _embedded: [] }); + } + }); await TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, CreateNewProjectDialogComponent, BrowserAnimationsModule ], + imports: [ReactiveFormsModule, CreateNewProjectDialogComponent, BrowserAnimationsModule], providers: [ { provide: MatDialog, useValue: mockDialog }, { provide: MatDialogRef, useValue: mockDialogRef }, + { provide: ProjectService, useValue: mockProjectService }, + { provide: CodeTableServices, useValue: mockCodeTableService }, + { provide: MatSnackBar, useValue: mockSnackbarService }, ], }).compileComponents(); @@ -58,139 +84,158 @@ describe('CreateNewProjectDialogComponent', () => { }); it('should enable bcParksSection when a region is selected', () => { - component.projectForm.get('bcParksRegion')?.setValue('Northern'); + component.projectForm.get('bcParksRegion')?.setValue(1); fixture.detectChanges(); - const bcParksSectionControl = component.projectForm.get('bcParksSection'); - expect(bcParksSectionControl?.enabled).toBeTrue(); - expect(component.bcParksSections).toEqual(['Omineca', 'Peace', 'Skeena']); + expect(component.projectForm.get('bcParksSection')?.enabled).toBeTrue(); }); it('should reset and disable bcParksSection when no region is selected', () => { - component.projectForm.get('bcParksRegion')?.setValue(null); + component.projectForm.get('bcParksRegion')?.setValue(null); // Simulate no region selected fixture.detectChanges(); - const bcParksSectionControl = component.projectForm.get('bcParksSection'); - expect(bcParksSectionControl?.disabled).toBeTrue(); + expect(component.projectForm.get('bcParksSection')?.disabled).toBeTrue(); expect(component.bcParksSections).toEqual([]); }); + it('should fetch and set code tables on initialization', () => { + // Mocking the responses for fetchCodeTable + mockCodeTableService.fetchCodeTable.and.callFake((name: string) => { + switch (name) { + case 'programAreaCodes': + return of({ _embedded: { programArea: [{ name: 'Program Area 1' }] } }); + case 'forestRegionCodes': + return of({ _embedded: { forestRegionCode: [{ name: 'Forest Region 1' }] } }); + default: + return of({ _embedded: [] }); + } + }); + + // Trigger the loadCodeTables method + component.loadCodeTables(); + fixture.detectChanges(); + + // Verify that the service was called with correct table names + expect(mockCodeTableService.fetchCodeTable).toHaveBeenCalledWith('programAreaCodes'); + expect(mockCodeTableService.fetchCodeTable).toHaveBeenCalledWith('forestRegionCodes'); + + // Verify the component's state is updated correctly + expect(component.businessAreas).toEqual([{ name: 'Program Area 1' }]); + expect(component.forestRegions).toEqual([{ name: 'Forest Region 1' }]); + }); + + + it('should display correct error messages', () => { + component.projectForm.get('projectName')?.setErrors({ required: true }); + expect(component.getErrorMessage('projectName')).toBe(Messages.requiredField); + + component.projectForm.get('projectLeadEmail')?.setErrors({ email: true }); + expect(component.getErrorMessage('projectLeadEmail')).toBe(Messages.invalidEmail); + }); + + it('should create a new project and close dialog on success', () => { + // Mock createProject to simulate a successful API response + mockProjectService.createProject.and.returnValue(of({})); + + // Populate the form with valid values + component.projectForm.patchValue({ + projectName: 'New Project', // Required field + businessArea: 'Area 1', // Required field + forestRegion: 1, // Required field + forestDistrict: 2, // Required field + bcParksRegion: 3, // Required field + bcParksSection: 4, // Required field + projectLead: 'John Doe', // Optional field + projectLeadEmail: 'john.doe@example.com', // Optional field + siteUnitName: 'Unit 1', // Optional field + closestCommunity: 'Community 1', // Required field + }); + + // Call the function to create a project + component.onCreate(); + + // Assertions + expect(mockProjectService.createProject).toHaveBeenCalled(); // Ensure createProject was called + expect(mockSnackbarService.open).toHaveBeenCalledWith( + Messages.projectCreatedSuccess, + 'OK', + { duration: 100000, panelClass: 'snackbar-success' } + ); // Ensure snackbar was called + expect(mockDialogRef.close).toHaveBeenCalledWith({ success: true }); // Ensure the dialog was closed + }); + + // Future task + // it('should handle duplicate project error during creation', () => { + // mockProjectService.createProject.and.returnValue( + // throwError({ status: 500, error: { message: 'duplicate' } }) + // ); + + // component.onCreate(); + + // expect(mockDialog.open).toHaveBeenCalledWith(ConfirmationDialogComponent, { + // data: { indicator: 'duplicate-project', projectName: '' }, + // width: '500px', + // }); + // }); + it('should open confirmation dialog on cancel', () => { const mockAfterClosed = of(true); - mockDialog.open.and.returnValue({ - afterClosed: () => mockAfterClosed, - } as any); + mockDialog.open.and.returnValue({ afterClosed: () => mockAfterClosed } as any); component.onCancel(); + expect(mockDialog.open).toHaveBeenCalledWith(ConfirmationDialogComponent, { data: { indicator: 'confirm-cancel' }, width: '500px', }); - }); - it('should close the dialog if confirmation dialog returns true', () => { - const mockAfterClosed = of(true); - mockDialog.open.and.returnValue({ - afterClosed: () => mockAfterClosed, - } as any); - - component.onCancel(); - expect(mockDialog.open).toHaveBeenCalled(); mockAfterClosed.subscribe(() => { expect(mockDialogRef.close).toHaveBeenCalled(); }); }); - it('should not close the dialog if confirmation dialog returns false', () => { + it('should not close dialog if cancel confirmation returns false', () => { const mockAfterClosed = of(false); - mockDialog.open.and.returnValue({ - afterClosed: () => mockAfterClosed, - } as any); + mockDialog.open.and.returnValue({ afterClosed: () => mockAfterClosed } as any); component.onCancel(); + expect(mockDialog.open).toHaveBeenCalled(); mockAfterClosed.subscribe(() => { expect(mockDialogRef.close).not.toHaveBeenCalled(); }); }); - it('should close the dialog on successful form submission', () => { - component.projectForm.patchValue({ - projectName: 'Test Project', - latLong: '123.456', - businessArea: 'Area 1', - forestRegion: 'Region 1', - forestDistrict: 'District 1', - bcParksRegion: 'Northern', - bcParksSection: 'Omineca', - projectLead: 'John Doe', - projectLeadEmail: 'john.doe@example.com', - siteUnitName: 'Unit 1', - closestCommunity: 'Community 1', - }); - - component.onCreate(); - expect(mockDialogRef.close).toHaveBeenCalledWith(component.projectForm.value); - }); - - it('should not close the dialog if the form is invalid', () => { - component.projectForm.patchValue({ - projectName: '', - }); - - component.onCreate(); - expect(mockDialogRef.close).not.toHaveBeenCalled(); - }); - it('should return the correct error message for required fields', () => { - component.projectForm.get('projectName')?.setErrors({ required: true }); - const errorMessage = component.getErrorMessage('projectName'); - expect(errorMessage).toBe(Messages.requiredField); - }); + it('should not create a new project if the form is invalid', () => { + component.projectForm.get('projectName')?.setValue(''); // Invalid since it's required - it('should return the correct error message for maxlength errors', () => { - component.projectForm.get('projectName')?.setErrors({ maxlength: true }); - const errorMessage = component.getErrorMessage('projectName'); - expect(errorMessage).toBe(Messages.maxLengthExceeded); - }); + component.onCreate(); - it('should return the correct error message for email format errors', () => { - component.projectForm.get('projectLeadEmail')?.setErrors({ email: true }); - const errorMessage = component.getErrorMessage('projectLeadEmail'); - expect(errorMessage).toBe(Messages.invalidEmail); + expect(mockProjectService.createProject).not.toHaveBeenCalled(); + expect(mockSnackbarService.open).not.toHaveBeenCalled(); + expect(mockDialogRef.close).not.toHaveBeenCalled(); }); + + it('should update bcParksSections when a bcParksRegion is selected', () => { + // Mock data for allBcParksSections + component.allBcParksSections = [ + { parentOrgUnitId: '1', name: 'Section 1' }, + { parentOrgUnitId: '2', name: 'Section 2' }, + ]; - it('should dynamically display error messages in the template', () => { - const projectNameControl = component.projectForm.get('projectName'); - projectNameControl?.setErrors({ maxlength: true }); - projectNameControl?.markAsTouched(); + // Set bcParksRegion value to 1 + component.projectForm.get('bcParksRegion')?.setValue('1'); fixture.detectChanges(); - const errorElement = fixture.nativeElement.querySelector('.form-field .error'); - expect(errorElement.textContent.trim()).toBe(Messages.maxLengthExceeded); - }); + // Check if bcParksSections is updated correctly + expect(component.bcParksSections).toEqual([{ parentOrgUnitId: '1', name: 'Section 1' }]); - it('should show a snackbar message on successful form submission', () => { - const mockSnackbar = spyOn(component['snackbarService'], 'open'); - component.projectForm.patchValue({ - projectName: 'Test Project', - latLong: '123.456', - businessArea: 'Area 1', - forestRegion: 'Region 1', - forestDistrict: 'District 1', - bcParksRegion: 'Northern', - bcParksSection: 'Omineca', - projectLead: 'John Doe', - projectLeadEmail: 'john.doe@example.com', - siteUnitName: 'Unit 1', - closestCommunity: 'Community 1', - }); - - component.onCreate(); + // Set bcParksRegion value to 2 + component.projectForm.get('bcParksRegion')?.setValue('2'); + fixture.detectChanges(); - expect(mockSnackbar).toHaveBeenCalledWith( - Messages.projectCreatedSuccess, - 'OK', - { duration: 100000, panelClass: 'snackbar-success' } - ); + // Check if bcParksSections is updated correctly + expect(component.bcParksSections).toEqual([{ parentOrgUnitId: '2', name: 'Section 2' }]); }); + }); diff --git a/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.ts b/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.ts index 0e9853a9d..71bbd7894 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.ts @@ -1,10 +1,13 @@ import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatDialog , MatDialogRef } from '@angular/material/dialog'; import { ConfirmationDialogComponent } from 'src/app/components/confirmation-dialog/confirmation-dialog.component'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Messages } from 'src/app/utils/messages'; +import { ProjectService } from 'src/app/services/project-services'; +import { CodeTableServices } from 'src/app/services/code-table-services'; +import { Project } from 'src/app/components/models'; @Component({ selector: 'app-create-new-project-dialog', standalone: true, @@ -15,7 +18,8 @@ import { Messages } from 'src/app/utils/messages'; templateUrl: './create-new-project-dialog.component.html', styleUrls: ['./create-new-project-dialog.component.scss'] }) -export class CreateNewProjectDialogComponent { +export class CreateNewProjectDialogComponent implements OnInit { + [key: string]: any; // Add this line to allow dynamic properties projectForm: FormGroup; messages = Messages; // Regions and sections mapping @@ -27,17 +31,20 @@ export class CreateNewProjectDialogComponent { 'West Coast': ['Central Coast/North Island', 'Haida Gwaii/South Island'] }; - businessAreas = ['Area 1', 'Area 2', 'Area 3']; // Example data - forestRegions = ['Region 1', 'Region 2', 'Region 3']; // Example data - forestDistricts = ['District 1', 'District 2', 'District 3']; // Example data - bcParksRegions = Object.keys(this.regionToSections); - bcParksSections: string[] = []; // Dynamically updated based on the selected region + businessAreas : any[] = []; + forestRegions : any[] = []; + forestDistricts : any[] = []; + bcParksRegions : any[] = []; + bcParksSections: any[] = []; + allBcParksSections: any[] = []; // To hold all sections initially constructor( private readonly fb: FormBuilder, private readonly dialog: MatDialog, private readonly dialogRef: MatDialogRef, private readonly snackbarService: MatSnackBar, + private readonly projectService: ProjectService, + private readonly codeTableService: CodeTableServices ) { this.projectForm = this.fb.group({ @@ -55,21 +62,46 @@ export class CreateNewProjectDialogComponent { }); // Dynamically enable/disable bcParksSection based on bcParksRegion selection - this.projectForm.get('bcParksRegion')?.valueChanges.subscribe((region: string | number) => { - if (region) { - this.projectForm.get('bcParksSection')?.enable(); - this.bcParksSections = this.regionToSections[region] || []; - } else { - this.projectForm.get('bcParksSection')?.reset(); - this.projectForm.get('bcParksSection')?.disable(); - this.bcParksSections = []; - } - }); + this.projectForm.get('bcParksRegion')?.valueChanges.subscribe((regionId: number) => { + if (regionId) { + this.projectForm.get('bcParksSection')?.enable(); + this.bcParksSections = this.allBcParksSections.filter( + (section) => section.parentOrgUnitId === regionId.toString() + ); + } else { + this.projectForm.get('bcParksSection')?.reset(); + this.projectForm.get('bcParksSection')?.disable(); + this.bcParksSections = []; + } + }); + } + ngOnInit(): void { + this.loadCodeTables(); // Call the helper method to load code tables } + loadCodeTables(): void { + const codeTables = [ + { name: 'programAreaCodes', property: 'businessAreas', embeddedKey: 'programArea' }, + { name: 'forestRegionCodes', property: 'forestRegions', embeddedKey: 'forestRegionCode' }, + { name: 'forestDistrictCodes', property: 'forestDistricts', embeddedKey: 'forestDistrictCode' }, + { name: 'bcParksRegionCodes', property: 'bcParksRegions', embeddedKey: 'bcParksRegionCode' }, + { name: 'bcParksSectionCodes', property: 'allBcParksSections', embeddedKey: 'bcParksSectionCode' }, + ]; + + codeTables.forEach((table) => { + this.codeTableService.fetchCodeTable(table.name).subscribe({ + next: (data) => { + this[table.property] = data?._embedded?.[table.embeddedKey] || []; + }, + error: (err) => { + console.error(`Error fetching ${table.name}`, err); + }, + }); + }); + } getErrorMessage(controlName: string): string | null { const control = this.projectForm.get(controlName); - if (!control || !control.errors) return null; + if (!control?.errors) return null; if (control.hasError('required')) { return this.messages.requiredField; @@ -86,25 +118,65 @@ export class CreateNewProjectDialogComponent { onCreate(): void { if (this.projectForm.valid) { - console.log(this.projectForm.value); - //call POST endpoint, - // if return 500 error with duplicate project name error message, - - // this.dialog.open(ConfirmationDialogComponent, { - // data: { - // indicator: 'duplicate-project', - // projectName: '', - // }, - // width: '500px', - // }); - - //OK will return the user to the Modal and allow further editing. just close the Modal for now - this.snackbarService.open( - this.messages.projectCreatedSuccess, - 'OK', - { duration: 100000, panelClass: 'snackbar-success' }, - ) - this.dialogRef.close(this.projectForm.value); + const newProject: Project = { + projectName: this.projectForm.get('projectName')?.value ?? '', + programAreaGuid: this.projectForm.get('businessArea')?.value ?? '', + forestRegionOrgUnitId: Number(this.projectForm.get('forestRegion')?.value) || 0, + forestDistrictOrgUnitId: Number(this.projectForm.get('forestDistrict')?.value) || 0, + bcParksRegionOrgUnitId: Number(this.projectForm.get('bcParksRegion')?.value) || 0, + bcParksSectionOrgUnitId: Number(this.projectForm.get('bcParksSection')?.value) || 0, + projectLead: this.projectForm.get('projectLead')?.value ?? '', + projectLeadEmailAddress: this.projectForm.get('projectLeadEmail')?.value ?? '', + siteUnitName: this.projectForm.get('siteUnitName')?.value ?? '', + closestCommunityName: this.projectForm.get('closestCommunity')?.value ?? '', + fireCentreOrgUnitId: this.projectForm.get('fireCentre')?.value ?? 0, + generalScopeCode: { + generalScopeCode: "SL_ACT" + }, + projectTypeCode: { + projectTypeCode: "FUEL_MGMT" + }, + projectDescription: this.projectForm.get('projectDescription')?.value ?? '', + projectNumber: this.projectForm.get('projectNumber')?.value ?? '', + totalFundingRequestAmount: + this.projectForm.get('totalFundingRequestAmount')?.value ?? '', + totalAllocatedAmount: this.projectForm.get('totalAllocatedAmount')?.value ?? '', + totalPlannedProjectSizeHa: + this.projectForm.get('totalPlannedProjectSizeHa')?.value ?? '', + totalPlannedCostPerHectare: + this.projectForm.get('totalPlannedCostPerHectare')?.value ?? '', + totalActualAmount: this.projectForm.get('totalActualAmount')?.value ?? 0, + isMultiFiscalYearProj: false, + }; + + this.projectService.createProject(newProject).subscribe({ + next: (response) => { + this.snackbarService.open( + this.messages.projectCreatedSuccess, + 'OK', + { duration: 100000, panelClass: 'snackbar-success' }, + ); + this.dialogRef.close({ success: true }); + }, + error: (err) =>{ + if (err.status === 500 && err.error.message.includes('duplicate')) { + this.dialog.open(ConfirmationDialogComponent, { + data: { + indicator: 'duplicate-project', + projectName: '', + }, + width: '500px', + }); + } + else{ + this.snackbarService.open( + "Create project failed", + 'OK', + { duration: 5000, panelClass: 'snackbar-error' } + ); + } + } + }) } } diff --git a/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.html b/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.html index 28fb68bdc..fa11d2e33 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.html +++ b/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.html @@ -7,23 +7,18 @@ - - - -
-
- -
-
- - Off/On - +
+ + {{ projectList?.length }} Results
- - -
- {{ resultCount }} Results +
+
@@ -63,15 +58,15 @@
Region - {{ project.forestRegionOrgUnitId }} + {{ getDescription('forestRegionCode',project.forestRegionOrgUnitId) }}
- Total Project Hectares + Total Hectares {{ project.totalPlannedProjectSizeHa }} Ha
Business Area - {{ project.bcParksRegionOrgUnitId }} + {{ getDescription('programAreaCode',project.programAreaGuid) }}
Business Area Lead diff --git a/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.scss b/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.scss index 2ce49588a..b73627c7e 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.scss +++ b/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.scss @@ -6,6 +6,8 @@ border-bottom: 1px solid #ccc; font-family: var(--wf-font-family-main); font-size: 14px; + gap: 12px; + position: relative; } .sort-dropdown { @@ -112,6 +114,7 @@ } .list-contents { + height: calc(100vh - 330px); mat-expansion-panel { border-bottom: 1px solid #C6C8CB; margin: 0px !important; @@ -245,4 +248,29 @@ .custom-indicator{ position: absolute; left: 10px; +} + +.create-project-button-container{ + .create-project-button{ + cursor: pointer; + display: inline-flex; + padding: 10px 16px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 8px; + background: #013366; + .button-text{ + color: #FFF; + font-family: "BCSans", "Noto Sans", Verdana, Arial, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; + .icon{ + vertical-align: text-bottom; + padding-right: 5px; + } + } + } } \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.spec.ts b/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.spec.ts index 5beb8ecbb..ed2d7b667 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.spec.ts @@ -6,18 +6,52 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { ActivatedRoute, Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { ProjectService } from 'src/app/services/project-services'; +import { CodeTableServices } from 'src/app/services/code-table-services'; import { ResourcesRoutes } from 'src/app/utils'; +import { of, throwError } from 'rxjs'; describe('ProjectsListComponent', () => { let component: ProjectsListComponent; let fixture: ComponentFixture; let debugElement: DebugElement; + let mockProjectService = { + fetchProjects: jasmine.createSpy('fetchProjects').and.returnValue(of({ + _embedded: { + project: [ + { projectNumber: 1, projectName: 'Project 1', forestRegionOrgUnitId: 101, totalPlannedProjectSizeHa: 100 }, + { projectNumber: 2, projectName: 'Project 2', forestRegionOrgUnitId: 102, totalPlannedProjectSizeHa: 200 }, + ], + }, + })), + }; + + let mockCodeTableService = { + fetchCodeTable: jasmine.createSpy('fetchCodeTable').and.callFake((name: 'programAreaCodes' | 'forestRegionCodes') => { + const mockData = { + programAreaCodes: { _embedded: { programArea: [{ programAreaGuid: 'guid1', programAreaName: 'Area 1' }] } }, + forestRegionCodes: { _embedded: { forestRegionCode: [{ orgUnitId: 101, orgUnitName: 'Region 1' }] } }, + }; + return of(mockData[name]); + }), + }; + + let mockDialog = { + open: jasmine.createSpy('open').and.returnValue({ + afterClosed: () => of({ success: true }), + }), + }; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ProjectsListComponent, BrowserAnimationsModule, MatExpansionModule, MatSlideToggleModule], // Using standalone component + imports: [ProjectsListComponent, BrowserAnimationsModule, MatExpansionModule, MatSlideToggleModule], providers: [ - { provide: ActivatedRoute, useValue: ActivatedRoute }, // Provide mock for ActivatedRoute + { provide: ProjectService, useValue: mockProjectService }, + { provide: CodeTableServices, useValue: mockCodeTableService }, + { provide: MatDialog, useValue: mockDialog }, + { provide: ActivatedRoute, useValue: ActivatedRoute }, ], }).compileComponents(); @@ -32,165 +66,100 @@ describe('ProjectsListComponent', () => { }); it('should render the correct number of projects', () => { - const projectItems = debugElement.queryAll(By.css('.project-name')); - expect(projectItems.length).toBe(component.projectList.length); - expect(projectItems[0].nativeElement.textContent).toContain( - component.projectList[0].projectName - ); - }); - - it('should display fiscalYearActivityTypes correctly', () => { - const activityTypes = debugElement.queryAll(By.css('.activity-type')); - const expectedCount = component.fiscalYearActivityTypes.length * component.projectList.length; - expect(activityTypes.length).toBe(expectedCount); - }); - - it('should handle sort change correctly', () => { - spyOn(component, 'onSortChange'); - const select = debugElement.query(By.css('select')).nativeElement; - select.value = component.sortOptions[1].value; // Change to second option - select.dispatchEvent(new Event('change')); - fixture.detectChanges(); - - expect(component.onSortChange).toHaveBeenCalled(); - expect(select.value).toBe('descending'); - }); - - it('should toggle syncWithMap when the toggle is clicked', () => { - spyOn(component, 'onSyncMapToggleChange'); // Spy on the method - const toggleDebugElement = debugElement.query(By.css('mat-slide-toggle')); - const toggle = toggleDebugElement.componentInstance; // Access MatSlideToggle instance + // Mock the project data to simulate a successful API response + component.projectList = [ + { projectNumber: 1, projectName: 'Project 1' }, + { projectNumber: 2, projectName: 'Project 2' }, + ]; - // Simulate the toggle event - toggle.change.emit({ checked: true }); // Emit the change event + // Trigger change detection to update the DOM fixture.detectChanges(); - expect(component.onSyncMapToggleChange).toHaveBeenCalled(); // Assert the method was called + // Query DOM elements + const projectItems = fixture.debugElement.queryAll(By.css('.project-name')); + + // Assertions + expect(projectItems.length).toBe(2); // Mock data contains 2 projects + expect(projectItems[0].nativeElement.textContent).toContain('Project 1'); + expect(projectItems[1].nativeElement.textContent).toContain('Project 2'); }); - - it('should display the correct data in project details', () => { - const firstProject = component.projectList[0]; - const detailElements = debugElement.queryAll(By.css('.detail span')); - expect(detailElements[0].nativeElement.textContent).toContain('Region'); - expect(detailElements[1].nativeElement.textContent).toContain( - firstProject.forestRegionOrgUnitId - ); - expect(detailElements[3].nativeElement.textContent).toContain( - firstProject.totalPlannedProjectSizeHa.toString() - ); + + it('should load code tables on init', () => { + expect(mockCodeTableService.fetchCodeTable).toHaveBeenCalledWith('programAreaCodes'); + expect(mockCodeTableService.fetchCodeTable).toHaveBeenCalledWith('forestRegionCodes'); }); - - it('should expand and collapse the expansion panel', () => { - const panelDebugElement = debugElement.query(By.css('mat-expansion-panel')); - const panel = panelDebugElement.componentInstance as any; - panel.open(); + it('should handle errors when loading code tables', () => { + mockCodeTableService.fetchCodeTable.and.returnValue(throwError('Error fetching data')); + component.loadCodeTables(); fixture.detectChanges(); - expect(panel.expanded).toBeTrue(); - panel.close(); - fixture.detectChanges(); - expect(panel.expanded).toBeFalse(); + expect(component.programAreaCode).toEqual([]); + expect(component.forestRegionCode).toEqual([]); }); - - it('should render all sort options correctly', () => { - const options = debugElement.queryAll(By.css('select option')); - expect(options.length).toBe(component.sortOptions.length + 1); // +1 for the default option - expect(options[1].nativeElement.textContent).toContain('Name (A-Z)'); - expect(options[2].nativeElement.textContent).toContain('Name (Z-A)'); - }); - - it('should initialize with default values', () => { - expect(component.selectedSort).toBe(''); - expect(component.syncWithMap).toBeFalse(); - expect(component.resultCount).toBe(3); - expect(component.projectList.length).toBe(3); - expect(component.fiscalYearActivityTypes.length).toBe(3); - }); - - it('should update selectedSort when onSortChange is called', () => { - const mockEvent = { target: { value: 'ascending' } }; - component.onSortChange(mockEvent); - expect(component.selectedSort).toBe('ascending'); + it('should handle errors when loading projects', () => { + mockProjectService.fetchProjects.and.returnValue(throwError('Error fetching projects')); + component.loadProjects(); + fixture.detectChanges(); + expect(component.projectList).toEqual([]); }); - - it('should update syncWithMap when onSyncMapToggleChange is called', () => { - component.onSyncMapToggleChange({ checked: true }); - expect(component.syncWithMap).toBeTrue(); - component.onSyncMapToggleChange({ checked: false }); - expect(component.syncWithMap).toBeFalse(); - }); - it('should display the correct number of results', () => { - const resultCountElement = debugElement.query(By.css('.result-count span')).nativeElement; - expect(resultCountElement.textContent).toContain(`${component.resultCount} Results`); - }); - - it('should display project names correctly', () => { - const projectNames = debugElement.queryAll(By.css('.project-name')).map(el => el.nativeElement.textContent.trim()); - const expectedNames = component.projectList.map(project => project.projectName); - expect(projectNames).toEqual(expectedNames); + it('should open the dialog to create a new project and reload projects if successful', () => { + spyOn(component, 'loadProjects'); + component.createNewProject(); + expect(mockDialog.open).toHaveBeenCalledWith(jasmine.any(Function), jasmine.objectContaining({ + width: '880px', + disableClose: true, + hasBackdrop: true, + })); + expect(component.loadProjects).toHaveBeenCalled(); }); - it('should display the correct project details when expanded', () => { - const panelDebugElement = debugElement.query(By.css('mat-expansion-panel')); - const panelInstance = panelDebugElement.componentInstance; - - panelInstance.open(); // Expand the panel + it('should return the correct description from code tables', () => { + component.loadCodeTables(); // Load the mock code tables fixture.detectChanges(); + const description = 'Region 1'; + expect(description).toBe('Region 1'); - const detailElements = debugElement.queryAll(By.css('.project-header-details .detail span')); - const firstProject = component.projectList[0]; - - expect(detailElements[0].nativeElement.textContent).toContain('Region'); - expect(detailElements[1].nativeElement.textContent).toContain(firstProject.forestRegionOrgUnitId.toString()); - expect(detailElements[3].nativeElement.textContent).toContain(firstProject.totalPlannedProjectSizeHa.toString()); + const unknownDescription = component.getDescription('forestRegionCode', 999); + expect(unknownDescription).toBe('Unknown'); }); - - it('should handle empty projectList gracefully', () => { - component.projectList = []; - fixture.detectChanges(); - const projectItems = debugElement.queryAll(By.css('.project-name')); - expect(projectItems.length).toBe(0); - }); + + it('should handle sort change correctly', () => { + spyOn(component, 'onSortChange').and.callThrough(); - it('should handle empty fiscalYearActivityTypes gracefully', () => { - component.fiscalYearActivityTypes = []; + const select = debugElement.query(By.css('select')).nativeElement; + select.value = 'ascending'; // Change to "ascending" + select.dispatchEvent(new Event('change')); fixture.detectChanges(); - const activityTypes = debugElement.queryAll(By.css('.activity-type')); - expect(activityTypes.length).toBe(0); + expect(component.onSortChange).toHaveBeenCalled(); + expect(component.selectedSort).toBe('ascending'); }); - it('should display all sort options dynamically', () => { - const sortOptions = debugElement.queryAll(By.css('select option')); - expect(sortOptions.length).toBe(component.sortOptions.length + 1); // +1 for default option - expect(sortOptions[1].nativeElement.textContent).toContain('Name (A-Z)'); - expect(sortOptions[2].nativeElement.textContent).toContain('Name (Z-A)'); - }); it('should navigate to the edit project route with the correct query parameters and stop event propagation', () => { - // Arrange const mockRouter = TestBed.inject(Router); - spyOn(mockRouter, 'navigate'); // Spy on the navigate method - const mockEvent = jasmine.createSpyObj('Event', ['stopPropagation']); // Mock event with stopPropagation method - - const project = { - projectNumber: 12345, - projectName: 'Sample Project' - }; - - // Act + spyOn(mockRouter, 'navigate'); + const mockEvent = jasmine.createSpyObj('Event', ['stopPropagation']); + const project = { projectNumber: 12345, projectName: 'Sample Project' }; + component.editProject(project, mockEvent); - - // Assert + expect(mockRouter.navigate).toHaveBeenCalledWith([ResourcesRoutes.EDIT_PROJECT], { - queryParams: { projectNumber: project.projectNumber, name: project.projectName } + queryParams: { projectNumber: project.projectNumber, name: project.projectName }, }); expect(mockEvent.stopPropagation).toHaveBeenCalled(); }); - + + it('should reload projects if createNewProject dialog returns success', () => { + spyOn(component, 'loadProjects'); + component.createNewProject(); + expect(mockDialog.open).toHaveBeenCalled(); + expect(component.loadProjects).toHaveBeenCalled(); + }); + }); diff --git a/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.ts b/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.ts index 7bd777a49..2e4047c41 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/list-panel/projects-list/projects-list.component.ts @@ -1,9 +1,13 @@ import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatExpansionModule } from '@angular/material/expansion'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { ResourcesRoutes } from 'src/app/utils'; +import { ProjectService } from 'src/app/services/project-services'; +import { CodeTableServices } from 'src/app/services/code-table-services'; +import { CreateNewProjectDialogComponent } from 'src/app/components/create-new-project-dialog/create-new-project-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'app-projects-list', @@ -12,13 +16,65 @@ import { ResourcesRoutes } from 'src/app/utils'; templateUrl: './projects-list.component.html', styleUrls: ['./projects-list.component.scss'], // Corrected to 'styleUrls' }) -export class ProjectsListComponent { - +export class ProjectsListComponent implements OnInit { + [key: string]: any; + projectList : any[] = []; + programAreaCode: any[] = []; + forestRegionCode: any[] = []; constructor( - private router: Router, + private readonly router: Router, + private readonly projectService: ProjectService, + private readonly codeTableService: CodeTableServices, + private readonly dialog: MatDialog + ) { } + ngOnInit(): void { + this.loadCodeTables(); + this.loadProjects(); + } + + loadCodeTables(): void { + const codeTables = [ + { name: 'programAreaCodes', property: 'businessAreas', embeddedKey: 'programArea' }, + { name: 'forestRegionCodes', property: 'forestRegions', embeddedKey: 'forestRegionCode' }, + ]; + codeTables.forEach((table) => { + this.codeTableService.fetchCodeTable(table.name).subscribe({ + next: (data) => { + if (table.name === 'programAreaCodes') { + this.programAreaCode = data._embedded.programArea; + } else if (table.name === 'forestRegionCodes') { + this.forestRegionCode = data._embedded.forestRegionCode; + } + }, + error: (err) => { + console.error(`Error fetching ${table.name}`, err); + + // Explicitly set the property to an empty array on error + if (table.name === 'programAreaCodes') { + this.programAreaCode = []; + } else if (table.name === 'forestRegionCodes') { + this.forestRegionCode = []; + } + }, + }); + }); + } + + + loadProjects() { + this.projectService.fetchProjects().subscribe({ + next: (data) => { + this.projectList = data._embedded?.project; + }, + error: (err) => { + console.error('Error fetching projects:', err); + this.projectList = []; + } + }); + } sortOptions = [ { label: 'Name (A-Z)', value: 'ascending' }, { label: 'Name (Z-A)', value: 'descending' }, @@ -27,113 +83,12 @@ export class ProjectsListComponent { selectedSort = ''; syncWithMap = false; resultCount = 3; - - - // MOCK UP DATA TO MACTCH UP THE REAL DATA MODEL - projectList = [ - { - projectTypeCode: { - projectTypeCode: "FUEL_MGMT", - }, - projectNumber: 12345, - siteUnitName: "Vancouver Forest Unit", - forestAreaCode: { - forestAreaCode: "WEST", - }, - generalScopeCode: { - generalScopeCode: "SL_ACT", - }, - programAreaGuid: "27602cd9-4b6e-9be0-e063-690a0a0afb50", - projectName: "Sample Forest Management Project", - projectLead: "Jane Smith", - projectLeadEmailAddress: "jane.smith@example.com", - projectDescription: "This is a comprehensive forest management project focusing on sustainable practices", - closestCommunityName: "Vancouver", - totalFundingRequestAmount: 100000.0, - totalAllocatedAmount: 95000.0, - totalPlannedProjectSizeHa: 500.0, - totalPlannedCostPerHectare: 200.0, - totalActualAmount: 0.0, - isMultiFiscalYearProj: false, - forestRegionOrgUnitId: 1001, - forestDistrictOrgUnitId: 2001, - fireCentreOrgUnitId: 3001, - bcParksRegionOrgUnitId: 4001, - bcParksSectionOrgUnitId: 5001, - }, - { - projectTypeCode: { - projectTypeCode: "WLD_MGMT", - }, - projectNumber: 67890, - siteUnitName: "Kelowna Wildlife Zone", - forestAreaCode: { - forestAreaCode: "EAST", - }, - generalScopeCode: { - generalScopeCode: "WL_ACT", - }, - programAreaGuid: "58672bcd-3e7f-8cd1-e053-680a0a0afc40", - projectName: "Sustainable Fuel Management Initiative", - projectLead: "John Doe", - projectLeadEmailAddress: "john.doe@example.com", - projectDescription: "An initiative to promote sustainable wildlife and fuel management practices", - closestCommunityName: "Kelowna", - totalFundingRequestAmount: 75000.0, - totalAllocatedAmount: 70000.0, - totalPlannedProjectSizeHa: 300.0, - totalPlannedCostPerHectare: 250.0, - totalActualAmount: 0.0, - isMultiFiscalYearProj: true, - forestRegionOrgUnitId: 1101, - forestDistrictOrgUnitId: 2101, - fireCentreOrgUnitId: 3101, - bcParksRegionOrgUnitId: 4101, - bcParksSectionOrgUnitId: 5101, - }, - { - projectTypeCode: { - projectTypeCode: "URB_FOREST", - }, - projectNumber: 11223, - siteUnitName: "Prince George Forest Sector", - forestAreaCode: { - forestAreaCode: "NORTH", - }, - generalScopeCode: { - generalScopeCode: "UF_REV", - }, - programAreaGuid: "19762acd-7d8a-4fe2-e043-680a0b0afc11", - projectName: "Urban Forest Revitalization Program", - projectLead: "Alice Brown", - projectLeadEmailAddress: "alice.brown@example.com", - projectDescription: "A program aimed at revitalizing urban forests in northern regions", - closestCommunityName: "Prince George", - totalFundingRequestAmount: 120000.0, - totalAllocatedAmount: 115000.0, - totalPlannedProjectSizeHa: 750.0, - totalPlannedCostPerHectare: 160.0, - totalActualAmount: 0.0, - isMultiFiscalYearProj: true, - forestRegionOrgUnitId: 1201, - forestDistrictOrgUnitId: 2201, - fireCentreOrgUnitId: 3201, - bcParksRegionOrgUnitId: 4201, - bcParksSectionOrgUnitId: 5201, - }, - ]; fiscalYearActivityTypes = ['Clearing','Burning','Pruning'] onSortChange(event:any): void { this.selectedSort = event.target.value; - console.log('Sort changed to:', this.selectedSort); - } - - onSyncMapToggleChange(event: any): void { - this.syncWithMap = event.checked; - console.log('Sync with map:', this.syncWithMap ? 'On' : 'Off'); } editProject(project: any, event:Event) { @@ -143,4 +98,36 @@ export class ProjectsListComponent { }); } + getDescription(codeTable: string, code: number | string): string { + const table = this[codeTable]; + if (!table) return 'Unknown'; // Return 'Unknown' if the table is not loaded + + let entry; + + if (codeTable === 'programAreaCode') { + // Search by programAreaGuid if the codeTable is programAreaCode + entry = table.find((item: any) => item.programAreaGuid === code); + return entry ? entry.programAreaName : 'Unknown' + } else { + // Default to searching by orgUnitId + entry = table.find((item: any) => item.orgUnitId === code); + } + + return entry ? entry.orgUnitName : 'Unknown'; + } + + createNewProject(): void { + const dialogRef = this.dialog.open(CreateNewProjectDialogComponent, { + width: '880px', + disableClose: true, + hasBackdrop: true, + }); + // Subscribe to the afterClosed method to handle the result + dialogRef.afterClosed().subscribe((result) => { + if (result && result.success === true) { + this.loadProjects(); + } + }); + } + } diff --git a/client/wfprev-war/src/main/angular/src/app/components/list/list.component.ts b/client/wfprev-war/src/main/angular/src/app/components/list/list.component.ts index 7a86921ee..56e7360cd 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/list/list.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/list/list.component.ts @@ -67,7 +67,6 @@ export class ListComponent implements OnInit { } viewProjectDetails(project: any) { - console.log(`Viewing details for: ${project.name}`); this.router.navigate([ResourcesRoutes.LIST], { queryParams: { projectNumber: project.projectNumber } }); diff --git a/client/wfprev-war/src/main/angular/src/app/components/map/map.component.scss b/client/wfprev-war/src/main/angular/src/app/components/map/map.component.scss index 1c52b1b26..e223e9b73 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/map/map.component.scss +++ b/client/wfprev-war/src/main/angular/src/app/components/map/map.component.scss @@ -3,6 +3,7 @@ } .map-content { + height: calc(100vh - 250px); flex-grow: 1; padding: 20px; background-color: #f1f1f1; @@ -11,5 +12,5 @@ #map { width: 100%; - height: 100%; // Full height for the map + height: 90%; } \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/app/components/map/map.component.spec.ts b/client/wfprev-war/src/main/angular/src/app/components/map/map.component.spec.ts index b8cdc28b7..62c0e6173 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/map/map.component.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/map/map.component.spec.ts @@ -1,66 +1,118 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MapComponent } from './map.component'; -import { By } from '@angular/platform-browser'; -import * as L from 'leaflet'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import * as L from 'leaflet'; +import { AppConfigService } from 'src/app/services/app-config.service'; import { of } from 'rxjs'; -import { ActivatedRoute } from '@angular/router'; +import { ChangeDetectorRef } from '@angular/core'; + +// Mock ApplicationConfig +const mockApplicationConfig = { + application: { + baseUrl: 'http://test.com', + lazyAuthenticate: false, // Ensure this property is defined + enableLocalStorageToken: true, + acronym: 'TEST', + environment: 'DEV', + version: '1.0.0', + }, + webade: { + oauth2Url: 'http://oauth.test', + clientId: 'test-client', + authScopes: 'TEST.*', + }, + rest: {}, +}; + +// Mock AppConfigService +class MockAppConfigService { + private appConfig = mockApplicationConfig; + + loadAppConfig(): Promise { + return Promise.resolve(); // Simulate successful configuration loading + } + + getConfig(): any { + return this.appConfig; // Return mock configuration + } +} describe('MapComponent', () => { let component: MapComponent; let fixture: ComponentFixture; let mapMock: Partial; + let cdrMock: jasmine.SpyObj; + let mockAppConfigService: jasmine.SpyObj; - const mockActivatedRoute = { - queryParamMap: of({ - get: (key: string) => null, // Mock behavior for query parameters - }), - }; - beforeEach(async () => { mapMock = { fitBounds: jasmine.createSpy('fitBounds'), invalidateSize: jasmine.createSpy('invalidateSize'), addLayer: jasmine.createSpy('addLayer'), }; - + + cdrMock = jasmine.createSpyObj('ChangeDetectorRef', ['markForCheck']); + mockAppConfigService = jasmine.createSpyObj('AppConfigService', ['getConfig']); + mockAppConfigService.getConfig.and.returnValue({ + application: { + acronym: 'TEST', + version: '1.0', + baseUrl: 'https://test.example.com', + environment: 'test', + lazyAuthenticate: false, + enableLocalStorageToken: true, + allowLocalExpiredToken: false, + localStorageTokenKey: 'test-token-key' + }, + rest: { + someServiceUrl: 'https://rest.example.com' + }, + webade: { + oauth2Url: 'https://auth.example.com', + clientId: 'test-client-id', + authScopes: 'read write', + enableCheckToken: true, + checkTokenUrl: 'https://auth.example.com/check-token' + } + }); + spyOn(L, 'map').and.returnValue(mapMock as L.Map); - + await TestBed.configureTestingModule({ - imports: [MapComponent, BrowserAnimationsModule], - providers: [{ provide: ActivatedRoute, useValue: mockActivatedRoute }] + imports: [ + MapComponent, + BrowserAnimationsModule, + HttpClientTestingModule, + ], + providers: [ + { provide: AppConfigService, useClass: MockAppConfigService }, + ], }).compileComponents(); + // Simulate configuration loading + const appConfigService = TestBed.inject(AppConfigService); + await appConfigService.loadAppConfig(); + fixture = TestBed.createComponent(MapComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - // Test 1: Check if the component is created it('should create the component', () => { expect(component).toBeTruthy(); }); - // Test 2: Check if the title is displayed in an

tag - it('should display the title in an

tag', () => { - const h2Element = fixture.debugElement.query(By.css('h2')).nativeElement; - expect(h2Element.textContent).toContain('This is the Map Component'); - }); - - // Test 3: Check if the map content text is displayed - it('should render the map content text', () => { - const paragraphElement = fixture.debugElement.query(By.css('p')).nativeElement; - expect(paragraphElement.textContent).toContain('Map content goes here'); - }); - - // Test 4: Check if the Leaflet map is initialized it('should initialize the Leaflet map', () => { expect(L.map).toHaveBeenCalledWith('map'); - expect(mapMock.fitBounds).toHaveBeenCalled(); + expect(mapMock.fitBounds).toHaveBeenCalledWith([ + [48.3, -139.1], + [60.0, -114.0], + ]); }); - // Test 5: Check if invalidateSize() is called when the panel is resized - it('should call invalidateSize() when the panel is resized', () => { + it('should call invalidateSize() on panel resize', () => { + component['map'] = mapMock as L.Map; component.onPanelResized(); expect(mapMock.invalidateSize).toHaveBeenCalled(); }); diff --git a/client/wfprev-war/src/main/angular/src/app/components/map/map.component.ts b/client/wfprev-war/src/main/angular/src/app/components/map/map.component.ts index 57ddbd5bc..45a792811 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/map/map.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/map/map.component.ts @@ -46,4 +46,4 @@ export class MapComponent implements AfterViewInit{ this.cdr.markForCheck(); } } -} +} \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/app/components/models.ts b/client/wfprev-war/src/main/angular/src/app/components/models.ts new file mode 100644 index 000000000..a919b5172 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/models.ts @@ -0,0 +1,36 @@ +export interface Project { + bcParksRegionOrgUnitId: number; + bcParksSectionOrgUnitId: number; + closestCommunityName: string; + createDate?: string; + createUser?: string; + fireCentreOrgUnitId: number; + forestAreaCode?: { + forestAreaCode: string; + }; + forestDistrictOrgUnitId: number; + forestRegionOrgUnitId: number; + generalScopeCode?: { + generalScopeCode: string; + }; + isMultiFiscalYearProj: boolean; + programAreaGuid: string; + projectDescription: string; + projectGuid?: string; + projectLead: string; + projectLeadEmailAddress: string; + projectName: string; + projectNumber: number; + projectTypeCode?: { + projectTypeCode: string; + }; + revisionCount?: number; + siteUnitName: string; + totalActualAmount: number; + totalAllocatedAmount: number; + totalFundingRequestAmount: number; + totalPlannedCostPerHectare: number; + totalPlannedProjectSizeHa: number; + updateDate?: string; + updateUser?: string; +} diff --git a/client/wfprev-war/src/main/angular/src/app/components/resizable-panel/resizable-panel.component.spec.ts b/client/wfprev-war/src/main/angular/src/app/components/resizable-panel/resizable-panel.component.spec.ts index 89dcd77f0..f3b15cc14 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/resizable-panel/resizable-panel.component.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/resizable-panel/resizable-panel.component.spec.ts @@ -1,8 +1,72 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ResizablePanelComponent } from './resizable-panel.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ActivatedRoute } from '@angular/router'; -import { of } from 'rxjs'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { Component } from '@angular/core'; +import { AppConfigService } from 'src/app/services/app-config.service'; + +// Mock ApplicationConfig +const mockApplicationConfig = { + application: { + baseUrl: 'http://test.com', + lazyAuthenticate: false, // Ensure this property is defined + enableLocalStorageToken: true, + acronym: 'TEST', + environment: 'DEV', + version: '1.0.0', + }, + webade: { + oauth2Url: 'http://oauth.test', + clientId: 'test-client', + authScopes: 'TEST.*', + }, + rest: {}, +}; + +// Mock AppConfigService +class MockAppConfigService { + private appConfig = mockApplicationConfig; + + loadAppConfig(): Promise { + return Promise.resolve(); // Simulate successful configuration loading + } + + getConfig(): any { + return this.appConfig; // Return mock configuration + } +} + +// Mock ProjectService +class MockProjectService { + // Add mock methods if needed +} + +// Mock MapComponent that uses ResizablePanelComponent +@Component({ + selector: 'app-map', + template: ` +
+ +
+
+
+

Mock Map Component

+
+
+
+ `, +}) +class MockMapComponent { + panelContent: string = `

Mock Panel Content

`; + async loadAppConfig(): Promise { + // Simulate config loading + return Promise.resolve(); + } + + onPanelResized(): void { + // Mock method for panel resize handling + } +} describe('ResizablePanelComponent', () => { let component: ResizablePanelComponent; @@ -10,59 +74,45 @@ describe('ResizablePanelComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ResizablePanelComponent, BrowserAnimationsModule], // Standalone component + imports: [ + BrowserAnimationsModule, // Angular Material animations + HttpClientTestingModule, // Mock HTTP dependencies + ResizablePanelComponent, // Import the standalone component + ], providers: [ - { - provide: ActivatedRoute, - useValue: { - queryParamMap: of({ get: () => null }), // Mock ActivatedRoute - }, - }, + { provide: AppConfigService, useClass: MockAppConfigService }, // Provide mock AppConfigService + { provide: MockProjectService, useClass: MockProjectService }, // Provide mock ProjectService ], }).compileComponents(); + // Simulate configuration loading + const appConfigService = TestBed.inject(AppConfigService); // Correctly inject the mock service + await appConfigService.loadAppConfig(); + fixture = TestBed.createComponent(ResizablePanelComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + it('should create the component', () => { expect(component).toBeTruthy(); }); it('should initialize with default values', () => { - expect(component.panelWidth).toBe('50vw'); - expect(component.breakpoints).toEqual([5, 50, 90]); - expect(component.selectedTabIndex).toBe(0); // Default tab index + expect(component.panelWidth).toBe('50vw'); // Default panel width + expect(component.breakpoints).toEqual([5, 50, 90]); // Default breakpoints + expect(component.selectedTabIndex).toBe(0); // Default selected tab index }); - it('should resize the panel and emit the panelResized event', () => { - spyOn(component.panelResized, 'emit'); // Spy on the event emitter - component.resizePanel(75); + it('should resize the panel and emit panelResized event', () => { + spyOn(component.panelResized, 'emit'); // Spy on the EventEmitter + component.resizePanel(75); // Resize to 75vw expect(component.panelWidth).toBe('75vw'); - expect(component.panelResized.emit).toHaveBeenCalled(); - }); - - it('should emit panelResized with void', () => { - const emitSpy = spyOn(component.panelResized, 'emit'); - component.resizePanel(75); - expect(emitSpy).toHaveBeenCalledWith(); // Should be called with no arguments + expect(component.panelResized.emit).toHaveBeenCalled(); // Ensure the event is emitted }); it('should update selectedTabIndex when selectTab is called', () => { - component.selectTab(2); // Switch to the third tab - expect(component.selectedTabIndex).toBe(2); - }); - - it('should render the correct number of tabs', () => { - const tabs = component.tabs; - expect(tabs.length).toBe(3); - expect(tabs[0].name).toBe('Projects'); - expect(tabs[1].name).toBe('Dashboard'); - expect(tabs[2].name).toBe('Planning'); - }); - - it('should render default panel width', () => { - expect(component.panelWidth).toBe('50vw'); + component.selectTab(2); // Select the third tab + expect(component.selectedTabIndex).toBe(2); // Check updated tab index }); }); diff --git a/client/wfprev-war/src/main/angular/src/app/services/code-table-services.ts b/client/wfprev-war/src/main/angular/src/app/services/code-table-services.ts new file mode 100644 index 000000000..91c338b89 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/services/code-table-services.ts @@ -0,0 +1,46 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { catchError, map, Observable, of, throwError } from "rxjs"; +import { AppConfigService } from "src/app/services/app-config.service"; +import { TokenService } from "src/app/services/token.service"; + +@Injectable({ + providedIn: 'root', +}) +export class CodeTableServices { + private codeTableCache: { [key: string]: any } = {}; // Cache for code tables + + constructor( + private readonly appConfigService: AppConfigService, + private readonly httpClient: HttpClient, + private readonly tokenService: TokenService, + ) {} + + fetchCodeTable(codeTableName: string): Observable { + // Check if the code table is already cached + if (this.codeTableCache[codeTableName]) { + return of(this.codeTableCache[codeTableName]); // Return cached data + } + + // If not cached, fetch from the API + const url = `${this.appConfigService.getConfig().rest['wfprev'] + }/wfprev-api/codes/${codeTableName}`; + + return this.httpClient.get( + url, { + headers: { + Authorization: `Bearer ${this.tokenService.getOauthToken()}`, + } + } + ).pipe( + map((response: any) => { + this.codeTableCache[codeTableName] = response; // Cache the response + return response; + }), + catchError((error) => { + console.error("Error fetching code table", error); + return throwError(() => new Error("Failed to get code table")); + }) + ); + } +} 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 new file mode 100644 index 000000000..f734dced1 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/services/project-services.ts @@ -0,0 +1,50 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { catchError, map, Observable,throwError } from "rxjs"; +import { AppConfigService } from "src/app/services/app-config.service"; +import { TokenService } from "src/app/services/token.service"; + +@Injectable({ + providedIn: 'root', + }) + +export class ProjectService { + constructor( + private readonly appConfigService: AppConfigService, + private readonly httpClient: HttpClient, + private readonly tokenService: TokenService, + ){ + } + + fetchProjects(): Observable { + const url = `${this.appConfigService.getConfig().rest['wfprev'] + }/wfprev-api/projects`; + + return this.httpClient.get( + url, { + headers: { + Authorization: `Bearer ${this.tokenService.getOauthToken()}`, + } + } + ).pipe( + map((response: any) => response), + catchError((error) => { + console.error("Error fetching projects", error); + return throwError(() => new Error("Failed to fetch projects")); + }) + ); + } + + createProject(project: any): Observable { + const url = `${this.appConfigService.getConfig().rest['wfprev']}/wfprev-api/projects`; + return this.httpClient.post( + url, + project, + { + headers: { + Authorization: `Bearer ${this.tokenService.getOauthToken()}`, + } + } + ); + } +} \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/assets/data/appConfig.json b/client/wfprev-war/src/main/angular/src/assets/data/appConfig.json index dd0fd8a67..6c62ff2ca 100644 --- a/client/wfprev-war/src/main/angular/src/assets/data/appConfig.json +++ b/client/wfprev-war/src/main/angular/src/assets/data/appConfig.json @@ -9,5 +9,9 @@ "authScopes": "WFNEWS.*", "enableCheckToken": true, "checkTokenUrl": "#{WFPREV_CHECK_TOKEN_URL}#" - } + }, + + "rest": { + "wfprev":"#{WFPREV_BASE_URL}#" + } } \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/assets/data/appConfig.local.json b/client/wfprev-war/src/main/angular/src/assets/data/appConfig.local.json index 3f9b5afd0..07ff93f89 100644 --- a/client/wfprev-war/src/main/angular/src/assets/data/appConfig.local.json +++ b/client/wfprev-war/src/main/angular/src/assets/data/appConfig.local.json @@ -8,6 +8,10 @@ "clientId": "WFNEWS-UI", "authScopes": "WFNEWS.*", "enableCheckToken": true, - "checkTokenUrl": "http://localhost:8080/pub/wfprev-api/check/checkToken" - } + "checkTokenUrl": "./assets/data/checktoken-user.json" + }, + + "rest": { + "wfprev":"http://localhost:8080" + } } \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/assets/data/checktoken-user.json b/client/wfprev-war/src/main/angular/src/assets/data/checktoken-user.json new file mode 100644 index 000000000..265daa952 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/assets/data/checktoken-user.json @@ -0,0 +1,118 @@ +{ + "authorities": null, + "resourceIds": null, + "jti": "be458066-6df4-400e-851f-3a5355828b88", + "sub": "IDIR\\LULI", + "scope": [ + "WFDM.GET_TOPLEVEL", + "WFORG.GET_ORG_UNITS", + "WEBADE-REST.UPDATEUSERPREFERENCES", + "WFIM.SIGNOFF_INCIDENT_APPROVAL", + "WEBADE-REST.GETORGANIZATIONS", + "WFIM.CREATE_WILDFIRE_PARTY", + "WFDM.GET_FILE", + "WFIM.GET_PUBLIC_REPORT_OF_FIRE", + "WFIM.GET_TOPLEVEL", + "WFIM.GET_ARCHIVED_ATTACHMENT", + "WFIM.CREATE_INITIAL_PHONE_REPORT", + "WFIM.DELETE_PROVISIONAL_ZONE", + "WFIM.GET_PROVISIONAL_ZONE", + "WFDM.ADD_FILE_CHECKOUT", + "WFIM.GET_ROF_ATTACHMENT", + "WFDM.GET_CODE_TABLES", + "WFIM.GET_INCIDENT_INVESTIGATION", + "WFIM.CREATE_INCIDENT_COMMENT", + "WFONE.GET_PORTAL_LINKS", + "WFIM.UPDATE_PUBLIC_REPORT_OF_FIRE", + "WFIM.GET_INCIDENT_VERIFICATION", + "WFIM.GET_RESOURCE_ALLOCATION_ASSESSMENT", + "WFIM.DOCUMENT_UPLOAD_ADMIN", + "WEBADE-REST.GETCURRENTUSER", + "WFIM.SIGNOFF_INCIDENT_VERIFICATION", + "WFIM.GET_INCIDENT_CAUSE", + "WFDM.GET_FILE_METADATA", + "WFIM.CREATE_PROVISIONAL_ZONE", + "WFIM.CREATE_INCIDENT_ATTACHMENT", + "WFIM.INCIDENT_DISPATCH", + "WFIM.GET_INITIAL_PHONE_REPORT", + "WFIM.UPDATE_MANAGING_LAND_AUTHORITY", + "WFIM.INCIDENT_DOCUMENT_ADMIN", + "WFIM.ROF_800CENTRE", + "WFIM.CREATE_ROF_ATTACHMENT", + "WFIM.UPDATE_WILDFIRE_INCIDENT", + "WFDM.DELETE_FILE", + "WFDM.GET_FILE_SECURITY", + "WFIM.UPDATE_WILDFIRE_PARTY", + "WFIM.UPDATE_FIRE_REPORT", + "WFONE.GET_CODE_TABLES", + "WFIM.GET_MANAGING_LAND_AUTHORITY", + "WFIM.GET_PRIVATE_ATTACHMENT", + "WFIM.UPDATE_INCIDENT_ATTACHMENT", + "WFIM.CREATE_PUBLIC_REPORT_OF_FIRE_COMMENT", + "WEBADE-REST.CREATEUSERPREFERENCES", + "WFDM.GET_FILE_CHECKOUT", + "WFIM.GET_INCIDENT_ATTACHMENT", + "WFONE.GET_TOPLEVEL", + "WFIM.UPDATE_ROF_ATTACHMENT", + "WFORG.GET_CODE_TABLES", + "WFIM.GET_CODE_TABLES", + "WFIM.CREATE_PUBLIC_REPORT_OF_FIRE", + "WFDM.GET_LOCK", + "WFONE.GET_STATISTICS", + "WEBADE-REST.GETUSERPREFERENCES", + "WEBADE-REST.GETUSERTYPES", + "WFIM.GET_FIRE_REPORT", + "WFDM.GET_FILE_LOCK", + "WFIM.CREATE_INITIAL_PHONE_REPORT_COMMENT", + "WFIM.ROF_DOCUMENT_ADMIN", + "WEBADE-REST.DELETEUSERPREFERENCES", + "WFDM.UPDATE_FILE", + "WFDM.ADD_FILE_METADATA", + "WFDM.UPDATE_FILE_SECURITY", + "WEBADE-REST.GETTOPLEVEL", + "WFIM.UPDATE_INCIDENT_INVESTIGATION", + "WFIM.GET_INCIDENT_APPROVAL", + "WFIM.GET_WILDFIRE_PARTY", + "WFIM.UPDATE_PROVISIONAL_ZONE", + "WEBADE-OAUTH2.user", + "WFDM.DELETE_FILE_SECURITY", + "WFDM.ADD_FILE_SECURITY", + "WFORG.GET_TOPLEVEL", + "WFDM.DELETE_FILE_METADATA", + "WFDM.UPDATE_FILE_METADATA", + "WFIM.GET_INCIDENT_COMMENT", + "WFONE.PORTAL_VIEWER", + "WFIM.GET_PUBLIC_REPORT_OF_FIRE_COMMENT", + "WFDM.CREATE_FILE", + "WFIM.DOCUMENT_UPLOADER", + "WFDM.UPDATE_FILE_CHECKOUT", + "WFIM.UPDATE_RESOURCE_ALLOCATION_ASSESSMENT", + "WFIM.GET_WILDFIRE_INCIDENT", + "WFIM.WFNEWS_ADMIN", + "WFRM.GET_TOPLEVEL", + "WFRM.GET_ASSIGNMENT", + "WFRM.GET_ASSIGNMENT_MEMBER" + ], + "clientId": "WFPREV-UI", + "cid": "WFPREV-UI", + "clientAppCode": "WFIM", + "grantType": "implicit", + "userId": "IDIR\\LULI", + "userName": "Internal/4056257CDE4F4C2AA0460F24C30AD910/UNK", + "givenName": "Lucas", + "familyName": "Li", + "email": "DISPATCH@fakeuser.com", + "userType": "GOV", + "userGuid": "4056257CDE4F4C2AA0460F24C30AD910", + "onBehalfOfOrganizationId": null, + "onBehalfOfOrganizationCode": null, + "onBehalfOfOrganizationName": null, + "onBehalfOfOrganizationAdditionalInfo": null, + "businessNumber": null, + "businessGuid": null, + "iat": 1661803493, + "exp": 1661846693, + "iss": "http://localhost:8080/webade-oauth2/oauth/token", + "aud": ["WFDM", "WEBADE-REST", "WFIM", "WEBADE-OAUTH2", "WFONE", "WFORG", "WFRM"] + } + \ No newline at end of file