Skip to content

Commit e4ae549

Browse files
Add support for special appointments and start on mamography workflow
1 parent bf60b72 commit e4ae549

File tree

13 files changed

+506
-259
lines changed

13 files changed

+506
-259
lines changed

app/filters/nunjucks.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// app/filters/nunjucks.js
2+
3+
4+
5+
const log = (a, description=false) => {
6+
7+
if (description){
8+
description = `console.log("${description}:");`
9+
}
10+
return `<script>${description}console.log(${JSON.stringify(a, null, '\t')});</script>`
11+
}
12+
13+
module.exports = {
14+
log
15+
}

app/lib/generators/event-generator.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,14 @@ const generateEvent = ({ slot, participant, clinic, outcomeWeights }) => {
6464
const today = simulatedDateTime.startOf('day');
6565
const isPast = slotDateTime.isBefore(simulatedDateTime);
6666

67-
// If it's an assessment clinic, override the outcome weights
67+
// Check if this is a special event (participant has extra needs)
68+
const isSpecialAppointment = Boolean(participant.extraNeeds?.length);
69+
70+
// Double the duration for participants with extra needs
71+
const duration = isSpecialAppointment ? slot.duration * 2 : slot.duration;
72+
const endDateTime = dayjs(slot.dateTime).add(duration, 'minute');
73+
74+
// If it’s an assessment clinic, override the outcome weights
6875
const eventWeights = clinic.clinicType === 'assessment' ?
6976
{
7077
'clear': 0.4,
@@ -84,13 +91,15 @@ const generateEvent = ({ slot, participant, clinic, outcomeWeights }) => {
8491
type: clinic.clinicType,
8592
timing: {
8693
startTime: slot.dateTime,
87-
endTime: slot.endDateTime,
88-
duration: slot.duration
94+
endTime: endDateTime.toISOString(),
95+
duration
8996
},
9097
status: 'scheduled',
9198
details: {
9299
screeningType: 'mammogram',
93-
machineId: generateId()
100+
machineId: generateId(),
101+
isSpecialAppointment,
102+
extraNeeds: participant.extraNeeds
94103
},
95104
statusHistory: [
96105
{
@@ -110,8 +119,7 @@ const generateEvent = ({ slot, participant, clinic, outcomeWeights }) => {
110119
...eventBase,
111120
status,
112121
details: {
113-
screeningType: 'mammogram',
114-
machineId: generateId(),
122+
...eventBase.details,
115123
imagesTaken: status === 'attended' ?
116124
['RCC', 'LCC', 'RMLO', 'LMLO'] : null,
117125
notScreenedReason: status === 'attended_not_screened' ?
@@ -122,10 +130,14 @@ const generateEvent = ({ slot, participant, clinic, outcomeWeights }) => {
122130

123131
if (status === 'attended') {
124132
const actualStartOffset = faker.number.int({ min: -5, max: 5 });
125-
const actualDurationOffset = faker.number.int({ min: -3, max: 5 });
133+
134+
// For special events, allow more time variation
135+
const durationOffset = isSpecialAppointment ?
136+
faker.number.int({ min: -3, max: 10 }) :
137+
faker.number.int({ min: -3, max: 5 });
126138

127139
const actualStartTime = slotDateTime.add(actualStartOffset, 'minute');
128-
const actualEndTime = actualStartTime.add(slot.duration + actualDurationOffset, 'minute');
140+
const actualEndTime = actualStartTime.add(slot.duration + durationOffset, 'minute');
129141

130142
event.timing = {
131143
...event.timing,

app/lib/generators/participant-generator.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,40 @@ const generateId = require('../utils/id-generator');
44
const weighted = require('weighted');
55
const { generateBSUAppropriateAddress } = require('./address-generator');
66

7+
// List of possible extra needs
8+
const EXTRA_NEEDS = [
9+
'Agoraphobia',
10+
'Implants',
11+
'Learning difficulties',
12+
'Physical restriction',
13+
'Registered disabled',
14+
'Social reasons',
15+
'Wheelchair user',
16+
'Transgender',
17+
// 'Other' // need to come up with some free text replies before using this
18+
]
19+
20+
// Generate extra needs for a participant
21+
const generateExtraNeeds = (config = { probability: 0.08 }) => {
22+
// Check if they should have extra needs
23+
if (Math.random() > config.probability) {
24+
return null
25+
}
26+
27+
// Use weighted to determine how many needs they should have
28+
const needCount = weighted.select({
29+
1: 0.7, // 70% chance of 1 need
30+
2: 0.2, // 20% chance of 2 needs
31+
3: 0.1 // 10% chance of 3 needs
32+
})
33+
34+
// Select that many random needs
35+
return faker.helpers.arrayElements(EXTRA_NEEDS, {
36+
min: needCount,
37+
max: needCount
38+
})
39+
}
40+
741
// Generate a UK phone number
842
const generateUKPhoneNumber = () => {
943
const numberTypes = {
@@ -160,7 +194,11 @@ const generateNonCancerousProcedures = () => {
160194
};
161195

162196

163-
const generateParticipant = ({ ethnicities, breastScreeningUnits }) => {
197+
const generateParticipant = ({
198+
ethnicities,
199+
breastScreeningUnits,
200+
extraNeedsConfig = { probability: 0.08 }
201+
}) => {
164202
const id = generateId();
165203

166204
// Randomly assign to a BSU
@@ -179,6 +217,7 @@ const generateParticipant = ({ ethnicities, breastScreeningUnits }) => {
179217
id,
180218
sxNumber: generateSXNumber(assignedBSU.abbreviation),
181219
assignedBSU: assignedBSU.id,
220+
extraNeeds: generateExtraNeeds(extraNeedsConfig),
182221
demographicInformation: {
183222
firstName: faker.person.firstName('female'),
184223
middleName,

app/routes/events.js

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,75 @@ function getEventData(data, clinicId, eventId) {
2828
}
2929
}
3030

31+
// Update event status and add to history
32+
function updateEventStatus(event, newStatus) {
33+
return {
34+
...event,
35+
status: newStatus,
36+
statusHistory: [
37+
...event.statusHistory,
38+
{
39+
status: newStatus,
40+
timestamp: new Date().toISOString()
41+
}
42+
]
43+
}
44+
}
45+
3146
module.exports = router => {
3247

33-
// Event within clinic context
34-
router.get('/clinics/:clinicId/events/:eventId', (req, res) => {
48+
// Set clinics to active in nav for all urls starting with /clinics
49+
router.use('/clinics/:clinicId/events/:eventId', (req, res, next) => {
3550
const eventData = getEventData(req.session.data, req.params.clinicId, req.params.eventId)
36-
51+
3752
if (!eventData) {
53+
console.log(`No event ${req.params.eventId} found for clinic ${req.params.clinicId}`)
3854
res.redirect('/clinics/' + req.params.clinicId)
3955
return
4056
}
4157

58+
res.locals.eventData = eventData
59+
res.locals.clinic = eventData.clinic,
60+
res.locals.event = eventData.event,
61+
res.locals.participant = eventData.participant,
62+
res.locals.unit = eventData.unit,
63+
res.locals.clinicId = req.params.clinicId,
64+
res.locals.eventId = req.params.eventId
65+
66+
next();
67+
});
68+
69+
// Event within clinic context
70+
router.get('/clinics/:clinicId/events/:eventId', (req, res) => {
4271
res.render('events/show', {
43-
clinic: eventData.clinic,
44-
event: eventData.event,
45-
participant: eventData.participant,
46-
unit: eventData.unit,
47-
clinicId: req.params.clinicId,
48-
eventId: req.params.eventId
4972
})
5073
})
5174

75+
// Event within clinic context
76+
router.get('/clinics/:clinicId/events/:eventId/imaging', (req, res) => {
77+
res.render('events/mamography/imaging', {
78+
})
79+
})
80+
81+
// // Advance status to attened / complete
82+
// router.post('/clinics/:clinicId/events/:eventId/attended', (req, res) => {
83+
84+
// res.redirect(`/clinics/${req.params.clinicId}/events/${req.params.eventId}`, {
85+
// })
86+
// })
87+
88+
// Handle screening completion
89+
router.post('/clinics/:clinicId/events/:eventId/complete', (req, res) => {
90+
const { clinicId, eventId } = req.params
91+
92+
// Update event status to attended
93+
const eventIndex = req.session.data.events.findIndex(e => e.id === eventId)
94+
req.session.data.events[eventIndex] = updateEventStatus(
95+
req.session.data.events[eventIndex],
96+
'attended'
97+
)
98+
99+
res.redirect(`/clinics/${clinicId}/events/${eventId}`)
100+
})
101+
52102
};
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
{% set dob %}
2+
{{ participant.demographicInformation.dateOfBirth | formatDate }}<br>
3+
<span class="nhsuk-hint">({{ participant.demographicInformation.dateOfBirth | formatRelativeDate(true) }} old)</span>
4+
{% endset %}
5+
6+
{% set address %}
7+
{{ participant.demographicInformation.address.line1 }}<br>
8+
{% if participant.demographicInformation.address.line2 %}
9+
{{ participant.demographicInformation.address.line2 }}<br>
10+
{% endif %}
11+
{{ participant.demographicInformation.address.city }}<br>
12+
{{ participant.demographicInformation.address.postcode }}
13+
{% endset %}
14+
15+
{% set allowEdits = allowEdits | default(true) %}
16+
17+
{{ summaryList({
18+
rows: [
19+
{
20+
key: {
21+
text: "Full name"
22+
},
23+
value: {
24+
text: participant | getFullName
25+
},
26+
actions: {
27+
items: [
28+
{
29+
href: "#",
30+
text: "Change",
31+
visuallyHiddenText: "date of birth"
32+
} if allowEdits
33+
]
34+
}
35+
},
36+
{
37+
key: {
38+
text: "Gender"
39+
},
40+
value: {
41+
text: "Female"
42+
},
43+
actions: {
44+
items: [
45+
]
46+
}
47+
} if not isMinimalParticipant,
48+
{
49+
key: {
50+
text: "NHS number"
51+
},
52+
value: {
53+
text: participant.medicalInformation.nhsNumber | formatNhsNumber
54+
},
55+
actions: {
56+
items: [
57+
]
58+
}
59+
} if not isMinimalParticipant,
60+
{
61+
key: {
62+
text: "SX number"
63+
},
64+
value: {
65+
text: participant.sxNumber
66+
},
67+
actions: {
68+
items: [
69+
{
70+
href: "#",
71+
text: "Change",
72+
visuallyHiddenText: "SX number"
73+
} if allowEdits
74+
]
75+
}
76+
} if not isMinimalParticipant,
77+
{
78+
key: {
79+
text: "Date of birth"
80+
},
81+
value: {
82+
html: dob
83+
},
84+
actions: {
85+
items: [
86+
]
87+
}
88+
},
89+
{
90+
key: {
91+
text: "Address"
92+
},
93+
value: {
94+
html: address
95+
},
96+
actions: {
97+
items: [
98+
{
99+
href: "#",
100+
text: "Change",
101+
visuallyHiddenText: "address"
102+
} if allowEdits
103+
]
104+
}
105+
},
106+
{
107+
key: {
108+
text: "Phone"
109+
},
110+
value: {
111+
text: participant.demographicInformation.phone | formatPhoneNumber
112+
},
113+
actions: {
114+
items: [
115+
{
116+
href: "#",
117+
text: "Change",
118+
visuallyHiddenText: "phone"
119+
} if allowEdits
120+
]
121+
}
122+
} if not isMinimalParticipant,
123+
{
124+
key: {
125+
text: "Email"
126+
},
127+
value: {
128+
text: participant.demographicInformation.email
129+
},
130+
actions: {
131+
items: [
132+
{
133+
href: "#",
134+
text: "Change",
135+
visuallyHiddenText: "email"
136+
} if allowEdits
137+
]
138+
}
139+
} if not isMinimalParticipant
140+
]
141+
}) }}

app/views/_templates/layout-app.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
{% endif %}
8787

8888
{% if formAction or isForm %}
89-
<form action="{{formAction or './'}}" method="{{ formMethod or 'GET'}}">
89+
<form action="{{formAction or './'}}" method="{{ formMethod or 'POST'}}">
9090
{% endif %}
9191
{% block pageContent %}{% endblock %}
9292
{% if formAction or isForm %}

app/views/clinics/show.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ <h1 class="nhsuk-heading-l">
7171
<a href="/clinics/{{ clinicId }}/events/{{ event.id }}" class="nhsuk-link">
7272
{{ participant | getFullName }}
7373
</a>
74+
{% if event.details.isSpecialAppointment %}<br>
75+
{{ tag({
76+
text: "Special appointment",
77+
classes: "nhsuk-tag--orange nhsuk-u-margin-top-2"
78+
})}}
79+
{% endif %}
7480
</td>
7581
<td class="nhsuk-table__cell">{{ participant.medicalInformation.nhsNumber | formatNhsNumber | noWrap }}</td>
7682
<td class="nhsuk-table__cell">

0 commit comments

Comments
 (0)