Skip to content

Commit a5049de

Browse files
Add generators and filters
1 parent 97cd5a4 commit a5049de

16 files changed

+991
-40
lines changed

app/data/breast-screening-units.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
module.exports = [
22
{
33
id: "f66f2a7d-99a8-4793-8371-3d075e1a7c54",
4-
name: "Oxford Breast screening unit",
4+
name: "Oxford Breast Imaging Centre",
5+
address: `Surgery and Diagnostics Centre
6+
Churchill Hospital
7+
Old Road,
8+
Headington
9+
Oxford
10+
OX3 7LE`,
11+
phoneNumber: "01865 235621",
12+
abbreviation: "OXF"
513
}
614
]

app/data/ethnicities.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module.exports = {
2+
"Asian or Asian British": [
3+
"Bangladeshi",
4+
"Chinese",
5+
"Indian",
6+
"Pakistani",
7+
"Another Asian background"
8+
],
9+
"Black, African, Black British or Caribbean": [
10+
"African",
11+
"Caribbean",
12+
"Another black background"
13+
],
14+
"Mixed or multiple ethnic groups": [
15+
"Asian and White",
16+
"Black African and White",
17+
"Black Caribbean and White",
18+
"Another mixed background",
19+
],
20+
"White": [
21+
"British, English, Northern Irish, Scottish, or Welsh",
22+
"Irish",
23+
"Irish Traveller or Gypsy",
24+
"Another White background"
25+
],
26+
"Another ethnic group": [
27+
"Arab",
28+
"Another ethnic background"
29+
]
30+
}

app/data/session-data-defaults.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
1-
const users = require("./users")
2-
const breastScreeningUnits = require("./breast-screening-units.js")
1+
// app/data/session-data-defaults.js
32

3+
const users = require("./users");
4+
const breastScreeningUnits = require("./breast-screening-units");
5+
const ethnicities = require("./ethnicities");
46

7+
// Load generated data
8+
let participants = [];
9+
let clinics = [];
10+
let events = [];
11+
12+
try {
13+
participants = require("./generated/participants.json").participants;
14+
clinics = require("./generated/clinics.json").clinics;
15+
events = require("./generated/events.json").events;
16+
} catch (err) {
17+
console.warn('Generated data files not found. Please run the data generator first.');
18+
}
519

620
module.exports = {
721
users,
822
currentUser: users[0],
9-
breastScreeningUnits
10-
}
23+
breastScreeningUnits,
24+
participants,
25+
clinics,
26+
events
27+
};

app/data/users.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = [
33
firstName: "Jane",
44
lastName: "Hitchin",
55
role: "mamographer",
6-
id: "ae7537b3-aed1-4620-87fd-9dc5b5bdc8cb"
6+
id: "ae7537b3-aed1-4620-87fd-9dc5b5bdc8cb",
7+
breastScreeningUnit: "f66f2a7d-99a8-4793-8371-3d075e1a7c54"
78
}
89
]

app/filters.js

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
14
module.exports = function (env) { /* eslint-disable-line func-names,no-unused-vars */
25
/**
36
* Instantiate object used to store the methods registered as a
@@ -7,39 +10,32 @@ module.exports = function (env) { /* eslint-disable-line func-names,no-unused-va
710
*/
811
const filters = {};
912

10-
/* ------------------------------------------------------------------
11-
add your methods to the filters obj below this comment block:
12-
@example:
13-
14-
filters.sayHi = function(name) {
15-
return 'Hi ' + name + '!'
16-
}
17-
18-
Which in your templates would be used as:
19-
20-
{{ 'Paul' | sayHi }} => 'Hi Paul'
21-
22-
Notice the first argument of your filters method is whatever
23-
gets 'piped' via '|' to the filter.
24-
25-
Filters can take additional arguments, for example:
26-
27-
filters.sayHi = function(name,tone) {
28-
return (tone == 'formal' ? 'Greetings' : 'Hi') + ' ' + name + '!'
29-
}
30-
31-
Which would be used like this:
32-
33-
{{ 'Joel' | sayHi('formal') }} => 'Greetings Joel!'
34-
{{ 'Gemma' | sayHi }} => 'Hi Gemma!'
35-
36-
For more on filters and how to write them see the Nunjucks
37-
documentation.
13+
// Get all files from utils directory
14+
const utilsPath = path.join(__dirname, 'lib/utils');
15+
16+
try {
17+
// Read all files in the utils directory
18+
const files = fs.readdirSync(utilsPath);
19+
20+
files.forEach(file => {
21+
// Only process .js files
22+
if (path.extname(file) === '.js') {
23+
// Get the utils module
24+
const utils = require(path.join(utilsPath, file));
25+
26+
// Add each exported function as a filter
27+
Object.entries(utils).forEach(([name, func]) => {
28+
// Only add if it's a function
29+
if (typeof func === 'function') {
30+
filters[name] = func;
31+
}
32+
});
33+
}
34+
});
35+
} catch (err) {
36+
console.warn('Error loading filters from utils:', err);
37+
}
3838

39-
------------------------------------------------------------------ */
4039

41-
/* ------------------------------------------------------------------
42-
keep the following line to return your filters to the app
43-
------------------------------------------------------------------ */
4440
return filters;
4541
};

app/lib/generate-seed-data.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// app/lib/generate-seed-data.js
2+
3+
const { faker } = require('@faker-js/faker');
4+
const weighted = require('weighted');
5+
const fs = require('fs');
6+
const path = require('path');
7+
8+
const { generateParticipant } = require('./generators/participant-generator');
9+
const { generateClinicsForBSU } = require('./generators/clinic-generator');
10+
const { generateEvent } = require('./generators/event-generator');
11+
12+
// Load existing data
13+
const breastScreeningUnits = require('../data/breast-screening-units');
14+
const ethnicities = require('../data/ethnicities');
15+
16+
const CONFIG = {
17+
numberOfParticipants: 1000,
18+
outputPath: path.join(__dirname, '../data/generated'),
19+
clinicDefaults: {
20+
slotsPerDay: 32,
21+
daysToGenerate: 5,
22+
startTime: '09:00',
23+
endTime: '17:00',
24+
slotDurationMinutes: 8
25+
},
26+
eventOutcomes: {
27+
'clear': 0.95,
28+
'needs_further_tests': 0.04,
29+
'cancer_detected': 0.01
30+
}
31+
};
32+
33+
const generateData = async () => {
34+
// Create output directory if it doesn't exist
35+
if (!fs.existsSync(CONFIG.outputPath)) {
36+
fs.mkdirSync(CONFIG.outputPath, { recursive: true });
37+
}
38+
39+
// Generate base data
40+
console.log('Generating participants...');
41+
const participants = Array.from({ length: CONFIG.numberOfParticipants }, () =>
42+
generateParticipant({ ethnicities, breastScreeningUnits })
43+
);
44+
45+
console.log('Generating clinics and events...');
46+
const clinics = [];
47+
const events = [];
48+
49+
// Calculate date range
50+
const startDate = new Date();
51+
startDate.setDate(startDate.getDate() - 3);
52+
53+
for (let i = 0; i < CONFIG.clinicDefaults.daysToGenerate; i++) {
54+
const clinicDate = new Date(startDate);
55+
clinicDate.setDate(clinicDate.getDate() + i);
56+
57+
// Generate clinics for each BSU (currently just Oxford)
58+
breastScreeningUnits.forEach(unit => {
59+
const newClinics = generateClinicsForBSU({
60+
date: clinicDate,
61+
breastScreeningUnit: unit,
62+
config: CONFIG.clinicDefaults
63+
});
64+
65+
// Generate events for each clinic
66+
newClinics.forEach(clinic => {
67+
const clinicEvents = clinic.slots
68+
.filter(slot => Math.random() > 0.2)
69+
.map(slot => {
70+
const participant = faker.helpers.arrayElement(participants);
71+
return generateEvent({
72+
slot,
73+
participant,
74+
clinic,
75+
outcomeWeights: CONFIG.eventOutcomes
76+
});
77+
});
78+
79+
events.push(...clinicEvents);
80+
});
81+
82+
clinics.push(...newClinics);
83+
});
84+
}
85+
86+
// Write generated data to files
87+
const writeData = (filename, data) => {
88+
fs.writeFileSync(
89+
path.join(CONFIG.outputPath, filename),
90+
JSON.stringify(data, null, 2)
91+
);
92+
};
93+
94+
writeData('participants.json', { participants });
95+
writeData('clinics.json', { clinics });
96+
writeData('events.json', { events });
97+
98+
console.log('\nData generation complete!');
99+
console.log(`Generated:`);
100+
console.log(`- ${participants.length} participants`);
101+
console.log(`- ${clinics.length} clinics`);
102+
console.log(`- ${events.length} events`);
103+
};
104+
105+
// Run the generator
106+
generateData().catch(console.error);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// app/lib/generators/clinic-generator.js
2+
3+
const { faker } = require('@faker-js/faker');
4+
const generateId = require('../utils/id-generator');
5+
const weighted = require('weighted');
6+
7+
const CLINIC_TYPES = [
8+
{ type: 'hospital', weight: 0.7 },
9+
{ type: 'mobile_unit', weight: 0.3 }
10+
];
11+
12+
const generateTimeSlots = (date, config) => {
13+
const slots = [];
14+
const startTime = new Date(`${date.toISOString().split('T')[0]}T${config.startTime}`);
15+
const endTime = new Date(`${date.toISOString().split('T')[0]}T${config.endTime}`);
16+
17+
let currentTime = new Date(startTime);
18+
while (currentTime < endTime) {
19+
const slotId = generateId();
20+
slots.push({
21+
id: slotId,
22+
dateTime: new Date(currentTime).toISOString(),
23+
type: 'screening',
24+
capacity: 2,
25+
bookedCount: 0
26+
});
27+
currentTime.setMinutes(currentTime.getMinutes() + config.slotDurationMinutes);
28+
}
29+
return slots;
30+
};
31+
32+
// Generate multiple clinics for a BSU on a given day
33+
const generateClinicsForBSU = ({ date, breastScreeningUnit, config }) => {
34+
// Determine number of clinics for this BSU today (1-2)
35+
const numberOfClinics = Math.random() < 0.3 ? 2 : 1;
36+
37+
return Array.from({ length: numberOfClinics }, () => {
38+
// If this is the second clinic for the day, make it more likely to be a mobile unit
39+
const isSecondClinic = numberOfClinics === 2;
40+
const clinicType = weighted.select(
41+
CLINIC_TYPES.map(t => t.type),
42+
CLINIC_TYPES.map(t => isSecondClinic ? (t.type === 'mobile_unit' ? 0.7 : 0.3) : t.weight)
43+
);
44+
45+
return {
46+
id: generateId(),
47+
date: date.toISOString().split('T')[0],
48+
breastScreeningUnitId: breastScreeningUnit.id,
49+
clinicType,
50+
location: clinicType === 'hospital' ?
51+
breastScreeningUnit.address :
52+
generateMobileLocation(breastScreeningUnit),
53+
slots: generateTimeSlots(date, config),
54+
status: date < new Date() ? 'completed' : 'scheduled',
55+
staffing: {
56+
mamographers: [],
57+
radiologists: [],
58+
support: []
59+
},
60+
targetBookings: 60,
61+
targetAttendance: 40,
62+
notes: null
63+
};
64+
});
65+
};
66+
67+
const generateMobileLocation = (bsu) => {
68+
const locations = [
69+
'Community Centre',
70+
'Health Centre',
71+
'Leisure Centre',
72+
'Shopping Centre Car Park',
73+
'Supermarket Car Park'
74+
];
75+
76+
const location = faker.helpers.arrayElement(locations);
77+
return {
78+
name: `${faker.location.city()} ${location}`,
79+
address: {
80+
line1: faker.location.streetAddress(),
81+
city: faker.location.city(),
82+
postcode: faker.location.zipCode('??# #??')
83+
}
84+
};
85+
};
86+
87+
module.exports = {
88+
generateClinicsForBSU
89+
};

0 commit comments

Comments
 (0)