diff --git a/app/assets/javascript/close-clinic.js b/app/assets/javascript/close-clinic.js index 7345f448..52eec296 100644 --- a/app/assets/javascript/close-clinic.js +++ b/app/assets/javascript/close-clinic.js @@ -12,6 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { const clinicId = container.dataset.clinicId const fetchOptions = { headers: { 'X-Requested-With': 'XMLHttpRequest' } } + let detailsToOpen = null // Counts in the card headings and inset text aren't updated in place - // this link invites a refresh instead @@ -21,7 +22,22 @@ document.addEventListener('DOMContentLoaded', () => { } // Any swapped row means the page counts may be stale - container.addEventListener('fragment:swapped', showRefreshLink) + container.addEventListener('fragment:swapped', (event) => { + showRefreshLink() + + if (detailsToOpen !== event.detail.fragment.dataset.fragmentId) return + + const appointmentId = detailsToOpen + detailsToOpen = null + window.openModal('app-form-modal', { + loadUrl: `/clinics/${clinicId}/close/reason/${appointmentId}`, + onSuccess: () => { + const row = rowFor(appointmentId) + if (!row) return window.location.reload() + refreshRow(row).catch(() => window.location.reload()) + } + }) + }) const rowFor = (appointmentId) => container.querySelector(`tr[data-fragment-id="${appointmentId}"]`) @@ -64,6 +80,11 @@ document.addEventListener('DOMContentLoaded', () => { } container.addEventListener('click', (event) => { + const actionLink = event.target.closest('a[data-open-details-after-action]') + if (actionLink) { + detailsToOpen = actionLink.closest('[data-fragment-id]')?.dataset.fragmentId + } + const bulkLink = event.target.closest('.js-bulk-action') if (bulkLink) { event.preventDefault() diff --git a/app/assets/javascript/modal.js b/app/assets/javascript/modal.js index ebec212a..11a03a0a 100644 --- a/app/assets/javascript/modal.js +++ b/app/assets/javascript/modal.js @@ -720,7 +720,7 @@ class AppModal { // Flow complete — close and refresh. Capture the callback before // close(), which resets it. const onSuccess = this._onSuccessCallback - this.close() + this.close({ notifyClose: false }) if (onSuccess) { onSuccess() } else { diff --git a/app/routes/clinics.js b/app/routes/clinics.js index 38051912..fde37a9c 100644 --- a/app/routes/clinics.js +++ b/app/routes/clinics.js @@ -66,7 +66,9 @@ const CLOSE_STATUS_ACTIONS = { attended_not_screened: { from: 'checked_in', resolves: true }, did_not_attend: { from: 'scheduled', resolves: true }, checked_in: { from: 'attended_not_screened', resolves: false }, - scheduled: { from: 'did_not_attend', resolves: false } + scheduled: { from: 'did_not_attend', resolves: false }, + checked_in_from_scheduled: { from: 'scheduled', to: 'checked_in', resolves: false }, + scheduled_from_checked_in: { from: 'checked_in', to: 'scheduled', resolves: false } } /** @@ -261,7 +263,7 @@ module.exports = (router) => { } const data = req.session.data - updateAppointmentStatus(data, appointmentId, status) + updateAppointmentStatus(data, appointmentId, action.to || status) trackCloseResolvedIds(data, clinicId, [appointmentId], action.resolves) if (req.xhr) { @@ -454,7 +456,9 @@ module.exports = (router) => { const updatedClinic = updateClinic(data, clinicId, { status: 'closed' }) if (updatedClinic) { - req.flash('success', `Clinic ${updatedClinic.clinicCode} closed`) + req.flash('success', { + html: `Clinic ${updatedClinic.clinicCode} closed. View report` + }) } // This clinic's close flow is finished - drop its resolved tracking diff --git a/app/views/_includes/close-clinic-appointment-row.njk b/app/views/_includes/close-clinic-appointment-row.njk index bb495058..ce31b494 100644 --- a/app/views/_includes/close-clinic-appointment-row.njk +++ b/app/views/_includes/close-clinic-appointment-row.njk @@ -16,35 +16,53 @@ {% macro closeClinicAppointmentRow(appointment, participant, clinicId, showActions) %} {% set detailsLink = { - text: "Manage details" if appointment | hasStoppedDetails else "Add details", + text: "Change details" if appointment | hasStoppedDetails else "Add details", href: "/clinics/" + clinicId + "/close/reason/" + appointment.id } %} {{ appointment.statusHistory[0].timestamp | formatTimeString }} -

{{ participant | getFullName }}

+

{{ participant | getFullNameReversed }}

NHS: {{ participant.medicalInformation.nhsNumber | formatNhsNumber }}

View appointment

{{ appointment.status | toTag({ vocabulary: "appointment" }) }} + {% if appointment.status == "in_progress" and appointment.sessionDetails.startedBy %} + with {{ appointment.sessionDetails.startedBy | getUsername({ + format: 'short', + identifyCurrentUser: true + }) }} + {% endif %} {% if appointment.status == "rescheduled" and appointment | hasStoppedDetails %} {{ "attended_not_screened" | toTag({ vocabulary: "appointment" }) }} {% endif %} + {% if appointment | hasStoppedDetails %} +

{{ appointment.appointmentStopped.stoppedReason | join(", ") }}

+ {% endif %} + {% if showActions and appointment.status == "checked_in" %} +

+ Change to scheduled +

+ {% elseif showActions and appointment.status == "scheduled" %} +

+ Change to checked in +

+ {% endif %} {% if showActions %} {% if appointment.status == "in_progress" or appointment.status == "paused" %} Go to appointment {% elseif appointment.status == "checked_in" %} - Mark as attended not screened + Mark as attended not screened {% elseif appointment.status == "scheduled" %} Mark as did not attend {% elseif appointment.status == "attended_not_screened" %} - Undo + Undo status
{{ appLink(detailsLink | openInModal) }} {% elseif appointment.status == "did_not_attend" %} - Undo + Undo status {% elseif appointment.status == "rescheduled" and appointment | hasStoppedDetails %} {{ appLink(detailsLink | openInModal) }} {% endif %} diff --git a/app/views/clinics/close.html b/app/views/clinics/close.html index 234d699f..d4fcb423 100644 --- a/app/views/clinics/close.html +++ b/app/views/clinics/close.html @@ -51,8 +51,7 @@

{% set introHtml %} -

Record an appointment outcome for every participant to close this clinic.

-

There were {{ appointmentCount }} total participants in this clinic, and {{ needsOutcomeCount }} still need a final outcome assigned.

+

{{ needsOutcomeCount }} appointments need a completed status before this clinic can be closed.

{% endset %} {{ insetText({ @@ -63,26 +62,28 @@

{% set needsOutcomeHtml %} {% if inProgressAppointments | length %}

In progress

-

Complete or end these appointments to close the clinic.

+ {% if inProgressAppointments | length == 1 %} +

This appointment needs to be completed or ended by the mammographer it is assigned to.

+ {% else %} +

These appointments need to be completed or ended by the mammographers they are assigned to.

+ {% endif %} {{ statusGroupTable(inProgressAppointments, clinicId, true) }} {% endif %} - {# Checked in, not screened - includes attended not screened appointments still needing details #} + {# Checked in, not started - includes attended not screened appointments still needing details #} {% if checkedInAppointments | length %} -

Checked in, not screened

- {{ bulkActionControl(clinicId, "attended_not_screened", "checked_in", "Mark all as attended not screened", "attended not screened") }} +

Checked in, not started

{{ statusGroupTable(checkedInAppointments, clinicId, true) }} {% endif %} {% if scheduledAppointments | length %} -

Did not check in

- {{ bulkActionControl(clinicId, "did_not_attend", "scheduled", "Mark all as did not attend", "did not attend") }} +

Not checked in

{{ statusGroupTable(scheduledAppointments, clinicId, true) }} {% endif %} {% endset %} {{ card({ - heading: "Needs an outcome (" + needsOutcomeCount + ")", + heading: "To be completed (" + needsOutcomeCount + ")", headingLevel: "2", feature: true, descriptionHtml: needsOutcomeHtml @@ -90,14 +91,14 @@

Did not check in

{% endif %} {% set outcomeRecordedHtml %} -

{% if outcomeRecordedAppointments | length %} {{ statusGroupTable(outcomeRecordedAppointments, clinicId, false) }} {% endif %} +

Some appointment statuses have been updated.

{% endset %} {{ card({ - heading: "Outcome recorded (" + outcomeRecordedAppointments | length + ")", + heading: "Completed appointments (" + outcomeRecordedAppointments | length + ")", headingLevel: "2", feature: true, classes: "app-card--feature-green", diff --git a/package-lock.json b/package-lock.json index ceae4ff0..1ecfd6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1470,6 +1470,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -1487,15 +1488,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", - "peer": true, "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -1524,6 +1523,7 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1627,8 +1627,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/async": { "version": "2.6.4", @@ -1973,6 +1972,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", @@ -2090,6 +2090,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2173,7 +2174,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 6" } @@ -2284,7 +2284,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2298,7 +2297,6 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -2907,6 +2905,7 @@ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3005,6 +3004,7 @@ "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], @@ -3197,7 +3197,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3296,7 +3295,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.6.0" } @@ -3401,7 +3399,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -3518,7 +3515,6 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -3528,7 +3524,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -3852,7 +3847,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.10" } @@ -3960,8 +3954,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-wsl": { "version": "1.1.0", @@ -4191,7 +4184,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -4334,7 +4326,6 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -4344,6 +4335,7 @@ "resolved": "https://registry.npmjs.org/nhsuk-frontend/-/nhsuk-frontend-10.6.0.tgz", "integrity": "sha512-ZhnQiK6q6C8d0nwwzYSI+dSPuvJMqVPPtCHCoQif75L2Y8QW4Im0Q76UvqrNWkPAkX/lV2W1QNbot27ej61+3g==", "license": "MIT", + "peer": true, "engines": { "node": "^20.9.0 || ^22.11.0 || >= 24.11.0" }, @@ -4568,7 +4560,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", - "peer": true, "dependencies": { "wrappy": "1" } @@ -4687,7 +4678,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -4814,6 +4804,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", @@ -5327,7 +5318,6 @@ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", - "peer": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -5582,7 +5572,6 @@ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -5683,6 +5672,7 @@ "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.100.0.tgz", "integrity": "sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw==", "license": "MIT", + "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", @@ -6089,7 +6079,6 @@ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", @@ -6230,7 +6219,6 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", - "peer": true, "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -6961,8 +6949,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/ws": { "version": "8.21.0",