From 690b909270f278ade3b7a34821dbd6667c3933c2 Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 10 Jul 2024 17:16:07 +0100 Subject: [PATCH 01/22] Add workers to workplace data returned for NHS API database call --- backend/server/models/establishment.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/server/models/establishment.js b/backend/server/models/establishment.js index c401d4d2ad..2663e108ea 100644 --- a/backend/server/models/establishment.js +++ b/backend/server/models/establishment.js @@ -2366,11 +2366,11 @@ module.exports = function (sequelize, DataTypes) { 'dataOwner', 'NumberOfStaffValue', 'parentId', + 'OverallWdfEligibility', ]; Establishment.getNhsBsaApiDataByWorkplaceId = async function (where) { return await this.findOne({ - nhsBsaAttributes, as: 'establishment', where: { @@ -2384,6 +2384,15 @@ module.exports = function (sequelize, DataTypes) { attributes: ['name', 'category'], required: true, }, + { + model: sequelize.models.worker, + as: 'workers', + attributes: ['WdfEligible', 'LastWdfEligibility'], + where: { + archived: false, + }, + required: false, + }, ], }); }; From c0e7a18852cab11056f61935d1f9ae433fc0970d Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Wed, 10 Jul 2024 17:18:23 +0100 Subject: [PATCH 02/22] Update NHS API endpoint to get workplace data to use WDF data from establishment table instead of WDF report --- .../server/routes/nhsBsaApi/workplaceData.js | 41 ++-- .../routes/nhsBsaApi/workplaceData.spec.js | 201 ++++++++++-------- 2 files changed, 132 insertions(+), 110 deletions(-) diff --git a/backend/server/routes/nhsBsaApi/workplaceData.js b/backend/server/routes/nhsBsaApi/workplaceData.js index af960b8b56..26f801cd44 100644 --- a/backend/server/routes/nhsBsaApi/workplaceData.js +++ b/backend/server/routes/nhsBsaApi/workplaceData.js @@ -51,8 +51,6 @@ const nhsBsaApi = async (req, res) => { }; const workplaceObject = async (workplace) => { - const wdfEligible = await wdfData(workplace.id, WdfCalculator.effectiveDate); - return { workplaceId: workplace.nmdsId, workplaceName: workplace.NameValue, @@ -66,10 +64,18 @@ const workplaceObject = async (workplace) => { numberOfWorkplaceStaff: workplace.NumberOfStaffValue, serviceName: workplace.mainService.name, serviceCategory: workplace.mainService.category, - ...wdfEligible, + eligibilityPercentage: calculatePercentageOfWorkersEligible(workplace.workers), + eligibilityDate: workplace.overallWdfEligibility, + isEligible: workplaceIsEligible(workplace), }; }; +const workplaceIsEligible = (workplace) => { + return workplace.overallWdfEligibility && workplace.overallWdfEligibility.getTime() > WdfCalculator.effectiveDate + ? true + : false; +}; + const subsidiariesList = async (establishmentId) => { const subs = await models.establishment.getNhsBsaApiDataForSubs(establishmentId); @@ -90,33 +96,16 @@ const parentWorkplace = async (parentId) => { return await workplaceObject(parentWorkplace); }; -const wdfData = async (workplaceId, effectiveFrom) => { - const reportData = await models.sequelize.query( - `SELECT * FROM cqc.wdfsummaryreport(:givenEffectiveDate) WHERE "EstablishmentID" = '${workplaceId}'`, - { - replacements: { - givenEffectiveDate: effectiveFrom, - }, - type: models.sequelize.QueryTypes.SELECT, - }, - ); +const calculatePercentageOfWorkersEligible = (workers) => { + const numberOfWorkers = workers?.length; - const wdfMeeting = reportData.find((workplace) => workplace.EstablishmentID === workplaceId); - if (wdfMeeting) { - const percentageEligibleWorkers = - wdfMeeting.WorkerCount > 0 ? Math.floor((wdfMeeting.WorkerCompletedCount / wdfMeeting.WorkerCount) * 100) : 0; - - return { - eligibilityPercentage: percentageEligibleWorkers, - eligibilityDate: wdfMeeting.OverallWdfEligibility, - isEligible: - wdfMeeting.OverallWdfEligibility && wdfMeeting.OverallWdfEligibility.getTime() > effectiveFrom ? true : false, - }; - } + if (!numberOfWorkers) return 0; + const numberOfEligibleWorkers = workers.filter((worker) => worker.get('WdfEligible')).length; + + return Math.floor((numberOfEligibleWorkers / numberOfWorkers) * 100); }; router.route('/:workplaceId').get(authLimiter, authorization.isAuthorised, nhsBsaApi); module.exports = router; module.exports.nhsBsaApi = nhsBsaApi; module.exports.subsidiariesList = subsidiariesList; -module.exports.wdfData = wdfData; diff --git a/backend/server/test/unit/routes/nhsBsaApi/workplaceData.spec.js b/backend/server/test/unit/routes/nhsBsaApi/workplaceData.spec.js index b210298582..9367ca87a4 100644 --- a/backend/server/test/unit/routes/nhsBsaApi/workplaceData.spec.js +++ b/backend/server/test/unit/routes/nhsBsaApi/workplaceData.spec.js @@ -2,38 +2,41 @@ const expect = require('chai').expect; const sinon = require('sinon'); const httpMocks = require('node-mocks-http'); -const { nhsBsaApi, subsidiariesList, wdfData } = require('../../../../routes/nhsBsaApi/workplaceData'); +const { nhsBsaApi } = require('../../../../routes/nhsBsaApi/workplaceData'); const models = require('../../../../models'); +const WdfCalculator = require('../../../../models/classes/wdfCalculator').WdfCalculator; describe('server/routes/nhsBsaApi/workplaceData.js', () => { const workplaceId = 'J1001845'; - let result; - result = { - id: 949, - nmdsId: 'J1001845', - NameValue: 'SKILLS FOR CARE', - address1: 'WEST GATE', - locationId: null, - town: 'LEEDS', - postcode: 'LS1 2RP', - isParent: false, - dataOwner: 'Workplace', - NumberOfStaffValue: 2, - parentId: null, - mainService: { - name: 'Domiciliary care services', - category: 'Adult domiciliary', + const request = { + method: 'GET', + url: `/api/v1/workplaces/${workplaceId}`, + + params: { + workplaceId: workplaceId, }, }; + let result; beforeEach(() => { - const request = { - method: 'GET', - url: `/api/v1/workplaces/${workplaceId}`, - - params: { - workplaceId: workplaceId, + result = { + id: 949, + nmdsId: 'J1001845', + NameValue: 'SKILLS FOR CARE', + address1: 'WEST GATE', + locationId: null, + town: 'LEEDS', + postcode: 'LS1 2RP', + isParent: false, + dataOwner: 'Workplace', + NumberOfStaffValue: 2, + parentId: null, + overallWdfEligibility: new Date('2021-05-13T09:27:34.471Z'), + mainService: { + name: 'Domiciliary care services', + category: 'Adult domiciliary', }, + workers: [], }; req = httpMocks.createRequest(request); @@ -45,13 +48,19 @@ describe('server/routes/nhsBsaApi/workplaceData.js', () => { }); describe('nhsBsaApi', () => { - it('should return 200 when successfully retrieving a workplace data ', async () => { + it('should return 200 when workplace data successfully retrieved', async () => { sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); await nhsBsaApi(req, res); - const response = res._getJSONData(); expect(res.statusCode).to.equal(200); + }); + + it('should return data from database call in expected format', async () => { + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); + + await nhsBsaApi(req, res); + const response = res._getJSONData(); expect(response.workplaceData).to.deep.equal({ workplaceDetails: { @@ -74,14 +83,16 @@ describe('server/routes/nhsBsaApi/workplaceData.js', () => { }); }); - it('should return successfully a list of subsidiaries', async () => { - sinon.stub(models.establishment, 'getNhsBsaApiDataForSubs').returns([result]); + it('should return subsidiaries as an array formatted in the same way as a standalone', async () => { + result.isParent = true; - const establishmentId = result.id; + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); + sinon.stub(models.establishment, 'getNhsBsaApiDataForSubs').returns([result]); - const subs = await subsidiariesList(establishmentId); + await nhsBsaApi(req, res); + const response = res._getJSONData(); - expect(subs).to.deep.equal([ + expect(response.workplaceData.subsidiaries).to.deep.equal([ { workplaceId: 'J1001845', workplaceName: 'SKILLS FOR CARE', @@ -92,7 +103,7 @@ describe('server/routes/nhsBsaApi/workplaceData.js', () => { serviceName: 'Domiciliary care services', serviceCategory: 'Adult domiciliary', eligibilityPercentage: 0, - eligibilityDate: new Date('2021-05-13T09:27:34.471Z'), + eligibilityDate: '2021-05-13T09:27:34.471Z', isEligible: false, }, ]); @@ -112,84 +123,106 @@ describe('server/routes/nhsBsaApi/workplaceData.js', () => { expect(res.statusCode).to.deep.equal(500); }); - }); - describe('wdfData', () => { - const establishmentId = 'A1234567'; + describe('eligibilityPercentage', () => { + it('should set eligibilityPercentage to 0 when workers returned as null', async () => { + result.workers = null; + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); + + await nhsBsaApi(req, res); + const response = res._getJSONData(); - const returnData = (WorkerCompletedCount = 1, WorkerCount = 4, OverallWdfEligibility = null) => { - return [ - { - EstablishmentID: establishmentId, - WorkerCompletedCount, - WorkerCount, - OverallWdfEligibility, - }, - ]; - }; + expect(response.workplaceData.workplaceDetails.eligibilityPercentage).to.equal(0); + }); - it('should return isEligible as false when no date returned for OverallWdfEligibility', async () => { - sinon.stub(models.sequelize, 'query').returns(returnData(1, 4, null)); - const effectiveTime = new Date('2022-05-13T09:27:34.471Z').getTime(); - const wdf = await wdfData(establishmentId, effectiveTime); + it('should set eligibilityPercentage to 0 when workers returned as empty array', async () => { + result.workers = []; + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); - expect(wdf.isEligible).to.equal(false); - expect(wdf.eligibilityDate).to.equal(null); - }); + await nhsBsaApi(req, res); + const response = res._getJSONData(); - it('should return isEligible as false when date returned for OverallWdfEligibility is before effectiveTime', async () => { - const OverallWdfEligibility = new Date('2022-03-13T09:27:34.471Z'); - const effectiveTime = new Date('2022-05-13T09:27:34.471Z').getTime(); - sinon.stub(models.sequelize, 'query').returns(returnData(1, 4, OverallWdfEligibility)); + expect(response.workplaceData.workplaceDetails.eligibilityPercentage).to.equal(0); + }); - const wdf = await wdfData(establishmentId, effectiveTime); + it('should set eligibilityPercentage to 0 when several workers returned but none have WdfEligible as true', async () => { + result.workers = [{ get: () => false }, { get: () => false }, { get: () => false }]; + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); - expect(wdf.isEligible).to.equal(false); - expect(wdf.eligibilityDate).to.equal(OverallWdfEligibility); - }); + await nhsBsaApi(req, res); + const response = res._getJSONData(); - it('should return isEligible as true when date returned for OverallWdfEligibility is after effectiveTime', async () => { - const OverallWdfEligibility = new Date('2022-07-13T09:27:34.471Z'); - const effectiveTime = new Date('2022-06-13T09:27:34.471Z').getTime(); - sinon.stub(models.sequelize, 'query').returns(returnData(4, 4, OverallWdfEligibility)); + expect(response.workplaceData.workplaceDetails.eligibilityPercentage).to.equal(0); + }); - const wdf = await wdfData(establishmentId, effectiveTime); + it('should set eligibilityPercentage to 100 when all workers returned have WdfEligible as true', async () => { + result.workers = [{ get: () => true }, { get: () => true }, { get: () => true }]; + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); - expect(wdf.isEligible).to.equal(true); - expect(wdf.eligibilityDate).to.equal(OverallWdfEligibility); - }); + await nhsBsaApi(req, res); + const response = res._getJSONData(); - describe('eligibilityPercentage', () => { - it('should set percentage to 25 when 1 out of 4 workers completed', async () => { - sinon.stub(models.sequelize, 'query').returns(returnData(1, 4)); + expect(response.workplaceData.workplaceDetails.eligibilityPercentage).to.equal(100); + }); + + it('should set eligibilityPercentage to 25 when 1 out of 4 workers have WdfEligible as true', async () => { + result.workers = [{ get: () => true }, { get: () => false }, { get: () => false }, { get: () => false }]; + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); - const wdf = await wdfData(establishmentId); + await nhsBsaApi(req, res); + const response = res._getJSONData(); - expect(wdf.eligibilityPercentage).to.equal(25); + expect(response.workplaceData.workplaceDetails.eligibilityPercentage).to.equal(25); }); - it('should set percentage to 50 when 5 out of 10 workers completed', async () => { - sinon.stub(models.sequelize, 'query').returns(returnData(5, 10)); + it('should set percentage to 66 when 2 out of 3 workers have WdfEligible as true', async () => { + result.workers = [{ get: () => true }, { get: () => true }, { get: () => false }]; + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); - const wdf = await wdfData(establishmentId); + await nhsBsaApi(req, res); + const response = res._getJSONData(); - expect(wdf.eligibilityPercentage).to.equal(50); + expect(response.workplaceData.workplaceDetails.eligibilityPercentage).to.equal(66); }); + }); - it('should set percentage to 0 when worker count is 0', async () => { - sinon.stub(models.sequelize, 'query').returns(returnData(0, 0)); + describe('isEligible', () => { + it('should return isEligible as false when no date returned for OverallWdfEligibility', async () => { + result.overallWdfEligibility = null; + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); - const wdf = await wdfData(establishmentId); + await nhsBsaApi(req, res); + const response = res._getJSONData(); - expect(wdf.eligibilityPercentage).to.equal(0); + expect(response.workplaceData.workplaceDetails.isEligible).to.equal(false); + expect(response.workplaceData.workplaceDetails.eligibilityDate).to.equal(null); }); - it('should set percentage to 100 when worker count is 9 and completed is 9', async () => { - sinon.stub(models.sequelize, 'query').returns(returnData(9, 9)); + it('should return isEligible as false when date returned for OverallWdfEligibility is before effectiveTime', async () => { + const overallWdfEligibility = '2022-03-13T09:27:34.471Z'; + result.overallWdfEligibility = new Date(overallWdfEligibility); + WdfCalculator.effectiveDate = new Date('2022-05-13T09:27:34.471Z').getTime(); + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); + + await nhsBsaApi(req, res); + const response = res._getJSONData(); + + expect(response.workplaceData.workplaceDetails.isEligible).to.equal(false); + expect(response.workplaceData.workplaceDetails.eligibilityDate).to.equal(overallWdfEligibility); + }); + + it('should return isEligible as true when date returned for overallWdfEligibility is after effectiveTime', async () => { + const overallWdfEligibility = '2022-07-13T09:27:34.471Z'; + result.overallWdfEligibility = new Date(overallWdfEligibility); + + WdfCalculator.effectiveDate = new Date('2022-06-13T09:27:34.471Z').getTime(); + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); - const wdf = await wdfData(establishmentId); + await nhsBsaApi(req, res); + const response = res._getJSONData(); - expect(wdf.eligibilityPercentage).to.equal(100); + expect(response.workplaceData.workplaceDetails.isEligible).to.equal(true); + expect(response.workplaceData.workplaceDetails.eligibilityDate).to.equal(overallWdfEligibility); }); }); }); From acf3d5ff985356cb23bfdefadc09b92d2d50d6bc Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 12 Jul 2024 09:23:55 +0100 Subject: [PATCH 03/22] Refactor NHS BSA query to ensure data returned for subs is same as for parent/stand alone --- backend/server/models/establishment.js | 47 ++++--------------- .../server/routes/nhsBsaApi/workplaceData.js | 10 ++-- 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/backend/server/models/establishment.js b/backend/server/models/establishment.js index 2663e108ea..293841b975 100644 --- a/backend/server/models/establishment.js +++ b/backend/server/models/establishment.js @@ -2354,25 +2354,9 @@ module.exports = function (sequelize, DataTypes) { }); }; - const nhsBsaAttributes = [ - 'id', - 'nmdsId', - 'NameValue', - 'address1', - 'locationId', - 'town', - 'postcode', - 'isParent', - 'dataOwner', - 'NumberOfStaffValue', - 'parentId', - 'OverallWdfEligibility', - ]; - - Establishment.getNhsBsaApiDataByWorkplaceId = async function (where) { - return await this.findOne({ + const nhsBsaApiQuery = (where) => { + return { as: 'establishment', - where: { archived: false, ...where, @@ -2387,35 +2371,22 @@ module.exports = function (sequelize, DataTypes) { { model: sequelize.models.worker, as: 'workers', - attributes: ['WdfEligible', 'LastWdfEligibility'], + attributes: ['WdfEligible'], where: { archived: false, }, required: false, }, ], - }); + }; }; - Establishment.getNhsBsaApiDataForSubs = async function (establishmentId) { - return await this.findAll({ - nhsBsaAttributes, - as: 'establishment', - - where: { - archived: false, - parentId: establishmentId, - }, + Establishment.getNhsBsaApiDataByWorkplaceId = async function (workplaceId) { + return await this.findOne(nhsBsaApiQuery({ nmdsId: workplaceId })); + }; - include: [ - { - model: sequelize.models.services, - as: 'mainService', - attributes: ['name', 'category'], - required: true, - }, - ], - }); + Establishment.getNhsBsaApiDataForSubs = async function (parentId) { + return await this.findAll(nhsBsaApiQuery({ parentId })); }; return Establishment; diff --git a/backend/server/routes/nhsBsaApi/workplaceData.js b/backend/server/routes/nhsBsaApi/workplaceData.js index 26f801cd44..0942900cc4 100644 --- a/backend/server/routes/nhsBsaApi/workplaceData.js +++ b/backend/server/routes/nhsBsaApi/workplaceData.js @@ -9,12 +9,8 @@ const WdfCalculator = require('../../models/classes/wdfCalculator').WdfCalculato const nhsBsaApi = async (req, res) => { const workplaceId = req.params.workplaceId; - const where = { - nmdsId: workplaceId, - }; - try { - const workplaceDetail = await models.establishment.getNhsBsaApiDataByWorkplaceId(where); + const workplaceDetail = await models.establishment.getNhsBsaApiDataByWorkplaceId(workplaceId); if (!workplaceDetail) return res.status(404).json({ error: 'Can not find this Id.' }); const isParent = workplaceDetail.isParent; @@ -76,8 +72,8 @@ const workplaceIsEligible = (workplace) => { : false; }; -const subsidiariesList = async (establishmentId) => { - const subs = await models.establishment.getNhsBsaApiDataForSubs(establishmentId); +const subsidiariesList = async (parentId) => { + const subs = await models.establishment.getNhsBsaApiDataForSubs(parentId); const subsidiaries = await Promise.all( subs.map(async (workplace) => { From c329db6ffbe33993aa0c7fd4c85247823dff27ed Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Fri, 12 Jul 2024 10:07:27 +0100 Subject: [PATCH 04/22] Create separate database query for parent to show distinction between type of workplace ID used in WHERE clause --- backend/server/models/establishment.js | 4 +++ .../server/routes/nhsBsaApi/workplaceData.js | 23 +++++++-------- .../routes/nhsBsaApi/workplaceData.spec.js | 29 +++++++++++++++++-- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/backend/server/models/establishment.js b/backend/server/models/establishment.js index 293841b975..da66e1c123 100644 --- a/backend/server/models/establishment.js +++ b/backend/server/models/establishment.js @@ -2385,6 +2385,10 @@ module.exports = function (sequelize, DataTypes) { return await this.findOne(nhsBsaApiQuery({ nmdsId: workplaceId })); }; + Establishment.getNhsBsaApiDataForParent = async function (workplaceId) { + return await this.findOne(nhsBsaApiQuery({ id: workplaceId })); + }; + Establishment.getNhsBsaApiDataForSubs = async function (parentId) { return await this.findAll(nhsBsaApiQuery({ parentId })); }; diff --git a/backend/server/routes/nhsBsaApi/workplaceData.js b/backend/server/routes/nhsBsaApi/workplaceData.js index 0942900cc4..ea68ddfc97 100644 --- a/backend/server/routes/nhsBsaApi/workplaceData.js +++ b/backend/server/routes/nhsBsaApi/workplaceData.js @@ -10,29 +10,29 @@ const nhsBsaApi = async (req, res) => { const workplaceId = req.params.workplaceId; try { - const workplaceDetail = await models.establishment.getNhsBsaApiDataByWorkplaceId(workplaceId); - if (!workplaceDetail) return res.status(404).json({ error: 'Can not find this Id.' }); + const workplace = await models.establishment.getNhsBsaApiDataByWorkplaceId(workplaceId); + if (!workplace) return res.status(404).json({ error: 'Cannot find this Id.' }); - const isParent = workplaceDetail.isParent; - const establishmentId = workplaceDetail.id; - const parentId = workplaceDetail.parentId; + const isParent = workplace.isParent; + const establishmentId = workplace.id; + const parentId = workplace.parentId; let workplaceData = null; if (isParent) { workplaceData = { - isParent: workplaceDetail.isParent, - workplaceDetails: await workplaceObject(workplaceDetail), + isParent, + workplaceDetails: await workplaceObject(workplace), subsidiaries: await subsidiariesList(establishmentId), }; } else if (parentId) { workplaceData = { - workplaceDetails: await workplaceObject(workplaceDetail), + workplaceDetails: await workplaceObject(workplace), parentWorkplace: await parentWorkplace(parentId), }; } else { workplaceData = { - workplaceDetails: await workplaceObject(workplaceDetail), + workplaceDetails: await workplaceObject(workplace), }; } @@ -84,10 +84,7 @@ const subsidiariesList = async (parentId) => { }; const parentWorkplace = async (parentId) => { - const where = { - id: parentId, - }; - const parentWorkplace = await models.establishment.getNhsBsaApiDataByWorkplaceId(where); + const parentWorkplace = await models.establishment.getNhsBsaApiDataForParent(parentId); return await workplaceObject(parentWorkplace); }; diff --git a/backend/server/test/unit/routes/nhsBsaApi/workplaceData.spec.js b/backend/server/test/unit/routes/nhsBsaApi/workplaceData.spec.js index 9367ca87a4..e7ea85413b 100644 --- a/backend/server/test/unit/routes/nhsBsaApi/workplaceData.spec.js +++ b/backend/server/test/unit/routes/nhsBsaApi/workplaceData.spec.js @@ -83,7 +83,7 @@ describe('server/routes/nhsBsaApi/workplaceData.js', () => { }); }); - it('should return subsidiaries as an array formatted in the same way as a standalone', async () => { + it('should return subsidiaries as an array formatted in the same way as a standalone when is parent', async () => { result.isParent = true; sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); @@ -92,6 +92,7 @@ describe('server/routes/nhsBsaApi/workplaceData.js', () => { await nhsBsaApi(req, res); const response = res._getJSONData(); + expect(response.workplaceData.isParent).to.equal(true); expect(response.workplaceData.subsidiaries).to.deep.equal([ { workplaceId: 'J1001845', @@ -109,12 +110,36 @@ describe('server/routes/nhsBsaApi/workplaceData.js', () => { ]); }); + it('should return parent data formatted in the same way as a standalone when workplace has parent ID', async () => { + result.parentId = 'J1001845'; + + sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(result); + sinon.stub(models.establishment, 'getNhsBsaApiDataForParent').returns(result); + + await nhsBsaApi(req, res); + const response = res._getJSONData(); + + expect(response.workplaceData.parentWorkplace).to.deep.equal({ + workplaceId: 'J1001845', + workplaceName: 'SKILLS FOR CARE', + dataOwner: 'Workplace', + workplaceAddress: { firstLine: 'WEST GATE', town: 'LEEDS', postCode: 'LS1 2RP' }, + locationId: null, + numberOfWorkplaceStaff: 2, + serviceName: 'Domiciliary care services', + serviceCategory: 'Adult domiciliary', + eligibilityPercentage: 0, + eligibilityDate: '2021-05-13T09:27:34.471Z', + isEligible: false, + }); + }); + it('should return 404 when workplace is not found', async () => { sinon.stub(models.establishment, 'getNhsBsaApiDataByWorkplaceId').returns(null); await nhsBsaApi(req, res); expect(res.statusCode).to.deep.equal(404); - expect(res._getJSONData()).to.deep.equal({ error: 'Can not find this Id.' }); + expect(res._getJSONData()).to.deep.equal({ error: 'Cannot find this Id.' }); }); it('should return 500 when an error is thrown', async () => { From 3aa8b087079321d493cb3840ef9b3ff56f28db3b Mon Sep 17 00:00:00 2001 From: Sabrina Date: Tue, 20 Aug 2024 13:39:34 +0100 Subject: [PATCH 05/22] Show the mandatory training category in select training category --- ...select-training-category.component.spec.ts | 22 ++++++++++++++++++- .../select-training-category.directive.ts | 8 +++++-- 2 files changed, 27 insertions(+), 3 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 b111de12a0..fba4dfc392 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 @@ -19,9 +19,10 @@ import { establishmentBuilder } from '@core/test-utils/MockEstablishmentService' import { Establishment } from '@core/model/establishment.model'; import { SelectTrainingCategoryComponent } from './select-training-category.component'; import { trainingCategories } from '@core/test-utils/MockTrainingCategoriesService'; +import sinon from 'sinon'; describe('SelectTrainingCategoryComponent', () => { - async function setup(prefill = false) { + async function setup(prefill = false, qsParamGetMock = sinon.stub()) { const establishment = establishmentBuilder() as Establishment; const worker = workerBuilder(); @@ -54,6 +55,9 @@ describe('SelectTrainingCategoryComponent', () => { establishment: establishment, trainingCategories: trainingCategories, }, + queryParamMap: { + get: qsParamGetMock, + }, }, }, }, @@ -165,4 +169,20 @@ describe('SelectTrainingCategoryComponent', () => { expect(component.form.invalid).toBeTruthy(); expect(getAllByText('Select the training category').length).toEqual(1); }); + + it('should pre-fill when adding a record to a mandatory training category', async () => { + const qsParamGetMock = sinon.stub().returns(JSON.stringify({ id: 2, category: 'Autism' })); + + const { component, fixture } = await setup(false, qsParamGetMock); + + fixture.detectChanges(); + + expect(component.form.value).toEqual({ category: 2 }); + }); + + it('should pre-fill if there is a selected category', async () => { + const { component } = await setup(true); + + expect(component.form.value).toEqual({ category: 1 }); + }); }); 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 4bda9b5a98..b3a3e3866e 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 @@ -64,11 +64,15 @@ export class SelectTrainingCategoryDirective implements OnInit, AfterViewInit { protected prefillForm(): void { let selectedCategory = this.trainingService.selectedTraining?.trainingCategory; - if (selectedCategory) { + if (this.route.snapshot.queryParamMap.get('trainingCategory')) { + const mandatoryTrainingCategory = JSON.parse(this.route.snapshot.queryParamMap.get('trainingCategory')); + this.form.setValue({ category: mandatoryTrainingCategory.id }); + this.preFilledId = mandatoryTrainingCategory.id; + } else if (selectedCategory) { this.form.setValue({ category: selectedCategory?.id }); this.preFilledId = selectedCategory?.id; - this.form.get('category').updateValueAndValidity(); } + this.form.get('category').updateValueAndValidity(); } protected submit(selectedCategory: any): void {} From ed1bd1576a9d0f1f865f8085028e6a074fc76a11 Mon Sep 17 00:00:00 2001 From: Sabrina Date: Tue, 20 Aug 2024 14:12:58 +0100 Subject: [PATCH 06/22] Show training category on edit training record --- .../add-edit-training.component.spec.ts | 9 ++++++++- .../add-edit-training/add-edit-training.component.ts | 3 ++- .../select-training-category-multiple.component.spec.ts | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-edit-training/add-edit-training.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-edit-training/add-edit-training.component.spec.ts index 5dc4dab891..5d5762b851 100644 --- a/frontend/src/app/features/training-and-qualifications/add-edit-training/add-edit-training.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-edit-training/add-edit-training.component.spec.ts @@ -97,7 +97,7 @@ describe('AddEditTrainingComponent', () => { expect(getByText(component.worker.nameOrId, { exact: false })).toBeTruthy(); }); - describe('Training category select/display', async () => { + describe('Training category display', async () => { it('should show the training category displayed as text when there is a training category present and update the form value', async () => { const qsParamGetMock = sinon.stub(); const { component, fixture, getByText, getByTestId, queryByTestId, workerService } = await setup( @@ -155,6 +155,13 @@ describe('AddEditTrainingComponent', () => { expect(form.value).toEqual(expectedFormValue); expect(getByTestId('changeTrainingCategoryLink')).toBeTruthy(); }); + + it('should show the training category displayed as text when editing an existing training record', async () => { + const { getByText, getByTestId } = await setup(); + + expect(getByTestId('trainingCategoryDisplay')).toBeTruthy(); + expect(getByText('Communication')).toBeTruthy(); + }); }); describe('title', () => { diff --git a/frontend/src/app/features/training-and-qualifications/add-edit-training/add-edit-training.component.ts b/frontend/src/app/features/training-and-qualifications/add-edit-training/add-edit-training.component.ts index 778d70437c..72f89ada15 100644 --- a/frontend/src/app/features/training-and-qualifications/add-edit-training/add-edit-training.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-edit-training/add-edit-training.component.ts @@ -87,7 +87,8 @@ export class AddEditTrainingComponent extends AddEditTrainingDirective implement (trainingRecord) => { if (trainingRecord) { this.trainingRecord = trainingRecord; - this.trainingCategory = this.trainingRecord.trainingCategory; + this.category = trainingRecord.trainingCategory.category; + this.trainingCategory = trainingRecord.trainingCategory; const completed = this.trainingRecord.completed ? dayjs(this.trainingRecord.completed, DATE_PARSE_FORMAT) 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 fb1ade6a1c..728962800f 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 @@ -19,9 +19,10 @@ 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 sinon from 'sinon'; describe('SelectTrainingCategoryMultipleComponent', () => { - async function setup(prefill = false, accessedFromSummary = false) { + async function setup(prefill = false, accessedFromSummary = false, qsParamGetMock = sinon.stub()) { const establishment = establishmentBuilder() as Establishment; const { fixture, getByText, getAllByText, getByTestId } = await render(SelectTrainingCategoryMultipleComponent, { imports: [HttpClientTestingModule, SharedModule, RouterModule, RouterTestingModule, AddMultipleTrainingModule], @@ -50,6 +51,9 @@ describe('SelectTrainingCategoryMultipleComponent', () => { parent: { url: [{ path: accessedFromSummary ? 'confirm-training' : 'select-staff' }], }, + queryParamMap: { + get: qsParamGetMock, + }, }, }, }, From 16676b0fb653e88cbfd43da3363c2844333f9aad Mon Sep 17 00:00:00 2001 From: Duncan Carter Date: Tue, 20 Aug 2024 14:40:34 +0100 Subject: [PATCH 07/22] Add Health and care visa and Inside or outside UK questions to WDF routing --- .../wdf/wdf-data-change/wdf-routing.module.ts | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/frontend/src/app/features/wdf/wdf-data-change/wdf-routing.module.ts b/frontend/src/app/features/wdf/wdf-data-change/wdf-routing.module.ts index 5af29926d6..b26f741f2e 100644 --- a/frontend/src/app/features/wdf/wdf-data-change/wdf-routing.module.ts +++ b/frontend/src/app/features/wdf/wdf-data-change/wdf-routing.module.ts @@ -4,48 +4,34 @@ import { CheckPermissionsGuard } from '@core/guards/permissions/check-permission import { HasPermissionsGuard } from '@core/guards/permissions/has-permissions/has-permissions.guard'; import { WorkerResolver } from '@core/resolvers/worker.resolver'; import { WorkplaceResolver } from '@core/resolvers/workplace.resolver'; -import { - AdultSocialCareStartedComponent, -} from '@features/workers/adult-social-care-started/adult-social-care-started.component'; -import { - ApprenticeshipTrainingComponent, -} from '@features/workers/apprenticeship-training/apprenticeship-training.component'; +import { AdultSocialCareStartedComponent } from '@features/workers/adult-social-care-started/adult-social-care-started.component'; +import { ApprenticeshipTrainingComponent } from '@features/workers/apprenticeship-training/apprenticeship-training.component'; import { AverageWeeklyHoursComponent } from '@features/workers/average-weekly-hours/average-weekly-hours.component'; import { BritishCitizenshipComponent } from '@features/workers/british-citizenship/british-citizenship.component'; import { CareCertificateComponent } from '@features/workers/care-certificate/care-certificate.component'; -import { - ContractWithZeroHoursComponent, -} from '@features/workers/contract-with-zero-hours/contract-with-zero-hours.component'; +import { ContractWithZeroHoursComponent } from '@features/workers/contract-with-zero-hours/contract-with-zero-hours.component'; import { CountryOfBirthComponent } from '@features/workers/country-of-birth/country-of-birth.component'; import { DateOfBirthComponent } from '@features/workers/date-of-birth/date-of-birth.component'; import { DaysOfSicknessComponent } from '@features/workers/days-of-sickness/days-of-sickness.component'; import { DisabilityComponent } from '@features/workers/disability/disability.component'; +import { EmployedFromOutsideUkComponent } from '@features/workers/employed-from-outside-uk/employed-from-outside-uk.component'; import { EthnicityComponent } from '@features/workers/ethnicity/ethnicity.component'; import { GenderComponent } from '@features/workers/gender/gender.component'; +import { HealthAndCareVisaComponent } from '@features/workers/health-and-care-visa/health-and-care-visa.component'; import { HomePostcodeComponent } from '@features/workers/home-postcode/home-postcode.component'; import { MainJobStartDateComponent } from '@features/workers/main-job-start-date/main-job-start-date.component'; -import { - MentalHealthProfessionalComponent, -} from '@features/workers/mental-health-professional/mental-health-professional.component'; -import { - NationalInsuranceNumberComponent, -} from '@features/workers/national-insurance-number/national-insurance-number.component'; +import { MentalHealthProfessionalComponent } from '@features/workers/mental-health-professional/mental-health-professional.component'; +import { NationalInsuranceNumberComponent } from '@features/workers/national-insurance-number/national-insurance-number.component'; import { NationalityComponent } from '@features/workers/nationality/nationality.component'; import { NursingCategoryComponent } from '@features/workers/nursing-category/nursing-category.component'; import { NursingSpecialismComponent } from '@features/workers/nursing-specialism/nursing-specialism.component'; -import { - OtherQualificationsLevelComponent, -} from '@features/workers/other-qualifications-level/other-qualifications-level.component'; +import { OtherQualificationsLevelComponent } from '@features/workers/other-qualifications-level/other-qualifications-level.component'; import { OtherQualificationsComponent } from '@features/workers/other-qualifications/other-qualifications.component'; import { RecruitedFromComponent } from '@features/workers/recruited-from/recruited-from.component'; import { SalaryComponent } from '@features/workers/salary/salary.component'; import { SelectRecordTypeComponent } from '@features/workers/select-record-type/select-record-type.component'; -import { - SocialCareQualificationLevelComponent, -} from '@features/workers/social-care-qualification-level/social-care-qualification-level.component'; -import { - SocialCareQualificationComponent, -} from '@features/workers/social-care-qualification/social-care-qualification.component'; +import { SocialCareQualificationLevelComponent } from '@features/workers/social-care-qualification-level/social-care-qualification-level.component'; +import { SocialCareQualificationComponent } from '@features/workers/social-care-qualification/social-care-qualification.component'; import { StaffDetailsComponent } from '@features/workers/staff-details/staff-details.component'; import { WeeklyContractedHoursComponent } from '@features/workers/weekly-contracted-hours/weekly-contracted-hours.component'; import { YearArrivedUkComponent } from '@features/workers/year-arrived-uk/year-arrived-uk.component'; @@ -169,6 +155,16 @@ const routes: Routes = [ component: RecruitedFromComponent, data: { title: 'Recruited From' }, }, + { + path: 'health-and-care-visa', + component: HealthAndCareVisaComponent, + data: { title: 'Health and Care Visa' }, + }, + { + path: 'inside-or-outside-of-uk', + component: EmployedFromOutsideUkComponent, + data: { title: 'Inside or Outside UK' }, + }, { path: 'adult-social-care-started', component: AdultSocialCareStartedComponent, @@ -348,6 +344,16 @@ const routes: Routes = [ component: RecruitedFromComponent, data: { title: 'Recruited From' }, }, + { + path: 'health-and-care-visa', + component: HealthAndCareVisaComponent, + data: { title: 'Health and Care Visa' }, + }, + { + path: 'inside-or-outside-of-uk', + component: EmployedFromOutsideUkComponent, + data: { title: 'Inside or Outside UK' }, + }, { path: 'adult-social-care-started', component: AdultSocialCareStartedComponent, From 299c6d6a604b8b23f79dee26abfffc2cfe8bde4e Mon Sep 17 00:00:00 2001 From: Sabrina Date: Tue, 20 Aug 2024 16:21:59 +0100 Subject: [PATCH 08/22] Add check to clear selected category at beginning of add training record --- .../select-staff/select-staff.component.spec.ts | 10 ++++++++++ .../select-staff/select-staff.component.ts | 7 +++++++ .../select-record-type.component.spec.ts | 13 ++++++++++++- .../select-record-type.component.ts | 8 ++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-staff/select-staff.component.spec.ts b/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-staff/select-staff.component.spec.ts index 6f59094436..6ed560f1c8 100644 --- a/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-staff/select-staff.component.spec.ts +++ b/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-staff/select-staff.component.spec.ts @@ -90,6 +90,7 @@ describe('SelectStaffComponent', () => { trainingService, 'clearUpdatingSelectedStaffForMultipleTraining', ).and.callThrough(); + const clearSelectedTrainingCategorySpy = spyOn(trainingService, 'clearSelectedTrainingCategory').and.callThrough(); return { component, @@ -110,6 +111,7 @@ describe('SelectStaffComponent', () => { searchSpy, workers, clearUpdatingSelectedStaffForMultipleTrainingSpy, + clearSelectedTrainingCategorySpy, }; } @@ -599,4 +601,12 @@ describe('SelectStaffComponent', () => { expect(spy.calls.mostRecent().args[0]).toEqual(['../']); }); }); + + it('should call trainingService if there are no selected workers when landing on the page', async () => { + const { component, clearSelectedTrainingCategorySpy } = await setup(); + + component.ngOnInit(); + + expect(clearSelectedTrainingCategorySpy).toHaveBeenCalled(); + }); }); diff --git a/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-staff/select-staff.component.ts b/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-staff/select-staff.component.ts index 5b1798267a..18cafa379c 100644 --- a/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-staff/select-staff.component.ts +++ b/frontend/src/app/features/training-and-qualifications/add-multiple-training/select-staff/select-staff.component.ts @@ -72,6 +72,13 @@ export class SelectStaffComponent implements OnInit, AfterViewInit { private prefill(): void { this.selectedWorkers = this.trainingService.selectedStaff.map((worker) => worker.uid); this.updateSelectAllLinks(); + this.clearSelectedTrainingCategoryOnPageEntry(); + } + + private clearSelectedTrainingCategoryOnPageEntry() { + if (this.selectedWorkers.length === 0) { + this.trainingService.clearSelectedTrainingCategory(); + } } public getPageOfWorkers(): void { diff --git a/frontend/src/app/features/workers/select-record-type/select-record-type.component.spec.ts b/frontend/src/app/features/workers/select-record-type/select-record-type.component.spec.ts index 9e540a70f0..7c4c8b2a4e 100644 --- a/frontend/src/app/features/workers/select-record-type/select-record-type.component.spec.ts +++ b/frontend/src/app/features/workers/select-record-type/select-record-type.component.spec.ts @@ -30,6 +30,8 @@ describe('SelectRecordTypeComponent', () => { 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 { component, @@ -37,6 +39,7 @@ describe('SelectRecordTypeComponent', () => { routerSpy, getByText, getAllByText, + clearSelectedTrainingCategorySpy, }; } @@ -62,7 +65,7 @@ describe('SelectRecordTypeComponent', () => { fixture.detectChanges(); const form = component.form; expect(form.valid).toBeTruthy(); - // expect(form.value).toEqual({ selectRecordType: 'Training course' }); + expect(form.value).toEqual({ selectRecordType: 'Training course' }); }); it('should not prefill the radio button when navigating from training page', async () => { @@ -74,4 +77,12 @@ describe('SelectRecordTypeComponent', () => { expect(form.value).toEqual({ selectRecordType: null }); }); }); + + it('should call trainingService if there is no trainingOrQualificationPreviouslySelected when landing on the page', async () => { + const { component, clearSelectedTrainingCategorySpy } = await setup(); + + component.ngOnInit(); + + expect(clearSelectedTrainingCategorySpy).toHaveBeenCalled(); + }); }); diff --git a/frontend/src/app/features/workers/select-record-type/select-record-type.component.ts b/frontend/src/app/features/workers/select-record-type/select-record-type.component.ts index 58741bbdda..97bdf25a73 100644 --- a/frontend/src/app/features/workers/select-record-type/select-record-type.component.ts +++ b/frontend/src/app/features/workers/select-record-type/select-record-type.component.ts @@ -57,6 +57,14 @@ export class SelectRecordTypeComponent implements OnInit, AfterViewInit { if (this.trainingOrQualificationPreviouslySelected) { this.prefill(this.getPrefillValue()); } + + this.clearSelectedTrainingCategoryOnPageEntry(); + } + + private clearSelectedTrainingCategoryOnPageEntry() { + if (!this.trainingOrQualificationPreviouslySelected || this.trainingOrQualificationPreviouslySelected === 'null') { + this.trainingService.clearSelectedTrainingCategory(); + } } private setupForm(): void { From 304a2ab6582801119a0531b4b94c3dc24511bbf5 Mon Sep 17 00:00:00 2001 From: Sabrina Date: Wed, 21 Aug 2024 09:24:29 +0100 Subject: [PATCH 09/22] Add styling change to only show header menu button on small screens --- .../src/app/core/components/header/header.component.html | 2 +- .../src/app/core/components/header/header.component.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/core/components/header/header.component.html b/frontend/src/app/core/components/header/header.component.html index e7b0444e97..e88fc5f72b 100644 --- a/frontend/src/app/core/components/header/header.component.html +++ b/frontend/src/app/core/components/header/header.component.html @@ -8,7 +8,7 @@ From c5d48a775153961351f37eb9240e7942fb3838c7 Mon Sep 17 00:00:00 2001 From: Sabrina Date: Thu, 22 Aug 2024 10:27:09 +0100 Subject: [PATCH 18/22] Add second error message to the select training category --- ...select-training-category.component.spec.ts | 2 +- ...aining-category-multiple.component.spec.ts | 2 +- .../select-training-category.component.html | 49 ++++++++++--------- .../select-training-category.directive.ts | 2 + 4 files changed, 31 insertions(+), 24 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 fba4dfc392..3865be338d 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 @@ -167,7 +167,7 @@ describe('SelectTrainingCategoryComponent', () => { fixture.detectChanges(); expect(component.form.invalid).toBeTruthy(); - expect(getAllByText('Select the training category').length).toEqual(1); + expect(getAllByText('Select the training category').length).toEqual(2); }); it('should pre-fill when adding a record to a mandatory training category', async () => { 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 728962800f..3e1be784fa 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 @@ -228,6 +228,6 @@ describe('SelectTrainingCategoryMultipleComponent', () => { fixture.detectChanges(); expect(component.form.invalid).toBeTruthy(); - expect(getAllByText('Select the training category').length).toEqual(1); + expect(getAllByText('Select the training category').length).toEqual(2); }); }); 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 503d981ce9..01b6db4083 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 @@ -1,6 +1,6 @@ -
+
@@ -8,33 +8,38 @@ {{ section }}

{{ title }}

+
+

+ Error: {{ formErrorsMap[0].type[0].message }} +

+ - - -
-
- - +
+
+ + +
+
-
-
- +
+
+
+ +
+ +

+ Error: {{ errorMessage }} +

+
+ +
diff --git a/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.spec.ts b/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.spec.ts new file mode 100644 index 0000000000..58c861f6e3 --- /dev/null +++ b/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.spec.ts @@ -0,0 +1,69 @@ +import { render } from '@testing-library/angular'; +import { GroupedRadioButtonAccordionComponent } from './grouped-radio-button-accordion.component'; +import { SharedModule } from '@shared/shared.module'; +import { FormsModule } from '@angular/forms'; + +describe('GroupedRadioButtonAccordionComponent', () => { + async function setup(props?) { + const { fixture, getByText, getByTestId } = await render(GroupedRadioButtonAccordionComponent, { + imports: [SharedModule, FormsModule], + providers: [], + componentProperties: { + preFilledId: props?.preFilledId ? props.preFilledId : 10, + formControlName: props?.formControlName ? props.formControlName : 'testAccordions', + textShowHideAll: props?.textShowHideAll ? props.textShowHideAll : 'category', + hasError: props?.hasError ? props.hasError : false, + errorMessage: props?.errorMessage ? props.errorMessage : 'Select the training category', + accordions: props?.accordions + ? props.accordions + : [ + { + title: 'Test Accordion', + descriptionText: 'A Description', + open: false, + index: 0, + items: [ + { + id: 1, + label: 'option 1', + }, + { + id: 2, + label: 'option 2', + }, + ], + }, + ], + }, + }); + + const component = fixture.componentInstance; + + return { component, fixture, getByText }; + } + + it('should render', async () => { + const { component } = await setup(); + expect(component).toBeTruthy(); + }); + + it('should display the toggle text', async () => { + const { getByText } = await setup(); + + const toggleText = getByText('Show all category'); + expect(toggleText).toBeTruthy; + }); + + it('display an error message that we passed in', async () => { + const { getByText } = await setup({ hasError: true, errorMessage: 'Select the job role' }); + + const errorMessage = getByText('Select the job role'); + expect(errorMessage).toBeTruthy(); + }); + + it('open all the accordions when there is an error', async () => { + const { component } = await setup({ hasError: true, errorMessage: 'Select the job role' }); + + expect(component.showAll).toBe(true); + }); +}); diff --git a/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.ts b/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.ts index 82a1dca962..6a31c13785 100644 --- a/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.ts +++ b/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.ts @@ -18,6 +18,8 @@ export class GroupedRadioButtonAccordionComponent implements ControlValueAccesso @Input() preFilledId: number; @Input() formControlName: string; @Input() textShowHideAll?: string; + @Input() hasError: boolean = false; + @Input() errorMessage: string; @Input() set accordions( value: { title: string; @@ -67,6 +69,13 @@ export class GroupedRadioButtonAccordionComponent implements ControlValueAccesso this.toggleAccordionOfPrefilledRadioButton(); } + ngOnChanges(): void { + if (this.hasError) { + this.openAll(); + this.updateToggleAlltext(); + } + } + private openAll(): void { this.showAll = true; this.accordions.forEach((x) => (x.open = true)); 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 01b6db4083..be174b35ad 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 @@ -8,19 +8,19 @@ {{ section }}

{{ title }}

-
-

- Error: {{ formErrorsMap[0].type[0].message }} -

- -
+ + +
+
Date: Fri, 23 Aug 2024 11:27:54 +0100 Subject: [PATCH 20/22] fix accordion error msg id so that summary click works on other page --- .../grouped-radio-button-accordion.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.html b/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.html index c0d02f2846..19695185de 100644 --- a/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.html +++ b/frontend/src/app/shared/components/accordions/radio-button-accordion/grouped-radio-button-accordion/grouped-radio-button-accordion.component.html @@ -10,7 +10,7 @@
-

+

Error: {{ errorMessage }}

From 78d01cd5786645139de15877b634cdaf90404d83 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Fri, 23 Aug 2024 11:28:25 +0100 Subject: [PATCH 21/22] pass in error props to accordion in main role page --- .../main-job-role.component.html | 2 + .../main-job-role.component.spec.ts | 130 +++++++++--------- .../main-job-role/main-job-role.component.ts | 1 - 3 files changed, 69 insertions(+), 64 deletions(-) diff --git a/frontend/src/app/features/workers/main-job-role/main-job-role.component.html b/frontend/src/app/features/workers/main-job-role/main-job-role.component.html index c8f2797ea4..0b97fe7e80 100644 --- a/frontend/src/app/features/workers/main-job-role/main-job-role.component.html +++ b/frontend/src/app/features/workers/main-job-role/main-job-role.component.html @@ -15,6 +15,8 @@

{{ worker ? 'Update' : 'Select' }} their mai textShowHideAll="job roles" [preFilledId]="preFilledId" data-testid="accordian" + [hasError]="submitted && form.invalid" + [errorMessage]="formErrorsMap[0].type[0].message" >

diff --git a/frontend/src/app/features/workers/main-job-role/main-job-role.component.spec.ts b/frontend/src/app/features/workers/main-job-role/main-job-role.component.spec.ts index 1e873e3f2e..b356745de8 100644 --- a/frontend/src/app/features/workers/main-job-role/main-job-role.component.spec.ts +++ b/frontend/src/app/features/workers/main-job-role/main-job-role.component.spec.ts @@ -22,7 +22,7 @@ import { Worker } from '@core/model/worker.model'; import userEvent from '@testing-library/user-event'; import { AlertService } from '@core/services/alert.service'; -describe('MainJobRoleComponent', () => { +fdescribe('MainJobRoleComponent', () => { async function setup(insideFlow = true, returnToMandatoryDetails = false, addNewWorker = false) { let path; if (returnToMandatoryDetails) { @@ -32,71 +32,74 @@ describe('MainJobRoleComponent', () => { } else { path = 'staff-record-summary'; } - const { fixture, getByText, getByTestId, getByLabelText, queryByTestId } = await render(MainJobRoleComponent, { - imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, ReactiveFormsModule], - declarations: [ProgressBarComponent], - schemas: [NO_ERRORS_SCHEMA], - providers: [ - UntypedFormBuilder, - AlertService, - WindowRef, - { - provide: PermissionsService, - useFactory: MockPermissionsService.factory(), - deps: [HttpClient, Router, UserService], - }, - { - provide: UserService, - useFactory: MockUserService.factory(0, Roles.Admin), - deps: [HttpClient], - }, - { - provide: WorkerService, - useClass: MockWorkerServiceWithUpdateWorker, - }, - { - provide: ActivatedRoute, - useValue: { - parent: { + const { fixture, getByText, getAllByText, getByTestId, getByLabelText, queryByTestId } = await render( + MainJobRoleComponent, + { + imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, ReactiveFormsModule], + declarations: [ProgressBarComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + UntypedFormBuilder, + AlertService, + WindowRef, + { + provide: PermissionsService, + useFactory: MockPermissionsService.factory(), + deps: [HttpClient, Router, UserService], + }, + { + provide: UserService, + useFactory: MockUserService.factory(0, Roles.Admin), + deps: [HttpClient], + }, + { + provide: WorkerService, + useClass: MockWorkerServiceWithUpdateWorker, + }, + { + provide: ActivatedRoute, + useValue: { + parent: { + snapshot: { + url: [{ path }], + data: { + establishment: { uid: 'mocked-uid' }, + primaryWorkplace: {}, + }, + }, + }, snapshot: { - url: [{ path }], + params: {}, data: { - establishment: { uid: 'mocked-uid' }, - primaryWorkplace: {}, + jobs: [ + { + id: 4, + jobRoleGroup: 'Professional and related roles', + title: 'Allied health professional (not occupational therapist)', + }, + { + id: 10, + jobRoleGroup: 'Care providing roles', + title: 'Care worker', + }, + { + id: 23, + title: 'Registered nurse', + jobRoleGroup: 'Professional and related roles', + }, + { + id: 27, + title: 'Social worker', + jobRoleGroup: 'Professional and related roles', + }, + ], }, }, }, - snapshot: { - params: {}, - data: { - jobs: [ - { - id: 4, - jobRoleGroup: 'Professional and related roles', - title: 'Allied health professional (not occupational therapist)', - }, - { - id: 10, - jobRoleGroup: 'Care providing roles', - title: 'Care worker', - }, - { - id: 23, - title: 'Registered nurse', - jobRoleGroup: 'Professional and related roles', - }, - { - id: 27, - title: 'Social worker', - jobRoleGroup: 'Professional and related roles', - }, - ], - }, - }, }, - }, - ], - }); + ], + }, + ); const component = fixture.componentInstance; const injector = getTestBed(); @@ -128,6 +131,7 @@ describe('MainJobRoleComponent', () => { fixture, getByTestId, getByText, + getAllByText, getByLabelText, router, routerSpy, @@ -294,14 +298,14 @@ describe('MainJobRoleComponent', () => { expect(updateWorkerSpy).not.toHaveBeenCalled(); }); - it('should return an error message if user clicked submit without selecting a job role', async () => { - const { fixture, getByText } = await setup(true, false, true); + fit('should return an error message if user clicked submit without selecting a job role', async () => { + const { fixture, getByText, getAllByText } = await setup(true, false, true); userEvent.click(getByText('Save this staff record')); fixture.detectChanges(); expect(getByText('There is a problem')).toBeTruthy(); - expect(getByText('Select the job role')).toBeTruthy(); + expect(getAllByText('Select the job role')).toHaveSize(2); }); }); diff --git a/frontend/src/app/features/workers/main-job-role/main-job-role.component.ts b/frontend/src/app/features/workers/main-job-role/main-job-role.component.ts index 8345f5fcfb..7c443f756f 100644 --- a/frontend/src/app/features/workers/main-job-role/main-job-role.component.ts +++ b/frontend/src/app/features/workers/main-job-role/main-job-role.component.ts @@ -7,7 +7,6 @@ import { BackLinkService } from '@core/services/backLink.service'; import { ErrorSummaryService } from '@core/services/error-summary.service'; import { NewWorkerMandatoryInfo, WorkerService } from '@core/services/worker.service'; import { EstablishmentService } from '@core/services/establishment.service'; -import { Contracts } from '@core/model/contracts.enum'; import { AlertService } from '@core/services/alert.service'; @Component({ From 2cffbe3752dbbf6e60336bcb644d0ab5a17ec56d Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Fri, 23 Aug 2024 11:46:00 +0100 Subject: [PATCH 22/22] change testid: accordian --> groupedAccordion to fix typo --- .../select-training-category.component.spec.ts | 4 ++-- .../select-training-category-multiple.component.spec.ts | 4 ++-- .../workers/main-job-role/main-job-role.component.html | 2 +- .../workers/main-job-role/main-job-role.component.spec.ts | 6 +++--- .../select-training-category.component.html | 2 +- 5 files changed, 9 insertions(+), 9 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 d008190e28..3e83e8a2d4 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 @@ -121,14 +121,14 @@ describe('SelectTrainingCategoryComponent', () => { expect(cancelLink).toBeTruthy(); }); - it('should show an accordian with the correct categories in', async () => { + it('should show an accordion with the correct categories in', async () => { const { component, getByTestId } = await setup(true); 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('accordian')).toBeTruthy(); + expect(getByTestId('groupedAccordion')).toBeTruthy(); }); it('should call the training service and navigate to the details page', async () => { 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 3e1be784fa..10d86daaa3 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 @@ -137,14 +137,14 @@ describe('SelectTrainingCategoryMultipleComponent', () => { expect(routerSpy).toHaveBeenCalledWith(['/dashboard'], { fragment: 'training-and-qualifications' }); }); - it('should show an accordian with the correct categories in', async () => { + it('should show an accordion with the correct categories in', async () => { const { component, getByTestId } = await setup(true); 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('accordian')).toBeTruthy(); + expect(getByTestId('groupedAccordion')).toBeTruthy(); }); it('should return to the select staff page if there is no selected staff', async () => { diff --git a/frontend/src/app/features/workers/main-job-role/main-job-role.component.html b/frontend/src/app/features/workers/main-job-role/main-job-role.component.html index 0b97fe7e80..7d48ddd06c 100644 --- a/frontend/src/app/features/workers/main-job-role/main-job-role.component.html +++ b/frontend/src/app/features/workers/main-job-role/main-job-role.component.html @@ -14,7 +14,7 @@

{{ worker ? 'Update' : 'Select' }} their mai [accordions]="jobGroups" textShowHideAll="job roles" [preFilledId]="preFilledId" - data-testid="accordian" + data-testid="groupedAccordion" [hasError]="submitted && form.invalid" [errorMessage]="formErrorsMap[0].type[0].message" > diff --git a/frontend/src/app/features/workers/main-job-role/main-job-role.component.spec.ts b/frontend/src/app/features/workers/main-job-role/main-job-role.component.spec.ts index b356745de8..bcd0bb5c34 100644 --- a/frontend/src/app/features/workers/main-job-role/main-job-role.component.spec.ts +++ b/frontend/src/app/features/workers/main-job-role/main-job-role.component.spec.ts @@ -22,7 +22,7 @@ import { Worker } from '@core/model/worker.model'; import userEvent from '@testing-library/user-event'; import { AlertService } from '@core/services/alert.service'; -fdescribe('MainJobRoleComponent', () => { +describe('MainJobRoleComponent', () => { async function setup(insideFlow = true, returnToMandatoryDetails = false, addNewWorker = false) { let path; if (returnToMandatoryDetails) { @@ -180,7 +180,7 @@ fdescribe('MainJobRoleComponent', () => { it('should show the accordion', async () => { const { getByTestId } = await setup(false, true); - expect(getByTestId('accordian')).toBeTruthy(); + expect(getByTestId('groupedAccordion')).toBeTruthy(); }); it('should show the accordion headings', async () => { @@ -298,7 +298,7 @@ fdescribe('MainJobRoleComponent', () => { expect(updateWorkerSpy).not.toHaveBeenCalled(); }); - fit('should return an error message if user clicked submit without selecting a job role', async () => { + it('should return an error message if user clicked submit without selecting a job role', async () => { const { fixture, getByText, getAllByText } = await setup(true, false, true); userEvent.click(getByText('Save this staff record')); 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 be174b35ad..672d6de479 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 @@ -14,7 +14,7 @@

{{ title }}

[accordions]="trainingGroups" textShowHideAll="categories" [preFilledId]="preFilledId" - data-testid="accordian" + data-testid="groupedAccordion" [hasError]="submitted && error" [errorMessage]="formErrorsMap[0].type[0].message" >