@@ -16,69 +16,148 @@ const { generateEvent } = require('./generators/event-generator')
1616const breastScreeningUnits = require ( '../data/breast-screening-units' )
1717const 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