Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/assets/sass/components/_card.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@
background-color: nhsuk-colour("grey-1");
color: nhsuk-colour("white");
}

.nhsuk-card--feature.app-card--feature-orange .nhsuk-card__heading {
background-color: nhsuk-colour("orange");
color: nhsuk-colour("white");
}

.nhsuk-card--feature.app-card--feature-green .nhsuk-card__heading {
background-color: nhsuk-colour("green");
color: nhsuk-colour("white");
}
19 changes: 13 additions & 6 deletions app/assets/sass/components/_compact.scss
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,6 @@
}

.app-clinic-appointments-table {
table-layout: fixed;
width: 100%;

.app-clinic-appointments-table__time-column {
width: 22%;
}

.app-clinic-appointments-table__time-column .nhsuk-tag {
display: inline-block;
Expand Down Expand Up @@ -428,3 +422,16 @@
@include nhsuk-font($size: 22, $weight: bold);
}
}

.app-clinic-appointments-table.nhsuk-u-margin-bottom-0 tbody tr:last-child td {
border-bottom: 0;
}

.app-clinic-appointments-table {
table-layout: fixed;
width: 100%;

.app-clinic-appointments-table__time-column {
width: 22%;
}
}
7 changes: 5 additions & 2 deletions app/lib/utils/clinics.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const getFilteredClinics = (clinics, filter = 'all') => {
switch (filter) {
case 'today':
return recentClinics.filter((clinic) =>
dayjs(clinic.date).isSame(today, 'day')
dayjs(clinic.date).isSame(today, 'day') && clinic.status !== 'closed'
)

case 'upcoming':
Expand All @@ -124,7 +124,10 @@ const getFilteredClinics = (clinics, filter = 'all') => {

case 'completed':
return recentClinics
.filter((clinic) => dayjs(clinic.date).isBefore(today, 'day'))
.filter((clinic) =>
dayjs(clinic.date).isBefore(today, 'day') ||
(dayjs(clinic.date).isSame(today, 'day') && clinic.status === 'closed')
)
.sort((a, b) => new Date(b.date) - new Date(a.date)) // Most recent first

case 'all':
Expand Down
194 changes: 193 additions & 1 deletion app/routes/clinics.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ const {
urlWithReferrer,
appendReferrer
} = require('../lib/utils/referrers')
const { getParticipant } = require('../lib/utils/participants')
const { getParticipant, getFullName } = require('../lib/utils/participants')
const { updateAppointmentStatus } = require('../lib/utils/appointment-status')
const { getAppointment } = require('../lib/utils/appointment-data')

/**
* Get clinic and its related data from id
Expand Down Expand Up @@ -163,6 +164,197 @@ module.exports = (router) => {
res.redirect(returnUrl)
})

// Close clinic page
router.get('/clinics/:id/close', (req, res) => {
const clinicData = getClinicData(req.session.data, req.params.id)

if (!clinicData) {
return res.redirect('/clinics')
}

const resolvedKey = `closeClinicResolved_${req.params.id}`

// Group by status so similar statuses appear together
const statusOrder = ["in_progress", "paused", "checked_in", "scheduled", "attended_not_screened", "did_not_attend", "complete", "partially_screened", "cancelled", "rescheduled"]
const sortedAppointments = [...clinicData.appointments].sort((a, b) =>
statusOrder.indexOf(a.status) - statusOrder.indexOf(b.status)
)

res.render('clinics/close', {
clinicId: req.params.id,
clinic: clinicData.clinic,
allAppointments: sortedAppointments
})
})

// Mark appointment as attended not screened from close clinic page
router.get('/clinics/:id/close/attended-not-screened/:appointmentId', (req, res) => {
const { id, appointmentId } = req.params
updateAppointmentStatus(req.session.data, appointmentId, 'attended_not_screened')
const resolvedKey = `closeClinicResolved_${id}`
if (!req.session[resolvedKey]) req.session[resolvedKey] = []
if (!req.session[resolvedKey].includes(appointmentId)) req.session[resolvedKey].push(appointmentId)
if (req.headers.accept?.includes('application/json')) {
return res.json({ status: 'success' })
}
res.redirect(`/clinics/${id}/close`)
})

// Undo attended not screened
router.get('/clinics/:id/close/undo-attended-not-screened/:appointmentId', (req, res) => {
const { id, appointmentId } = req.params
updateAppointmentStatus(req.session.data, appointmentId, 'checked_in')
const resolvedKey = `closeClinicResolved_${id}`
if (req.session[resolvedKey]) req.session[resolvedKey] = req.session[resolvedKey].filter((i) => i !== appointmentId)
if (req.headers.accept?.includes('application/json')) {
return res.json({ status: 'success' })
}
res.redirect(`/clinics/${id}/close`)
})

// Mark appointment as did not attend from close clinic page
router.get('/clinics/:id/close/did-not-attend/:appointmentId', (req, res) => {
const { id, appointmentId } = req.params
updateAppointmentStatus(req.session.data, appointmentId, 'did_not_attend')
const resolvedKey = `closeClinicResolved_${id}`
if (!req.session[resolvedKey]) req.session[resolvedKey] = []
if (!req.session[resolvedKey].includes(appointmentId)) req.session[resolvedKey].push(appointmentId)
if (req.headers.accept?.includes('application/json')) {
return res.json({ status: 'success' })
}
res.redirect(`/clinics/${id}/close`)
})

// Undo did not attend
router.get('/clinics/:id/close/undo-did-not-attend/:appointmentId', (req, res) => {
const { id, appointmentId } = req.params
updateAppointmentStatus(req.session.data, appointmentId, 'scheduled')
const resolvedKey = `closeClinicResolved_${id}`
if (req.session[resolvedKey]) req.session[resolvedKey] = req.session[resolvedKey].filter((i) => i !== appointmentId)
if (req.headers.accept?.includes('application/json')) {
return res.json({ status: 'success' })
}
res.redirect(`/clinics/${id}/close`)
})

// Bulk mark all checked-in as attended not screened
router.get('/clinics/:id/close/attended-not-screened-all', (req, res) => {
const { id } = req.params
const data = req.session.data
const appointments = data.appointments.filter(
(a) => a.clinicId === id && a.status === 'checked_in'
)
const resolvedKey = `closeClinicResolved_${id}`
if (!req.session[resolvedKey]) req.session[resolvedKey] = []
appointments.forEach((a) => {
updateAppointmentStatus(data, a.id, 'attended_not_screened')
if (!req.session[resolvedKey].includes(a.id)) req.session[resolvedKey].push(a.id)
})
if (req.headers.accept?.includes('application/json')) {
return res.json({ status: 'success', count: appointments.length })
}
res.redirect(`/clinics/${id}/close`)
})

// Bulk undo attended not screened (revert to checked_in)
router.get('/clinics/:id/close/undo-attended-not-screened-all', (req, res) => {
const { id } = req.params
const data = req.session.data
const resolvedKey = `closeClinicResolved_${id}`
const resolvedIds = req.session[resolvedKey] || []
const appointments = data.appointments.filter(
(a) => a.clinicId === id && a.status === 'attended_not_screened' && resolvedIds.includes(a.id)
)
appointments.forEach((a) => {
updateAppointmentStatus(data, a.id, 'checked_in')
})
if (req.session[resolvedKey]) {
req.session[resolvedKey] = req.session[resolvedKey].filter(
(i) => !appointments.find((a) => a.id === i)
)
}
if (req.headers.accept?.includes('application/json')) {
return res.json({ status: 'success', count: appointments.length })
}
res.redirect(`/clinics/${id}/close`)
})

// Bulk mark all remaining as did not attend
router.get('/clinics/:id/close/did-not-attend-all', (req, res) => {
const { id } = req.params
const data = req.session.data
const appointments = data.appointments.filter(
(a) => a.clinicId === id && a.status === 'scheduled'
)
const resolvedKey = `closeClinicResolved_${id}`
if (!req.session[resolvedKey]) req.session[resolvedKey] = []
appointments.forEach((a) => {
updateAppointmentStatus(data, a.id, 'did_not_attend')
if (!req.session[resolvedKey].includes(a.id)) req.session[resolvedKey].push(a.id)
})
if (req.headers.accept?.includes('application/json')) {
return res.json({ status: 'success', count: appointments.length })
}
res.redirect(`/clinics/${id}/close`)
})

// Bulk undo did not attend (revert to scheduled)
router.get('/clinics/:id/close/undo-did-not-attend-all', (req, res) => {
const { id } = req.params
const data = req.session.data
const resolvedKey = `closeClinicResolved_${id}`
const resolvedIds = req.session[resolvedKey] || []
const appointments = data.appointments.filter(
(a) => a.clinicId === id && a.status === 'did_not_attend' && resolvedIds.includes(a.id)
)
appointments.forEach((a) => {
updateAppointmentStatus(data, a.id, 'scheduled')
})
if (req.session[resolvedKey]) {
req.session[resolvedKey] = req.session[resolvedKey].filter(
(i) => !appointments.find((a) => a.id === i)
)
}
if (req.headers.accept?.includes('application/json')) {
return res.json({ status: 'success', count: appointments.length })
}
res.redirect(`/clinics/${id}/close`)
})

// Confirm and close clinic
router.post('/clinics/:id/close', (req, res) => {
const { id } = req.params
const data = req.session.data

// Check all appointments have a final outcome
const finalStatuses = ['complete', 'partially_screened', 'did_not_attend', 'attended_not_screened', 'cancelled', 'rescheduled']
const clinicAppointments = data.appointments.filter((a) => a.clinicId === id)
const unresolved = clinicAppointments.filter((a) => !finalStatuses.includes(a.status))

if (unresolved.length > 0) {
req.flash('error', [{
text: `${unresolved.length} participant${unresolved.length === 1 ? '' : 's'} still need${unresolved.length === 1 ? 's' : ''} an outcome recorded before the clinic can be closed`
}])
return res.redirect(`/clinics/${id}/close`)
}

const clinicIndex = data.clinics.findIndex((c) => c.id === id)

if (clinicIndex !== -1) {
const updatedClinic = { ...data.clinics[clinicIndex], status: 'closed' }
data.clinics[clinicIndex] = updatedClinic
if (data._changes?.clinics) {
data._changes.clinics[id] = updatedClinic
}
req.flash('success', `Clinic ${updatedClinic.clinicCode} closed`)
}

// Clean up resolved tracking
delete req.session[`closeClinicResolved_${id}`]

res.redirect('/clinics/completed')
})

// Single clinic view
const VALID_FILTERS = [
'remaining',
Expand Down
Loading