From 652bebc7a8ef83c2ad983158a04baf6e7f8a39cc Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 9 Dec 2024 12:12:44 +0000 Subject: [PATCH 01/72] Create new select-training-category page for mandatory training using directive --- ...and-manage-mandatory-training.component.ts | 2 +- .../add-mandatory-routing.module.ts | 10 + .../add-mandatory-training.module.ts | 4 + ...ining-category-mandatory.component.spec.ts | 177 ++++++++++++++++++ ...t-training-category-mandatory.component.ts | 52 +++++ .../select-training-category.directive.ts | 3 +- 6 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts create mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index 56cb36d6de..ca563a3bf7 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -78,7 +78,7 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { '/workplace', this.establishmentService.establishment.uid, 'add-and-manage-mandatory-training', - 'add-new-mandatory-training', + 'select-training-category', ]); } diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index 3f48735f74..8ffa0759a7 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -1,10 +1,12 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; +import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory/select-training-category-mandatory.component'; const routes: Routes = [ { @@ -15,6 +17,14 @@ const routes: Routes = [ component: AddAndManageMandatoryTrainingComponent, data: { title: 'List Mandatory Training' }, }, + { + path: 'select-training-category', + component: SelectTrainingCategoryMandatoryComponent, + data: { title: 'Select Training Category' }, + resolve: { + trainingCategories: TrainingCategoriesResolver, + }, + }, { path: 'remove-all-mandatory-training', component: RemoveAllMandatoryTrainingComponent, diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts index 8194e37cfe..449e4f3a39 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; +import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; import { AddMandatoryTrainingRoutingModule } from '@features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module'; import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; import { SharedModule } from '@shared/shared.module'; @@ -8,14 +9,17 @@ import { SharedModule } from '@shared/shared.module'; import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; +import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory/select-training-category-mandatory.component'; @NgModule({ imports: [CommonModule, AddMandatoryTrainingRoutingModule, ReactiveFormsModule, SharedModule], declarations: [ AddMandatoryTrainingComponent, + SelectTrainingCategoryMandatoryComponent, RemoveAllMandatoryTrainingComponent, AddAndManageMandatoryTrainingComponent, DeleteMandatoryTrainingCategoryComponent, ], + providers: [TrainingCategoriesResolver], }) export class AddMandatoryTrainingModule {} diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts new file mode 100644 index 0000000000..7f099a3354 --- /dev/null +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -0,0 +1,177 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { getTestBed } from '@angular/core/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Establishment } from '@core/model/establishment.model'; +import { BackLinkService } from '@core/services/backLink.service'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { TrainingService } from '@core/services/training.service'; +import { WindowRef } from '@core/services/window.ref'; +import { WorkerService } from '@core/services/worker.service'; +import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; +import { trainingCategories } from '@core/test-utils/MockTrainingCategoriesService'; +import { MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { MockWorkerService } from '@core/test-utils/MockWorkerService'; +import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; +import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; +import { SharedModule } from '@shared/shared.module'; +import { fireEvent, render } from '@testing-library/angular'; +import sinon from 'sinon'; + +import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; +import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory.component'; + +describe('SelectTrainingCategoryMandatoryComponent', () => { + async function setup() { + const establishment = establishmentBuilder() as Establishment; + const { fixture, getByText, getAllByText, getByTestId } = await render(SelectTrainingCategoryMandatoryComponent, { + imports: [HttpClientTestingModule, SharedModule, RouterModule, RouterTestingModule, AddMandatoryTrainingModule], + declarations: [GroupedRadioButtonAccordionComponent, RadioButtonAccordionComponent], + providers: [ + BackLinkService, + ErrorSummaryService, + WindowRef, + FormBuilder, + { + provide: WorkerService, + useClass: MockWorkerService, + }, + { + provide: TrainingService, + useClass: MockTrainingService, + }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + establishment: establishment, + trainingCategories: trainingCategories, + }, + parent: { + url: [{ path: 'select-staff' }], + }, + queryParamMap: { + get: sinon.stub(), + }, + }, + }, + }, + ], + }); + const component = fixture.componentInstance; + const injector = getTestBed(); + + const router = injector.inject(Router) as Router; + const trainingService = injector.inject(TrainingService) as TrainingService; + + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + const trainingServiceSpy = spyOn(trainingService, 'resetSelectedStaff').and.callThrough(); + + return { + component, + fixture, + getByText, + getAllByText, + getByTestId, + routerSpy, + trainingService, + trainingServiceSpy, + }; + } + + it('should create', async () => { + const { component } = await setup(); + expect(component).toBeTruthy(); + }); + + it('should show the page caption', async () => { + const { getByText } = await setup(); + + const caption = getByText('Add a mandatory training category'); + + expect(caption).toBeTruthy(); + }); + + it('should show the page heading', async () => { + const { getByText } = await setup(); + + const heading = getByText('Select the training category that you want to make mandatory'); + + expect(heading).toBeTruthy(); + }); + + it('should show the cancel link', async () => { + const { getByText } = await setup(); + + const cancelLink = getByText('Cancel'); + + expect(cancelLink).toBeTruthy(); + }); + + it('should reset selected staff in training service and navigate back to the add-and-manage-mandatory-training after clicking Cancel', async () => { + const { component, getByText, fixture, routerSpy, trainingServiceSpy } = await setup(); + + const cancelLink = getByText('Cancel'); + fireEvent.click(cancelLink); + fixture.detectChanges(); + + expect(trainingServiceSpy).toHaveBeenCalled(); + expect(routerSpy).toHaveBeenCalledWith([ + 'workplace', + component.establishmentUid, + 'add-and-manage-mandatory-training', + ]); + }); + + it('should show an accordion with the correct categories in', async () => { + const { component, getByTestId } = await setup(); + expect(component.categories).toEqual([ + { id: 1, seq: 10, category: 'Activity provision/Well-being', trainingCategoryGroup: 'Care skills and knowledge' }, + { id: 2, seq: 20, category: 'Autism', trainingCategoryGroup: 'Specific conditions and disabilities' }, + { id: 37, seq: 1, category: 'Other', trainingCategoryGroup: null }, + ]); + expect(getByTestId('groupedAccordion')).toBeTruthy(); + }); + + it('should call the training service and navigate to the select-job-roles page after selecting category and clicking continue', async () => { + const { component, getByText, routerSpy, trainingService } = await setup(); + + const trainingServiceSpy = spyOn(trainingService, 'setSelectedTrainingCategory'); + + const openAllLinkLink = getByText('Show all categories'); + fireEvent.click(openAllLinkLink); + + const autismCategory = getByText('Autism'); + fireEvent.click(autismCategory); + + const continueButton = getByText('Continue'); + fireEvent.click(continueButton); + + expect(trainingServiceSpy).toHaveBeenCalledWith({ + id: 2, + seq: 20, + category: 'Autism', + trainingCategoryGroup: 'Specific conditions and disabilities', + }); + expect(routerSpy).toHaveBeenCalledWith([ + 'workplace', + component.establishmentUid, + 'add-and-manage-mandatory-training', + 'select-job-roles', + ]); + }); + + it('should display required error message when no training category selected', async () => { + const { component, getByText, fixture, getAllByText } = await setup(); + + const continueButton = getByText('Continue'); + fireEvent.click(continueButton); + fixture.detectChanges(); + + expect(component.form.invalid).toBeTruthy(); + expect(getAllByText('Select the training category that you want to make mandatory').length).toEqual(3); + }); +}); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts new file mode 100644 index 0000000000..cb994aef1a --- /dev/null +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BackLinkService } from '@core/services/backLink.service'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { TrainingService } from '@core/services/training.service'; +import { WorkerService } from '@core/services/worker.service'; + +import { SelectTrainingCategoryDirective } from '../../../../shared/directives/select-training-category/select-training-category.directive'; + +@Component({ + selector: 'app-select-training-category-multiple', + templateUrl: '../../../../shared/directives/select-training-category/select-training-category.component.html', +}) +export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCategoryDirective { + constructor( + protected formBuilder: FormBuilder, + protected trainingService: TrainingService, + protected router: Router, + protected backLinkService: BackLinkService, + protected workerService: WorkerService, + protected route: ActivatedRoute, + protected errorSummaryService: ErrorSummaryService, + ) { + super(formBuilder, trainingService, router, backLinkService, workerService, route, errorSummaryService); + } + + public requiredErrorMessage: string = 'Select the training category that you want to make mandatory'; + + init(): void { + this.establishmentUid = this.route.snapshot.data.establishment.uid; + } + + protected setSectionHeading(): void { + this.section = 'Add a mandatory training category'; + } + + protected setTitle(): void { + this.title = 'Select the training category that you want to make mandatory'; + } + + public onCancel(event: Event) { + event.preventDefault(); + this.trainingService.resetState(); + this.router.navigate(['workplace', this.establishmentUid, 'add-and-manage-mandatory-training']); + } + + protected submit(selectedCategory): void { + this.trainingService.setSelectedTrainingCategory(selectedCategory); + this.router.navigate(['workplace', this.establishmentUid, 'add-and-manage-mandatory-training', 'select-job-roles']); + } +} diff --git a/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts b/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts index 5084312b65..04b1aef5fb 100644 --- a/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts +++ b/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts @@ -29,6 +29,7 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { public previousUrl: string[]; public preFilledId: number; public error = false; + public requiredErrorMessage: string = 'Select the training category'; private summaryText = { 'Care skills and knowledge': "'duty of care', 'safeguarding adults'", @@ -167,7 +168,7 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { type: [ { name: 'required', - message: 'Select the training category', + message: this.requiredErrorMessage, }, ], }, From 008a4ada82af5e25112949c3f4fd2e3cb6294c8a Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 9 Dec 2024 14:51:35 +0000 Subject: [PATCH 02/72] Refactor test to disregard matching page title and only look for message with error class --- ...lect-training-category-mandatory.component.spec.ts | 9 ++++++--- .../select-training-category.directive.ts | 11 ++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index 7f099a3354..fb6209a4c9 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -165,13 +165,16 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { }); it('should display required error message when no training category selected', async () => { - const { component, getByText, fixture, getAllByText } = await setup(); + const { fixture, getByText } = await setup(); const continueButton = getByText('Continue'); fireEvent.click(continueButton); fixture.detectChanges(); - expect(component.form.invalid).toBeTruthy(); - expect(getAllByText('Select the training category that you want to make mandatory').length).toEqual(3); + const errorMessage = getByText('Select the training category that you want to make mandatory', { + selector: '.govuk-error-message', + }); + + expect(errorMessage).toBeTruthy(); }); }); diff --git a/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts b/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts index 04b1aef5fb..f614a90eff 100644 --- a/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts +++ b/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts @@ -30,6 +30,7 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { public preFilledId: number; public error = false; public requiredErrorMessage: string = 'Select the training category'; + public submitButtonText: string = 'Continue'; private summaryText = { 'Care skills and knowledge': "'duty of care', 'safeguarding adults'", @@ -38,7 +39,6 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { 'Specific conditions and disabilities': "'dementia care', 'Oliver McGowan Mandatory Training'", 'Staff development': "'communication', 'leadership and management' ", }; - submitButtonText: string = 'Continue'; constructor( protected formBuilder: FormBuilder, @@ -62,6 +62,9 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { } protected init(): void {} + protected submit(selectedCategory: any): void {} + protected setSectionHeading(): void {} + public onCancel(event: Event) {} protected prefillForm(): void { let selectedCategory = this.trainingService.selectedTraining?.trainingCategory; @@ -78,14 +81,10 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { this.form.get('category').updateValueAndValidity(); } - protected submit(selectedCategory: any): void {} - protected setTitle(): void { this.title = 'Select the category that best matches the training taken'; } - protected setSectionHeading(): void {} - private getCategories(): void { this.categories = this.route.snapshot.data.trainingCategories; this.sortCategoriesByTrainingGroup(this.categories); @@ -142,8 +141,6 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { } } - public onCancel(event: Event) {} - public setBackLink(): void { this.backLinkService.showBackLink(); } From fa0b75e21f120f94a49931d39d61d50913325496 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 9 Dec 2024 15:35:24 +0000 Subject: [PATCH 03/72] Refactor tests --- ...ining-category-mandatory.component.spec.ts | 104 ++++++++++-------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index fb6209a4c9..564572b593 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -25,7 +25,8 @@ import { SelectTrainingCategoryMandatoryComponent } from './select-training-cate describe('SelectTrainingCategoryMandatoryComponent', () => { async function setup() { const establishment = establishmentBuilder() as Establishment; - const { fixture, getByText, getAllByText, getByTestId } = await render(SelectTrainingCategoryMandatoryComponent, { + + const setupTools = await render(SelectTrainingCategoryMandatoryComponent, { imports: [HttpClientTestingModule, SharedModule, RouterModule, RouterTestingModule, AddMandatoryTrainingModule], declarations: [GroupedRadioButtonAccordionComponent, RadioButtonAccordionComponent], providers: [ @@ -46,11 +47,8 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { useValue: { snapshot: { data: { - establishment: establishment, - trainingCategories: trainingCategories, - }, - parent: { - url: [{ path: 'select-staff' }], + establishment, + trainingCategories, }, queryParamMap: { get: sinon.stub(), @@ -60,7 +58,8 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { }, ], }); - const component = fixture.componentInstance; + + const component = setupTools.fixture.componentInstance; const injector = getTestBed(); const router = injector.inject(Router) as Router; @@ -68,17 +67,12 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); - const trainingServiceSpy = spyOn(trainingService, 'resetSelectedStaff').and.callThrough(); - return { + ...setupTools, component, - fixture, - getByText, - getAllByText, - getByTestId, routerSpy, trainingService, - trainingServiceSpy, + establishment, }; } @@ -103,43 +97,33 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { expect(heading).toBeTruthy(); }); - it('should show the cancel link', async () => { + it('should display the training category groups and descriptions in the accordion', async () => { const { getByText } = await setup(); - const cancelLink = getByText('Cancel'); - - expect(cancelLink).toBeTruthy(); - }); - - it('should reset selected staff in training service and navigate back to the add-and-manage-mandatory-training after clicking Cancel', async () => { - const { component, getByText, fixture, routerSpy, trainingServiceSpy } = await setup(); - - const cancelLink = getByText('Cancel'); - fireEvent.click(cancelLink); - fixture.detectChanges(); - - expect(trainingServiceSpy).toHaveBeenCalled(); - expect(routerSpy).toHaveBeenCalledWith([ - 'workplace', - component.establishmentUid, - 'add-and-manage-mandatory-training', - ]); - }); - - it('should show an accordion with the correct categories in', async () => { - const { component, getByTestId } = await setup(); - expect(component.categories).toEqual([ - { id: 1, seq: 10, category: 'Activity provision/Well-being', trainingCategoryGroup: 'Care skills and knowledge' }, - { id: 2, seq: 20, category: 'Autism', trainingCategoryGroup: 'Specific conditions and disabilities' }, - { id: 37, seq: 1, category: 'Other', trainingCategoryGroup: null }, - ]); - expect(getByTestId('groupedAccordion')).toBeTruthy(); + const trainingCategoryGroups = [ + { name: 'Care skills and knowledge', description: "Training like 'duty of care', 'safeguarding adults'" }, + { name: 'Health and safety in the workplace', description: "Training like 'fire safety', 'first aid'" }, + { + name: 'IT, digital and data in the workplace', + description: "Training like 'online safety and security', 'working with digital technology'", + }, + { + name: 'Specific conditions and disabilities', + description: "Training like 'dementia care', 'Oliver McGowan Mandatory Training'", + }, + { name: 'Staff development', description: "Training like 'communication', 'leadership and management'" }, + ]; + + trainingCategoryGroups.forEach((group) => { + expect(getByText(group.name)).toBeTruthy(); + expect(getByText(group.description)).toBeTruthy(); + }); }); - it('should call the training service and navigate to the select-job-roles page after selecting category and clicking continue', async () => { - const { component, getByText, routerSpy, trainingService } = await setup(); + it('should set the selected training category in the training service after selecting category and clicking continue', async () => { + const { getByText, trainingService } = await setup(); - const trainingServiceSpy = spyOn(trainingService, 'setSelectedTrainingCategory'); + const setSelectedTrainingCategorySpy = spyOn(trainingService, 'setSelectedTrainingCategory'); const openAllLinkLink = getByText('Show all categories'); fireEvent.click(openAllLinkLink); @@ -150,20 +134,44 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { const continueButton = getByText('Continue'); fireEvent.click(continueButton); - expect(trainingServiceSpy).toHaveBeenCalledWith({ + expect(setSelectedTrainingCategorySpy).toHaveBeenCalledWith({ id: 2, seq: 20, category: 'Autism', trainingCategoryGroup: 'Specific conditions and disabilities', }); + }); + + it('should navigate to the select-job-roles page after selecting category and clicking continue', async () => { + const { getByText, routerSpy, establishment } = await setup(); + + const openAllLinkLink = getByText('Show all categories'); + fireEvent.click(openAllLinkLink); + + const autismCategory = getByText('Autism'); + fireEvent.click(autismCategory); + + const continueButton = getByText('Continue'); + fireEvent.click(continueButton); + expect(routerSpy).toHaveBeenCalledWith([ 'workplace', - component.establishmentUid, + establishment.uid, 'add-and-manage-mandatory-training', 'select-job-roles', ]); }); + it('should navigate back to the add-and-manage-mandatory-training after clicking Cancel', async () => { + const { getByText, fixture, routerSpy, establishment } = await setup(); + + const cancelLink = getByText('Cancel'); + fireEvent.click(cancelLink); + fixture.detectChanges(); + + expect(routerSpy).toHaveBeenCalledWith(['workplace', establishment.uid, 'add-and-manage-mandatory-training']); + }); + it('should display required error message when no training category selected', async () => { const { fixture, getByText } = await setup(); From 18c2e2a4326d7ae01fbf3dd9501e625eb46907ab Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 11 Dec 2024 12:25:25 +0000 Subject: [PATCH 04/72] Hide other checkbox on mandatory training select training category page --- ...select-training-category.component.spec.ts | 16 ++++--- ...ining-category-mandatory.component.spec.ts | 7 +++ ...t-training-category-mandatory.component.ts | 1 + ...aining-category-multiple.component.spec.ts | 43 +++++++++++-------- .../select-training-category.component.html | 2 +- .../select-training-category.directive.ts | 1 + 6 files changed, 44 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-edit-training/select-training-category/select-training-category.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-edit-training/select-training-category/select-training-category.component.spec.ts index b7ee318c69..897fc8852f 100644 --- a/frontend/src/app/features/training-and-qualifications/add-edit-training/select-training-category/select-training-category.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-edit-training/select-training-category/select-training-category.component.spec.ts @@ -27,7 +27,7 @@ describe('SelectTrainingCategoryComponent', () => { const establishment = establishmentBuilder() as Establishment; const worker = workerBuilder(); - const { fixture, getByText, getAllByText, getByTestId } = await render(SelectTrainingCategoryComponent, { + const setupTools = await render(SelectTrainingCategoryComponent, { imports: [HttpClientTestingModule, SharedModule, RouterModule, RouterTestingModule, ReactiveFormsModule], declarations: [ SelectTrainingCategoryComponent, @@ -64,7 +64,7 @@ describe('SelectTrainingCategoryComponent', () => { }, ], }); - const component = fixture.componentInstance; + const component = setupTools.fixture.componentInstance; const injector = getTestBed(); const router = injector.inject(Router) as Router; @@ -75,11 +75,8 @@ describe('SelectTrainingCategoryComponent', () => { const trainingServiceSpy = spyOn(trainingService, 'resetSelectedStaff').and.callThrough(); return { + ...setupTools, component, - fixture, - getByText, - getAllByText, - getByTestId, routerSpy, trainingService, trainingServiceSpy, @@ -132,6 +129,13 @@ describe('SelectTrainingCategoryComponent', () => { expect(getByTestId('groupedAccordion')).toBeTruthy(); }); + it("should display 'The training is not in any of these categories' checkbox", async () => { + const { fixture, getByText } = await setup(); + + expect(getByText('The training is not in any of these categories')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('#otherCheckbox')).toBeTruthy(); + }); + it('should call the training service and navigate to the details page', async () => { const { component, getByText, routerSpy, trainingService } = await setup(true); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index 564572b593..f2e3e1d08d 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -185,4 +185,11 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { expect(errorMessage).toBeTruthy(); }); + + it("should not display 'The training is not in any of these categories' checkbox which is option on other 'Select training category' pages", async () => { + const { fixture, queryByText } = await setup(); + + expect(queryByText('The training is not in any of these categories')).toBeFalsy(); + expect(fixture.nativeElement.querySelector('#otherCheckbox')).toBeFalsy(); + }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index cb994aef1a..b79e91ffdb 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -26,6 +26,7 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate } public requiredErrorMessage: string = 'Select the training category that you want to make mandatory'; + public hideOtherCheckbox: boolean = true; init(): void { this.establishmentUid = this.route.snapshot.data.establishment.uid; diff --git a/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-training-category-multiple/select-training-category-multiple.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-training-category-multiple/select-training-category-multiple.component.spec.ts index 10d86daaa3..a8527cadd8 100644 --- a/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-training-category-multiple/select-training-category-multiple.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-training-category-multiple/select-training-category-multiple.component.spec.ts @@ -1,30 +1,31 @@ -import { fireEvent, render } from '@testing-library/angular'; -import { SelectTrainingCategoryMultipleComponent } from './select-training-category-multiple.component'; -import { getTestBed } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TrainingService } from '@core/services/training.service'; -import { MockTrainingService, MockTrainingServiceWithPreselectedStaff } from '@core/test-utils/MockTrainingService'; +import { getTestBed } from '@angular/core/testing'; +import { FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Establishment } from '@core/model/establishment.model'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; -import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; -import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; -import { SharedModule } from '@shared/shared.module'; -import { RouterTestingModule } from '@angular/router/testing'; +import { TrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; -import { FormBuilder } from '@angular/forms'; import { WorkerService } from '@core/services/worker.service'; -import { MockWorkerService } from '@core/test-utils/MockWorkerService'; -import { AddMultipleTrainingModule } from '../add-multiple-training.module'; import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; -import { Establishment } from '@core/model/establishment.model'; import { trainingCategories } from '@core/test-utils/MockTrainingCategoriesService'; +import { MockTrainingService, MockTrainingServiceWithPreselectedStaff } from '@core/test-utils/MockTrainingService'; +import { MockWorkerService } from '@core/test-utils/MockWorkerService'; +import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; +import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; +import { SharedModule } from '@shared/shared.module'; +import { fireEvent, render } from '@testing-library/angular'; import sinon from 'sinon'; +import { AddMultipleTrainingModule } from '../add-multiple-training.module'; +import { SelectTrainingCategoryMultipleComponent } from './select-training-category-multiple.component'; + describe('SelectTrainingCategoryMultipleComponent', () => { async function setup(prefill = false, accessedFromSummary = false, qsParamGetMock = sinon.stub()) { const establishment = establishmentBuilder() as Establishment; - const { fixture, getByText, getAllByText, getByTestId } = await render(SelectTrainingCategoryMultipleComponent, { + const setupTools = await render(SelectTrainingCategoryMultipleComponent, { imports: [HttpClientTestingModule, SharedModule, RouterModule, RouterTestingModule, AddMultipleTrainingModule], declarations: [GroupedRadioButtonAccordionComponent, RadioButtonAccordionComponent], providers: [ @@ -59,7 +60,7 @@ describe('SelectTrainingCategoryMultipleComponent', () => { }, ], }); - const component = fixture.componentInstance; + const component = setupTools.fixture.componentInstance; const injector = getTestBed(); const router = injector.inject(Router) as Router; @@ -70,11 +71,8 @@ describe('SelectTrainingCategoryMultipleComponent', () => { const trainingServiceSpy = spyOn(trainingService, 'resetSelectedStaff').and.callThrough(); return { + ...setupTools, component, - fixture, - getByText, - getAllByText, - getByTestId, routerSpy, trainingService, trainingServiceSpy, @@ -147,6 +145,13 @@ describe('SelectTrainingCategoryMultipleComponent', () => { expect(getByTestId('groupedAccordion')).toBeTruthy(); }); + it("should display 'The training is not in any of these categories' checkbox", async () => { + const { fixture, getByText } = await setup(); + + expect(getByText('The training is not in any of these categories')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('#otherCheckbox')).toBeTruthy(); + }); + it('should return to the select staff page if there is no selected staff', async () => { const { component, fixture, routerSpy } = await setup(); diff --git a/frontend/src/app/shared/directives/select-training-category/select-training-category.component.html b/frontend/src/app/shared/directives/select-training-category/select-training-category.component.html index 672d6de479..2fcc529fcc 100644 --- a/frontend/src/app/shared/directives/select-training-category/select-training-category.component.html +++ b/frontend/src/app/shared/directives/select-training-category/select-training-category.component.html @@ -19,7 +19,7 @@

{{ title }}

[errorMessage]="formErrorsMap[0].type[0].message" > -
+
Date: Wed, 11 Dec 2024 14:08:22 +0000 Subject: [PATCH 05/72] Increase spacing between accordion and submit buttons --- .../select-training-category.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/shared/directives/select-training-category/select-training-category.component.html b/frontend/src/app/shared/directives/select-training-category/select-training-category.component.html index 2fcc529fcc..e0e515eea1 100644 --- a/frontend/src/app/shared/directives/select-training-category/select-training-category.component.html +++ b/frontend/src/app/shared/directives/select-training-category/select-training-category.component.html @@ -40,7 +40,7 @@

{{ title }}

-
+
From fe50fa12f0e77b720db4068ecdca4e7cb22e827a Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 11 Dec 2024 14:40:25 +0000 Subject: [PATCH 06/72] Change name of following page to all-or-selected-job-roles --- .../select-training-category-mandatory.component.spec.ts | 4 ++-- .../select-training-category-mandatory.component.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index f2e3e1d08d..efc7e67f36 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -142,7 +142,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { }); }); - it('should navigate to the select-job-roles page after selecting category and clicking continue', async () => { + it('should navigate to the all-or-selected-job-roles page after selecting category and clicking continue', async () => { const { getByText, routerSpy, establishment } = await setup(); const openAllLinkLink = getByText('Show all categories'); @@ -158,7 +158,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { 'workplace', establishment.uid, 'add-and-manage-mandatory-training', - 'select-job-roles', + 'all-or-selected-job-roles', ]); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index b79e91ffdb..3493174482 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -48,6 +48,11 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate protected submit(selectedCategory): void { this.trainingService.setSelectedTrainingCategory(selectedCategory); - this.router.navigate(['workplace', this.establishmentUid, 'add-and-manage-mandatory-training', 'select-job-roles']); + this.router.navigate([ + 'workplace', + this.establishmentUid, + 'add-and-manage-mandatory-training', + 'all-or-selected-job-roles', + ]); } } From 292e26749cddef84feae951a42a1876451096595 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 11 Dec 2024 14:46:56 +0000 Subject: [PATCH 07/72] Set up skeleton of all-or-selected-job-roles page --- .../add-mandatory-routing.module.ts | 6 ++ .../add-mandatory-training.module.ts | 2 + .../all-or-selected-job-roles.component.html | 55 ++++++++++++ ...ll-or-selected-job-roles.component.spec.ts | 80 ++++++++++++++++++ .../all-or-selected-job-roles.component.ts | 84 +++++++++++++++++++ 5 files changed, 227 insertions(+) create mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html create mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts create mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index 8ffa0759a7..57c4013580 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -5,6 +5,7 @@ import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; +import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles/all-or-selected-job-roles.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory/select-training-category-mandatory.component'; @@ -25,6 +26,11 @@ const routes: Routes = [ trainingCategories: TrainingCategoriesResolver, }, }, + { + path: 'all-or-selected-job-roles', + component: AllOrSelectedJobRolesComponent, + data: { title: 'All or Selected Job Roles?' }, + }, { path: 'remove-all-mandatory-training', component: RemoveAllMandatoryTrainingComponent, diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts index 449e4f3a39..3eecd0b7f0 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts @@ -8,6 +8,7 @@ import { SharedModule } from '@shared/shared.module'; import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; +import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles/all-or-selected-job-roles.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory/select-training-category-mandatory.component'; @@ -19,6 +20,7 @@ import { SelectTrainingCategoryMandatoryComponent } from './select-training-cate RemoveAllMandatoryTrainingComponent, AddAndManageMandatoryTrainingComponent, DeleteMandatoryTrainingCategoryComponent, + AllOrSelectedJobRolesComponent, ], providers: [TrainingCategoriesResolver], }) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html new file mode 100644 index 0000000000..65ffeaffa1 --- /dev/null +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html @@ -0,0 +1,55 @@ + + +
+
+
+
+ + Add a mandatory training category +

Which job roles need this training?

+
+
+ +
+ + Error: {{ requiredErrorMessage }} + +
+ + +
+
+ + +
+ +
+ + Cancel +
+
+
+
+
diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts new file mode 100644 index 0000000000..5be27ca42f --- /dev/null +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -0,0 +1,80 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { getTestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TrainingService } from '@core/services/training.service'; +import { MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { SharedModule } from '@shared/shared.module'; +import { fireEvent, render } from '@testing-library/angular'; +import { Observable } from 'rxjs'; + +import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; +import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles.component'; + +describe('AllOrSelectedJobRolesComponent', () => { + async function setup() { + const setupTools = await render(AllOrSelectedJobRolesComponent, { + imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, AddMandatoryTrainingModule], + providers: [ + { provide: TrainingService, useClass: MockTrainingService }, + { + provide: ActivatedRoute, + useValue: { + params: Observable.from([{ establishmentuid: 'establishmentUid', id: 1 }]), + snapshot: { + data: { establishment: { uid: 'testuid' } }, + }, + }, + }, + ], + }); + + const component = setupTools.fixture.componentInstance; + const injector = getTestBed(); + const router = injector.inject(Router) as Router; + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + const trainingService = injector.inject(TrainingService) as TrainingService; + const clearSelectedTrainingCategorySpy = spyOn(trainingService, 'clearSelectedTrainingCategory').and.callThrough(); + + return { + ...setupTools, + component, + routerSpy, + clearSelectedTrainingCategorySpy, + }; + } + + it('should render component', async () => { + const { component } = await setup(); + expect(component).toBeTruthy(); + }); + + it('should show the page caption', async () => { + const { getByText } = await setup(); + + const caption = getByText('Add a mandatory training category'); + + expect(caption).toBeTruthy(); + }); + + it('should show the page heading', async () => { + const { getByText } = await setup(); + + const heading = getByText('Which job roles need this training?'); + + expect(heading).toBeTruthy(); + }); + + describe('Error messages', () => { + it('should display an error message if option not selected and Continue is clicked', async () => { + const { fixture, getByText, getAllByText } = await setup(); + + fireEvent.click(getByText('Continue')); + fixture.detectChanges(); + + expect( + getAllByText('Select whether this training is for all job roles or only selected job roles').length, + ).toEqual(2); + }); + }); +}); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts new file mode 100644 index 0000000000..f0c9ee0d1b --- /dev/null +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -0,0 +1,84 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ErrorDetails } from '@core/model/errorSummary.model'; +import { URLStructure } from '@core/model/url.model'; +import { BackLinkService } from '@core/services/backLink.service'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; + +@Component({ + selector: 'app-all-or-selected-job-roles', + templateUrl: './all-or-selected-job-roles.component.html', +}) +export class AllOrSelectedJobRolesComponent { + @ViewChild('formEl') formEl: ElementRef; + public form: UntypedFormGroup; + public submitted = false; + public formErrorsMap: ErrorDetails[] = []; + public returnTo: URLStructure; + public workplaceUid: string; + public requiredErrorMessage: string = 'Select whether this training is for all job roles or only selected job roles'; + + constructor( + private formBuilder: UntypedFormBuilder, + private router: Router, + private errorSummaryService: ErrorSummaryService, + private backLinkService: BackLinkService, + private route: ActivatedRoute, + ) { + this.setupForm(); + } + + ngOnInit() { + this.backLinkService.showBackLink(); + this.workplaceUid = this.route.snapshot.data?.establishment?.uid; + } + + ngAfterViewInit(): void { + this.errorSummaryService.formEl$.next(this.formEl); + } + + private navigateToNextPage() { + this.router.navigate(['']); + } + + public onSubmit(): void { + this.submitted = true; + this.errorSummaryService.syncFormErrorsEvent.next(true); + + if (this.form.valid) { + this.navigateToNextPage(); + } else { + this.errorSummaryService.scrollToErrorSummary(); + } + } + + public onCancel(event: Event): void { + event.preventDefault(); + + this.router.navigate(['workplace', this.workplaceUid, 'add-and-manage-mandatory-training']); + } + + private setupForm(): void { + this.submitted = false; + this.form = this.formBuilder.group({ + allOrSelectedJobRoles: [ + null, + { + validators: [Validators.required], + updateOn: 'submit', + }, + ], + }); + + this.formErrorsMap.push({ + item: 'allOrSelectedJobRoles', + type: [ + { + name: 'required', + message: this.requiredErrorMessage, + }, + ], + }); + } +} From ee954824e45048d66f0b723cfb6bb6091bc3cb8a Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 11 Dec 2024 17:10:32 +0000 Subject: [PATCH 08/72] Navigate back to select-training-category page if no category set in training service (create MockRouter to enable testing) --- .../src/app/core/test-utils/MockRouter.ts | 17 ++++++++ ...ll-or-selected-job-roles.component.spec.ts | 43 ++++++++++++++++--- .../all-or-selected-job-roles.component.ts | 14 +++++- 3 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/core/test-utils/MockRouter.ts diff --git a/frontend/src/app/core/test-utils/MockRouter.ts b/frontend/src/app/core/test-utils/MockRouter.ts new file mode 100644 index 0000000000..3951a9352d --- /dev/null +++ b/frontend/src/app/core/test-utils/MockRouter.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +@Injectable() +export class MockRouter extends Router { + public static factory(overrides: any = {}) { + return () => { + const service = new MockRouter(); + + Object.keys(overrides).forEach((overrideName) => { + service[overrideName] = overrides[overrideName]; + }); + + return service; + }; + } +} diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 5be27ca42f..ee87734f26 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -3,20 +3,40 @@ import { getTestBed } from '@angular/core/testing'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { TrainingService } from '@core/services/training.service'; -import { MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { MockRouter } from '@core/test-utils/MockRouter'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; +import userEvent from '@testing-library/user-event'; import { Observable } from 'rxjs'; import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles.component'; describe('AllOrSelectedJobRolesComponent', () => { - async function setup() { + async function setup(overrides: any = {}) { + const routerSpy = jasmine.createSpy('navigate').and.returnValue(Promise.resolve(true)); + const setupTools = await render(AllOrSelectedJobRolesComponent, { imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, AddMandatoryTrainingModule], providers: [ - { provide: TrainingService, useClass: MockTrainingService }, + { + provide: TrainingService, + useValue: { + selectedTraining: + overrides.selectedTraining !== undefined + ? overrides.selectedTraining + : { + trainingCategory: { + category: 'Activity provision, wellbeing', + id: 1, + seq: 0, + trainingCategoryGroup: 'Care skills and knowledge', + }, + }, + clearSelectedTrainingCategory: () => {}, + }, + }, + { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, { provide: ActivatedRoute, useValue: { @@ -31,8 +51,6 @@ describe('AllOrSelectedJobRolesComponent', () => { const component = setupTools.fixture.componentInstance; const injector = getTestBed(); - const router = injector.inject(Router) as Router; - const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); const trainingService = injector.inject(TrainingService) as TrainingService; const clearSelectedTrainingCategorySpy = spyOn(trainingService, 'clearSelectedTrainingCategory').and.callThrough(); @@ -65,6 +83,21 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(heading).toBeTruthy(); }); + it('should navigate back to the select training category page if no training category set in training service', async () => { + const { component, routerSpy } = await setup({ selectedTraining: null }); + + expect(routerSpy).toHaveBeenCalledWith(['../select-training-category'], { relativeTo: component.route }); + }); + + it('should navigate to the add-and-manage-mandatory-training page (relative route ../) if Cancel button is clicked', async () => { + const { component, getByText, routerSpy } = await setup({ selectedTraining: null }); + + const cancelButton = getByText('Cancel'); + userEvent.click(cancelButton); + + expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); + }); + describe('Error messages', () => { it('should display an error message if option not selected and Continue is clicked', async () => { const { fixture, getByText, getAllByText } = await setup(); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index f0c9ee0d1b..41b6356f09 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -2,9 +2,11 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; +import { SelectedTraining } from '@core/model/training.model'; import { URLStructure } from '@core/model/url.model'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { TrainingService } from '@core/services/training.service'; @Component({ selector: 'app-all-or-selected-job-roles', @@ -18,18 +20,26 @@ export class AllOrSelectedJobRolesComponent { public returnTo: URLStructure; public workplaceUid: string; public requiredErrorMessage: string = 'Select whether this training is for all job roles or only selected job roles'; + public selectedTrainingCategory: SelectedTraining; constructor( private formBuilder: UntypedFormBuilder, private router: Router, private errorSummaryService: ErrorSummaryService, private backLinkService: BackLinkService, - private route: ActivatedRoute, + public route: ActivatedRoute, + private trainingService: TrainingService, ) { this.setupForm(); } ngOnInit() { + this.selectedTrainingCategory = this.trainingService.selectedTraining; + + if (!this.selectedTrainingCategory) { + this.router.navigate(['../select-training-category'], { relativeTo: this.route }); + } + this.backLinkService.showBackLink(); this.workplaceUid = this.route.snapshot.data?.establishment?.uid; } @@ -56,7 +66,7 @@ export class AllOrSelectedJobRolesComponent { public onCancel(event: Event): void { event.preventDefault(); - this.router.navigate(['workplace', this.workplaceUid, 'add-and-manage-mandatory-training']); + this.router.navigate(['../'], { relativeTo: this.route }); } private setupForm(): void { From b28c6987158014cdf365a1a286e56689a16e7af9 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 11 Dec 2024 17:28:21 +0000 Subject: [PATCH 09/72] Display message below all job roles radio --- .../all-or-selected-job-roles.component.html | 4 ++++ ...ll-or-selected-job-roles.component.spec.ts | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html index 65ffeaffa1..a803edf203 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html @@ -26,6 +26,10 @@

Which job roles need this training?

/>
+
+ If you click Continue, '{{ selectedTrainingCategory?.trainingCategory?.category }}' will be mandatory for + everybody in your workplace. +
{ expect(caption).toBeTruthy(); }); + ['Activity provision, wellbeing', 'Digital leadership skills'].forEach((category) => { + it('should display mandatory for everybody message with selected training category when All job roles radio is clicked', async () => { + const selectedTraining = { + trainingCategory: { + category, + id: 1, + seq: 0, + trainingCategoryGroup: 'Care skills and knowledge', + }, + }; + + const { component, getByText, routerSpy } = await setup({ selectedTraining }); + + const expectedMessage = `If you click Continue, '${selectedTraining.trainingCategory.category}' will be mandatory for everybody in your workplace.`; + const allJobRolesRadio = getByText('All job roles'); + userEvent.click(allJobRolesRadio); + + expect(getByText(expectedMessage)).toBeTruthy(); + }); + }); + it('should show the page heading', async () => { const { getByText } = await setup(); From 9ceb91c52a82f11a22d71db36ac783d79579292f Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 11 Dec 2024 17:44:18 +0000 Subject: [PATCH 10/72] Only display mandatory for everybody message when All job roles radio is selected --- .../all-or-selected-job-roles.component.html | 4 +- ...ll-or-selected-job-roles.component.spec.ts | 66 +++++++++++++++---- .../all-or-selected-job-roles.component.ts | 7 +- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html index a803edf203..f1cb238365 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html @@ -23,10 +23,11 @@

Which job roles need this training?

value="allJobRoles" formControlName="allOrSelectedJobRoles" [class.govuk-input--error]="submitted && form.invalid" + (click)="selectRadio('allJobRoles')" />
-
+
If you click Continue, '{{ selectedTrainingCategory?.trainingCategory?.category }}' will be mandatory for everybody in your workplace.
@@ -38,6 +39,7 @@

Which job roles need this training?

type="radio" value="selectJobRoles" formControlName="allOrSelectedJobRoles" + (click)="selectRadio('selectJobRoles')" />
diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 799e50a402..0025daee11 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -75,24 +75,64 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(caption).toBeTruthy(); }); - ['Activity provision, wellbeing', 'Digital leadership skills'].forEach((category) => { - it('should display mandatory for everybody message with selected training category when All job roles radio is clicked', async () => { - const selectedTraining = { - trainingCategory: { - category, - id: 1, - seq: 0, - trainingCategoryGroup: 'Care skills and knowledge', - }, - }; + describe('Mandatory for everybody message', () => { + ['Activity provision, wellbeing', 'Digital leadership skills'].forEach((category) => { + it(`should display with selected training category (${category}) when All job roles radio is clicked`, async () => { + const selectedTraining = { + trainingCategory: { + category, + id: 1, + seq: 0, + trainingCategoryGroup: 'Care skills and knowledge', + }, + }; + + const { fixture, getByText } = await setup({ selectedTraining }); + + const expectedMessage = `If you click Continue, '${selectedTraining.trainingCategory.category}' will be mandatory for everybody in your workplace.`; + + const allJobRolesRadio = getByText('All job roles'); + userEvent.click(allJobRolesRadio); + fixture.detectChanges(); + + expect(getByText(expectedMessage)).toBeTruthy(); + }); + }); + + it('should not display on page load when no radio is selected', async () => { + const { queryByText } = await setup(); + + const mandatoryForEverybodyMessage = 'If you click Continue'; + + expect(queryByText(mandatoryForEverybodyMessage, { exact: false })).toBeFalsy(); + }); - const { component, getByText, routerSpy } = await setup({ selectedTraining }); + it('should not display after user clicks Only selected jobs radio', async () => { + const { fixture, getByText, queryByText } = await setup(); + + const mandatoryForEverybodyMessage = 'If you click Continue'; + + const selectedJobRolesRadio = getByText('Only selected job roles'); + userEvent.click(selectedJobRolesRadio); + fixture.detectChanges(); + + expect(queryByText(mandatoryForEverybodyMessage, { exact: false })).toBeFalsy(); + }); + + it('should stop displaying if user has clicked All job roles and then clicks Only selected jobs radio', async () => { + const { fixture, getByText, queryByText } = await setup(); + + const mandatoryForEverybodyMessage = 'If you click Continue'; - const expectedMessage = `If you click Continue, '${selectedTraining.trainingCategory.category}' will be mandatory for everybody in your workplace.`; const allJobRolesRadio = getByText('All job roles'); userEvent.click(allJobRolesRadio); + fixture.detectChanges(); + + const selectedJobRolesRadio = getByText('Only selected job roles'); + userEvent.click(selectedJobRolesRadio); + fixture.detectChanges(); - expect(getByText(expectedMessage)).toBeTruthy(); + expect(queryByText(mandatoryForEverybodyMessage, { exact: false })).toBeFalsy(); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index 41b6356f09..15e732e717 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -21,6 +21,7 @@ export class AllOrSelectedJobRolesComponent { public workplaceUid: string; public requiredErrorMessage: string = 'Select whether this training is for all job roles or only selected job roles'; public selectedTrainingCategory: SelectedTraining; + public selectedRadio: string = null; constructor( private formBuilder: UntypedFormBuilder, @@ -33,7 +34,7 @@ export class AllOrSelectedJobRolesComponent { this.setupForm(); } - ngOnInit() { + ngOnInit(): void { this.selectedTrainingCategory = this.trainingService.selectedTraining; if (!this.selectedTrainingCategory) { @@ -52,6 +53,10 @@ export class AllOrSelectedJobRolesComponent { this.router.navigate(['']); } + public selectRadio(selectedRadio: string): void { + this.selectedRadio = selectedRadio; + } + public onSubmit(): void { this.submitted = true; this.errorSummaryService.syncFormErrorsEvent.next(true); From 5948b838d05f915d6da52784612f7e36b2d17a7a Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 09:31:07 +0000 Subject: [PATCH 11/72] Clear state in training service on click of cancel --- ...ll-or-selected-job-roles.component.spec.ts | 49 ++++++++++++------- .../all-or-selected-job-roles.component.ts | 1 + 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 0025daee11..bd76259b02 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -33,7 +33,7 @@ describe('AllOrSelectedJobRolesComponent', () => { trainingCategoryGroup: 'Care skills and knowledge', }, }, - clearSelectedTrainingCategory: () => {}, + resetState: () => {}, }, }, { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, @@ -52,13 +52,13 @@ describe('AllOrSelectedJobRolesComponent', () => { const component = setupTools.fixture.componentInstance; const injector = getTestBed(); const trainingService = injector.inject(TrainingService) as TrainingService; - const clearSelectedTrainingCategorySpy = spyOn(trainingService, 'clearSelectedTrainingCategory').and.callThrough(); + const resetStateInTrainingServiceSpy = spyOn(trainingService, 'resetState').and.callThrough(); return { ...setupTools, component, routerSpy, - clearSelectedTrainingCategorySpy, + resetStateInTrainingServiceSpy, }; } @@ -75,6 +75,20 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(caption).toBeTruthy(); }); + it('should show the page heading', async () => { + const { getByText } = await setup(); + + const heading = getByText('Which job roles need this training?'); + + expect(heading).toBeTruthy(); + }); + + it('should navigate back to the select training category page if no training category set in training service', async () => { + const { component, routerSpy } = await setup({ selectedTraining: null }); + + expect(routerSpy).toHaveBeenCalledWith(['../select-training-category'], { relativeTo: component.route }); + }); + describe('Mandatory for everybody message', () => { ['Activity provision, wellbeing', 'Digital leadership skills'].forEach((category) => { it(`should display with selected training category (${category}) when All job roles radio is clicked`, async () => { @@ -136,27 +150,24 @@ describe('AllOrSelectedJobRolesComponent', () => { }); }); - it('should show the page heading', async () => { - const { getByText } = await setup(); + describe('Cancel button', () => { + it('should navigate to the add-and-manage-mandatory-training page (relative route ../) when clicked', async () => { + const { component, getByText, routerSpy } = await setup({ selectedTraining: null }); - const heading = getByText('Which job roles need this training?'); + const cancelButton = getByText('Cancel'); + userEvent.click(cancelButton); - expect(heading).toBeTruthy(); - }); - - it('should navigate back to the select training category page if no training category set in training service', async () => { - const { component, routerSpy } = await setup({ selectedTraining: null }); - - expect(routerSpy).toHaveBeenCalledWith(['../select-training-category'], { relativeTo: component.route }); - }); + expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); + }); - it('should navigate to the add-and-manage-mandatory-training page (relative route ../) if Cancel button is clicked', async () => { - const { component, getByText, routerSpy } = await setup({ selectedTraining: null }); + it('should clear state in training service when clicked', async () => { + const { getByText, resetStateInTrainingServiceSpy } = await setup({ selectedTraining: null }); - const cancelButton = getByText('Cancel'); - userEvent.click(cancelButton); + const cancelButton = getByText('Cancel'); + userEvent.click(cancelButton); - expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); + expect(resetStateInTrainingServiceSpy).toHaveBeenCalled(); + }); }); describe('Error messages', () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index 15e732e717..68f55a376e 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -71,6 +71,7 @@ export class AllOrSelectedJobRolesComponent { public onCancel(event: Event): void { event.preventDefault(); + this.trainingService.resetState(); this.router.navigate(['../'], { relativeTo: this.route }); } From 19ffa48743cb667c2e2b5addaeecfaed96b65cf5 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 09:54:28 +0000 Subject: [PATCH 12/72] Navigate to correct mandatory training pages depending on selected option --- ...ll-or-selected-job-roles.component.spec.ts | 26 +++++++++++++++++++ .../all-or-selected-job-roles.component.ts | 12 ++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index bd76259b02..82e271c73e 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -182,4 +182,30 @@ describe('AllOrSelectedJobRolesComponent', () => { ).toEqual(2); }); }); + + describe('On submit', () => { + it("should navigate back to add-and-manage-mandatory-training main page when user submits with 'All job roles' selected", async () => { + const { component, fixture, getByText, routerSpy } = await setup(); + + fireEvent.click(getByText('All job roles')); + fixture.detectChanges(); + + fireEvent.click(getByText('Continue')); + fixture.detectChanges(); + + expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); + }); + + it("should navigate to select-job-roles page when user submits with 'Only selected job roles' selected", async () => { + const { component, fixture, getByText, routerSpy } = await setup(); + + fireEvent.click(getByText('Only selected job roles')); + fixture.detectChanges(); + + fireEvent.click(getByText('Continue')); + fixture.detectChanges(); + + expect(routerSpy).toHaveBeenCalledWith(['../', 'select-job-roles'], { relativeTo: component.route }); + }); + }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index 68f55a376e..51c4649e50 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -49,8 +49,12 @@ export class AllOrSelectedJobRolesComponent { this.errorSummaryService.formEl$.next(this.formEl); } - private navigateToNextPage() { - this.router.navigate(['']); + private navigateToSelectJobRolesPage(): void { + this.router.navigate(['../', 'select-job-roles'], { relativeTo: this.route }); + } + + private navigateBackToAddMandatoryTrainingPage(): void { + this.router.navigate(['../'], { relativeTo: this.route }); } public selectRadio(selectedRadio: string): void { @@ -62,7 +66,9 @@ export class AllOrSelectedJobRolesComponent { this.errorSummaryService.syncFormErrorsEvent.next(true); if (this.form.valid) { - this.navigateToNextPage(); + this.selectedRadio == 'allJobRoles' + ? this.navigateBackToAddMandatoryTrainingPage() + : this.navigateToSelectJobRolesPage(); } else { this.errorSummaryService.scrollToErrorSummary(); } From a459445bbdc89c3eb445feb83ce69caad3a978ff Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 10:07:23 +0000 Subject: [PATCH 13/72] Add success banner when user submits with all job roles --- ...ll-or-selected-job-roles.component.spec.ts | 23 +++++++++++++++++++ .../all-or-selected-job-roles.component.ts | 14 ++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 82e271c73e..b15e49230e 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -2,7 +2,9 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { getTestBed } from '@angular/core/testing'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { AlertService } from '@core/services/alert.service'; import { TrainingService } from '@core/services/training.service'; +import { WindowRef } from '@core/services/window.ref'; import { MockRouter } from '@core/test-utils/MockRouter'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; @@ -46,19 +48,25 @@ describe('AllOrSelectedJobRolesComponent', () => { }, }, }, + AlertService, + WindowRef, ], }); const component = setupTools.fixture.componentInstance; const injector = getTestBed(); + const trainingService = injector.inject(TrainingService) as TrainingService; const resetStateInTrainingServiceSpy = spyOn(trainingService, 'resetState').and.callThrough(); + const alertService = injector.inject(AlertService) as AlertService; + const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); return { ...setupTools, component, routerSpy, resetStateInTrainingServiceSpy, + alertSpy, }; } @@ -196,6 +204,21 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); }); + it("should display 'Mandatory training category added' banner when 'All job roles' selected", async () => { + const { fixture, getByText, alertSpy } = await setup(); + + fireEvent.click(getByText('All job roles')); + fixture.detectChanges(); + + fireEvent.click(getByText('Continue')); + fixture.detectChanges(); + + expect(alertSpy).toHaveBeenCalledWith({ + type: 'success', + message: 'Mandatory training category added', + }); + }); + it("should navigate to select-job-roles page when user submits with 'Only selected job roles' selected", async () => { const { component, fixture, getByText, routerSpy } = await setup(); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index 51c4649e50..f59e55ab18 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; import { SelectedTraining } from '@core/model/training.model'; import { URLStructure } from '@core/model/url.model'; +import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; import { TrainingService } from '@core/services/training.service'; @@ -30,6 +31,7 @@ export class AllOrSelectedJobRolesComponent { private backLinkService: BackLinkService, public route: ActivatedRoute, private trainingService: TrainingService, + private alertService: AlertService, ) { this.setupForm(); } @@ -66,9 +68,15 @@ export class AllOrSelectedJobRolesComponent { this.errorSummaryService.syncFormErrorsEvent.next(true); if (this.form.valid) { - this.selectedRadio == 'allJobRoles' - ? this.navigateBackToAddMandatoryTrainingPage() - : this.navigateToSelectJobRolesPage(); + if (this.selectedRadio == 'allJobRoles') { + this.navigateBackToAddMandatoryTrainingPage(); + this.alertService.addAlert({ + type: 'success', + message: 'Mandatory training category added', + }); + } else { + this.navigateToSelectJobRolesPage(); + } } else { this.errorSummaryService.scrollToErrorSummary(); } From c176da19ef7e92a5fb1f61205fb5627130ea94a7 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 10:33:14 +0000 Subject: [PATCH 14/72] Reset state in training service when all job roles submitted --- ...ll-or-selected-job-roles.component.spec.ts | 44 ++++++++++++------- .../all-or-selected-job-roles.component.ts | 2 + 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index b15e49230e..716a1f48ae 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -192,30 +192,40 @@ describe('AllOrSelectedJobRolesComponent', () => { }); describe('On submit', () => { - it("should navigate back to add-and-manage-mandatory-training main page when user submits with 'All job roles' selected", async () => { - const { component, fixture, getByText, routerSpy } = await setup(); + describe("when 'All job roles selected'", () => { + const selectAllJobRolesAndSubmit = (fixture, getByText) => { + fireEvent.click(getByText('All job roles')); + fixture.detectChanges(); - fireEvent.click(getByText('All job roles')); - fixture.detectChanges(); + fireEvent.click(getByText('Continue')); + fixture.detectChanges(); + }; - fireEvent.click(getByText('Continue')); - fixture.detectChanges(); + it('should navigate back to add-and-manage-mandatory-training main page', async () => { + const { component, fixture, getByText, routerSpy } = await setup(); - expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); - }); + selectAllJobRolesAndSubmit(fixture, getByText); - it("should display 'Mandatory training category added' banner when 'All job roles' selected", async () => { - const { fixture, getByText, alertSpy } = await setup(); + expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); + }); - fireEvent.click(getByText('All job roles')); - fixture.detectChanges(); + it("should display 'Mandatory training category added' banner", async () => { + const { fixture, getByText, alertSpy } = await setup(); - fireEvent.click(getByText('Continue')); - fixture.detectChanges(); + selectAllJobRolesAndSubmit(fixture, getByText); + + expect(alertSpy).toHaveBeenCalledWith({ + type: 'success', + message: 'Mandatory training category added', + }); + }); + + it('should clear state in training service', async () => { + const { fixture, getByText, resetStateInTrainingServiceSpy } = await setup(); + + selectAllJobRolesAndSubmit(fixture, getByText); - expect(alertSpy).toHaveBeenCalledWith({ - type: 'success', - message: 'Mandatory training category added', + expect(resetStateInTrainingServiceSpy).toHaveBeenCalled(); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index f59e55ab18..23b18aa0a0 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -70,6 +70,8 @@ export class AllOrSelectedJobRolesComponent { if (this.form.valid) { if (this.selectedRadio == 'allJobRoles') { this.navigateBackToAddMandatoryTrainingPage(); + this.trainingService.resetState(); + this.alertService.addAlert({ type: 'success', message: 'Mandatory training category added', From ded907428631e8b49ef2d9d0171f97e4c995b6da Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 11:15:15 +0000 Subject: [PATCH 15/72] Set up backend call to create mandatory training when all job roles submitted --- ...ll-or-selected-job-roles.component.spec.ts | 50 ++++++++++++++++++- .../all-or-selected-job-roles.component.ts | 44 +++++++++++++--- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 716a1f48ae..27c1579788 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -3,8 +3,10 @@ import { getTestBed } from '@angular/core/testing'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { AlertService } from '@core/services/alert.service'; +import { EstablishmentService } from '@core/services/establishment.service'; import { TrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; +import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { MockRouter } from '@core/test-utils/MockRouter'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; @@ -16,6 +18,7 @@ import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles.comp describe('AllOrSelectedJobRolesComponent', () => { async function setup(overrides: any = {}) { + const establishment = establishmentBuilder(); const routerSpy = jasmine.createSpy('navigate').and.returnValue(Promise.resolve(true)); const setupTools = await render(AllOrSelectedJobRolesComponent, { @@ -39,12 +42,15 @@ describe('AllOrSelectedJobRolesComponent', () => { }, }, { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, + { provide: EstablishmentService, useClass: MockEstablishmentService }, { provide: ActivatedRoute, useValue: { - params: Observable.from([{ establishmentuid: 'establishmentUid', id: 1 }]), + params: Observable.from([{ establishmentuid: establishment.uid }]), snapshot: { - data: { establishment: { uid: 'testuid' } }, + parent: { + data: { establishment }, + }, }, }, }, @@ -61,12 +67,21 @@ describe('AllOrSelectedJobRolesComponent', () => { const alertService = injector.inject(AlertService) as AlertService; const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); + + const establishmentService = injector.inject(EstablishmentService) as EstablishmentService; + const createAndUpdateMandatoryTrainingSpy = spyOn( + establishmentService, + 'createAndUpdateMandatoryTraining', + ).and.callThrough(); + return { ...setupTools, component, routerSpy, resetStateInTrainingServiceSpy, alertSpy, + createAndUpdateMandatoryTrainingSpy, + establishment, }; } @@ -201,6 +216,37 @@ describe('AllOrSelectedJobRolesComponent', () => { fixture.detectChanges(); }; + it('should call createAndUpdateMandatoryTraining with training category in service and allJobRoles true', async () => { + const selectedTraining = { + trainingCategory: { + category: 'Activity provision, wellbeing', + id: 3, + seq: 0, + trainingCategoryGroup: 'Care skills and knowledge', + }, + }; + + const { fixture, getByText, establishment, createAndUpdateMandatoryTrainingSpy } = await setup({ + selectedTraining, + }); + + selectAllJobRolesAndSubmit(fixture, getByText); + + expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { + trainingCategoryId: selectedTraining.trainingCategory.id, + allJobRoles: true, + jobs: [], + }); + }); + + it('should navigate back to add-and-manage-mandatory-training main page', async () => { + const { component, fixture, getByText, routerSpy } = await setup(); + + selectAllJobRolesAndSubmit(fixture, getByText); + + expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); + }); + it('should navigate back to add-and-manage-mandatory-training main page', async () => { const { component, fixture, getByText, routerSpy } = await setup(); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index 23b18aa0a0..a3d41bbfcd 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -2,12 +2,15 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; +import { Establishment } from '@core/model/establishment.model'; import { SelectedTraining } from '@core/model/training.model'; import { URLStructure } from '@core/model/url.model'; import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { EstablishmentService } from '@core/services/establishment.service'; import { TrainingService } from '@core/services/training.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-all-or-selected-job-roles', @@ -23,6 +26,8 @@ export class AllOrSelectedJobRolesComponent { public requiredErrorMessage: string = 'Select whether this training is for all job roles or only selected job roles'; public selectedTrainingCategory: SelectedTraining; public selectedRadio: string = null; + private subscriptions: Subscription = new Subscription(); + private establishment: Establishment; constructor( private formBuilder: UntypedFormBuilder, @@ -32,11 +37,13 @@ export class AllOrSelectedJobRolesComponent { public route: ActivatedRoute, private trainingService: TrainingService, private alertService: AlertService, + private establishmentService: EstablishmentService, ) { this.setupForm(); } ngOnInit(): void { + this.establishment = this.route.snapshot.parent?.data?.establishment; this.selectedTrainingCategory = this.trainingService.selectedTraining; if (!this.selectedTrainingCategory) { @@ -69,13 +76,7 @@ export class AllOrSelectedJobRolesComponent { if (this.form.valid) { if (this.selectedRadio == 'allJobRoles') { - this.navigateBackToAddMandatoryTrainingPage(); - this.trainingService.resetState(); - - this.alertService.addAlert({ - type: 'success', - message: 'Mandatory training category added', - }); + this.createMandatoryTraining(); } else { this.navigateToSelectJobRolesPage(); } @@ -84,6 +85,31 @@ export class AllOrSelectedJobRolesComponent { } } + private createMandatoryTraining(): void { + const props = { + trainingCategoryId: this.selectedTrainingCategory.trainingCategory.id, + allJobRoles: true, + jobs: [], + }; + + this.subscriptions.add( + this.establishmentService.createAndUpdateMandatoryTraining(this.establishment.uid, props).subscribe( + () => { + this.navigateBackToAddMandatoryTrainingPage(); + this.trainingService.resetState(); + + this.alertService.addAlert({ + type: 'success', + message: 'Mandatory training category added', + }); + }, + (error) => { + console.log(error); + }, + ), + ); + } + public onCancel(event: Event): void { event.preventDefault(); @@ -113,4 +139,8 @@ export class AllOrSelectedJobRolesComponent { ], }); } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } } From 0cc8353d02fa3cb0358ec0333e10fdc93e39b5ce Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 11:37:29 +0000 Subject: [PATCH 16/72] Display server error message if call to backend fails --- .../all-or-selected-job-roles.component.html | 7 ++++++- .../all-or-selected-job-roles.component.spec.ts | 14 +++++++++++++- .../all-or-selected-job-roles.component.ts | 5 +++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html index f1cb238365..d2a46aacc1 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html @@ -1,4 +1,9 @@ - +
diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 27c1579788..7b851fc036 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -11,7 +11,7 @@ import { MockRouter } from '@core/test-utils/MockRouter'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; -import { Observable } from 'rxjs'; +import { Observable, throwError } from 'rxjs'; import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles.component'; @@ -273,6 +273,18 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(resetStateInTrainingServiceSpy).toHaveBeenCalled(); }); + + it('should display server error message if call to backend fails', async () => { + const { fixture, getByText, createAndUpdateMandatoryTrainingSpy } = await setup(); + + createAndUpdateMandatoryTrainingSpy.and.returnValue(throwError(() => new Error('Unexpected error'))); + + selectAllJobRolesAndSubmit(fixture, getByText); + + const expectedErrorMessage = 'There has been a problem saving your mandatory training. Please try again.'; + + expect(getByText(expectedErrorMessage)).toBeTruthy(); + }); }); it("should navigate to select-job-roles page when user submits with 'Only selected job roles' selected", async () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index a3d41bbfcd..d4ee7323ba 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -26,6 +26,7 @@ export class AllOrSelectedJobRolesComponent { public requiredErrorMessage: string = 'Select whether this training is for all job roles or only selected job roles'; public selectedTrainingCategory: SelectedTraining; public selectedRadio: string = null; + public serverError: string; private subscriptions: Subscription = new Subscription(); private establishment: Establishment; @@ -103,8 +104,8 @@ export class AllOrSelectedJobRolesComponent { message: 'Mandatory training category added', }); }, - (error) => { - console.log(error); + () => { + this.serverError = 'There has been a problem saving your mandatory training. Please try again.'; }, ), ); From 1d1d5302418e681105303dae7217ac2c8ca60d46 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 12:31:08 +0000 Subject: [PATCH 17/72] Update styling of conditional and added red line when error --- .../all-or-selected-job-roles.component.html | 92 ++++++++++--------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html index d2a46aacc1..0f8828cc44 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html @@ -14,53 +14,55 @@

Which job roles need this training?

- -
- - Error: {{ requiredErrorMessage }} - -
- - -
-
- If you click Continue, '{{ selectedTrainingCategory?.trainingCategory?.category }}' will be mandatory for - everybody in your workplace. -
-
- - -
- -
- - Cancel +
+
+ + Error: {{ requiredErrorMessage }} + +
+ + +
+
+

+ If you click Continue, '{{ selectedTrainingCategory?.trainingCategory?.category }}' will be mandatory for + everybody in your workplace. +

+
+
+ + +
+
+ + Cancel +
From 7abfffc60566bf81e73f2c00076da814233d7a5f Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 15:35:11 +0000 Subject: [PATCH 18/72] Update select training category page to filter out categories which already have mandatory training --- .../add-mandatory-routing.module.ts | 2 + .../add-mandatory-training.module.ts | 3 +- ...ining-category-mandatory.component.spec.ts | 45 +++++++++++++++++-- ...t-training-category-mandatory.component.ts | 19 ++++++++ .../select-training-category.directive.ts | 4 +- 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index 57c4013580..e63b709a9e 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { MandatoryTrainingCategoriesResolver } from '@core/resolvers/mandatory-training-categories.resolver'; import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; @@ -24,6 +25,7 @@ const routes: Routes = [ data: { title: 'Select Training Category' }, resolve: { trainingCategories: TrainingCategoriesResolver, + existingMandatoryTraining: MandatoryTrainingCategoriesResolver, }, }, { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts index 3eecd0b7f0..a12f80628d 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; +import { MandatoryTrainingCategoriesResolver } from '@core/resolvers/mandatory-training-categories.resolver'; import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; import { AddMandatoryTrainingRoutingModule } from '@features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module'; import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; @@ -22,6 +23,6 @@ import { SelectTrainingCategoryMandatoryComponent } from './select-training-cate DeleteMandatoryTrainingCategoryComponent, AllOrSelectedJobRolesComponent, ], - providers: [TrainingCategoriesResolver], + providers: [TrainingCategoriesResolver, MandatoryTrainingCategoriesResolver], }) export class AddMandatoryTrainingModule {} diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index efc7e67f36..76e9cc8aa9 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -11,7 +11,7 @@ import { WindowRef } from '@core/services/window.ref'; import { WorkerService } from '@core/services/worker.service'; import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; import { trainingCategories } from '@core/test-utils/MockTrainingCategoriesService'; -import { MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { MockTrainingService, MockTrainingServiceWithPreselectedStaff } from '@core/test-utils/MockTrainingService'; import { MockWorkerService } from '@core/test-utils/MockWorkerService'; import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; @@ -23,7 +23,7 @@ import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory.component'; describe('SelectTrainingCategoryMandatoryComponent', () => { - async function setup() { + async function setup(overrides: any = {}) { const establishment = establishmentBuilder() as Establishment; const setupTools = await render(SelectTrainingCategoryMandatoryComponent, { @@ -40,7 +40,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { }, { provide: TrainingService, - useClass: MockTrainingService, + useClass: overrides.prefill ? MockTrainingServiceWithPreselectedStaff : MockTrainingService, }, { provide: ActivatedRoute, @@ -48,7 +48,8 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { snapshot: { data: { establishment, - trainingCategories, + trainingCategories: overrides.trainingCategories ?? trainingCategories, + existingMandatoryTraining: overrides.existingMandatoryTraining ?? {}, }, queryParamMap: { get: sinon.stub(), @@ -192,4 +193,40 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { expect(queryByText('The training is not in any of these categories')).toBeFalsy(); expect(fixture.nativeElement.querySelector('#otherCheckbox')).toBeFalsy(); }); + + it('should prefill the training category radio when already selected category in training service', async () => { + const { component } = await setup({ prefill: true }); + + expect(component.form.value).toEqual({ category: 1 }); + }); + + it('should not include training categories which already have mandatory training', async () => { + const mockTrainingCategories = [ + { id: 1, seq: 10, category: 'Activity provision/Well-being', trainingCategoryGroup: 'Care skills and knowledge' }, + { id: 2, seq: 20, category: 'Autism', trainingCategoryGroup: 'Specific conditions and disabilities' }, + { id: 3, seq: 20, category: 'Continence care', trainingCategoryGroup: 'Care skills and knowledge' }, + ]; + + const existingMandatoryTraining = { + mandatoryTraining: [ + { + category: mockTrainingCategories[2].category, + establishmentId: 4090, + jobs: [{}, {}], + trainingCategoryId: mockTrainingCategories[2].id, + }, + ], + }; + + const overrides = { + trainingCategories: mockTrainingCategories, + existingMandatoryTraining, + }; + + const { queryByText } = await setup(overrides); + + expect(queryByText(mockTrainingCategories[0].category)).toBeTruthy(); + expect(queryByText(mockTrainingCategories[1].category)).toBeTruthy(); + expect(queryByText(mockTrainingCategories[2].category)).toBeFalsy(); + }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index 3493174482..535ade0cfb 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -40,6 +40,25 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate this.title = 'Select the training category that you want to make mandatory'; } + protected getCategories(): void { + const allTrainingCategories = this.route.snapshot.data.trainingCategories; + const existingMandatoryTraining = this.route.snapshot.data.existingMandatoryTraining; + + const trainingCategoryIdsWithExistingMandatoryTraining = existingMandatoryTraining?.mandatoryTraining?.map( + (existingMandatoryTrainings) => existingMandatoryTrainings.trainingCategoryId, + ); + + if (trainingCategoryIdsWithExistingMandatoryTraining?.length) { + this.categories = allTrainingCategories.filter( + (category) => !trainingCategoryIdsWithExistingMandatoryTraining.includes(category.id), + ); + } else { + this.categories = allTrainingCategories; + } + + this.sortCategoriesByTrainingGroup(this.categories); + } + public onCancel(event: Event) { event.preventDefault(); this.trainingService.resetState(); diff --git a/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts b/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts index 15fec2d65e..28dec47bd9 100644 --- a/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts +++ b/frontend/src/app/shared/directives/select-training-category/select-training-category.directive.ts @@ -86,13 +86,13 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { this.title = 'Select the category that best matches the training taken'; } - private getCategories(): void { + protected getCategories(): void { this.categories = this.route.snapshot.data.trainingCategories; this.sortCategoriesByTrainingGroup(this.categories); this.otherCategory = this.categories.filter((category) => category.trainingCategoryGroup === null)[0]; } - private sortCategoriesByTrainingGroup(trainingCategories) { + protected sortCategoriesByTrainingGroup(trainingCategories) { this.trainingGroups = []; for (const group of Object.keys(this.summaryText)) { let currentTrainingGroup = { From 10b1497c3698b079994255daa52bd531c0430f35 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 15:58:42 +0000 Subject: [PATCH 19/72] Updated all job roles mandatory message to use hidden class rather than ngIf --- .../all-or-selected-job-roles.component.html | 5 ++++- ...ll-or-selected-job-roles.component.spec.ts | 21 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html index 0f8828cc44..dba87a6b6f 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html @@ -32,7 +32,10 @@

Which job roles need this training?

/>
-
+

If you click Continue, '{{ selectedTrainingCategory?.trainingCategory?.category }}' will be mandatory for everybody in your workplace. diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 7b851fc036..85d7fa38a2 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -132,35 +132,36 @@ describe('AllOrSelectedJobRolesComponent', () => { userEvent.click(allJobRolesRadio); fixture.detectChanges(); - expect(getByText(expectedMessage)).toBeTruthy(); + const mandatoryForEverybodyMessage = getByText(expectedMessage); + + expect(mandatoryForEverybodyMessage).toBeTruthy(); + expect(mandatoryForEverybodyMessage.parentElement).not.toHaveClass('govuk-radios__conditional--hidden'); }); }); it('should not display on page load when no radio is selected', async () => { const { queryByText } = await setup(); - const mandatoryForEverybodyMessage = 'If you click Continue'; + const mandatoryForEverybodyMessage = queryByText('If you click Continue', { exact: false }); - expect(queryByText(mandatoryForEverybodyMessage, { exact: false })).toBeFalsy(); + expect(mandatoryForEverybodyMessage.parentElement).toHaveClass('govuk-radios__conditional--hidden'); }); it('should not display after user clicks Only selected jobs radio', async () => { const { fixture, getByText, queryByText } = await setup(); - const mandatoryForEverybodyMessage = 'If you click Continue'; - const selectedJobRolesRadio = getByText('Only selected job roles'); userEvent.click(selectedJobRolesRadio); fixture.detectChanges(); - expect(queryByText(mandatoryForEverybodyMessage, { exact: false })).toBeFalsy(); + const mandatoryForEverybodyMessage = queryByText('If you click Continue', { exact: false }); + + expect(mandatoryForEverybodyMessage.parentElement).toHaveClass('govuk-radios__conditional--hidden'); }); it('should stop displaying if user has clicked All job roles and then clicks Only selected jobs radio', async () => { const { fixture, getByText, queryByText } = await setup(); - const mandatoryForEverybodyMessage = 'If you click Continue'; - const allJobRolesRadio = getByText('All job roles'); userEvent.click(allJobRolesRadio); fixture.detectChanges(); @@ -169,7 +170,9 @@ describe('AllOrSelectedJobRolesComponent', () => { userEvent.click(selectedJobRolesRadio); fixture.detectChanges(); - expect(queryByText(mandatoryForEverybodyMessage, { exact: false })).toBeFalsy(); + const mandatoryForEverybodyMessage = queryByText('If you click Continue', { exact: false }); + + expect(mandatoryForEverybodyMessage.parentElement).toHaveClass('govuk-radios__conditional--hidden'); }); }); From 2b5c6552e47a518d7071daa8b7c43914b77001ae Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 16:18:03 +0000 Subject: [PATCH 20/72] Update selector --- .../select-training-category-mandatory.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index 535ade0cfb..ce297ff91a 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -9,7 +9,7 @@ import { WorkerService } from '@core/services/worker.service'; import { SelectTrainingCategoryDirective } from '../../../../shared/directives/select-training-category/select-training-category.directive'; @Component({ - selector: 'app-select-training-category-multiple', + selector: 'app-select-training-category-mandatory', templateUrl: '../../../../shared/directives/select-training-category/select-training-category.component.html', }) export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCategoryDirective { From f595555f4704ab024fd64af53e710d64fa23eed7 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 13 Dec 2024 11:55:11 +0000 Subject: [PATCH 21/72] Stop error message from wrapping --- .../all-or-selected-job-roles.component.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html index dba87a6b6f..5ce2d356e0 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.html @@ -16,7 +16,12 @@

Which job roles need this training?

- + Error: {{ requiredErrorMessage }}
From 49efb80440889288d1aa6d08f6dec6f9cf76ea93 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 13 Dec 2024 11:55:53 +0000 Subject: [PATCH 22/72] Remove duplicate test --- .../all-or-selected-job-roles.component.spec.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 85d7fa38a2..f4bbb3e454 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -250,14 +250,6 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); }); - it('should navigate back to add-and-manage-mandatory-training main page', async () => { - const { component, fixture, getByText, routerSpy } = await setup(); - - selectAllJobRolesAndSubmit(fixture, getByText); - - expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); - }); - it("should display 'Mandatory training category added' banner", async () => { const { fixture, getByText, alertSpy } = await setup(); From ccd39e405646b6e73fb7ff297714c51c7dcd4ed2 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 13 Dec 2024 12:48:30 +0000 Subject: [PATCH 23/72] Reduce nesting in spec file and remove incorrect setting of selected training to null in Cancel button tests --- ...ll-or-selected-job-roles.component.spec.ts | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index f4bbb3e454..733bd38f21 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -19,6 +19,14 @@ import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles.comp describe('AllOrSelectedJobRolesComponent', () => { async function setup(overrides: any = {}) { const establishment = establishmentBuilder(); + const selectedTraining = { + trainingCategory: { + category: 'Activity provision, wellbeing', + id: 1, + seq: 0, + trainingCategoryGroup: 'Care skills and knowledge', + }, + }; const routerSpy = jasmine.createSpy('navigate').and.returnValue(Promise.resolve(true)); const setupTools = await render(AllOrSelectedJobRolesComponent, { @@ -27,17 +35,7 @@ describe('AllOrSelectedJobRolesComponent', () => { { provide: TrainingService, useValue: { - selectedTraining: - overrides.selectedTraining !== undefined - ? overrides.selectedTraining - : { - trainingCategory: { - category: 'Activity provision, wellbeing', - id: 1, - seq: 0, - trainingCategoryGroup: 'Care skills and knowledge', - }, - }, + selectedTraining: overrides.selectedTraining !== undefined ? overrides.selectedTraining : selectedTraining, resetState: () => {}, }, }, @@ -82,6 +80,7 @@ describe('AllOrSelectedJobRolesComponent', () => { alertSpy, createAndUpdateMandatoryTrainingSpy, establishment, + selectedTraining, }; } @@ -115,16 +114,7 @@ describe('AllOrSelectedJobRolesComponent', () => { describe('Mandatory for everybody message', () => { ['Activity provision, wellbeing', 'Digital leadership skills'].forEach((category) => { it(`should display with selected training category (${category}) when All job roles radio is clicked`, async () => { - const selectedTraining = { - trainingCategory: { - category, - id: 1, - seq: 0, - trainingCategoryGroup: 'Care skills and knowledge', - }, - }; - - const { fixture, getByText } = await setup({ selectedTraining }); + const { fixture, getByText, selectedTraining } = await setup(); const expectedMessage = `If you click Continue, '${selectedTraining.trainingCategory.category}' will be mandatory for everybody in your workplace.`; @@ -178,7 +168,7 @@ describe('AllOrSelectedJobRolesComponent', () => { describe('Cancel button', () => { it('should navigate to the add-and-manage-mandatory-training page (relative route ../) when clicked', async () => { - const { component, getByText, routerSpy } = await setup({ selectedTraining: null }); + const { component, getByText, routerSpy } = await setup(); const cancelButton = getByText('Cancel'); userEvent.click(cancelButton); @@ -187,7 +177,7 @@ describe('AllOrSelectedJobRolesComponent', () => { }); it('should clear state in training service when clicked', async () => { - const { getByText, resetStateInTrainingServiceSpy } = await setup({ selectedTraining: null }); + const { getByText, resetStateInTrainingServiceSpy } = await setup(); const cancelButton = getByText('Cancel'); userEvent.click(cancelButton); From 56b533b4c64385be10ed505633b2ccb8de198d2e Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 17:13:38 +0000 Subject: [PATCH 24/72] Set up skeleton of select-job-roles page for mandatory training --- .../add-mandatory-routing.module.ts | 8 + .../add-mandatory-training.module.ts | 2 + .../select-job-roles-mandatory.component.html | 55 ++++++ ...lect-job-roles-mandatory.component.spec.ts | 163 ++++++++++++++++++ .../select-job-roles-mandatory.component.ts | 59 +++++++ 5 files changed, 287 insertions(+) create mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html create mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts create mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index e63b709a9e..9880b487bd 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { JobsResolver } from '@core/resolvers/jobs.resolver'; import { MandatoryTrainingCategoriesResolver } from '@core/resolvers/mandatory-training-categories.resolver'; import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; @@ -8,6 +9,7 @@ import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandato import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles/all-or-selected-job-roles.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; +import { SelectJobRolesMandatoryComponent } from './select-job-roles-mandatory/select-job-roles-mandatory.component'; import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory/select-training-category-mandatory.component'; const routes: Routes = [ @@ -33,6 +35,12 @@ const routes: Routes = [ component: AllOrSelectedJobRolesComponent, data: { title: 'All or Selected Job Roles?' }, }, + { + path: 'select-job-roles', + component: SelectJobRolesMandatoryComponent, + data: { title: 'Select Job Roles' }, + resolve: { jobs: JobsResolver }, + }, { path: 'remove-all-mandatory-training', component: RemoveAllMandatoryTrainingComponent, diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts index a12f80628d..ca1626ce5e 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts @@ -11,6 +11,7 @@ import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandato import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles/all-or-selected-job-roles.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; +import { SelectJobRolesMandatoryComponent } from './select-job-roles-mandatory/select-job-roles-mandatory.component'; import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory/select-training-category-mandatory.component'; @NgModule({ @@ -22,6 +23,7 @@ import { SelectTrainingCategoryMandatoryComponent } from './select-training-cate AddAndManageMandatoryTrainingComponent, DeleteMandatoryTrainingCategoryComponent, AllOrSelectedJobRolesComponent, + SelectJobRolesMandatoryComponent, ], providers: [TrainingCategoriesResolver, MandatoryTrainingCategoriesResolver], }) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html new file mode 100644 index 0000000000..580116d637 --- /dev/null +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html @@ -0,0 +1,55 @@ +
+
+
+
+
+ + Add a mandatory training category +

Select the job roles which need this training

+
+
+ +
+ +

+ Error: {{ errorMessageOnEmptyInput }} +

+ + +
+ + +
+
+
+
+
+
+
+ +
+ + Cancel +
+
+
diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts new file mode 100644 index 0000000000..fef5ffa281 --- /dev/null +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -0,0 +1,163 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { getTestBed } from '@angular/core/testing'; +import { FormBuilder } from '@angular/forms'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Establishment } from '@core/model/establishment.model'; +import { BackLinkService } from '@core/services/backLink.service'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { TrainingService } from '@core/services/training.service'; +import { WindowRef } from '@core/services/window.ref'; +import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; +import { MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; +import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; +import { SharedModule } from '@shared/shared.module'; +import { render } from '@testing-library/angular'; +import userEvent from '@testing-library/user-event'; + +import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; +import { SelectJobRolesMandatoryComponent } from './select-job-roles-mandatory.component'; + +describe('SelectJobRolesMandatoryComponent', () => { + const mockAvailableJobs = [ + { + id: 4, + title: 'Allied health professional (not occupational therapist)', + jobRoleGroup: 'Professional and related roles', + }, + { + id: 10, + title: 'Care worker', + jobRoleGroup: 'Care providing roles', + }, + { + id: 23, + title: 'Registered nurse', + jobRoleGroup: 'Professional and related roles', + }, + { + id: 27, + title: 'Social worker', + jobRoleGroup: 'Professional and related roles', + }, + { + id: 20, + title: 'Other (directly involved in providing care)', + jobRoleGroup: 'Care providing roles', + }, + ]; + + async function setup() { + const establishment = establishmentBuilder() as Establishment; + + const setupTools = await render(SelectJobRolesMandatoryComponent, { + imports: [HttpClientTestingModule, SharedModule, RouterModule, RouterTestingModule, AddMandatoryTrainingModule], + declarations: [GroupedRadioButtonAccordionComponent, RadioButtonAccordionComponent], + providers: [ + BackLinkService, + ErrorSummaryService, + WindowRef, + FormBuilder, + { + provide: TrainingService, + useClass: MockTrainingService, + }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + establishment, + jobs: mockAvailableJobs, + }, + }, + }, + }, + ], + }); + + const component = setupTools.fixture.componentInstance; + + const injector = getTestBed(); + + const router = injector.inject(Router) as Router; + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + const trainingService = injector.inject(TrainingService) as TrainingService; + const resetStateInTrainingServiceSpy = spyOn(trainingService, 'resetState').and.callThrough(); + + return { + ...setupTools, + component, + routerSpy, + resetStateInTrainingServiceSpy, + }; + } + + it('should create', async () => { + const { component } = await setup(); + expect(component).toBeTruthy(); + }); + + it('should show the page caption', async () => { + const { getByText } = await setup(); + + const caption = getByText('Add a mandatory training category'); + + expect(caption).toBeTruthy(); + }); + + it('should show the page heading', async () => { + const { getByText } = await setup(); + + const heading = getByText('Select the job roles which need this training'); + + expect(heading).toBeTruthy(); + }); + + describe('Accordion', () => { + it('should render an accordion for job role selection', async () => { + const { getByTestId, getByText } = await setup(); + + expect(getByTestId('selectJobRolesAccordion')).toBeTruthy(); + expect(getByText('Show all job roles')).toBeTruthy(); + }); + + it('should render an accordion section for each job role group', async () => { + const { getByText } = await setup(); + + expect(getByText('Care providing roles')).toBeTruthy(); + expect(getByText('Professional and related roles')).toBeTruthy(); + }); + + it('should render a checkbox for each job role', async () => { + const { getByRole } = await setup(); + + mockAvailableJobs.forEach((job) => { + const checkbox = getByRole('checkbox', { name: job.title }); + expect(checkbox).toBeTruthy(); + }); + }); + }); + + describe('On click of Cancel button', () => { + it('should return to the add-and-manage-mandatory-training page', async () => { + const { component, getByText, routerSpy } = await setup(); + + const cancelButton = getByText('Cancel'); + userEvent.click(cancelButton); + + expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); + }); + + it('should clear state in training service', async () => { + const { getByText, resetStateInTrainingServiceSpy } = await setup(); + + const cancelButton = getByText('Cancel'); + userEvent.click(cancelButton); + + expect(resetStateInTrainingServiceSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts new file mode 100644 index 0000000000..4af7c48228 --- /dev/null +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Job, JobGroup } from '@core/model/job.model'; +import { JobService } from '@core/services/job.service'; +import { TrainingService } from '@core/services/training.service'; +import { CustomValidators } from '@shared/validators/custom-form-validators'; + +@Component({ + selector: 'app-select-job-roles-mandatory', + templateUrl: './select-job-roles-mandatory.component.html', +}) +export class SelectJobRolesMandatoryComponent { + constructor( + private formBuilder: UntypedFormBuilder, + private trainingService: TrainingService, + private router: Router, + public route: ActivatedRoute, + ) {} + + public form: UntypedFormGroup; + public jobGroups: JobGroup[] = []; + public jobsAvailable: Job[] = []; + public submitted: boolean; + public selectedJobIds: number[] = []; + + ngOnInit(): void { + this.getJobs(); + this.setupForm(); + } + + private getJobs(): void { + this.jobsAvailable = this.route.snapshot.data.jobs; + this.jobGroups = JobService.sortJobsByJobGroup(this.jobsAvailable); + } + + private setupForm(): void { + this.form = this.formBuilder.group({ + selectedJobRoles: [[], { validators: CustomValidators.validateArrayNotEmpty(), updateOn: 'submit' }], + }); + } + + public onCheckboxClick(target: HTMLInputElement) { + const jobId = Number(target.value); + + if (this.selectedJobIds.includes(jobId)) { + this.selectedJobIds = this.selectedJobIds.filter((id) => id !== jobId); + } else { + this.selectedJobIds = [...this.selectedJobIds, jobId]; + } + } + + public onCancel(event: Event): void { + event.preventDefault(); + + this.trainingService.resetState(); + this.router.navigate(['../'], { relativeTo: this.route }); + } +} From 34235365a7de3bc43237705d54c8e8937273f6b4 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 12 Dec 2024 17:42:28 +0000 Subject: [PATCH 25/72] Add error message for when user submits with no checkboxes ticked --- .../select-job-roles-mandatory.component.html | 28 +++++----- ...lect-job-roles-mandatory.component.spec.ts | 53 ++++++++++++++++++- .../select-job-roles-mandatory.component.ts | 37 ++++++++++++- 3 files changed, 103 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html index 580116d637..163a22fd2a 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html @@ -1,6 +1,8 @@ + +
-
+
@@ -38,18 +40,18 @@

Select the job roles which need this trainin

-
-
- - Cancel -
+
+ + Cancel +
+
diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index fef5ffa281..78eaeb7cd1 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -13,7 +13,7 @@ import { MockTrainingService } from '@core/test-utils/MockTrainingService'; import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; import { SharedModule } from '@shared/shared.module'; -import { render } from '@testing-library/angular'; +import { render, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; @@ -141,6 +141,57 @@ describe('SelectJobRolesMandatoryComponent', () => { }); }); + describe('Errors', () => { + const expectedErrorMessage = 'Select the job roles that need this training'; + const errorSummaryBoxHeading = 'There is a problem'; + + it('should display an error message on submit if no job roles are selected', async () => { + const { fixture, getByRole, getByText, getByTestId } = await setup(); + + userEvent.click(getByRole('button', { name: 'Save mandatory training' })); + fixture.detectChanges(); + + const accordion = getByTestId('selectJobRolesAccordion'); + expect(within(accordion).getByText(expectedErrorMessage)).toBeTruthy(); + + const thereIsAProblemMessage = getByText(errorSummaryBoxHeading); + + const errorSummaryBox = thereIsAProblemMessage.parentElement; + expect(within(errorSummaryBox).getByText(expectedErrorMessage)).toBeTruthy(); + }); + + it('should expand the whole accordion on error', async () => { + const { fixture, getByRole, getByText } = await setup(); + + userEvent.click(getByRole('button', { name: 'Save mandatory training' })); + fixture.detectChanges(); + + expect(getByText('Hide all job roles')).toBeTruthy(); + }); + + it('should continue to display error messages after empty submit and then user selects job roles', async () => { + const { fixture, getByRole, getByText } = await setup(); + + userEvent.click(getByRole('button', { name: 'Save mandatory training' })); + fixture.detectChanges(); + + const errorSummaryBox = getByText(errorSummaryBoxHeading).parentElement; + + expect(errorSummaryBox).toBeTruthy(); + expect(within(errorSummaryBox).getByText(expectedErrorMessage)).toBeTruthy(); + + userEvent.click(getByText('Care worker')); + userEvent.click(getByText('Registered nurse')); + + fixture.detectChanges(); + + const errorSummaryBoxStillThere = getByText(errorSummaryBoxHeading).parentElement; + + expect(errorSummaryBoxStillThere).toBeTruthy(); + expect(within(errorSummaryBoxStillThere).getByText(expectedErrorMessage)).toBeTruthy(); + }); + }); + describe('On click of Cancel button', () => { it('should return to the add-and-manage-mandatory-training page', async () => { const { component, getByText, routerSpy } = await setup(); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 4af7c48228..c74ec29b66 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -1,9 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ElementRef, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { ErrorDetails } from '@core/model/errorSummary.model'; import { Job, JobGroup } from '@core/model/job.model'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; import { JobService } from '@core/services/job.service'; import { TrainingService } from '@core/services/training.service'; +import { AccordionGroupComponent } from '@shared/components/accordions/generic-accordion/accordion-group/accordion-group.component'; import { CustomValidators } from '@shared/validators/custom-form-validators'; @Component({ @@ -15,14 +18,20 @@ export class SelectJobRolesMandatoryComponent { private formBuilder: UntypedFormBuilder, private trainingService: TrainingService, private router: Router, + private errorSummaryService: ErrorSummaryService, public route: ActivatedRoute, ) {} + @ViewChild('accordion') accordion: AccordionGroupComponent; + @ViewChild('formEl') formEl: ElementRef; + public form: UntypedFormGroup; public jobGroups: JobGroup[] = []; public jobsAvailable: Job[] = []; public submitted: boolean; public selectedJobIds: number[] = []; + public errorMessageOnEmptyInput: string = 'Select the job roles that need this training'; + public formErrorsMap: Array = []; ngOnInit(): void { this.getJobs(); @@ -38,6 +47,18 @@ export class SelectJobRolesMandatoryComponent { this.form = this.formBuilder.group({ selectedJobRoles: [[], { validators: CustomValidators.validateArrayNotEmpty(), updateOn: 'submit' }], }); + + this.formErrorsMap = [ + { + item: 'selectedJobRoles', + type: [ + { + name: 'selectedNone', + message: this.errorMessageOnEmptyInput, + }, + ], + }, + ]; } public onCheckboxClick(target: HTMLInputElement) { @@ -50,10 +71,24 @@ export class SelectJobRolesMandatoryComponent { } } + public onSubmit(): void { + this.submitted = true; + + this.form.get('selectedJobRoles').setValue(this.selectedJobIds); + + if (this.form.invalid) { + this.accordion.showAll(); + } + } + public onCancel(event: Event): void { event.preventDefault(); this.trainingService.resetState(); this.router.navigate(['../'], { relativeTo: this.route }); } + + ngAfterViewInit() { + this.errorSummaryService.formEl$.next(this.formEl); + } } From ff207cd1c565836b4460b191ac1a1cc8ad2beba7 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 13 Dec 2024 09:25:43 +0000 Subject: [PATCH 26/72] Add navigation back to main mandatory training page and banner on submit --- ...lect-job-roles-mandatory.component.spec.ts | 43 +++++++++++++++++++ .../select-job-roles-mandatory.component.ts | 18 ++++++++ 2 files changed, 61 insertions(+) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index 78eaeb7cd1..bf20febd2d 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -4,6 +4,7 @@ import { FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { Establishment } from '@core/model/establishment.model'; +import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; import { TrainingService } from '@core/services/training.service'; @@ -57,6 +58,7 @@ describe('SelectJobRolesMandatoryComponent', () => { providers: [ BackLinkService, ErrorSummaryService, + AlertService, WindowRef, FormBuilder, { @@ -84,6 +86,9 @@ describe('SelectJobRolesMandatoryComponent', () => { const router = injector.inject(Router) as Router; const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + const alertService = injector.inject(AlertService) as AlertService; + const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); + const trainingService = injector.inject(TrainingService) as TrainingService; const resetStateInTrainingServiceSpy = spyOn(trainingService, 'resetState').and.callThrough(); @@ -92,6 +97,7 @@ describe('SelectJobRolesMandatoryComponent', () => { component, routerSpy, resetStateInTrainingServiceSpy, + alertSpy, }; } @@ -141,6 +147,43 @@ describe('SelectJobRolesMandatoryComponent', () => { }); }); + describe('On submit', () => { + const selectJobRolesAndSave = (getByText) => { + userEvent.click(getByText('Show all job roles')); + userEvent.click(getByText('Registered nurse')); + userEvent.click(getByText('Social worker')); + + userEvent.click(getByText('Save mandatory training')); + }; + + it('should navigate back to add-and-manage-mandatory-training main page', async () => { + const { component, getByText, routerSpy } = await setup(); + + selectJobRolesAndSave(getByText); + + expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); + }); + + it("should display 'Mandatory training category added' banner", async () => { + const { getByText, alertSpy } = await setup(); + + selectJobRolesAndSave(getByText); + + expect(alertSpy).toHaveBeenCalledWith({ + type: 'success', + message: 'Mandatory training category added', + }); + }); + + it('should clear state in training service', async () => { + const { getByText, resetStateInTrainingServiceSpy } = await setup(); + + selectJobRolesAndSave(getByText); + + expect(resetStateInTrainingServiceSpy).toHaveBeenCalled(); + }); + }); + describe('Errors', () => { const expectedErrorMessage = 'Select the job roles that need this training'; const errorSummaryBoxHeading = 'There is a problem'; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index c74ec29b66..327d4ef30b 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -3,6 +3,8 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; import { Job, JobGroup } from '@core/model/job.model'; +import { AlertService } from '@core/services/alert.service'; +import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; import { JobService } from '@core/services/job.service'; import { TrainingService } from '@core/services/training.service'; @@ -19,6 +21,8 @@ export class SelectJobRolesMandatoryComponent { private trainingService: TrainingService, private router: Router, private errorSummaryService: ErrorSummaryService, + private backLinkService: BackLinkService, + private alertService: AlertService, public route: ActivatedRoute, ) {} @@ -36,6 +40,7 @@ export class SelectJobRolesMandatoryComponent { ngOnInit(): void { this.getJobs(); this.setupForm(); + this.backLinkService.showBackLink(); } private getJobs(): void { @@ -78,13 +83,26 @@ export class SelectJobRolesMandatoryComponent { if (this.form.invalid) { this.accordion.showAll(); + this.errorSummaryService.scrollToErrorSummary(); + return; } + + this.navigateBackToAddMandatoryTrainingPage(); + this.alertService.addAlert({ + type: 'success', + message: 'Mandatory training category added', + }); + this.trainingService.resetState(); } public onCancel(event: Event): void { event.preventDefault(); this.trainingService.resetState(); + this.navigateBackToAddMandatoryTrainingPage(); + } + + private navigateBackToAddMandatoryTrainingPage(): void { this.router.navigate(['../'], { relativeTo: this.route }); } From 8e3356dad8e48b2a42932520f4f2bce0fbb287b2 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 13 Dec 2024 09:40:57 +0000 Subject: [PATCH 27/72] Navigate back to select training category page if no training set in service --- ...lect-job-roles-mandatory.component.spec.ts | 30 +++++++++++++++---- .../select-job-roles-mandatory.component.ts | 10 ++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index bf20febd2d..e81354afd3 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -10,7 +10,7 @@ import { ErrorSummaryService } from '@core/services/error-summary.service'; import { TrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; -import { MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { MockRouter } from '@core/test-utils/MockRouter'; import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; import { SharedModule } from '@shared/shared.module'; @@ -49,8 +49,9 @@ describe('SelectJobRolesMandatoryComponent', () => { }, ]; - async function setup() { + async function setup(overrides: any = {}) { const establishment = establishmentBuilder() as Establishment; + const routerSpy = jasmine.createSpy('navigate').and.returnValue(Promise.resolve(true)); const setupTools = await render(SelectJobRolesMandatoryComponent, { imports: [HttpClientTestingModule, SharedModule, RouterModule, RouterTestingModule, AddMandatoryTrainingModule], @@ -61,9 +62,23 @@ describe('SelectJobRolesMandatoryComponent', () => { AlertService, WindowRef, FormBuilder, + { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, { provide: TrainingService, - useClass: MockTrainingService, + useValue: { + selectedTraining: + overrides.selectedTraining !== undefined + ? overrides.selectedTraining + : { + trainingCategory: { + category: 'Activity provision, wellbeing', + id: 1, + seq: 0, + trainingCategoryGroup: 'Care skills and knowledge', + }, + }, + resetState: () => {}, + }, }, { provide: ActivatedRoute, @@ -83,9 +98,6 @@ describe('SelectJobRolesMandatoryComponent', () => { const injector = getTestBed(); - const router = injector.inject(Router) as Router; - const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); - const alertService = injector.inject(AlertService) as AlertService; const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); @@ -122,6 +134,12 @@ describe('SelectJobRolesMandatoryComponent', () => { expect(heading).toBeTruthy(); }); + it('should navigate back to the select training category page if no training category set in training service', async () => { + const { component, routerSpy } = await setup({ selectedTraining: null }); + + expect(routerSpy).toHaveBeenCalledWith(['../select-training-category'], { relativeTo: component.route }); + }); + describe('Accordion', () => { it('should render an accordion for job role selection', async () => { const { getByTestId, getByText } = await setup(); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 327d4ef30b..0f08d19448 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -3,6 +3,7 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; import { Job, JobGroup } from '@core/model/job.model'; +import { SelectedTraining } from '@core/model/training.model'; import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; @@ -36,11 +37,18 @@ export class SelectJobRolesMandatoryComponent { public selectedJobIds: number[] = []; public errorMessageOnEmptyInput: string = 'Select the job roles that need this training'; public formErrorsMap: Array = []; + private selectedTrainingCategory: SelectedTraining; ngOnInit(): void { + this.selectedTrainingCategory = this.trainingService.selectedTraining; + + if (!this.selectedTrainingCategory) { + this.router.navigate(['../select-training-category'], { relativeTo: this.route }); + } + + this.backLinkService.showBackLink(); this.getJobs(); this.setupForm(); - this.backLinkService.showBackLink(); } private getJobs(): void { From 602cc1c24e495668ca668ffd5ca043f783a4b637 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 13 Dec 2024 12:36:42 +0000 Subject: [PATCH 28/72] Call backend on submit --- ...lect-job-roles-mandatory.component.spec.ts | 56 ++++++++++++++----- .../select-job-roles-mandatory.component.ts | 45 ++++++++++++--- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index e81354afd3..3057f01643 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -7,9 +7,10 @@ import { Establishment } from '@core/model/establishment.model'; import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { EstablishmentService } from '@core/services/establishment.service'; import { TrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; -import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; +import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { MockRouter } from '@core/test-utils/MockRouter'; import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; @@ -52,6 +53,14 @@ describe('SelectJobRolesMandatoryComponent', () => { async function setup(overrides: any = {}) { const establishment = establishmentBuilder() as Establishment; const routerSpy = jasmine.createSpy('navigate').and.returnValue(Promise.resolve(true)); + const selectedTraining = { + trainingCategory: { + category: 'Activity provision, wellbeing', + id: 1, + seq: 0, + trainingCategoryGroup: 'Care skills and knowledge', + }, + }; const setupTools = await render(SelectJobRolesMandatoryComponent, { imports: [HttpClientTestingModule, SharedModule, RouterModule, RouterTestingModule, AddMandatoryTrainingModule], @@ -63,20 +72,11 @@ describe('SelectJobRolesMandatoryComponent', () => { WindowRef, FormBuilder, { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, + { provide: EstablishmentService, useClass: MockEstablishmentService }, { provide: TrainingService, useValue: { - selectedTraining: - overrides.selectedTraining !== undefined - ? overrides.selectedTraining - : { - trainingCategory: { - category: 'Activity provision, wellbeing', - id: 1, - seq: 0, - trainingCategoryGroup: 'Care skills and knowledge', - }, - }, + selectedTraining: overrides.selectedTraining !== undefined ? overrides.selectedTraining : selectedTraining, resetState: () => {}, }, }, @@ -104,12 +104,21 @@ describe('SelectJobRolesMandatoryComponent', () => { const trainingService = injector.inject(TrainingService) as TrainingService; const resetStateInTrainingServiceSpy = spyOn(trainingService, 'resetState').and.callThrough(); + const establishmentService = injector.inject(EstablishmentService) as EstablishmentService; + const createAndUpdateMandatoryTrainingSpy = spyOn( + establishmentService, + 'createAndUpdateMandatoryTraining', + ).and.callThrough(); + return { ...setupTools, component, routerSpy, resetStateInTrainingServiceSpy, alertSpy, + selectedTraining, + createAndUpdateMandatoryTrainingSpy, + establishment, }; } @@ -166,10 +175,10 @@ describe('SelectJobRolesMandatoryComponent', () => { }); describe('On submit', () => { - const selectJobRolesAndSave = (getByText) => { + const selectJobRolesAndSave = (getByText, jobRoles = [mockAvailableJobs[0]]) => { userEvent.click(getByText('Show all job roles')); - userEvent.click(getByText('Registered nurse')); - userEvent.click(getByText('Social worker')); + + jobRoles.forEach((role) => userEvent.click(getByText(role.title))); userEvent.click(getByText('Save mandatory training')); }; @@ -200,6 +209,23 @@ describe('SelectJobRolesMandatoryComponent', () => { expect(resetStateInTrainingServiceSpy).toHaveBeenCalled(); }); + + [ + [mockAvailableJobs[0], mockAvailableJobs[1]], + [mockAvailableJobs[2], mockAvailableJobs[3]], + ].forEach((jobRoleSet) => { + it(`should call createAndUpdateMandatoryTraining with training category in service and selected job roles ('${jobRoleSet[0].title}', '${jobRoleSet[1].title}')`, async () => { + const { getByText, establishment, selectedTraining, createAndUpdateMandatoryTrainingSpy } = await setup(); + + selectJobRolesAndSave(getByText, jobRoleSet); + + expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { + trainingCategoryId: selectedTraining.trainingCategory.id, + allJobRoles: false, + jobs: [{ id: jobRoleSet[0].id }, { id: jobRoleSet[1].id }], + }); + }); + }); }); describe('Errors', () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 0f08d19448..9dd8b38830 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -2,15 +2,18 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; +import { Establishment } from '@core/model/establishment.model'; import { Job, JobGroup } from '@core/model/job.model'; import { SelectedTraining } from '@core/model/training.model'; import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { EstablishmentService } from '@core/services/establishment.service'; import { JobService } from '@core/services/job.service'; import { TrainingService } from '@core/services/training.service'; import { AccordionGroupComponent } from '@shared/components/accordions/generic-accordion/accordion-group/accordion-group.component'; import { CustomValidators } from '@shared/validators/custom-form-validators'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-select-job-roles-mandatory', @@ -24,6 +27,7 @@ export class SelectJobRolesMandatoryComponent { private errorSummaryService: ErrorSummaryService, private backLinkService: BackLinkService, private alertService: AlertService, + private establishmentService: EstablishmentService, public route: ActivatedRoute, ) {} @@ -37,10 +41,13 @@ export class SelectJobRolesMandatoryComponent { public selectedJobIds: number[] = []; public errorMessageOnEmptyInput: string = 'Select the job roles that need this training'; public formErrorsMap: Array = []; + public subscriptions: Subscription = new Subscription(); + private establishment: Establishment; private selectedTrainingCategory: SelectedTraining; ngOnInit(): void { this.selectedTrainingCategory = this.trainingService.selectedTraining; + this.establishment = this.route.snapshot.data?.establishment; if (!this.selectedTrainingCategory) { this.router.navigate(['../select-training-category'], { relativeTo: this.route }); @@ -95,12 +102,32 @@ export class SelectJobRolesMandatoryComponent { return; } - this.navigateBackToAddMandatoryTrainingPage(); - this.alertService.addAlert({ - type: 'success', - message: 'Mandatory training category added', - }); - this.trainingService.resetState(); + this.createMandatoryTraining(); + } + + private createMandatoryTraining(): void { + const props = { + trainingCategoryId: this.selectedTrainingCategory.trainingCategory.id, + allJobRoles: false, + jobs: this.selectedJobIds.map((id) => { + return { id }; + }), + }; + + this.subscriptions.add( + this.establishmentService.createAndUpdateMandatoryTraining(this.establishment.uid, props).subscribe( + () => { + this.navigateBackToAddMandatoryTrainingPage(); + this.trainingService.resetState(); + + this.alertService.addAlert({ + type: 'success', + message: 'Mandatory training category added', + }); + }, + () => {}, + ), + ); } public onCancel(event: Event): void { @@ -114,7 +141,11 @@ export class SelectJobRolesMandatoryComponent { this.router.navigate(['../'], { relativeTo: this.route }); } - ngAfterViewInit() { + ngAfterViewInit(): void { this.errorSummaryService.formEl$.next(this.formEl); } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } } From 238644cfb75c060e7efd093047721f8fb75398f1 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 13 Dec 2024 13:08:04 +0000 Subject: [PATCH 29/72] Add server message for when backend call fails --- .../select-job-roles-mandatory.component.html | 10 ++++-- ...lect-job-roles-mandatory.component.spec.ts | 34 ++++++++++++++----- .../select-job-roles-mandatory.component.ts | 5 ++- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html index 163a22fd2a..dcfc8eb714 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html @@ -1,8 +1,14 @@ - + +
-
+
diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index 3057f01643..11b403139b 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -17,6 +17,7 @@ import { RadioButtonAccordionComponent } from '@shared/components/accordions/rad import { SharedModule } from '@shared/shared.module'; import { render, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; +import { throwError } from 'rxjs'; import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; import { SelectJobRolesMandatoryComponent } from './select-job-roles-mandatory.component'; @@ -175,26 +176,28 @@ describe('SelectJobRolesMandatoryComponent', () => { }); describe('On submit', () => { - const selectJobRolesAndSave = (getByText, jobRoles = [mockAvailableJobs[0]]) => { + const selectJobRolesAndSave = (fixture, getByText, jobRoles = [mockAvailableJobs[0]]) => { userEvent.click(getByText('Show all job roles')); jobRoles.forEach((role) => userEvent.click(getByText(role.title))); + fixture.detectChanges(); userEvent.click(getByText('Save mandatory training')); + fixture.detectChanges(); }; it('should navigate back to add-and-manage-mandatory-training main page', async () => { - const { component, getByText, routerSpy } = await setup(); + const { fixture, component, getByText, routerSpy } = await setup(); - selectJobRolesAndSave(getByText); + selectJobRolesAndSave(fixture, getByText); expect(routerSpy).toHaveBeenCalledWith(['../'], { relativeTo: component.route }); }); it("should display 'Mandatory training category added' banner", async () => { - const { getByText, alertSpy } = await setup(); + const { fixture, getByText, alertSpy } = await setup(); - selectJobRolesAndSave(getByText); + selectJobRolesAndSave(fixture, getByText); expect(alertSpy).toHaveBeenCalledWith({ type: 'success', @@ -203,9 +206,9 @@ describe('SelectJobRolesMandatoryComponent', () => { }); it('should clear state in training service', async () => { - const { getByText, resetStateInTrainingServiceSpy } = await setup(); + const { fixture, getByText, resetStateInTrainingServiceSpy } = await setup(); - selectJobRolesAndSave(getByText); + selectJobRolesAndSave(fixture, getByText); expect(resetStateInTrainingServiceSpy).toHaveBeenCalled(); }); @@ -215,9 +218,10 @@ describe('SelectJobRolesMandatoryComponent', () => { [mockAvailableJobs[2], mockAvailableJobs[3]], ].forEach((jobRoleSet) => { it(`should call createAndUpdateMandatoryTraining with training category in service and selected job roles ('${jobRoleSet[0].title}', '${jobRoleSet[1].title}')`, async () => { - const { getByText, establishment, selectedTraining, createAndUpdateMandatoryTrainingSpy } = await setup(); + const { fixture, getByText, establishment, selectedTraining, createAndUpdateMandatoryTrainingSpy } = + await setup(); - selectJobRolesAndSave(getByText, jobRoleSet); + selectJobRolesAndSave(fixture, getByText, jobRoleSet); expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { trainingCategoryId: selectedTraining.trainingCategory.id, @@ -226,6 +230,18 @@ describe('SelectJobRolesMandatoryComponent', () => { }); }); }); + + it('should display server error message if call to backend fails', async () => { + const { fixture, getByText, createAndUpdateMandatoryTrainingSpy } = await setup(); + + createAndUpdateMandatoryTrainingSpy.and.returnValue(throwError(() => new Error('Unexpected error'))); + + selectJobRolesAndSave(fixture, getByText); + + const expectedErrorMessage = 'There has been a problem saving your mandatory training. Please try again.'; + + expect(getByText(expectedErrorMessage)).toBeTruthy(); + }); }); describe('Errors', () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 9dd8b38830..7d6b541c17 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -41,6 +41,7 @@ export class SelectJobRolesMandatoryComponent { public selectedJobIds: number[] = []; public errorMessageOnEmptyInput: string = 'Select the job roles that need this training'; public formErrorsMap: Array = []; + public serverError: string; public subscriptions: Subscription = new Subscription(); private establishment: Establishment; private selectedTrainingCategory: SelectedTraining; @@ -125,7 +126,9 @@ export class SelectJobRolesMandatoryComponent { message: 'Mandatory training category added', }); }, - () => {}, + () => { + this.serverError = 'There has been a problem saving your mandatory training. Please try again.'; + }, ), ); } From 985b2a329adcca41be15770e615f4edb14a1908e Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 13 Dec 2024 16:22:39 +0000 Subject: [PATCH 30/72] Prefill 'Only selected job roles' radio when going back to All or selected job roles page --- frontend/src/app/app.module.ts | 31 ++++------- .../src/app/core/services/training.service.ts | 17 ++++++ ...ll-or-selected-job-roles.component.spec.ts | 54 +++++++++++++++---- .../all-or-selected-job-roles.component.ts | 9 +++- ...lect-job-roles-mandatory.component.spec.ts | 6 +-- .../select-job-roles-mandatory.component.ts | 4 +- ...ining-category-mandatory.component.spec.ts | 6 +-- ...t-training-category-mandatory.component.ts | 4 +- 8 files changed, 86 insertions(+), 45 deletions(-) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 10396b25f2..e6dfc09022 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -4,9 +4,7 @@ import { ErrorHandler, NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; -import { - ProblemWithTheServiceComponent, -} from '@core/components/error/problem-with-the-service/problem-with-the-service.component'; +import { ProblemWithTheServiceComponent } from '@core/components/error/problem-with-the-service/problem-with-the-service.component'; import { ServiceUnavailableComponent } from '@core/components/error/service-unavailable/service-unavailable.component'; import { FooterComponent } from '@core/components/footer/footer.component'; import { HeaderComponent } from '@core/components/header/header.component'; @@ -20,9 +18,7 @@ import { AllUsersForEstablishmentResolver } from '@core/resolvers/dashboard/all- import { TotalStaffRecordsResolver } from '@core/resolvers/dashboard/total-staff-records.resolver'; import { FundingReportResolver } from '@core/resolvers/funding-report.resolver'; import { GetMissingCqcLocationsResolver } from '@core/resolvers/getMissingCqcLocations/getMissingCqcLocations.resolver'; -import { - GetNoOfWorkersWhoRequireInternationalRecruitmentAnswersResolver, -} from '@core/resolvers/international-recruitment/no-of-workers-who-require-international-recruitment-answers.resolver'; +import { GetNoOfWorkersWhoRequireInternationalRecruitmentAnswersResolver } from '@core/resolvers/international-recruitment/no-of-workers-who-require-international-recruitment-answers.resolver'; import { LoggedInUserResolver } from '@core/resolvers/logged-in-user.resolver'; import { NotificationsListResolver } from '@core/resolvers/notifications-list.resolver'; import { PageResolver } from '@core/resolvers/page.resolver'; @@ -51,17 +47,13 @@ import { PreviousRouteService } from '@core/services/previous-route.service'; import { QualificationService } from '@core/services/qualification.service'; import { RecruitmentService } from '@core/services/recruitment.service'; import { RegistrationService } from '@core/services/registration.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService, TrainingService } from '@core/services/training.service'; import { windowProvider, WindowToken } from '@core/services/window'; import { WindowRef } from '@core/services/window.ref'; import { WorkerService } from '@core/services/worker.service'; import { AdminSkipService } from '@features/bulk-upload/admin-skip.service'; -import { - ParentWorkplaceAccounts, -} from '@features/create-account/workplace/parent-workplace-accounts/parent-workplace-accounts.component'; -import { - SelectMainServiceComponent, -} from '@features/create-account/workplace/select-main-service/select-main-service.component'; +import { ParentWorkplaceAccounts } from '@features/create-account/workplace/parent-workplace-accounts/parent-workplace-accounts.component'; +import { SelectMainServiceComponent } from '@features/create-account/workplace/select-main-service/select-main-service.component'; import { AscWdsCertificateComponent } from '@features/dashboard/asc-wds-certificate/asc-wds-certificate.component'; import { DashboardHeaderComponent } from '@features/dashboard/dashboard-header/dashboard-header.component'; import { DashboardComponent } from '@features/dashboard/dashboard.component'; @@ -88,9 +80,7 @@ import { NewWorkplaceTabComponent } from '@features/new-dashboard/workplace-tab/ import { ResetPasswordConfirmationComponent } from '@features/reset-password/confirmation/confirmation.component'; import { ResetPasswordEditComponent } from '@features/reset-password/edit/edit.component'; import { ResetPasswordComponent } from '@features/reset-password/reset-password.component'; -import { - SelectStarterJobRolesComponent, -} from '@features/workplace/select-starter-job-roles/select-starter-job-roles.component'; +import { SelectStarterJobRolesComponent } from '@features/workplace/select-starter-job-roles/select-starter-job-roles.component'; import { BenchmarksModule } from '@shared/components/benchmarks-tab/benchmarks.module'; import { DataAreaTabModule } from '@shared/components/data-area-tab/data-area-tab.module'; import { FeatureFlagsService } from '@shared/services/feature-flags.service'; @@ -100,12 +90,8 @@ import { HighchartsChartModule } from 'highcharts-angular'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { - StaffMismatchBannerComponent, -} from './features/dashboard/home-tab/staff-mismatch-banner/staff-mismatch-banner.component'; -import { - MigratedUserTermsConditionsComponent, -} from './features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; +import { StaffMismatchBannerComponent } from './features/dashboard/home-tab/staff-mismatch-banner/staff-mismatch-banner.component'; +import { MigratedUserTermsConditionsComponent } from './features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; import { SatisfactionSurveyComponent } from './features/satisfaction-survey/satisfaction-survey.component'; import { SentryErrorHandler } from './SentryErrorHandler.component'; @@ -191,6 +177,7 @@ import { SentryErrorHandler } from './SentryErrorHandler.component'; RegistrationService, { provide: ErrorHandler, useClass: SentryErrorHandler }, TrainingService, + MandatoryTrainingService, WindowRef, WorkerService, InternationalRecruitmentService, diff --git a/frontend/src/app/core/services/training.service.ts b/frontend/src/app/core/services/training.service.ts index c9e0635028..2ff5563d05 100644 --- a/frontend/src/app/core/services/training.service.ts +++ b/frontend/src/app/core/services/training.service.ts @@ -125,3 +125,20 @@ export class TrainingService { this.updatingSelectedStaffForMultipleTraining = null; } } + +export class MandatoryTrainingService extends TrainingService { + _onlySelectedJobRoles: boolean = null; + + public get onlySelectedJobRoles(): boolean { + return this._onlySelectedJobRoles; + } + + public set onlySelectedJobRoles(onlySelected: boolean) { + this._onlySelectedJobRoles = onlySelected; + } + + public resetState(): void { + this.onlySelectedJobRoles = null; + super.resetState(); + } +} diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 733bd38f21..f076a9f951 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -4,7 +4,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { AlertService } from '@core/services/alert.service'; import { EstablishmentService } from '@core/services/establishment.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { MockRouter } from '@core/test-utils/MockRouter'; @@ -33,10 +33,14 @@ describe('AllOrSelectedJobRolesComponent', () => { imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, AddMandatoryTrainingModule], providers: [ { - provide: TrainingService, + provide: MandatoryTrainingService, useValue: { selectedTraining: overrides.selectedTraining !== undefined ? overrides.selectedTraining : selectedTraining, resetState: () => {}, + set onlySelectedJobRoles(onlySelected) {}, + get onlySelectedJobRoles() { + return overrides.onlySelectedJobRoles ?? false; + }, }, }, { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, @@ -60,8 +64,9 @@ describe('AllOrSelectedJobRolesComponent', () => { const component = setupTools.fixture.componentInstance; const injector = getTestBed(); - const trainingService = injector.inject(TrainingService) as TrainingService; + const trainingService = injector.inject(MandatoryTrainingService) as MandatoryTrainingService; const resetStateInTrainingServiceSpy = spyOn(trainingService, 'resetState').and.callThrough(); + const onlySelectedJobRolesSpy = spyOnProperty(trainingService, 'onlySelectedJobRoles', 'set'); const alertService = injector.inject(AlertService) as AlertService; const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); @@ -81,6 +86,7 @@ describe('AllOrSelectedJobRolesComponent', () => { createAndUpdateMandatoryTrainingSpy, establishment, selectedTraining, + onlySelectedJobRolesSpy, }; } @@ -166,6 +172,18 @@ describe('AllOrSelectedJobRolesComponent', () => { }); }); + describe('Prefill', () => { + it("should prefill the 'Only selected job roles' radio when set in training service", async () => { + const { getByLabelText } = await setup({ onlySelectedJobRoles: true }); + + const onlySelectedJobRolesRadio = getByLabelText('Only selected job roles') as HTMLInputElement; + const allJobRolesRadio = getByLabelText('All job roles') as HTMLInputElement; + + expect(onlySelectedJobRolesRadio.checked).toBeTruthy(); + expect(allJobRolesRadio.checked).toBeFalsy(); + }); + }); + describe('Cancel button', () => { it('should navigate to the add-and-manage-mandatory-training page (relative route ../) when clicked', async () => { const { component, getByText, routerSpy } = await setup(); @@ -200,7 +218,7 @@ describe('AllOrSelectedJobRolesComponent', () => { }); describe('On submit', () => { - describe("when 'All job roles selected'", () => { + describe("when 'All job roles' selected", () => { const selectAllJobRolesAndSubmit = (fixture, getByText) => { fireEvent.click(getByText('All job roles')); fixture.detectChanges(); @@ -272,16 +290,30 @@ describe('AllOrSelectedJobRolesComponent', () => { }); }); - it("should navigate to select-job-roles page when user submits with 'Only selected job roles' selected", async () => { - const { component, fixture, getByText, routerSpy } = await setup(); + describe("when 'Only selected job roles' selected", () => { + it('should navigate to select-job-roles page', async () => { + const { component, fixture, getByText, routerSpy } = await setup(); - fireEvent.click(getByText('Only selected job roles')); - fixture.detectChanges(); + fireEvent.click(getByText('Only selected job roles')); + fixture.detectChanges(); - fireEvent.click(getByText('Continue')); - fixture.detectChanges(); + fireEvent.click(getByText('Continue')); + fixture.detectChanges(); + + expect(routerSpy).toHaveBeenCalledWith(['../', 'select-job-roles'], { relativeTo: component.route }); + }); + + it('should set onlySelectedJobRoles in training service', async () => { + const { fixture, getByText, onlySelectedJobRolesSpy } = await setup(); - expect(routerSpy).toHaveBeenCalledWith(['../', 'select-job-roles'], { relativeTo: component.route }); + fireEvent.click(getByText('Only selected job roles')); + fixture.detectChanges(); + + fireEvent.click(getByText('Continue')); + fixture.detectChanges(); + + expect(onlySelectedJobRolesSpy).toHaveBeenCalledWith(true); + }); }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index d4ee7323ba..4335bd3500 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -9,7 +9,7 @@ import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; import { EstablishmentService } from '@core/services/establishment.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService } from '@core/services/training.service'; import { Subscription } from 'rxjs'; @Component({ @@ -36,7 +36,7 @@ export class AllOrSelectedJobRolesComponent { private errorSummaryService: ErrorSummaryService, private backLinkService: BackLinkService, public route: ActivatedRoute, - private trainingService: TrainingService, + private trainingService: MandatoryTrainingService, private alertService: AlertService, private establishmentService: EstablishmentService, ) { @@ -47,6 +47,10 @@ export class AllOrSelectedJobRolesComponent { this.establishment = this.route.snapshot.parent?.data?.establishment; this.selectedTrainingCategory = this.trainingService.selectedTraining; + if (this.trainingService.onlySelectedJobRoles) { + this.form.setValue({ allOrSelectedJobRoles: 'selectJobRoles' }); + } + if (!this.selectedTrainingCategory) { this.router.navigate(['../select-training-category'], { relativeTo: this.route }); } @@ -79,6 +83,7 @@ export class AllOrSelectedJobRolesComponent { if (this.selectedRadio == 'allJobRoles') { this.createMandatoryTraining(); } else { + this.trainingService.onlySelectedJobRoles = true; this.navigateToSelectJobRolesPage(); } } else { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index 11b403139b..254a05eece 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -8,7 +8,7 @@ import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; import { EstablishmentService } from '@core/services/establishment.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { MockRouter } from '@core/test-utils/MockRouter'; @@ -75,7 +75,7 @@ describe('SelectJobRolesMandatoryComponent', () => { { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, { provide: EstablishmentService, useClass: MockEstablishmentService }, { - provide: TrainingService, + provide: MandatoryTrainingService, useValue: { selectedTraining: overrides.selectedTraining !== undefined ? overrides.selectedTraining : selectedTraining, resetState: () => {}, @@ -102,7 +102,7 @@ describe('SelectJobRolesMandatoryComponent', () => { const alertService = injector.inject(AlertService) as AlertService; const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); - const trainingService = injector.inject(TrainingService) as TrainingService; + const trainingService = injector.inject(MandatoryTrainingService) as MandatoryTrainingService; const resetStateInTrainingServiceSpy = spyOn(trainingService, 'resetState').and.callThrough(); const establishmentService = injector.inject(EstablishmentService) as EstablishmentService; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 7d6b541c17..65c89eaeac 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -10,7 +10,7 @@ import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; import { EstablishmentService } from '@core/services/establishment.service'; import { JobService } from '@core/services/job.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService } from '@core/services/training.service'; import { AccordionGroupComponent } from '@shared/components/accordions/generic-accordion/accordion-group/accordion-group.component'; import { CustomValidators } from '@shared/validators/custom-form-validators'; import { Subscription } from 'rxjs'; @@ -22,7 +22,7 @@ import { Subscription } from 'rxjs'; export class SelectJobRolesMandatoryComponent { constructor( private formBuilder: UntypedFormBuilder, - private trainingService: TrainingService, + private trainingService: MandatoryTrainingService, private router: Router, private errorSummaryService: ErrorSummaryService, private backLinkService: BackLinkService, diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index 76e9cc8aa9..74e94536bf 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -6,7 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { Establishment } from '@core/model/establishment.model'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { WorkerService } from '@core/services/worker.service'; import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; @@ -39,7 +39,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { useClass: MockWorkerService, }, { - provide: TrainingService, + provide: MandatoryTrainingService, useClass: overrides.prefill ? MockTrainingServiceWithPreselectedStaff : MockTrainingService, }, { @@ -64,7 +64,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { const injector = getTestBed(); const router = injector.inject(Router) as Router; - const trainingService = injector.inject(TrainingService) as TrainingService; + const trainingService = injector.inject(MandatoryTrainingService) as MandatoryTrainingService; const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index ce297ff91a..0b691e0629 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -3,7 +3,7 @@ import { FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService } from '@core/services/training.service'; import { WorkerService } from '@core/services/worker.service'; import { SelectTrainingCategoryDirective } from '../../../../shared/directives/select-training-category/select-training-category.directive'; @@ -15,7 +15,7 @@ import { SelectTrainingCategoryDirective } from '../../../../shared/directives/s export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCategoryDirective { constructor( protected formBuilder: FormBuilder, - protected trainingService: TrainingService, + protected trainingService: MandatoryTrainingService, protected router: Router, protected backLinkService: BackLinkService, protected workerService: WorkerService, From 48d201e0a446d4880b4d34f9675a029e460326f0 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 12:27:45 +0000 Subject: [PATCH 31/72] Update heading --- .../select-job-roles-mandatory.component.html | 2 +- .../select-job-roles-mandatory.component.spec.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html index dcfc8eb714..39eaf4e6cf 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html @@ -13,7 +13,7 @@
Add a mandatory training category -

Select the job roles which need this training

+

Select the job roles that need this training

diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index 254a05eece..d79626e884 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -12,8 +12,12 @@ import { MandatoryTrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { MockRouter } from '@core/test-utils/MockRouter'; -import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; -import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; +import { + GroupedRadioButtonAccordionComponent, +} from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; +import { + RadioButtonAccordionComponent, +} from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; import { SharedModule } from '@shared/shared.module'; import { render, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; @@ -139,7 +143,7 @@ describe('SelectJobRolesMandatoryComponent', () => { it('should show the page heading', async () => { const { getByText } = await setup(); - const heading = getByText('Select the job roles which need this training'); + const heading = getByText('Select the job roles that need this training'); expect(heading).toBeTruthy(); }); From 4f0ad9de30ceb7636ad4c0f841994f2bd16f5f44 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 12:37:52 +0000 Subject: [PATCH 32/72] Move remove all link to table header --- ...d-manage-mandatory-training.component.html | 41 +++++++++---------- ...anage-mandatory-training.component.spec.ts | 27 +++++++----- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html index 68f60675c6..8f707a2757 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html @@ -20,31 +20,29 @@

- - - + + + @@ -78,6 +76,7 @@

{{ job.title }}
+

diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 6eb72d32b9..5d6b16f19b 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -3,12 +3,13 @@ import { getTestBed } from '@angular/core/testing'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { JourneyType } from '@core/breadcrumb/breadcrumb.model'; +import { Establishment } from '@core/model/establishment.model'; import { BreadcrumbService } from '@core/services/breadcrumb.service'; import { EstablishmentService } from '@core/services/establishment.service'; import { TrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { MockBreadcrumbService } from '@core/test-utils/MockBreadcrumbService'; -import { MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; +import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { MockTrainingService } from '@core/test-utils/MockTrainingService'; import { ParentSubsidiaryViewService } from '@shared/services/parent-subsidiary-view.service'; import { SharedModule } from '@shared/shared.module'; @@ -18,6 +19,8 @@ import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandato describe('AddAndManageMandatoryTrainingComponent', () => { async function setup(isOwnWorkplace = true, duplicateJobRoles = false) { + const establishment = establishmentBuilder() as Establishment; + const { getByText, getByLabelText, getByTestId, fixture } = await render(AddAndManageMandatoryTrainingComponent, { imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule], declarations: [], @@ -47,9 +50,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { parent: { snapshot: { data: { - establishment: { - uid: '123', - }, + establishment, }, }, }, @@ -73,6 +74,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { component, parentSubsidiaryViewService, establishmentService, + establishment, }; } @@ -96,11 +98,14 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(addMandatoryTrainingButton.textContent).toContain('Add a mandatory training category'); }); - it('should show the Remove all mandatory training categories link', async () => { - const { getByTestId } = await setup(); + it('should show the Remove all link with link to the remove all page', async () => { + const { getByText, establishment } = await setup(); - const removeMandatoryTrainingLink = getByTestId('removeMandatoryTrainingLink'); - expect(removeMandatoryTrainingLink).toBeTruthy(); + const removeMandatoryTrainingLink = getByText('Remove all') as HTMLAnchorElement; + + expect(removeMandatoryTrainingLink.href).toContain( + `/workplace/${establishment.uid}/add-and-manage-mandatory-training/remove-all-mandatory-training`, + ); }); it('should show the manage mandatory training table', async () => { @@ -111,13 +116,13 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(mandatoryTrainingTable).toBeTruthy(); }); - it('should show the manage mandatory training table heading', async () => { + it('should show the manage mandatory training table headings', async () => { const { getByTestId } = await setup(); const mandatoryTrainingTableHeading = getByTestId('training-table-heading'); - expect(mandatoryTrainingTableHeading.textContent).toContain('Mandatory training categories'); - expect(mandatoryTrainingTableHeading.textContent).toContain('Job roles'); + expect(mandatoryTrainingTableHeading.textContent).toContain('Mandatory training category'); + expect(mandatoryTrainingTableHeading.textContent).toContain('Job role'); }); describe('mandatory training table records', () => { From ffa7b352a4d9467f9a9922802bd44a7d1e0e0c90 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 12:58:27 +0000 Subject: [PATCH 33/72] Update spacing for add button and description text --- ...d-manage-mandatory-training.component.html | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html index 8f707a2757..c949b6478f 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html @@ -6,15 +6,19 @@

-
-
+
+

- Add the training categories you want to make mandatory for your staff. It will help you identify
- who is missing training and let you know when
- training expires. + Add the training categories you want to make mandatory for your staff. It will help you identify who is missing + training and let you know when training expires.

-
+ +
@@ -29,8 +33,7 @@

+ diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 7f3ba7c2e7..0aa453bda1 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -1,5 +1,6 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ActivatedRoute, RouterModule } from '@angular/router'; +import { getTestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { Establishment } from '@core/model/establishment.model'; import { BreadcrumbService } from '@core/services/breadcrumb.service'; @@ -10,17 +11,17 @@ import { MockBreadcrumbService } from '@core/test-utils/MockBreadcrumbService'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { mockMandatoryTraining, MockTrainingService } from '@core/test-utils/MockTrainingService'; import { SharedModule } from '@shared/shared.module'; -import { render } from '@testing-library/angular'; +import { fireEvent, render } from '@testing-library/angular'; import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training.component'; describe('AddAndManageMandatoryTrainingComponent', () => { async function setup(overrides: any = {}) { const establishment = establishmentBuilder() as Establishment; - const mandatoryTraining = mockMandatoryTraining(); + const existingMandatoryTraining = mockMandatoryTraining(); const setupTools = await render(AddAndManageMandatoryTrainingComponent, { - imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule], + imports: [SharedModule, RouterModule, RouterTestingModule.withRoutes([]), HttpClientTestingModule], declarations: [], providers: [ { @@ -45,7 +46,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { snapshot: { url: [{ path: 'add-and-manage-mandatory-training' }], data: { - existingMandatoryTraining: overrides.mandatoryTraining ?? mandatoryTraining, + existingMandatoryTraining: overrides.mandatoryTraining ?? existingMandatoryTraining, }, }, parent: { @@ -62,10 +63,19 @@ describe('AddAndManageMandatoryTrainingComponent', () => { const component = setupTools.fixture.componentInstance; + const injector = getTestBed(); + + const router = injector.inject(Router) as Router; + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + const currentRoute = injector.inject(ActivatedRoute) as ActivatedRoute; + return { ...setupTools, component, establishment, + existingMandatoryTraining, + routerSpy, + currentRoute, }; } @@ -151,24 +161,40 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(autismCategory.textContent).toContain('Activities worker, coordinator'); }); - it('should show all if there are any duplicate job roles and the job roles length is the old all job roles length', async () => { - const { getByTestId } = await setup({ duplicateJobRoles: true }); + it(`should have a Remove link for each training category which takes user to its remove page`, async () => { + const { getByTestId, existingMandatoryTraining, routerSpy, currentRoute } = await setup(); - const coshCategory = getByTestId('titleAll'); - const autismCategory = getByTestId('titleJob'); + existingMandatoryTraining.mandatoryTraining.forEach((trainingCategory) => { + const removeLink = getByTestId('remove-link-' + trainingCategory.category) as HTMLAnchorElement; + fireEvent.click(removeLink); - expect(coshCategory.textContent).toContain('All'); - expect(autismCategory.textContent).toContain('Activities worker, coordinator'); + expect(routerSpy).toHaveBeenCalledWith( + [trainingCategory.trainingCategoryId, 'delete-mandatory-training-category'], + { relativeTo: currentRoute }, + ); + }); }); - it('should show all if there are any duplicate job roles and the job roles length is the old all job roles length', async () => { - const { getByTestId } = await setup({ duplicateJobRoles: true }); + describe('Handling duplicate job roles', () => { + it('should show all if there are any duplicate job roles and the job roles length is the old all job roles length', async () => { + const { getByTestId } = await setup({ duplicateJobRoles: true }); - const coshCategory = getByTestId('titleAll'); - const autismCategory = getByTestId('titleJob'); + const coshCategory = getByTestId('titleAll'); + const autismCategory = getByTestId('titleJob'); - expect(coshCategory.textContent).toContain('All'); - expect(autismCategory.textContent).toContain('Activities worker, coordinator'); + expect(coshCategory.textContent).toContain('All'); + expect(autismCategory.textContent).toContain('Activities worker, coordinator'); + }); + + it('should show all if there are any duplicate job roles and the job roles length is the old all job roles length', async () => { + const { getByTestId } = await setup({ duplicateJobRoles: true }); + + const coshCategory = getByTestId('titleAll'); + const autismCategory = getByTestId('titleJob'); + + expect(coshCategory.textContent).toContain('All'); + expect(autismCategory.textContent).toContain('Activities worker, coordinator'); + }); }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index 172044af3c..a2683ed7f9 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -74,4 +74,9 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { 'select-training-category', ]); } + + public navigateToDeletePage(event: Event, trainingCategoryId: number): void { + event.preventDefault(); + this.router.navigate([trainingCategoryId, 'delete-mandatory-training-category'], { relativeTo: this.route }); + } } From e4522b086083ba8d286a0d90460364ff388d37f2 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 16:23:16 +0000 Subject: [PATCH 37/72] Align Remove link to right --- .../add-and-manage-mandatory-training.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html index 62b4704d7f..6f9186c84a 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html @@ -79,10 +79,11 @@

{{ job.title }}
-

- diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 23f4c1172e..d43c2e167a 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -171,6 +171,21 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(autismCategory.textContent).toContain('Activities worker, coordinator'); }); + it('should navigate to select-training-category with training category ID and name in params when category link clicked', async () => { + const { getByText, existingMandatoryTraining, routerSpy, currentRoute } = await setup(); + + existingMandatoryTraining.mandatoryTraining.forEach((trainingCategory) => { + fireEvent.click(getByText(trainingCategory.category)); + + expect(routerSpy).toHaveBeenCalledWith(['select-training-category'], { + relativeTo: currentRoute, + queryParams: { + trainingCategory: `{"id":${trainingCategory.trainingCategoryId},"category":"${trainingCategory.category}"}`, + }, + }); + }); + }); + it(`should have a Remove link for each training category which takes user to its remove page`, async () => { const { getByTestId, existingMandatoryTraining, routerSpy, currentRoute } = await setup(); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index f1bb2c4d1d..7362bb700a 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { JourneyType } from '@core/breadcrumb/breadcrumb.model'; import { Establishment } from '@core/model/establishment.model'; import { BreadcrumbService } from '@core/services/breadcrumb.service'; @@ -66,8 +66,20 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { }); } - public navigateToAddNewMandatoryTraining() { - this.router.navigate(['select-training-category'], { relativeTo: this.route }); + public navigateToAddNewMandatoryTraining(event: Event, existingMandatoryTraining = null): void { + event.preventDefault(); + const extras: NavigationExtras = { relativeTo: this.route }; + + if (existingMandatoryTraining) { + extras.queryParams = { + trainingCategory: JSON.stringify({ + id: existingMandatoryTraining.trainingCategoryId, + category: existingMandatoryTraining.category, + }), + }; + } + + this.router.navigate(['select-training-category'], extras); } public navigateToDeletePage(event: Event, trainingCategoryId: number): void { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index 74e94536bf..793d20818a 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -13,11 +13,14 @@ import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService' import { trainingCategories } from '@core/test-utils/MockTrainingCategoriesService'; import { MockTrainingService, MockTrainingServiceWithPreselectedStaff } from '@core/test-utils/MockTrainingService'; import { MockWorkerService } from '@core/test-utils/MockWorkerService'; -import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; -import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; +import { + GroupedRadioButtonAccordionComponent, +} from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; +import { + RadioButtonAccordionComponent, +} from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; -import sinon from 'sinon'; import { AddMandatoryTrainingModule } from '../add-mandatory-training.module'; import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory.component'; @@ -52,7 +55,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { existingMandatoryTraining: overrides.existingMandatoryTraining ?? {}, }, queryParamMap: { - get: sinon.stub(), + get: () => overrides.selectedTraining ?? null, }, }, }, @@ -200,7 +203,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { expect(component.form.value).toEqual({ category: 1 }); }); - it('should not include training categories which already have mandatory training', async () => { + describe('Existing mandatory training', () => { const mockTrainingCategories = [ { id: 1, seq: 10, category: 'Activity provision/Well-being', trainingCategoryGroup: 'Care skills and knowledge' }, { id: 2, seq: 20, category: 'Autism', trainingCategoryGroup: 'Specific conditions and disabilities' }, @@ -218,15 +221,34 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { ], }; - const overrides = { - trainingCategories: mockTrainingCategories, - existingMandatoryTraining, - }; + it('should not include training categories which already have mandatory training', async () => { + const overrides = { + trainingCategories: mockTrainingCategories, + existingMandatoryTraining, + }; + + const { queryByText } = await setup(overrides); - const { queryByText } = await setup(overrides); + expect(queryByText(mockTrainingCategories[0].category)).toBeTruthy(); + expect(queryByText(mockTrainingCategories[1].category)).toBeTruthy(); + expect(queryByText(mockTrainingCategories[2].category)).toBeFalsy(); + }); - expect(queryByText(mockTrainingCategories[0].category)).toBeTruthy(); - expect(queryByText(mockTrainingCategories[1].category)).toBeTruthy(); - expect(queryByText(mockTrainingCategories[2].category)).toBeFalsy(); + it('should include training category and prefill the radio when ID of existing mandatory training category in params', async () => { + const overrides = { + trainingCategories: mockTrainingCategories, + existingMandatoryTraining, + selectedTraining: JSON.stringify({ + id: mockTrainingCategories[2].id, + category: mockTrainingCategories[2].category, + }), + }; + + const { component, queryByText } = await setup(overrides); + + const selectedExistingMandatoryTrainingCategory = queryByText(mockTrainingCategories[2].category); + expect(selectedExistingMandatoryTrainingCategory).toBeTruthy(); + expect(component.form.value).toEqual({ category: mockTrainingCategories[2].id }); + }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index 0b691e0629..661cbffd4b 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -6,7 +6,9 @@ import { ErrorSummaryService } from '@core/services/error-summary.service'; import { MandatoryTrainingService } from '@core/services/training.service'; import { WorkerService } from '@core/services/worker.service'; -import { SelectTrainingCategoryDirective } from '../../../../shared/directives/select-training-category/select-training-category.directive'; +import { + SelectTrainingCategoryDirective, +} from '../../../../shared/directives/select-training-category/select-training-category.directive'; @Component({ selector: 'app-select-training-category-mandatory', @@ -30,6 +32,15 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate init(): void { this.establishmentUid = this.route.snapshot.data.establishment.uid; + + if (this.route.snapshot.queryParamMap.get('trainingCategory')) { + const mandatoryTrainingCategory = JSON.parse(this.route.snapshot.queryParamMap.get('trainingCategory')); + const categoryId = parseInt(mandatoryTrainingCategory?.id, 10); + + if (categoryId) { + this.preFilledId = categoryId; + } + } } protected setSectionHeading(): void { @@ -50,7 +61,8 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate if (trainingCategoryIdsWithExistingMandatoryTraining?.length) { this.categories = allTrainingCategories.filter( - (category) => !trainingCategoryIdsWithExistingMandatoryTraining.includes(category.id), + (category) => + !trainingCategoryIdsWithExistingMandatoryTraining.includes(category.id) || category.id == this.preFilledId, ); } else { this.categories = allTrainingCategories; @@ -59,7 +71,7 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate this.sortCategoriesByTrainingGroup(this.categories); } - public onCancel(event: Event) { + public onCancel(event: Event): void { event.preventDefault(); this.trainingService.resetState(); this.router.navigate(['workplace', this.establishmentUid, 'add-and-manage-mandatory-training']); From c74529099a0f2111518f00a4f5acf636991ddb73 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 18 Dec 2024 09:16:44 +0000 Subject: [PATCH 49/72] Update clicking into existing mandatory training to set state in service instead of passing data in params --- .../src/app/core/services/training.service.ts | 16 +++++++- .../core/test-utils/MockTrainingService.ts | 17 +++++++- ...anage-mandatory-training.component.spec.ts | 40 ++++++++++--------- ...and-manage-mandatory-training.component.ts | 16 +++----- 4 files changed, 57 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/core/services/training.service.ts b/frontend/src/app/core/services/training.service.ts index 2ff5563d05..537af76e8a 100644 --- a/frontend/src/app/core/services/training.service.ts +++ b/frontend/src/app/core/services/training.service.ts @@ -1,7 +1,12 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; -import { allMandatoryTrainingCategories, SelectedTraining, TrainingCategory } from '@core/model/training.model'; +import { + allMandatoryTrainingCategories, + mandatoryTraining, + SelectedTraining, + TrainingCategory, +} from '@core/model/training.model'; import { Worker } from '@core/model/worker.model'; import { BehaviorSubject, Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; @@ -128,6 +133,7 @@ export class TrainingService { export class MandatoryTrainingService extends TrainingService { _onlySelectedJobRoles: boolean = null; + _existingMandatoryTraining: mandatoryTraining = null; public get onlySelectedJobRoles(): boolean { return this._onlySelectedJobRoles; @@ -141,4 +147,12 @@ export class MandatoryTrainingService extends TrainingService { this.onlySelectedJobRoles = null; super.resetState(); } + + public set existingMandatoryTraining(mandatoryTraining) { + this._existingMandatoryTraining = mandatoryTraining; + } + + public get existingMandatoryTraining(): mandatoryTraining { + return this._existingMandatoryTraining; + } } diff --git a/frontend/src/app/core/test-utils/MockTrainingService.ts b/frontend/src/app/core/test-utils/MockTrainingService.ts index fee2566eda..491f94ff6c 100644 --- a/frontend/src/app/core/test-utils/MockTrainingService.ts +++ b/frontend/src/app/core/test-utils/MockTrainingService.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { allMandatoryTrainingCategories, TrainingCategory } from '@core/model/training.model'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService, TrainingService } from '@core/services/training.service'; import { Observable, of } from 'rxjs'; import { AllJobs, JobsWithDuplicates } from '../../../mockdata/jobs'; @@ -79,6 +79,21 @@ export class MockTrainingServiceWithPreselectedStaff extends MockTrainingService } } +@Injectable() +export class MockMandatoryTrainingService extends MandatoryTrainingService { + public static factory(overrides = {}) { + return (http: HttpClient) => { + const service = new MockMandatoryTrainingService(http); + + Object.keys(overrides).forEach((overrideName) => { + service[overrideName] = overrides[overrideName]; + }); + + return service; + }; + } +} + export const mockMandatoryTraining = (duplicateJobRoles = false) => { return { allJobRolesCount: 37, diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index d43c2e167a..08f9497d22 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -5,11 +5,15 @@ import { RouterTestingModule } from '@angular/router/testing'; import { Establishment } from '@core/model/establishment.model'; import { BreadcrumbService } from '@core/services/breadcrumb.service'; import { EstablishmentService } from '@core/services/establishment.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { MockBreadcrumbService } from '@core/test-utils/MockBreadcrumbService'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; -import { mockMandatoryTraining, MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { + mockMandatoryTraining, + MockMandatoryTrainingService, + MockTrainingService, +} from '@core/test-utils/MockTrainingService'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; @@ -40,8 +44,10 @@ describe('AddAndManageMandatoryTrainingComponent', () => { useClass: WindowRef, }, { - provide: TrainingService, - useFactory: MockTrainingService.factory(overrides.duplicateJobRoles ?? false), + provide: MandatoryTrainingService, + useFactory: overrides.duplicateJobRoles + ? MockTrainingService.factory(overrides.duplicateJobRoles) + : MockMandatoryTrainingService.factory(), }, { provide: EstablishmentService, @@ -77,6 +83,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { existingMandatoryTraining, routerSpy, currentRoute, + injector, }; } @@ -171,17 +178,22 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(autismCategory.textContent).toContain('Activities worker, coordinator'); }); - it('should navigate to select-training-category with training category ID and name in params when category link clicked', async () => { - const { getByText, existingMandatoryTraining, routerSpy, currentRoute } = await setup(); + it('should navigate to select-training-category and set existing mandatory training in service when category link clicked', async () => { + const { getByText, existingMandatoryTraining, routerSpy, currentRoute, injector } = await setup(); + + const mandatoryTrainingService = injector.inject(MandatoryTrainingService) as MandatoryTrainingService; + const setExistingMandatoryTrainingSpy = spyOnProperty( + mandatoryTrainingService, + 'existingMandatoryTraining', + 'set', + ).and.stub(); existingMandatoryTraining.mandatoryTraining.forEach((trainingCategory) => { fireEvent.click(getByText(trainingCategory.category)); + expect(setExistingMandatoryTrainingSpy).toHaveBeenCalledWith(trainingCategory); expect(routerSpy).toHaveBeenCalledWith(['select-training-category'], { relativeTo: currentRoute, - queryParams: { - trainingCategory: `{"id":${trainingCategory.trainingCategoryId},"category":"${trainingCategory.category}"}`, - }, }); }); }); @@ -210,16 +222,6 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(coshCategory.textContent).toContain('All'); expect(autismCategory.textContent).toContain('Activities worker, coordinator'); }); - - it('should show all if there are any duplicate job roles and the job roles length is the old all job roles length', async () => { - const { getByTestId } = await setup({ duplicateJobRoles: true }); - - const coshCategory = getByTestId('titleAll'); - const autismCategory = getByTestId('titleJob'); - - expect(coshCategory.textContent).toContain('All'); - expect(autismCategory.textContent).toContain('Activities worker, coordinator'); - }); }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index 7362bb700a..f477684d0f 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { JourneyType } from '@core/breadcrumb/breadcrumb.model'; import { Establishment } from '@core/model/establishment.model'; import { BreadcrumbService } from '@core/services/breadcrumb.service'; import { EstablishmentService } from '@core/services/establishment.service'; import { JobService } from '@core/services/job.service'; -import { TrainingService } from '@core/services/training.service'; +import { MandatoryTrainingService } from '@core/services/training.service'; @Component({ selector: 'app-add-and-manage-mandatory-training', @@ -19,7 +19,7 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { public previousAllJobsLength = [29, 31, 32]; constructor( - public trainingService: TrainingService, + public trainingService: MandatoryTrainingService, private route: ActivatedRoute, public jobService: JobService, private breadcrumbService: BreadcrumbService, @@ -68,18 +68,12 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { public navigateToAddNewMandatoryTraining(event: Event, existingMandatoryTraining = null): void { event.preventDefault(); - const extras: NavigationExtras = { relativeTo: this.route }; if (existingMandatoryTraining) { - extras.queryParams = { - trainingCategory: JSON.stringify({ - id: existingMandatoryTraining.trainingCategoryId, - category: existingMandatoryTraining.category, - }), - }; + this.trainingService.existingMandatoryTraining = existingMandatoryTraining; } - this.router.navigate(['select-training-category'], extras); + this.router.navigate(['select-training-category'], { relativeTo: this.route }); } public navigateToDeletePage(event: Event, trainingCategoryId: number): void { From ad1ce4edb46e741fd5eda58e6fe20a3151847679 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 18 Dec 2024 14:00:08 +0000 Subject: [PATCH 50/72] Desk check changes: Add caption, align button content and message, display Remove all link when more than one mandatory training category set up --- ...d-and-manage-mandatory-training.component.html | 5 +++-- ...nd-manage-mandatory-training.component.spec.ts | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html index 99aa401165..6168a2b168 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html @@ -1,5 +1,6 @@
+ Add a mandatory training category

Add and manage mandatory
training categories @@ -8,7 +9,7 @@

-

+

Add the training categories you want to make mandatory for your staff. It will help you to identify who is missing training and let you know when training expires.

@@ -32,7 +33,7 @@

diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index d9a1a1519e..ef34dff36f 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -9,11 +9,7 @@ import { MandatoryTrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { MockBreadcrumbService } from '@core/test-utils/MockBreadcrumbService'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; -import { - mockMandatoryTraining, - MockMandatoryTrainingService, - MockTrainingService, -} from '@core/test-utils/MockTrainingService'; +import { mockMandatoryTraining, MockMandatoryTrainingService } from '@core/test-utils/MockTrainingService'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; @@ -45,9 +41,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { }, { provide: MandatoryTrainingService, - useFactory: overrides.duplicateJobRoles - ? MockTrainingService.factory(overrides.duplicateJobRoles) - : MockMandatoryTrainingService.factory(), + useFactory: MockMandatoryTrainingService.factory(), }, { provide: EstablishmentService, @@ -211,17 +205,5 @@ describe('AddAndManageMandatoryTrainingComponent', () => { ); }); }); - - describe('Handling duplicate job roles', () => { - it('should show all if there are any duplicate job roles and the job roles length is the old all job roles length', async () => { - const { getByTestId } = await setup({ duplicateJobRoles: true }); - - const coshCategory = getByTestId('titleAll'); - const autismCategory = getByTestId('titleJob'); - - expect(coshCategory.textContent).toContain('All'); - expect(autismCategory.textContent).toContain('Activities worker, coordinator'); - }); - }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index 20b8d7f382..b7557e06af 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -15,8 +15,6 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { public establishment: Establishment; public existingMandatoryTrainings: any; public allJobsLength: Number; - public mandatoryTrainingHasDuplicateJobRoles = []; - public previousAllJobsLength = [29, 31, 32]; constructor( public trainingService: MandatoryTrainingService, @@ -34,30 +32,6 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { this.existingMandatoryTrainings = this.route.snapshot.data?.existingMandatoryTraining; this.sortTrainingAlphabetically(this.existingMandatoryTrainings.mandatoryTraining); this.allJobsLength = this.existingMandatoryTrainings.allJobRolesCount; - this.setMandatoryTrainingHasDuplicateJobRoles(); - } - - public checkDuplicateJobRoles(jobs): boolean { - for (let i = 0; i < jobs.length; i++) { - for (let j = i + 1; j < jobs.length; j++) { - if (jobs[i].id === jobs[j].id) { - return true; - } - } - } - } - - public setMandatoryTrainingHasDuplicateJobRoles() { - let mandatoryTraining = this.existingMandatoryTrainings.mandatoryTraining; - - mandatoryTraining.forEach((trainingCategory, index) => { - this.mandatoryTrainingHasDuplicateJobRoles.push({ - [trainingCategory.trainingCategoryId]: { - hasDuplicates: this.checkDuplicateJobRoles(trainingCategory.jobs), - hasPreviousAllJobsLength: this.previousAllJobsLength.includes(trainingCategory.jobs.length), - }, - }); - }); } public sortTrainingAlphabetically(training) { From 2c58ad14963743fb6f5748b71e3e9d73af7f10e4 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 9 Jan 2025 08:28:28 +0000 Subject: [PATCH 68/72] Clear state before navigating to add/edit mandatory training pages to ensure no leftover state from previous user interactions --- ...nd-manage-mandatory-training.component.spec.ts | 15 +++++++++++---- ...add-and-manage-mandatory-training.component.ts | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 287781141f..f9d4c349a5 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -70,6 +70,8 @@ describe('AddAndManageMandatoryTrainingComponent', () => { const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); const currentRoute = injector.inject(ActivatedRoute) as ActivatedRoute; + const mandatoryTrainingService = injector.inject(MandatoryTrainingService) as MandatoryTrainingService; + return { ...setupTools, component, @@ -78,6 +80,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { routerSpy, currentRoute, injector, + mandatoryTrainingService, }; } @@ -98,12 +101,15 @@ describe('AddAndManageMandatoryTrainingComponent', () => { ); }); - it("should navigate to the select-training-category page when 'Add a mandatory training category' link is clicked", async () => { - const { getByRole, routerSpy, currentRoute } = await setup(); + it("should navigate to the select-training-category page and clear state in training service when 'Add a mandatory training category' link is clicked", async () => { + const { getByRole, routerSpy, currentRoute, mandatoryTrainingService } = await setup(); + + const resetStateSpy = spyOn(mandatoryTrainingService, 'resetState'); const addMandatoryTrainingButton = getByRole('button', { name: 'Add a mandatory training category' }); fireEvent.click(addMandatoryTrainingButton); + expect(resetStateSpy).toHaveBeenCalled(); expect(routerSpy).toHaveBeenCalledWith(['select-training-category'], { relativeTo: currentRoute }); }); @@ -184,19 +190,20 @@ describe('AddAndManageMandatoryTrainingComponent', () => { }); it('should navigate to select-training-category and set mandatory training being edited in service when category link clicked', async () => { - const { getByText, existingMandatoryTraining, routerSpy, currentRoute, injector } = await setup(); + const { getByText, existingMandatoryTraining, routerSpy, currentRoute, mandatoryTrainingService } = await setup(); - const mandatoryTrainingService = injector.inject(MandatoryTrainingService) as MandatoryTrainingService; const setMandatoryTrainingBeingEditedSpy = spyOnProperty( mandatoryTrainingService, 'mandatoryTrainingBeingEdited', 'set', ).and.stub(); + const resetStateSpy = spyOn(mandatoryTrainingService, 'resetState'); existingMandatoryTraining.mandatoryTraining.forEach((trainingCategory) => { fireEvent.click(getByText(trainingCategory.category)); expect(setMandatoryTrainingBeingEditedSpy).toHaveBeenCalledWith(trainingCategory); + expect(resetStateSpy).toHaveBeenCalled(); expect(routerSpy).toHaveBeenCalledWith(['select-training-category'], { relativeTo: currentRoute, }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index b7557e06af..99f54dc421 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -42,6 +42,7 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { public navigateToAddNewMandatoryTraining(event: Event, mandatoryTrainingToEdit = null): void { event.preventDefault(); + this.trainingService.resetState(); if (mandatoryTrainingToEdit) { this.trainingService.mandatoryTrainingBeingEdited = mandatoryTrainingToEdit; From 2b7ae6419313233c7a54dad384955ff14a2d799c Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 9 Jan 2025 09:42:21 +0000 Subject: [PATCH 69/72] Reorganise routing to stop resolvers from always running on flow pages (was causing focus to disappear on clicking error summary link) --- .../add-mandatory-routing.module.ts | 69 +++++++++---------- .../all-or-selected-job-roles.component.ts | 4 +- .../select-job-roles-mandatory.component.ts | 4 +- ...t-training-category-mandatory.component.ts | 8 +-- 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index 5f660c6127..83baee1f5c 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -14,50 +14,49 @@ import { SelectTrainingCategoryMandatoryComponent } from './select-training-cate const routes: Routes = [ { path: '', + component: AddAndManageMandatoryTrainingComponent, + data: { title: 'List Mandatory Training' }, resolve: { existingMandatoryTraining: MandatoryTrainingCategoriesResolver, }, runGuardsAndResolvers: 'always', + }, + { + path: 'select-training-category', + component: SelectTrainingCategoryMandatoryComponent, + data: { title: 'Select Training Category' }, + resolve: { + trainingCategories: TrainingCategoriesResolver, + existingMandatoryTraining: MandatoryTrainingCategoriesResolver, + }, + }, + { + path: 'all-or-selected-job-roles', + component: AllOrSelectedJobRolesComponent, + data: { title: 'All or Selected Job Roles?' }, + }, + { + path: 'select-job-roles', + component: SelectJobRolesMandatoryComponent, + data: { title: 'Select Job Roles' }, + resolve: { jobs: JobsResolver }, + }, + { + path: 'remove-all-mandatory-training', + component: RemoveAllMandatoryTrainingComponent, + data: { title: 'Remove All Mandatory Training' }, + }, + { + path: ':trainingCategoryId', children: [ { - path: '', - component: AddAndManageMandatoryTrainingComponent, - data: { title: 'List Mandatory Training' }, - }, - { - path: 'select-training-category', - component: SelectTrainingCategoryMandatoryComponent, - data: { title: 'Select Training Category' }, + path: 'delete-mandatory-training-category', + component: DeleteMandatoryTrainingCategoryComponent, + data: { title: 'Delete Mandatory Training Category' }, resolve: { - trainingCategories: TrainingCategoriesResolver, + existingMandatoryTraining: MandatoryTrainingCategoriesResolver, }, }, - { - path: 'all-or-selected-job-roles', - component: AllOrSelectedJobRolesComponent, - data: { title: 'All or Selected Job Roles?' }, - }, - { - path: 'select-job-roles', - component: SelectJobRolesMandatoryComponent, - data: { title: 'Select Job Roles' }, - resolve: { jobs: JobsResolver }, - }, - { - path: 'remove-all-mandatory-training', - component: RemoveAllMandatoryTrainingComponent, - data: { title: 'Remove All Mandatory Training' }, - }, - { - path: ':trainingCategoryId', - children: [ - { - path: 'delete-mandatory-training-category', - component: DeleteMandatoryTrainingCategoryComponent, - data: { title: 'Delete Mandatory Training Category' }, - }, - ], - }, ], }, ]; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index e396f0996c..19810e0711 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; @@ -16,7 +16,7 @@ import { Subscription } from 'rxjs'; selector: 'app-all-or-selected-job-roles', templateUrl: './all-or-selected-job-roles.component.html', }) -export class AllOrSelectedJobRolesComponent { +export class AllOrSelectedJobRolesComponent implements OnInit, OnDestroy, AfterViewInit { @ViewChild('formEl') formEl: ElementRef; public form: UntypedFormGroup; public submitted = false; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index d7abfd6539..77eb739854 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; @@ -19,7 +19,7 @@ import { Subscription } from 'rxjs'; selector: 'app-select-job-roles-mandatory', templateUrl: './select-job-roles-mandatory.component.html', }) -export class SelectJobRolesMandatoryComponent { +export class SelectJobRolesMandatoryComponent implements OnInit, OnDestroy, AfterViewInit { constructor( private formBuilder: UntypedFormBuilder, private trainingService: MandatoryTrainingService, diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index fff072a8a1..70457f9843 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -13,6 +13,10 @@ import { SelectTrainingCategoryDirective } from '../../../../shared/directives/s templateUrl: '../../../../shared/directives/select-training-category/select-training-category.component.html', }) export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCategoryDirective { + public requiredErrorMessage: string = 'Select the training category that you want to make mandatory'; + public hideOtherCheckbox: boolean = true; + private mandatoryTrainingCategoryIdBeingEdited: number; + constructor( protected formBuilder: FormBuilder, protected trainingService: MandatoryTrainingService, @@ -25,10 +29,6 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate super(formBuilder, trainingService, router, backLinkService, workerService, route, errorSummaryService); } - public requiredErrorMessage: string = 'Select the training category that you want to make mandatory'; - public hideOtherCheckbox: boolean = true; - private mandatoryTrainingCategoryIdBeingEdited: number; - init(): void { this.establishmentUid = this.route.snapshot.data.establishment.uid; this.mandatoryTrainingCategoryIdBeingEdited = From 4fb9f62fc0d627f271c69cd1c257add0806a9bae Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 9 Jan 2025 10:04:31 +0000 Subject: [PATCH 70/72] Display banner after navigation to prevent screen flicker on submit from all-or-selected-job-roles page --- .../all-or-selected-job-roles.component.spec.ts | 4 +++- .../all-or-selected-job-roles.component.ts | 13 +++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 7da6bf1262..e6b8b85ab4 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -300,6 +300,7 @@ describe('AllOrSelectedJobRolesComponent', () => { const { fixture, getByText, alertSpy } = await setup(); selectAllJobRolesAndSubmit(fixture, getByText); + await fixture.whenStable(); expect(alertSpy).toHaveBeenCalledWith({ type: 'success', @@ -379,12 +380,13 @@ describe('AllOrSelectedJobRolesComponent', () => { }); it("should display 'Mandatory training category updated' banner when All job roles selected", async () => { - const { getByText, alertSpy } = await setup({ + const { fixture, getByText, alertSpy } = await setup({ mandatoryTrainingBeingEdited, allJobRolesCount: mandatoryTrainingBeingEdited.jobs.length, }); fireEvent.click(getByText('Continue')); + await fixture.whenStable(); expect(alertSpy).toHaveBeenCalledWith({ type: 'success', diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index 19810e0711..633caf1c5d 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -79,8 +79,8 @@ export class AllOrSelectedJobRolesComponent implements OnInit, OnDestroy, AfterV this.router.navigate(['../', 'select-job-roles'], { relativeTo: this.route }); } - private navigateBackToAddMandatoryTrainingPage(): void { - this.router.navigate(['../'], { relativeTo: this.route }); + private navigateBackToAddMandatoryTrainingPage(): Promise { + return this.router.navigate(['../'], { relativeTo: this.route }); } public selectRadio(selectedRadio: string): void { @@ -109,12 +109,13 @@ export class AllOrSelectedJobRolesComponent implements OnInit, OnDestroy, AfterV this.subscriptions.add( this.establishmentService.createAndUpdateMandatoryTraining(this.establishment.uid, props).subscribe( () => { - this.navigateBackToAddMandatoryTrainingPage(); this.trainingService.resetState(); - this.alertService.addAlert({ - type: 'success', - message: `Mandatory training category ${this.mandatoryTrainingBeingEdited ? 'updated' : 'added'}`, + this.navigateBackToAddMandatoryTrainingPage().then(() => { + this.alertService.addAlert({ + type: 'success', + message: `Mandatory training category ${this.mandatoryTrainingBeingEdited ? 'updated' : 'added'}`, + }); }); }, () => { From 5d50d358a042f626d15d482137561026fb1735a8 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 9 Jan 2025 10:07:55 +0000 Subject: [PATCH 71/72] Display banner after navigation to prevent screen flicker on submit from mandatory select-job-roles page --- .../select-job-roles-mandatory.component.spec.ts | 4 +++- .../select-job-roles-mandatory.component.ts | 13 +++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index 1908ec8733..39ab305ec4 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -196,6 +196,7 @@ describe('SelectJobRolesMandatoryComponent', () => { const { fixture, getByText, alertSpy } = await setup(); selectJobRolesAndSave(fixture, getByText); + await fixture.whenStable(); expect(alertSpy).toHaveBeenCalledWith({ type: 'success', @@ -400,11 +401,12 @@ describe('SelectJobRolesMandatoryComponent', () => { const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; const mandatoryTrainingBeingEdited = createMandatoryTrainingBeingEdited(jobs); - const { getByText, alertSpy } = await setup({ + const { fixture, getByText, alertSpy } = await setup({ mandatoryTrainingBeingEdited, }); userEvent.click(getByText('Save mandatory training')); + await fixture.whenStable(); expect(alertSpy).toHaveBeenCalledWith({ type: 'success', diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 77eb739854..58e1df6d5b 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -119,12 +119,13 @@ export class SelectJobRolesMandatoryComponent implements OnInit, OnDestroy, Afte this.subscriptions.add( this.establishmentService.createAndUpdateMandatoryTraining(this.establishment.uid, props).subscribe( () => { - this.navigateBackToAddMandatoryTrainingPage(); this.trainingService.resetState(); - this.alertService.addAlert({ - type: 'success', - message: `Mandatory training category ${this.mandatoryTrainingBeingEdited ? 'updated' : 'added'}`, + this.navigateBackToAddMandatoryTrainingPage().then(() => { + this.alertService.addAlert({ + type: 'success', + message: `Mandatory training category ${this.mandatoryTrainingBeingEdited ? 'updated' : 'added'}`, + }); }); }, () => { @@ -170,8 +171,8 @@ export class SelectJobRolesMandatoryComponent implements OnInit, OnDestroy, Afte this.navigateBackToAddMandatoryTrainingPage(); } - private navigateBackToAddMandatoryTrainingPage(): void { - this.router.navigate(['../'], { relativeTo: this.route }); + private navigateBackToAddMandatoryTrainingPage(): Promise { + return this.router.navigate(['../'], { relativeTo: this.route }); } ngAfterViewInit(): void { From b9c0fb49247b01892d76f660ba3ef0d1245b4ffd Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 9 Jan 2025 10:32:18 +0000 Subject: [PATCH 72/72] Add banner changes to delete and delete-all mandatory training pages --- ...datory-training-category.component.spec.ts | 8 +++---- ...e-mandatory-training-category.component.ts | 13 ++++++----- ...e-all-mandatory-training.component.spec.ts | 13 +++++++++++ ...delete-all-mandatory-training.component.ts | 22 ++++++++++++------- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts index fdb501b85b..e73c3bb916 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts @@ -11,9 +11,7 @@ import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService' import { MockRouter } from '@core/test-utils/MockRouter'; import { MockTrainingCategoryService } from '@core/test-utils/MockTrainingCategoriesService'; import { mockMandatoryTraining, MockTrainingService } from '@core/test-utils/MockTrainingService'; -import { - AddMandatoryTrainingModule, -} from '@features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module'; +import { AddMandatoryTrainingModule } from '@features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; @@ -148,9 +146,11 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { }); it("should display a success banner with 'Mandatory training category removed'", async () => { - const { alertSpy, getByText } = await setup(); + const { fixture, alertSpy, getByText } = await setup(); fireEvent.click(getByText('Remove category')); + await fixture.whenStable(); + expect(alertSpy).toHaveBeenCalledWith({ type: 'success', message: 'Mandatory training category removed', diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts index 4e255768ca..69bbb3cbd2 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts @@ -50,17 +50,18 @@ export class DeleteMandatoryTrainingCategoryComponent implements OnInit, OnDestr this.trainingService .deleteCategoryById(this.establishment.id, this.selectedCategory.trainingCategoryId) .subscribe(() => { - this.navigateBackToMandatoryTrainingHomePage(); - this.alertService.addAlert({ - type: 'success', - message: 'Mandatory training category removed', + this.navigateBackToMandatoryTrainingHomePage().then(() => { + this.alertService.addAlert({ + type: 'success', + message: 'Mandatory training category removed', + }); }); }), ); } - public navigateBackToMandatoryTrainingHomePage(): void { - this.router.navigate(['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training']); + public navigateBackToMandatoryTrainingHomePage(): Promise { + return this.router.navigate(['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training']); } ngOnDestroy(): void { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.spec.ts index 66a525e066..19b9e99c24 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.spec.ts @@ -119,6 +119,19 @@ describe('RemoveAllMandatoryTrainingComponent', () => { expect(routerSpy).toHaveBeenCalledWith(['/workplace', establishment.uid, 'add-and-manage-mandatory-training']); }); + + it("should display a success banner with 'All mandatory training categories removed'", async () => { + const { fixture, alertSpy, getByText } = await setup(); + + const submitButton = getByText('Remove categories'); + fireEvent.click(submitButton); + await fixture.whenStable(); + + expect(alertSpy).toHaveBeenCalledWith({ + type: 'success', + message: 'All mandatory training categories removed', + }); + }); }); it('should return to the add-and-manage-mandatory-training when Cancel is clicked', async () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.ts index d71de71d75..d16eff6c95 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Establishment } from '@core/model/establishment.model'; import { AlertService } from '@core/services/alert.service'; @@ -11,9 +11,10 @@ import { Subscription } from 'rxjs'; selector: 'app-remove-all-selections-dialog', templateUrl: './delete-all-mandatory-training.component.html', }) -export class RemoveAllMandatoryTrainingComponent implements OnInit { +export class RemoveAllMandatoryTrainingComponent implements OnInit, OnDestroy { public establishment: Establishment; private subscriptions: Subscription = new Subscription(); + constructor( protected backLinkService: BackLinkService, private trainingService: TrainingService, @@ -31,16 +32,21 @@ export class RemoveAllMandatoryTrainingComponent implements OnInit { public deleteMandatoryTraining(): void { this.subscriptions.add( this.trainingService.deleteAllMandatoryTraining(this.establishment.id).subscribe(() => { - this.navigateToPreviousPage(); - this.alertService.addAlert({ - type: 'success', - message: 'All mandatory training categories removed', + this.navigateToPreviousPage().then(() => { + this.alertService.addAlert({ + type: 'success', + message: 'All mandatory training categories removed', + }); }); }), ); } - public navigateToPreviousPage(): void { - this.router.navigate(['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training']); + public navigateToPreviousPage(): Promise { + return this.router.navigate(['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training']); + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); } }
Mandatory training categoriesJob rolesMandatory training categoryJob role + Remove all +
Date: Mon, 16 Dec 2024 13:10:40 +0000 Subject: [PATCH 34/72] Update component to use resolver to retrieve data from backend --- .../core/test-utils/MockTrainingService.ts | 56 ++++++++++--------- ...anage-mandatory-training.component.spec.ts | 26 ++------- ...and-manage-mandatory-training.component.ts | 25 ++------- .../add-mandatory-routing.module.ts | 15 ++++- 4 files changed, 52 insertions(+), 70 deletions(-) diff --git a/frontend/src/app/core/test-utils/MockTrainingService.ts b/frontend/src/app/core/test-utils/MockTrainingService.ts index 09dee08a8e..d5ba968a47 100644 --- a/frontend/src/app/core/test-utils/MockTrainingService.ts +++ b/frontend/src/app/core/test-utils/MockTrainingService.ts @@ -40,32 +40,7 @@ export class MockTrainingService extends TrainingService { } public getAllMandatoryTrainings(): Observable { - return of({ - allJobRolesCount: 37, - lastUpdated: new Date(), - mandatoryTraining: [ - { - trainingCategoryId: 123, - allJobRoles: false, - category: 'Autism', - selectedJobRoles: true, - jobs: [ - { - id: 15, - title: 'Activities worker, coordinator', - }, - ], - }, - { - trainingCategoryId: 9, - allJobRoles: true, - category: 'Coshh', - selectedJobRoles: true, - jobs: this._duplicateJobRoles ? JobsWithDuplicates : AllJobs, - }, - ], - mandatoryTrainingCount: 2, - }); + return of(mockMandatoryTraining(this._duplicateJobRoles)); } public deleteCategoryById(establishmentId, categoryId) { @@ -99,3 +74,32 @@ export class MockTrainingServiceWithPreselectedStaff extends MockTrainingService }; } } + +export const mockMandatoryTraining = (duplicateJobRoles) => { + return { + allJobRolesCount: 37, + lastUpdated: new Date(), + mandatoryTraining: [ + { + trainingCategoryId: 123, + allJobRoles: false, + category: 'Autism', + selectedJobRoles: true, + jobs: [ + { + id: 15, + title: 'Activities worker, coordinator', + }, + ], + }, + { + trainingCategoryId: 9, + allJobRoles: true, + category: 'Coshh', + selectedJobRoles: true, + jobs: duplicateJobRoles ? JobsWithDuplicates : AllJobs, + }, + ], + mandatoryTrainingCount: 2, + }; +}; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 5d6b16f19b..1c96bd5fed 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -2,7 +2,6 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { getTestBed } from '@angular/core/testing'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { JourneyType } from '@core/breadcrumb/breadcrumb.model'; import { Establishment } from '@core/model/establishment.model'; import { BreadcrumbService } from '@core/services/breadcrumb.service'; import { EstablishmentService } from '@core/services/establishment.service'; @@ -10,7 +9,7 @@ import { TrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { MockBreadcrumbService } from '@core/test-utils/MockBreadcrumbService'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; -import { MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { mockMandatoryTraining, MockTrainingService } from '@core/test-utils/MockTrainingService'; import { ParentSubsidiaryViewService } from '@shared/services/parent-subsidiary-view.service'; import { SharedModule } from '@shared/shared.module'; import { render } from '@testing-library/angular'; @@ -46,6 +45,9 @@ describe('AddAndManageMandatoryTrainingComponent', () => { useValue: { snapshot: { url: [{ path: 'add-and-manage-mandatory-training' }], + data: { + existingMandatoryTraining: mockMandatoryTraining(duplicateJobRoles), + }, }, parent: { snapshot: { @@ -165,24 +167,4 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(autismCategory.textContent).toContain('Activities worker, coordinator'); }); }); - - describe('getBreadcrumbsJourney', () => { - it('should return mandatory training journey when viewing sub as parent', async () => { - const { component, parentSubsidiaryViewService } = await setup(); - spyOn(parentSubsidiaryViewService, 'getViewingSubAsParent').and.returnValue(true); - expect(component.getBreadcrumbsJourney()).toBe(JourneyType.MANDATORY_TRAINING); - }); - - it('should return mandatory training journey when is own workplace', async () => { - const { component } = await setup(); - - expect(component.getBreadcrumbsJourney()).toBe(JourneyType.MANDATORY_TRAINING); - }); - - it('should return all workplaces journey when is not own workplace and not in parent sub view', async () => { - const { component } = await setup(false); - - expect(component.getBreadcrumbsJourney()).toBe(JourneyType.ALL_WORKPLACES); - }); - }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index ca563a3bf7..172044af3c 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -6,15 +6,12 @@ import { BreadcrumbService } from '@core/services/breadcrumb.service'; import { EstablishmentService } from '@core/services/establishment.service'; import { JobService } from '@core/services/job.service'; import { TrainingService } from '@core/services/training.service'; -import { ParentSubsidiaryViewService } from '@shared/services/parent-subsidiary-view.service'; -import { Subscription } from 'rxjs'; @Component({ selector: 'app-add-and-manage-mandatory-training', templateUrl: './add-and-manage-mandatory-training.component.html', }) export class AddAndManageMandatoryTrainingComponent implements OnInit { - private subscriptions: Subscription = new Subscription(); public establishment: Establishment; public existingMandatoryTrainings: any; public allJobsLength: Number; @@ -28,20 +25,16 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { private breadcrumbService: BreadcrumbService, private router: Router, public establishmentService: EstablishmentService, - private parentSubsidiaryViewService: ParentSubsidiaryViewService, ) {} ngOnInit(): void { - this.breadcrumbService.show(this.getBreadcrumbsJourney()); + this.breadcrumbService.show(JourneyType.MANDATORY_TRAINING); this.establishment = this.route.parent.snapshot.data.establishment; - this.subscriptions.add( - this.trainingService.getAllMandatoryTrainings(this.establishment.uid).subscribe((trainings) => { - this.existingMandatoryTrainings = trainings; - this.sortTrainingAlphabetically(trainings.mandatoryTraining); - this.allJobsLength = trainings.allJobRolesCount; - this.setMandatoryTrainingHasDuplicateJobRoles(); - }), - ); + + this.existingMandatoryTrainings = this.route.snapshot.data?.existingMandatoryTraining; + this.sortTrainingAlphabetically(this.existingMandatoryTrainings.mandatoryTraining); + this.allJobsLength = this.existingMandatoryTrainings.allJobRolesCount; + this.setMandatoryTrainingHasDuplicateJobRoles(); } public checkDuplicateJobRoles(jobs): boolean { @@ -81,10 +74,4 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { 'select-training-category', ]); } - - public getBreadcrumbsJourney(): JourneyType { - return this.establishmentService.isOwnWorkplace() || this.parentSubsidiaryViewService.getViewingSubAsParent() - ? JourneyType.MANDATORY_TRAINING - : JourneyType.ALL_WORKPLACES; - } } diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index 9880b487bd..afe30b76e6 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -3,14 +3,20 @@ import { RouterModule, Routes } from '@angular/router'; import { JobsResolver } from '@core/resolvers/jobs.resolver'; import { MandatoryTrainingCategoriesResolver } from '@core/resolvers/mandatory-training-categories.resolver'; import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; -import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; +import { + DeleteMandatoryTrainingCategoryComponent, +} from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; -import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; +import { + AddAndManageMandatoryTrainingComponent, +} from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles/all-or-selected-job-roles.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; import { SelectJobRolesMandatoryComponent } from './select-job-roles-mandatory/select-job-roles-mandatory.component'; -import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory/select-training-category-mandatory.component'; +import { + SelectTrainingCategoryMandatoryComponent, +} from './select-training-category-mandatory/select-training-category-mandatory.component'; const routes: Routes = [ { @@ -20,6 +26,9 @@ const routes: Routes = [ path: '', component: AddAndManageMandatoryTrainingComponent, data: { title: 'List Mandatory Training' }, + resolve: { + existingMandatoryTraining: MandatoryTrainingCategoriesResolver, + }, }, { path: 'select-training-category', From 149b28442dfa7eebbb15d474b470198055c7be44 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 15:10:28 +0000 Subject: [PATCH 35/72] Refactor spec file and add test for not displaying Remove all link when no mandatory training set up --- .../core/test-utils/MockTrainingService.ts | 2 +- ...anage-mandatory-training.component.spec.ts | 56 ++++++++++--------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/core/test-utils/MockTrainingService.ts b/frontend/src/app/core/test-utils/MockTrainingService.ts index d5ba968a47..cc9eb0021f 100644 --- a/frontend/src/app/core/test-utils/MockTrainingService.ts +++ b/frontend/src/app/core/test-utils/MockTrainingService.ts @@ -75,7 +75,7 @@ export class MockTrainingServiceWithPreselectedStaff extends MockTrainingService } } -export const mockMandatoryTraining = (duplicateJobRoles) => { +export const mockMandatoryTraining = (duplicateJobRoles = false) => { return { allJobRolesCount: 37, lastUpdated: new Date(), diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 1c96bd5fed..7f3ba7c2e7 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -1,5 +1,4 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { getTestBed } from '@angular/core/testing'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { Establishment } from '@core/model/establishment.model'; @@ -10,17 +9,17 @@ import { WindowRef } from '@core/services/window.ref'; import { MockBreadcrumbService } from '@core/test-utils/MockBreadcrumbService'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { mockMandatoryTraining, MockTrainingService } from '@core/test-utils/MockTrainingService'; -import { ParentSubsidiaryViewService } from '@shared/services/parent-subsidiary-view.service'; import { SharedModule } from '@shared/shared.module'; import { render } from '@testing-library/angular'; import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training.component'; describe('AddAndManageMandatoryTrainingComponent', () => { - async function setup(isOwnWorkplace = true, duplicateJobRoles = false) { + async function setup(overrides: any = {}) { const establishment = establishmentBuilder() as Establishment; + const mandatoryTraining = mockMandatoryTraining(); - const { getByText, getByLabelText, getByTestId, fixture } = await render(AddAndManageMandatoryTrainingComponent, { + const setupTools = await render(AddAndManageMandatoryTrainingComponent, { imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule], declarations: [], providers: [ @@ -34,7 +33,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { }, { provide: TrainingService, - useFactory: MockTrainingService.factory(duplicateJobRoles), + useFactory: MockTrainingService.factory(overrides.duplicateJobRoles ?? false), }, { provide: EstablishmentService, @@ -46,7 +45,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { snapshot: { url: [{ path: 'add-and-manage-mandatory-training' }], data: { - existingMandatoryTraining: mockMandatoryTraining(duplicateJobRoles), + existingMandatoryTraining: overrides.mandatoryTraining ?? mandatoryTraining, }, }, parent: { @@ -61,21 +60,11 @@ describe('AddAndManageMandatoryTrainingComponent', () => { ], }); - const injector = getTestBed(); - const parentSubsidiaryViewService = injector.inject(ParentSubsidiaryViewService) as ParentSubsidiaryViewService; - const establishmentService = injector.inject(EstablishmentService) as EstablishmentService; - spyOn(establishmentService, 'isOwnWorkplace').and.returnValue(isOwnWorkplace); - - const component = fixture.componentInstance; + const component = setupTools.fixture.componentInstance; return { - getByText, - getByLabelText, - getByTestId, - fixture, + ...setupTools, component, - parentSubsidiaryViewService, - establishmentService, establishment, }; } @@ -100,14 +89,29 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(addMandatoryTrainingButton.textContent).toContain('Add a mandatory training category'); }); - it('should show the Remove all link with link to the remove all page', async () => { - const { getByText, establishment } = await setup(); + describe('Remove all link', () => { + it('should show with link to the remove all page when there is mandatory training', async () => { + const { getByText, establishment } = await setup(); - const removeMandatoryTrainingLink = getByText('Remove all') as HTMLAnchorElement; + const removeMandatoryTrainingLink = getByText('Remove all') as HTMLAnchorElement; - expect(removeMandatoryTrainingLink.href).toContain( - `/workplace/${establishment.uid}/add-and-manage-mandatory-training/remove-all-mandatory-training`, - ); + expect(removeMandatoryTrainingLink.href).toContain( + `/workplace/${establishment.uid}/add-and-manage-mandatory-training/remove-all-mandatory-training`, + ); + }); + + it('should not show if no mandatory training set up', async () => { + const mandatoryTraining = { + allJobRolesCount: 37, + lastUpdated: new Date(), + mandatoryTraining: [], + mandatoryTrainingCount: 0, + }; + + const { queryByText } = await setup({ mandatoryTraining }); + + expect(queryByText('Remove all')).toBeFalsy(); + }); }); it('should show the manage mandatory training table', async () => { @@ -148,7 +152,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { }); it('should show all if there are any duplicate job roles and the job roles length is the old all job roles length', async () => { - const { getByTestId } = await setup(true, true); + const { getByTestId } = await setup({ duplicateJobRoles: true }); const coshCategory = getByTestId('titleAll'); const autismCategory = getByTestId('titleJob'); @@ -158,7 +162,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { }); it('should show all if there are any duplicate job roles and the job roles length is the old all job roles length', async () => { - const { getByTestId } = await setup(true, true); + const { getByTestId } = await setup({ duplicateJobRoles: true }); const coshCategory = getByTestId('titleAll'); const autismCategory = getByTestId('titleJob'); From 46982d88e9836ed50adc0dbec1e3c9d0215d7e31 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 15:47:52 +0000 Subject: [PATCH 36/72] Add remove link for each training category --- ...d-manage-mandatory-training.component.html | 10 +++- ...anage-mandatory-training.component.spec.ts | 60 +++++++++++++------ ...and-manage-mandatory-training.component.ts | 5 ++ 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html index c949b6478f..62b4704d7f 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html @@ -79,7 +79,15 @@

{{ job.title }}
-

+ + Remove + +
+ Remove From 57dbab7e16642b3c76d2bc8e44727b5605a51d3f Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 16:30:35 +0000 Subject: [PATCH 38/72] Refactor to use relative route for link and establishment from main snapshot --- ...anage-mandatory-training.component.spec.ts | 22 +++++++++---------- ...and-manage-mandatory-training.component.ts | 9 ++------ 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 0aa453bda1..28f7abd6b9 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -46,16 +46,10 @@ describe('AddAndManageMandatoryTrainingComponent', () => { snapshot: { url: [{ path: 'add-and-manage-mandatory-training' }], data: { + establishment, existingMandatoryTraining: overrides.mandatoryTraining ?? existingMandatoryTraining, }, }, - parent: { - snapshot: { - data: { - establishment, - }, - }, - }, }, }, ], @@ -84,19 +78,25 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(component).toBeTruthy(); }); - it('should show header, paragraph and links for manage mandatory training', async () => { + it('should show header and paragraph', async () => { const { getByTestId } = await setup(); const mandatoryTrainingHeader = getByTestId('heading'); - const mandatoryTrainingInfo = getByTestId('mandatoryTrainingInfo'); - const addMandatoryTrainingButton = getByTestId('mandatoryTrainingButton'); expect(mandatoryTrainingHeader.textContent).toContain('Add and manage mandatory training categories'); expect(mandatoryTrainingInfo.textContent).toContain( 'Add the training categories you want to make mandatory for your staff. It will help you identify who is missing training and let you know when training expires.', ); - expect(addMandatoryTrainingButton.textContent).toContain('Add a mandatory training category'); + }); + + it("should navigate to the select-training-category page when 'Add a mandatory training category' link is clicked", async () => { + const { getByText, routerSpy, currentRoute } = await setup(); + + const addMandatoryTrainingButton = getByText('Add a mandatory training category'); + fireEvent.click(addMandatoryTrainingButton); + + expect(routerSpy).toHaveBeenCalledWith(['select-training-category'], { relativeTo: currentRoute }); }); describe('Remove all link', () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index a2683ed7f9..f1bb2c4d1d 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -29,8 +29,8 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { ngOnInit(): void { this.breadcrumbService.show(JourneyType.MANDATORY_TRAINING); - this.establishment = this.route.parent.snapshot.data.establishment; + this.establishment = this.route.snapshot.data?.establishment; this.existingMandatoryTrainings = this.route.snapshot.data?.existingMandatoryTraining; this.sortTrainingAlphabetically(this.existingMandatoryTrainings.mandatoryTraining); this.allJobsLength = this.existingMandatoryTrainings.allJobRolesCount; @@ -67,12 +67,7 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { } public navigateToAddNewMandatoryTraining() { - this.router.navigate([ - '/workplace', - this.establishmentService.establishment.uid, - 'add-and-manage-mandatory-training', - 'select-training-category', - ]); + this.router.navigate(['select-training-category'], { relativeTo: this.route }); } public navigateToDeletePage(event: Event, trainingCategoryId: number): void { From af20698390ee3de2547c85a1fe8b5b262891c49f Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 17:04:14 +0000 Subject: [PATCH 39/72] Refactor delete-mandatory-training-category component --- ...mandatory-training-category.component.html | 4 +- ...datory-training-category.component.spec.ts | 102 +++++++++--------- ...e-mandatory-training-category.component.ts | 42 ++++---- 3 files changed, 74 insertions(+), 74 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html index 48d7605290..7787c7ea91 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html @@ -31,7 +31,9 @@

You're about to remove this mandatory training categ or - Cancel diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts index c18e8a6c4f..4bb68faaae 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts @@ -4,41 +4,32 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { AlertService } from '@core/services/alert.service'; import { BackService } from '@core/services/back.service'; -import { EstablishmentService } from '@core/services/establishment.service'; +import { TrainingCategoryService } from '@core/services/training-category.service'; import { TrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; -import { MockArticlesService } from '@core/test-utils/MockArticlesService'; -import { MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; -import { MockPagesService } from '@core/test-utils/MockPagesService'; +import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; +import { MockTrainingCategoryService } from '@core/test-utils/MockTrainingCategoriesService'; import { MockTrainingService } from '@core/test-utils/MockTrainingService'; -import { AddMandatoryTrainingModule } from '@features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module'; +import { + AddMandatoryTrainingModule, +} from '@features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module'; import { SharedModule } from '@shared/shared.module'; -import { fireEvent, getByText, render } from '@testing-library/angular'; +import { fireEvent, render } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import { DeleteMandatoryTrainingCategoryComponent } from './delete-mandatory-training-category.component'; -import { TrainingCategoryService } from '@core/services/training-category.service'; -import { MockTrainingCategoryService } from '@core/test-utils/MockTrainingCategoriesService'; describe('DeleteMandatoryTrainingCategoryComponent', () => { - const pages = MockPagesService.pagesFactory(); - const articleList = MockArticlesService.articleListFactory(); - async function setup(trainingCategoryId = '1') { - const { fixture, getByText, getAllByText } = await render(DeleteMandatoryTrainingCategoryComponent, { + const establishment = establishmentBuilder(); + + const setupTools = await render(DeleteMandatoryTrainingCategoryComponent, { imports: [SharedModule, RouterModule, RouterTestingModule, AddMandatoryTrainingModule, HttpClientTestingModule], declarations: [], providers: [ AlertService, BackService, - { - provide: WindowRef, - useClass: WindowRef, - }, - { - provide: EstablishmentService, - useClass: MockEstablishmentService, - }, + WindowRef, { provide: TrainingService, useClass: MockTrainingService, @@ -51,13 +42,9 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { provide: ActivatedRoute, useValue: { snapshot: { - parent: { - url: [{ path: trainingCategoryId }], - data: { - establishment: { - uid: '9', - }, - }, + params: { trainingCategoryId }, + data: { + establishment, }, url: [{ path: 'delete-mandatory-training-category' }], }, @@ -67,21 +54,25 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { }); const injector = getTestBed(); - const router = injector.inject(Router) as Router; + const alertService = injector.inject(AlertService) as AlertService; + const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); + const trainingService = injector.inject(TrainingService) as TrainingService; - const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); const deleteMandatoryTrainingCategorySpy = spyOn(trainingService, 'deleteCategoryById').and.callThrough(); - const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); - const component = fixture.componentInstance; + + const router = injector.inject(Router) as Router; + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + const component = setupTools.fixture.componentInstance; + return { + ...setupTools, component, - fixture, - getByText, - getAllByText, routerSpy, deleteMandatoryTrainingCategorySpy, alertSpy, + establishment, }; } @@ -92,7 +83,7 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { it('should display the heading', async () => { const { getByText } = await setup(); - expect(getByText(`You're about to remove this mandatory training category`)); + expect(getByText("You're about to remove this mandatory training category")).toBeTruthy(); }); it('should display the correct training name when navigating to the page with a category ID', async () => { @@ -100,34 +91,21 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { expect(getAllByText('Activity provision/Well-being', { exact: false }).length).toEqual(2); }); - it('Should render Remove categories button and cancel link', async () => { + it('should render Remove categories button and cancel link', async () => { const { getByText } = await setup(); expect(getByText('Remove category')).toBeTruthy(); expect(getByText('Cancel')).toBeTruthy(); }); - describe('Cancel link', () => { - it('should navigate back to the mandatory details summary page when clicked', async () => { - const { component, getByText, routerSpy } = await setup(); - userEvent.click(getByText('Cancel')); - expect(routerSpy).toHaveBeenCalledWith([ - '/workplace', - component.establishment.uid, - 'add-and-manage-mandatory-training', - ]); - }); - }); - - describe('Remove category button', () => { - it('should call the deleteCategoryById function in the training service when clicked', async () => { + describe('On submit', () => { + it('should call deleteCategoryById in the training service', async () => { const { getByText, deleteMandatoryTrainingCategorySpy } = await setup(); + userEvent.click(getByText('Remove category')); expect(deleteMandatoryTrainingCategorySpy).toHaveBeenCalled(); }); - }); - describe('success alert', async () => { - it('should display a success banner when a category is removed', async () => { + it("should display a success banner with 'Mandatory training category removed'", async () => { const { alertSpy, getByText } = await setup(); fireEvent.click(getByText('Remove category')); @@ -136,5 +114,23 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { message: 'Mandatory training category removed', }); }); + + it('should navigate back to add-and-manage-mandatory-training page', async () => { + const { getByText, routerSpy, establishment } = await setup(); + + userEvent.click(getByText('Remove category')); + + expect(routerSpy).toHaveBeenCalledWith(['/workplace', establishment.uid, 'add-and-manage-mandatory-training']); + }); + }); + + describe('Cancel link', () => { + it('should navigate back to the mandatory details summary page when clicked', async () => { + const { component, getByText, routerSpy, establishment } = await setup(); + + userEvent.click(getByText('Cancel')); + + expect(routerSpy).toHaveBeenCalledWith(['/workplace', establishment.uid, 'add-and-manage-mandatory-training']); + }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts index 8ef26e3f99..59de6b55a0 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts @@ -1,11 +1,9 @@ import { Component, OnInit } from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Establishment } from '@core/model/establishment.model'; import { TrainingCategory } from '@core/model/training.model'; import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; -import { EstablishmentService } from '@core/services/establishment.service'; import { TrainingCategoryService } from '@core/services/training-category.service'; import { TrainingService } from '@core/services/training.service'; import { Subscription } from 'rxjs'; @@ -14,12 +12,11 @@ import { Subscription } from 'rxjs'; selector: 'app-delete-mandatory-training-category', templateUrl: './delete-mandatory-training-category.component.html', }) -export class DeleteMandatoryTrainingCategoryComponent implements OnInit { - public categories: TrainingCategory[]; +export class DeleteMandatoryTrainingCategoryComponent implements OnInit, OnDestroy { public selectedCategory: TrainingCategory; - public form: UntypedFormGroup; public establishment: Establishment; private subscriptions: Subscription = new Subscription(); + constructor( protected backLinkService: BackLinkService, protected trainingService: TrainingService, @@ -27,31 +24,36 @@ export class DeleteMandatoryTrainingCategoryComponent implements OnInit { protected route: ActivatedRoute, protected router: Router, private alertService: AlertService, - protected establishmentService: EstablishmentService, ) {} ngOnInit(): void { - this.setBackLink(); - const id = parseInt(this.route.snapshot.parent.url[0].path, 10); - this.establishment = this.route.snapshot.parent.data.establishment; - this.trainingCategoryService.getCategories().subscribe((x) => (this.selectedCategory = x.find((y) => y.id === id))); + this.backLinkService.showBackLink(); + + const trainingCategoryIdInParams = parseInt(this.route.snapshot.params?.trainingCategoryId); + this.establishment = this.route.snapshot.data.establishment; + + this.trainingCategoryService.getCategories().subscribe((x) => { + this.selectedCategory = x.find((y) => y.id === trainingCategoryIdInParams); + }); } public onDelete(): void { - this.trainingService.deleteCategoryById(this.establishment.id, this.selectedCategory.id).subscribe(() => { - this.router.navigate(['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training']); - this.alertService.addAlert({ - type: 'success', - message: 'Mandatory training category removed', - }); - }); + this.subscriptions.add( + this.trainingService.deleteCategoryById(this.establishment.id, this.selectedCategory.id).subscribe(() => { + this.navigateBackToMandatoryTrainingHomePage(); + this.alertService.addAlert({ + type: 'success', + message: 'Mandatory training category removed', + }); + }), + ); } - public onCancel(): void { + public navigateBackToMandatoryTrainingHomePage(): void { this.router.navigate(['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training']); } - public setBackLink(): void { - this.backLinkService.showBackLink(); + private ngOnDestroy(): void { + this.subscriptions.unsubscribe(); } } From a8f72bc538ca7479ab71c321fa20498a65242f05 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Mon, 16 Dec 2024 17:25:11 +0000 Subject: [PATCH 40/72] Update delete-mandatory-training-category component to use existing mandatory training instead of all training categories --- .../add-mandatory-routing.module.ts | 33 +++++++++-------- ...datory-training-category.component.spec.ts | 36 ++++++++++++++----- ...e-mandatory-training-category.component.ts | 15 +++++--- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index afe30b76e6..f3bfd7bcb5 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -21,14 +21,14 @@ import { const routes: Routes = [ { path: '', + resolve: { + existingMandatoryTraining: MandatoryTrainingCategoriesResolver, + }, children: [ { path: '', component: AddAndManageMandatoryTrainingComponent, data: { title: 'List Mandatory Training' }, - resolve: { - existingMandatoryTraining: MandatoryTrainingCategoriesResolver, - }, }, { path: 'select-training-category', @@ -36,7 +36,6 @@ const routes: Routes = [ data: { title: 'Select Training Category' }, resolve: { trainingCategories: TrainingCategoriesResolver, - existingMandatoryTraining: MandatoryTrainingCategoriesResolver, }, }, { @@ -60,20 +59,20 @@ const routes: Routes = [ component: AddMandatoryTrainingComponent, data: { title: 'Add New Mandatory Training' }, }, - ], - }, - { - path: ':trainingCategoryId', - children: [ - { - path: 'edit-mandatory-training', - component: AddMandatoryTrainingComponent, - data: { title: 'Edit Mandatory Training' }, - }, { - path: 'delete-mandatory-training-category', - component: DeleteMandatoryTrainingCategoryComponent, - data: { title: 'Delete Mandatory Training Category' }, + path: ':trainingCategoryId', + children: [ + { + path: 'edit-mandatory-training', + component: AddMandatoryTrainingComponent, + data: { title: 'Edit Mandatory Training' }, + }, + { + path: 'delete-mandatory-training-category', + component: DeleteMandatoryTrainingCategoryComponent, + data: { title: 'Delete Mandatory Training Category' }, + }, + ], }, ], }, diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts index 4bb68faaae..c33f8190f9 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts @@ -8,8 +8,9 @@ import { TrainingCategoryService } from '@core/services/training-category.servic import { TrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; +import { MockRouter } from '@core/test-utils/MockRouter'; import { MockTrainingCategoryService } from '@core/test-utils/MockTrainingCategoriesService'; -import { MockTrainingService } from '@core/test-utils/MockTrainingService'; +import { mockMandatoryTraining, MockTrainingService } from '@core/test-utils/MockTrainingService'; import { AddMandatoryTrainingModule, } from '@features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module'; @@ -20,8 +21,13 @@ import userEvent from '@testing-library/user-event'; import { DeleteMandatoryTrainingCategoryComponent } from './delete-mandatory-training-category.component'; describe('DeleteMandatoryTrainingCategoryComponent', () => { - async function setup(trainingCategoryId = '1') { + async function setup(overrides: any = {}) { const establishment = establishmentBuilder(); + const routerSpy = jasmine.createSpy('navigate').and.returnValue(Promise.resolve(true)); + + const existingMandatoryTraining = mockMandatoryTraining(); + const selectedTraining = existingMandatoryTraining.mandatoryTraining[0]; + const trainingIdInParams = selectedTraining.trainingCategoryId; const setupTools = await render(DeleteMandatoryTrainingCategoryComponent, { imports: [SharedModule, RouterModule, RouterTestingModule, AddMandatoryTrainingModule, HttpClientTestingModule], @@ -34,6 +40,7 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { provide: TrainingService, useClass: MockTrainingService, }, + { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, { provide: TrainingCategoryService, useClass: MockTrainingCategoryService, @@ -42,9 +49,12 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { provide: ActivatedRoute, useValue: { snapshot: { - params: { trainingCategoryId }, + params: { + trainingCategoryId: overrides.trainingCategoryId ?? trainingIdInParams, + }, data: { establishment, + existingMandatoryTraining: overrides.mandatoryTraining ?? existingMandatoryTraining, }, url: [{ path: 'delete-mandatory-training-category' }], }, @@ -61,9 +71,6 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { const trainingService = injector.inject(TrainingService) as TrainingService; const deleteMandatoryTrainingCategorySpy = spyOn(trainingService, 'deleteCategoryById').and.callThrough(); - const router = injector.inject(Router) as Router; - const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); - const component = setupTools.fixture.componentInstance; return { @@ -73,6 +80,7 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { deleteMandatoryTrainingCategorySpy, alertSpy, establishment, + selectedTraining, }; } @@ -87,16 +95,26 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { }); it('should display the correct training name when navigating to the page with a category ID', async () => { - const { getAllByText } = await setup(); - expect(getAllByText('Activity provision/Well-being', { exact: false }).length).toEqual(2); + const { getAllByText, selectedTraining } = await setup(); + + expect(getAllByText(selectedTraining.category, { exact: false }).length).toEqual(2); }); it('should render Remove categories button and cancel link', async () => { const { getByText } = await setup(); + expect(getByText('Remove category')).toBeTruthy(); expect(getByText('Cancel')).toBeTruthy(); }); + it('should navigate back to add-and-manage-mandatory-training page if training category not found in existing mandatory training', async () => { + const unexpectedTrainingCategoryId = '301'; + + const { getByText, routerSpy, establishment } = await setup({ trainingCategoryId: unexpectedTrainingCategoryId }); + + expect(routerSpy).toHaveBeenCalledWith(['/workplace', establishment.uid, 'add-and-manage-mandatory-training']); + }); + describe('On submit', () => { it('should call deleteCategoryById in the training service', async () => { const { getByText, deleteMandatoryTrainingCategorySpy } = await setup(); @@ -126,7 +144,7 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { describe('Cancel link', () => { it('should navigate back to the mandatory details summary page when clicked', async () => { - const { component, getByText, routerSpy, establishment } = await setup(); + const { getByText, routerSpy, establishment } = await setup(); userEvent.click(getByText('Cancel')); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts index 59de6b55a0..1db42da9f1 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Establishment } from '@core/model/establishment.model'; import { TrainingCategory } from '@core/model/training.model'; @@ -31,10 +31,15 @@ export class DeleteMandatoryTrainingCategoryComponent implements OnInit, OnDestr const trainingCategoryIdInParams = parseInt(this.route.snapshot.params?.trainingCategoryId); this.establishment = this.route.snapshot.data.establishment; + const existingMandatoryTraining = this.route.snapshot.data.existingMandatoryTraining; - this.trainingCategoryService.getCategories().subscribe((x) => { - this.selectedCategory = x.find((y) => y.id === trainingCategoryIdInParams); - }); + this.selectedCategory = existingMandatoryTraining?.mandatoryTraining.find( + (category) => category.trainingCategoryId === trainingCategoryIdInParams, + ); + + if (!this.selectedCategory) { + this.navigateBackToMandatoryTrainingHomePage(); + } } public onDelete(): void { @@ -53,7 +58,7 @@ export class DeleteMandatoryTrainingCategoryComponent implements OnInit, OnDestr this.router.navigate(['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training']); } - private ngOnDestroy(): void { + ngOnDestroy(): void { this.subscriptions.unsubscribe(); } } From 185c89b89ab1a37b03209c1d880ca931007ec9f0 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Tue, 17 Dec 2024 08:47:34 +0000 Subject: [PATCH 41/72] Display job roles if not all on delete page --- frontend/src/app/core/model/training.model.ts | 3 ++- .../add-mandatory-routing.module.ts | 1 + ...mandatory-training-category.component.html | 10 ++++++- ...datory-training-category.component.spec.ts | 27 +++++++++++++++++-- ...e-mandatory-training-category.component.ts | 23 +++++++++------- 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/core/model/training.model.ts b/frontend/src/app/core/model/training.model.ts index 76c53b5ec6..6b76d6d0c2 100644 --- a/frontend/src/app/core/model/training.model.ts +++ b/frontend/src/app/core/model/training.model.ts @@ -97,8 +97,9 @@ export interface mandatoryJobs { export interface mandatoryTraining { trainingCategoryId: number; - allJobRoles: boolean; + allJobRoles?: boolean; selectedJobRoles?: boolean; + category?: string; jobs: mandatoryJobs[]; } export interface allMandatoryTrainingCategories { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index f3bfd7bcb5..7400a05117 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -24,6 +24,7 @@ const routes: Routes = [ resolve: { existingMandatoryTraining: MandatoryTrainingCategoriesResolver, }, + runGuardsAndResolvers: 'always', children: [ { path: '', diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html index 7787c7ea91..38d3a8153c 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html @@ -17,7 +17,15 @@

You're about to remove this mandatory training categ
Job roles
-
All
+
+
    +
  • {{ job.title }}
  • +
+ All +
diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts index c33f8190f9..d7863282dc 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts @@ -115,12 +115,35 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { expect(routerSpy).toHaveBeenCalledWith(['/workplace', establishment.uid, 'add-and-manage-mandatory-training']); }); + describe('Displaying job roles', () => { + it('should display selected job roles for selected mandatory training when it is not for all job roles', async () => { + const { getByText, selectedTraining } = await setup(); + + selectedTraining.jobs.forEach((job) => { + expect(getByText(job.title)).toBeTruthy(); + }); + }); + + it('should display All when number of job roles for selected training matches allJobRolesCount', async () => { + const mandatoryTraining = mockMandatoryTraining(); + const selectedTraining = mandatoryTraining.mandatoryTraining[1]; + const trainingIdInParams = selectedTraining.trainingCategoryId; + + const { getByText } = await setup({ mandatoryTraining, trainingCategoryId: trainingIdInParams }); + + expect(getByText('All')).toBeTruthy(); + }); + }); + describe('On submit', () => { it('should call deleteCategoryById in the training service', async () => { - const { getByText, deleteMandatoryTrainingCategorySpy } = await setup(); + const { getByText, deleteMandatoryTrainingCategorySpy, establishment, selectedTraining } = await setup(); userEvent.click(getByText('Remove category')); - expect(deleteMandatoryTrainingCategorySpy).toHaveBeenCalled(); + expect(deleteMandatoryTrainingCategorySpy).toHaveBeenCalledWith( + establishment.id, + selectedTraining.trainingCategoryId, + ); }); it("should display a success banner with 'Mandatory training category removed'", async () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts index 1db42da9f1..4e255768ca 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Establishment } from '@core/model/establishment.model'; -import { TrainingCategory } from '@core/model/training.model'; +import { mandatoryTraining } from '@core/model/training.model'; import { AlertService } from '@core/services/alert.service'; import { BackLinkService } from '@core/services/backLink.service'; import { TrainingCategoryService } from '@core/services/training-category.service'; @@ -13,8 +13,9 @@ import { Subscription } from 'rxjs'; templateUrl: './delete-mandatory-training-category.component.html', }) export class DeleteMandatoryTrainingCategoryComponent implements OnInit, OnDestroy { - public selectedCategory: TrainingCategory; + public selectedCategory: mandatoryTraining; public establishment: Establishment; + public allJobRolesCount: number; private subscriptions: Subscription = new Subscription(); constructor( @@ -33,6 +34,8 @@ export class DeleteMandatoryTrainingCategoryComponent implements OnInit, OnDestr this.establishment = this.route.snapshot.data.establishment; const existingMandatoryTraining = this.route.snapshot.data.existingMandatoryTraining; + this.allJobRolesCount = existingMandatoryTraining?.allJobRolesCount; + this.selectedCategory = existingMandatoryTraining?.mandatoryTraining.find( (category) => category.trainingCategoryId === trainingCategoryIdInParams, ); @@ -44,13 +47,15 @@ export class DeleteMandatoryTrainingCategoryComponent implements OnInit, OnDestr public onDelete(): void { this.subscriptions.add( - this.trainingService.deleteCategoryById(this.establishment.id, this.selectedCategory.id).subscribe(() => { - this.navigateBackToMandatoryTrainingHomePage(); - this.alertService.addAlert({ - type: 'success', - message: 'Mandatory training category removed', - }); - }), + this.trainingService + .deleteCategoryById(this.establishment.id, this.selectedCategory.trainingCategoryId) + .subscribe(() => { + this.navigateBackToMandatoryTrainingHomePage(); + this.alertService.addAlert({ + type: 'success', + message: 'Mandatory training category removed', + }); + }), ); } From 5d354054d3af8ac3b7979f53661e51f69064ce01 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Tue, 17 Dec 2024 08:51:44 +0000 Subject: [PATCH 42/72] Remove unused test id --- .../delete-mandatory-training-category.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html index 38d3a8153c..e874ee59d8 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html @@ -13,7 +13,7 @@

You're about to remove this mandatory training categ
Training category
-
{{ selectedCategory?.category }}
+
{{ selectedCategory?.category }}
Job roles
From 87fe0ec4de42046577ea4b9dfab6144eeb69c282 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Tue, 17 Dec 2024 09:34:07 +0000 Subject: [PATCH 43/72] Update explanation message on Remove mandatory training page --- .../delete-mandatory-training-category.component.html | 5 ++--- .../delete-mandatory-training-category.component.spec.ts | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html index e874ee59d8..46a1912b8c 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.html @@ -6,9 +6,8 @@

You're about to remove this mandatory training categ
-

- If you do this, '{{ selectedCategory?.category | lowercase }}' will not be mandatory for any of your staff at the - moment. +

+ If you do this, '{{ selectedCategory?.category }}' will no longer be mandatory for any of your staff.

diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts index d7863282dc..fdb501b85b 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component.spec.ts @@ -94,10 +94,11 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { expect(getByText("You're about to remove this mandatory training category")).toBeTruthy(); }); - it('should display the correct training name when navigating to the page with a category ID', async () => { - const { getAllByText, selectedTraining } = await setup(); + it('should display a warning message with training category name', async () => { + const { getByText, selectedTraining } = await setup(); - expect(getAllByText(selectedTraining.category, { exact: false }).length).toEqual(2); + const expectedMessage = `If you do this, '${selectedTraining?.category}' will no longer be mandatory for any of your staff.`; + expect(getByText(expectedMessage)).toBeTruthy(); }); it('should render Remove categories button and cancel link', async () => { @@ -110,7 +111,7 @@ describe('DeleteMandatoryTrainingCategoryComponent', () => { it('should navigate back to add-and-manage-mandatory-training page if training category not found in existing mandatory training', async () => { const unexpectedTrainingCategoryId = '301'; - const { getByText, routerSpy, establishment } = await setup({ trainingCategoryId: unexpectedTrainingCategoryId }); + const { routerSpy, establishment } = await setup({ trainingCategoryId: unexpectedTrainingCategoryId }); expect(routerSpy).toHaveBeenCalledWith(['/workplace', establishment.uid, 'add-and-manage-mandatory-training']); }); From d4ae13b8303982e0190a998eec436e84ece38144 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Tue, 17 Dec 2024 09:59:22 +0000 Subject: [PATCH 44/72] Update wording of explanation message on main add mandatory training page --- .../add-and-manage-mandatory-training.component.html | 2 +- .../add-and-manage-mandatory-training.component.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html index 6f9186c84a..ce23a38763 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html @@ -9,7 +9,7 @@

- Add the training categories you want to make mandatory for your staff. It will help you identify who is missing + Add the training categories you want to make mandatory for your staff. It will help you to identify who is missing training and let you know when training expires.

diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 28f7abd6b9..c3adb6c97b 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -86,7 +86,7 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(mandatoryTrainingHeader.textContent).toContain('Add and manage mandatory training categories'); expect(mandatoryTrainingInfo.textContent).toContain( - 'Add the training categories you want to make mandatory for your staff. It will help you identify who is missing training and let you know when training expires.', + 'Add the training categories you want to make mandatory for your staff. It will help you to identify who is missing training and let you know when training expires.', ); }); From 06e6b51cb6dd52065e47ed623254e5c1fbe6dc22 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Tue, 17 Dec 2024 10:31:28 +0000 Subject: [PATCH 45/72] Update description message on remove all mandatory training page and refactor tests --- .../core/test-utils/MockTrainingService.ts | 4 ++ ...lete-all-mandatory-training.component.html | 4 +- ...e-all-mandatory-training.component.spec.ts | 56 ++++++++++--------- ...delete-all-mandatory-training.component.ts | 6 +- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/core/test-utils/MockTrainingService.ts b/frontend/src/app/core/test-utils/MockTrainingService.ts index cc9eb0021f..fee2566eda 100644 --- a/frontend/src/app/core/test-utils/MockTrainingService.ts +++ b/frontend/src/app/core/test-utils/MockTrainingService.ts @@ -46,6 +46,10 @@ export class MockTrainingService extends TrainingService { public deleteCategoryById(establishmentId, categoryId) { return of({}); } + + public deleteAllMandatoryTraining(establishmentId) { + return of({}); + } } @Injectable() diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.html index c90c3fa22f..c4dac72802 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/delete-mandatory-training/delete-all-mandatory-training.component.html @@ -1,9 +1,7 @@

You're about to remove all mandatory training categories for your workplace

-

- If you do this, your workplace will not have any training set up as being mandatory for your staff at the moment. -

+

If you do this, your workplace will no longer have any training set up as being mandatory for your staff.

Mandatory training category Job role +
{{ job.title }}
-
+ Date: Tue, 17 Dec 2024 11:17:20 +0000 Subject: [PATCH 47/72] Add test for not displaying table when no mandatory training set up --- ...anage-mandatory-training.component.spec.ts | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index c3adb6c97b..23f4c1172e 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -16,6 +16,13 @@ import { fireEvent, render } from '@testing-library/angular'; import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training.component'; describe('AddAndManageMandatoryTrainingComponent', () => { + const noMandatoryTraining = { + allJobRolesCount: 37, + lastUpdated: new Date(), + mandatoryTraining: [], + mandatoryTrainingCount: 0, + }; + async function setup(overrides: any = {}) { const establishment = establishmentBuilder() as Establishment; const existingMandatoryTraining = mockMandatoryTraining(); @@ -111,34 +118,37 @@ describe('AddAndManageMandatoryTrainingComponent', () => { }); it('should not show if no mandatory training set up', async () => { - const mandatoryTraining = { - allJobRolesCount: 37, - lastUpdated: new Date(), - mandatoryTraining: [], - mandatoryTrainingCount: 0, - }; - - const { queryByText } = await setup({ mandatoryTraining }); + const { queryByText } = await setup({ mandatoryTraining: noMandatoryTraining }); expect(queryByText('Remove all')).toBeFalsy(); }); }); - it('should show the manage mandatory training table', async () => { - const { getByTestId } = await setup(); + describe('Mandatory training table', () => { + it('should show the manage mandatory training table', async () => { + const { getByTestId } = await setup(); - const mandatoryTrainingTable = getByTestId('training-table'); + const mandatoryTrainingTable = getByTestId('training-table'); - expect(mandatoryTrainingTable).toBeTruthy(); - }); + expect(mandatoryTrainingTable).toBeTruthy(); + }); - it('should show the manage mandatory training table headings', async () => { - const { getByTestId } = await setup(); + it('should show the manage mandatory training table headings', async () => { + const { getByTestId } = await setup(); + + const mandatoryTrainingTableHeading = getByTestId('training-table-heading'); - const mandatoryTrainingTableHeading = getByTestId('training-table-heading'); + expect(mandatoryTrainingTableHeading.textContent).toContain('Mandatory training category'); + expect(mandatoryTrainingTableHeading.textContent).toContain('Job role'); + }); - expect(mandatoryTrainingTableHeading.textContent).toContain('Mandatory training category'); - expect(mandatoryTrainingTableHeading.textContent).toContain('Job role'); + it('should not show if no mandatory training set up', async () => { + const { queryByTestId } = await setup({ mandatoryTraining: noMandatoryTraining }); + + const mandatoryTrainingTableHeadings = queryByTestId('training-table-heading'); + + expect(mandatoryTrainingTableHeadings).toBeFalsy(); + }); }); describe('mandatory training table records', () => { From 62f604516f7eab3e71ffd1f7741296af3f854902 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Tue, 17 Dec 2024 17:59:51 +0000 Subject: [PATCH 48/72] Navigate to select training category page and pass data in params when clicking into existing mandatory training --- ...d-manage-mandatory-training.component.html | 4 +- ...anage-mandatory-training.component.spec.ts | 15 ++++++ ...and-manage-mandatory-training.component.ts | 18 +++++-- ...ining-category-mandatory.component.spec.ts | 48 ++++++++++++++----- ...t-training-category-mandatory.component.ts | 18 +++++-- 5 files changed, 82 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html index 99aa401165..10535c46bd 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html @@ -19,7 +19,7 @@

style="width: 37%" data-testid="mandatoryTrainingButton" > - @@ -55,7 +55,7 @@

>

- + {{ records.category }} Job role { }); it("should navigate to the select-training-category page when 'Add a mandatory training category' link is clicked", async () => { - const { getByText, routerSpy, currentRoute } = await setup(); + const { getByRole, routerSpy, currentRoute } = await setup(); - const addMandatoryTrainingButton = getByText('Add a mandatory training category'); + const addMandatoryTrainingButton = getByRole('button', { name: 'Add a mandatory training category' }); fireEvent.click(addMandatoryTrainingButton); expect(routerSpy).toHaveBeenCalledWith(['select-training-category'], { relativeTo: currentRoute }); @@ -122,6 +122,17 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(queryByText('Remove all')).toBeFalsy(); }); + + it('should not show if mandatory training only set up for one training category', async () => { + const existingMandatoryTraining = mockMandatoryTraining(); + + existingMandatoryTraining.mandatoryTraining = [existingMandatoryTraining.mandatoryTraining[0]]; + existingMandatoryTraining.mandatoryTrainingCount = 1; + + const { queryByText } = await setup({ mandatoryTraining: existingMandatoryTraining }); + + expect(queryByText('Remove all')).toBeFalsy(); + }); }); describe('Mandatory training table', () => { From 5d80c51909edf54c38ba6e636040bd03b2e6f760 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 18 Dec 2024 15:26:26 +0000 Subject: [PATCH 51/72] Update select-training-category page to prefill existing mandatory training set in training service rather than params --- ...ining-category-mandatory.component.spec.ts | 13 ++++++------ ...t-training-category-mandatory.component.ts | 21 +++++++++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index 793d20818a..f4121530de 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -11,7 +11,7 @@ import { WindowRef } from '@core/services/window.ref'; import { WorkerService } from '@core/services/worker.service'; import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; import { trainingCategories } from '@core/test-utils/MockTrainingCategoriesService'; -import { MockTrainingService, MockTrainingServiceWithPreselectedStaff } from '@core/test-utils/MockTrainingService'; +import { MockMandatoryTrainingService, MockTrainingServiceWithPreselectedStaff } from '@core/test-utils/MockTrainingService'; import { MockWorkerService } from '@core/test-utils/MockWorkerService'; import { GroupedRadioButtonAccordionComponent, @@ -43,7 +43,9 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { }, { provide: MandatoryTrainingService, - useClass: overrides.prefill ? MockTrainingServiceWithPreselectedStaff : MockTrainingService, + useFactory: overrides.prefill + ? MockTrainingServiceWithPreselectedStaff.factory() + : MockMandatoryTrainingService.factory(overrides.trainingService), }, { provide: ActivatedRoute, @@ -234,14 +236,11 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { expect(queryByText(mockTrainingCategories[2].category)).toBeFalsy(); }); - it('should include training category and prefill the radio when ID of existing mandatory training category in params', async () => { + it('should include training category and prefill the radio when existing mandatory training set in service', async () => { const overrides = { trainingCategories: mockTrainingCategories, existingMandatoryTraining, - selectedTraining: JSON.stringify({ - id: mockTrainingCategories[2].id, - category: mockTrainingCategories[2].category, - }), + trainingService: { existingMandatoryTraining: existingMandatoryTraining.mandatoryTraining[0] }, }; const { component, queryByText } = await setup(overrides); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index 661cbffd4b..4304954693 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -32,14 +32,16 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate init(): void { this.establishmentUid = this.route.snapshot.data.establishment.uid; + this.getPrefilledId(); + } - if (this.route.snapshot.queryParamMap.get('trainingCategory')) { - const mandatoryTrainingCategory = JSON.parse(this.route.snapshot.queryParamMap.get('trainingCategory')); - const categoryId = parseInt(mandatoryTrainingCategory?.id, 10); + protected getPrefilledId(): void { + const selectedCategory = this.trainingService.selectedTraining?.trainingCategory; - if (categoryId) { - this.preFilledId = categoryId; - } + if (selectedCategory) { + this.preFilledId = selectedCategory?.id; + } else if (this.trainingService.existingMandatoryTraining) { + this.preFilledId = this.trainingService.existingMandatoryTraining.trainingCategoryId; } } @@ -71,6 +73,13 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate this.sortCategoriesByTrainingGroup(this.categories); } + protected prefillForm(): void { + if (this.preFilledId) { + this.form.setValue({ category: this.preFilledId }); + this.form.get('category').updateValueAndValidity(); + } + } + public onCancel(event: Event): void { event.preventDefault(); this.trainingService.resetState(); From d7b28fea4c5249ce11a0bb14f282cd5e9f9cde83 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 19 Dec 2024 15:39:44 +0000 Subject: [PATCH 52/72] Prefill with selectedTraining over existing mandatory category for case where user has changed category and then goes back to page --- ...ining-category-mandatory.component.spec.ts | 21 +++++++++++++++++++ ...t-training-category-mandatory.component.ts | 8 +++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index f4121530de..517670a050 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -249,5 +249,26 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { expect(selectedExistingMandatoryTrainingCategory).toBeTruthy(); expect(component.form.value).toEqual({ category: mockTrainingCategories[2].id }); }); + + it('should prefill the category in selectedTraining but still include existing mandatory training category in options when both set in service (user changed category and has gone back to page)', async () => { + const selectedTraining = { + trainingCategory: mockTrainingCategories[0], + }; + + const overrides = { + trainingCategories: mockTrainingCategories, + existingMandatoryTraining, + trainingService: { + existingMandatoryTraining: existingMandatoryTraining.mandatoryTraining[0], + _selectedTraining: selectedTraining, + }, + }; + + const { component, queryByText } = await setup(overrides); + + const selectedExistingMandatoryTrainingCategory = queryByText(mockTrainingCategories[2].category); + expect(selectedExistingMandatoryTrainingCategory).toBeTruthy(); + expect(component.form.value).toEqual({ category: selectedTraining.trainingCategory.id }); + }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index 4304954693..94f367c2ef 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -29,9 +29,12 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate public requiredErrorMessage: string = 'Select the training category that you want to make mandatory'; public hideOtherCheckbox: boolean = true; + private existingMandatoryTrainingCategoryId: number; init(): void { this.establishmentUid = this.route.snapshot.data.establishment.uid; + this.existingMandatoryTrainingCategoryId = + this.trainingService.existingMandatoryTraining?.trainingCategoryId ?? null; this.getPrefilledId(); } @@ -40,7 +43,7 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate if (selectedCategory) { this.preFilledId = selectedCategory?.id; - } else if (this.trainingService.existingMandatoryTraining) { + } else if (this.existingMandatoryTrainingCategoryId) { this.preFilledId = this.trainingService.existingMandatoryTraining.trainingCategoryId; } } @@ -64,7 +67,8 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate if (trainingCategoryIdsWithExistingMandatoryTraining?.length) { this.categories = allTrainingCategories.filter( (category) => - !trainingCategoryIdsWithExistingMandatoryTraining.includes(category.id) || category.id == this.preFilledId, + !trainingCategoryIdsWithExistingMandatoryTraining.includes(category.id) || + category.id == this.existingMandatoryTrainingCategoryId, ); } else { this.categories = allTrainingCategories; From f0ec0ce981cd98260f513e113474580c4617e81e Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 19 Dec 2024 16:13:55 +0000 Subject: [PATCH 53/72] Use mock mandatory training service in all-or-selected-job-roles --- .../all-or-selected-job-roles.component.spec.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index f076a9f951..1277d05031 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -8,6 +8,7 @@ import { MandatoryTrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { MockRouter } from '@core/test-utils/MockRouter'; +import { MockMandatoryTrainingService } from '@core/test-utils/MockTrainingService'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; @@ -34,14 +35,7 @@ describe('AllOrSelectedJobRolesComponent', () => { providers: [ { provide: MandatoryTrainingService, - useValue: { - selectedTraining: overrides.selectedTraining !== undefined ? overrides.selectedTraining : selectedTraining, - resetState: () => {}, - set onlySelectedJobRoles(onlySelected) {}, - get onlySelectedJobRoles() { - return overrides.onlySelectedJobRoles ?? false; - }, - }, + useFactory: MockMandatoryTrainingService.factory({ selectedTraining, ...overrides }), }, { provide: Router, useFactory: MockRouter.factory({ navigate: routerSpy }) }, { provide: EstablishmentService, useClass: MockEstablishmentService }, From 613c2233ca1bbcb104aeb0dd190f161c59b264b1 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 19 Dec 2024 16:51:09 +0000 Subject: [PATCH 54/72] Update all-or-selected-job-roles to prefill when editing existing mandatory training --- .../src/app/core/services/training.service.ts | 2 + ...ll-or-selected-job-roles.component.spec.ts | 44 +++++++++++++++++++ .../all-or-selected-job-roles.component.ts | 7 +++ ...t-training-category-mandatory.component.ts | 2 + 4 files changed, 55 insertions(+) diff --git a/frontend/src/app/core/services/training.service.ts b/frontend/src/app/core/services/training.service.ts index 537af76e8a..2fafc0df8a 100644 --- a/frontend/src/app/core/services/training.service.ts +++ b/frontend/src/app/core/services/training.service.ts @@ -134,6 +134,7 @@ export class TrainingService { export class MandatoryTrainingService extends TrainingService { _onlySelectedJobRoles: boolean = null; _existingMandatoryTraining: mandatoryTraining = null; + public allJobRolesCount: number; public get onlySelectedJobRoles(): boolean { return this._onlySelectedJobRoles; @@ -145,6 +146,7 @@ export class MandatoryTrainingService extends TrainingService { public resetState(): void { this.onlySelectedJobRoles = null; + this.existingMandatoryTraining = null; super.resetState(); } diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 1277d05031..72854a9b6a 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -176,6 +176,50 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(onlySelectedJobRolesRadio.checked).toBeTruthy(); expect(allJobRolesRadio.checked).toBeFalsy(); }); + + const existingMandatoryTraining = { + category: 'Activity provision/Well-being', + establishmentId: 4090, + jobs: [{}, {}], + trainingCategoryId: 1, + }; + + it("should prefill the 'Only selected job roles' radio when existingMandatoryTraining in training service does not match allJobRolesCount", async () => { + const { getByLabelText } = await setup({ existingMandatoryTraining, allJobRolesCount: 37 }); + + const onlySelectedJobRolesRadio = getByLabelText('Only selected job roles') as HTMLInputElement; + const allJobRolesRadio = getByLabelText('All job roles') as HTMLInputElement; + + expect(onlySelectedJobRolesRadio.checked).toBeTruthy(); + expect(allJobRolesRadio.checked).toBeFalsy(); + }); + + it("should prefill the 'All job roles' radio when existingMandatoryTraining in training service does match allJobRolesCount", async () => { + const { getByLabelText } = await setup({ + existingMandatoryTraining, + allJobRolesCount: existingMandatoryTraining.jobs.length, + }); + + const allJobRolesRadio = getByLabelText('All job roles') as HTMLInputElement; + const onlySelectedJobRolesRadio = getByLabelText('Only selected job roles') as HTMLInputElement; + + expect(allJobRolesRadio.checked).toBeTruthy(); + expect(onlySelectedJobRolesRadio.checked).toBeFalsy(); + }); + + it("should prefill the 'Only selected job roles' radio when set in training service even if existingMandatoryTraining (for case when user has changed from all to selected and then gone back to this page)", async () => { + const { getByLabelText } = await setup({ + existingMandatoryTraining, + allJobRolesCount: existingMandatoryTraining.jobs.length, + onlySelectedJobRoles: true, + }); + + const onlySelectedJobRolesRadio = getByLabelText('Only selected job roles') as HTMLInputElement; + const allJobRolesRadio = getByLabelText('All job roles') as HTMLInputElement; + + expect(onlySelectedJobRolesRadio.checked).toBeTruthy(); + expect(allJobRolesRadio.checked).toBeFalsy(); + }); }); describe('Cancel button', () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index 4335bd3500..508cb0ebd7 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -46,9 +46,16 @@ export class AllOrSelectedJobRolesComponent { ngOnInit(): void { this.establishment = this.route.snapshot.parent?.data?.establishment; this.selectedTrainingCategory = this.trainingService.selectedTraining; + const existingMandatoryTraining = this.trainingService.existingMandatoryTraining; + const allJobRolesCount = this.trainingService.allJobRolesCount; if (this.trainingService.onlySelectedJobRoles) { this.form.setValue({ allOrSelectedJobRoles: 'selectJobRoles' }); + } else if (existingMandatoryTraining) { + this.form.setValue({ + allOrSelectedJobRoles: + existingMandatoryTraining.jobs.length == allJobRolesCount ? 'allJobRoles' : 'selectJobRoles', + }); } if (!this.selectedTrainingCategory) { diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index 94f367c2ef..0a700470ad 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -60,6 +60,8 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate const allTrainingCategories = this.route.snapshot.data.trainingCategories; const existingMandatoryTraining = this.route.snapshot.data.existingMandatoryTraining; + this.trainingService.allJobRolesCount = existingMandatoryTraining.allJobRolesCount; + const trainingCategoryIdsWithExistingMandatoryTraining = existingMandatoryTraining?.mandatoryTraining?.map( (existingMandatoryTrainings) => existingMandatoryTrainings.trainingCategoryId, ); From 5d0578ec66ae0fa1ad49fa6606222b5c19da1aec Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Thu, 19 Dec 2024 17:26:48 +0000 Subject: [PATCH 55/72] Pass in previous training category ID to service call so category is replaced when new category selected for existing mandatory training --- .../src/app/core/services/training.service.ts | 8 ++--- ...ll-or-selected-job-roles.component.spec.ts | 23 ++++++++++++ .../all-or-selected-job-roles.component.ts | 35 +++++++++++++------ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/core/services/training.service.ts b/frontend/src/app/core/services/training.service.ts index 2fafc0df8a..5683c33dc1 100644 --- a/frontend/src/app/core/services/training.service.ts +++ b/frontend/src/app/core/services/training.service.ts @@ -1,12 +1,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; -import { - allMandatoryTrainingCategories, - mandatoryTraining, - SelectedTraining, - TrainingCategory, -} from '@core/model/training.model'; +import { mandatoryTraining } from '@core/model/establishment.model'; +import { allMandatoryTrainingCategories, SelectedTraining, TrainingCategory } from '@core/model/training.model'; import { Worker } from '@core/model/worker.model'; import { BehaviorSubject, Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 72854a9b6a..16d17d4b3c 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -353,5 +353,28 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(onlySelectedJobRolesSpy).toHaveBeenCalledWith(true); }); }); + + it('should include previousTrainingCategoryId in submit props when editing existing mandatory training and All job roles selected', async () => { + const existingMandatoryTraining = { + category: 'Activity provision/Well-being', + establishmentId: 4090, + jobs: [{}, {}], + trainingCategoryId: 1, + }; + + const { getByText, createAndUpdateMandatoryTrainingSpy, establishment, selectedTraining } = await setup({ + existingMandatoryTraining, + allJobRolesCount: existingMandatoryTraining.jobs.length, + }); + + fireEvent.click(getByText('Continue')); + + expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { + previousTrainingCategoryId: existingMandatoryTraining.trainingCategoryId, + trainingCategoryId: selectedTraining.trainingCategory.id, + allJobRoles: true, + jobs: [], + }); + }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index 508cb0ebd7..eee942cd8d 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -2,7 +2,7 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; -import { Establishment } from '@core/model/establishment.model'; +import { Establishment, mandatoryTraining } from '@core/model/establishment.model'; import { SelectedTraining } from '@core/model/training.model'; import { URLStructure } from '@core/model/url.model'; import { AlertService } from '@core/services/alert.service'; @@ -29,6 +29,7 @@ export class AllOrSelectedJobRolesComponent { public serverError: string; private subscriptions: Subscription = new Subscription(); private establishment: Establishment; + private existingMandatoryTraining: mandatoryTraining; constructor( private formBuilder: UntypedFormBuilder, @@ -46,16 +47,20 @@ export class AllOrSelectedJobRolesComponent { ngOnInit(): void { this.establishment = this.route.snapshot.parent?.data?.establishment; this.selectedTrainingCategory = this.trainingService.selectedTraining; - const existingMandatoryTraining = this.trainingService.existingMandatoryTraining; + this.existingMandatoryTraining = this.trainingService.existingMandatoryTraining; const allJobRolesCount = this.trainingService.allJobRolesCount; if (this.trainingService.onlySelectedJobRoles) { this.form.setValue({ allOrSelectedJobRoles: 'selectJobRoles' }); - } else if (existingMandatoryTraining) { + this.selectedRadio = 'selectJobRoles'; + } else if (this.existingMandatoryTraining) { + const selected = + this.existingMandatoryTraining.jobs.length == allJobRolesCount ? 'allJobRoles' : 'selectJobRoles'; + this.form.setValue({ - allOrSelectedJobRoles: - existingMandatoryTraining.jobs.length == allJobRolesCount ? 'allJobRoles' : 'selectJobRoles', + allOrSelectedJobRoles: selected, }); + this.selectedRadio = selected; } if (!this.selectedTrainingCategory) { @@ -99,11 +104,7 @@ export class AllOrSelectedJobRolesComponent { } private createMandatoryTraining(): void { - const props = { - trainingCategoryId: this.selectedTrainingCategory.trainingCategory.id, - allJobRoles: true, - jobs: [], - }; + const props = this.generateUpdateProps(); this.subscriptions.add( this.establishmentService.createAndUpdateMandatoryTraining(this.establishment.uid, props).subscribe( @@ -123,6 +124,20 @@ export class AllOrSelectedJobRolesComponent { ); } + private generateUpdateProps(): mandatoryTraining { + const props: mandatoryTraining = { + trainingCategoryId: this.selectedTrainingCategory.trainingCategory.id, + allJobRoles: true, + jobs: [], + }; + + if (this.existingMandatoryTraining?.trainingCategoryId) { + props.previousTrainingCategoryId = this.existingMandatoryTraining.trainingCategoryId; + } + + return props; + } + public onCancel(event: Event): void { event.preventDefault(); From 26d06a849a50183a4c0c34cfcad009c590e4e57e Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 20 Dec 2024 09:25:10 +0000 Subject: [PATCH 56/72] Prefill on select-job-roles page when editing existing mandatory training --- .../select-job-roles-mandatory.component.html | 2 +- ...lect-job-roles-mandatory.component.spec.ts | 59 +++++++++++++++---- .../select-job-roles-mandatory.component.ts | 20 ++++++- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html index 39eaf4e6cf..9728faa2ad 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.html @@ -26,7 +26,7 @@

Select the job roles that need this training *ngFor="let group of jobGroups" [title]="group.title" [description]="group.descriptionText" - [expandedAtStart]="false" + [expandedAtStart]="jobGroupsToOpenAtStart?.includes(group.title)" >
diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index d79626e884..3c109423fe 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -12,12 +12,9 @@ import { MandatoryTrainingService } from '@core/services/training.service'; import { WindowRef } from '@core/services/window.ref'; import { establishmentBuilder, MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; import { MockRouter } from '@core/test-utils/MockRouter'; -import { - GroupedRadioButtonAccordionComponent, -} from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; -import { - RadioButtonAccordionComponent, -} from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; +import { MockMandatoryTrainingService } from '@core/test-utils/MockTrainingService'; +import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; +import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; import { SharedModule } from '@shared/shared.module'; import { render, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; @@ -80,10 +77,7 @@ describe('SelectJobRolesMandatoryComponent', () => { { provide: EstablishmentService, useClass: MockEstablishmentService }, { provide: MandatoryTrainingService, - useValue: { - selectedTraining: overrides.selectedTraining !== undefined ? overrides.selectedTraining : selectedTraining, - resetState: () => {}, - }, + useFactory: MockMandatoryTrainingService.factory({ selectedTraining, ...overrides }), }, { provide: ActivatedRoute, @@ -318,4 +312,49 @@ describe('SelectJobRolesMandatoryComponent', () => { expect(resetStateInTrainingServiceSpy).toHaveBeenCalled(); }); }); + + describe('Existing mandatory training', () => { + const createExistingMandatoryTraining = (jobs) => { + return { + category: 'Activity provision/Well-being', + establishmentId: 4090, + jobs, + trainingCategoryId: 1, + }; + }; + + it('should check the currently selected job roles if existingMandatoryTraining in training service (when editing existing mandatory training)', async () => { + const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; + + const { getByLabelText } = await setup({ existingMandatoryTraining: createExistingMandatoryTraining(jobs) }); + + jobs.forEach((jobRole) => { + const jobRoleCheckbox = getByLabelText(jobRole.title) as HTMLInputElement; + expect(jobRoleCheckbox.checked).toBeTruthy(); + }); + }); + + it('should expand the accordion for job groups that have job roles selected', async () => { + const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; + + const { getByLabelText } = await setup({ existingMandatoryTraining: createExistingMandatoryTraining(jobs) }); + + jobs.forEach((jobRole) => { + const jobRoleGroupAccordionSection = getByLabelText(jobRole.jobRoleGroup); + expect(within(jobRoleGroupAccordionSection).getByText('Hide')).toBeTruthy(); // is expanded + }); + }); + + it('should not expand the accordion for job groups that do not have job roles selected', async () => { + const jobs = [mockAvailableJobs[0]]; + + const { getByLabelText } = await setup({ existingMandatoryTraining: createExistingMandatoryTraining(jobs) }); + + const jobRoleGroupAccordionSectionWithPreselected = getByLabelText(jobs[0].jobRoleGroup); + expect(within(jobRoleGroupAccordionSectionWithPreselected).getByText('Hide')).toBeTruthy(); // is expanded + + const jobRoleGroupAccordionSectionWithNoneSelected = getByLabelText(mockAvailableJobs[1].jobRoleGroup); + expect(within(jobRoleGroupAccordionSectionWithNoneSelected).getByText('Show')).toBeTruthy(); // not expanded + }); + }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 65c89eaeac..8b604b2c7b 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -2,7 +2,7 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; -import { Establishment } from '@core/model/establishment.model'; +import { Establishment, mandatoryTraining } from '@core/model/establishment.model'; import { Job, JobGroup } from '@core/model/job.model'; import { SelectedTraining } from '@core/model/training.model'; import { AlertService } from '@core/services/alert.service'; @@ -45,6 +45,8 @@ export class SelectJobRolesMandatoryComponent { public subscriptions: Subscription = new Subscription(); private establishment: Establishment; private selectedTrainingCategory: SelectedTraining; + private existingMandatoryTraining: mandatoryTraining; + public jobGroupsToOpenAtStart: string[] = []; ngOnInit(): void { this.selectedTrainingCategory = this.trainingService.selectedTraining; @@ -57,6 +59,11 @@ export class SelectJobRolesMandatoryComponent { this.backLinkService.showBackLink(); this.getJobs(); this.setupForm(); + this.existingMandatoryTraining = this.trainingService.existingMandatoryTraining; + + if (this.existingMandatoryTraining) { + this.prefillForm(); + } } private getJobs(): void { @@ -133,6 +140,17 @@ export class SelectJobRolesMandatoryComponent { ); } + private prefillForm(): void { + this.selectedJobIds = this.existingMandatoryTraining.jobs.map((job) => Number(job.id)); + this.jobGroupsToOpenAtStart = this.jobGroups + .filter((group) => group.items.some((job) => this.selectedJobIds.includes(job.id))) + .map((group) => group.title); + + this.form.patchValue({ + selectedJobRoles: this.selectedJobIds, + }); + } + public onCancel(event: Event): void { event.preventDefault(); From d668d9129ea482236dc8fc21f68017fbc83e6b9d Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 20 Dec 2024 09:36:29 +0000 Subject: [PATCH 57/72] Set previousTrainingCategoryId when submitting for existing mandatory training on select-job-roles page --- ...lect-job-roles-mandatory.component.spec.ts | 18 ++++++++++++++ .../select-job-roles-mandatory.component.ts | 24 +++++++++++++------ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index 3c109423fe..b98b8d2d46 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -356,5 +356,23 @@ describe('SelectJobRolesMandatoryComponent', () => { const jobRoleGroupAccordionSectionWithNoneSelected = getByLabelText(mockAvailableJobs[1].jobRoleGroup); expect(within(jobRoleGroupAccordionSectionWithNoneSelected).getByText('Show')).toBeTruthy(); // not expanded }); + + it('should call createAndUpdateMandatoryTraining with training category in service and previous training ID', async () => { + const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; + const existingMandatoryTraining = createExistingMandatoryTraining(jobs); + + const { getByText, establishment, selectedTraining, createAndUpdateMandatoryTrainingSpy } = await setup({ + existingMandatoryTraining, + }); + + userEvent.click(getByText('Save mandatory training')); + + expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { + previousTrainingCategoryId: existingMandatoryTraining.trainingCategoryId, + trainingCategoryId: selectedTraining.trainingCategory.id, + allJobRoles: false, + jobs: [{ id: jobs[0].id }, { id: jobs[1].id }], + }); + }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 8b604b2c7b..dec1c4e309 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -114,13 +114,7 @@ export class SelectJobRolesMandatoryComponent { } private createMandatoryTraining(): void { - const props = { - trainingCategoryId: this.selectedTrainingCategory.trainingCategory.id, - allJobRoles: false, - jobs: this.selectedJobIds.map((id) => { - return { id }; - }), - }; + const props = this.generateUpdateProps(); this.subscriptions.add( this.establishmentService.createAndUpdateMandatoryTraining(this.establishment.uid, props).subscribe( @@ -140,6 +134,22 @@ export class SelectJobRolesMandatoryComponent { ); } + private generateUpdateProps(): mandatoryTraining { + const props: mandatoryTraining = { + trainingCategoryId: this.selectedTrainingCategory.trainingCategory.id, + allJobRoles: false, + jobs: this.selectedJobIds.map((id) => { + return { id }; + }), + }; + + if (this.existingMandatoryTraining?.trainingCategoryId) { + props.previousTrainingCategoryId = this.existingMandatoryTraining.trainingCategoryId; + } + + return props; + } + private prefillForm(): void { this.selectedJobIds = this.existingMandatoryTraining.jobs.map((job) => Number(job.id)); this.jobGroupsToOpenAtStart = this.jobGroups From c12298d9f30855122cd19da0a97fd16be7e71604 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 20 Dec 2024 10:12:59 +0000 Subject: [PATCH 58/72] Don't prefill job roles if all job roles previously selected for existing mandatory training --- ...lect-job-roles-mandatory.component.spec.ts | 19 ++++++++++++++++++- .../select-job-roles-mandatory.component.ts | 2 ++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index b98b8d2d46..28e43c40ca 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -326,7 +326,10 @@ describe('SelectJobRolesMandatoryComponent', () => { it('should check the currently selected job roles if existingMandatoryTraining in training service (when editing existing mandatory training)', async () => { const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; - const { getByLabelText } = await setup({ existingMandatoryTraining: createExistingMandatoryTraining(jobs) }); + const { getByLabelText } = await setup({ + existingMandatoryTraining: createExistingMandatoryTraining(jobs), + allJobRolesCount: 37, + }); jobs.forEach((jobRole) => { const jobRoleCheckbox = getByLabelText(jobRole.title) as HTMLInputElement; @@ -334,6 +337,20 @@ describe('SelectJobRolesMandatoryComponent', () => { }); }); + it('should not check the currently selected job roles if existingMandatoryTraining has all job roles (when editing existing mandatory training)', async () => { + const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; + + const { getByLabelText } = await setup({ + existingMandatoryTraining: createExistingMandatoryTraining(jobs), + allJobRolesCount: 2, + }); + + jobs.forEach((jobRole) => { + const jobRoleCheckbox = getByLabelText(jobRole.title) as HTMLInputElement; + expect(jobRoleCheckbox.checked).toBeFalsy(); + }); + }); + it('should expand the accordion for job groups that have job roles selected', async () => { const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index dec1c4e309..bcc0d9b2ac 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -151,6 +151,8 @@ export class SelectJobRolesMandatoryComponent { } private prefillForm(): void { + if (this.existingMandatoryTraining.jobs?.length == this.trainingService.allJobRolesCount) return; + this.selectedJobIds = this.existingMandatoryTraining.jobs.map((job) => Number(job.id)); this.jobGroupsToOpenAtStart = this.jobGroups .filter((group) => group.items.some((job) => this.selectedJobIds.includes(job.id))) From efae1d29eb03aba18c354503974140a09a6a3200 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 20 Dec 2024 16:04:51 +0000 Subject: [PATCH 59/72] Rename field in training service to mandatoryTrainingBeingEdited to differentiate between that and existingMandatoryTraining used to represent all existing mandatory training --- .../src/app/core/services/training.service.ts | 12 ++++----- ...anage-mandatory-training.component.spec.ts | 8 +++--- ...and-manage-mandatory-training.component.ts | 6 ++--- ...ll-or-selected-job-roles.component.spec.ts | 26 +++++++++---------- .../all-or-selected-job-roles.component.ts | 12 ++++----- ...lect-job-roles-mandatory.component.spec.ts | 26 +++++++++++-------- .../select-job-roles-mandatory.component.ts | 14 +++++----- ...ining-category-mandatory.component.spec.ts | 17 ++++++------ ...t-training-category-mandatory.component.ts | 19 ++++++-------- 9 files changed, 70 insertions(+), 70 deletions(-) diff --git a/frontend/src/app/core/services/training.service.ts b/frontend/src/app/core/services/training.service.ts index 5683c33dc1..06687c6a48 100644 --- a/frontend/src/app/core/services/training.service.ts +++ b/frontend/src/app/core/services/training.service.ts @@ -129,7 +129,7 @@ export class TrainingService { export class MandatoryTrainingService extends TrainingService { _onlySelectedJobRoles: boolean = null; - _existingMandatoryTraining: mandatoryTraining = null; + _mandatoryTrainingBeingEdited: mandatoryTraining = null; public allJobRolesCount: number; public get onlySelectedJobRoles(): boolean { @@ -142,15 +142,15 @@ export class MandatoryTrainingService extends TrainingService { public resetState(): void { this.onlySelectedJobRoles = null; - this.existingMandatoryTraining = null; + this.mandatoryTrainingBeingEdited = null; super.resetState(); } - public set existingMandatoryTraining(mandatoryTraining) { - this._existingMandatoryTraining = mandatoryTraining; + public set mandatoryTrainingBeingEdited(mandatoryTraining) { + this._mandatoryTrainingBeingEdited = mandatoryTraining; } - public get existingMandatoryTraining(): mandatoryTraining { - return this._existingMandatoryTraining; + public get mandatoryTrainingBeingEdited(): mandatoryTraining { + return this._mandatoryTrainingBeingEdited; } } diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts index 08f9497d22..d9a1a1519e 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.spec.ts @@ -178,20 +178,20 @@ describe('AddAndManageMandatoryTrainingComponent', () => { expect(autismCategory.textContent).toContain('Activities worker, coordinator'); }); - it('should navigate to select-training-category and set existing mandatory training in service when category link clicked', async () => { + it('should navigate to select-training-category and set mandatory training being edited in service when category link clicked', async () => { const { getByText, existingMandatoryTraining, routerSpy, currentRoute, injector } = await setup(); const mandatoryTrainingService = injector.inject(MandatoryTrainingService) as MandatoryTrainingService; - const setExistingMandatoryTrainingSpy = spyOnProperty( + const setMandatoryTrainingBeingEditedSpy = spyOnProperty( mandatoryTrainingService, - 'existingMandatoryTraining', + 'mandatoryTrainingBeingEdited', 'set', ).and.stub(); existingMandatoryTraining.mandatoryTraining.forEach((trainingCategory) => { fireEvent.click(getByText(trainingCategory.category)); - expect(setExistingMandatoryTrainingSpy).toHaveBeenCalledWith(trainingCategory); + expect(setMandatoryTrainingBeingEditedSpy).toHaveBeenCalledWith(trainingCategory); expect(routerSpy).toHaveBeenCalledWith(['select-training-category'], { relativeTo: currentRoute, }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts index f477684d0f..20b8d7f382 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.ts @@ -66,11 +66,11 @@ export class AddAndManageMandatoryTrainingComponent implements OnInit { }); } - public navigateToAddNewMandatoryTraining(event: Event, existingMandatoryTraining = null): void { + public navigateToAddNewMandatoryTraining(event: Event, mandatoryTrainingToEdit = null): void { event.preventDefault(); - if (existingMandatoryTraining) { - this.trainingService.existingMandatoryTraining = existingMandatoryTraining; + if (mandatoryTrainingToEdit) { + this.trainingService.mandatoryTrainingBeingEdited = mandatoryTrainingToEdit; } this.router.navigate(['select-training-category'], { relativeTo: this.route }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 16d17d4b3c..788018ccd1 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -177,15 +177,15 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(allJobRolesRadio.checked).toBeFalsy(); }); - const existingMandatoryTraining = { + const mandatoryTrainingBeingEdited = { category: 'Activity provision/Well-being', establishmentId: 4090, jobs: [{}, {}], trainingCategoryId: 1, }; - it("should prefill the 'Only selected job roles' radio when existingMandatoryTraining in training service does not match allJobRolesCount", async () => { - const { getByLabelText } = await setup({ existingMandatoryTraining, allJobRolesCount: 37 }); + it("should prefill the 'Only selected job roles' radio when mandatoryTrainingBeingEdited in training service does not match allJobRolesCount", async () => { + const { getByLabelText } = await setup({ mandatoryTrainingBeingEdited, allJobRolesCount: 37 }); const onlySelectedJobRolesRadio = getByLabelText('Only selected job roles') as HTMLInputElement; const allJobRolesRadio = getByLabelText('All job roles') as HTMLInputElement; @@ -194,10 +194,10 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(allJobRolesRadio.checked).toBeFalsy(); }); - it("should prefill the 'All job roles' radio when existingMandatoryTraining in training service does match allJobRolesCount", async () => { + it("should prefill the 'All job roles' radio when mandatoryTrainingBeingEdited in training service does match allJobRolesCount", async () => { const { getByLabelText } = await setup({ - existingMandatoryTraining, - allJobRolesCount: existingMandatoryTraining.jobs.length, + mandatoryTrainingBeingEdited, + allJobRolesCount: mandatoryTrainingBeingEdited.jobs.length, }); const allJobRolesRadio = getByLabelText('All job roles') as HTMLInputElement; @@ -207,10 +207,10 @@ describe('AllOrSelectedJobRolesComponent', () => { expect(onlySelectedJobRolesRadio.checked).toBeFalsy(); }); - it("should prefill the 'Only selected job roles' radio when set in training service even if existingMandatoryTraining (for case when user has changed from all to selected and then gone back to this page)", async () => { + it("should prefill the 'Only selected job roles' radio when set in training service even if mandatoryTrainingBeingEdited (for case when user has changed from all to selected and then gone back to this page)", async () => { const { getByLabelText } = await setup({ - existingMandatoryTraining, - allJobRolesCount: existingMandatoryTraining.jobs.length, + mandatoryTrainingBeingEdited, + allJobRolesCount: mandatoryTrainingBeingEdited.jobs.length, onlySelectedJobRoles: true, }); @@ -355,7 +355,7 @@ describe('AllOrSelectedJobRolesComponent', () => { }); it('should include previousTrainingCategoryId in submit props when editing existing mandatory training and All job roles selected', async () => { - const existingMandatoryTraining = { + const mandatoryTrainingBeingEdited = { category: 'Activity provision/Well-being', establishmentId: 4090, jobs: [{}, {}], @@ -363,14 +363,14 @@ describe('AllOrSelectedJobRolesComponent', () => { }; const { getByText, createAndUpdateMandatoryTrainingSpy, establishment, selectedTraining } = await setup({ - existingMandatoryTraining, - allJobRolesCount: existingMandatoryTraining.jobs.length, + mandatoryTrainingBeingEdited, + allJobRolesCount: mandatoryTrainingBeingEdited.jobs.length, }); fireEvent.click(getByText('Continue')); expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { - previousTrainingCategoryId: existingMandatoryTraining.trainingCategoryId, + previousTrainingCategoryId: mandatoryTrainingBeingEdited.trainingCategoryId, trainingCategoryId: selectedTraining.trainingCategory.id, allJobRoles: true, jobs: [], diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index eee942cd8d..e05f05e9a9 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -29,7 +29,7 @@ export class AllOrSelectedJobRolesComponent { public serverError: string; private subscriptions: Subscription = new Subscription(); private establishment: Establishment; - private existingMandatoryTraining: mandatoryTraining; + private mandatoryTrainingBeingEdited: mandatoryTraining; constructor( private formBuilder: UntypedFormBuilder, @@ -47,15 +47,15 @@ export class AllOrSelectedJobRolesComponent { ngOnInit(): void { this.establishment = this.route.snapshot.parent?.data?.establishment; this.selectedTrainingCategory = this.trainingService.selectedTraining; - this.existingMandatoryTraining = this.trainingService.existingMandatoryTraining; + this.mandatoryTrainingBeingEdited = this.trainingService.mandatoryTrainingBeingEdited; const allJobRolesCount = this.trainingService.allJobRolesCount; if (this.trainingService.onlySelectedJobRoles) { this.form.setValue({ allOrSelectedJobRoles: 'selectJobRoles' }); this.selectedRadio = 'selectJobRoles'; - } else if (this.existingMandatoryTraining) { + } else if (this.mandatoryTrainingBeingEdited) { const selected = - this.existingMandatoryTraining.jobs.length == allJobRolesCount ? 'allJobRoles' : 'selectJobRoles'; + this.mandatoryTrainingBeingEdited.jobs.length == allJobRolesCount ? 'allJobRoles' : 'selectJobRoles'; this.form.setValue({ allOrSelectedJobRoles: selected, @@ -131,8 +131,8 @@ export class AllOrSelectedJobRolesComponent { jobs: [], }; - if (this.existingMandatoryTraining?.trainingCategoryId) { - props.previousTrainingCategoryId = this.existingMandatoryTraining.trainingCategoryId; + if (this.mandatoryTrainingBeingEdited?.trainingCategoryId) { + props.previousTrainingCategoryId = this.mandatoryTrainingBeingEdited.trainingCategoryId; } return props; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index 28e43c40ca..284cf98b19 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -313,8 +313,8 @@ describe('SelectJobRolesMandatoryComponent', () => { }); }); - describe('Existing mandatory training', () => { - const createExistingMandatoryTraining = (jobs) => { + describe('Existing mandatory training being edited', () => { + const createMandatoryTrainingBeingEdited = (jobs) => { return { category: 'Activity provision/Well-being', establishmentId: 4090, @@ -323,11 +323,11 @@ describe('SelectJobRolesMandatoryComponent', () => { }; }; - it('should check the currently selected job roles if existingMandatoryTraining in training service (when editing existing mandatory training)', async () => { + it('should check the currently selected job roles if mandatoryTrainingBeingEdited in training service (when editing existing mandatory training)', async () => { const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; const { getByLabelText } = await setup({ - existingMandatoryTraining: createExistingMandatoryTraining(jobs), + mandatoryTrainingBeingEdited: createMandatoryTrainingBeingEdited(jobs), allJobRolesCount: 37, }); @@ -337,11 +337,11 @@ describe('SelectJobRolesMandatoryComponent', () => { }); }); - it('should not check the currently selected job roles if existingMandatoryTraining has all job roles (when editing existing mandatory training)', async () => { + it('should not check the currently selected job roles if mandatoryTrainingBeingEdited has all job roles (when editing existing mandatory training)', async () => { const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; const { getByLabelText } = await setup({ - existingMandatoryTraining: createExistingMandatoryTraining(jobs), + mandatoryTrainingBeingEdited: createMandatoryTrainingBeingEdited(jobs), allJobRolesCount: 2, }); @@ -354,7 +354,9 @@ describe('SelectJobRolesMandatoryComponent', () => { it('should expand the accordion for job groups that have job roles selected', async () => { const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; - const { getByLabelText } = await setup({ existingMandatoryTraining: createExistingMandatoryTraining(jobs) }); + const { getByLabelText } = await setup({ + mandatoryTrainingBeingEdited: createMandatoryTrainingBeingEdited(jobs), + }); jobs.forEach((jobRole) => { const jobRoleGroupAccordionSection = getByLabelText(jobRole.jobRoleGroup); @@ -365,7 +367,9 @@ describe('SelectJobRolesMandatoryComponent', () => { it('should not expand the accordion for job groups that do not have job roles selected', async () => { const jobs = [mockAvailableJobs[0]]; - const { getByLabelText } = await setup({ existingMandatoryTraining: createExistingMandatoryTraining(jobs) }); + const { getByLabelText } = await setup({ + mandatoryTrainingBeingEdited: createMandatoryTrainingBeingEdited(jobs), + }); const jobRoleGroupAccordionSectionWithPreselected = getByLabelText(jobs[0].jobRoleGroup); expect(within(jobRoleGroupAccordionSectionWithPreselected).getByText('Hide')).toBeTruthy(); // is expanded @@ -376,16 +380,16 @@ describe('SelectJobRolesMandatoryComponent', () => { it('should call createAndUpdateMandatoryTraining with training category in service and previous training ID', async () => { const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; - const existingMandatoryTraining = createExistingMandatoryTraining(jobs); + const mandatoryTrainingBeingEdited = createMandatoryTrainingBeingEdited(jobs); const { getByText, establishment, selectedTraining, createAndUpdateMandatoryTrainingSpy } = await setup({ - existingMandatoryTraining, + mandatoryTrainingBeingEdited, }); userEvent.click(getByText('Save mandatory training')); expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { - previousTrainingCategoryId: existingMandatoryTraining.trainingCategoryId, + previousTrainingCategoryId: mandatoryTrainingBeingEdited.trainingCategoryId, trainingCategoryId: selectedTraining.trainingCategory.id, allJobRoles: false, jobs: [{ id: jobs[0].id }, { id: jobs[1].id }], diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index bcc0d9b2ac..7bdb3916c4 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -45,7 +45,7 @@ export class SelectJobRolesMandatoryComponent { public subscriptions: Subscription = new Subscription(); private establishment: Establishment; private selectedTrainingCategory: SelectedTraining; - private existingMandatoryTraining: mandatoryTraining; + private mandatoryTrainingBeingEdited: mandatoryTraining; public jobGroupsToOpenAtStart: string[] = []; ngOnInit(): void { @@ -59,9 +59,9 @@ export class SelectJobRolesMandatoryComponent { this.backLinkService.showBackLink(); this.getJobs(); this.setupForm(); - this.existingMandatoryTraining = this.trainingService.existingMandatoryTraining; + this.mandatoryTrainingBeingEdited = this.trainingService.mandatoryTrainingBeingEdited; - if (this.existingMandatoryTraining) { + if (this.mandatoryTrainingBeingEdited) { this.prefillForm(); } } @@ -143,17 +143,17 @@ export class SelectJobRolesMandatoryComponent { }), }; - if (this.existingMandatoryTraining?.trainingCategoryId) { - props.previousTrainingCategoryId = this.existingMandatoryTraining.trainingCategoryId; + if (this.mandatoryTrainingBeingEdited?.trainingCategoryId) { + props.previousTrainingCategoryId = this.mandatoryTrainingBeingEdited.trainingCategoryId; } return props; } private prefillForm(): void { - if (this.existingMandatoryTraining.jobs?.length == this.trainingService.allJobRolesCount) return; + if (this.mandatoryTrainingBeingEdited.jobs?.length == this.trainingService.allJobRolesCount) return; - this.selectedJobIds = this.existingMandatoryTraining.jobs.map((job) => Number(job.id)); + this.selectedJobIds = this.mandatoryTrainingBeingEdited.jobs.map((job) => Number(job.id)); this.jobGroupsToOpenAtStart = this.jobGroups .filter((group) => group.items.some((job) => this.selectedJobIds.includes(job.id))) .map((group) => group.title); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts index 517670a050..a3e3547fd9 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.spec.ts @@ -11,14 +11,13 @@ import { WindowRef } from '@core/services/window.ref'; import { WorkerService } from '@core/services/worker.service'; import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService'; import { trainingCategories } from '@core/test-utils/MockTrainingCategoriesService'; -import { MockMandatoryTrainingService, MockTrainingServiceWithPreselectedStaff } from '@core/test-utils/MockTrainingService'; -import { MockWorkerService } from '@core/test-utils/MockWorkerService'; -import { - GroupedRadioButtonAccordionComponent, -} from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; import { - RadioButtonAccordionComponent, -} from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; + MockMandatoryTrainingService, + MockTrainingServiceWithPreselectedStaff, +} from '@core/test-utils/MockTrainingService'; +import { MockWorkerService } from '@core/test-utils/MockWorkerService'; +import { GroupedRadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component'; +import { RadioButtonAccordionComponent } from '@shared/components/accordions/radio-button-accordion/radio-button-accordion.component'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; @@ -240,7 +239,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { const overrides = { trainingCategories: mockTrainingCategories, existingMandatoryTraining, - trainingService: { existingMandatoryTraining: existingMandatoryTraining.mandatoryTraining[0] }, + trainingService: { mandatoryTrainingBeingEdited: existingMandatoryTraining.mandatoryTraining[0] }, }; const { component, queryByText } = await setup(overrides); @@ -259,7 +258,7 @@ describe('SelectTrainingCategoryMandatoryComponent', () => { trainingCategories: mockTrainingCategories, existingMandatoryTraining, trainingService: { - existingMandatoryTraining: existingMandatoryTraining.mandatoryTraining[0], + mandatoryTrainingBeingEdited: existingMandatoryTraining.mandatoryTraining[0], _selectedTraining: selectedTraining, }, }; diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts index 0a700470ad..fff072a8a1 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-training-category-mandatory/select-training-category-mandatory.component.ts @@ -6,9 +6,7 @@ import { ErrorSummaryService } from '@core/services/error-summary.service'; import { MandatoryTrainingService } from '@core/services/training.service'; import { WorkerService } from '@core/services/worker.service'; -import { - SelectTrainingCategoryDirective, -} from '../../../../shared/directives/select-training-category/select-training-category.directive'; +import { SelectTrainingCategoryDirective } from '../../../../shared/directives/select-training-category/select-training-category.directive'; @Component({ selector: 'app-select-training-category-mandatory', @@ -29,12 +27,12 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate public requiredErrorMessage: string = 'Select the training category that you want to make mandatory'; public hideOtherCheckbox: boolean = true; - private existingMandatoryTrainingCategoryId: number; + private mandatoryTrainingCategoryIdBeingEdited: number; init(): void { this.establishmentUid = this.route.snapshot.data.establishment.uid; - this.existingMandatoryTrainingCategoryId = - this.trainingService.existingMandatoryTraining?.trainingCategoryId ?? null; + this.mandatoryTrainingCategoryIdBeingEdited = + this.trainingService.mandatoryTrainingBeingEdited?.trainingCategoryId ?? null; this.getPrefilledId(); } @@ -43,8 +41,8 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate if (selectedCategory) { this.preFilledId = selectedCategory?.id; - } else if (this.existingMandatoryTrainingCategoryId) { - this.preFilledId = this.trainingService.existingMandatoryTraining.trainingCategoryId; + } else if (this.mandatoryTrainingCategoryIdBeingEdited) { + this.preFilledId = this.trainingService.mandatoryTrainingBeingEdited.trainingCategoryId; } } @@ -70,7 +68,7 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate this.categories = allTrainingCategories.filter( (category) => !trainingCategoryIdsWithExistingMandatoryTraining.includes(category.id) || - category.id == this.existingMandatoryTrainingCategoryId, + category.id == this.mandatoryTrainingCategoryIdBeingEdited, ); } else { this.categories = allTrainingCategories; @@ -81,8 +79,7 @@ export class SelectTrainingCategoryMandatoryComponent extends SelectTrainingCate protected prefillForm(): void { if (this.preFilledId) { - this.form.setValue({ category: this.preFilledId }); - this.form.get('category').updateValueAndValidity(); + this.form.patchValue({ category: this.preFilledId }); } } From 338d421090c8f09e3db2ea705bf596d4a3390153 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 20 Dec 2024 16:30:52 +0000 Subject: [PATCH 60/72] Delete replaced add/edit mandatory training page and component --- .../add-mandatory-routing.module.ts | 23 +- .../add-mandatory-training.component.html | 163 ------ .../add-mandatory-training.component.spec.ts | 518 ------------------ .../add-mandatory-training.component.ts | 352 ------------ .../add-mandatory-training.module.ts | 2 - 5 files changed, 3 insertions(+), 1055 deletions(-) delete mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.html delete mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.spec.ts delete mode 100644 frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.ts diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts index 7400a05117..5f660c6127 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-routing.module.ts @@ -3,20 +3,13 @@ import { RouterModule, Routes } from '@angular/router'; import { JobsResolver } from '@core/resolvers/jobs.resolver'; import { MandatoryTrainingCategoriesResolver } from '@core/resolvers/mandatory-training-categories.resolver'; import { TrainingCategoriesResolver } from '@core/resolvers/training-categories.resolver'; -import { - DeleteMandatoryTrainingCategoryComponent, -} from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; +import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and-qualifications/add-mandatory-training/delete-mandatory-training-category/delete-mandatory-training-category.component'; -import { - AddAndManageMandatoryTrainingComponent, -} from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; -import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; +import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles/all-or-selected-job-roles.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; import { SelectJobRolesMandatoryComponent } from './select-job-roles-mandatory/select-job-roles-mandatory.component'; -import { - SelectTrainingCategoryMandatoryComponent, -} from './select-training-category-mandatory/select-training-category-mandatory.component'; +import { SelectTrainingCategoryMandatoryComponent } from './select-training-category-mandatory/select-training-category-mandatory.component'; const routes: Routes = [ { @@ -55,19 +48,9 @@ const routes: Routes = [ component: RemoveAllMandatoryTrainingComponent, data: { title: 'Remove All Mandatory Training' }, }, - { - path: 'add-new-mandatory-training', - component: AddMandatoryTrainingComponent, - data: { title: 'Add New Mandatory Training' }, - }, { path: ':trainingCategoryId', children: [ - { - path: 'edit-mandatory-training', - component: AddMandatoryTrainingComponent, - data: { title: 'Edit Mandatory Training' }, - }, { path: 'delete-mandatory-training-category', component: DeleteMandatoryTrainingCategoryComponent, diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.html deleted file mode 100644 index f915f394af..0000000000 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.html +++ /dev/null @@ -1,163 +0,0 @@ - - -
-
-

- {{ renderAsEditMandatoryTraining ? 'Mandatory training category' : 'Add a mandatory training category' }} -

-
-
- -
- -
-
-
- - Which job roles need this training? - - - - - - -
-
- - -
-
-
-
-
-
-
-
- - diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.spec.ts deleted file mode 100644 index b82ce1dbe9..0000000000 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.spec.ts +++ /dev/null @@ -1,518 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { getTestBed } from '@angular/core/testing'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; -import { AlertService } from '@core/services/alert.service'; -import { BackService } from '@core/services/back.service'; -import { DialogService } from '@core/services/dialog.service'; -import { EstablishmentService } from '@core/services/establishment.service'; -import { JobService } from '@core/services/job.service'; -import { TrainingService } from '@core/services/training.service'; -import { WindowRef } from '@core/services/window.ref'; -import { MockEstablishmentService } from '@core/test-utils/MockEstablishmentService'; -import { MockJobService } from '@core/test-utils/MockJobService'; -import { MockTrainingService } from '@core/test-utils/MockTrainingService'; -import { SharedModule } from '@shared/shared.module'; -import { fireEvent, render } from '@testing-library/angular'; - -import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; -import { AddMandatoryTrainingModule } from './add-mandatory-training.module'; -import { TrainingCategoryService } from '@core/services/training-category.service'; -import { MockTrainingCategoryService } from '@core/test-utils/MockTrainingCategoriesService'; - -describe('AddMandatoryTrainingComponent', () => { - async function setup(renderAsEditMandatoryTraining = false, trainingCategoryId = '9', hasDuplicateJobRoles = false) { - const { getByText, getByLabelText, getAllByLabelText, getAllByText, queryByText, fixture } = await render( - AddMandatoryTrainingComponent, - { - imports: [SharedModule, RouterModule, RouterTestingModule, AddMandatoryTrainingModule, HttpClientTestingModule], - declarations: [], - providers: [ - AlertService, - BackService, - DialogService, - { - provide: WindowRef, - useClass: WindowRef, - }, - { - provide: EstablishmentService, - useClass: MockEstablishmentService, - }, - { - provide: TrainingService, - useFactory: MockTrainingService.factory(hasDuplicateJobRoles), - }, - { - provide: TrainingCategoryService, - useClass: MockTrainingCategoryService, - }, - { - provide: JobService, - useClass: MockJobService, - }, - { - provide: ActivatedRoute, - useValue: { - snapshot: { - parent: { - url: [{ path: trainingCategoryId ? trainingCategoryId : 'add-and-manage-mandatory-training' }], - data: { - establishment: { - uid: '9', - }, - }, - }, - url: [ - { path: renderAsEditMandatoryTraining ? 'edit-mandatory-training' : 'add-new-mandatory-training' }, - ], - }, - }, - }, - ], - }, - ); - const component = fixture.componentInstance; - - const injector = getTestBed(); - const establishmentService = injector.inject(EstablishmentService); - const alertService = injector.inject(AlertService) as AlertService; - const router = injector.inject(Router) as Router; - - const createAndUpdateMandatoryTrainingSpy = spyOn( - establishmentService, - 'createAndUpdateMandatoryTraining', - ).and.callThrough(); - const alertSpy = spyOn(alertService, 'addAlert').and.callThrough(); - const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); - - return { - component, - fixture, - createAndUpdateMandatoryTrainingSpy, - alertSpy, - routerSpy, - getByText, - getByLabelText, - getAllByLabelText, - getAllByText, - queryByText, - }; - } - - describe('component renderings', async () => { - it('should render the component', async () => { - const { component } = await setup(); - expect(component).toBeTruthy(); - }); - - it('Should display the add mandatory training title when not in edit version of page', async () => { - const { getByText } = await setup(); - expect(getByText('Add a mandatory training category')).toBeTruthy(); - }); - - it('Should display the edit mandatory training title when in edit version of page', async () => { - const { getByText } = await setup(true); - expect(getByText('Mandatory training category')).toBeTruthy(); - }); - - it('should render the remove training link when in edit version of the page', async () => { - const { getByText } = await setup(true); - expect(getByText('Remove this mandatory training category')).toBeTruthy(); - }); - - it('Should render save and return button and cancel link', async () => { - const { getByText } = await setup(); - expect(getByText('Save and return')).toBeTruthy(); - expect(getByText('Cancel')).toBeTruthy(); - }); - }); - - describe('prefill', async () => { - it('should prefill the training category and all job roles when a mandatory training with all job roles is selected', async () => { - const { component } = await setup(true); - - expect(component.form.value.trainingCategory).toEqual(9); - expect(component.form.value.allOrSelectedJobRoles).toEqual('all'); - expect(component.form.value.selectedJobRoles).toEqual([]); - }); - - it('should prefill the training category and all job roles when a mandatory training with all job roles has duplicates and previous all job length', async () => { - const { component } = await setup(true, '9', true); - - expect(component.form.value.trainingCategory).toEqual(9); - expect(component.form.value.allOrSelectedJobRoles).toEqual('all'); - expect(component.form.value.selectedJobRoles).toEqual([]); - }); - - it('should prefill the training category and job roles when a mandatory training with only selected job roles', async () => { - const { component } = await setup(true, '123'); - - expect(component.form.value.trainingCategory).toEqual(123); - expect(component.form.value.allOrSelectedJobRoles).toEqual('selected'); - expect(component.form.value.selectedJobRoles).toEqual([ - { - id: 15, - }, - ]); - }); - }); - - describe('trainingCategory form', async () => { - it('Should call createAndUpdateMandatoryTraining on submit when a training is selected and all Job roles is selected', async () => { - const { component, createAndUpdateMandatoryTrainingSpy, fixture, getByLabelText, getByText } = await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[0].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(component.establishment.uid, { - previousTrainingCategoryId: undefined, - trainingCategoryId: 1, - allJobRoles: true, - jobs: [], - }); - }); - - it('Should call createAndUpdateMandatoryTraining on submit when a training is selected and one specified job role is selected', async () => { - const { component, createAndUpdateMandatoryTrainingSpy, fixture, getByLabelText, getByText } = await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[1].label); - fireEvent.click(allJobRolesRadioButton); - - const specficJobRoleSelect = getByLabelText('Job role 1', { exact: true }); - fireEvent.change(specficJobRoleSelect, { target: { value: 27 } }); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(component.establishment.uid, { - previousTrainingCategoryId: undefined, - trainingCategoryId: 1, - allJobRoles: false, - jobs: [{ id: '27' }], - }); - }); - - it('Should call createAndUpdateMandatoryTraining on submit when a training is selected and multiple specified job roles are selected', async () => { - const { component, createAndUpdateMandatoryTrainingSpy, fixture, getByLabelText, getByText } = await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[1].label); - fireEvent.click(allJobRolesRadioButton); - - const addAnotherJobRoleButton = getByText('Add another job role'); - fireEvent.click(addAnotherJobRoleButton); - - fixture.detectChanges(); - - const specifiedJobRoleOne = getByLabelText('Job role 1', { exact: true }); - const specifiedJobRoleTwo = getByLabelText('Job role 2', { exact: true }); - - fireEvent.change(specifiedJobRoleOne, { target: { value: 27 } }); - fireEvent.change(specifiedJobRoleTwo, { target: { value: 23 } }); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(component.establishment.uid, { - previousTrainingCategoryId: undefined, - trainingCategoryId: 1, - allJobRoles: false, - jobs: [{ id: '27' }, { id: '23' }], - }); - }); - - it('should call createAndUpdateMandatoryTraining with the previous category id when updating an existing mandatory training category', async () => { - const { component, createAndUpdateMandatoryTrainingSpy, fixture, getByLabelText, getByText } = await setup(true); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[1].label); - fireEvent.click(allJobRolesRadioButton); - - const addAnotherJobRoleButton = getByText('Add another job role'); - fireEvent.click(addAnotherJobRoleButton); - - fixture.detectChanges(); - - const specifiedJobRoleOne = getByLabelText('Job role 1', { exact: true }); - const specifiedJobRoleTwo = getByLabelText('Job role 2', { exact: true }); - - fireEvent.change(specifiedJobRoleOne, { target: { value: 27 } }); - fireEvent.change(specifiedJobRoleTwo, { target: { value: 23 } }); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(component.establishment.uid, { - previousTrainingCategoryId: 9, - trainingCategoryId: 1, - allJobRoles: false, - jobs: [{ id: '27' }, { id: '23' }], - }); - }); - }); - - describe('allOrSelectedJobRoles form', async () => { - it('Should not display a job role selection when All job roles is selected', async () => { - const { component, fixture, getByLabelText, queryByText } = await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[0].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - expect(queryByText('Job role 1', { exact: true })).toBeFalsy(); - }); - - it('Should display a job role selection when All job roles is not selected selected', async () => { - const { component, fixture, getByLabelText, queryByText } = await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[1].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - expect(queryByText('Job role 1', { exact: true })).toBeTruthy(); - }); - }); - - describe('error messages', async () => { - describe('mandatory training category', async () => { - it('Should display a Select the training category you want to be mandatory error message if the form is submitted without a category input and all job roles is selected', async () => { - const { createAndUpdateMandatoryTrainingSpy, component, fixture, getByLabelText, getByText, getAllByText } = - await setup(); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[0].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).not.toHaveBeenCalled(); - expect(component.form.invalid).toBeTruthy(); - expect(getAllByText('Select the training category you want to be mandatory').length).toEqual(2); - }); - - it('Should display a Select the training category you want to be mandatory error message if the form is submitted without a category input and only selected job roles is selected', async () => { - const { createAndUpdateMandatoryTrainingSpy, component, fixture, getByLabelText, getByText, getAllByText } = - await setup(); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[1].label); - fireEvent.click(allJobRolesRadioButton); - - const specficJobRoleSelect = getByLabelText('Job role 1', { exact: true }); - fireEvent.change(specficJobRoleSelect, { target: { value: 27 } }); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).not.toHaveBeenCalled(); - expect(component.form.invalid).toBeTruthy(); - expect(getAllByText('Select the training category you want to be mandatory').length).toEqual(2); - }); - }); - - describe('allOrSelectedJobRoles', async () => { - it('Should display a Select which job roles need this training error message if the form is submitted without a job role radio button selected', async () => { - const { createAndUpdateMandatoryTrainingSpy, component, fixture, getByLabelText, getByText, getAllByText } = - await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).not.toHaveBeenCalled(); - expect(component.form.invalid).toBeTruthy(); - expect(getAllByText('Select which job roles need this training').length).toEqual(2); - }); - }); - - describe('job roles', async () => { - it('Should display a select the job role error message if the form is submitted without a specifc job role input and a mandatory training is selected', async () => { - const { createAndUpdateMandatoryTrainingSpy, component, fixture, getByLabelText, getByText } = await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[1].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).not.toHaveBeenCalled(); - expect(component.form.invalid).toBeTruthy(); - expect(getByText('Select the job role')).toBeTruthy(); - expect(getByText('Select the job role (job role 1)')).toBeTruthy(); - }); - - it('Should display multiple select the job role error messages if the form is submitted when several specifc job role inputs are empty and a mandatory training is selected', async () => { - const { createAndUpdateMandatoryTrainingSpy, component, fixture, getByLabelText, getByText, getAllByText } = - await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[1].label); - fireEvent.click(allJobRolesRadioButton); - - const addAnotherJobRoleButton = getByText('Add another job role'); - fireEvent.click(addAnotherJobRoleButton); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).not.toHaveBeenCalled(); - expect(component.form.invalid).toBeTruthy(); - expect(getAllByText('Select the job role').length).toEqual(2); - expect(getByText('Select the job role (job role 1)')).toBeTruthy(); - expect(getByText('Select the job role (job role 2)')).toBeTruthy(); - }); - }); - - it('should display a mandatory training error and a job role error if a mandatory training is not provided, only selected job roles is selected and a job role is not specified', async () => { - const { createAndUpdateMandatoryTrainingSpy, component, fixture, getByLabelText, getByText, getAllByText } = - await setup(); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[1].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(createAndUpdateMandatoryTrainingSpy).not.toHaveBeenCalled(); - expect(component.form.invalid).toBeTruthy(); - expect(getAllByText('Select the training category you want to be mandatory').length).toEqual(2); - expect(getByText('Select the job role')).toBeTruthy(); - expect(getByText('Select the job role (job role 1)')).toBeTruthy(); - }); - }); - - describe('success alert', async () => { - it('should show success banner when a mandatory training is saved', async () => { - const { component, alertSpy, fixture, getByLabelText, getByText } = await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[0].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(alertSpy).toHaveBeenCalledWith({ - type: 'success', - message: 'Mandatory training category added', - }); - }); - - it('should show update banner when a mandatory training is saved', async () => { - const { component, alertSpy, fixture, getByLabelText, getByText } = await setup(true); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[0].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(alertSpy).toHaveBeenCalledWith({ - type: 'success', - message: 'Mandatory training category updated', - }); - }); - }); - - describe('navigation', async () => { - it('should navigate to add and manage mandatory training categories when a record is saved ', async () => { - const { component, routerSpy, fixture, getByLabelText, getByText } = await setup(); - - const mandatoryTrainigCategorySelect = getByLabelText('Training category', { exact: false }); - fireEvent.change(mandatoryTrainigCategorySelect, { target: { value: 1 } }); - - const allJobRolesRadioButton = getByLabelText(component.allOrSelectedJobRoleOptions[0].label); - fireEvent.click(allJobRolesRadioButton); - - fixture.detectChanges(); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(routerSpy).toHaveBeenCalledWith([ - '/workplace', - component.establishment.uid, - 'add-and-manage-mandatory-training', - ]); - }); - - it('should navigate to add and manage mandatory training categories when a record is updated ', async () => { - const { component, routerSpy, getByText } = await setup(true); - - const submitButton = getByText('Save and return'); - fireEvent.click(submitButton); - - expect(routerSpy).toHaveBeenCalledWith([ - '/workplace', - component.establishment.uid, - 'add-and-manage-mandatory-training', - ]); - }); - }); - - it('should update the mandatory training with all job roles when it has duplicates and previous all job length', async () => { - const { component, createAndUpdateMandatoryTrainingSpy } = await setup(true, '9', true); - component.ngOnInit(); - expect(component.form.value.allOrSelectedJobRoles).toEqual('all'); - - expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(component.establishment.uid, { - previousTrainingCategoryId: 9, - trainingCategoryId: 9, - allJobRoles: true, - jobs: [], - }); - }); -}); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.ts deleted file mode 100644 index f33fa9386d..0000000000 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.component.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; -import { ErrorDefinition, ErrorDetails } from '@core/model/errorSummary.model'; -import { Establishment, mandatoryTrainingJobOption } from '@core/model/establishment.model'; -import { Job } from '@core/model/job.model'; -import { allMandatoryTrainingCategories, TrainingCategory } from '@core/model/training.model'; -import { URLStructure } from '@core/model/url.model'; -import { AlertService } from '@core/services/alert.service'; -import { BackLinkService } from '@core/services/backLink.service'; -import { ErrorSummaryService } from '@core/services/error-summary.service'; -import { EstablishmentService } from '@core/services/establishment.service'; -import { JobService } from '@core/services/job.service'; -import { TrainingCategoryService } from '@core/services/training-category.service'; -import { TrainingService } from '@core/services/training.service'; -import { Subscription } from 'rxjs'; -import { take } from 'rxjs/internal/operators/take'; - -@Component({ - selector: 'app-add-mandatory-training', - templateUrl: './add-mandatory-training.component.html', -}) -export class AddMandatoryTrainingComponent implements OnInit, OnDestroy { - @ViewChild('formEl') formEl: ElementRef; - public form: UntypedFormGroup; - public renderAsEditMandatoryTraining: boolean; - public submitted = false; - public preExistingTraining: any; - public categories: TrainingCategory[]; - public filteredTrainingCategories: TrainingCategory[]; - private subscriptions: Subscription = new Subscription(); - public jobs: Job[] = []; - public allJobsLength: Number; - public previousAllJobsLength = [29, 31, 32]; - public hasDuplicateJobRoles: boolean; - public filteredJobs: Array = []; - public trainings: TrainingCategory[] = []; - public establishment: Establishment; - public primaryWorkplace: Establishment; - public formErrorsMap: Array = []; - public serverError: string; - public serverErrorsMap: Array = []; - public return: URLStructure; - public existingMandatoryTrainings: allMandatoryTrainingCategories; - public allOrSelectedJobRoleOptions = [ - { - label: 'All job roles', - value: mandatoryTrainingJobOption.all, - }, - { - label: `Only selected job roles`, - value: mandatoryTrainingJobOption.selected, - }, - ]; - constructor( - protected backLinkService: BackLinkService, - private trainingService: TrainingService, - private trainingCategoryService: TrainingCategoryService, - protected formBuilder: UntypedFormBuilder, - protected errorSummaryService: ErrorSummaryService, - protected establishmentService: EstablishmentService, - private jobService: JobService, - protected router: Router, - private route: ActivatedRoute, - private alertService: AlertService, - ) {} - - get selectedJobRolesArray(): UntypedFormArray { - return this.form.get('selectedJobRoles') as UntypedFormArray; - } - - ngOnInit(): void { - this.primaryWorkplace = this.establishmentService.primaryWorkplace; - this.establishment = this.route.snapshot.parent.data.establishment; - - this.renderAsEditMandatoryTraining = this.route.snapshot.url[0].path === 'edit-mandatory-training'; - this.return = { url: ['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training'] }; - - this.getAllJobs(); - this.setUpForm(); - this.setupServerErrorsMap(); - this.backLinkService.showBackLink(); - - this.subscriptions.add( - this.trainingService.getAllMandatoryTrainings(this.establishment.uid).subscribe((existingMandatoryTraining) => { - this.existingMandatoryTrainings = existingMandatoryTraining; - this.getAllTrainingCategories(); - }), - ); - } - - private getAllTrainingCategories(): void { - this.subscriptions.add( - this.trainingCategoryService - .getCategories() - .pipe(take(1)) - .subscribe((trainings) => { - this.trainings = this.filterTrainingCategories(trainings); - if (this.renderAsEditMandatoryTraining) { - this.prefill(); - this.updateMandatoryTrainingWithPreviousAllJobsRecordLength(); - } - }), - ); - } - - public filterTrainingCategories(trainings): TrainingCategory[] { - const preSelectedIds = this.existingMandatoryTrainings.mandatoryTraining.map( - (existingMandatoryTrainings) => existingMandatoryTrainings.trainingCategoryId, - ); - if (this.renderAsEditMandatoryTraining) { - this.preExistingTraining = this.existingMandatoryTrainings.mandatoryTraining.find((mandatoryTrainingObject) => { - return mandatoryTrainingObject.trainingCategoryId === parseInt(this.route.snapshot.parent.url[0].path, 10); - }); - - return trainings.filter((training) => { - return this.preExistingTraining.trainingCategoryId === training.id || !preSelectedIds.includes(training.id); - }); - } else { - return trainings.filter((training) => { - return !preSelectedIds.includes(training.id); - }); - } - } - - private getAllJobs(): void { - this.subscriptions.add( - this.jobService - .getJobs() - .pipe(take(1)) - .subscribe((jobs) => { - this.allJobsLength = jobs.length; - this.jobs = jobs; - }), - ); - } - - private setUpForm(trainingId = null): void { - this.form = this.formBuilder.group({ - trainingCategory: [trainingId, [Validators.required]], - allOrSelectedJobRoles: [null, [Validators.required]], - selectedJobRoles: this.formBuilder.array([]), - }); - } - - private setupServerErrorsMap(): void { - const serverErrorMessage = 'There has been a problem saving your mandatory training. Please try again.'; - this.serverErrorsMap = [ - { - name: 500, - message: serverErrorMessage, - }, - { - name: 400, - message: serverErrorMessage, - }, - { - name: 404, - message: serverErrorMessage, - }, - ]; - } - - protected setupFormErrorsMap(): void { - this.formErrorsMap = [ - { - item: 'trainingCategory', - type: [ - { - name: 'required', - message: 'Select the training category you want to be mandatory', - }, - ], - }, - { - item: 'allOrSelectedJobRoles', - type: [ - { - name: 'required', - message: 'Select which job roles need this training', - }, - ], - }, - ]; - this.selectedJobRolesArray.controls.forEach((_, index) => { - this.formErrorsMap.push({ - item: `selectedJobRoles.id.${index}`, - type: [ - { - name: 'required', - message: `Select the job role (job role ${index + 1})`, - }, - ], - }); - }); - } - - public checkDuplicateJobRoles(preExistingTrainingJobsDuplicates): boolean { - for (let i = 0; i < preExistingTrainingJobsDuplicates.length; i++) { - for (let j = i + 1; j < preExistingTrainingJobsDuplicates.length; j++) { - if (preExistingTrainingJobsDuplicates[i].id === preExistingTrainingJobsDuplicates[j].id) { - return true; - } - } - } - } - - public filterPreExistingTrainingJobsDuplicates(preExistingTrainingJobs) { - if (preExistingTrainingJobs > 1) { - let filtered = preExistingTrainingJobs.filter( - (obj1, index, arr) => - arr.findIndex((obj2) => { - obj2.id === obj1.id; - }) === index, - ); - return filtered; - } else { - return preExistingTrainingJobs; - } - } - - public prefill(): void { - this.form.patchValue({ - trainingCategory: this.preExistingTraining.trainingCategoryId, - allOrSelectedJobRoles: - this.preExistingTraining.jobs.length === this.allJobsLength || - (this.previousAllJobsLength.includes(this.preExistingTraining.jobs.length) && - this.checkDuplicateJobRoles(this.preExistingTraining.jobs)) - ? mandatoryTrainingJobOption.all - : mandatoryTrainingJobOption.selected, - selectedJobRoles: this.prefillJobRoles(), - }); - } - - protected prefillJobRoles() { - let filteredPreExistingTrainingJobs = this.filterPreExistingTrainingJobsDuplicates(this.preExistingTraining.jobs); - return this.preExistingTraining.jobs.length === this.allJobsLength || - (this.previousAllJobsLength.includes(this.preExistingTraining.jobs.length) && - this.checkDuplicateJobRoles(this.preExistingTraining.jobs)) - ? null - : filteredPreExistingTrainingJobs.forEach((job) => { - this.selectedJobRolesArray.push(this.createVacancyControl(job.id)); - }); - } - - public addVacancy(): void { - this.selectedJobRolesArray.push(this.createVacancyControl()); - this.setupFormErrorsMap(); - } - - public filterJobList(jobIndex): Job[] { - return this.jobs.filter( - (job) => - !this.selectedJobRolesArray.controls.some( - (jobRole) => - jobRole !== this.selectedJobRolesArray.controls[jobIndex] && - parseInt(jobRole.get('id').value, 10) === job.id, - ), - ); - } - - public removeVacancy(event: Event, jobIndex): void { - event.preventDefault(); - this.selectedJobRolesArray.removeAt(jobIndex); - } - - private createVacancyControl(jobId = null): UntypedFormGroup { - return this.formBuilder.group({ - id: [jobId, [Validators.required]], - }); - } - - protected generateUpdateProps(): any { - return { - previousTrainingCategoryId: this.preExistingTraining?.trainingCategoryId, - trainingCategoryId: parseInt(this.form.get('trainingCategory').value, 10), - allJobRoles: this.form.get('allOrSelectedJobRoles').value === mandatoryTrainingJobOption.all ? true : false, - jobs: this.form.get('selectedJobRoles').value, - }; - } - - protected createAndUpdateMandatoryTraining(props): void { - this.subscriptions.add( - this.establishmentService.createAndUpdateMandatoryTraining(this.establishment.uid, props).subscribe( - () => { - this.router.navigate(['/workplace', this.establishment.uid, 'add-and-manage-mandatory-training']); - this.alertService.addAlert({ - type: 'success', - message: this.renderAsEditMandatoryTraining - ? 'Mandatory training category updated' - : 'Mandatory training category added', - }); - }, - (error) => { - this.serverError = this.errorSummaryService.getServerErrorMessage(error.status, this.serverErrorsMap); - }, - ), - ); - } - - public onVacancyTypeSelectionChange(): void { - const allOrSelectedJobRoles = this.form.get('allOrSelectedJobRoles').value; - if (allOrSelectedJobRoles === mandatoryTrainingJobOption.all) { - while (this.selectedJobRolesArray.length > 0) { - this.selectedJobRolesArray.removeAt(0); - } - this.selectedJobRolesArray.reset([], { emitEvent: false }); - } else if (this.renderAsEditMandatoryTraining) { - this.preExistingTraining.jobs.length === this.allJobsLength || - (this.previousAllJobsLength.includes(this.preExistingTraining.jobs.length) && - this.checkDuplicateJobRoles(this.preExistingTraining.jobs)) - ? this.addVacancy() - : this.prefillJobRoles(); - } else { - this.addVacancy(); - } - } - - public updateMandatoryTrainingWithPreviousAllJobsRecordLength(): void { - const props = this.generateUpdateProps(); - - if ( - this.previousAllJobsLength.includes(this.preExistingTraining.jobs.length) && - this.checkDuplicateJobRoles(this.preExistingTraining.jobs) - ) { - this.subscriptions.add( - this.establishmentService.createAndUpdateMandatoryTraining(this.establishment.uid, props).subscribe( - () => {}, - (error) => { - console.error(error.error.message); - }, - ), - ); - } - } - - public onSubmit(): void { - this.submitted = true; - this.setupFormErrorsMap(); - - if (!this.form.valid) { - this.errorSummaryService.scrollToErrorSummary(); - return; - } - const props = this.generateUpdateProps(); - this.createAndUpdateMandatoryTraining(props); - } - - ngOnDestroy(): void { - this.subscriptions.unsubscribe(); - } -} diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts index ca1626ce5e..cbb760e410 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-mandatory-training.module.ts @@ -8,7 +8,6 @@ import { DeleteMandatoryTrainingCategoryComponent } from '@features/training-and import { SharedModule } from '@shared/shared.module'; import { AddAndManageMandatoryTrainingComponent } from './add-and-manage-mandatory-training/add-and-manage-mandatory-training.component'; -import { AddMandatoryTrainingComponent } from './add-mandatory-training.component'; import { AllOrSelectedJobRolesComponent } from './all-or-selected-job-roles/all-or-selected-job-roles.component'; import { RemoveAllMandatoryTrainingComponent } from './delete-mandatory-training/delete-all-mandatory-training.component'; import { SelectJobRolesMandatoryComponent } from './select-job-roles-mandatory/select-job-roles-mandatory.component'; @@ -17,7 +16,6 @@ import { SelectTrainingCategoryMandatoryComponent } from './select-training-cate @NgModule({ imports: [CommonModule, AddMandatoryTrainingRoutingModule, ReactiveFormsModule, SharedModule], declarations: [ - AddMandatoryTrainingComponent, SelectTrainingCategoryMandatoryComponent, RemoveAllMandatoryTrainingComponent, AddAndManageMandatoryTrainingComponent, From 8d762a32d013a6edc00a485c07f8a486918295f0 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 3 Jan 2025 14:39:51 +0000 Subject: [PATCH 61/72] Updated banner to say 'updated' when editing existing mandatory training --- .../select-job-roles-mandatory.component.spec.ts | 16 ++++++++++++++++ .../select-job-roles-mandatory.component.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts index 284cf98b19..1908ec8733 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.spec.ts @@ -395,5 +395,21 @@ describe('SelectJobRolesMandatoryComponent', () => { jobs: [{ id: jobs[0].id }, { id: jobs[1].id }], }); }); + + it("should display 'Mandatory training category updated' banner on submit", async () => { + const jobs = [mockAvailableJobs[0], mockAvailableJobs[1]]; + const mandatoryTrainingBeingEdited = createMandatoryTrainingBeingEdited(jobs); + + const { getByText, alertSpy } = await setup({ + mandatoryTrainingBeingEdited, + }); + + userEvent.click(getByText('Save mandatory training')); + + expect(alertSpy).toHaveBeenCalledWith({ + type: 'success', + message: 'Mandatory training category updated', + }); + }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts index 7bdb3916c4..d7abfd6539 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/select-job-roles-mandatory/select-job-roles-mandatory.component.ts @@ -124,7 +124,7 @@ export class SelectJobRolesMandatoryComponent { this.alertService.addAlert({ type: 'success', - message: 'Mandatory training category added', + message: `Mandatory training category ${this.mandatoryTrainingBeingEdited ? 'updated' : 'added'}`, }); }, () => { From c51b94f1ab9e775d3c164f18dc43cec55818dbb8 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 3 Jan 2025 15:10:32 +0000 Subject: [PATCH 62/72] Updated banner to say 'updated' when editing existing mandatory training when selecting all job roles on previous page --- ...ll-or-selected-job-roles.component.spec.ts | 36 +++++++++++++------ .../all-or-selected-job-roles.component.ts | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts index 788018ccd1..7da6bf1262 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.spec.ts @@ -354,7 +354,7 @@ describe('AllOrSelectedJobRolesComponent', () => { }); }); - it('should include previousTrainingCategoryId in submit props when editing existing mandatory training and All job roles selected', async () => { + describe('Editing existing mandatory training', () => { const mandatoryTrainingBeingEdited = { category: 'Activity provision/Well-being', establishmentId: 4090, @@ -362,18 +362,34 @@ describe('AllOrSelectedJobRolesComponent', () => { trainingCategoryId: 1, }; - const { getByText, createAndUpdateMandatoryTrainingSpy, establishment, selectedTraining } = await setup({ - mandatoryTrainingBeingEdited, - allJobRolesCount: mandatoryTrainingBeingEdited.jobs.length, + it('should include previousTrainingCategoryId in submit props when All job roles selected', async () => { + const { getByText, createAndUpdateMandatoryTrainingSpy, establishment, selectedTraining } = await setup({ + mandatoryTrainingBeingEdited, + allJobRolesCount: mandatoryTrainingBeingEdited.jobs.length, + }); + + fireEvent.click(getByText('Continue')); + + expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { + previousTrainingCategoryId: mandatoryTrainingBeingEdited.trainingCategoryId, + trainingCategoryId: selectedTraining.trainingCategory.id, + allJobRoles: true, + jobs: [], + }); }); - fireEvent.click(getByText('Continue')); + it("should display 'Mandatory training category updated' banner when All job roles selected", async () => { + const { getByText, alertSpy } = await setup({ + mandatoryTrainingBeingEdited, + allJobRolesCount: mandatoryTrainingBeingEdited.jobs.length, + }); - expect(createAndUpdateMandatoryTrainingSpy).toHaveBeenCalledWith(establishment.uid, { - previousTrainingCategoryId: mandatoryTrainingBeingEdited.trainingCategoryId, - trainingCategoryId: selectedTraining.trainingCategory.id, - allJobRoles: true, - jobs: [], + fireEvent.click(getByText('Continue')); + + expect(alertSpy).toHaveBeenCalledWith({ + type: 'success', + message: 'Mandatory training category updated', + }); }); }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts index e05f05e9a9..e396f0996c 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/all-or-selected-job-roles/all-or-selected-job-roles.component.ts @@ -114,7 +114,7 @@ export class AllOrSelectedJobRolesComponent { this.alertService.addAlert({ type: 'success', - message: 'Mandatory training category added', + message: `Mandatory training category ${this.mandatoryTrainingBeingEdited ? 'updated' : 'added'}`, }); }, () => { From c3e2b9c1ea1e8c084eebe2acdf312810faa33914 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 3 Jan 2025 12:51:35 +0000 Subject: [PATCH 63/72] Add basic tests for viewMandatoryTraining --- .../establishments/mandatoryTraining/index.js | 1 + .../mandatoryTraining/index.spec.js | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/backend/server/routes/establishments/mandatoryTraining/index.js b/backend/server/routes/establishments/mandatoryTraining/index.js index 028366abb8..cb02f15190 100644 --- a/backend/server/routes/establishments/mandatoryTraining/index.js +++ b/backend/server/routes/establishments/mandatoryTraining/index.js @@ -93,3 +93,4 @@ router.route('/:categoryId').delete(deleteMandatoryTrainingById); module.exports = router; module.exports.createAndUpdateMandatoryTraining = createAndUpdateMandatoryTraining; module.exports.deleteMandatoryTrainingById = deleteMandatoryTrainingById; +module.exports.viewMandatoryTraining = viewMandatoryTraining; diff --git a/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js b/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js index 46c7289f6a..d79ec83fed 100644 --- a/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js +++ b/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js @@ -6,6 +6,7 @@ const sinon = require('sinon'); const { createAndUpdateMandatoryTraining, deleteMandatoryTrainingById, + viewMandatoryTraining, } = require('../../../../../routes/establishments/mandatoryTraining/index.js'); const MandatoryTraining = require('../../../../../models/classes/mandatoryTraining').MandatoryTraining; @@ -34,6 +35,7 @@ describe('mandatoryTraining/index.js', () => { expect(res.statusCode).to.deep.equal(500); }); }); + describe('createAndUpdateMandatoryTraining', () => { it('should save the record for mandatory training if isvalid , not exists and all job role is selected', async () => { sinon.stub(MandatoryTraining.prototype, 'load').callsFake(() => { @@ -84,4 +86,45 @@ describe('mandatoryTraining/index.js', () => { expect(res.statusCode).to.deep.equal(500); }); }); + + describe('viewMandatoryTraining', () => { + let req; + let res; + + beforeEach(() => { + req = httpMocks.createRequest(); + req.establishmentId = 'mockId'; + res = httpMocks.createResponse(); + }); + + const mockFetchData = { + allJobRolesCount: 37, + lastUpdated: '2025-01-03T11:55:55.734Z', + mandatoryTraining: [{}], + mandatoryTrainingCount: 1, + }; + + it('should fetch all mandatory training for establishment passed in request', async () => { + const fetchSpy = sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); + + await viewMandatoryTraining(req, res); + expect(res.statusCode).to.deep.equal(200); + expect(fetchSpy).to.have.been.calledWith(req.establishmentId); + }); + + it('should return data from fetch and 200 status if fetch successful', async () => { + sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); + + await viewMandatoryTraining(req, res); + expect(res.statusCode).to.equal(200); + expect(res._getJSONData()).to.deep.equal(mockFetchData); + }); + + it('should return 500 status if error when fetching data', async () => { + sinon.stub(MandatoryTraining, 'fetch').throws('Unexpected error'); + + await viewMandatoryTraining(req, res); + expect(res.statusCode).to.equal(500); + }); + }); }); From 4d23aabbc005e88e4344e12c1941685ec1dfb3c9 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 3 Jan 2025 13:27:49 +0000 Subject: [PATCH 64/72] Update mandatory training to be all job roles when duplicates --- .../establishments/mandatoryTraining/index.js | 28 ++++++++++ .../mandatoryTraining/index.spec.js | 55 +++++++++++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/backend/server/routes/establishments/mandatoryTraining/index.js b/backend/server/routes/establishments/mandatoryTraining/index.js index cb02f15190..82a0c97f97 100644 --- a/backend/server/routes/establishments/mandatoryTraining/index.js +++ b/backend/server/routes/establishments/mandatoryTraining/index.js @@ -14,8 +14,22 @@ const { hasPermission } = require('../../../utils/security/hasPermission'); const viewMandatoryTraining = async (req, res) => { const establishmentId = req.establishmentId; + try { const allMandatoryTrainingRecords = await MandatoryTraining.fetch(establishmentId); + + for (mandatoryTrainingCategory of allMandatoryTrainingRecords.mandatoryTraining) { + if (hasDuplicateJobs(mandatoryTrainingCategory.jobs)) { + const thisMandatoryTrainingRecord = new MandatoryTraining(establishmentId); + await thisMandatoryTrainingRecord.load({ + trainingCategoryId: mandatoryTrainingCategory.trainingCategoryId, + allJobRoles: true, + jobs: [], + }); + await thisMandatoryTrainingRecord.save(req.userUid); + } + } + return res.status(200).json(allMandatoryTrainingRecords); } catch (err) { console.error(err); @@ -30,6 +44,7 @@ const viewAllMandatoryTraining = async (req, res) => { const establishmentId = req.establishmentId; try { const allMandatoryTrainingRecords = await MandatoryTraining.fetchAllMandatoryTrainings(establishmentId); + return res.status(200).json(allMandatoryTrainingRecords); } catch (err) { console.error(err); @@ -37,6 +52,19 @@ const viewAllMandatoryTraining = async (req, res) => { } }; +const hasDuplicateJobs = (jobs) => { + const seenJobIds = new Set(); + + for (const job of jobs) { + if (seenJobIds.has(job.id)) { + return true; // Duplicate found + } + seenJobIds.add(job.id); + } + + return false; // No duplicates found +}; + /** * Handle POST request for creating new mandatory training */ diff --git a/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js b/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js index d79ec83fed..48c520eb4e 100644 --- a/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js +++ b/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js @@ -97,15 +97,24 @@ describe('mandatoryTraining/index.js', () => { res = httpMocks.createResponse(); }); - const mockFetchData = { - allJobRolesCount: 37, - lastUpdated: '2025-01-03T11:55:55.734Z', - mandatoryTraining: [{}], - mandatoryTrainingCount: 1, + const createMockFetchData = () => { + return { + allJobRolesCount: 37, + lastUpdated: '2025-01-03T11:55:55.734Z', + mandatoryTraining: [ + { + category: 'Activity provision, wellbeing', + establishmentId: 100, + jobs: [{ id: 22, title: 'Registered manager' }], + trainingCategoryId: 1, + }, + ], + mandatoryTrainingCount: 1, + }; }; it('should fetch all mandatory training for establishment passed in request', async () => { - const fetchSpy = sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); + const fetchSpy = sinon.stub(MandatoryTraining, 'fetch').callsFake(() => createMockFetchData()); await viewMandatoryTraining(req, res); expect(res.statusCode).to.deep.equal(200); @@ -113,6 +122,7 @@ describe('mandatoryTraining/index.js', () => { }); it('should return data from fetch and 200 status if fetch successful', async () => { + const mockFetchData = createMockFetchData(); sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); await viewMandatoryTraining(req, res); @@ -126,5 +136,38 @@ describe('mandatoryTraining/index.js', () => { await viewMandatoryTraining(req, res); expect(res.statusCode).to.equal(500); }); + + describe('Handling duplicate job roles', () => { + it('should not call load or save for mandatory training when no duplicate job roles in retrieved mandatory training', async () => { + const loadSpy = sinon.stub(MandatoryTraining.prototype, 'load'); + const saveSpy = sinon.stub(MandatoryTraining.prototype, 'save'); + + sinon.stub(MandatoryTraining, 'fetch').callsFake(() => createMockFetchData()); + + await viewMandatoryTraining(req, res); + expect(loadSpy).not.to.have.been.called; + expect(saveSpy).not.to.have.been.called; + }); + + it('should call load and save for mandatory training instance when duplicate job roles in retrieved mandatory training', async () => { + const loadSpy = sinon.stub(MandatoryTraining.prototype, 'load'); + const saveSpy = sinon.stub(MandatoryTraining.prototype, 'save'); + + const mockFetchData = createMockFetchData(); + + const mockJobRoles = Array.from({ length: 28 }, (_, index) => ({ + id: index + 1, + title: `Job role ${index + 1}`, + })); + mockJobRoles.push({ id: 1, title: 'Job role 1' }); + mockFetchData.mandatoryTraining[0].jobs = mockJobRoles; + + sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); + + await viewMandatoryTraining(req, res); + expect(loadSpy).to.have.been.called; + expect(saveSpy).to.have.been.called; + }); + }); }); }); From b4b88e02ae3528c92d4ee2900abe0cc464bc1ff9 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 3 Jan 2025 15:41:25 +0000 Subject: [PATCH 65/72] Only update mandatory training when number of job roles is previous all job roles length --- .../establishments/mandatoryTraining/index.js | 32 ++++++++------- .../mandatoryTraining/index.spec.js | 41 ++++++++++++++++--- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/backend/server/routes/establishments/mandatoryTraining/index.js b/backend/server/routes/establishments/mandatoryTraining/index.js index 82a0c97f97..5dadc97eaa 100644 --- a/backend/server/routes/establishments/mandatoryTraining/index.js +++ b/backend/server/routes/establishments/mandatoryTraining/index.js @@ -14,12 +14,16 @@ const { hasPermission } = require('../../../utils/security/hasPermission'); const viewMandatoryTraining = async (req, res) => { const establishmentId = req.establishmentId; + const previousAllJobsLengths = [29, 31, 32]; try { const allMandatoryTrainingRecords = await MandatoryTraining.fetch(establishmentId); for (mandatoryTrainingCategory of allMandatoryTrainingRecords.mandatoryTraining) { - if (hasDuplicateJobs(mandatoryTrainingCategory.jobs)) { + if ( + hasDuplicateJobs(mandatoryTrainingCategory.jobs) && + previousAllJobsLengths.includes(mandatoryTrainingCategory.jobs.length) + ) { const thisMandatoryTrainingRecord = new MandatoryTraining(establishmentId); await thisMandatoryTrainingRecord.load({ trainingCategoryId: mandatoryTrainingCategory.trainingCategoryId, @@ -37,6 +41,19 @@ const viewMandatoryTraining = async (req, res) => { } }; +const hasDuplicateJobs = (jobRoles) => { + const seenJobIds = new Set(); + + for (const jobRole of jobRoles) { + if (seenJobIds.has(jobRole.id)) { + return true; // Duplicate found + } + seenJobIds.add(jobRole.id); + } + + return false; // No duplicates found +}; + /** * Handle GET request for getting all saved mandatory training for view all mandatory training */ @@ -52,19 +69,6 @@ const viewAllMandatoryTraining = async (req, res) => { } }; -const hasDuplicateJobs = (jobs) => { - const seenJobIds = new Set(); - - for (const job of jobs) { - if (seenJobIds.has(job.id)) { - return true; // Duplicate found - } - seenJobIds.add(job.id); - } - - return false; // No duplicates found -}; - /** * Handle POST request for creating new mandatory training */ diff --git a/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js b/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js index 48c520eb4e..570e9dada0 100644 --- a/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js +++ b/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js @@ -94,6 +94,7 @@ describe('mandatoryTraining/index.js', () => { beforeEach(() => { req = httpMocks.createRequest(); req.establishmentId = 'mockId'; + req.userUid = 'abc123'; res = httpMocks.createResponse(); }); @@ -149,24 +150,54 @@ describe('mandatoryTraining/index.js', () => { expect(saveSpy).not.to.have.been.called; }); - it('should call load and save for mandatory training instance when duplicate job roles in retrieved mandatory training', async () => { + const previousAllJobsLengths = [29, 31, 32]; + + previousAllJobsLengths.forEach((allJobsLength) => { + it(`should call load and save for mandatory training instance when duplicate job roles in retrieved mandatory training and has length of previous all job roles (${allJobsLength})`, async () => { + const loadSpy = sinon.stub(MandatoryTraining.prototype, 'load'); + const saveSpy = sinon.stub(MandatoryTraining.prototype, 'save'); + + const mockFetchData = createMockFetchData(); + + const mockJobRoles = Array.from({ length: allJobsLength - 1 }, (_, index) => ({ + id: index + 1, + title: `Job role ${index + 1}`, + })); + mockJobRoles.push({ id: 1, title: 'Job role 1' }); + mockFetchData.mandatoryTraining[0].jobs = mockJobRoles; + + sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); + + await viewMandatoryTraining(req, res); + + expect(loadSpy).to.have.been.calledWith({ + trainingCategoryId: mockFetchData.mandatoryTraining[0].trainingCategoryId, + allJobRoles: true, + jobs: [], + }); + expect(saveSpy).to.have.been.calledWith(req.userUid); + }); + }); + + it('should not call load or save for mandatory training when number of jobs is previous all job roles length (29) but no duplicate job roles', async () => { const loadSpy = sinon.stub(MandatoryTraining.prototype, 'load'); const saveSpy = sinon.stub(MandatoryTraining.prototype, 'save'); const mockFetchData = createMockFetchData(); - const mockJobRoles = Array.from({ length: 28 }, (_, index) => ({ + const mockJobRoles = Array.from({ length: 29 }, (_, index) => ({ id: index + 1, title: `Job role ${index + 1}`, })); - mockJobRoles.push({ id: 1, title: 'Job role 1' }); + mockFetchData.mandatoryTraining[0].jobs = mockJobRoles; sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); await viewMandatoryTraining(req, res); - expect(loadSpy).to.have.been.called; - expect(saveSpy).to.have.been.called; + + expect(loadSpy).not.to.have.been.called; + expect(saveSpy).not.to.have.been.called; }); }); }); From 288c1b267e4b3ad74a7b2382122849306b375e01 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 3 Jan 2025 15:54:28 +0000 Subject: [PATCH 66/72] Retrieve the mandatory training data again if any duplicates have been updated --- .../establishments/mandatoryTraining/index.js | 37 ++++++++++++------- .../mandatoryTraining/index.spec.js | 3 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/backend/server/routes/establishments/mandatoryTraining/index.js b/backend/server/routes/establishments/mandatoryTraining/index.js index 5dadc97eaa..92b44d8d3b 100644 --- a/backend/server/routes/establishments/mandatoryTraining/index.js +++ b/backend/server/routes/establishments/mandatoryTraining/index.js @@ -17,20 +17,29 @@ const viewMandatoryTraining = async (req, res) => { const previousAllJobsLengths = [29, 31, 32]; try { - const allMandatoryTrainingRecords = await MandatoryTraining.fetch(establishmentId); - - for (mandatoryTrainingCategory of allMandatoryTrainingRecords.mandatoryTraining) { - if ( - hasDuplicateJobs(mandatoryTrainingCategory.jobs) && - previousAllJobsLengths.includes(mandatoryTrainingCategory.jobs.length) - ) { - const thisMandatoryTrainingRecord = new MandatoryTraining(establishmentId); - await thisMandatoryTrainingRecord.load({ - trainingCategoryId: mandatoryTrainingCategory.trainingCategoryId, - allJobRoles: true, - jobs: [], - }); - await thisMandatoryTrainingRecord.save(req.userUid); + let allMandatoryTrainingRecords = await MandatoryTraining.fetch(establishmentId); + let duplicateJobRolesUpdated = false; + + if (allMandatoryTrainingRecords?.mandatoryTraining?.length) { + for (mandatoryTrainingCategory of allMandatoryTrainingRecords.mandatoryTraining) { + if ( + hasDuplicateJobs(mandatoryTrainingCategory.jobs) && + previousAllJobsLengths.includes(mandatoryTrainingCategory.jobs.length) + ) { + duplicateJobRolesUpdated = true; + + const thisMandatoryTrainingRecord = new MandatoryTraining(establishmentId); + await thisMandatoryTrainingRecord.load({ + trainingCategoryId: mandatoryTrainingCategory.trainingCategoryId, + allJobRoles: true, + jobs: [], + }); + await thisMandatoryTrainingRecord.save(req.userUid); + } + } + + if (duplicateJobRolesUpdated) { + allMandatoryTrainingRecords = await MandatoryTraining.fetch(establishmentId); } } diff --git a/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js b/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js index 570e9dada0..45049678b0 100644 --- a/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js +++ b/backend/server/test/unit/routes/establishments/mandatoryTraining/index.spec.js @@ -166,7 +166,7 @@ describe('mandatoryTraining/index.js', () => { mockJobRoles.push({ id: 1, title: 'Job role 1' }); mockFetchData.mandatoryTraining[0].jobs = mockJobRoles; - sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); + const fetchSpy = sinon.stub(MandatoryTraining, 'fetch').callsFake(() => mockFetchData); await viewMandatoryTraining(req, res); @@ -176,6 +176,7 @@ describe('mandatoryTraining/index.js', () => { jobs: [], }); expect(saveSpy).to.have.been.calledWith(req.userUid); + expect(fetchSpy).to.have.been.calledTwice; }); }); From fe486872e858892af8a2b708de72f64528b780db Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 3 Jan 2025 16:20:20 +0000 Subject: [PATCH 67/72] Remove duplicates logic from frontend --- ...d-manage-mandatory-training.component.html | 17 +++--------- ...anage-mandatory-training.component.spec.ts | 22 ++-------------- ...and-manage-mandatory-training.component.ts | 26 ------------------- 3 files changed, 5 insertions(+), 60 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html index 10535c46bd..5c4f47e902 100644 --- a/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html +++ b/frontend/src/app/features/training-and-qualifications/add-mandatory-training/add-and-manage-mandatory-training/add-and-manage-mandatory-training.component.html @@ -61,21 +61,10 @@

- - {{ 'All' }} + + All - +
{{ job.title }}