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
119 changes: 119 additions & 0 deletions app/assets/sass/components/clinic-card.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
@use "nhsuk-frontend/dist/nhsuk" as *;

// Support both class names while templates are being refined.
.app-clinic,
.app-clinc {
border: 1px solid $nhsuk-border-colour;
background-color: nhsuk-colour("white");
@include nhsuk-responsive-margin(4, "bottom");

&__header,
.app-clinic__header {
display: flex;
gap: nhsuk-spacing(3);
align-items: flex-start;
justify-content: space-between;
@include nhsuk-responsive-padding(4);

@include nhsuk-media-query($until: tablet) {
flex-direction: column;
align-items: stretch;
}
}

&__title,
.app-clinic__title {
.nhsuk-tag {
@include nhsuk-responsive-margin(2, "bottom");
}

.nhsuk-heading-xs {
margin-bottom: 0;
}
}

&__nav,
.app-clinic__nav {
margin-left: auto;

@include nhsuk-media-query($until: tablet) {
margin-left: 0;
}
}

&__nav-list,
.app-clinic__nav-list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
gap: nhsuk-spacing(3);

@include nhsuk-media-query($until: tablet) {
flex-wrap: wrap;
}
}

&__nav-item,
.app-clinic__nav-item {
margin: 0;
}

&__content,
.app-clinic__content {
@include nhsuk-font-size(19);
@include nhsuk-responsive-padding(4);
padding-top: 0;
border-bottom: 1px solid $nhsuk-border-colour;
}

&__footer,
.app-clinic__footer {
@include nhsuk-responsive-padding(4);
padding-top: 0;
padding-bottom: 0;
background-color: nhsuk-colour("grey-5");

.nhsuk-body-s {
margin-top: 0;
}

.app-clinic__changes-table {
margin-bottom: 0;

.nhsuk-table__header,
.nhsuk-table__cell {
@include nhsuk-font-size(16);
line-height: 1.35;
}

.nhsuk-table__body .nhsuk-table__row:last-child .nhsuk-table__cell {
border-bottom: 0;
}
}

.app-clinic__changes-definitions {
margin: 0;
}

.app-clinic__changes-pair {
@include nhsuk-responsive-margin(2, "bottom");

&:last-child {
margin-bottom: 0;
}
}

.app-clinic__changes-term {
@include nhsuk-font-size(16);
color: $nhsuk-secondary-text-colour;
margin: 0;
}

.app-clinic__changes-description {
@include nhsuk-font-size(16);
line-height: 1.35;
margin: 0;
}
}
}
1 change: 1 addition & 0 deletions app/assets/sass/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@forward "components/app-card";
@forward "components/secondary-navigation";
@forward "components/appointments-summary";
@forward "components/clinic-card";



Expand Down
9 changes: 2 additions & 7 deletions app/data/session-data-defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,10 @@ const base = {
hideCard: true
},
{
text: 'View appointments',
description: 'View and manage appointments for your site',
text: 'Clinics',
description: 'View appointments for your site',
hrefTemplate: '/site/:id/availability/day'
},
{
text: 'Manage clinics',
description: 'Create clinics and review recently created clinic series',
hrefTemplate: '/site/:id/clinics'
},
{
text: 'Change site details',
description: 'Change site details and accessibility information',
Expand Down
147 changes: 138 additions & 9 deletions app/routes/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,42 @@ function getToday() {
return override_today || DateTime.now().toFormat('yyyy-MM-dd');
}

function getRelativeDayLabel(dateISO, todayISO) {
const target = DateTime.fromISO(dateISO || '').startOf('day');
const today = DateTime.fromISO(todayISO || '').startOf('day');
if (!target.isValid || !today.isValid) return null;

const diffDays = Math.round(target.diff(today, 'days').days);
if (diffDays === 0) return 'Today';
if (diffDays === -1) return 'Yesterday';
if (diffDays === 1) return 'Tomorrow';
return null;
}

function getRelativeWeekLabel(weekStartISO, todayISO) {
const targetWeek = DateTime.fromISO(weekStartISO || '').startOf('week');
const thisWeek = DateTime.fromISO(todayISO || '').startOf('week');
if (!targetWeek.isValid || !thisWeek.isValid) return null;

const diffWeeks = Math.round(targetWeek.diff(thisWeek, 'weeks').weeks);
if (diffWeeks === 0) return 'This week';
if (diffWeeks === -1) return 'Last week';
if (diffWeeks === 1) return 'Next week';
return null;
}

function getRelativeMonthLabel(monthDateISO, todayISO) {
const targetMonth = DateTime.fromISO(monthDateISO || '').startOf('month');
const thisMonth = DateTime.fromISO(todayISO || '').startOf('month');
if (!targetMonth.isValid || !thisMonth.isValid) return null;

const diffMonths = Math.round(targetMonth.diff(thisMonth, 'months').months);
if (diffMonths === 0) return 'This month';
if (diffMonths === -1) return 'Last month';
if (diffMonths === 1) return 'Next month';
return null;
}

function asArray(value) {
if (Array.isArray(value)) return value;
if (value === undefined || value === null || value === '') return [];
Expand Down Expand Up @@ -1143,6 +1179,7 @@ function buildSessionHistory(siteRecurringSessions, startDate = null, endDate =
from: session.from,
until: session.until,
services: session.services || [],
childSessions: asArray(session.childSessions || []).slice().sort((a, b) => String(a?.date || '').localeCompare(String(b?.date || ''))),
capacity: Number(session.capacity) || 0,
slotLength: Number(session.slotLength) || 0
});
Expand Down Expand Up @@ -1211,7 +1248,7 @@ function slotMatchesSession(slot, session) {
&& (!slot?.recurringSessionId || !session?.recurringId || String(slot.recurringSessionId) === String(session.recurringId));
}

function buildWeekAvailabilitySummary(week, dailyAvailability, slotsByDate, servicesById, siteBookings, siteId, today, recurringSessionsById = {}) {
function buildWeekAvailabilitySummary(week, dailyAvailability, slotsByDate, servicesById, siteBookings, siteId, today, recurringSessionsById = {}, backHref = null) {
return week.map((day) => {
const sessions = sortSessionsForAvailability(dailyAvailability?.[day]?.sessions);
const dateSlots = asArray(slotsByDate?.[day]);
Expand All @@ -1222,6 +1259,10 @@ function buildWeekAvailabilitySummary(week, dailyAvailability, slotsByDate, serv
const totalSlots = sessionSlots.length;
const resolvedLabel = session.label || recurringSessionsById?.[session?.recurringId]?.label || '';

const changeHrefBase = day < today || !session?.recurringId
? null
: `/site/${siteId}/change/session/${session.id}`;

return {
id: session.id,
label: resolvedLabel,
Expand All @@ -1238,7 +1279,9 @@ function buildWeekAvailabilitySummary(week, dailyAvailability, slotsByDate, serv
})),
bookedTotal,
unbookedTotal: Math.max(0, totalSlots - bookedTotal),
actionHref: day < today || !session?.recurringId ? null : `/site/${siteId}/change/session/${session.id}`
actionHref: changeHrefBase
? `${changeHrefBase}${backHref ? `?back=${encodeURIComponent(backHref)}` : ''}`
: null
};
});

Expand Down Expand Up @@ -1299,7 +1342,8 @@ function buildMonthAvailabilitySummary(weekRanges, dailyAvailability, slotsByDat
siteBookings,
siteId,
today,
recurringSessionsById
recurringSessionsById,
null
);

const services = new Map();
Expand Down Expand Up @@ -2273,13 +2317,91 @@ router.get('/site/:id/debug/recurring-expansion', (req, res) => {
// VIEW AVAILABILITY
// -----------------------------------------------------------------------------
router.get('/site/:id/availability/day', (req, res) => {
const data = req.session.data;
const site_id = req.site_id;
const date = req.query.date || getToday();
const today = getToday();
const tomorrow = DateTime.fromISO(date).plus({ days: 1 }).toISODate();
const yesterday = DateTime.fromISO(date).minus({ days: 1 }).toISODate();
const daySummary = buildWeekAvailabilitySummary(
[date],
res.locals.dailyAvailability,
res.locals.slots,
data.services || {},
data?.bookings?.[site_id] || {},
site_id,
today,
data?.recurring_sessions?.[site_id] || {},
`/site/${site_id}/availability/day?date=${date}`
)[0] || {
date,
sessions: [],
totalAppointments: 0,
bookedAppointments: 0,
unbookedAppointments: 0,
isToday: date === today,
isPast: date < today,
dayViewHref: `/site/${site_id}/availability/day?date=${date}`
};

if ((daySummary.sessions || []).length === 0 && (res.locals.dailyAvailability?.[date]?.sessions || []).length > 0) {
const dateSlots = asArray(res.locals.slots?.[date]);
const sessions = sortSessionsForAvailability(res.locals.dailyAvailability?.[date]?.sessions);
const siteBookings = data?.bookings?.[site_id] || {};
const servicesById = data.services || {};

const fallbackSessions = sessions.map((session) => {
const sessionSlots = dateSlots.filter((slot) => (
(slot?.sessionId && session?.id && String(slot.sessionId) === String(session.id))
|| (
!slot?.sessionId
&& slot?.group?.start === session?.from
&& slot?.group?.end === session?.until
&& (!slot?.recurringSessionId || !session?.recurringId || String(slot.recurringSessionId) === String(session.recurringId))
)
));

const bookedTotal = sessionSlots.filter((slot) => slot?.booking_status === 'scheduled').length;
const totalSlots = sessionSlots.length;
const resolvedLabel = session.label || data?.recurring_sessions?.[site_id]?.[session?.recurringId]?.label || '';

return {
id: session.id,
label: resolvedLabel,
from: session.from,
until: session.until,
services: asArray(session.services).map((serviceId) => ({
id: serviceId,
name: servicesById?.[serviceId]?.name || serviceId,
bookedCount: sessionSlots.filter((slot) => (
slot?.booking_status === 'scheduled'
&& slot?.booking_id
&& siteBookings?.[slot.booking_id]?.service === serviceId
)).length
})),
bookedTotal,
unbookedTotal: Math.max(0, totalSlots - bookedTotal),
actionHref: date < today || !session?.recurringId
? null
: `/site/${site_id}/change/session/${session.id}?back=${encodeURIComponent(`/site/${site_id}/availability/day?date=${date}`)}`
};
});

daySummary.sessions = fallbackSessions;
daySummary.totalAppointments = fallbackSessions.reduce((sum, session) => sum + session.bookedTotal + session.unbookedTotal, 0);
daySummary.bookedAppointments = fallbackSessions.reduce((sum, session) => sum + session.bookedTotal, 0);
daySummary.unbookedAppointments = Math.max(0, daySummary.totalAppointments - daySummary.bookedAppointments);
}

res.render('site/availability/day', {
date,
today: getToday(),
tomorrow: DateTime.fromISO(date).plus({ days: 1 }).toISODate(),
yesterday: DateTime.fromISO(date).minus({ days: 1 }).toISODate()
today,
tomorrow,
yesterday,
daySummary,
dayHeading: getRelativeDayLabel(date, today),
previousDayLabel: getRelativeDayLabel(yesterday, today),
nextDayLabel: getRelativeDayLabel(tomorrow, today)
});
});

Expand Down Expand Up @@ -2315,7 +2437,8 @@ router.get('/site/:id/availability/week', (req, res) => {
data?.bookings?.[site_id] || {},
site_id,
today,
data?.recurring_sessions?.[site_id] || {}
data?.recurring_sessions?.[site_id] || {},
`/site/${site_id}/availability/week?date=${startFromDate}`
);

res.render('site/availability/week', {
Expand All @@ -2324,7 +2447,10 @@ router.get('/site/:id/availability/week', (req, res) => {
week,
weekDays,
previousWeek,
nextWeek
nextWeek,
weekHeading: getRelativeWeekLabel(week[0], today),
previousWeekLabel: getRelativeWeekLabel(previousWeek.start, today),
nextWeekLabel: getRelativeWeekLabel(nextWeek.start, today)
});
});

Expand All @@ -2349,7 +2475,10 @@ router.get('/site/:id/availability/month', (req, res) => {
currentDate: monthData.currentDate,
previousMonthDate: monthData.previousMonthDate,
nextMonthDate: monthData.nextMonthDate,
monthWeeks
monthWeeks,
monthHeading: getRelativeMonthLabel(monthData.currentDate, today),
previousMonthLabel: getRelativeMonthLabel(monthData.previousMonthDate, today),
nextMonthLabel: getRelativeMonthLabel(monthData.nextMonthDate, today)
});
});

Expand Down
Loading