Skip to content

Commit 2b63e86

Browse files
Support test scenarios (#26)
* Add support for hard coded test scenarios for user research * Add support for test scenarios and force first clinic of the day to screening
1 parent 74f7ef2 commit 2b63e86

File tree

8 files changed

+325
-124
lines changed

8 files changed

+325
-124
lines changed

app/data/breast-screening-units.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ module.exports = [
1717
clinicTypes: ['screening', 'assessment'], // Can do both
1818
// Default operating hours for the BSU
1919
sessionPatterns: [
20-
{
21-
name: 'full_day',
22-
type: 'single',
23-
sessions: [
24-
{ startTime: '09:00', endTime: '17:00' },
25-
],
26-
},
20+
// {
21+
// name: 'full_day',
22+
// type: 'single',
23+
// sessions: [
24+
// { startTime: '09:00', endTime: '17:00' },
25+
// ],
26+
// },
2727
{
2828
name: 'split_day',
2929
type: 'paired',

app/data/test-scenarios.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// app/data/test-scenarios.js
2+
3+
/**
4+
* Test scenarios define specific participants and events that should always exist
5+
* in the generated data. This ensures we have consistent test cases.
6+
*
7+
* Only specify what needs to be consistent - any unspecified fields will be randomly generated.
8+
* This allows natural variation while maintaining key test conditions.
9+
*/
10+
module.exports = [
11+
{
12+
participant: {
13+
id: 'bc724e9f',
14+
demographicInformation: {
15+
firstName: 'Janet',
16+
middleName: null,
17+
lastName: 'Williams',
18+
dateOfBirth: '1959-07-22',
19+
},
20+
extraNeeds: ['Wheelchair user'],
21+
},
22+
scheduling: {
23+
whenRelativeToToday: 0,
24+
status: 'scheduled',
25+
approximateTime: '10:30',
26+
},
27+
},
28+
{
29+
participant: {
30+
id: 'aab45c3d',
31+
demographicInformation: {
32+
firstName: 'Dianna',
33+
lastName: 'McIntosh',
34+
middleName: 'Rose',
35+
dateOfBirth: '1964-03-15',
36+
},
37+
extraNeeds: null,
38+
},
39+
scheduling: {
40+
whenRelativeToToday: 0,
41+
status: 'checked_in',
42+
approximateTime: '11:30',
43+
// slotIndex: 20,
44+
},
45+
},
46+
]

app/lib/generate-seed-data.js

Lines changed: 174 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,69 +16,148 @@ const { generateEvent } = require('./generators/event-generator')
1616
const breastScreeningUnits = require('../data/breast-screening-units')
1717
const ethnicities = require('../data/ethnicities')
1818

19-
const generateSnapshot = (date, allParticipants, unit) => {
19+
// Hardcoded scenarios for user research
20+
const testScenarios = require('../data/test-scenarios')
21+
22+
// Find nearest slot at or after the target time
23+
// Used by test scenarios so we can populate a slot at a given time
24+
const findNearestSlot = (slots, targetTime) => {
25+
if (!targetTime) return null
26+
27+
const [targetHour, targetMinute] = targetTime.split(':').map(Number)
28+
const targetMinutes = targetHour * 60 + targetMinute
29+
30+
// Filter to only slots at or after target time
31+
const eligibleSlots = slots.filter(slot => {
32+
const slotTime = dayjs(slot.dateTime)
33+
const slotMinutes = slotTime.hour() * 60 + slotTime.minute()
34+
return slotMinutes >= targetMinutes
35+
})
36+
37+
if (eligibleSlots.length === 0) return null
38+
39+
// Find the nearest from eligible slots
40+
return eligibleSlots.reduce((nearest, slot) => {
41+
const slotTime = dayjs(slot.dateTime)
42+
const slotMinutes = slotTime.hour() * 60 + slotTime.minute()
43+
44+
if (!nearest) return slot
45+
46+
const currentDiff = Math.abs(targetMinutes - slotMinutes)
47+
const nearestDiff = Math.abs(
48+
targetMinutes -
49+
(dayjs(nearest.dateTime).hour() * 60 + dayjs(nearest.dateTime).minute())
50+
)
51+
52+
return currentDiff < nearestDiff ? slot : nearest
53+
})
54+
}
55+
56+
const generateClinicsForDay = (date, allParticipants, unit) => {
2057
const clinics = []
2158
const events = []
2259
const usedParticipantsInSnapshot = new Set()
2360
const participants = [...allParticipants]
2461

62+
// Check if this snapshot date is for the recent period (not historical)
63+
const isRecentSnapshot = dayjs(date).isAfter(dayjs().subtract(1, 'month'))
64+
65+
// Only look for test scenarios in recent snapshots
66+
const testScenariosForDay = isRecentSnapshot
67+
? testScenarios.filter(scenario => {
68+
const targetDate = dayjs().startOf('day').add(scenario.scheduling.whenRelativeToToday, 'day')
69+
return targetDate.isSame(dayjs(date).startOf('day'), 'day')
70+
})
71+
: []
72+
2573
// Pre-filter eligible participants once
2674
const clinicDate = dayjs(date)
2775
const eligibleParticipants = participants.filter(p => {
2876
const age = clinicDate.diff(dayjs(p.demographicInformation.dateOfBirth), 'year')
2977
return age >= 50 && age <= 70
3078
})
3179

32-
// Generate a 5 days of clinics
33-
for (let i = 0; i < config.clinics.daysToGenerate; i++) {
34-
const clinicDate = dayjs(date).add(i, 'day')
35-
const newClinics = generateClinicsForBSU({
36-
date: clinicDate.toDate(),
37-
breastScreeningUnit: unit,
38-
})
80+
// Generate clinics for this day
81+
const newClinics = generateClinicsForBSU({
82+
date: date.toDate(),
83+
breastScreeningUnit: unit,
84+
})
85+
86+
// For test scenarios, only use first clinic of the day
87+
if (testScenariosForDay.length > 0 && newClinics.length > 0) {
88+
const firstClinic = newClinics[0]
89+
90+
testScenariosForDay.forEach(scenario => {
91+
const participant = participants.find(p => p.id === scenario.participant.id)
92+
if (!participant) return
3993

40-
newClinics.forEach(clinic => {
41-
const bookableSlots = clinic.slots
42-
.filter(() => Math.random() < config.generation.bookingProbability)
43-
44-
bookableSlots.forEach(slot => {
45-
// Filter from pre-filtered eligible participants
46-
const availableParticipants = eligibleParticipants.filter(p =>
47-
!usedParticipantsInSnapshot.has(p.id)
48-
)
49-
50-
// If we need more participants, create them
51-
if (availableParticipants.length === 0) {
52-
const newParticipant = generateParticipant({
53-
ethnicities,
54-
breastScreeningUnits: [unit],
55-
})
56-
participants.push(newParticipant)
57-
availableParticipants.push(newParticipant)
58-
}
59-
60-
for (let i = 0; i < slot.capacity; i++) {
61-
if (availableParticipants.length === 0) break
62-
const randomIndex = Math.floor(Math.random() * availableParticipants.length)
63-
const participant = availableParticipants[randomIndex]
64-
65-
const event = generateEvent({
66-
slot,
67-
participant,
68-
clinic,
69-
outcomeWeights: config.screening.outcomes[clinic.clinicType],
70-
})
71-
72-
events.push(event)
73-
usedParticipantsInSnapshot.add(participant.id)
74-
availableParticipants.splice(randomIndex, 1)
75-
}
94+
const slot = scenario.scheduling.slotIndex !== undefined
95+
? firstClinic.slots[scenario.scheduling.slotIndex]
96+
: findNearestSlot(firstClinic.slots, scenario.scheduling.approximateTime)
97+
98+
if (!slot) {
99+
console.log(`Warning: Could not find suitable slot for test participant ${participant.id}`)
100+
return
101+
}
102+
103+
const event = generateEvent({
104+
slot,
105+
participant,
106+
clinic: firstClinic,
107+
outcomeWeights: config.screening.outcomes[firstClinic.clinicType],
108+
forceStatus: scenario.scheduling.status,
76109
})
77110

78-
clinics.push(clinic)
111+
events.push(event)
112+
usedParticipantsInSnapshot.add(participant.id)
79113
})
80114
}
81115

116+
// Handle regular clinic slot allocation for all clinics
117+
newClinics.forEach(clinic => {
118+
// Continue with random slot allocation as before
119+
const remainingSlots = clinic.slots
120+
.filter(() => Math.random() < config.generation.bookingProbability)
121+
// Filter out slots used by test participants
122+
.filter(slot => !events.some(e => e.slotId === slot.id))
123+
124+
remainingSlots.forEach(slot => {
125+
// Filter from pre-filtered eligible participants
126+
const availableParticipants = eligibleParticipants.filter(p =>
127+
!usedParticipantsInSnapshot.has(p.id)
128+
)
129+
130+
// If we need more participants, create them
131+
if (availableParticipants.length === 0) {
132+
const newParticipant = generateParticipant({
133+
ethnicities,
134+
breastScreeningUnits: [unit],
135+
})
136+
participants.push(newParticipant)
137+
availableParticipants.push(newParticipant)
138+
}
139+
140+
for (let i = 0; i < slot.capacity; i++) {
141+
if (availableParticipants.length === 0) break
142+
const randomIndex = Math.floor(Math.random() * availableParticipants.length)
143+
const participant = availableParticipants[randomIndex]
144+
145+
const event = generateEvent({
146+
slot,
147+
participant,
148+
clinic,
149+
outcomeWeights: config.screening.outcomes[clinic.clinicType],
150+
})
151+
152+
events.push(event)
153+
usedParticipantsInSnapshot.add(participant.id)
154+
availableParticipants.splice(randomIndex, 1)
155+
}
156+
})
157+
158+
clinics.push(clinic)
159+
})
160+
82161
return {
83162
clinics,
84163
events,
@@ -91,24 +170,54 @@ const generateData = async () => {
91170
fs.mkdirSync(config.paths.generatedData, { recursive: true })
92171
}
93172

94-
console.log('Generating initial participants...')
95-
const participants = Array.from(
173+
// Create test participants first, using generateParticipant but with overrides
174+
console.log('Generating test scenario participants...')
175+
const testParticipants = testScenarios.map(scenario => {
176+
return generateParticipant({
177+
ethnicities,
178+
breastScreeningUnits,
179+
overrides: scenario.participant,
180+
})
181+
})
182+
183+
console.log('Generating random participants...')
184+
const randomParticipants = Array.from(
96185
{ length: config.generation.numberOfParticipants },
97186
() => generateParticipant({ ethnicities, breastScreeningUnits })
98187
)
99188

189+
// Combine test and random participants
190+
const participants = [...testParticipants, ...randomParticipants]
191+
100192
console.log('Generating clinics and events...')
101193
const today = dayjs().startOf('day')
102-
const snapshots = [
103-
today.subtract(9, 'year').add(3, 'month'),
104-
today.subtract(6, 'year').add(2, 'month'),
105-
today.subtract(3, 'year').add(1, 'month'),
106-
today.subtract(config.clinics.daysBeforeToday, 'days'),
194+
195+
// Helper function to generate multiple days from a start date
196+
const generateDayRange = (startDate, numberOfDays) => {
197+
return Array.from(
198+
{ length: numberOfDays },
199+
(_, i) => dayjs(startDate).add(i, 'days')
200+
)
201+
}
202+
203+
// Generate days for each historical period
204+
const historicalSnapshots = [
205+
...generateDayRange(today.subtract(9, 'year').add(3, 'month'), config.clinics.daysToGenerate),
206+
...generateDayRange(today.subtract(6, 'year').add(2, 'month'), config.clinics.daysToGenerate),
207+
...generateDayRange(today.subtract(3, 'year').add(1, 'month'), config.clinics.daysToGenerate),
107208
]
108209

210+
// Generate recent days
211+
const recentSnapshots = generateDayRange(
212+
today.subtract(config.clinics.daysBeforeToday, 'days'),
213+
config.clinics.daysToGenerate
214+
)
215+
216+
const snapshots = [...historicalSnapshots, ...recentSnapshots]
217+
109218
// Generate all data in batches per BSU
110219
const allData = breastScreeningUnits.map(unit => {
111-
const unitSnapshots = snapshots.map(date => generateSnapshot(date, participants, unit))
220+
const unitSnapshots = snapshots.map(date => generateClinicsForDay(date, participants, unit))
112221
return {
113222
clinics: [].concat(...unitSnapshots.map(s => s.clinics)),
114223
events: [].concat(...unitSnapshots.map(s => s.events)),
@@ -124,6 +233,17 @@ const generateData = async () => {
124233
// Combine initial and new participants
125234
const finalParticipants = [...participants, ...allNewParticipants]
126235

236+
// Sort events by start time within each clinic
237+
const sortedEvents = allEvents.sort((a, b) => {
238+
// First sort by clinic ID to group events together
239+
if (a.clinicId !== b.clinicId) {
240+
return a.clinicId.localeCompare(b.clinicId)
241+
}
242+
// Then by start time within each clinic
243+
return new Date(a.timing.startTime) - new Date(b.timing.startTime)
244+
})
245+
246+
127247
// breastScreeningUnits.forEach(unit => {
128248
// snapshots.forEach(date => {
129249
// const { clinics, events, newParticipants } = generateSnapshot(date, participants, unit);
@@ -147,7 +267,7 @@ const generateData = async () => {
147267
slots: clinic.slots.sort((a, b) => new Date(a.dateTime) - new Date(b.dateTime)),
148268
})),
149269
})
150-
writeData('events.json', { events: allEvents })
270+
writeData('events.json', { events: sortedEvents })
151271
writeData('generation-info.json', {
152272
generatedAt: new Date().toISOString(),
153273
stats: {

0 commit comments

Comments
 (0)