diff --git a/client/wfprev-war/src/main/angular/src/app/app.component.ts b/client/wfprev-war/src/main/angular/src/app/app.component.ts index ead5a6821..d031b9987 100644 --- a/client/wfprev-war/src/main/angular/src/app/app.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/app.component.ts @@ -22,7 +22,6 @@ export class AppComponent { ) { } - setActive(menuItem: string): void { this.activeRoute = menuItem; switch (menuItem) { diff --git a/client/wfprev-war/src/main/angular/src/app/app.routing.spec.ts b/client/wfprev-war/src/main/angular/src/app/app.routing.spec.ts index a043defea..1007301ee 100644 --- a/client/wfprev-war/src/main/angular/src/app/app.routing.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/app.routing.spec.ts @@ -31,7 +31,7 @@ describe('App Routing Module', () => { it('should have correct routes defined', () => { const routes = router.config; - expect(routes.length).toBe(4); + expect(routes.length).toBe(5); }); it('should define route for LIST path', () => { diff --git a/client/wfprev-war/src/main/angular/src/app/app.routing.ts b/client/wfprev-war/src/main/angular/src/app/app.routing.ts index 3b03dcc7f..3c7fd046a 100644 --- a/client/wfprev-war/src/main/angular/src/app/app.routing.ts +++ b/client/wfprev-war/src/main/angular/src/app/app.routing.ts @@ -11,25 +11,33 @@ const PANEL_ROUTES: Routes = [ path: ResourcesRoutes.LIST, loadChildren: () => import('src/app/components/list.module').then(m => m.ListModule), - canActivate: [PrevAuthGuard], - data: { scopes: PROFILE_SCOPES }, + canActivate: [PrevAuthGuard], + data: { scopes: PROFILE_SCOPES }, }, { path: ResourcesRoutes.MAP, loadChildren: () => import('src/app/components/map.module').then(m => m.MapModule), - canActivate: [PrevAuthGuard], - data: { scopes: PROFILE_SCOPES } + canActivate: [PrevAuthGuard], + data: { scopes: PROFILE_SCOPES } }, { path: ResourcesRoutes.ERROR_PAGE, component: ErrorPageComponent, pathMatch: 'full', }, - - { path: '', - redirectTo: ResourcesRoutes.MAP, - pathMatch: 'full' } // Default route to map + { + path: ResourcesRoutes.EDIT_PROJECT, + loadChildren: () => + import('src/app/components/edit-project.module').then(m => m.EditProjectModule), + canActivate: [PrevAuthGuard], + data: { scopes: PROFILE_SCOPES } + }, + { + path: '', + redirectTo: ResourcesRoutes.MAP, + pathMatch: 'full' + } ]; export const ROUTING = RouterModule.forRoot(PANEL_ROUTES, {}); \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.html b/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.html index 864a76800..643d11b80 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.html +++ b/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.html @@ -2,7 +2,7 @@
Create New Project
-
+
diff --git a/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.scss b/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.scss index 7daa6df8f..7fbd61258 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.scss +++ b/client/wfprev-war/src/main/angular/src/app/components/create-new-project-dialog/create-new-project-dialog.component.scss @@ -153,4 +153,8 @@ select { background-repeat: no-repeat; background-position: right 10px center; background-size: 12px; +} + +::ng-deep .mdc-snackbar__surface{ + background-color: transparent !important; } \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project.module.ts b/client/wfprev-war/src/main/angular/src/app/components/edit-project.module.ts new file mode 100644 index 000000000..7e617cc38 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { EditProjectComponent } from 'src/app/components/edit-project/edit-project.component'; + + +const routes: Routes = [ + { path: '', component: EditProjectComponent } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes), EditProjectComponent] +}) +export class EditProjectModule {} \ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.html b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.html new file mode 100644 index 000000000..03056d2db --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.html @@ -0,0 +1,34 @@ +
+
+ {{projectName}} +
+ + + +
+ +
+
+ + + +
+

Fiscals Section

+
+
+ + + +
+

Documents Section

+
+
+ + + +
+

History Section

+
+
+
+
\ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.scss b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.scss new file mode 100644 index 000000000..bb321e180 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.scss @@ -0,0 +1,32 @@ +.project-title { + color: #000; + font-size: 34px; + font-style: normal; + font-weight: 400; + line-height: normal; + padding: 12px 24px; + } + + .custom-tab-group { + display: flex; + justify-content: flex-start; + + // Angular Material tab header overrides + ::ng-deep { + .mat-mdc-tab-header { + width: 50%; + padding-left: 30px; + } + + .mat-mdc-tab { + font-size: 22px; + color: #000; + font-style: normal; + font-weight: 400; + } + + .mat-mdc-tab-indicator { + background-color: black !important; /* Change the underline color to black */ + } + } + } \ No newline at end of file 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 new file mode 100644 index 000000000..3aadedc3d --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.spec.ts @@ -0,0 +1,58 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { EditProjectComponent } from './edit-project.component'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ProjectDetailsComponent } from 'src/app/components/edit-project/project-details/project-details.component'; + +describe('EditProjectComponent', () => { + let component: EditProjectComponent; + let fixture: ComponentFixture; + let mockActivatedRoute: Partial; + + beforeEach(async () => { + const mockParamMap: ParamMap = { + has: (key: string) => key === 'name', + get: (key: string) => (key === 'name' ? 'Test Project' : null), + getAll: () => [], + keys: [], + }; + + mockActivatedRoute = { + queryParamMap: of(mockParamMap), + }; + + await TestBed.configureTestingModule({ + imports: [EditProjectComponent, BrowserAnimationsModule], + providers: [{ provide: ActivatedRoute, useValue: mockActivatedRoute }], + }).compileComponents(); + + fixture = TestBed.createComponent(EditProjectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set the projectName from query parameters', () => { + expect(component.projectName).toBe('Test Project'); + }); + + it('should display the project name in the title', () => { + const projectTitleElement = fixture.debugElement.query(By.css('.project-title span')).nativeElement; + expect(projectTitleElement.textContent).toContain('Test Project'); + }); + + it('should render the Details tab', () => { + const detailsTab = fixture.debugElement.query(By.css('.details-tab')); + expect(detailsTab).toBeTruthy(); + }); + + it('should display ProjectDetailsComponent inside the Details tab', () => { + const projectDetailsComponent = fixture.debugElement.query(By.directive(ProjectDetailsComponent)); + expect(projectDetailsComponent).toBeTruthy(); + }); +}); diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.stories.ts b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.stories.ts new file mode 100644 index 000000000..c874e4157 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.stories.ts @@ -0,0 +1,53 @@ +import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { EditProjectComponent } from './edit-project.component'; +import { MatTabsModule } from '@angular/material/tabs'; +import { ProjectDetailsComponent } from 'src/app/components/edit-project/project-details/project-details.component'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +const meta: Meta = { + title: 'EditProjectComponent', + component: EditProjectComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [EditProjectComponent, MatTabsModule, ProjectDetailsComponent,BrowserAnimationsModule ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + queryParamMap: of({ + get: (key: string) => (key === 'name' ? 'Sample Project' : null), + }), + }, + }, + ], + }), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +export const WithCustomProjectName: Story = { + decorators: [ + moduleMetadata({ + providers: [ + { + provide: ActivatedRoute, + useValue: { + queryParamMap: of({ + get: (key: string) => (key === 'name' ? 'Custom Project' : null), + }), + }, + }, + ], + }), + ], + args: {}, +}; 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 new file mode 100644 index 000000000..f549fda1d --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/edit-project.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from '@angular/core'; +import { MatTabsModule } from '@angular/material/tabs'; +import { ActivatedRoute } from '@angular/router'; +import { ProjectDetailsComponent } from 'src/app/components/edit-project/project-details/project-details.component'; + +@Component({ + selector: 'app-edit-project', + standalone: true, + imports: [MatTabsModule, ProjectDetailsComponent], + templateUrl: './edit-project.component.html', + styleUrl: './edit-project.component.scss' +}) +export class EditProjectComponent implements OnInit { + projectName: string | null = null; + + constructor(private route: ActivatedRoute) {} + ngOnInit(): void { + this.route.queryParamMap.subscribe((params) => { + this.projectName = params.get('name'); + }); + } + +} 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 new file mode 100644 index 000000000..ec85ba092 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.html @@ -0,0 +1,214 @@ +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
+
+ + + +
+ Collapsed Icon + Expanded Icon +
+ Project Description +
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ + + +
+
+
+
+
\ No newline at end of file diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.scss b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.scss new file mode 100644 index 000000000..53b6805bc --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.scss @@ -0,0 +1,280 @@ +.details-container { + margin: 19px; + border-radius: 6px; + border: 1px solid #C6C8CB; + + .form-grid { + padding: 18px; + display: flex; + flex-wrap: wrap; + gap: 20px; + justify-content: space-between; + + + .form-item { + flex: 1 1 calc(20% - 20px); // 5 items per row with a 20px gap + min-width: 200px; + + label { + padding: 9px; + color: #282828; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + input, + select{ + color: #282828; + font-size: 15px; + font-style: normal; + font-weight: 700; + width: 100%; + margin: 8px; + font-size: 14px; + border: none; + border-bottom: 1px solid #282828; + outline: none; + } + + select{ + -webkit-appearance: none; + background-image: url('/assets/dropdown-icon.svg'); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 12px; + } + textarea { + width: 100%; + margin: 8px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + } + + textarea { + resize: vertical; + min-height: 80px; + } + } + + .full-width { + flex: 1 1 100%; // Span the entire row + } + + .form-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + + button { + padding: 10px 20px; + font-size: 16px; + } + } + + .site-unit { + flex:1 1 20% + } + + .closet-community { + flex:1 1 20% + } + + .secondary-rational{ + flex: 1 1 20%; // Set the width to 50% of the row + } + } + } + + @media (max-width: 1024px) { + .form-grid { + .form-item { + flex: 1 1 calc(33.33% - 20px); // 3 items per row on medium screens + } + } + } + + @media (max-width: 768px) { + .form-grid { + .form-item { + flex: 1 1 calc(50% - 20px); // 2 items per row on small screens + } + } + } + + @media (max-width: 480px) { + .form-grid { + .form-item { + flex: 1 1 100%; // 1 item per row on very small screens + } + } + } + + .footer{ + border: 1px solid #C7C7C7; + background: #F2F2F2; + width: 100vw; + margin: -18px; + } + + .button-row{ + display: flex; + height: 43px; + padding: 14px 17px 14px 23px; + justify-content: flex-end; + align-items: flex-start; + gap: 14px; + button { + padding: 12px 32px; + border: none; + border-radius: 4px; + font-size: 16px; + cursor: pointer; + } + + button.primary { + background-color: var(--wf-primary-color); + color: #fff; + } + + button.primary:disabled { + background-color: #c0c0c0; + cursor: not-allowed; + } + + button.secondary { + border-radius: 4px; + border: 1px solid #234075; + background: #FFF; + color: #234075; + font-family: var(--wf-font-family-main); + font-style: normal; + } + + button.secondary:hover { + background-color: #f1f1f1; + } + } + + .description-map-section{ + display: flex; + width: 100%; + height: auto; + gap: 19px; + } + + .project-description-container{ + flex:1; + padding-left: 19px; + background-color: var(--colour-white); + mat-expansion-panel { + border-bottom: 1px solid #C6C8CB; + margin: 0px !important; + background-color: #FFF; + + ::ng-deep .mat-expansion-panel-body{ + padding: 0; + } + + .project-title { + background-color: inherit; + font-weight: bold; + display: flex; + color: #000; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: normal; + gap:10px; + } + } + + .project-description{ + .editable-description{ + padding:0 16px; + width: 100%; + font-size: 16px; + font-weight: 400; + border: none; + color: #242424; + } + } + + .project-description-footer{ + border: 1px solid #C7C7C7; + background: #F2F2F2; + } + } + + .map-section{ + padding-right: 19px; + flex: 1; + height: 600px; /* Fixed height for the map */ + position: relative; /* Ensure positioning context for the map */ + #map { + height: 100%; + } + } + + .latlong-section { + display: flex; + flex-direction: column; + align-items: flex-end; + padding: 10px; + border-top: 1px solid #ccc; + + form { + display: flex; + flex-direction: column; /* Stack label and input/button container */ + align-items: flex-end; + gap: 5px; + + label { + font-weight: bold; + margin-bottom: 5px; /* Add space below the label */ + text-align: right; /* Align label text to the right */ + } + + .input-group { + display: flex; /* Align input and button side by side */ + align-items: center; /* Vertically center-align the button */ + gap: 10px; /* Add space between the input and button */ + + input { + padding: 5px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; + width: 300px; /* Adjust input width */ + } + + .save-button { + padding: 8px 18px; + background: #E9E9E9; + color: black; + border: 1px solid #B1B1B1; + border-radius: 6px; + cursor: pointer; + font-size: 15px; + font-weight: 700; + } + + .save-button :disabled { + background-color: #c0c0c0; + cursor: not-allowed; + } + + .save-button :disabled { + background-color: #c0c0c0; + cursor: not-allowed; + } + } + } + } + + .required { + color: #EF1616; + } \ No newline at end of file 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 new file mode 100644 index 000000000..2a6f23614 --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.spec.ts @@ -0,0 +1,88 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProjectDetailsComponent } from './project-details.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import * as L from 'leaflet'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('ProjectDetailsComponent', () => { + let component: ProjectDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProjectDetailsComponent, ReactiveFormsModule,BrowserAnimationsModule], + }).compileComponents(); + + fixture = TestBed.createComponent(ProjectDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('Form Initialization', () => { + it('should initialize the form with sample data', () => { + const formValues = component.detailsForm.value; + expect(formValues.projectLead).toEqual(component.sampleData.projectLead); + expect(formValues.projectLeadEmailAddress).toEqual( + component.sampleData.projectLeadEmailAddress + ); + expect(formValues.projectTypeCode).toEqual( + component.sampleData.projectTypeCode.projectTypeCode + ); + expect(formValues.coordinates).toEqual(component.sampleData.coordinates); + }); + + it('should mark the form as invalid if required fields are missing', () => { + component.detailsForm.controls['projectTypeCode'].setValue(''); + expect(component.detailsForm.invalid).toBeTrue(); + }); + + it('should mark the form as valid if all required fields are provided', () => { + expect(component.detailsForm.valid).toBeTrue(); + }); + }); + + describe('Map Initialization', () => { + let mapSpy: jasmine.SpyObj; + + beforeEach(() => { + mapSpy = jasmine.createSpyObj('L.Map', ['setView', 'addLayer', 'remove', 'invalidateSize']); + spyOn(L, 'map').and.returnValue(mapSpy); + }); + + it('should not reinitialize the map if it already exists', () => { + component['map'] = mapSpy; + component.ngAfterViewInit(); + expect(L.map).toHaveBeenCalledTimes(0); // Should not be called again + }); + + it('should add a marker to the map', () => { + component.initMap(); + expect(mapSpy.addLayer).toHaveBeenCalled(); + }); + }); + + describe('onCancel Method', () => { + it('should reset the form', () => { + spyOn(component.detailsForm, 'reset'); + component.onCancel(); + expect(component.detailsForm.reset).toHaveBeenCalled(); + }); + }); + + describe('Integration Tests', () => { + it('should display form inputs with correct initial values', () => { + const projectLeadInput = fixture.nativeElement.querySelector('#projectLead'); + expect(projectLeadInput.value).toBe(component.sampleData.projectLead); + }); + + it('should update form values when inputs are changed', () => { + const projectLeadControl = component.detailsForm.controls['projectLead']; + projectLeadControl.setValue('New Lead'); + expect(component.detailsForm.controls['projectLead'].value).toBe('New Lead'); + }); + }); +}); diff --git a/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.stories.ts b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.stories.ts new file mode 100644 index 000000000..3cba0ec5b --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.stories.ts @@ -0,0 +1,87 @@ +import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { ProjectDetailsComponent } from './project-details.component'; +import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { CommonModule } from '@angular/common'; + +const fb = new FormBuilder(); + +const meta: Meta = { + title: 'ProjectDetailsComponent', + component: ProjectDetailsComponent, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [ProjectDetailsComponent, ReactiveFormsModule, MatExpansionModule, CommonModule], + }), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + detailsForm: fb.group({ + projectLead: ['', [Validators.required]], + projectLeadEmailAddress: ['', [Validators.required, Validators.email]], + projectTypeCode: ['', [Validators.required]], + businessArea: ['', [Validators.required]], + forestRegion: ['', [Validators.required]], + forestDistrict: ['', []], + bcParksRegion: ['', []], + bcParksDistrict: ['', []], + siteUnitName: ['', [Validators.required]], + closestCommunityName: ['', []], + fundingStream: ['', []], + totalFundingRequestAmount: [0, [Validators.required, Validators.min(0)]], + totalAllocatedAmount: [0, [Validators.required, Validators.min(0)]], + projectDescription: ['', []], + coordinates: ['', []], + }), + }, +}; + +export const WithPrepopulatedForm: Story = { + args: { + detailsForm: fb.group({ + projectLead: ['Jane Smith', [Validators.required]], + projectLeadEmailAddress: ['jane.smith@example.com', [Validators.required, Validators.email]], + projectTypeCode: ['FUEL_MGMT', [Validators.required]], + businessArea: ['Area1', [Validators.required]], + forestRegion: ['Region1', [Validators.required]], + forestDistrict: ['District1', []], + bcParksRegion: ['Region1', []], + bcParksDistrict: ['District1', []], + siteUnitName: ['Vancouver Forest Unit', [Validators.required]], + closestCommunityName: ['Vancouver', []], + fundingStream: ['Stream1', []], + totalFundingRequestAmount: [100000, [Validators.required, Validators.min(0)]], + totalAllocatedAmount: [95000, [Validators.required, Validators.min(0)]], + projectDescription: ['This is a sample project.', []], + coordinates: ['48.407326,-123.329773', []], + }), + }, +}; + +export const FormWithErrors: Story = { + args: { + detailsForm: fb.group({ + projectLead: ['', [Validators.required]], // Missing required field + projectLeadEmailAddress: ['invalid-email', [Validators.required, Validators.email]], + projectTypeCode: ['', [Validators.required]], // Missing required field + businessArea: ['', [Validators.required]], // Missing required field + forestRegion: ['', [Validators.required]], // Missing required field + forestDistrict: ['', []], + bcParksRegion: ['', []], + bcParksDistrict: ['', []], + siteUnitName: ['', [Validators.required]], // Missing required field + closestCommunityName: ['', []], + fundingStream: ['', []], + totalFundingRequestAmount: [-100, [Validators.required, Validators.min(0)]], // Invalid value + totalAllocatedAmount: [-50, [Validators.required, Validators.min(0)]], // Invalid value + projectDescription: ['', []], + coordinates: ['', []], + }), + }, +}; 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 new file mode 100644 index 000000000..a12c2c3db --- /dev/null +++ b/client/wfprev-war/src/main/angular/src/app/components/edit-project/project-details/project-details.component.ts @@ -0,0 +1,137 @@ +import { CommonModule } from '@angular/common'; +import { AfterViewInit, Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { MatExpansionModule } from '@angular/material/expansion'; +import L from 'leaflet'; + +@Component({ + selector: 'app-project-details', + standalone: true, + imports: [ReactiveFormsModule,MatExpansionModule,CommonModule], + templateUrl: './project-details.component.html', + styleUrl: './project-details.component.scss' +}) +export class ProjectDetailsComponent implements OnInit, AfterViewInit{ + + private map: L.Map | undefined; + + detailsForm!: FormGroup; + + sampleData = { + 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, + coordinates:[48.407326,-123.329773], + primaryObjective: 'Objective1' + }; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.detailsForm = this.fb.group({ + projectLead: [this.sampleData.projectLead], + projectLeadEmailAddress: [ + this.sampleData.projectLeadEmailAddress, + [Validators.email], + ], + projectTypeCode: [ + this.sampleData.projectTypeCode.projectTypeCode, + [Validators.required], + ], + businessArea: [ + this.sampleData.projectTypeCode.projectTypeCode, + [Validators.required], + ], + forestRegion: [ + this.sampleData.forestRegionOrgUnitId, + ], + forestDistrict: [this.sampleData.forestDistrictOrgUnitId], + bcParksRegion: [this.sampleData.bcParksRegionOrgUnitId], + bcParksDistrict: [this.sampleData.bcParksSectionOrgUnitId], + siteUnitName: [this.sampleData.siteUnitName], + closestCommunityName: [this.sampleData.closestCommunityName, [Validators.required]], + fundingStream: [Validators.required], + totalFundingRequestAmount: [ + this.sampleData.totalFundingRequestAmount, + ], + totalAllocatedAmount: [ + this.sampleData.totalAllocatedAmount, + ], + projectDescription: [this.sampleData.projectDescription], + coordinates :[this.sampleData.coordinates], + primaryObjective: [this.sampleData.primaryObjective, [Validators.required]] + }); + + + } + + ngAfterViewInit(): void { + setTimeout(() => { + this.initMap(); + }); + } + + initMap(): void { + if (this.map) { + return; + } + this.map = L.map('map', { + center: [this.sampleData.coordinates[0], this.sampleData.coordinates[1]], + zoom: 13, + zoomControl: false, + }); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }).addTo(this.map); + + + // Add a marker at the project's coordinates + L.marker([this.sampleData.coordinates[0], this.sampleData.coordinates[1]]).addTo(this.map); + + // Bind a popup to the marker + // marker.bindPopup('Project Location: ' + this.sampleData.projectName).openPopup(); + } + + onSave(): void { + if (this.detailsForm.valid) { + // Submit the form data or perform actions here + } else { + console.error('Form is invalid!'); + } + } + + onSaveLatLong() : void{ + + } + + onCancel(): void { + this.detailsForm.reset(); + } +} 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 ecdcc8246..28fb68bdc 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 @@ -53,7 +53,7 @@ detail-icon Details
-
+
edit-icon Edit
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 1717e40dc..5beb8ecbb 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 @@ -5,6 +5,8 @@ import { DebugElement } from '@angular/core'; 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 { ResourcesRoutes } from 'src/app/utils'; describe('ProjectsListComponent', () => { let component: ProjectsListComponent; @@ -14,6 +16,9 @@ describe('ProjectsListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProjectsListComponent, BrowserAnimationsModule, MatExpansionModule, MatSlideToggleModule], // Using standalone component + providers: [ + { provide: ActivatedRoute, useValue: ActivatedRoute }, // Provide mock for ActivatedRoute + ], }).compileComponents(); fixture = TestBed.createComponent(ProjectsListComponent); @@ -167,4 +172,25 @@ describe('ProjectsListComponent', () => { 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 + component.editProject(project, mockEvent); + + // Assert + expect(mockRouter.navigate).toHaveBeenCalledWith([ResourcesRoutes.EDIT_PROJECT], { + queryParams: { projectNumber: project.projectNumber, name: project.projectName } + }); + expect(mockEvent.stopPropagation).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 8aadc379a..7bd777a49 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 @@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatExpansionModule } from '@angular/material/expansion'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ResourcesRoutes } from 'src/app/utils'; @Component({ selector: 'app-projects-list', @@ -11,6 +13,12 @@ import { MatExpansionModule } from '@angular/material/expansion'; styleUrls: ['./projects-list.component.scss'], // Corrected to 'styleUrls' }) export class ProjectsListComponent { + + constructor( + private router: Router, + ) { + } + sortOptions = [ { label: 'Name (A-Z)', value: 'ascending' }, { label: 'Name (Z-A)', value: 'descending' }, @@ -115,7 +123,7 @@ export class ProjectsListComponent { }, ]; - fiscalYearActivityTypes = ['Clearning','Burning','Pruning'] + fiscalYearActivityTypes = ['Clearing','Burning','Pruning'] onSortChange(event:any): void { @@ -127,5 +135,12 @@ export class ProjectsListComponent { this.syncWithMap = event.checked; console.log('Sync with map:', this.syncWithMap ? 'On' : 'Off'); } + + editProject(project: any, event:Event) { + event.stopPropagation(); + this.router.navigate([ResourcesRoutes.EDIT_PROJECT], { + queryParams: { projectNumber: project.projectNumber, name: project.projectName} + }); + } } 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 48377d6ea..b8cdc28b7 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 @@ -3,12 +3,20 @@ import { MapComponent } from './map.component'; import { By } from '@angular/platform-browser'; import * as L from 'leaflet'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { of } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; describe('MapComponent', () => { let component: MapComponent; let fixture: ComponentFixture; let mapMock: Partial; + const mockActivatedRoute = { + queryParamMap: of({ + get: (key: string) => null, // Mock behavior for query parameters + }), + }; + beforeEach(async () => { mapMock = { fitBounds: jasmine.createSpy('fitBounds'), @@ -19,7 +27,8 @@ describe('MapComponent', () => { spyOn(L, 'map').and.returnValue(mapMock as L.Map); await TestBed.configureTestingModule({ - imports: [MapComponent, BrowserAnimationsModule] + imports: [MapComponent, BrowserAnimationsModule], + providers: [{ provide: ActivatedRoute, useValue: mockActivatedRoute }] }).compileComponents(); fixture = TestBed.createComponent(MapComponent); 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 44aebb789..89dcd77f0 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,6 +1,8 @@ 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'; describe('ResizablePanelComponent', () => { let component: ResizablePanelComponent; @@ -9,6 +11,14 @@ describe('ResizablePanelComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ResizablePanelComponent, BrowserAnimationsModule], // Standalone component + providers: [ + { + provide: ActivatedRoute, + useValue: { + queryParamMap: of({ get: () => null }), // Mock ActivatedRoute + }, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ResizablePanelComponent); diff --git a/client/wfprev-war/src/main/angular/src/app/components/shared-layout/app-header/app-header.component.spec.ts b/client/wfprev-war/src/main/angular/src/app/components/shared-layout/app-header/app-header.component.spec.ts index 4399be3f8..eddc5c4ba 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/shared-layout/app-header/app-header.component.spec.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/shared-layout/app-header/app-header.component.spec.ts @@ -58,7 +58,7 @@ describe('AppHeaderComponent', () => { spyOn(window, 'open'); component.onSupportLinkClick(); expect(window.open).toHaveBeenCalledWith( - 'https://intranet.gov.bc.ca/bcws/provincial-programs/strategic-initiatives-and-innovation/wildfire-one/wildfire-one-training', + 'https://intranet.gov.bc.ca/bcws/corporate-governance/strategic-initiatives-and-innovation/wildfire-one/wildfire-one-training', '_blank', 'noopener' ); diff --git a/client/wfprev-war/src/main/angular/src/app/components/shared-layout/app-header/app-header.component.ts b/client/wfprev-war/src/main/angular/src/app/components/shared-layout/app-header/app-header.component.ts index 5abeded6d..992db0991 100644 --- a/client/wfprev-war/src/main/angular/src/app/components/shared-layout/app-header/app-header.component.ts +++ b/client/wfprev-war/src/main/angular/src/app/components/shared-layout/app-header/app-header.component.ts @@ -41,7 +41,7 @@ export class AppHeaderComponent { onSupportLinkClick() { //navigate to a support link page, upon decide which url would that be. - const url = 'https://intranet.gov.bc.ca/bcws/provincial-programs/strategic-initiatives-and-innovation/wildfire-one/wildfire-one-training' + const url = 'https://intranet.gov.bc.ca/bcws/corporate-governance/strategic-initiatives-and-innovation/wildfire-one/wildfire-one-training' window.open(url, '_blank', 'noopener'); } } diff --git a/client/wfprev-war/src/main/angular/src/app/utils/index.ts b/client/wfprev-war/src/main/angular/src/app/utils/index.ts index d3cbad714..cf16a93ee 100644 --- a/client/wfprev-war/src/main/angular/src/app/utils/index.ts +++ b/client/wfprev-war/src/main/angular/src/app/utils/index.ts @@ -7,7 +7,8 @@ export enum ResourcesRoutes { LANDING = '', MAP = 'map', LIST = 'list', - ERROR_PAGE = 'error-page' + ERROR_PAGE = 'error-page', + EDIT_PROJECT = 'edit-project' } export function appInitializerFactory(injector: Injector) { diff --git a/client/wfprev-war/src/main/angular/src/styles.scss b/client/wfprev-war/src/main/angular/src/styles.scss index f992e2a7e..dcd184434 100644 --- a/client/wfprev-war/src/main/angular/src/styles.scss +++ b/client/wfprev-war/src/main/angular/src/styles.scss @@ -46,14 +46,25 @@ body { width: fit-content; text-align: center; .mat-mdc-simple-snack-bar { - font-family: var(--wf-font-family-main); + font-family: var(--wf-font-family-main) !important; font-size: 13px; } .mat-button-wrapper { color: white; } - } + .mat-mdc-snack-bar-action { + color: white !important; + border: 1px solid white; + border-radius: 5px; + } +} + +html, body{ + font-family: var(--wf-font-family-main); + height: 100%; + overflow: hidden; /* Prevent scrolling if not needed */ +} .snackbar-error { background-color: #FF0000; color: var(--colour-white); diff --git a/client/wfprev-war/src/styles.scss b/client/wfprev-war/src/styles.scss deleted file mode 100644 index 90d4ee007..000000000 --- a/client/wfprev-war/src/styles.scss +++ /dev/null @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */