From 10188af9baf47622a0e6c55973b88c19b86420d0 Mon Sep 17 00:00:00 2001 From: warrensearle Date: Wed, 31 Mar 2021 09:38:26 +0100 Subject: [PATCH] #1212 Enable leadership judge assessment (#1239) * #1212 Enable leadership judge assessment * #1212 Amend timeline and downloads * Include preview urls * Include helpersTMP for local working copy helpers * Include npm scope for jac-uk * Use jac-kit@0.0.25 --- .../firebase-hosting-pull-request.yml | 42 ++- .npmrc | 2 +- package.json | 2 +- .../ModalViews/IndependentAssessorChange.vue | 3 + .../ModalViews/LeadershipJudgeDetails.vue | 119 +++++++ src/helpersTMP/Timeline/exerciseTimeline.js | 303 ++++++++++++++++++ src/helpersTMP/date.js | 79 +++++ src/store/exercise/document.js | 4 + .../Exercises/Applications/Application.vue | 124 ++++++- .../Exercises/Edit/AssessmentOptions.vue | 25 ++ src/views/Exercises/Edit/Downloads.vue | 116 ++++--- src/views/Exercises/Edit/Timeline.vue | 58 ++-- src/views/Exercises/Show.vue | 8 +- .../Exercises/Show/AssessmentOptions.vue | 17 + src/views/Exercises/Show/Downloads.vue | 6 +- src/views/Exercises/Show/Timeline.vue | 2 +- .../views/Exercises/Show/Downloads.spec.js | 1 + 17 files changed, 819 insertions(+), 92 deletions(-) create mode 100644 src/components/ModalViews/LeadershipJudgeDetails.vue create mode 100644 src/helpersTMP/Timeline/exerciseTimeline.js create mode 100644 src/helpersTMP/date.js diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index 00611bc6c..f4795b827 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -1,17 +1,49 @@ -# This file was auto-generated by the Firebase CLI -# https://github.com/firebase/firebase-tools +# Builds and deploys to firebase hosting preview channel. -name: Deploy to Firebase Hosting on PR +name: Preview pull request 'on': pull_request jobs: - build_and_preview: + preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: potiuk/cancel-workflow-runs@master + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - uses: actions/setup-node@master + - name: Installing project dependencies + run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > ~/.npmrc && npm ci + - name: Create env file + run: | + touch .env.develop + echo "NODE_ENV=production" > .env.develop + echo "VUE_APP_FIREBASE_API_KEY=${{ secrets.DEVELOP_FIREBASE_API_KEY }}" >> .env.develop + echo "VUE_APP_FIREBASE_AUTH_DOMAIN=${{ secrets.DEVELOP_FIREBASE_AUTH_DOMAIN }}" >> .env.develop + echo "VUE_APP_FIREBASE_DATABASE_URL=${{ secrets.DEVELOP_FIREBASE_DATABASE_URL }}" >> .env.develop + echo "VUE_APP_FIREBASE_PROJECT_ID=${{ secrets.DEVELOP_FIREBASE_PROJECT_ID }}" >> .env.develop + echo "VUE_APP_FIREBASE_STORAGE_BUCKET=${{ secrets.DEVELOP_FIREBASE_STORAGE_BUCKET }}" >> .env.develop + echo "VUE_APP_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.DEVELOP_FIREBASE_MESSAGING_SENDER_ID }}" >> .env.develop + echo "VUE_APP_FIREBASE_APP_ID=${{ secrets.DEVELOP_FIREBASE_APP_ID }}" >> .env.develop + cat .env.develop + - name: Building the project + run: npm run build-develop - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: '${{ secrets.GITHUB_TOKEN }}' - firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_DIGITAL_PLATFORM_DEVELOP }}' + firebaseServiceAccount: '${{ secrets.DEVELOP_FIREBASE_SERVICE_ACCOUNT }}' projectId: digital-platform-develop + target: develop-admin-app env: FIREBASE_CLI_PREVIEWS: hostingchannels diff --git a/.npmrc b/.npmrc index 5477abf66..18b5fb82d 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -registry=https://npm.pkg.github.com/jac-uk +@jac-uk:registry=https://npm.pkg.github.com diff --git a/package.json b/package.json index 2f42544a7..8a141235e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "dependencies": { "@ckeditor/ckeditor5-build-classic": "^16.0.0", "@ckeditor/ckeditor5-vue": "^1.0.3", - "@jac-uk/jac-kit": "latest", + "@jac-uk/jac-kit": "0.0.25", "@ministryofjustice/frontend": "0.0.17-alpha", "@sentry/browser": "^5.30.0", "@sentry/integrations": "^5.30.0", diff --git a/src/components/ModalViews/IndependentAssessorChange.vue b/src/components/ModalViews/IndependentAssessorChange.vue index 322687d0e..fff3fa1be 100644 --- a/src/components/ModalViews/IndependentAssessorChange.vue +++ b/src/components/ModalViews/IndependentAssessorChange.vue @@ -123,4 +123,7 @@ export default { padding: 0; margin: 0; } + .modal__title { + color: white; + } diff --git a/src/components/ModalViews/LeadershipJudgeDetails.vue b/src/components/ModalViews/LeadershipJudgeDetails.vue new file mode 100644 index 000000000..010999a10 --- /dev/null +++ b/src/components/ModalViews/LeadershipJudgeDetails.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/src/helpersTMP/Timeline/exerciseTimeline.js b/src/helpersTMP/Timeline/exerciseTimeline.js new file mode 100644 index 000000000..be030a526 --- /dev/null +++ b/src/helpersTMP/Timeline/exerciseTimeline.js @@ -0,0 +1,303 @@ +import { isDate, formatDate } from '../date'; + +const getDateString = (date, format) => { + return isDate(date) ? formatDate(date, format) : null; +}; + +const getDateAndTime = (date, startTime) => { + if (!isDate(date) && !isDate(startTime)) { + return null; + } + const result = date; + result.setHours(startTime.getHours(), startTime.getMinutes()); + + return result; +}; + +const getDateAndTimeString = (date, startTime, endTime) => { + const dateString = getDateString(date); + const startTimeString = getDateString(startTime, 'time'); + const endTimeString = getDateString(endTime, 'time'); + + if (!dateString && !startTimeString && !endTimeString) { + return null; + } + + return `${dateString} - ${startTimeString} to ${endTimeString}`; +}; + +const createSelectionDay = (selectionDay) => { + const selectionDayEntry = { + entry: `Selection Day - ${selectionDay.selectionDayLocation}`, + date: selectionDay.selectionDayStart, + dateString: null, + }; + + const selectionDayStart = getDateString(selectionDay.selectionDayStart); + const selectionDayEnd = getDateString(selectionDay.selectionDayEnd); + + if (!selectionDayStart || !selectionDayEnd) { + selectionDayEntry.dateString = ''; + } else if (selectionDayStart !== selectionDayEnd) { + selectionDayEntry.dateString = `${selectionDayStart} to ${selectionDayEnd}`; + } else { + selectionDayEntry.dateString = `${selectionDayStart}`; + } + return selectionDayEntry; +}; + +const createShortlistingMethod = (method, startDate, endDate) => { + const shortlistingMethodEntry = { + entry: `${method}`, + date: startDate, + dateString: null, + }; + + const formattedStartDate = getDateString(startDate); + const formattedEndDate = getDateString(endDate); + + if (!formattedStartDate || !formattedEndDate) { + shortlistingMethodEntry.dateString = ''; + } else if (formattedStartDate !== formattedEndDate) { + shortlistingMethodEntry.dateString = `${formattedStartDate} to ${formattedEndDate}`; + } else { + shortlistingMethodEntry.dateString = `${formattedStartDate}`; + } + + return shortlistingMethodEntry; +}; + +const exerciseTimeline = (data) => { + const timeline = []; + + if (data.applicationOpenDate) { + timeline.push( + { + entry: 'Open for applications', + date: data.applicationOpenDate, + dateString: getDateString(data.applicationOpenDate), + } + ); + } + + if (data.applicationCloseDate) { + timeline.push( + { + entry: 'Closed for applications', + date: data.applicationCloseDate, + dateString: getDateString(data.applicationCloseDate), + } + ); + } + + if (data.shortlistingMethods && data.shortlistingMethods.length > 0) { + if (data.shortlistingOutcomeDate) { + timeline.push( + { + entry: 'Shortlisting outcome', + date: data.shortlistingOutcomeDate, + dateString: getDateString(data.shortlistingOutcomeDate, 'month'), + } + ); + } + + if (data.shortlistingMethods.includes('paper-sift')) { + timeline.push( + createShortlistingMethod('Sift', data.siftStartDate, data.siftEndDate) + ); + } + + if (data.shortlistingMethods.includes('name-blind-paper-sift')) { + timeline.push( + createShortlistingMethod('Name-blind sift', data.nameBlindSiftStartDate, data.nameBlindSiftEndDate) + ); + } + + if (data.shortlistingMethods.includes('telephone-assessment')) { + timeline.push(createShortlistingMethod('Telephone assessment', data.telephoneAssessmentStartDate, data.telephoneAssessmentEndDate)); + } + + if (data.shortlistingMethods.includes('situational-judgement-qualifying-test')) { + if (data.situationalJudgementTestDate) { + timeline.push( + { + entry: 'Situational judgement qualifying test (QT)', + date: getDateAndTime(data.situationalJudgementTestDate, data.situationalJudgementTestStartTime), + dateString: getDateAndTimeString(data.situationalJudgementTestDate, data.situationalJudgementTestStartTime, data.situationalJudgementTestEndTime), + } + ); + } + if (data.situationalJudgementTestOutcome) { + timeline.push( + { + entry: 'Situational judgement QT outcome to candidates', + date: data.situationalJudgementTestOutcome, + dateString: getDateString(data.situationalJudgementTestOutcome), + } + ); + } + } + + if (data.shortlistingMethods.includes('critical-analysis-qualifying-test')) { + if (data.criticalAnalysisTestDate) { + timeline.push( + { + entry: 'Critical analysis qualifying test (QT)', + date: getDateAndTime(data.criticalAnalysisTestDate, data.criticalAnalysisTestStartTime), + dateString: getDateAndTimeString(data.criticalAnalysisTestDate, data.criticalAnalysisTestStartTime, data.criticalAnalysisTestEndTime), + } + ); + } + if (data.criticalAnalysisTestOutcome) { + timeline.push( + { + entry: 'Critical analysis QT outcome to candidates', + date: data.criticalAnalysisTestOutcome, + dateString: getDateString(data.criticalAnalysisTestOutcome), + } + ); + } + } + + if (data.shortlistingMethods.includes('scenario-test-qualifying-test')) { + if (data.scenarioTestDate) { + timeline.push( + { + entry: 'Scenario test', + date: getDateAndTime(data.scenarioTestDate, data.scenarioTestStartTime), + dateString: getDateAndTimeString(data.scenarioTestDate, data.scenarioTestStartTime, data.scenarioTestEndTime), + } + ); + } + if (data.scenarioTestOutcome) { + timeline.push( + { + entry: 'Scenario test outcome to candidates', + date: data.scenarioTestOutcome, + dateString: getDateString(data.scenarioTestOutcome), + } + ); + } + } + } + + if (!(data.assessmentMethods && data.assessmentMethods.independentAssessments === false)) { + if (data.contactIndependentAssessors) { + timeline.push( + { + entry: 'JAC Contacts Independent Assessors', + date: data.contactIndependentAssessors, + dateString: getDateString(data.contactIndependentAssessors), + } + ); + } + + if (data.independentAssessmentsReturnDate) { + timeline.push( + { + entry: 'Return date for independent assessments', + date: data.independentAssessmentsReturnDate, + dateString: getDateString(data.independentAssessmentsReturnDate), + } + ); + } + } + + if (data.eligibilitySCCDate) { + timeline.push( + { + entry: 'Eligibility SCC', + date: data.eligibilitySCCDate, + dateString: getDateString(data.eligibilitySCCDate), + } + ); + } + + if (data.selectionDays && data.selectionDays.length > 0) { + for (let i = 0; i < data.selectionDays.length; i++) { + if (data.selectionDays[i].selectionDayStart) { + timeline.push(createSelectionDay(data.selectionDays[i])); + } + } + } + + if (data.characterChecksDate) { + timeline.push( + { + entry: 'Character Checks', + date: data.characterChecksDate, + dateString: getDateString(data.characterChecksDate), + } + ); + } + + if (data.characterChecksReturnDate) { + timeline.push( + { + entry: 'Character Checks return', + date: data.characterChecksReturnDate, + dateString: getDateString(data.characterChecksReturnDate), + } + ); + } + + if (data.statutoryConsultationDate) { + timeline.push( + { + entry: 'Statutory consultation', + date: data.statutoryConsultationDate, + dateString: getDateString(data.statutoryConsultationDate), + } + ); + } + + if (data.characterAndSCCDate) { + timeline.push( + { + entry: 'Character and Selection SCC', + date: data.characterAndSCCDate, + dateString: getDateString(data.characterAndSCCDate), + } + ); + } + + if (data.finalOutcome) { + timeline.push( + { + entry: 'Selection process outcome', + date: data.finalOutcome, + dateString: getDateString(data.finalOutcome), + } + ); + } + + if (data.equalMeritSecondStageStartDate) { + timeline.push( + createShortlistingMethod('Equal merit second stage', data.equalMeritSecondStageStartDate, data.equalMeritSecondStageEndDate) + ); + } + + if (data.eMPSCCDate) { + timeline.push( + { + entry: 'EMP SCC', + date: data.eMPSCCDate, + dateString: getDateString(data.eMPSCCDate), + } + ); + } + + if (data.eMPOutcomeDate) { + timeline.push( + { + entry: 'EMP Outcomes', + date: data.eMPOutcomeDate, + dateString: getDateString(data.eMPOutcomeDate), + } + ); + } + + return timeline; +}; + +export default exerciseTimeline; diff --git a/src/helpersTMP/date.js b/src/helpersTMP/date.js new file mode 100644 index 000000000..c68bc82ce --- /dev/null +++ b/src/helpersTMP/date.js @@ -0,0 +1,79 @@ +const isDate = (date) => date instanceof Date; + +const isDateInFuture = (date) => { + // @TODO #388 update to full datetime instead of hardcoding time + if (!(date instanceof Date)) { + throw 'Supplied date must be a Date object'; + } + + const today = new Date(); + + date = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + 13, + 0, + 0 + ); + + return date > today; +}; + +const isDateGreaterThan = (dateOne, dateTwo) => { + return dateOne > dateTwo; +}; + +// @TODO can this be removed? /src/filters.js contains `formatDate` +const formatDate = (date, type) => { + if (!(date instanceof Date)) { + throw 'Supplied date must be a Date object'; + } + + if (type && type === 'time') { + return date.toLocaleString('en-GB', { hour: 'numeric', minute: 'numeric', hour12: true }).toLowerCase(); + } + + const month = date.toLocaleString('en-GB', { month: 'long' }); + + if (type && type === 'month') { + return `${month} ${date.getFullYear()}`; + } + + return `${date.getDate()} ${month} ${date.getFullYear()}`; +}; + +const parseEstimatedDate = (value) => { + if (value instanceof Date) { + return value; + } + + if (typeof value != 'string') { + return; + } + const parts = value.split('-'); + + const [year, month, day] = [...parts, 1]; + const date = new Date(Date.UTC(year, month - 1, day)); + + return date; +}; + +const validateYear = (val) => { + val = parseInt(val); + + if (isNaN(val) || val.toString().length !== 4) { + return null; + } + + return val; +}; + +export { + isDate, + isDateInFuture, + isDateGreaterThan, + formatDate, + parseEstimatedDate, + validateYear +}; diff --git a/src/store/exercise/document.js b/src/store/exercise/document.js index 791499f0a..72a2bcf9b 100644 --- a/src/store/exercise/document.js +++ b/src/store/exercise/document.js @@ -124,5 +124,9 @@ export default { } } }, + hasIndependentAssessments: (state) => { + if (state.record === null) return true; + return !(state.record.assessmentMethods && state.record.assessmentMethods.independentAssessments === false); + }, }, }; diff --git a/src/views/Exercises/Applications/Application.vue b/src/views/Exercises/Applications/Application.vue index 055a76018..6886ac93d 100644 --- a/src/views/Exercises/Applications/Application.vue +++ b/src/views/Exercises/Applications/Application.vue @@ -1498,8 +1498,11 @@ -
-

+
+

Independent assessors

@@ -1610,7 +1613,103 @@ @close="closeModal('modalRef')" /> +
+ +
+

+ Leadership judge details +

+ +
+ + +
+
+ Full name +
+
+ {{ application.leadershipJudgeDetails.fullName }} +
+
+ +
+
+ Title or position +
+
+ {{ application.leadershipJudgeDetails.title }} +
+
+ + + + +
+
+
+ No information for Leadership Judge +
+
+ +
+
+ + + +
+
+ + Independent Assessments + + + Leadership Judge Assessment + + @@ -81,6 +94,7 @@ import ErrorSummary from '@jac-uk/jac-kit/draftComponents/Form/ErrorSummary'; import BackLink from '@jac-uk/jac-kit/draftComponents/BackLink'; import RadioGroup from '@jac-uk/jac-kit/draftComponents/Form/RadioGroup'; import RadioItem from '@jac-uk/jac-kit/draftComponents/Form/RadioItem'; +import Checkbox from '@jac-uk/jac-kit/draftComponents/Form/Checkbox'; export default { components: { @@ -88,11 +102,22 @@ export default { BackLink, RadioGroup, RadioItem, + Checkbox, }, extends: Form, data(){ const defaults = { assessmentOptions: null, + // TODO: `assessmentMethods` will eventually replace `assessmentOptions` + assessmentMethods: { + independentAssessments: true, + leadershipJudgeAssessment: false, + // selfAssessment: false, + // statementOfEligibility: false, + // statementOfSuitability: false, + // coveringLetter: false, + // cv: false, + }, }; const data = this.$store.getters['exerciseDocument/data'](); const exercise = { ...defaults, ...data }; diff --git a/src/views/Exercises/Edit/Downloads.vue b/src/views/Exercises/Edit/Downloads.vue index 29881b8a6..ddb3f7dd5 100644 --- a/src/views/Exercises/Edit/Downloads.vue +++ b/src/views/Exercises/Edit/Downloads.vue @@ -147,67 +147,79 @@ export default { }), ...mapGetters('exerciseDocument', { exerciseId: 'id', + hasIndependentAssessments: 'hasIndependentAssessments', }), uploadPath() { return `/exercise/${this.exerciseId}`; }, uploadList() { - const data = [ - { - title: 'Job Description', - id: 'jobDescriptions', - name: 'job-descriptions', - mandatory: true, - }, - { - title: 'Terms and Conditions', - id: 'termsAndConditions', - name: 'terms-and-conditions', - mandatory: true, - }, - { - title: 'Competency Framework', - id: 'competencyFramework', - name: 'competency-framework', - mandatory: false, - }, - { - title: 'Pensions Information', - id: 'pensionsInformation', - name: 'pensions-information', - mandatory: false, - }, - { - title: 'Skills and Abilities Criteria', - id: 'skillsAndAbilitiesCriteria', - name: 'skills-and-abilities-criteria', - mandatory: false, - }, - { + const data = []; + + data.push({ + title: 'Job Description', + id: 'jobDescriptions', + name: 'job-descriptions', + mandatory: true, + }); + + data.push({ + title: 'Terms and Conditions', + id: 'termsAndConditions', + name: 'terms-and-conditions', + mandatory: true, + }); + + data.push({ + title: 'Competency Framework', + id: 'competencyFramework', + name: 'competency-framework', + mandatory: false, + }); + + data.push({ + title: 'Pensions Information', + id: 'pensionsInformation', + name: 'pensions-information', + mandatory: false, + }); + + data.push({ + title: 'Skills and Abilities Criteria', + id: 'skillsAndAbilitiesCriteria', + name: 'skills-and-abilities-criteria', + mandatory: false, + }); + + if (this.hasIndependentAssessments) { + data.push({ title: 'Independent Assessors', id: 'independentAssessors', name: 'independent-assessors', mandatory: true, - }, - { - title: 'Candidate Assessment Form', - id: 'candidateAssessementForms', - name: 'candidate-assessement-forms', - mandatory: false, - }, - { - title: 'Welsh Translation', - id: 'welshTranslation', - name: 'welsh-translation', - mandatory: false, - }, - { - title: 'Other Downloads', - id: 'otherDownloads', - name: 'other-downloads', - mandatory: false, - }, - ]; + }); + } + + data.push({ + title: 'Candidate Assessment Form', + id: 'candidateAssessementForms', + name: 'candidate-assessement-forms', + mandatory: false, + }); + + data.push({ + title: 'Welsh Translation', + id: 'welshTranslation', + name: 'welsh-translation', + mandatory: false, + }); + + data.push({ + title: 'Other Downloads', + id: 'otherDownloads', + name: 'other-downloads', + mandatory: false, + }); + return data; }, }, diff --git a/src/views/Exercises/Edit/Timeline.vue b/src/views/Exercises/Edit/Timeline.vue index 246fbff61..63a2b2d15 100644 --- a/src/views/Exercises/Edit/Timeline.vue +++ b/src/views/Exercises/Edit/Timeline.vue @@ -197,32 +197,34 @@ required /> -

- Independent Assessors -

- - - +
+

+ Independent Assessors +

+ + + +

Eligibility SCC @@ -328,6 +330,7 @@

diff --git a/src/views/Exercises/Show/Downloads.vue b/src/views/Exercises/Show/Downloads.vue index 9ab883dfd..7d40cb8fe 100644 --- a/src/views/Exercises/Show/Downloads.vue +++ b/src/views/Exercises/Show/Downloads.vue @@ -67,7 +67,10 @@
-
+
Independent Assessors
@@ -197,6 +200,7 @@ export default { exerciseId: 'id', //exercise: 'record', isEditable: 'isEditable', + hasIndependentAssessments: 'hasIndependentAssessments', }), exercise() { return this.$store.getters['exerciseDocument/data'](); diff --git a/src/views/Exercises/Show/Timeline.vue b/src/views/Exercises/Show/Timeline.vue index cc29e299f..227511616 100644 --- a/src/views/Exercises/Show/Timeline.vue +++ b/src/views/Exercises/Show/Timeline.vue @@ -21,7 +21,7 @@ import { mapGetters } from 'vuex'; import Timeline from '@jac-uk/jac-kit/draftComponents/Timeline'; import createTimeline from '@jac-uk/jac-kit/helpers/Timeline/createTimeline'; -import exerciseTimeline from '@jac-uk/jac-kit/helpers/Timeline/exerciseTimeline'; +import exerciseTimeline from '@/helpersTMP/Timeline/exerciseTimeline'; export default { components: { diff --git a/tests/unit/views/Exercises/Show/Downloads.spec.js b/tests/unit/views/Exercises/Show/Downloads.spec.js index 83e564ac6..e67d3bf07 100644 --- a/tests/unit/views/Exercises/Show/Downloads.spec.js +++ b/tests/unit/views/Exercises/Show/Downloads.spec.js @@ -43,6 +43,7 @@ const createTestSubject = () => { id: jest.fn(), data: () => mockData, isEditable: mockIsApproved, + hasIndependentAssessments: () => true, }, }, },