Skip to content

Commit d2e9eaa

Browse files
Add first pass at pre-appointment questionnaire
1 parent 36cf3c6 commit d2e9eaa

File tree

13 files changed

+1680
-51
lines changed

13 files changed

+1680
-51
lines changed

app/filters.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,30 @@ module.exports = function (env) { /* eslint-disable-line func-names,no-unused-va
1212

1313
// Get all files from utils directory
1414
const utilsPath = path.join(__dirname, 'lib/utils');
15+
const filtersPath = path.join(__dirname, 'filters');
16+
17+
const folderPaths = [utilsPath, filtersPath]
1518

1619
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-
}
20+
folderPaths.forEach(folderPath => {
21+
const files = fs.readdirSync(folderPath);
22+
23+
files.forEach(file => {
24+
if (path.extname(file) === '.js') {
25+
const module = require(path.join(folderPath, file));
26+
27+
Object.entries(module).forEach(([name, func]) => {
28+
if (typeof func === 'function') {
29+
filters[name] = func;
30+
}
31+
});
32+
}
33+
});
3434
});
35-
} catch (err) {
36-
console.warn('Error loading filters from utils:', err);
35+
}
36+
catch (err) {
37+
console.warn('Error loading filters:', err);
3738
}
3839

39-
4040
return filters;
4141
};

app/filters/formatting.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Format a yes/no/not answered response with optional additional details
3+
* @param {string|boolean} value - The response value to format
4+
* @param {Object} [options] - Formatting options
5+
* @param {string} [options.yesValue] - Additional details to show after "Yes -" for positive responses
6+
* @param {string} [options.noText="No"] - Text to show for negative responses
7+
* @param {string} [options.notAnsweredText="Not answered"] - Text to show when no response given
8+
* @param {string} [options.yesPrefix="Yes"] - Text to show for positive responses (before any yesValue)
9+
* @returns {string} Formatted response text
10+
* @example
11+
* formatAnswer("yes", { yesValue: "Details here" }) // Returns "Yes - Details here"
12+
* formatAnswer("no") // Returns "No"
13+
* formatAnswer(null) // Returns "Not answered"
14+
* formatAnswer("yes", { yesPrefix: "Currently" }) // Returns "Currently"
15+
*/
16+
const formatAnswer = (value, options = {}) => {
17+
const {
18+
yesValue = null,
19+
noText = "No",
20+
notAnsweredText = "Not answered",
21+
yesPrefix = "Yes"
22+
} = options;
23+
24+
// Handle null/undefined/empty string
25+
if (!value || value === "no" || value === "false") {
26+
return noText;
27+
}
28+
29+
// For any truthy value (includes "yes", true, etc)
30+
return yesValue ? `${yesPrefix} - ${yesValue}` : yesPrefix;
31+
};
32+
33+
module.exports = {
34+
formatAnswer
35+
};

app/lib/generators/participant-generator.js

Lines changed: 150 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,103 @@ const generateNHSNumber = () => {
6363
return `${baseNumber}${finalCheckDigit}`;
6464
};
6565

66+
// New medical history generators
67+
const generateMedicalHistorySurvey = () => {
68+
// 50% chance of having completed the survey
69+
if (Math.random() > 0.5) {
70+
return null;
71+
}
72+
73+
return {
74+
completedAt: faker.date.past({ years: 1 }).toISOString(),
75+
76+
// Non-cancerous procedures/diagnoses
77+
nonCancerousProcedures: generateNonCancerousProcedures(),
78+
79+
// Current hormone therapy (if they had previous cancer)
80+
onHormoneTherapy: Math.random() < 0.3, // 30% of those with cancer history
81+
82+
// Other medical history
83+
otherMedicalHistory: Math.random() < 0.3 ?
84+
faker.helpers.arrayElements([
85+
"Type 2 diabetes - diet controlled",
86+
"High blood pressure - medication",
87+
"Osteoarthritis",
88+
"Previous shoulder surgery",
89+
"Rheumatoid arthritis"
90+
], { min: 1, max: 2 }) :
91+
null
92+
};
93+
};
94+
95+
const generateNonCancerousProcedures = () => {
96+
const procedures = [];
97+
98+
const possibleProcedures = {
99+
'benign_lump': {
100+
probability: 0.15,
101+
details: () => ({
102+
dateDiscovered: faker.date.past({ years: 5 }).toISOString(),
103+
position: faker.helpers.arrayElement(['Left breast', 'Right breast']),
104+
wasRemoved: Math.random() < 0.7,
105+
pathology: 'Fibroadenoma'
106+
})
107+
},
108+
'cyst_aspiration': {
109+
probability: 0.1,
110+
details: () => ({
111+
date: faker.date.past({ years: 3 }).toISOString(),
112+
location: faker.helpers.arrayElement(['Left breast', 'Right breast']),
113+
notes: 'Simple cyst, fluid aspirated'
114+
})
115+
},
116+
'non_implant_augmentation': {
117+
probability: 0.02,
118+
details: () => ({
119+
date: faker.date.past({ years: 5 }).toISOString(),
120+
procedure: 'Fat transfer procedure',
121+
hospital: 'General Hospital'
122+
})
123+
},
124+
'breast_reduction': {
125+
probability: 0.03,
126+
details: () => ({
127+
date: faker.date.past({ years: 5 }).toISOString(),
128+
notes: 'Bilateral breast reduction',
129+
hospital: 'City Hospital'
130+
})
131+
},
132+
'previous_biopsy': {
133+
probability: 0.08,
134+
details: () => ({
135+
date: faker.date.past({ years: 2 }).toISOString(),
136+
result: 'Benign',
137+
location: faker.helpers.arrayElement(['Left breast', 'Right breast'])
138+
})
139+
},
140+
'skin_lesion': {
141+
probability: 0.05,
142+
details: () => ({
143+
date: faker.date.past({ years: 3 }).toISOString(),
144+
type: faker.helpers.arrayElement(['Seborrheic keratosis', 'Dermatofibroma']),
145+
location: faker.helpers.arrayElement(['Left breast', 'Right breast'])
146+
})
147+
}
148+
};
149+
150+
Object.entries(possibleProcedures).forEach(([type, config]) => {
151+
if (Math.random() < config.probability) {
152+
procedures.push({
153+
type,
154+
...config.details()
155+
});
156+
}
157+
});
158+
159+
return procedures;
160+
};
161+
162+
66163
const generateParticipant = ({ ethnicities, breastScreeningUnits }) => {
67164
const id = generateId();
68165

@@ -101,7 +198,8 @@ const generateParticipant = ({ ethnicities, breastScreeningUnits }) => {
101198
nhsNumber: generateNHSNumber(),
102199
riskFactors: generateRiskFactors(),
103200
familyHistory: generateFamilyHistory(),
104-
previousCancerHistory: generatePreviousCancerHistory()
201+
previousCancerHistory: generatePreviousCancerHistory(),
202+
medicalHistorySurvey: generateMedicalHistorySurvey()
105203
},
106204
currentHealthInformation: {
107205
isPregnant: false,
@@ -114,6 +212,31 @@ const generateParticipant = ({ ethnicities, breastScreeningUnits }) => {
114212
};
115213

116214

215+
// Modified family history generator to add more detail
216+
const generateFamilyHistory = () => {
217+
if (Math.random() > 0.15) return null; // 15% chance of family history
218+
219+
const affectedRelatives = faker.helpers.arrayElements(
220+
[
221+
{ relation: 'mother', age: faker.number.int({ min: 35, max: 75 }) },
222+
{ relation: 'sister', age: faker.number.int({ min: 30, max: 70 }) },
223+
{ relation: 'daughter', age: faker.number.int({ min: 25, max: 45 }) },
224+
{ relation: 'grandmother', age: faker.number.int({ min: 45, max: 85 }) },
225+
{ relation: 'aunt', age: faker.number.int({ min: 40, max: 80 }) }
226+
],
227+
{ min: 1, max: 3 }
228+
);
229+
230+
return {
231+
hasFirstDegreeHistory: affectedRelatives.some(r =>
232+
['mother', 'sister', 'daughter'].includes(r.relation)
233+
),
234+
affectedRelatives,
235+
additionalDetails: Math.random() < 0.3 ?
236+
'Multiple occurrences on maternal side' : null
237+
};
238+
};
239+
117240
const generateRiskFactors = () => {
118241
const factors = [];
119242
const possibleFactors = {
@@ -133,34 +256,43 @@ const generateRiskFactors = () => {
133256
return factors;
134257
};
135258

136-
const generateFamilyHistory = () => {
137-
if (Math.random() > 0.15) return null; // 15% chance of family history
138-
139-
return {
140-
hasFirstDegreeHistory: Math.random() < 0.7, // 70% of those with family history
141-
affectedRelatives: faker.helpers.arrayElements(
142-
['mother', 'sister', 'daughter', 'grandmother', 'aunt'],
143-
{ min: 1, max: 3 }
144-
)
145-
};
146-
};
147259

260+
// Modified previous cancer history to include more detail
148261
const generatePreviousCancerHistory = () => {
149262
if (Math.random() > 0.02) return null; // 2% chance of previous cancer
150263

264+
const treatments = faker.helpers.arrayElements(
265+
[
266+
{ type: 'surgery', details: 'Wide local excision' },
267+
{ type: 'radiotherapy', details: '15 fractions' },
268+
{ type: 'chemotherapy', details: '6 cycles' },
269+
{ type: 'hormone_therapy', details: '5 years tamoxifen' }
270+
],
271+
{ min: 1, max: 3 }
272+
);
273+
151274
return {
152275
yearDiagnosed: faker.date.past({ years: 20 }).getFullYear(),
153276
type: faker.helpers.arrayElement([
154277
'ductal_carcinoma_in_situ',
155278
'invasive_ductal_carcinoma',
156279
'invasive_lobular_carcinoma'
157280
]),
158-
treatment: faker.helpers.arrayElements([
159-
'surgery',
160-
'radiotherapy',
161-
'chemotherapy',
162-
'hormone_therapy'
163-
], { min: 1, max: 3 })
281+
position: faker.helpers.arrayElement([
282+
'Left breast - upper outer quadrant',
283+
'Right breast - upper outer quadrant',
284+
'Left breast - lower inner quadrant',
285+
'Right breast - lower inner quadrant'
286+
]),
287+
treatments,
288+
hospital: faker.helpers.arrayElement([
289+
'City General Hospital',
290+
'Royal County Hospital',
291+
'Memorial Cancer Centre',
292+
'University Teaching Hospital'
293+
]),
294+
additionalNotes: Math.random() < 0.3 ?
295+
'Regular follow-up completed' : null
164296
};
165297
};
166298

0 commit comments

Comments
 (0)