From 187bcc3f98cd216f4081fd0400c352eeaec23ac0 Mon Sep 17 00:00:00 2001 From: Marco Collura Date: Tue, 1 Oct 2024 14:53:45 +0100 Subject: [PATCH] Add Rechsedule requested by radio buttons --- server/models/appointment.ts | 1 + .../routes/appointments/appointmentSummary.ts | 3 + .../appointmentsController.test.ts | 16 ++++ .../appointments/appointmentsController.ts | 6 +- .../feedbackAnswersPresenter.test.ts | 10 +-- .../scheduleAppointmentForm.test.ts | 38 ++++++++- .../scheduleAppointmentForm.ts | 8 +- .../scheduleAppointmentPresenter.ts | 7 ++ .../scheduleAppointmentView.ts | 84 ++++++++++++++----- server/utils/errorMessages.ts | 5 +- .../scheduleAppointment.njk | 1 + 11 files changed, 143 insertions(+), 36 deletions(-) diff --git a/server/models/appointment.ts b/server/models/appointment.ts index e15033546..feffa22d9 100644 --- a/server/models/appointment.ts +++ b/server/models/appointment.ts @@ -27,5 +27,6 @@ export interface AppointmentSchedulingDetails { appointmentDeliveryType: AppointmentDeliveryType | null appointmentDeliveryAddress: Address | null npsOfficeCode: string | null + rescheduleRequestedBy?: string rescheduledReason?: string } diff --git a/server/routes/appointments/appointmentSummary.ts b/server/routes/appointments/appointmentSummary.ts index 4e43b621a..5640f0af4 100644 --- a/server/routes/appointments/appointmentSummary.ts +++ b/server/routes/appointments/appointmentSummary.ts @@ -71,6 +71,9 @@ export default class AppointmentSummary { lines: this.inPersonMeetingProbationOfficeAddressLines(this.deliusOfficeLocation), }) } + if (this.appointment.rescheduleRequestedBy) { + summary.push({ key: 'Appointment change requested by', lines: [this.appointment.rescheduleRequestedBy] }) + } if (this.appointment.rescheduledReason) { summary.push({ key: 'Reason for appointment change', lines: [this.appointment.rescheduledReason] }) } diff --git a/server/routes/appointments/appointmentsController.test.ts b/server/routes/appointments/appointmentsController.test.ts index e10de0c84..93145ca73 100644 --- a/server/routes/appointments/appointmentsController.test.ts +++ b/server/routes/appointments/appointmentsController.test.ts @@ -92,6 +92,8 @@ describe('Scheduling a supplier assessment appointment', () => { const draftBooking = draftAppointmentBookingFactory.build() draftsService.fetchDraft.mockResolvedValue(draftBooking) + const deliusServiceUser = deliusServiceUserFactory.build() + ramDeliusApiService.getCaseDetailsByCrn.mockResolvedValue(deliusServiceUser) interventionsService.getSupplierAssessment.mockResolvedValue(supplierAssessmentFactory.build()) interventionsService.getSentReferral.mockResolvedValue(sentReferralFactory.build()) interventionsService.getIntervention.mockResolvedValue(interventionFactory.build()) @@ -119,6 +121,8 @@ describe('Scheduling a supplier assessment appointment', () => { ) interventionsService.getSentReferral.mockResolvedValue(sentReferralFactory.build()) interventionsService.getIntervention.mockResolvedValue(interventionFactory.build()) + const deliusServiceUser = deliusServiceUserFactory.build() + ramDeliusApiService.getCaseDetailsByCrn.mockResolvedValue(deliusServiceUser) await request(app) .get(`/service-provider/referrals/1/supplier-assessment/schedule/${draftBooking.id}/details`) @@ -146,6 +150,8 @@ describe('Scheduling a supplier assessment appointment', () => { hmppsAuthService.getSPUserByUsername.mockResolvedValue( hmppsAuthUserFactory.build({ firstName: 'caseWorkerFirstName', lastName: 'caseWorkerLastName' }) ) + const deliusServiceUser = deliusServiceUserFactory.build() + ramDeliusApiService.getCaseDetailsByCrn.mockResolvedValue(deliusServiceUser) await request(app) .get(`/service-provider/referrals/1/supplier-assessment/schedule/${draftBooking.id}/details`) @@ -182,6 +188,8 @@ describe('Scheduling a supplier assessment appointment', () => { interventionsService.getSupplierAssessment.mockResolvedValue(supplierAssessmentFactory.build()) interventionsService.getSentReferral.mockResolvedValue(sentReferralFactory.build()) interventionsService.getIntervention.mockResolvedValue(interventionFactory.build()) + const deliusServiceUser = deliusServiceUserFactory.build() + ramDeliusApiService.getCaseDetailsByCrn.mockResolvedValue(deliusServiceUser) await request(app) .get(`/service-provider/referrals/1/supplier-assessment/schedule/${draftBooking.id}/details?clash=true`) @@ -251,6 +259,8 @@ describe('Scheduling a supplier assessment appointment', () => { interventionsService.getSupplierAssessment.mockResolvedValue(supplierAssessmentFactory.build()) interventionsService.getSentReferral.mockResolvedValue(sentReferralFactory.build()) interventionsService.getIntervention.mockResolvedValue(interventionFactory.build()) + const deliusServiceUser = deliusServiceUserFactory.build() + ramDeliusApiService.getCaseDetailsByCrn.mockResolvedValue(deliusServiceUser) await request(app) .post(`/service-provider/referrals/1/supplier-assessment/schedule/${draftBooking.id}/details`) @@ -630,6 +640,9 @@ describe('Scheduling a delivery session', () => { const appointment = actionPlanAppointmentFactory.build() + const deliusServiceUser = deliusServiceUserFactory.build() + ramDeliusApiService.getCaseDetailsByCrn.mockResolvedValue(deliusServiceUser) + interventionsService.getActionPlanAppointment.mockResolvedValue(appointment) interventionsService.getSentReferral.mockResolvedValue(sentReferralFactory.build()) interventionsService.getActionPlan.mockResolvedValue(actionPlanFactory.build()) @@ -705,6 +718,9 @@ describe('Scheduling a delivery session', () => { const actionPlan = actionPlanFactory.build() const appointment = actionPlanAppointmentFactory.build() + const deliusServiceUser = deliusServiceUserFactory.build() + ramDeliusApiService.getCaseDetailsByCrn.mockResolvedValue(deliusServiceUser) + interventionsService.getActionPlan.mockResolvedValue(actionPlan) interventionsService.getActionPlanAppointment.mockResolvedValue(appointment) interventionsService.getSentReferral.mockResolvedValue(sentReferralFactory.build()) diff --git a/server/routes/appointments/appointmentsController.ts b/server/routes/appointments/appointmentsController.ts index 01b2ad133..af178da4c 100644 --- a/server/routes/appointments/appointmentsController.ts +++ b/server/routes/appointments/appointmentsController.ts @@ -154,7 +154,8 @@ export default class AppointmentsController { overrideBackLinkHref ) - const view = new ScheduleAppointmentView(presenter) + const serviceUserName = `${serviceUser.name.forename} ${serviceUser.name.surname}` + const view = new ScheduleAppointmentView(req, serviceUserName, presenter) await ControllerUtils.renderWithLayout(req, res, view, serviceUser, 'service-provider') } @@ -375,7 +376,8 @@ export default class AppointmentsController { userInputData, serverError ) - const view = new ScheduleAppointmentView(presenter) + const serviceUserName = `${serviceUser.name.forename} ${serviceUser.name.surname}` + const view = new ScheduleAppointmentView(req, serviceUserName, presenter) await ControllerUtils.renderWithLayout(req, res, view, serviceUser, 'service-provider') } diff --git a/server/routes/appointments/feedback/shared/viewFeedback/feedbackAnswersPresenter.test.ts b/server/routes/appointments/feedback/shared/viewFeedback/feedbackAnswersPresenter.test.ts index c00cab8e5..8144498cb 100644 --- a/server/routes/appointments/feedback/shared/viewFeedback/feedbackAnswersPresenter.test.ts +++ b/server/routes/appointments/feedback/shared/viewFeedback/feedbackAnswersPresenter.test.ts @@ -296,14 +296,8 @@ describe(FeedbackAnswersPresenter, () => { const presenter = new FeedbackAnswersPresenter(appointment, serviceUser, false) - expect(presenter.notifyProbationPractitionerOfBehaviourAnswers).toEqual({ - question: `Does the probation practitioner need to be notified about poor behaviour?`, - answer: 'No', - }) - expect(presenter.notifyProbationPractitionerOfConcernsAnswers).toEqual({ - question: `Does the probation practitioner need to be notified about any concerns?`, - answer: 'No', - }) + expect(presenter.notifyProbationPractitionerOfBehaviourAnswers).toBeNull() + expect(presenter.notifyProbationPractitionerOfConcernsAnswers).toBeNull() }) }) }) diff --git a/server/routes/serviceProviderReferrals/scheduleAppointmentForm.test.ts b/server/routes/serviceProviderReferrals/scheduleAppointmentForm.test.ts index 04dcde700..03a9038df 100644 --- a/server/routes/serviceProviderReferrals/scheduleAppointmentForm.test.ts +++ b/server/routes/serviceProviderReferrals/scheduleAppointmentForm.test.ts @@ -44,8 +44,8 @@ describe(ScheduleAppointmentForm, () => { }) }) - describe('with a rescheduled reason', () => { - it('returns a paramsForUpdate with the completionDeadline key, an ISO-formatted date and a reason for rescheduling', async () => { + describe('with rescheduledReason and rescheduleRequestedBy', () => { + it('returns a paramsForUpdate with the completionDeadline key, an ISO-formatted date, rescheduledReason and rescheduleRequestedBy', async () => { const request = TestUtils.createRequest({ 'date-year': '2022', 'date-month': '4', @@ -58,6 +58,7 @@ describe(ScheduleAppointmentForm, () => { 'session-type': 'ONE_TO_ONE', 'meeting-method': 'PHONE_CALL', 'rescheduled-reason': 'test reason', + 'reschedule-requested-by': 'Service Provider', }) const data = await new ScheduleAppointmentForm(request, deliusOfficeLocations, false, null, true).data() @@ -70,6 +71,7 @@ describe(ScheduleAppointmentForm, () => { appointmentDeliveryAddress: null, npsOfficeCode: null, rescheduledReason: 'test reason', + rescheduleRequestedBy: 'Service Provider', }) }) }) @@ -327,6 +329,7 @@ describe(ScheduleAppointmentForm, () => { 'meeting-method': 'IN_PERSON_MEETING_PROBATION_OFFICE', 'delius-office-location-code': 'CRS0001', 'rescheduled-reason': '', + 'reschedule-requested-by': 'Service Provider', }) const data = await new ScheduleAppointmentForm(request, deliusOfficeLocations, false, null, true).data() @@ -342,6 +345,37 @@ describe(ScheduleAppointmentForm, () => { }) }) }) + describe('having no rescheduled reason when there is an existing appointment', () => { + it('returns an error message for empty rescheduled reason', async () => { + const request = TestUtils.createRequest({ + 'date-year': '2022', + 'date-month': '3', + 'date-day': '1', + 'time-hour': '1', + 'time-minute': '05', + 'time-part-of-day': 'pm', + 'duration-hours': '1', + 'duration-minutes': '30', + 'session-type': 'ONE_TO_ONE', + 'meeting-method': 'IN_PERSON_MEETING_PROBATION_OFFICE', + 'delius-office-location-code': 'CRS0001', + 'rescheduled-reason': 'test reason', + 'reschedule-requested-by': '', + }) + + const data = await new ScheduleAppointmentForm(request, deliusOfficeLocations, false, null, true).data() + + expect(data.error).toEqual({ + errors: [ + { + errorSummaryLinkedField: 'reschedule-requested-by', + formFields: ['reschedule-requested-by'], + message: 'Select who requested the appointment change', + }, + ], + }) + }) + }) describe('having a late date selection', () => { it('returns an error message for late appointment date', async () => { MockDate.set(new Date(2022, 2, 1)) diff --git a/server/routes/serviceProviderReferrals/scheduleAppointmentForm.ts b/server/routes/serviceProviderReferrals/scheduleAppointmentForm.ts index 1cdf4dfa6..2294489a4 100644 --- a/server/routes/serviceProviderReferrals/scheduleAppointmentForm.ts +++ b/server/routes/serviceProviderReferrals/scheduleAppointmentForm.ts @@ -99,6 +99,7 @@ export default class ScheduleAppointmentForm { appointmentDeliveryType: appointmentDeliveryType.value!, appointmentDeliveryAddress: appointmentDeliveryAddress.value, npsOfficeCode: deliusOfficeLocation.value, + rescheduleRequestedBy: this.request.body['reschedule-requested-by'], rescheduledReason: this.request.body['rescheduled-reason'], }, } @@ -114,7 +115,12 @@ export default class ScheduleAppointmentForm { private validateRescheduledReason(): ValidationChain[] { return this.hasExistingScheduledAppointment - ? [body('rescheduled-reason').notEmpty().withMessage(errorMessages.scheduleAppointment.recsheduledReason.empty)] + ? [ + body('reschedule-requested-by') + .notEmpty() + .withMessage(errorMessages.scheduleAppointment.rescheduleRequestedBy.emptyRadio), + body('rescheduled-reason').notEmpty().withMessage(errorMessages.scheduleAppointment.rescheduledReason.empty), + ] : [] } } diff --git a/server/routes/serviceProviderReferrals/scheduleAppointmentPresenter.ts b/server/routes/serviceProviderReferrals/scheduleAppointmentPresenter.ts index 1331b2d08..a0f7f4487 100644 --- a/server/routes/serviceProviderReferrals/scheduleAppointmentPresenter.ts +++ b/server/routes/serviceProviderReferrals/scheduleAppointmentPresenter.ts @@ -44,6 +44,13 @@ export default class ScheduleAppointmentPresenter { } } + get rescheduleRequestedBy(): Record { + return { + label: 'Appointment change requested by', + errorMessage: PresenterUtils.errorMessage(this.validationError, 'reschedule-requested-by'), + } + } + private readonly utils = new PresenterUtils(this.userInputData) get appointmentSummary(): SummaryListItem[] { diff --git a/server/routes/serviceProviderReferrals/scheduleAppointmentView.ts b/server/routes/serviceProviderReferrals/scheduleAppointmentView.ts index 732e078b2..99ba426d5 100644 --- a/server/routes/serviceProviderReferrals/scheduleAppointmentView.ts +++ b/server/routes/serviceProviderReferrals/scheduleAppointmentView.ts @@ -1,3 +1,4 @@ +import { Request } from 'express' import { BackLinkArgs, DateInputArgs, @@ -12,31 +13,14 @@ import ScheduleAppointmentPresenter from './scheduleAppointmentPresenter' import AddressFormComponent from '../shared/addressFormComponent' export default class ScheduleAppointmentView { - constructor(private readonly presenter: ScheduleAppointmentPresenter) {} + constructor( + readonly request: Request, + private readonly serviceUserName: string, + private readonly presenter: ScheduleAppointmentPresenter + ) {} addressFormView = new AddressFormComponent(this.presenter.fields.address, 'method-other-location') - get renderArgs(): [string, Record] { - return [ - 'serviceProviderReferrals/scheduleAppointment', - { - presenter: this.presenter, - dateInputArgs: this.dateInputArgs, - timeInputArgs: this.timeInputArgs, - durationDateInputArgs: this.durationDateInputArgs, - deliusOfficeLocationSelectArgs: this.deliusOfficeLocationSelectArgs, - errorSummaryArgs: this.errorSummaryArgs, - serverError: this.serverError, - address: this.addressFormView.inputArgs, - sessionTypeRadioInputArgs: this.sessionTypeRadioInputArgs, - rescheduledReasonTextareaArgs: this.rescheduledReasonTextareaArgs, - meetingMethodRadioInputArgs: this.meetingMethodRadioInputArgs.bind(this), - backLinkArgs: this.backLinkArgs, - appointmentSummaryListArgs: ViewUtils.summaryListArgs(this.presenter.appointmentSummary), - }, - ] - } - private readonly errorSummaryArgs = ViewUtils.govukErrorSummaryArgs(this.presenter.errorSummary) get serverError(): { message: string; classes: string } | null { @@ -248,6 +232,40 @@ export default class ScheduleAppointmentView { } } + private get rescheduleRequestedByRadioButtonArgs(): Record { + return { + classes: 'govuk-radios', + idPrefix: 'reschedule-requested-by', + name: 'reschedule-requested-by', + attributes: { 'data-cy': 'reschedule-requested-by-radios' }, + fieldset: { + legend: { + text: this.presenter.rescheduleRequestedBy.label, + isPageHeading: false, + classes: 'govuk-fieldset__legend--m', + }, + }, + hint: { + text: 'Select one option', + }, + errorMessage: ViewUtils.govukErrorMessage(this.presenter.rescheduleRequestedBy.errorMessage), + items: [ + { + id: 'rescheduleRequestedBySpRadio', + value: 'Service Provider', + text: 'Service Provider', + checked: this.request.body['reschedule-requested-by'] === 'Service Provider', + }, + { + id: 'rescheduleRequestedByUserRadio', + value: this.serviceUserName, + text: this.serviceUserName, + checked: this.request.body['reschedule-requested-by'] === this.serviceUserName, + }, + ], + } + } + private get rescheduledReasonTextareaArgs(): TextareaArgs { return { name: 'rescheduled-reason', @@ -267,4 +285,26 @@ export default class ScheduleAppointmentView { href: this.presenter.backLinkHref, } } + + get renderArgs(): [string, Record] { + return [ + 'serviceProviderReferrals/scheduleAppointment', + { + presenter: this.presenter, + dateInputArgs: this.dateInputArgs, + timeInputArgs: this.timeInputArgs, + durationDateInputArgs: this.durationDateInputArgs, + deliusOfficeLocationSelectArgs: this.deliusOfficeLocationSelectArgs, + errorSummaryArgs: this.errorSummaryArgs, + serverError: this.serverError, + address: this.addressFormView.inputArgs, + sessionTypeRadioInputArgs: this.sessionTypeRadioInputArgs, + rescheduleRequestedByRadioButtonArgs: this.rescheduleRequestedByRadioButtonArgs, + rescheduledReasonTextareaArgs: this.rescheduledReasonTextareaArgs, + meetingMethodRadioInputArgs: this.meetingMethodRadioInputArgs.bind(this), + backLinkArgs: this.backLinkArgs, + appointmentSummaryListArgs: ViewUtils.summaryListArgs(this.presenter.appointmentSummary), + }, + ] + } } diff --git a/server/utils/errorMessages.ts b/server/utils/errorMessages.ts index 8fccadf69..d013d54a6 100644 --- a/server/utils/errorMessages.ts +++ b/server/utils/errorMessages.ts @@ -305,7 +305,10 @@ export default { empty: 'Select an office', invalidOfficeSelection: 'Select an office from the list', }, - recsheduledReason: { + rescheduleRequestedBy: { + emptyRadio: 'Select who requested the appointment change', + }, + rescheduledReason: { empty: 'Enter reason for changing appointment', }, }, diff --git a/server/views/serviceProviderReferrals/scheduleAppointment.njk b/server/views/serviceProviderReferrals/scheduleAppointment.njk index ce4f8ab6e..d5da36c78 100644 --- a/server/views/serviceProviderReferrals/scheduleAppointment.njk +++ b/server/views/serviceProviderReferrals/scheduleAppointment.njk @@ -64,6 +64,7 @@ {% endset -%} {{ govukRadios(meetingMethodRadioInputArgs(deliusOfficeLocationHtml, otherLocationHtml)) }} {% if presenter.hasExistingScheduledAppointment %} + {{ govukRadios(rescheduleRequestedByRadioButtonArgs) }} {{ govukTextarea(rescheduledReasonTextareaArgs) }} {% endif %}