diff --git a/app/locals.js b/app/locals.js index 84adb7b..67711c5 100644 --- a/app/locals.js +++ b/app/locals.js @@ -10,5 +10,8 @@ module.exports = (req, res, next) => { res.locals.serviceEmail = 'england.digitallungcancerscreening@nhs.net' res.locals.serviceTelephone = '020 3835 1600' + res.locals.referrer = req.query.referrer + res.locals.query = req.query + next() } diff --git a/app/prototype_v4/content/contact.md b/app/prototype_v4/content/contact.md index 182e367..80ceb89 100644 --- a/app/prototype_v4/content/contact.md +++ b/app/prototype_v4/content/contact.md @@ -6,8 +6,8 @@ title: Contact us Call us on: {{ serviceTelephone | telephoneLink }} -**Phone lines are open:** -Monday to Friday 8am to 8pm +**Phone lines are open:** +Monday to Friday 8am to 8pm Saturdays 8am to 1pm ## If you have questions about the online service diff --git a/app/prototype_v4/controllers/authentication.js b/app/prototype_v4/controllers/authentication.js new file mode 100644 index 0000000..8305e2e --- /dev/null +++ b/app/prototype_v4/controllers/authentication.js @@ -0,0 +1,93 @@ +const version = 'v4' +const view = (template) => { + return `prototype_${version}/views/${template}` +} + +exports.signIn_get = (req, res) => { + + res.render(view('authentication/sign-in'), { + actions: { + back: '/prototype_v4', + next: '/prototype_v4/sign-in' + } + }) +} + +exports.signIn_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('authentication/sign-in'), { + errors, + actions: { + back: '/prototype_v4', + next: '/prototype_v4/sign-in' + } + }) + } else { + res.redirect('/prototype_v4/security-code') + } +} + +exports.securityCode_get = (req, res) => { + + res.render(view('authentication/security-code'), { + actions: { + back: '/prototype_v4/sign-in', + next: '/prototype_v4/security-code' + } + }) +} + +exports.securityCode_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('authentication/security-code'), { + errors, + actions: { + back: '/prototype_v4/sign-in', + next: '/prototype_v4/security-code' + } + }) + } else { + res.redirect('/prototype_v4/sign-in-agreement') + } +} + +exports.signInAgreement_get = (req, res) => { + + res.render(view('authentication/sign-in-agreement'), { + actions: { + back: '/prototype_v4/security-code', + accept: '/prototype_v4/sign-in-agreement', + decline: '/prototype_v4/sign-in-agreement-declined' + } + }) +} + +exports.signInAgreement_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('authentication/sign-in-agreement'), { + errors, + actions: { + back: '/prototype_v4/security-code', + accept: '/prototype_v4/sign-in-agreement', + decline: '/prototype_v4/sign-in-agreement-declined' + } + }) + } else { + res.redirect('/prototype_v4/accept-terms') + } +} + +exports.signInAgreementDeclined_get = (req, res) => { + + res.render(view('authentication/sign-in-agreement-declined'), { + actions: { + back: '/prototype_v4' + } + }) +} diff --git a/app/prototype_v4/controllers/question.js b/app/prototype_v4/controllers/question.js index e69de29..df4ff61 100644 --- a/app/prototype_v4/controllers/question.js +++ b/app/prototype_v4/controllers/question.js @@ -0,0 +1,1110 @@ +const version = 'v4' + +const view = (template) => { + return `prototype_${version}/views/${template}` +} + +const getHeightBack = (req) => { + const { answers } = req.session.data + + return answers?.height?.imperial ? '/prototype_v4/height-imperial' : '/prototype_v4/height-metric' +} + +const getWeightBack = (req) => { + const { answers } = req.session.data + + return answers?.weight?.imperial ? '/prototype_v4/weight-imperial' : '/prototype_v4/weight-metric' +} + +const getWeightNext = (req, defaultUnit) => { + const { answers } = req.session.data + + if (answers?.weight?.imperial) { + return '/prototype_v4/weight-imperial' + } + + if (answers?.weight?.metric) { + return '/prototype_v4/weight-metric' + } + + return `/prototype_${version}/weight-${defaultUnit}` +} + +const getDateOfBirth = (answers) => { + const day = Number(answers?.dateOfBirth?.day) + const month = Number(answers?.dateOfBirth?.month) + const year = Number(answers?.dateOfBirth?.year) + + if (!Number.isInteger(day) || !Number.isInteger(month) || !Number.isInteger(year)) { + return false + } + + const date = new Date(year, month - 1, day) + + if ( + date.getFullYear() !== year || + date.getMonth() !== month - 1 || + date.getDate() !== day + ) { + return false + } + + return date +} + +const getAge = (dateOfBirth) => { + const today = new Date() + let age = today.getFullYear() - dateOfBirth.getFullYear() + const monthDiff = today.getMonth() - dateOfBirth.getMonth() + + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dateOfBirth.getDate())) { + age-- + } + + return age +} + +const isEligibleForScanAge = (dateOfBirth) => { + const age = getAge(dateOfBirth) + + return age >= 55 && age <= 74 +} + +const smokingTypes = { + cigarettes: { + caption: 'Cigarette smoking', + frequencyHeading: 'How often do you smoke cigarettes?', + quantityHeading: 'How many cigarettes do you currently smoke in a normal day?', + changeHeading: 'Has the number of cigarettes you normally smoke changed over time?', + quantityUnit: 'cigarettes', + suffix: 'cigarettes' + }, + rolling_tobacco: { + caption: 'Rolling tobacco smoking', + frequencyHeading: 'How often do you smoke rolling tobacco or roll-ups?', + quantityHeading: 'How much rolling tobacco do you currently smoke in a normal week?', + changeHeading: 'Has the amount of rolling tobacco you normally smoke changed over time?', + quantityUnit: 'rolling tobacco' + }, + pipes: { + caption: 'Pipe smoking', + frequencyHeading: 'How often do you smoke a pipe?', + quantityHeading: 'How many full pipe loads do you currently smoke in a normal day?', + changeHeading: 'Has the number of full pipe loads you normally smoke changed over time?', + quantityUnit: 'full pipe loads', + suffix: 'full pipe loads' + }, + small_cigars: { + caption: 'Small cigar smoking', + frequencyHeading: 'How often do you smoke small cigars?', + quantityHeading: 'How many small cigars do you currently smoke in a normal day?', + changeHeading: 'Has the number of small cigars you normally smoke changed over time?', + quantityUnit: 'small cigars', + suffix: 'small cigars' + }, + medium_cigars: { + caption: 'Medium cigar smoking', + frequencyHeading: 'How often do you smoke medium cigars?', + quantityHeading: 'How many medium cigars do you currently smoke in a normal day?', + changeHeading: 'Has the number of medium cigars you normally smoke changed over time?', + quantityUnit: 'medium cigars', + suffix: 'medium cigars' + }, + large_cigars: { + caption: 'Large cigar smoking', + frequencyHeading: 'How often do you smoke large cigars?', + quantityHeading: 'How many large cigars do you currently smoke in a normal day?', + changeHeading: 'Has the number of large cigars you normally smoke changed over time?', + quantityUnit: 'large cigars', + suffix: 'large cigars' + }, + cigarillos: { + caption: 'Cigarillo smoking', + frequencyHeading: 'How often do you smoke cigarillos?', + quantityHeading: 'How many cigarillos do you currently smoke in a normal day?', + changeHeading: 'Has the number of cigarillos you normally smoke changed over time?', + quantityUnit: 'cigarillos', + suffix: 'cigarillos' + }, + shisha: { + caption: 'Shisha smoking', + settingHeading: 'Do you usually smoke shisha in a group or on your own?', + frequencyHeading: 'How often do you smoke shisha?', + quantityHeading: 'How many hours do you currently smoke shisha in a normal day?', + quantityUnit: 'hours', + suffix: 'hours' + } +} + +const nextStepAfterSmokingTypes = `/prototype_${version}/check-your-answers` + +const getSelectedSmokingTypes = (answers = {}) => { + const selectedTypes = Array.isArray(answers.typeOfSmoking) + ? answers.typeOfSmoking + : [answers.typeOfSmoking].filter(Boolean) + + return Object.keys(smokingTypes).filter((type) => selectedTypes.includes(type)) +} + +const getSmokingTypeSteps = (answers = {}) => { + return getSelectedSmokingTypes(answers).flatMap((type) => { + const steps = [] + + if (type === 'shisha') { + steps.push({ page: 'smoking-setting', type }) + } + + steps.push({ page: 'smoking-frequency', type }) + steps.push({ page: 'smoking-quantity', type }) + + if (type !== 'shisha') { + steps.push({ page: 'smoking-change', type }) + } + + return steps + }) +} + +const getSmokingTypeStepUrl = (step) => { + return `/prototype_${version}/${step.page}?type=${encodeURIComponent(step.type)}` +} + +const getSmokingTypeStep = (req, page) => { + const { answers } = req.session.data + const steps = getSmokingTypeSteps(answers) + const queryType = req.query?.type + const step = steps.find((step) => step.page === page && step.type === queryType) || + steps.find((step) => step.page === page) + + return { step, steps } +} + +const getSmokingTypeActions = (step, steps) => { + const index = steps.findIndex((item) => item.page === step.page && item.type === step.type) + const previousStep = steps[index - 1] + const nextStep = steps[index + 1] + + return { + next: getSmokingTypeStepUrl(step), + back: previousStep ? getSmokingTypeStepUrl(previousStep) : `/prototype_${version}/type-of-smoking`, + onward: nextStep ? getSmokingTypeStepUrl(nextStep) : nextStepAfterSmokingTypes, + cancel: `/prototype_${version}/` + } +} + +const renderSmokingTypeQuestion = (req, res, page, errors = []) => { + const { step, steps } = getSmokingTypeStep(req, page) + + if (!step) { + res.redirect(`/prototype_${version}/type-of-smoking`) + return + } + + res.render(view(`questions/${page}`), { + type: step.type, + smokingType: smokingTypes[step.type], + errors, + actions: getSmokingTypeActions(step, steps) + }) +} + +/// ------------------------------------------------------------------------ /// +/// +/// ------------------------------------------------------------------------ /// + +exports.acceptTerms_get = (req, res) => { + + res.render(view('questions/accept-terms'), { + actions: { + next: '/prototype_v4/accept-terms', + cancel: '/prototype_v4' + } + }) +} + +exports.acceptTerms_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/accept-terms'), { + errors, + actions: { + next: '/prototype_v4/accept-terms', + cancel: '/prototype_v4' + } + }) + } else { + res.redirect('/prototype_v4/phone-questionnaire') + } +} + +exports.phoneQuestionnaire_get = (req, res) => { + + res.render(view('questions/phone-questionnaire'), { + actions: { + next: '/prototype_v4/phone-questionnaire', + cancel: '/prototype_v4/' + } + }) +} + +exports.phoneQuestionnaire_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/phone-questionnaire'), { + errors, + actions: { + next: '/prototype_v4/phone-questionnaire', + cancel: '/prototype_v4/' + } + }) + } else { + if (answers.phoneQuestionnaire === 'yes') { + res.redirect('/prototype_v4/phone-questionnaire-exit') + } else { + res.redirect('/prototype_v4/smoker') + } + } +} + +exports.phoneQuestionnaireExit_get = (req, res) => { + + res.render(view('questions/phone-questionnaire-exit'), { + actions: { + back: '/prototype_v4/phone-questionnaire' + } + }) +} + +/// ------------------------------------------------------------------------ /// +/// Eligibility +/// ------------------------------------------------------------------------ /// + +exports.smoker_get = (req, res) => { + + res.render(view('questions/smoker'), { + actions: { + next: '/prototype_v4/smoker', + back: '/prototype_v4/phone-questionnaire', + cancel: '/prototype_v4/' + } + }) +} + +exports.smoker_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/smoker'), { + errors, + actions: { + next: '/prototype_v4/smoker', + back: '/prototype_v4/phone-questionnaire', + cancel: '/prototype_v4/' + } + }) + } else { + if (answers.smoker === 'no') { + res.redirect('/prototype_v4/not-eligible-for-screening') + } else { + res.redirect('/prototype_v4/date-of-birth') + } + } +} + +exports.dateOfBirth_get = (req, res) => { + + res.render(view('questions/date-of-birth'), { + actions: { + next: '/prototype_v4/date-of-birth', + back: '/prototype_v4/smoker', + cancel: '/prototype_v4/' + } + }) +} + +exports.dateOfBirth_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + const dateOfBirth = getDateOfBirth(answers) + + if (!dateOfBirth) { + errors.push({ + text: 'Enter a real date of birth', + href: '#dateOfBirth-day' + }) + } + + if (errors.length) { + res.render(view('questions/date-of-birth'), { + errors, + actions: { + next: '/prototype_v4/date-of-birth', + back: '/prototype_v4/smoker', + cancel: '/prototype_v4/' + } + }) + } else { + if (!isEligibleForScanAge(dateOfBirth)) { + res.redirect('/prototype_v4/not-eligible-for-scan') + } else { + res.redirect('/prototype_v4/face-to-face-appointment') + } + } +} + +exports.faceToFaceAppointment_get = (req, res) => { + + res.render(view('questions/face-to-face-appointment'), { + actions: { + next: '/prototype_v4/face-to-face-appointment', + back: '/prototype_v4/date-of-birth', + cancel: '/prototype_v4/' + } + }) +} + +exports.faceToFaceAppointment_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/face-to-face-appointment'), { + errors, + actions: { + next: '/prototype_v4/face-to-face-appointment', + back: '/prototype_v4/date-of-birth', + cancel: '/prototype_v4/' + } + }) + } else { + if (answers.faceToFaceAppointment === 'yes') { + res.redirect('/prototype_v4/book-appointment') + } else { + res.redirect('/prototype_v4/height-metric') + } + } +} + +exports.notEligibleForScreening_get = (req, res) => { + res.render(view('questions/not-eligible-for-screening'), { + actions: { + back: '/prototype_v4/smoker', + cancel: '/prototype_v4/' + } + }) +} + +exports.notEligibleForScan_get = (req, res) => { + res.render(view('questions/not-eligible-for-scan'), { + actions: { + back: '/prototype_v4/date-of-birth', + cancel: '/prototype_v4/' + } + }) +} + +exports.bookAppointment_get = (req, res) => { + res.render(view('questions/book-appointment'), { + actions: { + back: '/prototype_v4/face-to-face-appointment', + cancel: '/prototype_v4/' + } + }) +} + +/// ------------------------------------------------------------------------ /// +/// About you +/// ------------------------------------------------------------------------ /// + +exports.heightMetric_get = (req, res) => { + res.render(view('questions/height-metric'), { + actions: { + next: '/prototype_v4/height-metric', + switchUnits: '/prototype_v4/height-imperial', + back: '/prototype_v4/face-to-face-appointment', + cancel: '/prototype_v4/' + } + }) +} + +exports.heightMetric_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/height-metric'), { + errors, + actions: { + next: '/prototype_v4/height-metric', + switchUnits: '/prototype_v4/height-imperial', + back: '/prototype_v4/face-to-face-appointment', + cancel: '/prototype_v4/' + } + }) + } else { + delete answers.height?.imperial + res.redirect(getWeightNext(req, 'metric')) + } +} + +exports.heightImperial_get = (req, res) => { + res.render(view('questions/height-imperial'), { + actions: { + next: '/prototype_v4/height-imperial', + switchUnits: '/prototype_v4/height-metric', + back: '/prototype_v4/face-to-face-appointment', + cancel: '/prototype_v4/' + } + }) +} + +exports.heightImperial_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/height-imperial'), { + errors, + actions: { + next: '/prototype_v4/height-imperial', + switchUnits: '/prototype_v4/height-metric', + back: '/prototype_v4/face-to-face-appointment', + cancel: '/prototype_v4/' + } + }) + } else { + delete answers.height?.metric + res.redirect(getWeightNext(req, 'imperial')) + } +} + +exports.weightMetric_get = (req, res) => { + const back = getHeightBack(req) + + res.render(view('questions/weight-metric'), { + actions: { + next: '/prototype_v4/weight-metric', + switchUnits: '/prototype_v4/weight-imperial', + back, + cancel: '/prototype_v4/' + } + }) +} + +exports.weightMetric_post = (req, res) => { + const { answers } = req.session.data + const back = getHeightBack(req) + const errors = [] + + if (errors.length) { + res.render(view('questions/weight-metric'), { + errors, + actions: { + next: '/prototype_v4/weight-metric', + switchUnits: '/prototype_v4/weight-imperial', + back, + cancel: '/prototype_v4/' + } + }) + } else { + delete answers.weight?.imperial + res.redirect('/prototype_v4/gender') + } +} + +exports.weightImperial_get = (req, res) => { + const back = getHeightBack(req) + + res.render(view('questions/weight-imperial'), { + actions: { + next: '/prototype_v4/weight-imperial', + switchUnits: '/prototype_v4/weight-metric', + back, + cancel: '/prototype_v4/' + } + }) +} + +exports.weightImperial_post = (req, res) => { + const { answers } = req.session.data + const back = getHeightBack(req) + const errors = [] + + if (errors.length) { + res.render(view('questions/weight-imperial'), { + errors, + actions: { + next: '/prototype_v4/weight-imperial', + switchUnits: '/prototype_v4/weight-metric', + back, + cancel: '/prototype_v4/' + } + }) + } else { + delete answers.weight?.metric + res.redirect('/prototype_v4/gender') + } +} + +exports.gender_get = (req, res) => { + const back = getWeightBack(req) + + res.render(view('questions/gender'), { + actions: { + next: '/prototype_v4/gender', + back, + cancel: '/prototype_v4/' + } + }) +} + +exports.gender_post = (req, res) => { + const back = getWeightBack(req) + const errors = [] + + if (errors.length) { + res.render(view('questions/gender'), { + errors, + actions: { + next: '/prototype_v4/gender', + back, + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/sex') + } +} + +exports.sex_get = (req, res) => { + res.render(view('questions/sex'), { + actions: { + next: '/prototype_v4/sex', + back: '/prototype_v4/gender', + cancel: '/prototype_v4/' + } + }) +} + +exports.sex_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/sex'), { + errors, + actions: { + next: '/prototype_v4/sex', + back: '/prototype_v4/gender', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/ethnicity') + } +} + +exports.ethnicity_get = (req, res) => { + + res.render(view('questions/ethnicity'), { + actions: { + next: '/prototype_v4/ethnicity', + back: '/prototype_v4/gender', + cancel: '/prototype_v4/' + } + }) +} + +exports.ethnicity_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/ethnicity'), { + errors, + actions: { + next: '/prototype_v4/ehtnicity', + back: '/prototype_v4/gender', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/education') + } +} + +exports.education_get = (req, res) => { + + res.render(view('questions/education'), { + actions: { + next: '/prototype_v4/education', + back: '/prototype_v4/ethnicity', + cancel: '/prototype_v4/' + } + }) +} + +exports.education_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/education'), { + errors, + actions: { + next: '/prototype_v4/education', + back: '/prototype_v4/ethnicity', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/respiratory-conditions') + } +} + +/// ------------------------------------------------------------------------ /// +/// Your health +/// ------------------------------------------------------------------------ /// + +exports.respiratoryConditions_get = (req, res) => { + + res.render(view('questions/respiratory-conditions'), { + actions: { + next: '/prototype_v4/respiratory-conditions', + back: '/prototype_v4/education', + cancel: '/prototype_v4/' + } + }) +} + +exports.respiratoryConditions_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/respiratory-conditions'), { + errors, + actions: { + next: '/prototype_v4/respiratory-conditions', + back: '/prototype_v4/education', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/asbestos-at-work') + } +} + +exports.asbestosAtWork_get = (req, res) => { + + res.render(view('questions/asbestos-at-work'), { + actions: { + next: '/prototype_v4/asbestos-at-work', + back: '/prototype_v4/respiratory-conditions', + cancel: '/prototype_v4/' + } + }) +} + +exports.asbestosAtWork_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/asbestos-at-work'), { + errors, + actions: { + next: '/prototype_v4/asbestos-at-work', + back: '/prototype_v4/respiratory-conditions', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/asbestos-at-home') + } +} + +exports.asbestosAtHome_get = (req, res) => { + + res.render(view('questions/asbestos-at-home'), { + actions: { + next: '/prototype_v4/asbestos-at-home', + back: '/prototype_v4/asbestos-at-work', + cancel: '/prototype_v4/' + } + }) +} + +exports.asbestosAtHome_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/asbestos-at-home'), { + errors, + actions: { + next: '/prototype_v4/asbestos-at-home', + back: '/prototype_v4/asbestos-at-work', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/cancer-diagnosis') + } +} + +exports.cancerDiagnosis_get = (req, res) => { + + res.render(view('questions/cancer-diagnosis'), { + actions: { + next: '/prototype_v4/cancer-diagnosis', + back: '/prototype_v4/asbestos-at-work', + cancel: '/prototype_v4/' + } + }) +} + +exports.cancerDiagnosis_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/cancer-diagnosis'), { + errors, + actions: { + next: '/prototype_v4/cancer-diagnosis', + back: '/prototype_v4/asbestos-at-work', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/cancer-diagnosis-relatives') + } +} + +/// ------------------------------------------------------------------------ /// +/// Family history +/// ------------------------------------------------------------------------ /// + +exports.cancerDiagnosisRelatives_get = (req, res) => { + + res.render(view('questions/cancer-diagnosis-relatives'), { + actions: { + next: '/prototype_v4/cancer-diagnosis-relatives', + back: '/prototype_v4/cancer-diagnosis', + cancel: '/prototype_v4/' + } + }) +} + +exports.cancerDiagnosisRelatives_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/cancer-diagnosis-relatives'), { + errors, + actions: { + next: '/prototype_v4/cancer-diagnosis-relatives', + back: '/prototype_v4/cancer-diagnosis', + cancel: '/prototype_v4/' + } + }) + } else { + if (answers.cancerDiagnosisRelatives === 'yes') { + res.redirect('/prototype_v4/cancer-diagnosis-relatives-age') + } else { + delete answers.cancerDiagnosisRelativesAge + res.redirect('/prototype_v4/age-started-smoking') + } + } +} + +exports.cancerDiagnosisRelativesAge_get = (req, res) => { + + res.render(view('questions/cancer-diagnosis-relatives-age'), { + actions: { + next: '/prototype_v4/cancer-diagnosis-relatives-age', + back: '/prototype_v4/cancer-diagnosis-relatives', + cancel: '/prototype_v4/' + } + }) +} + +exports.cancerDiagnosisRelativesAge_post = (req, res) => { + const errors = [] + + if (errors.length) { + res.render(view('questions/cancer-diagnosis-relatives-age'), { + errors, + actions: { + next: '/prototype_v4/cancer-diagnosis-relatives-age', + back: '/prototype_v4/cancer-diagnosis-relatives', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/age-started-smoking') + } +} + +/// ------------------------------------------------------------------------ /// +/// Smoking habits +/// ------------------------------------------------------------------------ /// + +exports.ageStartedSmoking_get = (req, res) => { + const { answers } = req.session.data + const back = answers?.cancerDiagnosisRelativesAge ? '/prototype_v4/cancer-diagnosis-relatives-age' : '/prototype_v4/cancer-diagnosis-relatives' + + res.render(view('questions/age-started-smoking'), { + actions: { + next: '/prototype_v4/age-started-smoking', + back, + cancel: '/prototype_v4/' + } + }) +} + +exports.ageStartedSmoking_post = (req, res) => { + const { answers } = req.session.data + const back = answers?.cancerDiagnosisRelativesAge ? '/prototype_v4/cancer-diagnosis-relatives-age' : '/prototype_v4/cancer-diagnosis-relatives' + + const errors = [] + + // TODO: + // If not answered, throw error + // If the age started smoking is older than person's age + // based on date of birth, throw error + + if (errors.length) { + res.render(view('questions/age-started-smoking'), { + errors, + actions: { + next: '/prototype_v4/age-started-smoking', + back, + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/periods-stopped-smoking') + } +} + +exports.periodsStoppedSmoking_get = (req, res) => { + res.render(view('questions/periods-stopped-smoking'), { + actions: { + next: '/prototype_v4/periods-stopped-smoking', + back: '/prototype_v4/age-started-smoking', + cancel: '/prototype_v4/' + } + }) +} + +exports.periodsStoppedSmoking_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/periods-stopped-smoking'), { + errors, + actions: { + next: '/prototype_v4/periods-stopped-smoking', + back: '/prototype_v4/age-started-smoking', + cancel: '/prototype_v4/' + } + }) + } else { + if (answers.periodsStoppedSmoking === 'no') { + delete answers.yearsStoppedSmoking + } + res.redirect('/prototype_v4/type-of-smoking') + } +} + +/// ------------------------------------------------------------------------ /// +/// Tobacco +/// ------------------------------------------------------------------------ /// + +exports.typeOfSmoking_get = (req, res) => { + res.render(view('questions/type-of-smoking'), { + actions: { + next: '/prototype_v4/type-of-smoking', + back: '/prototype_v4/periods-stopped-smoking', + cancel: '/prototype_v4/' + } + }) +} + +exports.typeOfSmoking_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/type-of-smoking'), { + errors, + actions: { + next: '/prototype_v4/type-of-smoking', + back: '/prototype_v4/periods-stopped-smoking', + cancel: '/prototype_v4/' + } + }) + } else { + const selectedTypes = Array.isArray(answers.typeOfSmoking) + ? answers.typeOfSmoking + : [answers.typeOfSmoking].filter(Boolean) + const steps = getSmokingTypeSteps(answers) + + if (selectedTypes.includes('none')) { + res.redirect('/prototype_v4/type-of-smoking-exit') + } else if (steps.length) { + res.redirect(getSmokingTypeStepUrl(steps[0])) + } else { + res.redirect('/prototype_v4/type-of-smoking') + } + } +} + +exports.typeOfSmokingExit_get = (req, res) => { + res.render(view('questions/type-of-smoking-exit'), { + actions: { + back: '/prototype_v4/type-of-smoking' + } + }) +} + +exports.smokingFrequency_get = (req, res) => { + renderSmokingTypeQuestion(req, res, 'smoking-frequency') +} + +exports.smokingFrequency_post = (req, res) => { + const { step, steps } = getSmokingTypeStep(req, 'smoking-frequency') + const errors = [] + + if (!step) { + res.redirect('/prototype_v4/type-of-smoking') + return + } + + if (errors.length) { + renderSmokingTypeQuestion(req, res, 'smoking-frequency', errors) + } else { + res.redirect(getSmokingTypeActions(step, steps).onward) + } +} + +exports.smokingQuantity_get = (req, res) => { + renderSmokingTypeQuestion(req, res, 'smoking-quantity') +} + +exports.smokingQuantity_post = (req, res) => { + const { step, steps } = getSmokingTypeStep(req, 'smoking-quantity') + const errors = [] + + if (!step) { + res.redirect('/prototype_v4/type-of-smoking') + return + } + + if (errors.length) { + renderSmokingTypeQuestion(req, res, 'smoking-quantity', errors) + } else { + res.redirect(getSmokingTypeActions(step, steps).onward) + } +} + +exports.smokingSetting_get = (req, res) => { + renderSmokingTypeQuestion(req, res, 'smoking-setting') +} + +exports.smokingSetting_post = (req, res) => { + const { step, steps } = getSmokingTypeStep(req, 'smoking-setting') + const errors = [] + + if (!step) { + res.redirect('/prototype_v4/type-of-smoking') + return + } + + if (errors.length) { + renderSmokingTypeQuestion(req, res, 'smoking-setting', errors) + } else { + res.redirect(getSmokingTypeActions(step, steps).onward) + } +} + +exports.smokingChange_get = (req, res) => { + renderSmokingTypeQuestion(req, res, 'smoking-change') +} + +exports.smokingChange_post = (req, res) => { + const { step, steps } = getSmokingTypeStep(req, 'smoking-change') + const errors = [] + + if (!step) { + res.redirect('/prototype_v4/type-of-smoking') + return + } + + if (errors.length) { + renderSmokingTypeQuestion(req, res, 'smoking-change', errors) + } else { + res.redirect(getSmokingTypeActions(step, steps).onward) + } +} + +/// ------------------------------------------------------------------------ /// +/// Check your answers +/// ------------------------------------------------------------------------ /// + +exports.checkYourAnswers_get = (req, res) => { + + res.render(view('questions/check-your-answers'), { + actions: { + next: '/prototype_v4/check-your-answers', + back: '/prototype_v4/abc', + cancel: '/prototype_v4/' + } + }) +} + +exports.checkYourAnswers_post = (req, res) => { + res.redirect('/prototype_v4/confirmation') +} + +/// ------------------------------------------------------------------------ /// +/// Confirmation +/// ------------------------------------------------------------------------ /// + +exports.confirmation_get = (req, res) => { + res.render(view('questions/confirmation')) +} + +/// ------------------------------------------------------------------------ /// +/// Template +/// ------------------------------------------------------------------------ /// + +exports.XYZ_get = (req, res) => { + + res.render(view('questions/xyz'), { + actions: { + next: '/prototype_v4/xyz', + back: '/prototype_v4/abc', + cancel: '/prototype_v4/' + } + }) +} + +exports.XYZ_post = (req, res) => { + const { answers } = req.session.data + const errors = [] + + if (errors.length) { + res.render(view('questions/xyz'), { + errors, + actions: { + next: '/prototype_v4/xyz', + back: '/prototype_v4/abc', + cancel: '/prototype_v4/' + } + }) + } else { + res.redirect('/prototype_v4/mno') + } +} diff --git a/app/prototype_v4/routes.js b/app/prototype_v4/routes.js index d73285f..b9f4b6b 100644 --- a/app/prototype_v4/routes.js +++ b/app/prototype_v4/routes.js @@ -23,17 +23,162 @@ const hasView = (template) => { /// Controller modules - used for routing /// ------------------------------------------------------------------------ /// +const authenticationController = require('./controllers/authentication') const contentController = require('./controllers/content') const errorController = require('./controllers/error') +const questionController = require('./controllers/question') /// ------------------------------------------------------------------------ /// -/// +/// Start page /// ------------------------------------------------------------------------ /// -router.get('/prototype_v4', (_req, res) => { - res.render(view('index')) +router.get('/prototype_v4', (req, res) => { + res.redirect('/prototype_v4/start-page') }) +router.get('/prototype_v4/start-page', (req, res) => { + res.render(view('index'), { + actions: { + start: '/prototype_v4/sign-in' + } + }) +}) + +/// ------------------------------------------------------------------------ /// +/// Sign-in pages +/// ------------------------------------------------------------------------ /// + +router.get('/prototype_v4/sign-in', authenticationController.signIn_get) +router.post('/prototype_v4/sign-in', authenticationController.signIn_post) + +router.get('/prototype_v4/security-code', authenticationController.securityCode_get) +router.post('/prototype_v4/security-code', authenticationController.securityCode_post) + +router.get('/prototype_v4/sign-in-agreement', authenticationController.signInAgreement_get) +router.post('/prototype_v4/sign-in-agreement', authenticationController.signInAgreement_post) + +router.get('/prototype_v4/sign-in-agreement-declined', authenticationController.signInAgreementDeclined_get) + +/// ------------------------------------------------------------------------ /// +/// Terms and conditions page +/// ------------------------------------------------------------------------ /// + +router.get('/prototype_v4/accept-terms', questionController.acceptTerms_get) +router.post('/prototype_v4/accept-terms', questionController.acceptTerms_post) + +/// ------------------------------------------------------------------------ /// +/// Question pages +/// ------------------------------------------------------------------------ /// + +router.get('/prototype_v4/phone-questionnaire', questionController.phoneQuestionnaire_get) +router.post('/prototype_v4/phone-questionnaire', questionController.phoneQuestionnaire_post) + +router.get('/prototype_v4/phone-questionnaire-exit', questionController.phoneQuestionnaireExit_get) + +/// Eligibility ------------------------------------------------------------ /// + +router.get('/prototype_v4/smoker', questionController.smoker_get) +router.post('/prototype_v4/smoker', questionController.smoker_post) + +router.get('/prototype_v4/date-of-birth', questionController.dateOfBirth_get) +router.post('/prototype_v4/date-of-birth', questionController.dateOfBirth_post) + +router.get('/prototype_v4/face-to-face-appointment', questionController.faceToFaceAppointment_get) +router.post('/prototype_v4/face-to-face-appointment', questionController.faceToFaceAppointment_post) + +router.get('/prototype_v4/not-eligible-for-screening', questionController.notEligibleForScreening_get) + +router.get('/prototype_v4/not-eligible-for-scan', questionController.notEligibleForScan_get) + +router.get('/prototype_v4/book-appointment', questionController.bookAppointment_get) + + +/// About you -------------------------------------------------------------- /// + +router.get('/prototype_v4/height-metric', questionController.heightMetric_get) +router.post('/prototype_v4/height-metric', questionController.heightMetric_post) + +router.get('/prototype_v4/height-imperial', questionController.heightImperial_get) +router.post('/prototype_v4/height-imperial', questionController.heightImperial_post) + +router.get('/prototype_v4/weight-metric', questionController.weightMetric_get) +router.post('/prototype_v4/weight-metric', questionController.weightMetric_post) + +router.get('/prototype_v4/weight-imperial', questionController.weightImperial_get) +router.post('/prototype_v4/weight-imperial', questionController.weightImperial_post) + +router.get('/prototype_v4/sex', questionController.sex_get) +router.post('/prototype_v4/sex', questionController.sex_post) + +router.get('/prototype_v4/gender', questionController.gender_get) +router.post('/prototype_v4/gender', questionController.gender_post) + +router.get('/prototype_v4/ethnicity', questionController.ethnicity_get) +router.post('/prototype_v4/ethnicity', questionController.ethnicity_post) + +router.get('/prototype_v4/education', questionController.education_get) +router.post('/prototype_v4/education', questionController.education_post) + +/// Your health ------------------------------------------------------------ /// + +router.get('/prototype_v4/respiratory-conditions', questionController.respiratoryConditions_get) +router.post('/prototype_v4/respiratory-conditions', questionController.respiratoryConditions_post) + +router.get('/prototype_v4/asbestos-at-work', questionController.asbestosAtWork_get) +router.post('/prototype_v4/asbestos-at-work', questionController.asbestosAtWork_post) + +router.get('/prototype_v4/asbestos-at-home', questionController.asbestosAtHome_get) +router.post('/prototype_v4/asbestos-at-home', questionController.asbestosAtHome_post) + +router.get('/prototype_v4/cancer-diagnosis', questionController.cancerDiagnosis_get) +router.post('/prototype_v4/cancer-diagnosis', questionController.cancerDiagnosis_post) + +/// Family history --------------------------------------------------------- /// + +router.get('/prototype_v4/cancer-diagnosis-relatives', questionController.cancerDiagnosisRelatives_get) +router.post('/prototype_v4/cancer-diagnosis-relatives', questionController.cancerDiagnosisRelatives_post) + +router.get('/prototype_v4/cancer-diagnosis-relatives-age', questionController.cancerDiagnosisRelativesAge_get) +router.post('/prototype_v4/cancer-diagnosis-relatives-age', questionController.cancerDiagnosisRelativesAge_post) + +/// Smoking habits --------------------------------------------------------- /// + +router.get('/prototype_v4/age-started-smoking', questionController.ageStartedSmoking_get) +router.post('/prototype_v4/age-started-smoking', questionController.ageStartedSmoking_post) + +router.get('/prototype_v4/periods-stopped-smoking', questionController.periodsStoppedSmoking_get) +router.post('/prototype_v4/periods-stopped-smoking', questionController.periodsStoppedSmoking_post) + +/// Tobacco --------------------------------------------------------------- /// + +router.get('/prototype_v4/type-of-smoking', questionController.typeOfSmoking_get) +router.post('/prototype_v4/type-of-smoking', questionController.typeOfSmoking_post) + +router.get('/prototype_v4/type-of-smoking-exit', questionController.typeOfSmokingExit_get) + +router.get('/prototype_v4/smoking-frequency', questionController.smokingFrequency_get) +router.post('/prototype_v4/smoking-frequency', questionController.smokingFrequency_post) + +router.get('/prototype_v4/smoking-quantity', questionController.smokingQuantity_get) +router.post('/prototype_v4/smoking-quantity', questionController.smokingQuantity_post) + +router.get('/prototype_v4/smoking-setting', questionController.smokingSetting_get) +router.post('/prototype_v4/smoking-setting', questionController.smokingSetting_post) + +router.get('/prototype_v4/smoking-change', questionController.smokingChange_get) +router.post('/prototype_v4/smoking-change', questionController.smokingChange_post) + +// TODO: Change of amount questions + +/// Check your answers ----------------------------------------------------- /// + +router.get('/prototype_v4/check-your-answers', questionController.checkYourAnswers_get) +router.post('/prototype_v4/check-your-answers', questionController.checkYourAnswers_post) + +/// Confirmation ----------------------------------------------------------- /// + +router.get('/prototype_v4/confirmation', questionController.confirmation_get) + /// ------------------------------------------------------------------------ /// /// Static pages /// ------------------------------------------------------------------------ /// diff --git a/app/prototype_v4/views/authentication/security-code.html b/app/prototype_v4/views/authentication/security-code.html new file mode 100644 index 0000000..c9e961e --- /dev/null +++ b/app/prototype_v4/views/authentication/security-code.html @@ -0,0 +1,91 @@ +{% extends "prototype_v4/views/layouts/main.html" %} + +{% set title = "Enter the security code" %} + +{% block content %} + {% include "prototype_v4/views/includes/error-summary.html" %} + +
+ We have sent a 6-digit security code to your phone number ending in 0887. +
+ ++ It may take a few minutes to arrive. +
+ + {% set securityCodeHtml %} +Make sure your device:
+Wait 30 seconds before you send the security code again.
+ {% endset %} + + {{ details({ + summaryText: "Not received a security code?", + html: securityCodeHtml + }) }} + + + + {% set rememberDeviceHtml %} +We can remember the device you are using now, so you will not need to enter a security code when you log in with this device in the future.
+To keep your NHS login secure, you should only do this on your own personal or trusted devices.
+We may ask if you still want us to remember this device in the future.
+ {% endset %} + + {{ details({ + summaryText: "What does remember this device mean?", + html: rememberDeviceHtml + }) }} + + {% set mobileDeviceHtml %} +If you no longer have access to it, you can change the mobile phone number you use to log in.
+ {% endset %} + + {{ details({ + summaryText: "I cannot log in using my mobile phone", + html: mobileDeviceHtml + }) }} + ++ or +
+ + {{ button({ + text: "Create NHS account", + href: "#", + classes: "nhsuk-button--secondary" + }) }} + ++ An NHS account allows you to access a range of online health services with one set of login details. +
+ + + + {% set cardDescriptionHtml %} + + {% endset %} + + {{ card({ + heading: "Trouble with logging in?", + headingSize: "m", + descriptionHtml: cardDescriptionHtml + }) }} + +We are testing a new online questionnaire for lung cancer screening. This service will ask you some questions about your medical history and lifestyle.
@@ -43,13 +43,13 @@You should call us on 0207 555 444 to complete the questionnaire by phone.
+You should call us on {{ serviceTelephone }} to complete the questionnaire by phone.
+ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Check the information you have given us. If any of the details are wrong, you can change them. +
+ + + ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ ++ Cancel +
+ +This page allows you to view current and previous prototypes for the check if you need a lung scan service.
Last updated: 5 May 2026
Multiple changes have been made to v4, including major revisions to the tobacco smoking screens.
- + Open prototype -