diff --git a/.circleci/config.yml b/.circleci/config.yml index 4917cbd671..7e991cde94 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -524,57 +524,6 @@ workflows: only: /^v[0-9]+(\.[0-9]+)*$/ requires: - install - - serverless_deployment: - appname: sfcbenchmarks - folder: bulkUpload - context: serverless-deployment - requires: - - test_frontend - - test_backend - - performance - filters: - branches: - only: - - feat/benchmarks - - serverless_deployment: - appname: sfcstaging - folder: bulkUpload - context: serverless-deployment - requires: - - test_frontend - - test_backend - - performance - filters: - branches: - only: - - test - - serverless_deployment: - appname: sfcpreprod - folder: bulkUpload - context: serverless-deployment - requires: - - test_frontend - - test_backend - - performance - filters: - branches: - only: - - live - - serverless_deployment: - appname: sfcprod - folder: bulkUpload - context: serverless-deployment - requires: - - test_frontend - - test_backend - - performance - filters: - # ignore any commit on any branch by default - branches: - ignore: /.*/ - # only act on version tags - tags: - only: /^v[0-9]+(\.[0-9]+)*$/ - blue_green: name: blue_green_benchmarks appname: sfcbenchmarks @@ -710,6 +659,49 @@ workflows: # org: dhsc-skills-for-care-nmds-sc-2 # space: production # workspace_path: . + - serverless_deployment: + appname: sfcbenchmarks + folder: bulkUpload + context: serverless-deployment + requires: + - blue_green_benchmarks + filters: + branches: + only: + - feat/benchmarks + - serverless_deployment: + appname: sfcstaging + folder: bulkUpload + context: serverless-deployment + requires: + - blue_green_staging + filters: + branches: + only: + - test + - serverless_deployment: + appname: sfcpreprod + folder: bulkUpload + context: serverless-deployment + requires: + - preprod_deploy + filters: + branches: + only: + - live + - serverless_deployment: + appname: sfcprod + folder: bulkUpload + context: serverless-deployment + requires: + - prod_deploy + filters: + # ignore any commit on any branch by default + branches: + ignore: /.*/ + # only act on version tags + tags: + only: /^v[0-9]+(\.[0-9]+)*$/ - sentry_release: requires: - blue_green_benchmarks diff --git a/lambdas/bulkUpload/classes/trainingCSVValidator.js b/lambdas/bulkUpload/classes/trainingCSVValidator.js index 6b9a2d0489..1649fc145a 100644 --- a/lambdas/bulkUpload/classes/trainingCSVValidator.js +++ b/lambdas/bulkUpload/classes/trainingCSVValidator.js @@ -1,21 +1,21 @@ const moment = require('moment'); const BUDI = require('../classes/BUDI').BUDI; +const errors = require('../validateTraining/errors'); class TrainingCsvValidator { constructor(currentLine, lineNumber, mappings) { - this._currentLine = currentLine; - this._lineNumber = lineNumber; - this._validationErrors = []; - - this._localeStId = null; - this._uniqueWorkerId = null; - this._dateCompleted = null; - this._expiry = null; - this._description = null; - this._category = null; - this._accredited = null; - this._notes = null; + this.currentLine = currentLine; + this.lineNumber = lineNumber; + this.validationErrors = []; + this.localeStId = null; + this.uniqueWorkerId = null; + this.dateCompleted = null; + this.expiry = null; + this.description = null; + this.category = null; + this.accredited = null; + this.notes = null; this.BUDI = new BUDI(mappings); } @@ -63,413 +63,192 @@ class TrainingCsvValidator { return 2070; } - get lineNumber() { - return this._lineNumber; - } - - get currentLine() { - return this._currentLine; - } - - get localeStId() { - return this._localeStId; - } - - get uniqueWorkerId() { - return this._uniqueWorkerId; - } - - get completed() { - return this._dateCompleted; - } - - get expiry() { - return this._expiry; - } - - get description() { - return this._description; + validate() { + this._validateLocaleStId(); + this._validateUniqueWorkerId(); + this._validateDateCompleted(); + this._validateExpiry(); + this._validateDescription(); + this._validateCategory(); + this._validateAccredited(); + this._validateNotes(); } - get category() { - return this._category; + toJSON() { + return { + localId: this.localeStId, + uniqueWorkerId: this.uniqueWorkerId, + completed: this.dateCompleted ? this.dateCompleted.format('DD/MM/YYYY') : undefined, + expiry: this.expiry ? this.expiry.format('DD/MM/YYYY') : undefined, + description: this.description, + category: this.category, + accredited: this.accredited, + notes: this.notes, + lineNumber: this.lineNumber, + }; } - get accredited() { - return this._accredited; - } + toAPI() { + const changeProperties = { + trainingCategory: { + id: this.category, + }, + completed: this.dateCompleted ? this.dateCompleted.format('YYYY-MM-DD') : undefined, + expires: this.expiry ? this.expiry.format('YYYY-MM-DD') : undefined, + title: this.description ? this.description : undefined, + notes: this.notes ? this.notes : undefined, + accredited: this.accredited ? this.accredited : undefined, + }; - get notes() { - return this._notes; + return changeProperties; } _validateLocaleStId() { - const myLocaleStId = this._currentLine.LOCALESTID; + const localeStId = this.currentLine.LOCALESTID; const MAX_LENGTH = 50; - - if (!myLocaleStId || myLocaleStId.length === 0) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.LOCALESTID_ERROR, - errType: 'LOCALESTID_ERROR', - error: 'LOCALESTID has not been supplied', - source: this._currentLine.LOCALESTID, - column: 'LOCALESTID', - }); - return false; - } else if (myLocaleStId.length > MAX_LENGTH) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.LOCALESTID_ERROR, - errType: 'LOCALESTID_ERROR', - error: `LOCALESTID is longer than ${MAX_LENGTH} characters`, - source: this._currentLine.LOCALESTID, - column: 'LOCALESTID', - }); - return false; - } else { - this._localeStId = myLocaleStId; - return true; + const errMessage = errors._getValidateLocaleStIdErrMessage(localeStId, MAX_LENGTH); + if (!errMessage) { + this.localeStId = localeStId; + return; } + this._addValidationError('LOCALESTID_ERROR', errMessage, this.currentLine.LOCALESTID, 'LOCALESTID'); } _validateUniqueWorkerId() { - const myUniqueId = this._currentLine.UNIQUEWORKERID; + const uniqueId = this.currentLine.UNIQUEWORKERID; const MAX_LENGTH = 50; - - if (!myUniqueId || myUniqueId.length === 0) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.UNIQUE_WORKER_ID_ERROR, - errType: 'UNIQUE_WORKER_ID_ERROR', - error: 'UNIQUEWORKERID has not been supplied', - source: this._currentLine.UNIQUEWORKERID, - column: 'UNIQUEWORKERID', - }); - return false; - } else if (myUniqueId.length > MAX_LENGTH) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.UNIQUE_WORKER_ID_ERROR, - errType: 'UNIQUE_WORKER_ID_ERROR', - error: `UNIQUEWORKERID is longer than ${MAX_LENGTH} characters`, - source: this._currentLine.UNIQUEWORKERID, - column: 'UNIQUEWORKERID', - }); - return false; - } else { - this._uniqueWorkerId = myUniqueId; - return true; + const errMessage = errors._getValidateUniqueWorkerIdErrMessage(uniqueId, MAX_LENGTH); + if (!errMessage) { + this.uniqueWorkerId = uniqueId; + return; } + this._addValidationError('UNIQUE_WORKER_ID_ERROR', errMessage, this.currentLine.UNIQUEWORKERID, 'UNIQUEWORKERID'); } _validateDateCompleted() { - // optional - const myDateCompleted = this._currentLine.DATECOMPLETED; - const dateFormatRegex = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[012])\/\d{4}$/; - const actualDate = moment.utc(myDateCompleted, 'DD/MM/YYYY'); + if (!this.currentLine.DATECOMPLETED) { + this.dateCompleted = this.currentLine.DATECOMPLETED; + return; + } - if (myDateCompleted) { - if (!dateFormatRegex.test(myDateCompleted)) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.DATE_COMPLETED_ERROR, - errType: 'DATE_COMPLETED_ERROR', - error: 'DATECOMPLETED is incorrectly formatted', - source: this._currentLine.DATECOMPLETED, - column: 'DATECOMPLETED', - }); - return false; - } else if (!actualDate.isValid()) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.DATE_COMPLETED_ERROR, - errType: 'DATE_COMPLETED_ERROR', - error: 'DATECOMPLETED is invalid', - source: this._currentLine.DATECOMPLETED, - column: 'DATECOMPLETED', - }); - return false; - } else if (actualDate.isAfter(moment())) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.DATE_COMPLETED_ERROR, - errType: 'DATE_COMPLETED_ERROR', - error: 'DATECOMPLETED is in the future', - source: this._currentLine.DATECOMPLETED, - column: 'DATECOMPLETED', - }); - return false; - } else { - this._dateCompleted = actualDate; - return true; - } - } else { - return true; + const dateCompleted = moment.utc(this.currentLine.DATECOMPLETED, 'DD/MM/YYYY', true); + const errMessage = errors._getValidateDateCompletedErrMessage(dateCompleted); + + if (!errMessage) { + this.dateCompleted = dateCompleted; + return; } + this._addValidationError('DATE_COMPLETED_ERROR', errMessage, this.currentLine.DATECOMPLETED, 'DATECOMPLETED'); } _validateExpiry() { - // optional - const myDateExpiry = this._currentLine.EXPIRYDATE; - const dateFormatRegex = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[012])\/\d{4}$/; - const actualDate = moment.utc(myDateExpiry, 'DD/MM/YYYY'); - const myDateCompleted = this._currentLine.DATECOMPLETED; - const actualDateCompleted = moment.utc(myDateCompleted, 'DD/MM/YYYY'); + if (!this.currentLine.EXPIRYDATE) { + this.expiry = this.currentLine.EXPIRYDATE; + return; + } - if (myDateExpiry) { - if (!dateFormatRegex.test(myDateExpiry)) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.EXPIRY_DATE_ERROR, - errType: 'EXPIRY_DATE_ERROR', - error: 'EXPIRYDATE is incorrectly formatted', - source: this._currentLine.EXPIRYDATE, - column: 'EXPIRYDATE', - }); - return false; - } else if (!actualDate.isValid()) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.EXPIRY_DATE_ERROR, - errType: 'EXPIRY_DATE_ERROR', - error: 'EXPIRYDATE is invalid', - source: this._currentLine.EXPIRYDATE, - column: 'EXPIRYDATE', - }); - return false; - } else if (actualDate.isSameOrBefore(actualDateCompleted, 'day')) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.EXPIRY_DATE_ERROR, - errType: 'EXPIRY_DATE_ERROR', - error: 'EXPIRYDATE must be after DATECOMPLETED', - source: this._currentLine.EXPIRYDATE, - column: 'EXPIRYDATE/DATECOMPLETED', - }); - return false; - } else { - this._expiry = actualDate; - return true; - } - } else { - return true; + const expiredDate = moment.utc(this.currentLine.EXPIRYDATE, 'DD/MM/YYYY', true); + const validationErrorDetails = errors._getValidateExpiryErrDetails(expiredDate, this.dateCompleted); + + if (!validationErrorDetails) { + this.expiry = expiredDate; + return; } + + this._addValidationError( + 'EXPIRY_DATE_ERROR', + validationErrorDetails.errMessage, + this.currentLine.EXPIRYDATE, + validationErrorDetails.errColumnName, + ); } _validateDescription() { - const myDescription = this._currentLine.DESCRIPTION; + const description = this.currentLine.DESCRIPTION; const MAX_LENGTH = 120; + const errMessage = errors._getValidateDescriptionErrMessage(description, MAX_LENGTH); - if (!myDescription || myDescription.length === 0) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.DESCRIPTION_ERROR, - errType: 'DESCRIPTION_ERROR', - error: 'DESCRIPTION has not been supplied', - source: this._currentLine.DESCRIPTION, - column: 'DESCRIPTION', - }); - return false; - } else if (myDescription.length > MAX_LENGTH) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.DESCRIPTION_ERROR, - errType: 'DESCRIPTION_ERROR', - error: `DESCRIPTION is longer than ${MAX_LENGTH} characters`, - source: this._currentLine.DESCRIPTION, - column: 'DESCRIPTION', - }); - return false; - } else { - this._description = myDescription; - return true; + if (!errMessage) { + this.description = description; + return; } + this._addValidationError('DESCRIPTION_ERROR', errMessage, this.currentLine.DESCRIPTION, 'DESCRIPTION'); } _validateCategory() { - const myCategory = parseInt(this._currentLine.CATEGORY, 10); - - if (Number.isNaN(myCategory) || this.BUDI.trainingCategory(this.BUDI.TO_ASC, myCategory) === null) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.CATEGORY_ERROR, - errType: 'CATEGORY_ERROR', - error: 'CATEGORY has not been supplied', - source: this._currentLine.CATEGORY, - column: 'CATEGORY', - }); - return false; - } else { - this._category = myCategory; - return true; + const category = parseInt(this.currentLine.CATEGORY, 10); + + if (Number.isNaN(category) || this.BUDI.trainingCategory(this.BUDI.TO_ASC, category) === null) { + this._addValidationError( + 'CATEGORY_ERROR', + 'CATEGORY has not been supplied', + this.currentLine.CATEGORY, + 'CATEGORY', + ); + return; } + this.category = this.BUDI.trainingCategory(this.BUDI.TO_ASC, category); } _validateAccredited() { - if (this._currentLine.ACCREDITED) { - const myAccredited = parseInt(this._currentLine.ACCREDITED, 10); + if (this.currentLine.ACCREDITED) { + const accredited = parseInt(this.currentLine.ACCREDITED, 10); const ALLOWED_VALUES = [0, 1, 999]; - if (Number.isNaN(myAccredited) || !ALLOWED_VALUES.includes(myAccredited)) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.ACCREDITED_ERROR, - errType: 'ACCREDITED_ERROR', - error: 'ACCREDITED is invalid', - source: this._currentLine.ACCREDITED, - column: 'ACCREDITED', - }); - return false; - } else { - switch (myAccredited) { - case 0: - this._accredited = 'No'; - break; - case 1: - this._accredited = 'Yes'; - break; - case 999: - this._accredited = "Don't know"; - break; - } - return true; - } - } else { - return true; - } - } - _transformTrainingCategory() { - if (this._category) { - const mappedCategory = this.BUDI.trainingCategory(this.BUDI.TO_ASC, this._category); - if (mappedCategory === null) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.CATEGORY_ERROR, - errType: 'CATEGORY_ERROR', - error: 'CATEGORY has not been supplied', - source: this._currentLine.CATEGORY, - column: 'CATEGORY', - }); - } else { - this._category = mappedCategory; + if (Number.isNaN(accredited) || !ALLOWED_VALUES.includes(accredited)) { + this._addValidationError( + 'ACCREDITED_ERROR', + 'ACCREDITED is invalid', + this.currentLine.ACCREDITED, + 'ACCREDITED', + ); + return; } - } - } - - _validateNotes() { - const myNotes = this._currentLine.NOTES; - const MAX_LENGTH = 1000; - if (myNotes && myNotes.length > 0) { - if (myNotes.length > MAX_LENGTH) { - this._validationErrors.push({ - worker: this._currentLine.UNIQUEWORKERID, - name: this._currentLine.LOCALESTID, - lineNumber: this._lineNumber, - errCode: TrainingCsvValidator.NOTES_ERROR, - errType: 'NOTES_ERROR', - error: `NOTES is longer than ${MAX_LENGTH} characters`, - source: this._currentLine.NOTES, - column: 'NOTES', - }); - return false; - } else { - this._notes = myNotes; - return true; - } + this.accredited = this._convertAccreditedValue(accredited); } } - // returns true on success, false is any attribute of TrainingCsvValidator fails - validate() { - let status = true; - status = !this._validateLocaleStId() ? false : status; - status = !this._validateUniqueWorkerId() ? false : status; - status = !this._validateDateCompleted() ? false : status; - status = !this._validateExpiry() ? false : status; - status = !this._validateDescription() ? false : status; - status = !this._validateCategory() ? false : status; - status = !this._validateAccredited() ? false : status; - status = !this._validateNotes() ? false : status; - - return status; - } - - transform() { - let status = true; - - status = !this._transformTrainingCategory() ? false : status; + _convertAccreditedValue(key) { + const accreditedValues = { + 0: 'No', + 1: 'Yes', + 999: "Don't know", + }; - return status; + return accreditedValues[key] || ''; } - toJSON() { - return { - localId: this._localeStId, - uniqueWorkerId: this._uniqueWorkerId, - completed: this._dateCompleted ? this._dateCompleted.format('DD/MM/YYYY') : undefined, - expiry: this._expiry ? this._expiry.format('DD/MM/YYYY') : undefined, - description: this._description, - category: this._category, - accredited: this._accredited, - notes: this._notes, - lineNumber: this._lineNumber, - }; - } + _validateNotes() { + const notes = this.currentLine.NOTES; + const MAX_LENGTH = 1000; - toAPI() { - const changeProperties = { - trainingCategory: { - id: this._category, - }, - completed: this._dateCompleted ? this._dateCompleted.format('YYYY-MM-DD') : undefined, - expires: this._expiry ? this._expiry.format('YYYY-MM-DD') : undefined, - title: this._description ? this._description : undefined, - notes: this._notes ? this._notes : undefined, - accredited: this._accredited ? this._accredited : undefined, - }; + if (notes) { + if (notes.length > MAX_LENGTH) { + this._addValidationError( + 'NOTES_ERROR', + `NOTES is longer than ${MAX_LENGTH} characters`, + this.currentLine.NOTES, + 'NOTES', + ); + return; + } - return changeProperties; + this.notes = notes; + } } - get validationErrors() { - // include the "origin" of validation error - return this._validationErrors.map((thisValidation) => { - return { - origin: 'Training', - ...thisValidation, - }; + _addValidationError(errorType, errorMessage, errorSource, columnName) { + this.validationErrors.push({ + origin: 'Training', + worker: this.currentLine.UNIQUEWORKERID, + name: this.currentLine.LOCALESTID, + lineNumber: this.lineNumber, + errCode: TrainingCsvValidator[errorType], + errType: errorType, + error: errorMessage, + source: errorSource, + column: columnName, }); } } diff --git a/lambdas/bulkUpload/test/unit/classes/trainingCSVValidator.spec.js b/lambdas/bulkUpload/test/unit/classes/trainingCSVValidator.spec.js index eaba3e6db0..65ab292fd0 100644 --- a/lambdas/bulkUpload/test/unit/classes/trainingCSVValidator.spec.js +++ b/lambdas/bulkUpload/test/unit/classes/trainingCSVValidator.spec.js @@ -1,5 +1,6 @@ const expect = require('chai').expect; const sinon = require('sinon'); +const moment = require('moment'); const dbmodels = require('../../../../../server/models'); sinon.stub(dbmodels.status, 'ready').value(false); @@ -7,98 +8,616 @@ const TrainingCsvValidator = require('../../../classes/trainingCSVValidator').Tr const mappings = require('../../../../../reference/BUDIMappings').mappings; describe('trainingCSVValidator', () => { - describe('validations', () => { - it('should pass validation if no ACCREDITED is provided', async () => { - const validator = new TrainingCsvValidator( - { - LOCALESTID: 'foo', - UNIQUEWORKERID: 'bar', - CATEGORY: 1, - DESCRIPTION: 'training', - DATECOMPLETED: '', - EXPIRYDATE: '', - ACCREDITED: '', - NOTES: '', - }, - 2, - mappings, - ); - - // Regular validation has to run first for the establishment to populate the internal properties correctly - await validator.validate(); - - // call the method - await validator.transform(); - - // assert a error was returned - expect(validator._validationErrors).to.deep.equal([]); - expect(validator._validationErrors.length).to.equal(0); + describe('Validation', () => { + let trainingCsv; + + beforeEach(() => { + trainingCsv = { + LOCALESTID: 'foo', + UNIQUEWORKERID: 'bar', + CATEGORY: 1, + DESCRIPTION: 'training', + DATECOMPLETED: '01/01/2022', + EXPIRYDATE: '15/04/2022', + ACCREDITED: '', + NOTES: '', + }; + }); + + describe('_validateAccredited()', () => { + it('should pass validation if no ACCREDITED is provided', async () => { + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateAccredited(); + + expect(validator.validationErrors).to.deep.equal([]); + }); + + it('should pass validation and set accredited to Yes if ACCREDITED is 1', async () => { + trainingCsv.ACCREDITED = '1'; + + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateAccredited(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.accredited).to.equal('Yes'); + }); + + it('should pass validation and set accredited to No if ACCREDITED is 0', async () => { + trainingCsv.ACCREDITED = '0'; + + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateAccredited(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.accredited).to.equal('No'); + }); + + it("should pass validation and set ACCREDITED to Don't know if ACCREDITED is 999", async () => { + trainingCsv.ACCREDITED = '999'; + + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateAccredited(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.accredited).to.equal("Don't know"); + }); + + it('should add ACCREDITED_ERROR to validationErrors if invalid ACCREDITED is provided', async () => { + trainingCsv.ACCREDITED = '3'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateAccredited(); + + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1060, + errType: 'ACCREDITED_ERROR', + error: 'ACCREDITED is invalid', + lineNumber: 1, + name: 'foo', + source: '3', + column: 'ACCREDITED', + worker: 'bar', + }, + ]); + }); + }); + + describe('_validateCategory()', () => { + it('should add CATEGORY_ERROR to validationErrors if string containing letters is provided for Category', async () => { + trainingCsv.CATEGORY = 'bob'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateCategory(); + + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1050, + errType: 'CATEGORY_ERROR', + error: 'CATEGORY has not been supplied', + lineNumber: 1, + name: 'foo', + source: 'bob', + column: 'CATEGORY', + worker: 'bar', + }, + ]); + }); + + it('should add CATEGORY_ERROR to validationErrors if the Category provided is not a valid category number', async () => { + trainingCsv.CATEGORY = 41; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateCategory(); + + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1050, + errType: 'CATEGORY_ERROR', + error: 'CATEGORY has not been supplied', + lineNumber: 1, + name: 'foo', + source: 41, + column: 'CATEGORY', + worker: 'bar', + }, + ]); + }); + + it('should pass validation and set BUDI CATEGORY to ASC CATEGORY if the BUDI CATEGORY is a valid category', async () => { + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateCategory(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.category).to.equal(8); + }); + }); + + describe('_validateNotes()', () => { + it('should add NOTES_ERROR to validationErrors and leave notes as null if NOTES is longer than 1000 characters', async () => { + trainingCsv.NOTES = + 'LLorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. N'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateNotes(); + + expect(validator.notes).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1070, + errType: 'NOTES_ERROR', + error: 'NOTES is longer than 1000 characters', + source: trainingCsv.NOTES, + column: 'NOTES', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); + + it('should not add NOTES_ERROR to validationErrors and set notes to csv NOTES if NOTES is shorter than 1000 characters', async () => { + trainingCsv.NOTES = 'valid short note'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateNotes(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.notes).to.equal('valid short note'); + }); + + it('should leave notes as null and not add error if NOTES empty string', async () => { + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateNotes(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.notes).to.equal(null); + }); + }); + + describe('_getValidateLocaleStIdErrorStatus()', () => { + it('should add LOCALESTID_ERROR to validationErrors and set localStId as null if localeStId length === 0', async () => { + trainingCsv.LOCALESTID = ''; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateLocaleStId(); + + expect(validator.localeStId).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1000, + errType: 'LOCALESTID_ERROR', + error: 'LOCALESTID has not been supplied', + source: trainingCsv.LOCALESTID, + column: 'LOCALESTID', + lineNumber: 1, + name: '', + worker: 'bar', + }, + ]); + }); + + it("should add LOCALESTID_ERROR to validationErrors and leave localStId as null if localeStId doesn't exist", async () => { + trainingCsv.LOCALESTID = null; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateLocaleStId(); + + expect(validator.localeStId).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1000, + errType: 'LOCALESTID_ERROR', + error: 'LOCALESTID has not been supplied', + source: trainingCsv.LOCALESTID, + column: 'LOCALESTID', + lineNumber: 1, + name: null, + worker: 'bar', + }, + ]); + }); + + it("should add LOCALESTID_ERROR to validationErrors and leave localStId as null if localeStId's length is greater than MAX_LENGTH", async () => { + trainingCsv.LOCALESTID = 'Lorem ipsum dolor sit amet, consectetuer adipiscing'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateLocaleStId(); + + expect(validator.localeStId).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1000, + errType: 'LOCALESTID_ERROR', + error: 'LOCALESTID is longer than 50 characters', + source: trainingCsv.LOCALESTID, + column: 'LOCALESTID', + lineNumber: 1, + name: 'Lorem ipsum dolor sit amet, consectetuer adipiscing', + worker: 'bar', + }, + ]); + }); + }); + + describe('_validateLocaleStId()', async () => { + it('should pass validation and set uniqueWorkerId if a valid LOCALESTID is provided', async () => { + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateLocaleStId(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.localeStId).to.equal('foo'); + }); + }); + + describe('_validateUniqueWorkerId()', async () => { + it('should pass validation and set uniqueWorkerId if a valid UNIQUEWORKERID is provided', async () => { + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateUniqueWorkerId(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.uniqueWorkerId).to.equal('bar'); + }); + }); + + describe('_getValidateUniqueWorkerIdErrMessage()', () => { + it('should add UNIQUE_WORKER_ID_ERROR to validationErrors and set uniqueWorkerId as null if localeStId length === 0', async () => { + trainingCsv.UNIQUEWORKERID = ''; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateUniqueWorkerId(); + + expect(validator.uniqueWorkerId).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1010, + errType: 'UNIQUE_WORKER_ID_ERROR', + error: 'UNIQUEWORKERID has not been supplied', + source: trainingCsv.UNIQUEWORKERID, + column: 'UNIQUEWORKERID', + lineNumber: 1, + name: 'foo', + worker: '', + }, + ]); + }); + + it("should add UNIQUE_WORKER_ID_ERROR to validationErrors and leave uniqueWorkerId as null if localeStId doesn't exist", async () => { + trainingCsv.UNIQUEWORKERID = null; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateUniqueWorkerId(); + + expect(validator.uniqueWorkerId).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1010, + errType: 'UNIQUE_WORKER_ID_ERROR', + error: 'UNIQUEWORKERID has not been supplied', + source: trainingCsv.UNIQUEWORKERID, + column: 'UNIQUEWORKERID', + lineNumber: 1, + name: 'foo', + worker: null, + }, + ]); + }); + + it("should add UNIQUE_WORKER_ID_ERROR to validationErrors and leave uniqueWorkerId as null if localeStId's length is greater than MAX_LENGTH", async () => { + trainingCsv.UNIQUEWORKERID = 'Lorem ipsum dolor sit amet, consectetuer adipiscing'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateUniqueWorkerId(); + + expect(validator.uniqueWorkerId).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1010, + errType: 'UNIQUE_WORKER_ID_ERROR', + error: 'UNIQUEWORKERID is longer than 50 characters', + source: trainingCsv.UNIQUEWORKERID, + column: 'UNIQUEWORKERID', + lineNumber: 1, + name: 'foo', + worker: 'Lorem ipsum dolor sit amet, consectetuer adipiscing', + }, + ]); + }); + }); + + describe('_validateDateCompleted()', async () => { + it('should pass validation and set dateCompleted to DATECOMPLETED if a valid DATECOMPLETED is provided', async () => { + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateDateCompleted(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.dateCompleted).to.deep.equal(moment.utc('01/01/2022', 'DD/MM/YYYY', true)); + }); + + it('should pass validation and set dateCompleted to an empty string if the DATECOMPLETED is a empty string', async () => { + trainingCsv.DATECOMPLETED = ''; + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateDateCompleted(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.dateCompleted).to.equal(''); + }); + + it('should pass validation and set dateCompleted to null if the DATECOMPLETED is null', async () => { + trainingCsv.DATECOMPLETED = null; + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateDateCompleted(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.dateCompleted).to.equal(null); + }); }); - it('should pass validation if ACCREDITED is provided', async () => { - const validator = new TrainingCsvValidator( - { - LOCALESTID: 'foo', - UNIQUEWORKERID: 'bar', - CATEGORY: 1, - DESCRIPTION: 'training', - DATECOMPLETED: '', - EXPIRYDATE: '', - ACCREDITED: '1', - NOTES: '', - }, - 2, - mappings, - ); - - // Regular validation has to run first for the establishment to populate the internal properties correctly - await validator.validate(); - - // call the method - await validator.transform(); - - // assert a error was returned - expect(validator._validationErrors).to.deep.equal([]); - expect(validator._validationErrors.length).to.equal(0); - expect(validator.accredited).to.equal('Yes'); + describe('_getValidateDateCompletedErrMessage()', async () => { + it('should add DATE_COMPLETED_ERROR to validationErrors and set dateCompleted as null if DATECOMPLETED is incorrectly formatted', async () => { + trainingCsv.DATECOMPLETED = '12323423423'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateDateCompleted(); + + expect(validator.dateCompleted).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1020, + errType: 'DATE_COMPLETED_ERROR', + error: 'DATECOMPLETED is incorrectly formatted', + source: trainingCsv.DATECOMPLETED, + column: 'DATECOMPLETED', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); + + it('should add DATE_COMPLETED_ERROR to validationErrors and set dateCompleted as null if DATECOMPLETED is a date set in the future', async () => { + trainingCsv.DATECOMPLETED = '01/01/2099'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateDateCompleted(); + + expect(validator.dateCompleted).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1020, + errType: 'DATE_COMPLETED_ERROR', + error: 'DATECOMPLETED is in the future', + source: trainingCsv.DATECOMPLETED, + column: 'DATECOMPLETED', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); }); - it('should fail validation if invalid ACCREDITED is provided', async () => { - const validator = new TrainingCsvValidator( - { - LOCALESTID: 'foo', - UNIQUEWORKERID: 'bar', - CATEGORY: 1, - DESCRIPTION: 'training', - DATECOMPLETED: '', - EXPIRYDATE: '', - ACCREDITED: '3', - NOTES: '', - }, - 1, - mappings, - ); - - // Regular validation has to run first for the establishment to populate the internal properties correctly - await validator.validate(); - - // call the method - await validator.transform(); - - // assert a error was returned - expect(validator._validationErrors).to.deep.equal([ - { - errCode: 1060, - errType: 'ACCREDITED_ERROR', - error: 'ACCREDITED is invalid', - lineNumber: 1, - name: 'foo', - source: '3', - column: 'ACCREDITED', - worker: 'bar', - }, - ]); - expect(validator._validationErrors.length).to.equal(1); + describe('_validateExpiry()', async () => { + it('should pass validation and set expiry to EXPIRYDATE if a valid EXPIRYDATE is provided', async () => { + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateExpiry(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.expiry).to.deep.equal(moment.utc('15/04/2022', 'DD/MM/YYYY', true)); + }); + + it('should pass validation and set expiry to an empty string if EXPIRYDATE is an empty string', async () => { + trainingCsv.EXPIRYDATE = ''; + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateExpiry(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.expiry).to.deep.equal(''); + }); + + it('should pass validation and set expiry to null if EXPIRYDATE is null', async () => { + trainingCsv.EXPIRYDATE = null; + const validator = new TrainingCsvValidator(trainingCsv, 2, mappings); + + await validator._validateExpiry(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.expiry).to.deep.equal(null); + }); + }); + + describe('_getValidateExpiryErrMessage()', async () => { + it('should add EXPIRY_DATE_ERROR to validationErrors and set expiry as null if EXPIRYDATE is incorrectly formatted', async () => { + trainingCsv.EXPIRYDATE = '12323423423'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateExpiry(); + + expect(validator.expiry).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1030, + errType: 'EXPIRY_DATE_ERROR', + error: 'EXPIRYDATE is incorrectly formatted', + source: trainingCsv.EXPIRYDATE, + column: 'EXPIRYDATE', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); + + it('should add EXPIRY_DATE_ERROR to validationErrors and set expiry as null if EXPIRYDATE is a date set before DATECOMPLETED ', async () => { + trainingCsv.EXPIRYDATE = '01/01/2000'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateDateCompleted(); + await validator._validateExpiry(); + + expect(validator.expiry).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1030, + errType: 'EXPIRY_DATE_ERROR', + error: 'EXPIRYDATE must be after DATECOMPLETED', + source: trainingCsv.EXPIRYDATE, + column: 'EXPIRYDATE/DATECOMPLETED', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); + + it('should add EXPIRY_DATE_ERROR to validationErrors and set expiry as null if EXPIRYDATE is the same date as DATECOMPLETED ', async () => { + trainingCsv.EXPIRYDATE = '01/01/2022'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateDateCompleted(); + await validator._validateExpiry(); + + expect(validator.expiry).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1030, + errType: 'EXPIRY_DATE_ERROR', + error: 'EXPIRYDATE must be after DATECOMPLETED', + source: trainingCsv.EXPIRYDATE, + column: 'EXPIRYDATE/DATECOMPLETED', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); + }); + + describe('_validateDescription()', async () => { + it('should pass validation and set description to DESCRIPTION if a valid DESCRIPTION is provided', async () => { + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateDescription(); + + expect(validator.validationErrors).to.deep.equal([]); + expect(validator.description).to.equal('training'); + }); + }); + + describe('_getValidateDescriptionErrMessage()', async () => { + it('should add DESCRIPTION_ERROR to validationErrors and set description as null if DESCRIPTION is an empty string', async () => { + trainingCsv.DESCRIPTION = ''; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateDescription(); + + expect(validator.description).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1040, + errType: 'DESCRIPTION_ERROR', + error: 'DESCRIPTION has not been supplied', + source: trainingCsv.DESCRIPTION, + column: 'DESCRIPTION', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); + + it('should add DESCRIPTION_ERROR to validationErrors and set description as null if DESCRIPTION is null', async () => { + trainingCsv.DESCRIPTION = null; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateDescription(); + + expect(validator.description).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1040, + errType: 'DESCRIPTION_ERROR', + error: 'DESCRIPTION has not been supplied', + source: trainingCsv.DESCRIPTION, + column: 'DESCRIPTION', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); + + it('should add DESCRIPTION_ERROR to validationErrors and set description as null if DESCRIPTION is longer than MAX_LENGTH', async () => { + trainingCsv.DESCRIPTION = + 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis nato'; + + const validator = new TrainingCsvValidator(trainingCsv, 1, mappings); + + await validator._validateDescription(); + + expect(validator.description).to.equal(null); + expect(validator.validationErrors).to.deep.equal([ + { + origin: 'Training', + errCode: 1040, + errType: 'DESCRIPTION_ERROR', + error: 'DESCRIPTION is longer than 120 characters', + source: trainingCsv.DESCRIPTION, + column: 'DESCRIPTION', + lineNumber: 1, + name: 'foo', + worker: 'bar', + }, + ]); + }); }); }); }); diff --git a/lambdas/bulkUpload/validateTraining/errors.js b/lambdas/bulkUpload/validateTraining/errors.js new file mode 100644 index 0000000000..91629d7bcf --- /dev/null +++ b/lambdas/bulkUpload/validateTraining/errors.js @@ -0,0 +1,41 @@ +const moment = require('moment'); + +exports._getValidateLocaleStIdErrMessage = (localeStId, MAX_LENGTH) => { + if (!localeStId) { + return 'LOCALESTID has not been supplied'; + } else if (localeStId.length > MAX_LENGTH) { + return `LOCALESTID is longer than ${MAX_LENGTH} characters`; + } +}; + +exports._getValidateUniqueWorkerIdErrMessage = (uniqueId, MAX_LENGTH) => { + if (!uniqueId) { + return 'UNIQUEWORKERID has not been supplied'; + } else if (uniqueId.length > MAX_LENGTH) { + return `UNIQUEWORKERID is longer than ${MAX_LENGTH} characters`; + } +}; + +exports._getValidateDateCompletedErrMessage = (dateCompleted) => { + if (!dateCompleted.isValid()) { + return 'DATECOMPLETED is incorrectly formatted'; + } else if (dateCompleted.isAfter(moment())) { + return 'DATECOMPLETED is in the future'; + } +}; + +exports._getValidateExpiryErrDetails = (expiredDate, dateCompleted) => { + if (!expiredDate.isValid()) { + return { errMessage: 'EXPIRYDATE is incorrectly formatted', errColumnName: 'EXPIRYDATE' }; + } else if (expiredDate.isSameOrBefore(dateCompleted, 'day')) { + return { errMessage: 'EXPIRYDATE must be after DATECOMPLETED', errColumnName: 'EXPIRYDATE/DATECOMPLETED' }; + } +}; + +exports._getValidateDescriptionErrMessage = (description, MAX_LENGTH) => { + if (!description) { + return 'DESCRIPTION has not been supplied'; + } else if (description.length > MAX_LENGTH) { + return `DESCRIPTION is longer than ${MAX_LENGTH} characters`; + } +}; diff --git a/lambdas/bulkUpload/validateTraining/index.js b/lambdas/bulkUpload/validateTraining/index.js index a976dbb542..f458708d12 100644 --- a/lambdas/bulkUpload/validateTraining/index.js +++ b/lambdas/bulkUpload/validateTraining/index.js @@ -12,7 +12,6 @@ const runValidator = async (thisLine, currentLineNumber, mappings) => { const lineValidator = new TrainingCsvValidator(thisLine, currentLineNumber, mappings); lineValidator.validate(); - lineValidator.transform(); const APITrainingRecord = lineValidator.toAPI(); const JSONTrainingRecord = lineValidator.toJSON(); diff --git a/server/reports/targeted-emails/index.js b/server/reports/targeted-emails/index.js new file mode 100644 index 0000000000..7c21132da5 --- /dev/null +++ b/server/reports/targeted-emails/index.js @@ -0,0 +1,38 @@ +const excelUtils = require('../../utils/excelUtils'); +const { generateWorkplacesToEmailTab } = require('./workplacesToEmail'); +const { generateWorkplacesWithoutEmailTab } = require('./workplacesWithoutEmail'); + +const generateTargetedEmailsReport = async (workbook, users, establishmentNmdsIdList) => { + const workplacesToEmail = formatWorkplacesToEmail(users); + const workplacesWithoutEmail = getWorkplacesWithoutEmail(workplacesToEmail, establishmentNmdsIdList); + + generateWorkplacesToEmailTab(workbook, workplacesToEmail); + generateWorkplacesWithoutEmailTab(workbook, workplacesWithoutEmail); + + workbook.eachSheet((sheet) => { + excelUtils.fitColumnsToSize(sheet); + }); + + return workbook; +}; + +const formatWorkplacesToEmail = (users) => { + return users.map(formatWorkplace); +}; + +const formatWorkplace = (user) => { + return { + nmdsId: user.establishment.nmdsId, + emailAddress: user.get('email'), + }; +}; + +const getWorkplacesWithoutEmail = (workplacesToEmail, establishmentNmdsIdList) => { + return establishmentNmdsIdList.filter((id) => !workplacesToEmail.find((workplace) => workplace.nmdsId === id)); +}; + +module.exports = { + generateTargetedEmailsReport, + formatWorkplacesToEmail, + getWorkplacesWithoutEmail, +}; diff --git a/server/reports/targeted-emails/workplacesToEmail.js b/server/reports/targeted-emails/workplacesToEmail.js new file mode 100644 index 0000000000..2d9e5ae691 --- /dev/null +++ b/server/reports/targeted-emails/workplacesToEmail.js @@ -0,0 +1,27 @@ +const { makeRowBold } = require('../../utils/excelUtils'); + +const generateWorkplacesToEmailTab = (workbook, workplacesToEmail) => { + const workplacesToEmailTab = workbook.addWorksheet('Found Workplaces'); + + addContentToWorkplacesToEmailTab(workplacesToEmailTab, workplacesToEmail); +}; + +const addContentToWorkplacesToEmailTab = (workplacesToEmailTab, workplacesToEmail) => { + addHeaders(workplacesToEmailTab); + + workplacesToEmailTab.addRows(workplacesToEmail); +}; + +const addHeaders = (workplacesToEmailTab) => { + workplacesToEmailTab.columns = [ + { header: 'NMDS ID', key: 'nmdsId' }, + { header: 'Email Address', key: 'emailAddress' }, + ]; + + makeRowBold(workplacesToEmailTab, 1); +}; + +module.exports = { + generateWorkplacesToEmailTab, + addContentToWorkplacesToEmailTab, +}; diff --git a/server/reports/targeted-emails/workplacesWithoutEmail.js b/server/reports/targeted-emails/workplacesWithoutEmail.js new file mode 100644 index 0000000000..c94110a7fd --- /dev/null +++ b/server/reports/targeted-emails/workplacesWithoutEmail.js @@ -0,0 +1,26 @@ +const { makeRowBold } = require('../../utils/excelUtils'); + +const generateWorkplacesWithoutEmailTab = (workbook, workplacesWithoutEmail) => { + const workplacesWithoutEmailTab = workbook.addWorksheet('Workplaces without Email'); + + addContentToWorkplacesWithoutEmailTab(workplacesWithoutEmailTab, workplacesWithoutEmail); +}; + +const addContentToWorkplacesWithoutEmailTab = (workplacesWithoutEmailTab, workplacesWithoutEmail) => { + addHeaders(workplacesWithoutEmailTab); + + for (const id of workplacesWithoutEmail) { + workplacesWithoutEmailTab.addRow([id]); + } +}; + +const addHeaders = (workplacesWithoutEmailTab) => { + workplacesWithoutEmailTab.columns = [{ header: 'NMDS ID', key: 'nmdsId' }]; + + makeRowBold(workplacesWithoutEmailTab, 1); +}; + +module.exports = { + generateWorkplacesWithoutEmailTab, + addContentToWorkplacesWithoutEmailTab, +}; diff --git a/server/routes/admin/email-campaigns/targeted-emails/index.js b/server/routes/admin/email-campaigns/targeted-emails/index.js index 043c15d175..43e969cd66 100644 --- a/server/routes/admin/email-campaigns/targeted-emails/index.js +++ b/server/routes/admin/email-campaigns/targeted-emails/index.js @@ -9,6 +9,9 @@ const sendEmail = require('../../../../services/email-campaigns/targeted-emails/ const models = require('../../../../models/'); const { getTargetedEmailTemplates } = require('./templates'); const { sanitizeFilePath } = require('../../../../utils/security/sanitizeFilePath'); +const moment = require('moment'); +const excelJS = require('exceljs'); +const targetedEmailsReport = require('../../../../reports/targeted-emails'); const router = express.Router(); @@ -102,6 +105,33 @@ const createEmailCampaign = async (userID) => { }); }; +const createTargetedEmailsReport = async (req, res) => { + try { + const establishmentNmdsIdList = parseNmdsIdsIfFileExists(req.file); + const users = await getGroupOfUsers('multipleAccounts', establishmentNmdsIdList); + + const workbook = new excelJS.Workbook(); + + workbook.creator = 'Skills-For-Care'; + workbook.properties.date1904 = true; + + await targetedEmailsReport.generateTargetedEmailsReport(workbook, users, establishmentNmdsIdList); + + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader( + 'Content-Disposition', + 'attachment; filename=' + moment().format('DD-MM-YYYY') + '-targetedEmails.xlsx', + ); + + await workbook.xlsx.write(res); + + return res.status(200).end(); + } catch (error) { + console.error(error); + return res.status(500).send(); + } +}; + const parseNmdsIdsIfFileExists = (file) => { if (!file) return null; @@ -158,7 +188,10 @@ router.route('/').post( createTargetedEmailsCampaign, ); +router.route('/report').post(upload.single('targetedRecipientsFile'), createTargetedEmailsReport); + module.exports = router; module.exports.getTargetedTotalEmails = getTargetedTotalEmails; module.exports.createTargetedEmailsCampaign = createTargetedEmailsCampaign; module.exports.getGroupOfUsers = getGroupOfUsers; +module.exports.createTargetedEmailsReport = createTargetedEmailsReport; diff --git a/server/routes/admin/registrations/getAllRegistrations.js b/server/routes/admin/registrations/getAllRegistrations.js deleted file mode 100644 index 0db0456c1e..0000000000 --- a/server/routes/admin/registrations/getAllRegistrations.js +++ /dev/null @@ -1,210 +0,0 @@ -const moment = require('moment-timezone'); -const { Op } = require('sequelize'); - -const models = require('../../../models'); -const config = require('../../../config/config'); - -const getAllRegistrations = async (req, res) => { - try { - // Get the login, user and establishment records - const loginResults = await models.login.findAll({ - attributes: ['id', 'username'], - where: { - isActive: false, - status: { [Op.or]: ['PENDING', 'IN PROGRESS'] }, - }, - order: [['id', 'DESC']], - include: [ - { - model: models.user, - attributes: [ - 'EmailValue', - 'PhoneValue', - 'FullNameValue', - 'SecurityQuestionValue', - 'SecurityQuestionAnswerValue', - 'created', - ], - include: [ - { - model: models.establishment, - attributes: [ - 'NameValue', - 'IsRegulated', - 'LocationID', - 'ProvID', - 'Address1', - 'Address2', - 'Address3', - 'Town', - 'County', - 'PostCode', - 'NmdsID', - 'EstablishmentID', - 'Status', - 'EstablishmentUID', - ], - include: [ - { - model: models.services, - as: 'mainService', - attributes: ['id', 'name'], - }, - ], - }, - ], - }, - ], - }); - // Get the pending workplace records - const workplaceResults = await models.establishment.findAll({ - attributes: [ - 'NameValue', - 'IsRegulated', - 'LocationID', - 'ProvID', - 'Address1', - 'Address2', - 'Address3', - 'Town', - 'County', - 'PostCode', - 'NmdsID', - 'EstablishmentID', - 'ParentID', - 'ParentUID', - 'created', - 'updatedBy', - 'Status', - 'EstablishmentUID', - ], - where: { - ustatus: { [Op.or]: ['PENDING', 'IN PROGRESS'] }, - }, - order: [['id', 'DESC']], - include: [ - { - model: models.services, - as: 'mainService', - attributes: ['id', 'name'], - }, - ], - }); - - let arrToReturn, loginReturnArr, workplaceReturnArr; - if (loginResults) { - // Reply with mapped results - loginReturnArr = loginResults.map((registration) => { - registration = registration.toJSON(); - return { - name: registration.user.FullNameValue, - username: registration.username, - securityQuestion: registration.user.SecurityQuestionValue, - securityQuestionAnswer: registration.user.SecurityQuestionAnswerValue, - email: registration.user.EmailValue, - phone: registration.user.PhoneValue, - created: registration.user.created, - establishment: { - id: registration.user.establishment.EstablishmentID, - name: registration.user.establishment.NameValue, - isRegulated: registration.user.establishment.IsRegulated, - nmdsId: registration.user.establishment.NmdsID, - address: registration.user.establishment.Address1, - address2: registration.user.establishment.Address2, - address3: registration.user.establishment.Address3, - postcode: registration.user.establishment.PostCode, - town: registration.user.establishment.Town, - county: registration.user.establishment.County, - locationId: registration.user.establishment.LocationID, - provid: registration.user.establishment.ProvID, - mainService: registration.user.establishment.mainService.name, - status: registration.user.establishment.Status, - uid: registration.user.establishment.EstablishmentUID, - }, - }; - }); - } - if (workplaceResults) { - workplaceReturnArr = workplaceResults.map((registration) => { - registration = registration.toJSON(); - return { - created: registration.created, - username: registration.updatedBy, - establishment: { - id: registration.EstablishmentID, - name: registration.NameValue, - isRegulated: registration.IsRegulated, - nmdsId: registration.NmdsID, - address: registration.Address1, - address2: registration.Address2, - address3: registration.Address3, - postcode: registration.PostCode, - town: registration.Town, - county: registration.County, - locationId: registration.LocationID, - provid: registration.ProvID, - mainService: registration.mainService.name, - parentId: registration.ParentID, - parentUid: registration.ParentUID, - status: registration.Status, - uid: registration.EstablishmentUID, - }, - }; - }); - } - - if (loginResults && workplaceResults) { - let loginWorkplaceIds = new Set(loginReturnArr.map((d) => d.establishment.id)); - arrToReturn = [ - ...loginReturnArr, - ...workplaceReturnArr.filter((d) => !loginWorkplaceIds.has(d.establishment.id)), - ]; - - arrToReturn.sort(function (a, b) { - var dateA = new Date(a.created).getTime(); - var dateB = new Date(b.created).getTime(); - return dateB > dateA ? 1 : -1; - }); - - for (let i = 0; i < arrToReturn.length; i++) { - arrToReturn[i].created = moment.utc(arrToReturn[i].created).tz(config.get('timezone')).format('D/M/YYYY h:mma'); - //get parent establishment details - if (!arrToReturn[i].email) { - let fetchQuery = { - where: { - id: arrToReturn[i].establishment.parentId, - }, - }; - let parentEstablishment = await models.establishment.findOne(fetchQuery); - if (parentEstablishment) { - arrToReturn[i].establishment.parentEstablishmentId = parentEstablishment.nmdsId; - } - } - } - - res.status(200).send(arrToReturn); - } else if (loginReturnArr && !workplaceReturnArr) { - loginReturnArr.map((registration) => { - registration.created = moment.utc(registration.created).tz(config.get('timezone')).format('D/M/YYYY h:mma'); - }); - - res.status(200).send(loginReturnArr); - } else if (!loginReturnArr && workplaceReturnArr) { - workplaceReturnArr.map((registration) => { - registration.created = moment.utc(registration.created).tz(config.get('timezone')).format('D/M/YYYY h:mma'); - }); - - res.status(200).send(workplaceReturnArr); - } else { - res.status(200); - } - } catch (error) { - res.status(500); - } -}; - -const router = require('express').Router(); - -router.route('/').get(getAllRegistrations); - -module.exports = router; diff --git a/server/routes/admin/registrations/index.js b/server/routes/admin/registrations/index.js index 166580722a..85b8ed224a 100644 --- a/server/routes/admin/registrations/index.js +++ b/server/routes/admin/registrations/index.js @@ -2,7 +2,6 @@ const express = require('express'); const router = express.Router(); -router.use('/', require('./getAllRegistrations')); router.use('/', require('./getRegistrations')); router.use('/status', require('./getSingleRegistration')); router.use('/updateWorkplaceId', require('./updateWorkplaceId')); diff --git a/server/test/unit/reports/targeted-emails/index.spec.js b/server/test/unit/reports/targeted-emails/index.spec.js new file mode 100644 index 0000000000..e7e2cfa7f5 --- /dev/null +++ b/server/test/unit/reports/targeted-emails/index.spec.js @@ -0,0 +1,87 @@ +const expect = require('chai').expect; + +const { formatWorkplacesToEmail, getWorkplacesWithoutEmail } = require('../../../../reports/targeted-emails'); + +describe('reports/targetedEmails/index', () => { + const mockUsers = [ + { + get() { + return 'mock@email.com'; + }, + establishment: { + nmdsId: 'A123456', + }, + }, + { + get() { + return 'mock2@email.com'; + }, + establishment: { + nmdsId: 'A123459', + }, + }, + ]; + + const mockWorkplacesToEmail = [ + { + nmdsId: 'A123456', + emailAddress: 'mock@email.com', + }, + { + nmdsId: 'A123459', + emailAddress: 'mock2@email.com', + }, + ]; + + describe('formatWorkplacesToEmail()', () => { + it('should return array with objects containing nmdsId and email', async () => { + const data = formatWorkplacesToEmail(mockUsers); + + expect(data).to.deep.equal(mockWorkplacesToEmail); + }); + }); + + describe('getWorkplacesWithoutEmail()', () => { + const nmdsIdsList = ['A123456', 'A123459']; + + it('should return empty array when workplacesToEmail has all IDs', async () => { + const data = getWorkplacesWithoutEmail(mockWorkplacesToEmail, nmdsIdsList); + + expect(data).to.deep.equal([]); + }); + + it('should return array with all nmdsIds from nmdsIdsList when workplacesToEmail is empty', async () => { + const workplacesToEmail = []; + + const data = getWorkplacesWithoutEmail(workplacesToEmail, nmdsIdsList); + + expect(data).to.deep.equal(['A123456', 'A123459']); + }); + + it('should return array with first nmdsId when first ID not in workplacesToEmail', async () => { + const workplacesToEmail = [ + { + nmdsId: 'A123459', + emailAddress: 'mock2@email.com', + }, + ]; + + const data = getWorkplacesWithoutEmail(workplacesToEmail, nmdsIdsList); + + expect(data).to.deep.equal(['A123456']); + }); + + it('should return array with second nmdsId when second ID not in workplacesToEmail', async () => { + const workplacesToEmail = [ + { + nmdsId: 'A123456', + emailAddress: 'mock@email.com', + }, + ]; + + const data = getWorkplacesWithoutEmail(workplacesToEmail, nmdsIdsList); + + expect(data).to.deep.equal(['A123459']); + }); + }); +}); diff --git a/server/test/unit/reports/targeted-emails/workplacesToEmail.spec.js b/server/test/unit/reports/targeted-emails/workplacesToEmail.spec.js new file mode 100644 index 0000000000..1629ec3e15 --- /dev/null +++ b/server/test/unit/reports/targeted-emails/workplacesToEmail.spec.js @@ -0,0 +1,43 @@ +const expect = require('chai').expect; +const excelJS = require('exceljs'); + +const { addContentToWorkplacesToEmailTab } = require('../../../../reports/targeted-emails/workplacesToEmail'); + +describe('addContentToWorkplacesToEmailTab', () => { + let mockWorkplacesToEmailTab; + const mockWorkplaces = [ + { + nmdsId: 'A123456', + emailAddress: 'mock@email.com', + }, + { + nmdsId: 'A123459', + emailAddress: 'mock2@email.com', + }, + ]; + + beforeEach(() => { + mockWorkplacesToEmailTab = new excelJS.Workbook().addWorksheet('Found Workplaces'); + }); + + it('should add tab NMDS ID and Email Address headers to top row', async () => { + addContentToWorkplacesToEmailTab(mockWorkplacesToEmailTab, mockWorkplaces); + + expect(mockWorkplacesToEmailTab.getCell('A1').value).to.equal('NMDS ID'); + expect(mockWorkplacesToEmailTab.getCell('B1').value).to.equal('Email Address'); + }); + + it('should add the NMDS ID and Email Address of first workplace to second row', async () => { + addContentToWorkplacesToEmailTab(mockWorkplacesToEmailTab, mockWorkplaces); + + expect(mockWorkplacesToEmailTab.getCell('A2').value).to.equal('A123456'); + expect(mockWorkplacesToEmailTab.getCell('B2').value).to.equal('mock@email.com'); + }); + + it('should add the NMDS ID and Email Address of second workplace to third row', async () => { + addContentToWorkplacesToEmailTab(mockWorkplacesToEmailTab, mockWorkplaces); + + expect(mockWorkplacesToEmailTab.getCell('A3').value).to.equal('A123459'); + expect(mockWorkplacesToEmailTab.getCell('B3').value).to.equal('mock2@email.com'); + }); +}); diff --git a/server/test/unit/reports/targeted-emails/workplacesWithoutEmail.spec.js b/server/test/unit/reports/targeted-emails/workplacesWithoutEmail.spec.js new file mode 100644 index 0000000000..1ac0230c8f --- /dev/null +++ b/server/test/unit/reports/targeted-emails/workplacesWithoutEmail.spec.js @@ -0,0 +1,31 @@ +const expect = require('chai').expect; +const excelJS = require('exceljs'); + +const { addContentToWorkplacesWithoutEmailTab } = require('../../../../reports/targeted-emails/workplacesWithoutEmail'); + +describe('addContentToWorkplacesWithoutEmailTab', () => { + let mockWorkplacesWithoutEmailTab; + const workplacesWithoutEmail = ['A123456', 'A123459']; + + beforeEach(() => { + mockWorkplacesWithoutEmailTab = new excelJS.Workbook().addWorksheet('Found Workplaces'); + }); + + it('should add NMDS ID header to top row', async () => { + addContentToWorkplacesWithoutEmailTab(mockWorkplacesWithoutEmailTab, workplacesWithoutEmail); + + expect(mockWorkplacesWithoutEmailTab.getCell('A1').value).to.equal('NMDS ID'); + }); + + it('should add the first NMDS ID in workplacesWithoutEmail to second row', async () => { + addContentToWorkplacesWithoutEmailTab(mockWorkplacesWithoutEmailTab, workplacesWithoutEmail); + + expect(mockWorkplacesWithoutEmailTab.getCell('A2').value).to.equal('A123456'); + }); + + it('should add the second NMDS ID in workplacesWithoutEmail to third row', async () => { + addContentToWorkplacesWithoutEmailTab(mockWorkplacesWithoutEmailTab, workplacesWithoutEmail); + + expect(mockWorkplacesWithoutEmailTab.getCell('A3').value).to.equal('A123459'); + }); +}); diff --git a/server/test/unit/routes/admin/email-campaigns/targeted-emails/index.spec.js b/server/test/unit/routes/admin/email-campaigns/targeted-emails/index.spec.js index b24dbd0de9..98cbb0db25 100644 --- a/server/test/unit/routes/admin/email-campaigns/targeted-emails/index.spec.js +++ b/server/test/unit/routes/admin/email-campaigns/targeted-emails/index.spec.js @@ -7,6 +7,7 @@ const models = require('../../../../../../models'); const { build, fake, sequence } = require('@jackfranklin/test-data-bot/build'); const sendEmail = require('../../../../../../services/email-campaigns/targeted-emails/sendEmail'); const { Op } = require('sequelize'); +const excelJS = require('exceljs'); const user = build('User', { fields: { @@ -243,4 +244,45 @@ describe('server/routes/admin/email-campaigns/targeted-emails', () => { }); }); }); + + describe('createTargetedEmailsReport()', () => { + let req; + let res; + + beforeEach(() => { + sinon.stub(models.user, 'allPrimaryUsers').returns([]); + sinon.stub(fs, 'readFileSync'); + sinon.stub(fs, 'unlinkSync').returns(null); + + req = httpMocks.createRequest({ + method: 'POST', + url: '/api/admin/email-campaigns/targeted-emails/report', + role: 'Admin', + file: { filename: 'file' }, + }); + + res = httpMocks.createResponse(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should generate a report', async () => { + await targetedEmailsRoutes.createTargetedEmailsReport(req, res); + + expect(res.statusCode).to.equal(200); + expect(res._headers['content-type']).to.equal( + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + }); + + it('should return 500 status when an error is thrown', async () => { + sinon.stub(excelJS, 'Workbook').throws(); + + await targetedEmailsRoutes.createTargetedEmailsReport(req, res); + + expect(res.statusCode).to.equal(500); + }); + }); }); diff --git a/src/app/core/resolvers/admin/email-campaign-history.resolver.spec.ts b/src/app/core/resolvers/admin/email-campaign-history.resolver.spec.ts index fd532ad535..ed6c36acae 100644 --- a/src/app/core/resolvers/admin/email-campaign-history.resolver.spec.ts +++ b/src/app/core/resolvers/admin/email-campaign-history.resolver.spec.ts @@ -3,7 +3,6 @@ import { TestBed } from '@angular/core/testing'; import { ActivatedRouteSnapshot } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; -import { SearchModule } from '@features/search/search.module'; import { EmailCampaignHistoryResolver } from './email-campaign-history.resolver'; @@ -12,8 +11,8 @@ describe('EmailCampaignHistoryResolver', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [SearchModule, HttpClientTestingModule, RouterTestingModule.withRoutes([])], - providers: [EmailCampaignHistoryResolver], + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [EmailCampaignHistoryResolver, EmailCampaignService], }); resolver = TestBed.inject(EmailCampaignHistoryResolver); }); diff --git a/src/app/core/resolvers/admin/email-template.resolver.spec.ts b/src/app/core/resolvers/admin/email-template.resolver.spec.ts index fd7d5169b6..8f1bedad7d 100644 --- a/src/app/core/resolvers/admin/email-template.resolver.spec.ts +++ b/src/app/core/resolvers/admin/email-template.resolver.spec.ts @@ -3,7 +3,6 @@ import { TestBed } from '@angular/core/testing'; import { ActivatedRouteSnapshot } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; -import { SearchModule } from '@features/search/search.module'; import { EmailTemplateResolver } from './email-template.resolver'; @@ -12,8 +11,8 @@ describe('EmailTemplateResolver', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [SearchModule, HttpClientTestingModule, RouterTestingModule.withRoutes([])], - providers: [EmailTemplateResolver], + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [EmailTemplateResolver, EmailCampaignService], }); resolver = TestBed.inject(EmailTemplateResolver); }); diff --git a/src/app/core/resolvers/admin/inactive-workplaces-for-deletion.resolver.spec.ts b/src/app/core/resolvers/admin/inactive-workplaces-for-deletion.resolver.spec.ts index dc11122e8c..20a370d993 100644 --- a/src/app/core/resolvers/admin/inactive-workplaces-for-deletion.resolver.spec.ts +++ b/src/app/core/resolvers/admin/inactive-workplaces-for-deletion.resolver.spec.ts @@ -3,7 +3,6 @@ import { TestBed } from '@angular/core/testing'; import { ActivatedRouteSnapshot } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; -import { SearchModule } from '@features/search/search.module'; import { InactiveWorkplacesForDeletionResolver } from './inactive-workplaces-for-deletion.resolver'; @@ -12,8 +11,8 @@ describe('InactiveWorkplacesForDeletionResolver', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [SearchModule, HttpClientTestingModule, RouterTestingModule.withRoutes([])], - providers: [InactiveWorkplacesForDeletionResolver], + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [InactiveWorkplacesForDeletionResolver, EmailCampaignService], }); resolver = TestBed.inject(InactiveWorkplacesForDeletionResolver); }); diff --git a/src/app/core/resolvers/admin/inactive-workplaces.resolver.spec.ts b/src/app/core/resolvers/admin/inactive-workplaces.resolver.spec.ts index 188369aa77..26b928bc9b 100644 --- a/src/app/core/resolvers/admin/inactive-workplaces.resolver.spec.ts +++ b/src/app/core/resolvers/admin/inactive-workplaces.resolver.spec.ts @@ -3,7 +3,6 @@ import { TestBed } from '@angular/core/testing'; import { ActivatedRouteSnapshot } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; -import { SearchModule } from '@features/search/search.module'; import { InactiveWorkplacesResolver } from './inactive-workplaces.resolver'; @@ -12,8 +11,8 @@ describe('InactiveWorkplacesResolver', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [SearchModule, HttpClientTestingModule, RouterTestingModule.withRoutes([])], - providers: [InactiveWorkplacesResolver], + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [InactiveWorkplacesResolver, EmailCampaignService], }); resolver = TestBed.inject(InactiveWorkplacesResolver); }); diff --git a/src/app/core/services/admin/email-campaign.service.ts b/src/app/core/services/admin/email-campaign.service.ts index 63a17c9ce9..32f2c0da2d 100644 --- a/src/app/core/services/admin/email-campaign.service.ts +++ b/src/app/core/services/admin/email-campaign.service.ts @@ -54,6 +54,14 @@ export class EmailCampaignService { }); } + getTargetedEmailsReport(fileFormData: FormData): Observable { + return this.http.post('/api/admin/email-campaigns/targeted-emails/report', fileFormData, { + headers: { InterceptorSkipHeader: 'true' }, + observe: 'response', + responseType: 'blob' as 'json', + }); + } + createTargetedEmailsCampaign(groupType: string, templateId: string, nmdsIdsFileData?: FormData): Observable { const payload = { groupType, diff --git a/src/app/core/services/registrations.service.ts b/src/app/core/services/registrations.service.ts index 0f11afdb36..b2bfa1bc99 100644 --- a/src/app/core/services/registrations.service.ts +++ b/src/app/core/services/registrations.service.ts @@ -14,11 +14,8 @@ import { Observable } from 'rxjs'; }) export class RegistrationsService { constructor(private http: HttpClient) {} - public getAllRegistrations(): Observable { - return this.http.get('/api/admin/registrations'); - } - public getRegistrations(status: string): Observable { + public getRegistrations(status: string): Observable { return this.http.get(`/api/admin/registrations/${status}`); } diff --git a/src/app/features/admin/emails/inactive-emails/inactive-emails.component.spec.ts b/src/app/features/admin/emails/inactive-emails/inactive-emails.component.spec.ts index fcf34ce1d8..c36e0e82f4 100644 --- a/src/app/features/admin/emails/inactive-emails/inactive-emails.component.spec.ts +++ b/src/app/features/admin/emails/inactive-emails/inactive-emails.component.spec.ts @@ -1,10 +1,10 @@ +import { DecimalPipe } from '@angular/common'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; import { WindowRef } from '@core/services/window.ref'; -import { SearchModule } from '@features/search/search.module'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render, within } from '@testing-library/angular'; import { of } from 'rxjs'; @@ -14,8 +14,10 @@ import { InactiveEmailsComponent } from './inactive-emails.component'; describe('InactiveEmailsComponent', () => { async function setup() { return render(InactiveEmailsComponent, { - imports: [SharedModule, SearchModule, HttpClientTestingModule, RouterTestingModule], + imports: [SharedModule, HttpClientTestingModule, RouterTestingModule], providers: [ + EmailCampaignService, + DecimalPipe, { provide: WindowRef, useClass: WindowRef, diff --git a/src/app/features/admin/emails/targeted-emails/targeted-emails.component.html b/src/app/features/admin/emails/targeted-emails/targeted-emails.component.html index e010fe93fa..89297a6669 100644 --- a/src/app/features/admin/emails/targeted-emails/targeted-emails.component.html +++ b/src/app/features/admin/emails/targeted-emails/targeted-emails.component.html @@ -36,6 +36,19 @@
diff --git a/src/app/features/admin/emails/targeted-emails/targeted-emails.component.spec.ts b/src/app/features/admin/emails/targeted-emails/targeted-emails.component.spec.ts index cf61322747..8d25e30aa6 100644 --- a/src/app/features/admin/emails/targeted-emails/targeted-emails.component.spec.ts +++ b/src/app/features/admin/emails/targeted-emails/targeted-emails.component.spec.ts @@ -1,3 +1,4 @@ +import { DecimalPipe } from '@angular/common'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -5,7 +6,7 @@ import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; import { WindowRef } from '@core/services/window.ref'; -import { SearchModule } from '@features/search/search.module'; +import { AdminModule } from '@features/admin/admin.module'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; @@ -23,12 +24,14 @@ describe('TargetedEmailsComponent', () => { SharedModule, HttpClientTestingModule, RouterTestingModule, - SearchModule, FormsModule, ReactiveFormsModule, NgxDropzoneModule, + AdminModule, ], providers: [ + EmailCampaignService, + DecimalPipe, { provide: WindowRef, useClass: WindowRef, @@ -82,7 +85,7 @@ describe('TargetedEmailsComponent', () => { expect(totalEmail.innerHTML).toContain('1,500'); }); - it("should display 0 emails to be sent when the group and template haven't been selected", async () => { + it(`should display 0 emails to be sent when the group and template haven't been selected`, async () => { const component = await setup(); component.fixture.componentInstance.emailGroup = null; @@ -93,7 +96,7 @@ describe('TargetedEmailsComponent', () => { expect(totalEmails.innerHTML).toContain('0'); }); - it("should disable the Send emails button when the group and template haven't been selected", async () => { + it(`should disable the Send emails button when the group and template haven't been selected`, async () => { const component = await setup(); component.fixture.componentInstance.emailGroup = ''; @@ -263,6 +266,59 @@ describe('TargetedEmailsComponent', () => { expect(getTargetedTotalValidEmailsSpy).toHaveBeenCalledOnceWith(fileFormData); }); + it('should only display Download targeted emails report link after file uploaded when Multiple accounts selected', async () => { + const { fixture, getByLabelText, getAllByLabelText, queryByText } = await setup(); + + const emailCampaignService = TestBed.inject(EmailCampaignService); + spyOn(emailCampaignService, 'getTargetedTotalValidEmails').and.callFake(() => of({ totalEmails: 3 })); + + const groupSelect = getByLabelText('Email group', { exact: false }); + fireEvent.change(groupSelect, { target: { value: 'multipleAccounts' } }); + fixture.detectChanges(); + + expect(queryByText('Download targeted emails report')).toBeFalsy(); + + const fileInput = getAllByLabelText('upload files here'); + const file = new File(['some file content'], 'establishments.csv', { type: 'text/csv' }); + const fileFormData: FormData = new FormData(); + fileFormData.append('targetedRecipientsFile', file); + + userEvent.upload(fileInput[1], file); + fixture.detectChanges(); + + expect(queryByText('Download targeted emails report')).toBeTruthy(); + }); + + it('should call getTargetedEmailsReport in emailCampaign service when download button clicked', async () => { + const { fixture, getByText, getByLabelText, getAllByLabelText, queryByText } = await setup(); + + const emailCampaignService = TestBed.inject(EmailCampaignService); + spyOn(emailCampaignService, 'getTargetedTotalValidEmails').and.callFake(() => of({ totalEmails: 3 })); + const getTargetedEmailsReportSpy = spyOn(emailCampaignService, 'getTargetedEmailsReport').and.callFake(() => + of(null), + ); + const saveSpy = spyOn(fixture.componentInstance, 'saveFile').and.callFake(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function + + const groupSelect = getByLabelText('Email group', { exact: false }); + fireEvent.change(groupSelect, { target: { value: 'multipleAccounts' } }); + fixture.detectChanges(); + + expect(queryByText('Download targeted emails report')).toBeFalsy(); + + const fileInput = getAllByLabelText('upload files here'); + const file = new File(['some file content'], 'establishments.csv', { type: 'text/csv' }); + const fileFormData: FormData = new FormData(); + fileFormData.append('targetedRecipientsFile', file); + + userEvent.upload(fileInput[1], file); + fixture.detectChanges(); + + fireEvent.click(getByText('Download targeted emails report')); + + expect(getTargetedEmailsReportSpy).toHaveBeenCalledWith(fileFormData); + expect(saveSpy).toHaveBeenCalled(); + }); + it('should call createTargetedEmailsCampaign with multipleAccounts, template id and file when file uploaded and sending emails confirmed', async () => { const { fixture, getByText, getByLabelText, getAllByLabelText } = await setup(); diff --git a/src/app/features/admin/emails/targeted-emails/targeted-emails.component.ts b/src/app/features/admin/emails/targeted-emails/targeted-emails.component.ts index e3c9263b83..dcab33f3e5 100644 --- a/src/app/features/admin/emails/targeted-emails/targeted-emails.component.ts +++ b/src/app/features/admin/emails/targeted-emails/targeted-emails.component.ts @@ -1,10 +1,12 @@ import { DecimalPipe } from '@angular/common'; +import { HttpResponse } from '@angular/common/http'; import { Component, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { EmailType, TotalEmailsResponse } from '@core/model/emails.model'; import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; import { AlertService } from '@core/services/alert.service'; import { DialogService } from '@core/services/dialog.service'; +import saveAs from 'file-saver'; import { Subscription } from 'rxjs'; import { SendEmailsConfirmationDialogComponent } from '../dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component'; @@ -22,7 +24,7 @@ export class TargetedEmailsComponent implements OnDestroy { private subscriptions: Subscription = new Subscription(); public emailType = EmailType; public showDragAndDrop = false; - private nmdsIdsFileData: FormData | null = null; + public nmdsIdsFileData: FormData | null = null; constructor( public alertService: AlertService, @@ -82,6 +84,15 @@ export class TargetedEmailsComponent implements OnDestroy { ); } + public downloadTargetedEmailsReport(event: Event): void { + event.preventDefault(); + this.subscriptions.add( + this.emailCampaignService + .getTargetedEmailsReport(this.nmdsIdsFileData) + .subscribe((response) => this.saveFile(response)), + ); + } + public validateFile(file: File): void { this.nmdsIdsFileData = new FormData(); this.nmdsIdsFileData.append('targetedRecipientsFile', file, file.name); @@ -90,6 +101,16 @@ export class TargetedEmailsComponent implements OnDestroy { .subscribe((res: TotalEmailsResponse) => (this.totalEmails = res.totalEmails)); } + public saveFile(response: HttpResponse): void { + const filenameRegEx = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; + const header = response.headers.get('content-disposition'); + const filenameMatches = header && header.match(filenameRegEx); + const filename = filenameMatches && filenameMatches.length > 1 ? filenameMatches[1] : null; + const blob = new Blob([response.body], { type: 'text/plain;charset=utf-8' }); + + saveAs(blob, filename); + } + ngOnDestroy(): void { this.subscriptions.unsubscribe(); } diff --git a/src/app/features/admin/report/admin-report.component.spec.ts b/src/app/features/admin/report/admin-report.component.spec.ts index 059e81371e..901f02c20d 100644 --- a/src/app/features/admin/report/admin-report.component.spec.ts +++ b/src/app/features/admin/report/admin-report.component.spec.ts @@ -2,7 +2,6 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { ReportService } from '@core/services/report.service'; -import { SearchModule } from '@features/search/search.module'; import { SharedModule } from '@shared/shared.module'; import { fireEvent, render } from '@testing-library/angular'; import { of } from 'rxjs'; @@ -16,7 +15,7 @@ describe('ReportComponent', () => { async function setup() { return render(ReportComponent, { - imports: [SharedModule, SearchModule, HttpClientTestingModule, RouterTestingModule, AdminModule], + imports: [SharedModule, HttpClientTestingModule, RouterTestingModule, AdminModule], }); } diff --git a/src/app/features/search/cqc-status-change/cqc-confirmation-dialog.component.html b/src/app/features/search/cqc-status-change/cqc-confirmation-dialog.component.html deleted file mode 100644 index 8e6d957cbb..0000000000 --- a/src/app/features/search/cqc-status-change/cqc-confirmation-dialog.component.html +++ /dev/null @@ -1,10 +0,0 @@ -

- {{data.headingText}} -

- -

{{ data.paragraphText }}

- - - diff --git a/src/app/features/search/cqc-status-change/cqc-confirmation-dialog.component.ts b/src/app/features/search/cqc-status-change/cqc-confirmation-dialog.component.ts deleted file mode 100644 index 6d2a274f23..0000000000 --- a/src/app/features/search/cqc-status-change/cqc-confirmation-dialog.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { DialogComponent } from '@core/components/dialog.component'; -import { Dialog, DIALOG_DATA } from '@core/services/dialog.service'; - -@Component({ - selector: 'app-cqc-confirmation-dialog', - templateUrl: './cqc-confirmation-dialog.component.html', -}) -export class CqcConfirmationDialogComponent extends DialogComponent { - constructor( - @Inject(DIALOG_DATA) public data: { headingText: string, paragraphText: string, buttonText: string }, - public dialog: Dialog - ) { - super(data, dialog); - } - - public close(confirmed: boolean) { - this.dialog.close(confirmed); - } -} diff --git a/src/app/features/search/cqc-status-change/cqc-status-change.component.html b/src/app/features/search/cqc-status-change/cqc-status-change.component.html deleted file mode 100644 index 35c90eb968..0000000000 --- a/src/app/features/search/cqc-status-change/cqc-status-change.component.html +++ /dev/null @@ -1,66 +0,0 @@ -

CQC status change for {{ cqcStatusChange.orgName }}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Requested - {{ cqcStatusChange.requested }} - Username - {{ cqcStatusChange.username }} -
Workplace ID - {{ cqcStatusChange.workplaceId }} - Workplace - - {{ cqcStatusChange.orgName }} - -
Requested service - {{ cqcStatusChange.data.requestedService.name }} - Current service - {{ cqcStatusChange.data.currentService.name }} -
- Requested service name - {{ cqcStatusChange.data.requestedService.other }} - Current service name - {{ cqcStatusChange.data.currentService.other }} -
-
- - -
-
-
diff --git a/src/app/features/search/cqc-status-change/cqc-status-change.component.spec.ts b/src/app/features/search/cqc-status-change/cqc-status-change.component.spec.ts deleted file mode 100644 index cf78c5adef..0000000000 --- a/src/app/features/search/cqc-status-change/cqc-status-change.component.spec.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { SharedModule } from '@shared/shared.module'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { render, within } from '@testing-library/angular'; -import { spy } from 'sinon'; -import { of } from 'rxjs'; -import { RouterTestingModule } from '@angular/router/testing'; -import { WindowRef } from '@core/services/window.ref'; - -import { CqcStatusChangeComponent } from './cqc-status-change.component'; -import { CqcChangeData } from '@core/model/cqc-change-data.model'; -import { ApprovalRequest } from '@core/model/approval-request.model'; -import { FeatureFlagsService } from '@shared/services/feature-flags.service'; -import { MockFeatureFlagsService } from '@core/test-utils/MockFeatureFlagService'; - -const testChangeRequestId = 9999; -const testParentRequestUuid = '360c62a1-2e20-410d-a72b-9d4100a11f4e'; -const testUsername = 'Mary Poppins'; -const testOrgname = 'Fawlty Towers'; -const testUserId = 1111; -const testEstablishmentId = 2222; -const testEstablishmentUid = '9efce151-6167-4e99-9cbf-0b9f8ab987fa'; -const testWorkplaceId = 'B1234567'; -const testRequestedDate = new Date(); - -const approveButtonText = 'Approve'; -const rejectButtonText = 'Reject'; -const modalApproveText = 'Approve this change'; -const modalRejectText = 'Reject this change'; - -function cqcStatusChangeGenerator(otherCurrentService = false, otherRequestedService = false, usernameNull = false) { - const payload = { - requestId: testChangeRequestId, - requestUUID: testParentRequestUuid, - establishmentId: testEstablishmentId, - establishmentUid: testEstablishmentUid, - userId: testUserId, - workplaceId: testWorkplaceId, - userName: testUsername, - orgName: testOrgname, - requested: testRequestedDate, - status: 'Pending', - data: { - currentService: { - ID: 1, - name: 'Carers support', - }, - requestedService: { - ID: 2, - name: 'Service Name' - } - } as CqcChangeData - } as ApprovalRequest; - if (otherCurrentService) { - payload.data.currentService = { - ID: 5, - name: 'Other Service', - other: 'Other Service Name' - }; - } - if (otherRequestedService) { - payload.data.requestedService = { - ID: 6, - name: 'Other Service', - other: 'Other Service Name' - }; - if (usernameNull) { - payload.userName = null; - } - } - return payload; -} - -describe('CqcStatusChangeComponent', () => { - - async function getCqcStatusChangeComponent(otherCurrentService = false, otherRequestedService= false, usernameNull = false) { - return render(CqcStatusChangeComponent, { - imports: [ - ReactiveFormsModule, - HttpClientTestingModule, - SharedModule, - RouterTestingModule - ], - providers: [ - { provide: FeatureFlagsService, useClass: MockFeatureFlagsService}, - { - provide: WindowRef, - useClass: WindowRef - }, - ], - componentProperties: { - index: 0, - removeCqcStatusChanges: { - emit: spy(), - } as any, - cqcStatusChange: cqcStatusChangeGenerator(otherCurrentService, otherRequestedService, usernameNull), - }, - }); - } - async function clickFirstApproveButton() { - const component = await getCqcStatusChangeComponent(); - component.getByText(approveButtonText).click(); - const modalConfirmationDialog = await within(document.body).findByRole('dialog'); - return { component, modalConfirmationDialog }; - } - - async function clickFirstRejectButton() { - const component = await getCqcStatusChangeComponent(); - component.getByText(rejectButtonText).click(); - const modalConfirmationDialog = await within(document.body).findByRole('dialog'); - return { component, modalConfirmationDialog }; - } - - it('should create', async () => { - // Act - const component = await getCqcStatusChangeComponent(); - - // Assert - expect(component).toBeTruthy(); - }); - - it('should show up requested service name if requested service has an \'other\' type ', async () => { - const otherCurrentService = false; - const otherRequestedService = true; - const component = await getCqcStatusChangeComponent(otherCurrentService , otherRequestedService); - const otherServiceTitle = await within(document.body).findByTestId('cqc-requested-service-other-title'); - const otherServiceValue = await within(document.body).findByTestId('cqc-requested-service-other-value'); - const cqcStatusChange = cqcStatusChangeGenerator(otherCurrentService , otherRequestedService); - expect(otherServiceTitle.innerHTML).toContain(`Requested service name`); - expect(otherServiceValue.innerHTML).toContain(cqcStatusChange.data.requestedService.other); - - }); - it('shouldn\'t show up current service name if current service doesnt an \'other\' type ', async () => { - - const component = await getCqcStatusChangeComponent(); - - const currentServiceTitle = await within(document.body).queryByTestId('cqc-current-service-other-title'); - const currentServiceValue = await within(document.body).queryByTestId('cqc-current-service-other-value'); - - expect(currentServiceTitle).toBeNull(); - expect(currentServiceValue).toBeNull(); - - }); - it('should be able to approve a CQC Status Change request', async () => { - // Arrange - const { component, modalConfirmationDialog } = await clickFirstApproveButton(); - const cqcStatusChangeApproval = spyOn(component.fixture.componentInstance.cqcStatusChangeService, 'CqcStatusChangeApproval').and.callThrough(); - - // Act - within(modalConfirmationDialog).getByText(modalApproveText).click(); - - // Assert - expect(cqcStatusChangeApproval).toHaveBeenCalledWith({ - approvalId: testChangeRequestId, - establishmentId: testEstablishmentId, - userId: testUserId, - rejectionReason: 'Approved', - approve: true, - }); - }); - - it('should be able to reject a CQC Status Change request', async () => { - // Arrange - const { component, modalConfirmationDialog } = await clickFirstRejectButton(); - const cqcSatusChangeApproval = spyOn(component.fixture.componentInstance.cqcStatusChangeService, 'CqcStatusChangeApproval').and.callThrough(); - - // Act - within(modalConfirmationDialog).getByText(modalRejectText).click(); - - // Assert - expect(cqcSatusChangeApproval).toHaveBeenCalledWith({ - approvalId: testChangeRequestId, - establishmentId: testEstablishmentId, - userId: testUserId, - rejectionReason: 'Rejected', - approve: false, - }); - }); - - it('should show confirmation modal when approving a request', async () => { - // Arrange - const component = await getCqcStatusChangeComponent(); - const confirmationModal = spyOn(component.fixture.componentInstance.dialogService, 'open').and.callThrough(); - - // Act - component.getByText(approveButtonText).click(); - - // Teardown - const modalConfirmationDialog = await within(document.body).findByRole('dialog'); - within(modalConfirmationDialog).getByText(modalApproveText).click(); - - // Assert - expect(confirmationModal).toHaveBeenCalled(); - }); - - it('confirmation modal should display org name and requested service when approving a request', async () => { - // Act - const { modalConfirmationDialog } = await clickFirstApproveButton(); - const paragraph = within(modalConfirmationDialog).getByTestId('cqc-confirm-para'); - const cqcStatusChange = cqcStatusChangeGenerator(); - - // Teardown - within(modalConfirmationDialog).getByText(modalApproveText).click(); - - // Assert - expect(paragraph.innerHTML).toContain(`If you do this, ${testOrgname} will be flagged as CQC regulated and their new main service will be ${cqcStatusChange.data.requestedService.name}.`); - }); - - it('confirmation modal should display org name when rejecting a request', async () => { - // Act - const { modalConfirmationDialog } = await clickFirstRejectButton(); - const paragraph = within(modalConfirmationDialog).getByTestId('cqc-confirm-para'); - const cqcStatusChange = cqcStatusChangeGenerator(); - - // Teardown - within(modalConfirmationDialog).getByText(modalRejectText).click(); - - // Assert - expect(paragraph.innerHTML).toContain(`If you do this, ${testOrgname} will not be flagged as CQC regulated and their main service will still be ${cqcStatusChange.data.currentService.name}.`); - }); - - it('confirmation modal should show approval message when approving a request', async () => { - // Act - const { modalConfirmationDialog } = await clickFirstApproveButton(); - const approveHeading = within(modalConfirmationDialog).getByTestId('CQC-confirm-heading'); - const submitButton = within(modalConfirmationDialog).getByText(modalApproveText); - - // Teardown - within(modalConfirmationDialog).getByText(modalApproveText).click(); - - // Assert - expect(approveHeading.innerHTML).toContain('You\'re about to approve this request.'); - expect(submitButton).toBeTruthy(); - }); - - it('confirmation modal should show rejection message" when rejecting a request', async () => { - // Act - const { modalConfirmationDialog } = await clickFirstRejectButton(); - const rejectHeading = within(modalConfirmationDialog).getByTestId('CQC-confirm-heading'); - const submitButton = within(modalConfirmationDialog).getByText(modalRejectText); - - // Teardown - within(modalConfirmationDialog).getByText(modalRejectText).click(); - - // Assert - expect(rejectHeading.innerHTML).toContain('You\'re about to reject this request.'); - expect(submitButton).toBeTruthy(); - }); - - it('confirmation message should be shown after approving a request', async () => { - // Arrange - const { component, modalConfirmationDialog } = await clickFirstApproveButton(); - const addAlert = spyOn(component.fixture.componentInstance.alertService, 'addAlert').and.callThrough(); - spyOn(component.fixture.componentInstance.cqcStatusChangeService, 'CqcStatusChangeApproval').and.returnValue(of({})); - - // Act - within(modalConfirmationDialog).getByText(modalApproveText).click(); - component.fixture.detectChanges(); - - // Assert - expect(addAlert).toHaveBeenCalledWith({ - type: 'success', - message: `You\'ve approved the main service change for ${testOrgname}.` - }); - }); - - it('confirmation message should be shown after rejecting a request', async () => { - // Arrange - const { component, modalConfirmationDialog } = await clickFirstRejectButton(); - const addAlert = spyOn(component.fixture.componentInstance.alertService, 'addAlert').and.callThrough(); - spyOn(component.fixture.componentInstance.cqcStatusChangeService, 'CqcStatusChangeApproval').and.returnValue(of({})); - - // Act - within(modalConfirmationDialog).getByText(modalRejectText).click(); - component.fixture.detectChanges(); - - // Assert - expect(addAlert).toHaveBeenCalledWith({ - type: 'success', - message: `You\'ve rejected the main service change for ${testOrgname}.`, - }); - }); - -}); - diff --git a/src/app/features/search/cqc-status-change/cqc-status-change.component.ts b/src/app/features/search/cqc-status-change/cqc-status-change.component.ts deleted file mode 100644 index db539c48bc..0000000000 --- a/src/app/features/search/cqc-status-change/cqc-status-change.component.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { AlertService } from '@core/services/alert.service'; -import { CqcStatusChangeService } from '@core/services/cqc-status-change.service'; -import { DialogService } from '@core/services/dialog.service'; -import { SwitchWorkplaceService } from '@core/services/switch-workplace.service'; -import { CqcConfirmationDialogComponent } from '@features/search/cqc-status-change/cqc-confirmation-dialog.component'; - -@Component({ - selector: 'app-cqc-status-change', - templateUrl: './cqc-status-change.component.html', -}) -export class CqcStatusChangeComponent implements OnInit { - @Output() removeCqcStatusChanges: EventEmitter = new EventEmitter(); - @Input() index: number; - @Input() cqcStatusChange: any; - public cqcStatusChangeForm: FormGroup; - public approve: boolean; - public rejectionReason: string; - - constructor( - public cqcStatusChangeService: CqcStatusChangeService, - public switchWorkplaceService: SwitchWorkplaceService, - public dialogService: DialogService, - public alertService: AlertService, - ) {} - ngOnInit() { - this.cqcStatusChangeForm = new FormGroup({}); - } - - get establishmentId() { - return this.cqcStatusChange.establishmentId; - } - - public approveCQCRequest(approve: boolean, rejectionReason: string) { - this.approve = approve; - this.rejectionReason = rejectionReason; - - event.preventDefault(); - - this.dialogService - .open(CqcConfirmationDialogComponent, { - orgName: this.cqcStatusChange.orgName, - headingText: approve ? "You're about to approve this request." : "You're about to reject this request.", - paragraphText: approve - ? `If you do this, ${this.cqcStatusChange.orgName} will be flagged as CQC regulated and their new main service will be ${this.cqcStatusChange.data.requestedService.name}.` - : `If you do this, ${this.cqcStatusChange.orgName} will not be flagged as CQC regulated and their main service will still be ${this.cqcStatusChange.data.currentService.name}.`, - buttonText: approve ? 'Approve this change' : 'Reject this change', - }) - .afterClosed.subscribe((approveConfirmed) => { - if (approveConfirmed) { - this.approveOrRejectRequest(); - } - }); - } - - public navigateToWorkplace(id, username, nmdsId, e): void { - e.preventDefault(); - this.switchWorkplaceService.navigateToWorkplace(id, username, nmdsId); - } - - public onSubmit() { - // Nothing to do here - it's all done via the confirmation dialog. - } - - private approveOrRejectRequest() { - let data; - data = { - approvalId: this.cqcStatusChange.requestId, - establishmentId: this.cqcStatusChange.establishmentId, - userId: this.cqcStatusChange.userId, - rejectionReason: this.rejectionReason, - approve: this.approve, - }; - - this.cqcStatusChangeService.CqcStatusChangeApproval(data).subscribe( - () => { - this.removeCqcStatusChanges.emit(this.index); - this.showConfirmationMessage(); - }, - (err) => { - if (err instanceof HttpErrorResponse) { - this.populateErrorFromServer(err); - } - }, - ); - } - - private showConfirmationMessage() { - const approvedOrRejected = this.approve ? 'approved' : 'rejected'; - - this.alertService.addAlert({ - type: 'success', - message: `You've ${approvedOrRejected} the main service change for ${this.cqcStatusChange.orgName}.`, - }); - } - - private populateErrorFromServer(err) { - const validationErrors = err.error; - - Object.keys(validationErrors).forEach((prop) => { - const formControl = this.cqcStatusChangeForm.get(prop); - if (formControl) { - formControl.setErrors({ - serverError: validationErrors[prop], - }); - } - }); - } -} diff --git a/src/app/features/search/cqc-status-changes/cqc-status-changes.component.html b/src/app/features/search/cqc-status-changes/cqc-status-changes.component.html deleted file mode 100644 index cf6696cad0..0000000000 --- a/src/app/features/search/cqc-status-changes/cqc-status-changes.component.html +++ /dev/null @@ -1,6 +0,0 @@ -

- CQC status changes -

-
- -
diff --git a/src/app/features/search/cqc-status-changes/cqc-status-changes.component.spec.ts b/src/app/features/search/cqc-status-changes/cqc-status-changes.component.spec.ts deleted file mode 100644 index c498a4effc..0000000000 --- a/src/app/features/search/cqc-status-changes/cqc-status-changes.component.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { inject, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; -import { CqcStatusChangeService } from '@core/services/cqc-status-change.service'; -import { WindowRef } from '@core/services/window.ref'; -import { SharedModule } from '@shared/shared.module'; -import { render, RenderResult } from '@testing-library/angular'; -import { of } from 'rxjs'; - -import { CqcStatusChangeComponent } from '../cqc-status-change/cqc-status-change.component'; -import { CqcStatusChangesComponent } from '../cqc-status-changes/cqc-status-changes.component'; -import { FeatureFlagsService } from '@shared/services/feature-flags.service'; -import { MockFeatureFlagsService } from '@core/test-utils/MockFeatureFlagService'; - -describe('CqcStatusChangesComponent', () => { - let component: RenderResult; - - it('can get a CQC Status Change request', () => { - inject([HttpClientTestingModule], async () => { - const cqcStatusChanges = [ - { - requestId: 1, - requestUUID: '360c62a1-2e20-410d-a72b-9d4100a11f4e', - establishmentId: 1, - establishmentUid: '9efce151-6167-4e99-9cbf-0b9f8ab987fa', - userId: 222, - workplaceId: 'I1234567', - username: 'testuser', - orgName: 'testOrgname', - requested: '2019-08-27 16:04:35.914', - status: 'Pending', - data: { - currentService: { - ID: 1, - name: 'Carers support', - }, - requestedService: { - ID: 2, - name: 'Service Name', - }, - }, - }, - { - requestId: 2, - requestUUID: '360c62a1-2e20-410d-a72b-9d4100a11f2a', - establishmentId: 2, - establishmentUid: '9eece151-6167-4e99-9cbf-0b9f8ab111ba', - userId: '333', - workplaceId: 'E1234567', - username: 'testUsername2', - orgName: 'testOrgname2', - requested: '2020-05-20 16:04:35.914', - status: 'Pending', - data: { - currentService: { - ID: 4, - name: ' Some Service', - }, - requestedService: { - ID: 3, - name: 'Service Name', - }, - }, - }, - ]; - - const cqcStatusChangeService = TestBed.inject(CqcStatusChangeService); - spyOn(cqcStatusChangeService, 'getCqcStatusChanges').and.returnValue(of(cqcStatusChanges)); - - const { fixture } = await render(CqcStatusChangesComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule, SharedModule, RouterTestingModule], - declarations: [CqcStatusChangeComponent], - providers: [ - { - provide: WindowRef, - useClass: WindowRef, - }, - { provide: FeatureFlagsService, useClass: MockFeatureFlagsService}, - { - provide: CqcStatusChangeService, - useClass: cqcStatusChangeService, - }, - ], - }); - - const { componentInstance } = fixture; - - expect(componentInstance.cqcStatusChanges).toEqual(cqcStatusChanges); - }); - }); - - it('should remove parent requests', async () => { - const cqcStatusChanges = [ - { - requestId: 1, - requestUUID: '360c62a1-2e20-410d-a72b-9d4100a11f4e', - establishmentId: 1, - establishmentUid: '9efce151-6167-4e99-9cbf-0b9f8ab987fa', - userId: 222, - workplaceId: 'I1234567', - username: 'testuser', - orgName: 'testOrgname', - requested: '2019-08-27 16:04:35.914', - status: 'Pending', - data: { - currentService: { - ID: 1, - name: 'Carers support', - }, - requestedService: { - ID: 2, - name: 'Service Name', - }, - }, - }, - { - requestId: 2, - requestUUID: '360c62a1-2e20-410d-a72b-9d4100a11f2a', - establishmentId: 2, - establishmentUid: '9eece151-6167-4e99-9cbf-0b9f8ab111ba', - userId: '333', - workplaceId: 'E1234567', - username: 'testUsername2', - orgName: 'testOrgname2', - requested: '2020-05-20 16:04:35.914', - status: 'Pending', - data: { - currentService: { - ID: 4, - name: ' Some Service', - }, - requestedService: { - ID: 3, - name: 'Service Name', - }, - }, - }, - ]; - - const { fixture } = await render(CqcStatusChangesComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule, SharedModule, RouterTestingModule], - declarations: [CqcStatusChangeComponent], - providers: [ - { - provide: WindowRef, - useClass: WindowRef, - }, - { provide: FeatureFlagsService, useClass: MockFeatureFlagsService}, - - ], - componentProperties: { - cqcStatusChanges, - }, - }); - - const { componentInstance } = fixture; - - componentInstance.removeCqcStatusChanges(0); - - expect(componentInstance.cqcStatusChanges).toContain(cqcStatusChanges[0]); - expect(componentInstance.cqcStatusChanges).not.toContain(cqcStatusChanges[1]); - expect(componentInstance.cqcStatusChanges.length).toBe(1); - }); -}); diff --git a/src/app/features/search/cqc-status-changes/cqc-status-changes.component.ts b/src/app/features/search/cqc-status-changes/cqc-status-changes.component.ts deleted file mode 100644 index afca5fe17a..0000000000 --- a/src/app/features/search/cqc-status-changes/cqc-status-changes.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { CqcStatusChangeService } from '@core/services/cqc-status-change.service'; - -@Component({ - selector: 'app-cqc-status-changes', - templateUrl: './cqc-status-changes.component.html' -}) -export class CqcStatusChangesComponent implements OnInit { - public cqcStatusChanges = []; - - constructor( - public cqcStatusChangeService: CqcStatusChangeService - ) {} - - ngOnInit() { - this.getCqcStatusChanges(); - } - - public getCqcStatusChanges() { - this.cqcStatusChangeService.getCqcStatusChanges().subscribe( - data => { - this.cqcStatusChanges = data; - }, - error => this.onError(error) - ); - } - - public removeCqcStatusChanges(index: number) { - this.cqcStatusChanges.splice(index, 1); - } - - private onError(error) {} -} diff --git a/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.html b/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.html deleted file mode 100644 index 3009ad0d2b..0000000000 --- a/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.html +++ /dev/null @@ -1,12 +0,0 @@ -

Are you sure you want to send these emails?

- -

- This will send an email to {{ data.emailCount | number }} - - user. - users. - -

- - - diff --git a/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.spec.ts b/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.spec.ts deleted file mode 100644 index c0b7abeeae..0000000000 --- a/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Dialog, DIALOG_DATA } from '@core/services/dialog.service'; -import { SharedModule } from '@shared/shared.module'; -import { render } from '@testing-library/angular'; - -import { SendEmailsConfirmationDialogComponent } from './send-emails-confirmation-dialog.component'; - -describe('SendEmailsConfirmationDialogComponent', () => { - async function setup() { - return render(SendEmailsConfirmationDialogComponent, { - imports: [SharedModule], - providers: [ - { provide: DIALOG_DATA, useValue: {} }, - { provide: Dialog, useValue: {} }, - ], - }); - } - - it('should display confirmation dialog', async () => { - const component = await setup(); - component.fixture.componentInstance.data.emailCount = 10; - component.fixture.detectChanges(true); - const p = component.fixture.nativeElement.querySelector('p'); - expect(p.innerText).toContain('This will send an email to 10 users.'); - }); - - it('should pluralise the confirmation dialog', async () => { - const component = await setup(); - component.fixture.componentInstance.data.emailCount = 1; - component.fixture.detectChanges(true); - const p = component.fixture.nativeElement.querySelector('p'); - expect(p.innerText).toContain('This will send an email to 1 user.'); - }); -}); diff --git a/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.ts b/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.ts deleted file mode 100644 index 01751b3817..0000000000 --- a/src/app/features/search/emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { DialogComponent } from '@core/components/dialog.component'; -import { Dialog, DIALOG_DATA } from '@core/services/dialog.service'; - -@Component({ - selector: 'app-send-emails-confirmation-dialog', - templateUrl: './send-emails-confirmation-dialog.component.html', -}) -export class SendEmailsConfirmationDialogComponent extends DialogComponent { - constructor( - @Inject(DIALOG_DATA) public data: { emailCount: number }, - public dialog: Dialog, - ) { - super(data, dialog); - } - - public close(hasConfirmed: boolean) { - this.dialog.close(hasConfirmed); - } -} diff --git a/src/app/features/search/emails/emails.component.html b/src/app/features/search/emails/emails.component.html deleted file mode 100644 index 03a9d86086..0000000000 --- a/src/app/features/search/emails/emails.component.html +++ /dev/null @@ -1,160 +0,0 @@ -

Inactive Workplaces

- -
-
-
-
Total number of inactive workplaces to email
-
- {{ inactiveWorkplaces | number }} -
-
- -
-
- -
- -
- -
-

History

- - - - - - - - - - - - - -
DateTotal emails sent
{{ row.date | date: 'dd/MM/yyyy' }}{{ row.emails | number }}
- - -

No emails have been sent yet.

-
-
-
- - -
- -

Targeted Emails

- -
-
-
-
Total number of users to be emailed
-
- {{ totalEmails | number }} -
-
- -
-
-
- - -
-
- - -
-
-
- - -
-
diff --git a/src/app/features/search/emails/emails.component.spec.ts b/src/app/features/search/emails/emails.component.spec.ts deleted file mode 100644 index e4b74abc6f..0000000000 --- a/src/app/features/search/emails/emails.component.spec.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; -import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; -import { ReportService } from '@core/services/report.service'; -import { WindowRef } from '@core/services/window.ref'; -import { SharedModule } from '@shared/shared.module'; -import { fireEvent, render, within } from '@testing-library/angular'; -import { of } from 'rxjs'; - -import { SearchModule } from '../search.module'; -import { EmailsComponent } from './emails.component'; - -describe('EmailsComponent', () => { - async function setup() { - return render(EmailsComponent, { - imports: [SharedModule, SearchModule, HttpClientTestingModule, RouterTestingModule], - providers: [ - { - provide: WindowRef, - useClass: WindowRef, - }, - { - provide: ActivatedRoute, - useValue: { - snapshot: { - data: { - emailCampaignHistory: [], - inactiveWorkplaces: { inactiveWorkplaces: 0 }, - emailTemplates: { - templates: [], - }, - }, - }, - }, - }, - ], - }); - } - - describe('Inactive workplaces', () => { - it('should create', async () => { - const component = await setup(); - expect(component).toBeTruthy(); - }); - - it('should display the total number of inactive workplaces', async () => { - const component = await setup(); - - component.fixture.componentInstance.inactiveWorkplaces = 1250; - component.fixture.detectChanges(); - - const numInactiveWorkplaces = component.getByTestId('inactiveWorkplaces'); - const sendEmailsButton = component.fixture.nativeElement.querySelectorAll('button'); - expect(numInactiveWorkplaces.innerHTML).toContain('1,250'); - expect(sendEmailsButton[0].disabled).toBeFalsy(); - }); - - it('should disable the "Send emails" button when there are no inactive workplaces', async () => { - const component = await setup(); - - component.fixture.componentInstance.inactiveWorkplaces = 0; - component.fixture.detectChanges(); - - const sendEmailsButton = component.fixture.nativeElement.querySelectorAll('button'); - expect(sendEmailsButton[0].disabled).toBeTruthy(); - }); - - it('should display all existing history', async () => { - const component = await setup(); - - component.fixture.componentInstance.history = [ - { - date: '2021-01-05', - emails: 351, - }, - { - date: '2020-12-05', - emails: 772, - }, - ]; - - component.fixture.detectChanges(); - - const numInactiveWorkplaces = component.getByTestId('emailCampaignHistory'); - expect(numInactiveWorkplaces.innerHTML).toContain('05/01/2021'); - expect(numInactiveWorkplaces.innerHTML).toContain('772'); - expect(numInactiveWorkplaces.innerHTML).toContain('05/12/2020'); - expect(numInactiveWorkplaces.innerHTML).toContain('351'); - }); - - it('should display a message when no emails have been sent', async () => { - const component = await setup(); - - const numInactiveWorkplaces = component.getByTestId('emailCampaignHistory'); - expect(numInactiveWorkplaces.innerHTML).toContain('No emails have been sent yet.'); - }); - - it('should call confirmSendEmails when the "Send emails" button is clicked', async () => { - const component = await setup(); - - component.fixture.componentInstance.inactiveWorkplaces = 25; - component.fixture.detectChanges(); - - fireEvent.click(component.getByText('Send emails', { exact: true })); - - const dialog = await within(document.body).findByRole('dialog'); - const dialogHeader = within(dialog).getByTestId('send-emails-confirmation-header'); - - expect(dialogHeader).toBeTruthy(); - }); - - [ - { - emails: 2500, - expectedMessage: '2,500 emails have been scheduled to be sent.', - }, - { - emails: 1, - expectedMessage: '1 email has been scheduled to be sent.', - }, - ].forEach(({ emails, expectedMessage }) => { - it('should display an alert when the "Send emails" button is clicked', async () => { - const component = await setup(); - - component.fixture.componentInstance.inactiveWorkplaces = 2500; - component.fixture.detectChanges(); - - fireEvent.click(component.getByText('Send emails', { exact: true })); - - const emailCampaignService = TestBed.inject(EmailCampaignService); - spyOn(emailCampaignService, 'createInactiveWorkplacesCampaign').and.returnValue( - of({ - emails, - }), - ); - - spyOn(emailCampaignService, 'getInactiveWorkplaces').and.returnValue( - of({ - inactiveWorkplaces: 0, - }), - ); - - const addAlert = spyOn(component.fixture.componentInstance.alertService, 'addAlert').and.callThrough(); - - const dialog = await within(document.body).findByRole('dialog'); - within(dialog).getByText('Send emails').click(); - - expect(addAlert).toHaveBeenCalledWith({ - type: 'success', - message: expectedMessage, - }); - - const numInactiveWorkplaces = component.getByTestId('inactiveWorkplaces'); - expect(numInactiveWorkplaces.innerHTML).toContain('0'); - }); - }); - - it('should download a report when the "Download report" button is clicked', async () => { - const component = await setup(); - - const emailCampaignService = TestBed.inject(EmailCampaignService); - const getReport = spyOn(emailCampaignService, 'getInactiveWorkplacesReport').and.callFake(() => of(null)); - const saveAs = spyOn(component.fixture.componentInstance, 'saveFile').and.callFake(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function - - component.fixture.componentInstance.inactiveWorkplaces = 25; - component.fixture.detectChanges(); - - fireEvent.click(component.getByText('Download report', { exact: false })); - - expect(getReport).toHaveBeenCalled(); - expect(saveAs).toHaveBeenCalled(); - }); - }); - - describe('Targeted emails', () => { - it('should display the total number of emails to be sent', async () => { - const component = await setup(); - - component.fixture.componentInstance.totalEmails = 1500; - component.fixture.detectChanges(); - - const numInactiveWorkplaces = component.getByTestId('totalEmails'); - expect(numInactiveWorkplaces.innerHTML).toContain('1,500'); - }); - - it("should display 0 emails to be sent when the group and template haven't been selected", async () => { - const component = await setup(); - - component.fixture.componentInstance.emailGroup = null; - component.fixture.componentInstance.selectedTemplateId = null; - component.fixture.detectChanges(); - - const numInactiveWorkplaces = component.getByTestId('totalEmails'); - expect(numInactiveWorkplaces.innerHTML).toContain('0'); - }); - - it("should disable the Send emails button when the group and template haven't been selected", async () => { - const component = await setup(); - - component.fixture.componentInstance.emailGroup = ''; - component.fixture.componentInstance.selectedTemplateId = ''; - component.fixture.detectChanges(); - - const sendEmailsButton = component.fixture.nativeElement.querySelectorAll('button'); - expect(sendEmailsButton[1].disabled).toBeTruthy(); - }); - - it('should update the total emails when updateTotalEmails() is called', async () => { - const component = await setup(); - const emailCampaignService = TestBed.inject(EmailCampaignService); - const getTargetedTotalEmailsSpy = spyOn(emailCampaignService, 'getTargetedTotalEmails').and.callFake(() => - of({ totalEmails: 1500 }), - ); - - component.fixture.componentInstance.updateTotalEmails('primaryUsers'); - component.fixture.detectChanges(); - - expect(component.fixture.componentInstance.totalEmails).toEqual(1500); - expect(getTargetedTotalEmailsSpy).toHaveBeenCalled(); - }); - - it('should display the template names as options', async () => { - const component = await setup(); - const templates = [ - { - id: 1, - name: 'Template 1', - }, - { - id: 2, - name: 'Template 2', - }, - ]; - - component.fixture.componentInstance.templates = templates; - component.fixture.detectChanges(); - - const templateDropdown = component.getByTestId('selectedTemplateId'); - - expect(templateDropdown.childNodes[1].textContent).toEqual('Template 1'); - expect(templateDropdown.childNodes[2].textContent).toEqual('Template 2'); - }); - - it('should call confirmSendEmails when the "Send emails to selected group" button is clicked', async () => { - const component = await setup(); - - component.fixture.componentInstance.totalEmails = 45; - component.fixture.componentInstance.emailGroup = 'primaryUsers'; - component.fixture.componentInstance.selectedTemplateId = '1'; - component.fixture.detectChanges(); - - fireEvent.click(component.getByText('Send emails to selected group', { exact: true })); - - const dialog = await within(document.body).findByRole('dialog'); - const dialogHeader = within(dialog).getByTestId('send-emails-confirmation-header'); - - expect(dialogHeader).toBeTruthy(); - }); - - [ - { - emails: 1500, - expectedMessage: '1,500 emails have been scheduled to be sent.', - }, - { - emails: 1, - expectedMessage: '1 email has been scheduled to be sent.', - }, - ].forEach(({ emails, expectedMessage }) => { - it('should display an alert when the "Send emails to selected group" button is clicked', async () => { - const component = await setup(); - - component.fixture.componentInstance.totalEmails = emails; - component.fixture.componentInstance.emailGroup = 'primaryUsers'; - component.fixture.componentInstance.selectedTemplateId = '1'; - component.fixture.detectChanges(); - - fireEvent.click(component.getByText('Send emails to selected group', { exact: true })); - - const emailCampaignService = TestBed.inject(EmailCampaignService); - spyOn(emailCampaignService, 'createTargetedEmailsCampaign').and.returnValue( - of({ - emails, - }), - ); - - const addAlert = spyOn(component.fixture.componentInstance.alertService, 'addAlert').and.callThrough(); - - const dialog = await within(document.body).findByRole('dialog'); - within(dialog).getByText('Send emails').click(); - - expect(addAlert).toHaveBeenCalledWith({ - type: 'success', - message: expectedMessage, - }); - component.fixture.detectChanges(); - expect(component.fixture.componentInstance.emailGroup).toEqual(''); - expect(component.fixture.componentInstance.selectedTemplateId).toEqual(''); - }); - }); - }); - - describe('Reports', () => { - it('should download a registration survey report when the "Registration survey" button is clicked', async () => { - const component = await setup(); - - const reportService = TestBed.inject(ReportService); - const getReport = spyOn(reportService, 'getRegistrationSurveyReport').and.callFake(() => of(null)); - const saveAs = spyOn(component.fixture.componentInstance, 'saveFile').and.callFake(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function - - fireEvent.click(component.getByText('Registration survey', { exact: false })); - - expect(getReport).toHaveBeenCalled(); - expect(saveAs).toHaveBeenCalled(); - }); - - it('should download a satisfaction survey report when the "Satisfaction survey" button is clicked', async () => { - const component = await setup(); - - const reportService = TestBed.inject(ReportService); - const getReport = spyOn(reportService, 'getSatisfactionSurveyReport').and.callFake(() => of(null)); - const saveAs = spyOn(component.fixture.componentInstance, 'saveFile').and.callFake(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function - - fireEvent.click(component.getByText('Satisfaction survey', { exact: false })); - - expect(getReport).toHaveBeenCalled(); - expect(saveAs).toHaveBeenCalled(); - }); - - it('should download a delete report when the "Delete report" button is clicked', async () => { - const component = await setup(); - - const reportService = TestBed.inject(ReportService); - const getReport = spyOn(reportService, 'getDeleteReport').and.callFake(() => of(null)); - const saveAs = spyOn(component.fixture.componentInstance, 'saveFile').and.callFake(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function - - fireEvent.click(component.getByText('Delete report', { exact: false })); - - expect(getReport).toHaveBeenCalled(); - expect(saveAs).toHaveBeenCalled(); - }); - - it('should download a local authority progress report when the "Local admin authority progress" button is clicked', async () => { - const component = await setup(); - - const reportService = TestBed.inject(ReportService); - const getReport = spyOn(reportService, 'getLocalAuthorityAdminReport').and.callFake(() => of(null)); - const saveAs = spyOn(component.fixture.componentInstance, 'saveFile').and.callFake(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function - - fireEvent.click(component.getByText('Local admin authority progress', { exact: false })); - - expect(getReport).toHaveBeenCalled(); - expect(saveAs).toHaveBeenCalled(); - }); - - it('should download a WDF Summary report when the "WDF Summary Report" button is clicked', async () => { - const component = await setup(); - - const reportService = TestBed.inject(ReportService); - const getReport = spyOn(reportService, 'getWdfSummaryReport').and.callFake(() => of(null)); - const saveAs = spyOn(component.fixture.componentInstance, 'saveFile').and.callFake(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function - - fireEvent.click(component.getByText('WDF summary report', { exact: false })); - - expect(getReport).toHaveBeenCalled(); - expect(saveAs).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/app/features/search/emails/emails.component.ts b/src/app/features/search/emails/emails.component.ts deleted file mode 100644 index 0ce158ebf2..0000000000 --- a/src/app/features/search/emails/emails.component.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { DecimalPipe } from '@angular/common'; -import { HttpResponse } from '@angular/common/http'; -import { Component, OnDestroy } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { EmailType, TotalEmailsResponse } from '@core/model/emails.model'; -import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; -import { AlertService } from '@core/services/alert.service'; -import { DialogService } from '@core/services/dialog.service'; -import { ReportService } from '@core/services/report.service'; -import { saveAs } from 'file-saver'; -import { Subscription } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; - -import { - SendEmailsConfirmationDialogComponent, -} from './dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component'; - -@Component({ - selector: 'app-emails', - templateUrl: './emails.component.html', -}) -export class EmailsComponent implements OnDestroy { - public inactiveWorkplaces = this.route.snapshot.data.inactiveWorkplaces.inactiveWorkplaces; - public totalEmails = 0; - public emailGroup = ''; - public selectedTemplateId = ''; - public templates = this.route.snapshot.data.emailTemplates.templates; - public history = this.route.snapshot.data.emailCampaignHistory; - public isAdmin: boolean; - public now: Date = new Date(); - private subscriptions: Subscription = new Subscription(); - public emailType = EmailType; - - constructor( - public alertService: AlertService, - public dialogService: DialogService, - private route: ActivatedRoute, - private emailCampaignService: EmailCampaignService, - private decimalPipe: DecimalPipe, - private reportsService: ReportService, - ) {} - - ngOnDestroy(): void { - this.subscriptions.unsubscribe(); - } - - public updateTotalEmails(groupType: string): void { - if (groupType) { - this.subscriptions.add( - this.emailCampaignService - .getTargetedTotalEmails(groupType) - .subscribe((totalEmails: TotalEmailsResponse) => (this.totalEmails = totalEmails.totalEmails)), - ); - } else { - this.totalEmails = 0; - } - } - - public confirmSendEmails(event: Event, emailCount: number, type: EmailType): void { - event.preventDefault(); - - this.subscriptions.add( - this.dialogService - .open(SendEmailsConfirmationDialogComponent, { emailCount }) - .afterClosed.subscribe((hasConfirmed) => { - if (hasConfirmed) { - this.sendEmails(type); - } - }), - ); - } - - private sendEmails(type: EmailType): void { - switch (type) { - case EmailType.InactiveWorkplaces: - this.sendInactiveEmails(); - break; - - case EmailType.TargetedEmails: - this.sendTargetedEmails(); - break; - } - } - - private sendInactiveEmails(): void { - this.subscriptions.add( - this.emailCampaignService - .createInactiveWorkplacesCampaign() - .pipe( - switchMap((latestCampaign) => { - return this.emailCampaignService.getInactiveWorkplaces().pipe( - map(({ inactiveWorkplaces }) => ({ - latestCampaign, - inactiveWorkplaces, - })), - ); - }), - ) - .subscribe(({ latestCampaign, inactiveWorkplaces }) => { - this.history.unshift(latestCampaign); - - this.alertService.addAlert({ - type: 'success', - message: `${this.decimalPipe.transform(latestCampaign.emails)} ${ - latestCampaign.emails > 1 ? 'emails have' : 'email has' - } been scheduled to be sent.`, - }); - - this.inactiveWorkplaces = inactiveWorkplaces; - }), - ); - } - - private sendTargetedEmails(): void { - this.subscriptions.add( - this.emailCampaignService.createTargetedEmailsCampaign(this.emailGroup, this.selectedTemplateId).subscribe(() => { - this.alertService.addAlert({ - type: 'success', - message: `${this.decimalPipe.transform(this.totalEmails)} ${ - this.totalEmails > 1 ? 'emails have' : 'email has' - } been scheduled to be sent.`, - }); - this.emailGroup = ''; - this.selectedTemplateId = ''; - this.totalEmails = 0; - }), - ); - } - - public downloadReport(event: Event): void { - event.preventDefault(); - - this.subscriptions.add( - this.emailCampaignService.getInactiveWorkplacesReport().subscribe((response) => { - this.saveFile(response); - }), - ); - } - - public downloadRegistrationSurveyReport(event: Event): void { - event.preventDefault(); - this.subscriptions.add( - this.reportsService.getRegistrationSurveyReport().subscribe((response) => { - this.saveFile(response); - }), - ); - } - - public downloadSatisfactionSurveyReport(event: Event) { - event.preventDefault(); - this.subscriptions.add( - this.reportsService.getSatisfactionSurveyReport().subscribe((response) => { - this.saveFile(response); - }), - ); - } - - public downloadLocalAuthorityAdminReport(event: Event) { - event.preventDefault(); - this.subscriptions.add( - this.reportsService.getLocalAuthorityAdminReport().subscribe((response) => this.saveFile(response)), - ); - } - - public downloadDeleteReport(event: Event) { - event.preventDefault(); - this.subscriptions.add(this.reportsService.getDeleteReport().subscribe((response) => this.saveFile(response))); - } - - public downloadWdfSummaryReport(event: Event) { - event.preventDefault(); - this.subscriptions.add(this.reportsService.getWdfSummaryReport().subscribe((response) => this.saveFile(response))); - } - - public saveFile(response: HttpResponse) { - const filenameRegEx = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; - const header = response.headers.get('content-disposition'); - const filenameMatches = header && header.match(filenameRegEx); - const filename = filenameMatches && filenameMatches.length > 1 ? filenameMatches[1] : null; - const blob = new Blob([response.body], { type: 'text/plain;charset=utf-8' }); - - saveAs(blob, filename); - } -} diff --git a/src/app/features/search/parent-request/parent-confirmation-dialog.component.html b/src/app/features/search/parent-request/parent-confirmation-dialog.component.html deleted file mode 100644 index 3e3b3fb178..0000000000 --- a/src/app/features/search/parent-request/parent-confirmation-dialog.component.html +++ /dev/null @@ -1,10 +0,0 @@ -

- {{data.headingText}} -

- -

{{ data.paragraphText }}

- - - diff --git a/src/app/features/search/parent-request/parent-confirmation-dialog.component.ts b/src/app/features/search/parent-request/parent-confirmation-dialog.component.ts deleted file mode 100644 index 518545077c..0000000000 --- a/src/app/features/search/parent-request/parent-confirmation-dialog.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { DialogComponent } from '@core/components/dialog.component'; -import { Dialog, DIALOG_DATA } from '@core/services/dialog.service'; - -@Component({ - selector: 'app-parent-confirmation-dialog', - templateUrl: './parent-confirmation-dialog.component.html', -}) -export class ParentConfirmationDialogComponent extends DialogComponent { - constructor( - @Inject(DIALOG_DATA) public data: { headingText: string, paragraphText: string, buttonText: string }, - public dialog: Dialog - ) { - super(data, dialog); - } - - public close(confirmed: boolean) { - this.dialog.close(confirmed); - } -} diff --git a/src/app/features/search/parent-request/parent-request.component.html b/src/app/features/search/parent-request/parent-request.component.html deleted file mode 100644 index 532747a816..0000000000 --- a/src/app/features/search/parent-request/parent-request.component.html +++ /dev/null @@ -1,45 +0,0 @@ -

Parent Request from {{ parentRequest.orgName }}

-
- - - - - - - - - - - - - - - - - - -
Requested - {{ parentRequest.requested }} - Username - {{ parentRequest.userName }} -
Workplace ID - {{ parentRequest.workplaceId }} - Workplace - - {{ parentRequest.orgName }} - -
-
- - -
-
-
diff --git a/src/app/features/search/parent-request/parent-request.component.spec.ts b/src/app/features/search/parent-request/parent-request.component.spec.ts deleted file mode 100644 index 441df5abbc..0000000000 --- a/src/app/features/search/parent-request/parent-request.component.spec.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { HttpResponse } from '@angular/common/http'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; -import { WindowRef } from '@core/services/window.ref'; -import { MockFeatureFlagsService } from '@core/test-utils/MockFeatureFlagService'; -import { FeatureFlagsService } from '@shared/services/feature-flags.service'; -import { SharedModule } from '@shared/shared.module'; -import { render, within } from '@testing-library/angular'; -import { of } from 'rxjs'; -import { spy } from 'sinon'; - -import { ParentRequestComponent } from './parent-request.component'; - -const testParentRequestId = 9999; -const testParentRequestUuid = '360c62a1-2e20-410d-a72b-9d4100a11f4e'; -const testUsername = 'Mary Poppins'; -const testOrgname = 'Fawlty Towers'; -const testUserId = 1111; -const testEstablishmentId = 2222; -const testEstablishmentUid = '9efce151-6167-4e99-9cbf-0b9f8ab987fa'; -const testWorkplaceId = 'B1234567'; -const testRequestedDate = new Date(); -const parentRequest = { - requestId: testParentRequestId, - requestUUID: testParentRequestUuid, - establishmentId: testEstablishmentId, - establishmentUid: testEstablishmentUid, - userId: testUserId, - workplaceId: testWorkplaceId, - username: testUsername, - orgName: testOrgname, - requested: testRequestedDate, -}; - -const approveButtonText = 'Approve'; -const rejectButtonText = 'Reject'; -const modalApproveText = 'Approve request'; -const modalRejectText = 'Reject request'; - -describe('ParentRequestComponent', () => { - async function setupForSwitchWorkplace() { - const component = await getParentRequestComponent(); - const authToken = 'This is an auth token'; - const swappedEstablishmentData = { - headers: { - get: (header) => { - return header === 'authorization' ? authToken : null; - }, - }, - body: { - establishment: { - uid: testEstablishmentUid, - }, - }, - } as HttpResponse; - const getNewEstablishmentId = spyOn( - component.fixture.componentInstance.switchWorkplaceService, - 'getNewEstablishmentId', - ).and.returnValue(of(swappedEstablishmentData)); - const workplace = { uid: testEstablishmentUid }; - parentRequest.username = testUsername; - - return { - component, - authToken, - workplace, - getNewEstablishmentId, - }; - } - - async function getParentRequestComponent() { - return render(ParentRequestComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule, SharedModule, RouterTestingModule], - providers: [ - { - provide: WindowRef, - useClass: WindowRef, - }, - { provide: FeatureFlagsService, useClass: MockFeatureFlagsService }, - ], - componentProperties: { - index: 0, - removeParentRequest: { - emit: spy(), - } as any, - parentRequest, - }, - }); - } - - async function clickFirstApproveButton() { - const component = await getParentRequestComponent(); - component.getByText(approveButtonText).click(); - const modalConfirmationDialog = await within(document.body).findByRole('dialog'); - return { component, modalConfirmationDialog }; - } - - async function clickFirstRejectButton() { - const component = await getParentRequestComponent(); - component.getByText(rejectButtonText).click(); - const modalConfirmationDialog = await within(document.body).findByRole('dialog'); - return { component, modalConfirmationDialog }; - } - - it('should create', async () => { - // Act - const component = await getParentRequestComponent(); - - // Assert - expect(component).toBeTruthy(); - }); - - it('should be able to approve a become-a-parent request', async () => { - // Arrange - const { component, modalConfirmationDialog } = await clickFirstApproveButton(); - const parentRequestApproval = spyOn( - component.fixture.componentInstance.parentRequestsService, - 'parentApproval', - ).and.callThrough(); - - // Act - within(modalConfirmationDialog).getByText(modalApproveText).click(); - - // Assert - expect(parentRequestApproval).toHaveBeenCalledWith({ - parentRequestId: testParentRequestId, - establishmentId: testEstablishmentId, - userId: testUserId, - rejectionReason: 'Approved', - approve: true, - }); - }); - - it('should be able to reject a become-a-parent request', async () => { - // Arrange - const { component, modalConfirmationDialog } = await clickFirstRejectButton(); - const parentRequestApproval = spyOn( - component.fixture.componentInstance.parentRequestsService, - 'parentApproval', - ).and.callThrough(); - - // Act - within(modalConfirmationDialog).getByText(modalRejectText).click(); - - // Assert - expect(parentRequestApproval).toHaveBeenCalledWith({ - parentRequestId: testParentRequestId, - establishmentId: testEstablishmentId, - userId: testUserId, - rejectionReason: 'Rejected', - approve: false, - }); - }); - - it('should show confirmation modal when approving a request', async () => { - // Arrange - const component = await getParentRequestComponent(); - const confirmationModal = spyOn(component.fixture.componentInstance.dialogService, 'open').and.callThrough(); - - // Act - component.getByText(approveButtonText).click(); - - // Teardown - const modalConfirmationDialog = await within(document.body).findByRole('dialog'); - within(modalConfirmationDialog).getByText(modalApproveText).click(); - - // Assert - expect(confirmationModal).toHaveBeenCalled(); - }); - - it('confirmation modal should display org name when approving a request', async () => { - // Act - const { modalConfirmationDialog } = await clickFirstApproveButton(); - const paragraph = within(modalConfirmationDialog).getByTestId('parent-confirm-para'); - - // Teardown - within(modalConfirmationDialog).getByText(modalApproveText).click(); - - // Assert - expect(paragraph.innerHTML).toContain(`If you do this, ${testOrgname} will become a parent workplace`); - }); - - it('confirmation modal should display org name when rejecting a request', async () => { - // Act - const { modalConfirmationDialog } = await clickFirstRejectButton(); - const paragraph = within(modalConfirmationDialog).getByTestId('parent-confirm-para'); - - // Teardown - within(modalConfirmationDialog).getByText(modalRejectText).click(); - - // Assert - expect(paragraph.innerHTML).toContain(`If you do this, ${testOrgname} will not become a parent workplace`); - }); - - it('confirmation modal should show "Approve request" when approving a request', async () => { - // Act - const { modalConfirmationDialog } = await clickFirstApproveButton(); - const approveHeading = within(modalConfirmationDialog).getByTestId('parent-confirm-heading'); - const submitButton = within(modalConfirmationDialog).getByText(modalApproveText); - - // Teardown - within(modalConfirmationDialog).getByText(modalApproveText).click(); - - // Assert - expect(approveHeading.innerHTML).toContain("You're about to approve this request."); - expect(submitButton).toBeTruthy(); - }); - - it('confirmation modal should show "Reject request" when rejecting a request', async () => { - // Act - const { modalConfirmationDialog } = await clickFirstRejectButton(); - const rejectHeading = within(modalConfirmationDialog).getByTestId('parent-confirm-heading'); - const submitButton = within(modalConfirmationDialog).getByText(modalRejectText); - - // Teardown - within(modalConfirmationDialog).getByText(modalRejectText).click(); - - // Assert - expect(rejectHeading.innerHTML).toContain("You're about to reject this request."); - expect(submitButton).toBeTruthy(); - }); - - it('confirmation message should be shown after approving a request', async () => { - // Arrange - const { component, modalConfirmationDialog } = await clickFirstApproveButton(); - const addAlert = spyOn(component.fixture.componentInstance.alertService, 'addAlert').and.callThrough(); - spyOn(component.fixture.componentInstance.parentRequestsService, 'parentApproval').and.returnValue(of({})); - - // Act - within(modalConfirmationDialog).getByText(modalApproveText).click(); - component.fixture.detectChanges(); - - // Assert - expect(addAlert).toHaveBeenCalledWith({ - type: 'success', - message: `Parent request approved for ${testOrgname}.`, - }); - }); - - it('confirmation message should be shown after rejecting a request', async () => { - // Arrange - const { component, modalConfirmationDialog } = await clickFirstRejectButton(); - const addAlert = spyOn(component.fixture.componentInstance.alertService, 'addAlert').and.callThrough(); - spyOn(component.fixture.componentInstance.parentRequestsService, 'parentApproval').and.returnValue(of({})); - - // Act - within(modalConfirmationDialog).getByText(modalRejectText).click(); - component.fixture.detectChanges(); - - // Assert - expect(addAlert).toHaveBeenCalledWith({ - type: 'success', - message: `Parent request rejected for ${testOrgname}.`, - }); - }); - - it('should load workplace-specific notifications if user name not populated when switching to new workplace.', async () => { - // Arrange - const { component } = await setupForSwitchWorkplace(); - parentRequest.username = null; - const notificationData = { dummyNotification: 'I am a notification' }; - const getAllNotificationWorkplace = spyOn( - component.fixture.componentInstance.switchWorkplaceService, - 'getAllNotificationWorkplace', - ).and.returnValue(of(notificationData)); - - // Act - component.getByText(testOrgname).click(); - component.fixture.detectChanges(); - - // Assert - expect(getAllNotificationWorkplace).toHaveBeenCalled(); - }); - - it('should swap establishments when switching to new workplace', async () => { - const { component, getNewEstablishmentId } = await setupForSwitchWorkplace(); - - // Act - component.getByText(testOrgname).click(); - component.fixture.detectChanges(); - - // Assert - expect(getNewEstablishmentId).toHaveBeenCalled(); - }); -}); diff --git a/src/app/features/search/parent-request/parent-request.component.ts b/src/app/features/search/parent-request/parent-request.component.ts deleted file mode 100644 index 956b7bfd39..0000000000 --- a/src/app/features/search/parent-request/parent-request.component.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { AlertService } from '@core/services/alert.service'; -import { DialogService } from '@core/services/dialog.service'; -import { ParentRequestsService } from '@core/services/parent-requests.service'; -import { SwitchWorkplaceService } from '@core/services/switch-workplace.service'; -import { ParentConfirmationDialogComponent } from '@features/search/parent-request/parent-confirmation-dialog.component'; - -@Component({ - selector: 'app-parent-request', - templateUrl: './parent-request.component.html', -}) -export class ParentRequestComponent implements OnInit { - @Output() removeParentRequest: EventEmitter = new EventEmitter(); - @Input() index: number; - @Input() parentRequest: any; - public parentRequestForm: FormGroup; - public approve: boolean; - public rejectionReason: string; - - constructor( - public parentRequestsService: ParentRequestsService, - public switchWorkplaceService: SwitchWorkplaceService, - public dialogService: DialogService, - public alertService: AlertService, - ) {} - ngOnInit() { - this.parentRequestForm = new FormGroup({}); - } - - get establishmentId() { - return this.parentRequest.establishmentId; - } - - public approveParentRequest(approve: boolean, rejectionReason: string) { - this.approve = approve; - this.rejectionReason = rejectionReason; - - event.preventDefault(); - - this.dialogService - .open(ParentConfirmationDialogComponent, { - orgName: this.parentRequest.orgName, - headingText: approve ? "You're about to approve this request." : "You're about to reject this request.", - paragraphText: approve - ? `If you do this, ${this.parentRequest.orgName} will become a parent workplace.` - : `If you do this, ${this.parentRequest.orgName} will not become a parent workplace.`, - buttonText: approve ? 'Approve request' : 'Reject request', - }) - .afterClosed.subscribe((approveConfirmed) => { - if (approveConfirmed) { - this.approveOrRejectRequest(); - } - }); - } - - public navigateToWorkplace(id, username, nmdsId, e): void { - e.preventDefault(); - this.switchWorkplaceService.navigateToWorkplace(id, username, nmdsId); - } - - public onSubmit() { - // Nothing to do here - it's all done via the confirmation dialog. - } - - private approveOrRejectRequest() { - let data; - data = { - parentRequestId: this.parentRequest.requestId, - establishmentId: this.parentRequest.establishmentId, - userId: this.parentRequest.userId, - rejectionReason: this.rejectionReason, - approve: this.approve, - }; - - this.parentRequestsService.parentApproval(data).subscribe( - () => { - this.removeParentRequest.emit(this.index); - this.showConfirmationMessage(); - }, - (err) => { - if (err instanceof HttpErrorResponse) { - this.populateErrorFromServer(err); - } - }, - ); - } - - private showConfirmationMessage() { - const approvedOrRejected = this.approve ? 'approved' : 'rejected'; - - this.alertService.addAlert({ - type: 'success', - message: `Parent request ${approvedOrRejected} for ${this.parentRequest.orgName}.`, - }); - } - - private populateErrorFromServer(err) { - const validationErrors = err.error; - - Object.keys(validationErrors).forEach((prop) => { - const formControl = this.parentRequestForm.get(prop); - if (formControl) { - formControl.setErrors({ - serverError: validationErrors[prop], - }); - } - }); - } -} diff --git a/src/app/features/search/parent-requests/parent-requests.component.html b/src/app/features/search/parent-requests/parent-requests.component.html deleted file mode 100644 index 1bd182f35b..0000000000 --- a/src/app/features/search/parent-requests/parent-requests.component.html +++ /dev/null @@ -1,6 +0,0 @@ -

- Requests to Become a Parent -

-
- -
diff --git a/src/app/features/search/parent-requests/parent-requests.component.spec.ts b/src/app/features/search/parent-requests/parent-requests.component.spec.ts deleted file mode 100644 index 44df2c2d43..0000000000 --- a/src/app/features/search/parent-requests/parent-requests.component.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { inject, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; -import { ParentRequestsService } from '@core/services/parent-requests.service'; -import { WindowRef } from '@core/services/window.ref'; -import { SharedModule } from '@shared/shared.module'; -import { render, RenderResult } from '@testing-library/angular'; -import { of } from 'rxjs'; - -import { ParentRequestComponent } from '../parent-request/parent-request.component'; -import { ParentRequestsComponent } from './parent-requests.component'; -import { FeatureFlagsService } from '@shared/services/feature-flags.service'; -import { MockFeatureFlagsService } from '@core/test-utils/MockFeatureFlagService'; - -describe('ParentRequestsComponent', () => { - let component: RenderResult; - - it('can get parent requests', () => { - inject([HttpClientTestingModule], async () => { - const parentRequests = [ - { - establishmentId: 1111, - workplaceId: 'I1234567', - userName: 'Magnificent Maisie', - orgName: 'Marvellous Mansions', - requested: '2019-08-27 16:04:35.914', - }, - { - establishmentId: 3333, - workplaceId: 'B9999999', - userName: 'Everso Stupid', - orgName: 'Everly Towers', - requested: '2020-05-20 16:04:35.914', - }, - ]; - - const parentRequestsService = TestBed.inject(ParentRequestsService); - spyOn(parentRequestsService, 'getParentRequests').and.returnValue(of(parentRequests)); - - const { fixture } = await render(ParentRequestsComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule, SharedModule, RouterTestingModule], - declarations: [ParentRequestComponent], - providers: [ - { - provide: WindowRef, - useClass: WindowRef, - }, - { - provide: ParentRequestsService, - useClass: parentRequestsService, - }, - { provide: FeatureFlagsService, - useClass: MockFeatureFlagsService - }, - ], - }); - - const { componentInstance } = fixture; - - expect(componentInstance.parentRequests).toEqual(parentRequests); - }); - }); - - it('should remove parent requests', async () => { - const parentRequests = [ - { - establishmentId: 1111, - workplaceId: 'I1234567', - userName: 'Magnificent Maisie', - orgName: 'Marvellous Mansions', - requested: '2019-08-27 16:04:35.914', - }, - { - establishmentId: 3333, - workplaceId: 'B9999999', - userName: 'Everso Stupid', - orgName: 'Everly Towers', - requested: '2020-05-20 16:04:35.914', - }, - ]; - - const { fixture } = await render(ParentRequestsComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule, SharedModule, RouterTestingModule], - declarations: [ParentRequestComponent], - providers: [ - { - provide: WindowRef, - useClass: WindowRef, - }, - { provide: FeatureFlagsService, - useClass: MockFeatureFlagsService - }, - - ], - componentProperties: { - parentRequests, - }, - }); - - const { componentInstance } = fixture; - - componentInstance.removeParentRequest(0); - - expect(componentInstance.parentRequests).toContain(parentRequests[0]); - expect(componentInstance.parentRequests).not.toContain(parentRequests[1]); - expect(componentInstance.parentRequests.length).toBe(1); - }); -}); diff --git a/src/app/features/search/parent-requests/parent-requests.component.ts b/src/app/features/search/parent-requests/parent-requests.component.ts deleted file mode 100644 index 53f7311f8e..0000000000 --- a/src/app/features/search/parent-requests/parent-requests.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ParentRequestsService } from '@core/services/parent-requests.service'; - -@Component({ - selector: 'app-parent-requests', - templateUrl: './parent-requests.component.html' -}) -export class ParentRequestsComponent implements OnInit { - public parentRequests = []; - - constructor( - public parentRequestsService: ParentRequestsService, - ) {} - - ngOnInit() { - this.getParentRequests(); - } - - public getParentRequests() { - this.parentRequestsService.getParentRequests().subscribe( - data => { - this.parentRequests = data; - }, - error => this.onError(error) - ); - } - - public removeParentRequest(index: number) { - this.parentRequests.splice(index, 1); - } - - private onError(error) {} -} diff --git a/src/app/features/search/registration/registration.component.html b/src/app/features/search/registration/registration.component.html deleted file mode 100644 index bc5ce1ae1a..0000000000 --- a/src/app/features/search/registration/registration.component.html +++ /dev/null @@ -1,142 +0,0 @@ -

Registration from {{ registration.establishment.name }}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Created - {{ registration.created }} - Username - {{ registration.username }} - Parent Establishment ID - {{ registration.establishment.parentEstablishmentId }} -
Name - {{ registration.name }} -
Email - {{ registration.email }} - Phone number - {{ registration.phone }} -
Security question - {{ registration.securityQuestion }} - Security question answer - {{ registration.securityQuestionAnswer }} -
Regulated - {{ registration.establishment.isRegulated }} - Provider ID - {{ registration.establishment.provId }} -
Workplace ID - - - - Error: - - Enter a workplace ID. - - - Workplace ID must be between 1 and 8 characters. - - - Workplace ID must be between 1 and 8 characters. - - - Workplace ID must start with an uppercase letter. - - - {{ nmdsid.errors?.serverError }} - - - Location ID - {{ registration.establishment.locationId }} -
Address - {{ registration.establishment.name }}
- {{ registration.establishment.address }}
- {{ registration.establishment.address2 }}
- {{ registration.establishment.address3 }}
- {{ registration.establishment.town }}
- {{ registration.establishment.postcode }} -
County - {{ registration.establishment.county }} - Main Service - {{ registration.establishment.mainService }} -
-
- - -
-
- - -
-
-
diff --git a/src/app/features/search/registration/registration.component.spec.ts b/src/app/features/search/registration/registration.component.spec.ts deleted file mode 100644 index fac87bc966..0000000000 --- a/src/app/features/search/registration/registration.component.spec.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RegistrationsService } from '@core/services/registrations.service'; -import { FirstErrorPipe } from '@shared/pipes/first-error.pipe'; -import { fireEvent, render } from '@testing-library/angular'; -import userEvent from '@testing-library/user-event'; -import { throwError } from 'rxjs'; -import { spy } from 'sinon'; - -import { RegistrationComponent } from './registration.component'; - -const testUsername = 'mr-twiggy'; -const testEmail = 'mr.twiggy@cats.com'; -const testNmdsId = 'W1234567'; -const testWorkplaceId = 12345; -const newUserAccount = 'New-User-Account'; -const registrationTypeIrrelevant = 'Registration-Type-Irrelevant'; -const workplaceAddedByParent = 'Workplace-Added-By-Parent'; - -describe('RegistrationComponent', () => { - async function getRegistrationComponent(registrationType) { - const registration = { - email: null, - username: null, - establishment: { - id: testWorkplaceId, - nmdsId: testNmdsId, - }, - }; - - if (registrationType === newUserAccount) { - registration.username = testUsername; - registration.email = testEmail; - } - - return await render(RegistrationComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule], - providers: [RegistrationsService], - declarations: [FirstErrorPipe], - componentProperties: { - index: 0, - handleRegistration: { - emit: spy(), - } as any, - registration, - }, - }); - } - - it('should create', async () => { - const component = await getRegistrationComponent(registrationTypeIrrelevant); - - expect(component).toBeTruthy(); - }); - - it('should be able to approve the registration of a workplace created by a parent', async () => { - const { getByText, fixture } = await getRegistrationComponent(workplaceAddedByParent); - - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - fireEvent.click(getByText('Approve')); - - expect(registrationApproval).toHaveBeenCalledWith({ - establishmentId: testWorkplaceId, - nmdsId: testNmdsId, - approve: true, - }); - }); - - it('should be able to reject the registration of a workplace created by a parent', async () => { - const { getByText, fixture } = await getRegistrationComponent(workplaceAddedByParent); - - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - fireEvent.click(getByText('Reject')); - - expect(registrationApproval).toHaveBeenCalledWith({ - establishmentId: testWorkplaceId, - nmdsId: testNmdsId, - approve: false, - }); - }); - - it('should be able to approve the registration of a workplace created via a new user account', async () => { - const { getByText, fixture } = await getRegistrationComponent(newUserAccount); - - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - fireEvent.click(getByText('Approve')); - - expect(registrationApproval).toHaveBeenCalledWith({ - username: testUsername, - nmdsId: testNmdsId, - approve: true, - }); - }); - - it('should be able to reject the registration of a workplace created via a new user account', async () => { - const { getByText, fixture } = await getRegistrationComponent(newUserAccount); - - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - fireEvent.click(getByText('Reject')); - - expect(registrationApproval).toHaveBeenCalledWith({ - username: testUsername, - nmdsId: testNmdsId, - approve: false, - }); - }); - - it('should change the nmdsID for the registration of a Workplace', async () => { - const { getByText, fixture, container } = await getRegistrationComponent(registrationTypeIrrelevant); - - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - const nmdsIdInput = container.querySelector('input[name=nmdsid]') as HTMLElement; - userEvent.clear(nmdsIdInput); - - const newNmdsId = 'G1234567'; - userEvent.type(nmdsIdInput, newNmdsId); - fireEvent.click(getByText('Approve')); - - expect(registrationApproval).toHaveBeenCalledWith({ - establishmentId: testWorkplaceId, - nmdsId: newNmdsId, - approve: true, - }); - }); - - describe('FormValidation', () => { - it('validates a Workplace ID is required', async () => { - const { getByText, fixture, container } = await getRegistrationComponent(registrationTypeIrrelevant); - const { componentInstance: component } = fixture; - - spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - spyOn(component.handleRegistration, 'emit').and.callThrough(); - - const nmdsIdInput = container.querySelector('input[name=nmdsid]') as HTMLElement; - userEvent.clear(nmdsIdInput); - - userEvent.type(nmdsIdInput, ''); - - fireEvent.click(getByText('Approve')); - - expect(getByText('Enter a workplace ID.')); - expect(component.registrationsService.registrationApproval).toHaveBeenCalledTimes(0); - }); - - it('validates the min length of a Workplace ID', async () => { - const { getByText, fixture, container } = await getRegistrationComponent(registrationTypeIrrelevant); - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - const nmdsIdInput = container.querySelector('input[name=nmdsid]') as HTMLElement; - userEvent.clear(nmdsIdInput); - - userEvent.type(nmdsIdInput, 'W123456'); - - fireEvent.click(getByText('Approve')); - - expect(getByText('Workplace ID must be between 1 and 8 characters.')); - expect(registrationApproval).toHaveBeenCalledTimes(0); - }); - - it('validates the max length of a Workplace ID', async () => { - const { getByText, fixture, container } = await getRegistrationComponent(registrationTypeIrrelevant); - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - const nmdsIdInput = container.querySelector('input[name=nmdsid]') as HTMLElement; - userEvent.clear(nmdsIdInput); - - userEvent.type(nmdsIdInput, 'W12345678910'); - - fireEvent.click(getByText('Approve')); - - expect(getByText('Workplace ID must be between 1 and 8 characters.')); - expect(registrationApproval).toHaveBeenCalledTimes(0); - }); - - it('validates that a Workplace ID must start with a letter', async () => { - const { getByText, fixture, container } = await getRegistrationComponent(registrationTypeIrrelevant); - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - const nmdsIdInput = container.querySelector('input[name=nmdsid]') as HTMLElement; - userEvent.clear(nmdsIdInput); - - userEvent.type(nmdsIdInput, '12345678'); - - fireEvent.click(getByText('Approve')); - - expect(getByText('Workplace ID must start with an uppercase letter.')); - expect(registrationApproval).toHaveBeenCalledTimes(0); - }); - - it('validates that a Workplace ID must start with an uppercase letter', async () => { - const { getByText, fixture, container } = await getRegistrationComponent(registrationTypeIrrelevant); - const { componentInstance: component } = fixture; - - const registrationApproval = spyOn(component.registrationsService, 'registrationApproval').and.callThrough(); - - const nmdsIdInput = container.querySelector('input[name=nmdsid]') as HTMLElement; - userEvent.clear(nmdsIdInput); - - userEvent.type(nmdsIdInput, 'w1234567'); - - fireEvent.click(getByText('Approve')); - - expect(getByText('Workplace ID must start with an uppercase letter.')); - expect(registrationApproval).toHaveBeenCalledTimes(0); - }); - - it('validates that a Workplace ID cannot be the same as an existing Workplace ID', async () => { - const { getByText, fixture, container } = await getRegistrationComponent(registrationTypeIrrelevant); - const { componentInstance: component } = fixture; - - const mockErrorResponse = new HttpErrorResponse({ - status: 400, - statusText: 'Bad Request', - error: { - nmdsId: 'This workplace ID (W1234567) belongs to another workplace. Enter a different workplace ID.', - }, - }); - - spyOn(component.registrationsService, 'registrationApproval').and.returnValue(throwError(mockErrorResponse)); - - const nmdsIdInput = container.querySelector('input[name=nmdsid]') as HTMLElement; - userEvent.clear(nmdsIdInput); - - userEvent.type(nmdsIdInput, testNmdsId); - fireEvent.click(getByText('Approve')); - - expect( - getByText(`This workplace ID (${testNmdsId}) belongs to another workplace. Enter a different workplace ID.`), - ); - }); - }); -}); diff --git a/src/app/features/search/registration/registration.component.ts b/src/app/features/search/registration/registration.component.ts deleted file mode 100644 index 686cfc48d2..0000000000 --- a/src/app/features/search/registration/registration.component.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { RegistrationsService } from '@core/services/registrations.service'; - -@Component({ - selector: 'app-registration', - templateUrl: './registration.component.html', -}) -export class RegistrationComponent implements OnInit { - @Output() handleRegistration: EventEmitter = new EventEmitter(); - @Input() index: number; - @Input() registration: any; - public registrationForm: FormGroup; - public username: string; - public approve: boolean; - - constructor(public registrationsService: RegistrationsService) {} - ngOnInit() { - this.registrationForm = new FormGroup({ - nmdsId: new FormControl(this.registration.establishment.nmdsId, [ - Validators.required, - Validators.minLength(8), - Validators.maxLength(8), - ]), - }); - } - - get nmdsid() { - return this.registrationForm.get('nmdsId'); - } - - public approveRegistration(username: string, approve: boolean) { - this.username = username; - this.approve = approve; - } - public onSubmit() { - if (this.registrationForm.valid) { - let data; - data = { - username: this.username, - nmdsId: this.registrationForm.get('nmdsId').value, - approve: this.approve, - }; - - if (!this.registration.email) { - data = { - establishmentId: this.username, - nmdsId: this.registrationForm.get('nmdsId').value, - approve: this.approve, - }; - } - - this.registrationsService.registrationApproval(data).subscribe( - () => { - this.handleRegistration.emit(this.index); - }, - (err) => { - if (err instanceof HttpErrorResponse) { - this.populateErrorFromServer(err); - } - }, - ); - } - } - - private populateErrorFromServer(err) { - const validationErrors = err.error; - - Object.keys(validationErrors).forEach((prop) => { - const formControl = this.registrationForm.get(prop); - if (formControl) { - formControl.setErrors({ - serverError: validationErrors[prop], - }); - } - }); - } -} diff --git a/src/app/features/search/registrations/registrations.component.html b/src/app/features/search/registrations/registrations.component.html deleted file mode 100644 index 8015f5bd7f..0000000000 --- a/src/app/features/search/registrations/registrations.component.html +++ /dev/null @@ -1,8 +0,0 @@ -

Registrations

-
- -
diff --git a/src/app/features/search/registrations/registrations.component.spec.ts b/src/app/features/search/registrations/registrations.component.spec.ts deleted file mode 100644 index a662ad9a33..0000000000 --- a/src/app/features/search/registrations/registrations.component.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { inject, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; -import { RegistrationsService } from '@core/services/registrations.service'; -import { FirstErrorPipe } from '@shared/pipes/first-error.pipe'; -import { render, RenderResult } from '@testing-library/angular'; -import { of } from 'rxjs'; - -import { RegistrationComponent } from '../registration/registration.component'; -import { RegistrationsComponent } from './registrations.component'; - -describe('RegistrationsComponent', () => { - let component: RenderResult; - - it('should create', async () => { - component = await render(RegistrationsComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule], - declarations: [FirstErrorPipe, RegistrationComponent], - providers: [RegistrationsService], - }); - - expect(component).toBeTruthy(); - }); - - it('can get registrations', () => { - inject([HttpClientTestingModule], async () => { - const registrations = [ - { - establishment: { - id: 1, - nmdsId: 'J1234567', - }, - }, - { - establishment: { - id: 2, - nmdsId: 'J5678910', - }, - }, - ]; - - const registrationService = TestBed.inject(RegistrationsService); - spyOn(registrationService, 'getRegistrations').and.returnValue(of(registrations)); - - const { fixture } = await render(RegistrationsComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule], - declarations: [FirstErrorPipe, RegistrationComponent], - providers: [ - { - provide: RegistrationsService, - useClass: registrationService, - }, - ], - }); - - const { componentInstance } = fixture; - - expect(componentInstance.registrations).toEqual(registrations); - }); - }); - - it('should remove registrations', async () => { - const registrations = [ - { - establishment: { - id: 1, - nmdsId: 'J1234567', - }, - }, - { - establishment: { - id: 2, - nmdsId: 'J5678910', - }, - }, - ]; - - const { fixture } = await render(RegistrationsComponent, { - imports: [ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule], - declarations: [FirstErrorPipe, RegistrationComponent], - providers: [RegistrationsService], - componentProperties: { - registrations, - }, - }); - - const { componentInstance } = fixture; - - componentInstance.handleRegistration(0); - - expect(componentInstance.registrations).toContain(registrations[0]); - expect(componentInstance.registrations.length).toBe(1); - }); -}); diff --git a/src/app/features/search/registrations/registrations.component.ts b/src/app/features/search/registrations/registrations.component.ts deleted file mode 100644 index 458df3e355..0000000000 --- a/src/app/features/search/registrations/registrations.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { RegistrationsService } from '@core/services/registrations.service'; - -@Component({ - selector: 'app-registrations', - templateUrl: './registrations.component.html', -}) -export class RegistrationsComponent implements OnInit { - public registrations = []; - - constructor(public registrationsService: RegistrationsService) {} - - ngOnInit() { - this.getRegistrations(); - } - - public getRegistrations() { - this.registrationsService.getAllRegistrations().subscribe( - (data) => { - this.registrations = data; - }, - (error) => this.onError(error), - ); - } - - public handleRegistration(index: number) { - this.registrations.splice(index, 1); - } - - private onError(error) {} -} diff --git a/src/app/features/search/search-routing.module.ts b/src/app/features/search/search-routing.module.ts deleted file mode 100644 index 7eb482a4bf..0000000000 --- a/src/app/features/search/search-routing.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { EmailCampaignHistoryResolver } from '@core/resolvers/admin/email-campaign-history.resolver'; -import { EmailTemplateResolver } from '@core/resolvers/admin/email-template.resolver'; -import { InactiveWorkplacesResolver } from '@core/resolvers/admin/inactive-workplaces.resolver'; - -import { SearchComponent } from './search.component'; - -const routes: Routes = [ - { - path: '', - component: SearchComponent, - data: { title: 'Search' }, - resolve: { - emailCampaignHistory: EmailCampaignHistoryResolver, - inactiveWorkplaces: InactiveWorkplacesResolver, - emailTemplates: EmailTemplateResolver, - }, - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class SearchRoutingModule {} diff --git a/src/app/features/search/search.component.html b/src/app/features/search/search.component.html deleted file mode 100644 index 6599e0ab59..0000000000 --- a/src/app/features/search/search.component.html +++ /dev/null @@ -1,558 +0,0 @@ -
-
-
- - -
-
-
- - {{ form.subTitle }} -

- {{ form.title }} -

-
- - - -
-
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- -
-
- - -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
- - - Information - Your search returned no results. Please refine your search criteria. - -
-
- -

- Your search returned {{ results.length | number }} results. -

- -
- - - - - - - - - - - - - - - - - - - - - - - - -
NameUsernameWorkplace IDPostcode 
{{ item.name }}{{ item.username }} - {{ - item.establishment.nmdsId - }} - Workplace is pending - {{ item.establishment.postcode }} - {{ - workerDetailsLabel[item.uid] ? workerDetailsLabel[item.uid] : 'Open' - }} -
-
- - - - - - - - - - - - - - - -
Location IDWorkplace 
- {{ item.establishment.locationId }} - - {{ item.establishment.name }} -  
-
-
- - - - - - - - - - - - - - - -
Security questionAnswerLocked
- {{ item.securityQuestion }} - - {{ item.securityQuestionAnswer }} - - Yes, unlock - No -
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Workplace namePostcodeWorkplace IDLocation IDProvider ID 
- {{ item.name }} - {{ item.postcode }}{{ item.nmdsId || '-' }}{{ item.locationId || '-' }}- - {{ - workerDetailsLabel[item.uid] ? workerDetailsLabel[item.uid] : 'Open' - }} -
-
- - - - - - - - - - - - - - - - - -
AddressParent IDRegulatedData Owner
- {{ displayAddress(item) }} - - - {{ - item.parent.nmdsId - }} - - - - - - {{ item.isRegulated ? 'Yes' : 'No' }} - - {{ item.dataOwner }} -
-
-
- - - - - - - - - - - - - - - - - -
UsersSecurity questionAnswerLocked
- {{ user.name }} - - {{ user.securityQuestion }} - - {{ user.securityAnswer }} - - Yes, unlock - No -
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - -
Workplace nameWorkplace IDEmployer Type 
- {{ item.name }} - {{ item.nmdsId || '-' }} - {{ item.employerType?.other ? item.employerType.other : item.employerType?.value || '-' }} - - {{ - workerDetailsLabel[item.uid] ? workerDetailsLabel[item.uid] : 'Open' - }} -
-
- - - - - - - - - - - - - - - - - -
AddressParent IDRegulatedData Owner
- {{ displayAddressForGroups(item) }} - - - {{ - item.parent.nmdsId - }} - - - - - - {{ item.isRegulated ? 'Yes' : 'No' }} - - {{ item.dataOwner }} -
-
-
- - - - - - - - - - - - - - - - - -
UsersSecurity questionAnswerLocked
- {{ user.name }} - - {{ user.securityQuestion }} - - {{ user.securityAnswer }} - - Yes, unlock - No -
-
-
-
- - - - - -
-
diff --git a/src/app/features/search/search.component.spec.ts b/src/app/features/search/search.component.spec.ts deleted file mode 100644 index 00f7f64de6..0000000000 --- a/src/app/features/search/search.component.spec.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; -import { RegistrationsService } from '@core/services/registrations.service'; -import { SwitchWorkplaceService } from '@core/services/switch-workplace.service'; -import { WindowRef } from '@core/services/window.ref'; -import { MockFeatureFlagsService } from '@core/test-utils/MockFeatureFlagService'; -import { MockSwitchWorkplaceService } from '@core/test-utils/MockSwitchWorkplaceService'; -import { FeatureFlagsService } from '@shared/services/feature-flags.service'; -import { SharedModule } from '@shared/shared.module'; -import { fireEvent, render, within } from '@testing-library/angular'; -import { of } from 'rxjs'; - -import { SearchComponent } from './search.component'; - -const getSearchComponent = async () => { - return render(SearchComponent, { - detectChanges: false, - imports: [ - FormsModule, - ReactiveFormsModule, - HttpClientTestingModule, - SharedModule, - RouterTestingModule.withRoutes([ - { path: 'search-users', component: SearchComponent }, - { path: 'search-groups', component: SearchComponent }, - ]), - ], - providers: [ - { - provide: WindowRef, - useClass: WindowRef, - }, - { provide: FeatureFlagsService, useClass: MockFeatureFlagsService }, - { - provide: SwitchWorkplaceService, - useClass: MockSwitchWorkplaceService, - }, - [RegistrationsService], - ], - }); -}; - -const setup = async (fixture, navigate, getByText, isLocked = false) => { - const httpTestingController = TestBed.inject(HttpTestingController); - - await navigate('search-users'); - - fixture.detectChanges(); - - fireEvent.click(getByText('Search users')); - - const req = httpTestingController.expectOne('/api/admin/search/users'); - req.flush([ - { - uid: 'ad3bbca7-2913-4ba7-bb2d-01014be5c48f', - name: 'John Doe', - username: 'Username', - securityQuestion: 'Question', - securityQuestionAnswer: 'Answer', - isLocked, - establishment: { - uid: 'ad3bbca7-2913-4ba7-bb2d-01014be5c48f', - name: 'My workplace', - nmdsId: 'G1001376', - }, - }, - ]); - - fixture.detectChanges(); -}; - -describe('SearchComponent', () => { - afterEach(() => { - const httpTestingController = TestBed.inject(HttpTestingController); - httpTestingController.verify(); - }); - - describe('Users', () => { - it('should show the Users tab', async () => { - const { fixture, navigate, getByText } = await getSearchComponent(); - - await navigate('search-users'); - - fixture.detectChanges(); - - expect(getByText('Search for a user')); - }); - - it('should render the search results when searching for a user', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - await setup(fixture, navigate, getByText); - - await within(getByTestId('user-search-results')).getByText('John Doe'); - }); - it("should show a flag when user's workplace is pending", async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - await setup(fixture, navigate, getByText); - - fixture.componentInstance.results[0].establishment.ustatus = 'PENDING'; - fixture.detectChanges(); - - const result = getByTestId('user-search-results').querySelector('img'); - expect(result.src).toContain('flag-orange'); - }); - - it('should expand the User details when clicking Open', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - await setup(fixture, navigate, getByText); - - const searchResults = within(getByTestId('user-search-results')); - fireEvent.click(searchResults.getByText('Open')); - - expect(searchResults.getByText('My workplace')); - }); - - it('should navigate to workplace when clicking workplace id link', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - await setup(fixture, navigate, getByText); - - const switchWorkplaceService = TestBed.inject(SwitchWorkplaceService); - - const spy = spyOn(switchWorkplaceService, 'navigateToWorkplace'); - - const searchResults = within(getByTestId('user-search-results')); - fireEvent.click(searchResults.getByText('G1001376')); - - await expect(spy).toHaveBeenCalled(); - }); - - it('should navigate to workplace when clicking workplace name link', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - await setup(fixture, navigate, getByText); - - const switchWorkplaceService = TestBed.inject(SwitchWorkplaceService); - - const spy = spyOn(switchWorkplaceService, 'navigateToWorkplace'); - - const searchResults = within(getByTestId('user-search-results')); - fireEvent.click(searchResults.getByText('Open')); - fireEvent.click(searchResults.getByText('My workplace')); - - await expect(spy).toHaveBeenCalled(); - }); - - it('should open unlock user dialog when clicking unlock button', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - await setup(fixture, navigate, getByText, true); - - const registrationsService = TestBed.inject(RegistrationsService); - - const spy = spyOn(registrationsService, 'unlockAccount').and.returnValue(of({})); - - const searchResults = within(getByTestId('user-search-results')); - fireEvent.click(searchResults.getByText('Open')); - fireEvent.click(searchResults.getByText('Yes, unlock')); - - const adminUnlockModal = within(document.body).getByRole('dialog'); - const confirm = within(adminUnlockModal).getByText('Unlock account'); - confirm.click(); - - await expect(spy).toHaveBeenCalled(); - }); - }); - - describe('Groups', () => { - it('should show the Groups tab', async () => { - const { fixture, navigate, getByText } = await getSearchComponent(); - - await navigate('search-groups'); - - fixture.detectChanges(); - - expect(getByText('Search for a group')); - }); - - it('should render the search results when searching for a group', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - const httpTestingController = TestBed.inject(HttpTestingController); - - await navigate('search-groups'); - - fixture.detectChanges(); - - fireEvent.click(getByText('Search groups')); - - const req = httpTestingController.expectOne('/api/admin/search/groups'); - req.flush([ - { - uid: 'ad3bbca7-2913-4ba7-bb2d-01014be5c48f', - name: 'Workplace Name', - }, - ]); - - fixture.detectChanges(); - - await within(getByTestId('group-search-results')).getByText('Workplace Name'); - }); - - it('should expand the Workplace details when clicking Open', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - const httpTestingController = TestBed.inject(HttpTestingController); - - await navigate('search-groups'); - - fixture.detectChanges(); - - fireEvent.click(getByText('Search groups')); - - const req = httpTestingController.expectOne('/api/admin/search/groups'); - req.flush([ - { - uid: 'ad3bbca7-2913-4ba7-bb2d-01014be5c48f', - name: 'Workplace Name', - address1: '44', - address2: 'Grace St', - town: 'Leeds', - county: 'West Yorkshire', - postcode: 'WF14 9TS', - }, - ]); - - fixture.detectChanges(); - - const searchResults = within(getByTestId('group-search-results')); - fireEvent.click(searchResults.getByText('Open')); - - expect(searchResults.getByText('44 Grace St, Leeds, West Yorkshire, WF14 9TS')); - }); - - it('should collapse the Workplace details when clicking Close', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - const httpTestingController = TestBed.inject(HttpTestingController); - - await navigate('search-groups'); - - fixture.detectChanges(); - - fireEvent.click(getByText('Search groups')); - - const req = httpTestingController.expectOne('/api/admin/search/groups'); - req.flush([ - { - uid: 'ad3bbca7-2913-4ba7-bb2d-01014be5c48f', - name: 'Workplace Name', - address1: '44', - address2: 'Grace St', - town: 'Leeds', - county: 'West Yorkshire', - postcode: 'WF14 9TS', - }, - ]); - - fixture.detectChanges(); - - const searchResults = within(getByTestId('group-search-results')); - fireEvent.click(searchResults.getByText('Open')); - fireEvent.click(searchResults.getByText('Close')); - - expect(searchResults.queryByTestId('groups-workplace-details')).toBeNull(); - }); - - it('should show a warning when there are no search results', async () => { - const { fixture, navigate, getByText, getByTestId } = await getSearchComponent(); - - const httpTestingController = TestBed.inject(HttpTestingController); - - await navigate('search-groups'); - - fixture.detectChanges(); - - fireEvent.click(getByText('Search groups')); - - const req = httpTestingController.expectOne('/api/admin/search/groups'); - req.flush([]); - - fixture.detectChanges(); - - expect(getByTestId('no-search-results')); - }); - }); - - describe('Emails', () => { - it('should click the Emails tab', async () => { - const { getByText } = await getSearchComponent(); - - fireEvent.click(getByText('Emails')); - }); - }); -}); diff --git a/src/app/features/search/search.component.ts b/src/app/features/search/search.component.ts deleted file mode 100644 index cd2632b66f..0000000000 --- a/src/app/features/search/search.component.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Overlay } from '@angular/cdk/overlay'; -import { HttpClient } from '@angular/common/http'; -import { AfterViewInit, Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { AuthService } from '@core/services/auth.service'; -import { BackService } from '@core/services/back.service'; -import { DialogService } from '@core/services/dialog.service'; -import { SwitchWorkplaceService } from '@core/services/switch-workplace.service'; -import { - AdminUnlockConfirmationDialogComponent, -} from '@shared/components/admin-unlock-confirmation/admin-unlock-confirmation'; - -@Component({ - selector: 'app-search', - templateUrl: './search.component.html', - providers: [DialogService, AdminUnlockConfirmationDialogComponent, Overlay], -}) -export class SearchComponent implements OnInit, AfterViewInit { - public results = []; - public selectedWorkplaceUid: string; - public form = { - type: '', - title: '', - subTitle: '', - buttonText: '', - valid: true, - submitted: false, - username: '', - usernameLabel: '', - name: '', - nameLabel: '', - locationId: '', - employerType: 'All', - parent: false, - errors: [], - }; - - public workerDetails = []; - public workerDetailsLabel = []; - - constructor( - public router: Router, - public http: HttpClient, - public switchWorkplaceService: SwitchWorkplaceService, - private dialogService: DialogService, - protected backService: BackService, - protected authService: AuthService, - ) {} - - ngAfterViewInit() { - this.authService.isOnAdminScreen = true; - } - - ngOnInit() { - this.setBackLink(); - - if (this.router.url === '/search-users') { - this.form.type = 'users'; - this.form.usernameLabel = 'Username'; - this.form.nameLabel = 'Name'; - this.form.title = 'Search for a user'; - this.form.buttonText = 'Search users'; - } else if (this.router.url === '/search-establishments') { - this.form.type = 'establishments'; - this.form.usernameLabel = 'Postcode'; - this.form.nameLabel = 'Workplace ID'; - this.form.title = 'Search for a workplace'; - this.form.buttonText = 'Search workplaces'; - } else if (this.router.url === '/search-groups') { - this.form.type = 'groups'; - this.form.title = 'Search for a group'; - this.form.buttonText = 'Search groups'; - } else if (this.router.url === '/registrations') { - this.form.type = 'registrations'; - } else if (this.router.url === '/cqc-status-changes') { - this.form.type = 'cqc-status-changes'; - } else if (this.router.url === '/parent-requests') { - this.form.type = 'parent-requests'; - } else if (this.router.url === '/emails') { - this.form.type = 'emails'; - } - } - - public unlockUser(username: string, index: number, e) { - e.preventDefault(); - const data = { - username, - index, - removeUnlock: () => { - this.results[index].isLocked = false; - }, - }; - this.dialogService.open(AdminUnlockConfirmationDialogComponent, data); - } - - public unlockWorkplaceUser(username: string, workplaceIndex: number, userIndex: number, e) { - e.preventDefault(); - const data = { - username, - removeUnlock: () => { - this.results[workplaceIndex].users[userIndex].isLocked = false; - }, - }; - - this.dialogService.open(AdminUnlockConfirmationDialogComponent, data); - } - - public searchType(data, type) { - return this.http.post('/api/admin/search/' + type, data, { observe: 'response' }); - } - - public setEstablishmentId(id, username, nmdsId, e): void { - e.preventDefault(); - this.switchWorkplaceService.navigateToWorkplace(id, username, nmdsId); - } - - public onSubmit(): void { - this.form.errors = []; - this.form.submitted = true; - // this.errorSummaryService.syncFormErrorsEvent.next(true); - - if ( - this.form.username.length === 0 && - this.form.name.length === 0 && - this.form.locationId.length === 0 && - this.form.employerType.length === 0 - ) { - this.form.errors.push({ - error: 'Please enter at least 1 search value', - id: 'username', - }); - this.form.submitted = false; - } else { - let data = {}; - - if (this.form.type === 'users') { - data = { - username: this.form.username, - name: this.form.name, - }; - } else if (this.form.type === 'groups') { - data = { - employerType: this.form.employerType, - parent: this.form.parent, - }; - } else { - data = { - postcode: this.form.username, - nmdsId: this.form.name, - locationId: this.form.locationId, - }; - } - - this.searchType(data, this.form.type).subscribe( - (response) => this.onSuccess(response), - (error) => this.onError(error), - ); - } - } - - private onSuccess(data) { - this.results = data.body; - } - - private onError(error) {} - - protected setBackLink(): void { - this.backService.setBackLink({ url: ['/dashboard'] }); - } - - protected displayAddress(workplace) { - const secondaryAddress = - ' ' + [workplace.address2, workplace.town, workplace.county].filter(Boolean).join(', ') || ''; - - return workplace.address1 + secondaryAddress; - } - - protected displayAddressForGroups(workplace) { - const secondaryAddress = - ' ' + [workplace.address2, workplace.town, workplace.county, workplace.postcode].filter(Boolean).join(', ') || ''; - - return workplace.address1 + secondaryAddress; - } - - public toggleDetails(uid: string, event) { - event.preventDefault(); - this.workerDetails[uid] = !this.workerDetails[uid]; - this.workerDetailsLabel[uid] = this.workerDetailsLabel[uid] === 'Close' ? 'Open' : 'Close'; - } -} diff --git a/src/app/features/search/search.module.ts b/src/app/features/search/search.module.ts deleted file mode 100644 index 25d00fbdca..0000000000 --- a/src/app/features/search/search.module.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { OverlayModule } from '@angular/cdk/overlay'; -import { CommonModule, DecimalPipe } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { EmailCampaignHistoryResolver } from '@core/resolvers/admin/email-campaign-history.resolver'; -import { EmailTemplateResolver } from '@core/resolvers/admin/email-template.resolver'; -import { InactiveWorkplacesResolver } from '@core/resolvers/admin/inactive-workplaces.resolver'; -import { EmailCampaignService } from '@core/services/admin/email-campaign.service'; -import { DialogService } from '@core/services/dialog.service'; -import { - AdminUnlockConfirmationDialogComponent, -} from '@shared/components/admin-unlock-confirmation/admin-unlock-confirmation'; -import { SharedModule } from '@shared/shared.module'; - -import { CqcStatusChangeComponent } from './cqc-status-change/cqc-status-change.component'; -import { CqcStatusChangesComponent } from './cqc-status-changes/cqc-status-changes.component'; -import { - SendEmailsConfirmationDialogComponent, -} from './emails/dialogs/send-emails-confirmation-dialog/send-emails-confirmation-dialog.component'; -import { EmailsComponent } from './emails/emails.component'; -import { ParentRequestComponent } from './parent-request/parent-request.component'; -import { ParentRequestsComponent } from './parent-requests/parent-requests.component'; -import { RegistrationComponent } from './registration/registration.component'; -import { RegistrationsComponent } from './registrations/registrations.component'; -import { SearchRoutingModule } from './search-routing.module'; -import { SearchComponent } from './search.component'; - -@NgModule({ - imports: [CommonModule, OverlayModule, ReactiveFormsModule, SharedModule, SearchRoutingModule, FormsModule], - providers: [ - DialogService, - EmailCampaignHistoryResolver, - InactiveWorkplacesResolver, - EmailCampaignService, - DecimalPipe, - EmailTemplateResolver, - ], - declarations: [ - SearchComponent, - AdminUnlockConfirmationDialogComponent, - RegistrationComponent, - RegistrationsComponent, - ParentRequestComponent, - ParentRequestsComponent, - CqcStatusChangeComponent, - CqcStatusChangesComponent, - EmailsComponent, - SendEmailsConfirmationDialogComponent, - ], -}) -export class SearchModule {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index e745cc7529..f896e492e5 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -9,9 +9,9 @@ import { ArticleListResolver } from '@core/resolvers/article-list.resolver'; import { PageResolver } from '@core/resolvers/page.resolver'; import { DialogService } from '@core/services/dialog.service'; import { ArticleListComponent } from '@features/articles/article-list/article-list.component'; -import { CqcConfirmationDialogComponent } from '@features/search/cqc-status-change/cqc-confirmation-dialog.component'; -import { ParentConfirmationDialogComponent } from '@features/search/parent-request/parent-confirmation-dialog.component'; -import { DeleteWorkplaceDialogComponent } from '@features/workplace/delete-workplace-dialog/delete-workplace-dialog.component'; +import { + DeleteWorkplaceDialogComponent, +} from '@features/workplace/delete-workplace-dialog/delete-workplace-dialog.component'; import { AlertComponent } from '@shared/components/alert/alert.component'; import { CheckCQCDetailsComponent } from '@shared/components/check-cqc-details/check-cqc-details.component'; import { SummaryRecordValueComponent } from '@shared/components/summary-record-value/summary-record-value.component'; @@ -29,30 +29,42 @@ import { ChangeDataOwnerDialogComponent } from './components/change-data-owner-d import { CharacterCountComponent } from './components/character-count/character-count.component'; import { DatePickerComponent } from './components/date-picker/date-picker.component'; import { DetailsComponent } from './components/details/details.component'; -import { ValidationErrorMessageComponent } from './components/drag-and-drop/validation-error-message/validation-error-message.component'; +import { + ValidationErrorMessageComponent, +} from './components/drag-and-drop/validation-error-message/validation-error-message.component'; import { EligibilityIconComponent } from './components/eligibility-icon/eligibility-icon.component'; import { ErrorSummaryComponent } from './components/error-summary/error-summary.component'; import { InsetTextComponent } from './components/inset-text/inset-text.component'; -import { LinkToParentCancelDialogComponent } from './components/link-to-parent-cancel/link-to-parent-cancel-dialog.component'; -import { LinkToParentRemoveDialogComponent } from './components/link-to-parent-remove/link-to-parent-remove-dialog.component'; +import { + LinkToParentCancelDialogComponent, +} from './components/link-to-parent-cancel/link-to-parent-cancel-dialog.component'; +import { + LinkToParentRemoveDialogComponent, +} from './components/link-to-parent-remove/link-to-parent-remove-dialog.component'; import { LinkToParentDialogComponent } from './components/link-to-parent/link-to-parent-dialog.component'; import { MessagesComponent } from './components/messages/messages.component'; import { MoveWorkplaceDialogComponent } from './components/move-workplace/move-workplace-dialog.component'; -import { OwnershipChangeMessageDialogComponent } from './components/ownership-change-message/ownership-change-message-dialog.component'; +import { + OwnershipChangeMessageDialogComponent, +} from './components/ownership-change-message/ownership-change-message-dialog.component'; import { PageComponent } from './components/page/page.component'; import { PaginationComponent } from './components/pagination/pagination.component'; import { PanelComponent } from './components/panel/panel.component'; import { PhaseBannerComponent } from './components/phase-banner/phase-banner.component'; import { ProgressComponent } from './components/progress/progress.component'; import { RejectRequestDialogComponent } from './components/reject-request-dialog/reject-request-dialog.component'; -import { RemoveParentConfirmationComponent } from './components/remove-parent-confirmation/remove-parent-confirmation.component'; +import { + RemoveParentConfirmationComponent, +} from './components/remove-parent-confirmation/remove-parent-confirmation.component'; import { ReviewCheckboxComponent } from './components/review-checkbox/review-checkbox.component'; import { SearchInputComponent } from './components/search-input/search-input.component'; import { SetDataPermissionDialogComponent } from './components/set-data-permission/set-data-permission-dialog.component'; import { BasicRecordComponent } from './components/staff-record-summary/basic-record/basic-record.component'; import { EmploymentComponent } from './components/staff-record-summary/employment/employment.component'; import { PersonalDetailsComponent } from './components/staff-record-summary/personal-details/personal-details.component'; -import { QualificationsAndTrainingComponent } from './components/staff-record-summary/qualifications-and-training/qualifications-and-training.component'; +import { + QualificationsAndTrainingComponent, +} from './components/staff-record-summary/qualifications-and-training/qualifications-and-training.component'; import { StaffRecordSummaryComponent } from './components/staff-record-summary/staff-record-summary.component'; import { StaffRecordsTabComponent } from './components/staff-records-tab/staff-records-tab.component'; import { StaffSummaryComponent } from './components/staff-summary/staff-summary.component'; @@ -65,15 +77,23 @@ import { TabComponent } from './components/tabs/tab.component'; import { TabsComponent } from './components/tabs/tabs.component'; import { TotalStaffPanelComponent } from './components/total-staff-panel/total-staff-panel.component'; import { TotalStaffComponent } from './components/total-staff/total-staff.component'; -import { TrainingAndQualificationsCategoriesComponent } from './components/training-and-qualifications-categories/training-and-qualifications-categories.component'; -import { TrainingAndQualificationsSummaryComponent } from './components/training-and-qualifications-summary/training-and-qualifications-summary.component'; -import { TrainingAndQualificationsTabComponent } from './components/training-and-qualifications-tab/training-and-qualifications-tab.component'; +import { + TrainingAndQualificationsCategoriesComponent, +} from './components/training-and-qualifications-categories/training-and-qualifications-categories.component'; +import { + TrainingAndQualificationsSummaryComponent, +} from './components/training-and-qualifications-summary/training-and-qualifications-summary.component'; +import { + TrainingAndQualificationsTabComponent, +} from './components/training-and-qualifications-tab/training-and-qualifications-tab.component'; import { TrainingInfoPanelComponent } from './components/training-info-panel/training-info-panel.component'; import { TrainingLinkPanelComponent } from './components/training-link-panel/training-link-panel.component'; import { UserAccountsSummaryComponent } from './components/user-accounts-summary/user-accounts-summary.component'; import { WdfConfirmationPanelComponent } from './components/wdf-confirmation-panel/wdf-confirmation-panel.component'; import { WdfFieldConfirmationComponent } from './components/wdf-field-confirmation/wdf-field-confirmation.component'; -import { WdfStaffMismatchMessageComponent } from './components/wdf-staff-mismatch-message/wdf-staff-mismatch-message.component'; +import { + WdfStaffMismatchMessageComponent, +} from './components/wdf-staff-mismatch-message/wdf-staff-mismatch-message.component'; import { WdfTabComponent } from './components/wdf-tab/wdf-tab.component'; import { WhyCollectingFluJabComponent } from './components/why-collecting-flu-jab/why-collecting-flu-jab.component'; import { WorkplaceSummaryComponent } from './components/workplace-summary/workplace-summary.component'; @@ -163,8 +183,8 @@ import { WorkplacePermissionsBearerPipe } from './pipes/workplace-permissions-be BecomeAParentDialogComponent, OwnershipChangeMessageDialogComponent, DeleteWorkplaceDialogComponent, - ParentConfirmationDialogComponent, - CqcConfirmationDialogComponent, + // ParentConfirmationDialogComponent, + // CqcConfirmationDialogComponent, TotalStaffComponent, MoveWorkplaceDialogComponent, WdfFieldConfirmationComponent, @@ -248,8 +268,8 @@ import { WorkplacePermissionsBearerPipe } from './pipes/workplace-permissions-be BecomeAParentDialogComponent, OwnershipChangeMessageDialogComponent, DeleteWorkplaceDialogComponent, - ParentConfirmationDialogComponent, - CqcConfirmationDialogComponent, + // ParentConfirmationDialogComponent, + // CqcConfirmationDialogComponent, TotalStaffComponent, BulkUploadFileTypePipePipe, SanitizeVideoUrlPipe,