@erosolaraijs/cure 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -94
- package/dist/bin/cure.d.ts +2 -5
- package/dist/bin/cure.d.ts.map +1 -1
- package/dist/bin/cure.js +285 -124
- package/dist/bin/cure.js.map +1 -1
- package/dist/clinician/decisionSupport.d.ts +325 -0
- package/dist/clinician/decisionSupport.d.ts.map +1 -0
- package/dist/clinician/decisionSupport.js +604 -0
- package/dist/clinician/decisionSupport.js.map +1 -0
- package/dist/clinician/index.d.ts +5 -0
- package/dist/clinician/index.d.ts.map +1 -0
- package/dist/clinician/index.js +5 -0
- package/dist/clinician/index.js.map +1 -0
- package/dist/compliance/index.d.ts +5 -0
- package/dist/compliance/index.d.ts.map +1 -0
- package/dist/compliance/index.js +5 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/integrations/clinicalTrials/index.d.ts +5 -0
- package/dist/integrations/clinicalTrials/index.d.ts.map +1 -0
- package/dist/integrations/clinicalTrials/index.js +5 -0
- package/dist/integrations/clinicalTrials/index.js.map +1 -0
- package/dist/integrations/ehr/index.d.ts +5 -0
- package/dist/integrations/ehr/index.d.ts.map +1 -0
- package/dist/integrations/ehr/index.js +5 -0
- package/dist/integrations/ehr/index.js.map +1 -0
- package/dist/integrations/genomics/index.d.ts +5 -0
- package/dist/integrations/genomics/index.d.ts.map +1 -0
- package/dist/integrations/genomics/index.js +5 -0
- package/dist/integrations/genomics/index.js.map +1 -0
- package/dist/integrations/index.d.ts +7 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +10 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/ml/index.d.ts +5 -0
- package/dist/ml/index.d.ts.map +1 -0
- package/dist/ml/index.js +5 -0
- package/dist/ml/index.js.map +1 -0
- package/dist/ml/outcomePredictor.d.ts +297 -0
- package/dist/ml/outcomePredictor.d.ts.map +1 -0
- package/dist/ml/outcomePredictor.js +823 -0
- package/dist/ml/outcomePredictor.js.map +1 -0
- package/dist/patient/index.d.ts +5 -0
- package/dist/patient/index.d.ts.map +1 -0
- package/dist/patient/index.js +5 -0
- package/dist/patient/index.js.map +1 -0
- package/dist/patient/patientPortal.d.ts +337 -0
- package/dist/patient/patientPortal.d.ts.map +1 -0
- package/dist/patient/patientPortal.js +667 -0
- package/dist/patient/patientPortal.js.map +1 -0
- package/dist/safety/drugInteractions.d.ts +230 -0
- package/dist/safety/drugInteractions.d.ts.map +1 -0
- package/dist/safety/drugInteractions.js +697 -0
- package/dist/safety/drugInteractions.js.map +1 -0
- package/dist/safety/index.d.ts +5 -0
- package/dist/safety/index.d.ts.map +1 -0
- package/dist/safety/index.js +5 -0
- package/dist/safety/index.js.map +1 -0
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +5 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/retrospectiveValidator.d.ts +246 -0
- package/dist/validation/retrospectiveValidator.d.ts.map +1 -0
- package/dist/validation/retrospectiveValidator.js +602 -0
- package/dist/validation/retrospectiveValidator.js.map +1 -0
- package/package.json +1 -1
- package/src/bin/cure.ts +331 -140
- package/src/clinician/decisionSupport.ts +949 -0
- package/src/patient/patientPortal.ts +1039 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patient-Facing Portal Interfaces
|
|
3
|
+
*
|
|
4
|
+
* Provides patient engagement features:
|
|
5
|
+
* - Treatment plan viewing
|
|
6
|
+
* - Side effect tracking
|
|
7
|
+
* - Medication adherence monitoring
|
|
8
|
+
* - Quality of life assessments
|
|
9
|
+
* - Secure messaging with care team
|
|
10
|
+
* - Educational content delivery
|
|
11
|
+
* - Appointment management
|
|
12
|
+
*
|
|
13
|
+
* All interfaces designed for health literacy and accessibility.
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from 'events';
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
+
// PATIENT PORTAL SERVICE
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
export class PatientPortalService extends EventEmitter {
|
|
20
|
+
accounts = new Map();
|
|
21
|
+
treatmentSummaries = new Map();
|
|
22
|
+
symptomReports = new Map();
|
|
23
|
+
adherenceLogs = new Map();
|
|
24
|
+
qolAssessments = new Map();
|
|
25
|
+
messages = new Map();
|
|
26
|
+
educationalContent = new Map();
|
|
27
|
+
appointmentRequests = new Map();
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
this.initializeEducationalContent();
|
|
31
|
+
}
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
// ACCOUNT MANAGEMENT
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
35
|
+
/**
|
|
36
|
+
* Create a patient portal account
|
|
37
|
+
*/
|
|
38
|
+
async createAccount(accountData) {
|
|
39
|
+
const id = `PAT-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
40
|
+
const account = {
|
|
41
|
+
...accountData,
|
|
42
|
+
id,
|
|
43
|
+
createdAt: new Date()
|
|
44
|
+
};
|
|
45
|
+
this.accounts.set(id, account);
|
|
46
|
+
this.emit('account-created', { accountId: id, mrn: account.mrn });
|
|
47
|
+
return account;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get patient account
|
|
51
|
+
*/
|
|
52
|
+
getAccount(patientId) {
|
|
53
|
+
return this.accounts.get(patientId);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Record patient login
|
|
57
|
+
*/
|
|
58
|
+
recordLogin(patientId) {
|
|
59
|
+
const account = this.accounts.get(patientId);
|
|
60
|
+
if (account) {
|
|
61
|
+
account.lastLoginAt = new Date();
|
|
62
|
+
this.emit('patient-login', { patientId });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
// TREATMENT SUMMARY
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
68
|
+
/**
|
|
69
|
+
* Set patient treatment summary
|
|
70
|
+
*/
|
|
71
|
+
setTreatmentSummary(summary) {
|
|
72
|
+
this.treatmentSummaries.set(summary.patientId, summary);
|
|
73
|
+
this.emit('treatment-summary-updated', { patientId: summary.patientId });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get patient-friendly treatment summary
|
|
77
|
+
*/
|
|
78
|
+
getTreatmentSummary(patientId) {
|
|
79
|
+
return this.treatmentSummaries.get(patientId);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Generate simple language summary
|
|
83
|
+
*/
|
|
84
|
+
generateSimpleSummary(diagnosis, stage, treatment) {
|
|
85
|
+
// Convert medical terminology to patient-friendly language
|
|
86
|
+
const diagnosisExplanation = this.simplifyDiagnosis(diagnosis, stage);
|
|
87
|
+
const treatmentExplanation = this.simplifyTreatment(treatment);
|
|
88
|
+
const whatItMeans = [
|
|
89
|
+
'Your care team has created a treatment plan specifically for you',
|
|
90
|
+
'This plan is based on the specific characteristics of your cancer',
|
|
91
|
+
'Your doctors will monitor how well the treatment is working',
|
|
92
|
+
'You can ask questions at any time - your care team is here to help'
|
|
93
|
+
];
|
|
94
|
+
return {
|
|
95
|
+
diagnosisExplanation,
|
|
96
|
+
treatmentExplanation,
|
|
97
|
+
whatItMeans
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
simplifyDiagnosis(diagnosis, stage) {
|
|
101
|
+
// Simplified explanations for common diagnoses
|
|
102
|
+
const simplifications = {
|
|
103
|
+
'NSCLC': 'non-small cell lung cancer, the most common type of lung cancer',
|
|
104
|
+
'SCLC': 'small cell lung cancer',
|
|
105
|
+
'Breast Cancer': 'breast cancer',
|
|
106
|
+
'Adenocarcinoma': 'a type of cancer that starts in gland cells',
|
|
107
|
+
'Squamous Cell Carcinoma': 'a type of cancer that starts in flat cells'
|
|
108
|
+
};
|
|
109
|
+
let explanation = `You have been diagnosed with ${simplifications[diagnosis] || diagnosis}. `;
|
|
110
|
+
// Stage explanation
|
|
111
|
+
if (stage) {
|
|
112
|
+
const stageNum = stage.replace(/[^IViv0-4]/g, '').toUpperCase();
|
|
113
|
+
if (stageNum.includes('I') && !stageNum.includes('V')) {
|
|
114
|
+
explanation += 'This is an early stage, which often has more treatment options.';
|
|
115
|
+
}
|
|
116
|
+
else if (stageNum.includes('II')) {
|
|
117
|
+
explanation += 'This stage means the cancer has grown but is still considered treatable.';
|
|
118
|
+
}
|
|
119
|
+
else if (stageNum.includes('III')) {
|
|
120
|
+
explanation += 'This is a more advanced stage, but many effective treatments are available.';
|
|
121
|
+
}
|
|
122
|
+
else if (stageNum.includes('IV')) {
|
|
123
|
+
explanation += 'This stage means the cancer has spread, and treatment focuses on controlling the disease and maintaining quality of life.';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return explanation;
|
|
127
|
+
}
|
|
128
|
+
simplifyTreatment(treatment) {
|
|
129
|
+
const explanations = {
|
|
130
|
+
'immunotherapy': 'a treatment that helps your immune system fight the cancer',
|
|
131
|
+
'chemotherapy': 'medicine that kills fast-growing cells, including cancer cells',
|
|
132
|
+
'targeted therapy': 'medicine that targets specific features of cancer cells',
|
|
133
|
+
'radiation': 'high-energy beams that destroy cancer cells in a specific area',
|
|
134
|
+
'surgery': 'an operation to remove the cancer'
|
|
135
|
+
};
|
|
136
|
+
let explanation = treatment;
|
|
137
|
+
for (const [term, simple] of Object.entries(explanations)) {
|
|
138
|
+
if (treatment.toLowerCase().includes(term)) {
|
|
139
|
+
explanation = `Your treatment includes ${simple}.`;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return explanation;
|
|
144
|
+
}
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
146
|
+
// SYMPTOM TRACKING
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
148
|
+
/**
|
|
149
|
+
* Submit a symptom report
|
|
150
|
+
*/
|
|
151
|
+
async submitSymptomReport(patientId, symptoms, overallFeeling, concerns) {
|
|
152
|
+
const id = `SYM-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
153
|
+
// Analyze symptoms for follow-up needs
|
|
154
|
+
const { requiresFollowUp, followUpReason } = this.analyzeSymptoms(symptoms, overallFeeling);
|
|
155
|
+
const report = {
|
|
156
|
+
id,
|
|
157
|
+
patientId,
|
|
158
|
+
reportedAt: new Date(),
|
|
159
|
+
symptoms,
|
|
160
|
+
overallFeeling,
|
|
161
|
+
concernsForDoctor: concerns,
|
|
162
|
+
requiresFollowUp,
|
|
163
|
+
followUpReason
|
|
164
|
+
};
|
|
165
|
+
const patientReports = this.symptomReports.get(patientId) || [];
|
|
166
|
+
patientReports.push(report);
|
|
167
|
+
this.symptomReports.set(patientId, patientReports);
|
|
168
|
+
this.emit('symptom-report-submitted', { patientId, reportId: id, requiresFollowUp });
|
|
169
|
+
// Alert care team if needed
|
|
170
|
+
if (requiresFollowUp) {
|
|
171
|
+
this.emit('symptom-alert', {
|
|
172
|
+
patientId,
|
|
173
|
+
reportId: id,
|
|
174
|
+
reason: followUpReason,
|
|
175
|
+
severity: this.determineSeverity(symptoms)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return report;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get symptom history for patient
|
|
182
|
+
*/
|
|
183
|
+
getSymptomHistory(patientId, days) {
|
|
184
|
+
const reports = this.symptomReports.get(patientId) || [];
|
|
185
|
+
if (!days)
|
|
186
|
+
return reports;
|
|
187
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
188
|
+
return reports.filter(r => r.reportedAt >= cutoff);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get symptom trends
|
|
192
|
+
*/
|
|
193
|
+
getSymptomTrends(patientId) {
|
|
194
|
+
const reports = this.getSymptomHistory(patientId, 30);
|
|
195
|
+
const symptomData = new Map();
|
|
196
|
+
for (const report of reports) {
|
|
197
|
+
for (const symptom of report.symptoms) {
|
|
198
|
+
const data = symptomData.get(symptom.symptom) || [];
|
|
199
|
+
data.push(symptom.severity);
|
|
200
|
+
symptomData.set(symptom.symptom, data);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const trends = [];
|
|
204
|
+
for (const [symptom, severities] of symptomData) {
|
|
205
|
+
const avg = severities.reduce((a, b) => a + b, 0) / severities.length;
|
|
206
|
+
// Determine trend
|
|
207
|
+
let trend = 'stable';
|
|
208
|
+
if (severities.length >= 3) {
|
|
209
|
+
const recent = severities.slice(-3);
|
|
210
|
+
const earlier = severities.slice(0, Math.max(1, severities.length - 3));
|
|
211
|
+
const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length;
|
|
212
|
+
const earlierAvg = earlier.reduce((a, b) => a + b, 0) / earlier.length;
|
|
213
|
+
if (recentAvg < earlierAvg - 0.5)
|
|
214
|
+
trend = 'improving';
|
|
215
|
+
else if (recentAvg > earlierAvg + 0.5)
|
|
216
|
+
trend = 'worsening';
|
|
217
|
+
}
|
|
218
|
+
trends.push({
|
|
219
|
+
symptom,
|
|
220
|
+
trend,
|
|
221
|
+
averageSeverity: Math.round(avg * 10) / 10,
|
|
222
|
+
occurrences: severities.length
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return trends.sort((a, b) => b.averageSeverity - a.averageSeverity);
|
|
226
|
+
}
|
|
227
|
+
analyzeSymptoms(symptoms, overallFeeling) {
|
|
228
|
+
// Check for concerning symptoms
|
|
229
|
+
const concerningSymptoms = [
|
|
230
|
+
'fever', 'chest pain', 'shortness of breath', 'severe pain',
|
|
231
|
+
'bleeding', 'confusion', 'fainting', 'seizure'
|
|
232
|
+
];
|
|
233
|
+
const severeSymptomsCount = symptoms.filter(s => s.severity >= 4).length;
|
|
234
|
+
const concerningFound = symptoms.find(s => concerningSymptoms.some(cs => s.symptom.toLowerCase().includes(cs)));
|
|
235
|
+
if (concerningFound && concerningFound.severity >= 3) {
|
|
236
|
+
return {
|
|
237
|
+
requiresFollowUp: true,
|
|
238
|
+
followUpReason: `Patient reported ${concerningFound.symptom} with severity ${concerningFound.severity}/5`
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (severeSymptomsCount >= 2) {
|
|
242
|
+
return {
|
|
243
|
+
requiresFollowUp: true,
|
|
244
|
+
followUpReason: `Multiple severe symptoms reported (${severeSymptomsCount})`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (overallFeeling <= 2) {
|
|
248
|
+
return {
|
|
249
|
+
requiresFollowUp: true,
|
|
250
|
+
followUpReason: 'Patient overall feeling is very poor'
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return { requiresFollowUp: false };
|
|
254
|
+
}
|
|
255
|
+
determineSeverity(symptoms) {
|
|
256
|
+
const maxSeverity = Math.max(...symptoms.map(s => s.severity));
|
|
257
|
+
if (maxSeverity >= 4)
|
|
258
|
+
return 'high';
|
|
259
|
+
if (maxSeverity >= 3)
|
|
260
|
+
return 'moderate';
|
|
261
|
+
return 'low';
|
|
262
|
+
}
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
264
|
+
// MEDICATION ADHERENCE
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
266
|
+
/**
|
|
267
|
+
* Log medication taken
|
|
268
|
+
*/
|
|
269
|
+
logMedicationTaken(patientId, medication, scheduledTime, taken, takenTime, skippedReason, sideEffects) {
|
|
270
|
+
const logs = this.adherenceLogs.get(patientId) || [];
|
|
271
|
+
let medLog = logs.find(l => l.medication === medication);
|
|
272
|
+
if (!medLog) {
|
|
273
|
+
medLog = {
|
|
274
|
+
patientId,
|
|
275
|
+
medication,
|
|
276
|
+
logs: [],
|
|
277
|
+
adherenceRate: 100,
|
|
278
|
+
missedDoses: 0,
|
|
279
|
+
streakDays: 0
|
|
280
|
+
};
|
|
281
|
+
logs.push(medLog);
|
|
282
|
+
}
|
|
283
|
+
medLog.logs.push({
|
|
284
|
+
scheduledTime,
|
|
285
|
+
takenTime,
|
|
286
|
+
taken,
|
|
287
|
+
skippedReason,
|
|
288
|
+
sideEffects
|
|
289
|
+
});
|
|
290
|
+
// Update adherence metrics
|
|
291
|
+
const totalDoses = medLog.logs.length;
|
|
292
|
+
const takenDoses = medLog.logs.filter(l => l.taken).length;
|
|
293
|
+
medLog.adherenceRate = Math.round((takenDoses / totalDoses) * 100);
|
|
294
|
+
medLog.missedDoses = totalDoses - takenDoses;
|
|
295
|
+
// Calculate streak
|
|
296
|
+
const sortedLogs = [...medLog.logs].sort((a, b) => b.scheduledTime.getTime() - a.scheduledTime.getTime());
|
|
297
|
+
let streak = 0;
|
|
298
|
+
for (const log of sortedLogs) {
|
|
299
|
+
if (log.taken)
|
|
300
|
+
streak++;
|
|
301
|
+
else
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
medLog.streakDays = streak;
|
|
305
|
+
this.adherenceLogs.set(patientId, logs);
|
|
306
|
+
this.emit('medication-logged', { patientId, medication, taken });
|
|
307
|
+
// Alert for poor adherence
|
|
308
|
+
if (medLog.adherenceRate < 80) {
|
|
309
|
+
this.emit('adherence-concern', {
|
|
310
|
+
patientId,
|
|
311
|
+
medication,
|
|
312
|
+
adherenceRate: medLog.adherenceRate
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get adherence summary
|
|
318
|
+
*/
|
|
319
|
+
getAdherenceSummary(patientId) {
|
|
320
|
+
const medications = this.adherenceLogs.get(patientId) || [];
|
|
321
|
+
if (medications.length === 0) {
|
|
322
|
+
return {
|
|
323
|
+
medications: [],
|
|
324
|
+
overallAdherence: 100,
|
|
325
|
+
concerns: [],
|
|
326
|
+
encouragement: 'Great start! Remember to log your medications each day.'
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const overallAdherence = Math.round(medications.reduce((sum, m) => sum + m.adherenceRate, 0) / medications.length);
|
|
330
|
+
const concerns = [];
|
|
331
|
+
for (const med of medications) {
|
|
332
|
+
if (med.adherenceRate < 80) {
|
|
333
|
+
concerns.push(`${med.medication} adherence is ${med.adherenceRate}% - please talk to your care team if you're having trouble`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
let encouragement;
|
|
337
|
+
if (overallAdherence >= 95) {
|
|
338
|
+
encouragement = 'Excellent! You\'re doing a great job staying on track with your medications.';
|
|
339
|
+
}
|
|
340
|
+
else if (overallAdherence >= 80) {
|
|
341
|
+
encouragement = 'Good work! Try to take your medications at the same time each day to build a routine.';
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
encouragement = 'Remember, taking your medications as prescribed gives them the best chance to work. We\'re here to help if you\'re having difficulties.';
|
|
345
|
+
}
|
|
346
|
+
return { medications, overallAdherence, concerns, encouragement };
|
|
347
|
+
}
|
|
348
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
349
|
+
// QUALITY OF LIFE ASSESSMENTS
|
|
350
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
351
|
+
/**
|
|
352
|
+
* Submit quality of life assessment
|
|
353
|
+
*/
|
|
354
|
+
async submitQoLAssessment(patientId, assessmentType, responses) {
|
|
355
|
+
const id = `QOL-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
356
|
+
// Score the assessment
|
|
357
|
+
const summaryScores = this.scoreAssessment(assessmentType, responses, patientId);
|
|
358
|
+
const alerts = this.identifyQoLAlerts(summaryScores);
|
|
359
|
+
const assessment = {
|
|
360
|
+
id,
|
|
361
|
+
patientId,
|
|
362
|
+
assessmentType,
|
|
363
|
+
completedAt: new Date(),
|
|
364
|
+
responses,
|
|
365
|
+
summaryScores,
|
|
366
|
+
alerts
|
|
367
|
+
};
|
|
368
|
+
const patientAssessments = this.qolAssessments.get(patientId) || [];
|
|
369
|
+
patientAssessments.push(assessment);
|
|
370
|
+
this.qolAssessments.set(patientId, patientAssessments);
|
|
371
|
+
this.emit('qol-assessment-completed', { patientId, assessmentId: id });
|
|
372
|
+
// Alert for concerning scores
|
|
373
|
+
if (alerts.some(a => a.severity === 'high')) {
|
|
374
|
+
this.emit('qol-alert', { patientId, assessmentId: id, alerts });
|
|
375
|
+
}
|
|
376
|
+
return assessment;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get QoL assessment history
|
|
380
|
+
*/
|
|
381
|
+
getQoLHistory(patientId) {
|
|
382
|
+
return this.qolAssessments.get(patientId) || [];
|
|
383
|
+
}
|
|
384
|
+
scoreAssessment(type, responses, patientId) {
|
|
385
|
+
// Group responses by category
|
|
386
|
+
const categories = new Map();
|
|
387
|
+
for (const response of responses) {
|
|
388
|
+
const cat = response.category;
|
|
389
|
+
const data = categories.get(cat) || { scores: [], maxScore: 0 };
|
|
390
|
+
data.scores.push(response.score || 0);
|
|
391
|
+
data.maxScore = Math.max(data.maxScore, 5); // Assuming 5-point scale
|
|
392
|
+
categories.set(cat, data);
|
|
393
|
+
}
|
|
394
|
+
// Get previous assessment for comparison
|
|
395
|
+
const history = this.qolAssessments.get(patientId) || [];
|
|
396
|
+
const previous = history.length > 0 ? history[history.length - 1] : null;
|
|
397
|
+
const summaryScores = [];
|
|
398
|
+
for (const [domain, data] of categories) {
|
|
399
|
+
const score = Math.round(data.scores.reduce((a, b) => a + b, 0) / data.scores.length * 10) / 10;
|
|
400
|
+
const maxScore = data.maxScore;
|
|
401
|
+
// Get previous score for this domain
|
|
402
|
+
let changeFromLast;
|
|
403
|
+
if (previous) {
|
|
404
|
+
const prevDomain = previous.summaryScores.find(s => s.domain === domain);
|
|
405
|
+
if (prevDomain) {
|
|
406
|
+
changeFromLast = Math.round((score - prevDomain.score) * 10) / 10;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
summaryScores.push({
|
|
410
|
+
domain,
|
|
411
|
+
score,
|
|
412
|
+
maxScore,
|
|
413
|
+
interpretation: this.interpretScore(score, maxScore),
|
|
414
|
+
changeFromLast
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return summaryScores;
|
|
418
|
+
}
|
|
419
|
+
interpretScore(score, maxScore) {
|
|
420
|
+
const percentage = (score / maxScore) * 100;
|
|
421
|
+
if (percentage >= 80)
|
|
422
|
+
return 'Good - minimal impact on quality of life';
|
|
423
|
+
if (percentage >= 60)
|
|
424
|
+
return 'Moderate - some impact on daily activities';
|
|
425
|
+
if (percentage >= 40)
|
|
426
|
+
return 'Below average - noticeable impact on quality of life';
|
|
427
|
+
return 'Concerning - significant impact, please discuss with care team';
|
|
428
|
+
}
|
|
429
|
+
identifyQoLAlerts(scores) {
|
|
430
|
+
const alerts = [];
|
|
431
|
+
for (const score of scores) {
|
|
432
|
+
const percentage = (score.score / score.maxScore) * 100;
|
|
433
|
+
if (percentage < 40) {
|
|
434
|
+
alerts.push({
|
|
435
|
+
domain: score.domain,
|
|
436
|
+
concern: `${score.domain} score is below 40%`,
|
|
437
|
+
severity: 'high',
|
|
438
|
+
recommendation: 'Discuss with your care team at your next appointment'
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else if (percentage < 60 && score.changeFromLast !== undefined && score.changeFromLast < -1) {
|
|
442
|
+
alerts.push({
|
|
443
|
+
domain: score.domain,
|
|
444
|
+
concern: `${score.domain} has declined since last assessment`,
|
|
445
|
+
severity: 'moderate',
|
|
446
|
+
recommendation: 'Monitor and report if continues to decline'
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return alerts;
|
|
451
|
+
}
|
|
452
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
453
|
+
// SECURE MESSAGING
|
|
454
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
455
|
+
/**
|
|
456
|
+
* Send a message
|
|
457
|
+
*/
|
|
458
|
+
async sendMessage(patientId, body, category, priority = 'routine', subject) {
|
|
459
|
+
const id = `MSG-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
460
|
+
const threadId = `THR-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`;
|
|
461
|
+
const message = {
|
|
462
|
+
id,
|
|
463
|
+
threadId,
|
|
464
|
+
patientId,
|
|
465
|
+
direction: 'inbound',
|
|
466
|
+
sender: {
|
|
467
|
+
type: 'patient',
|
|
468
|
+
name: 'Patient', // Would be fetched from account
|
|
469
|
+
id: patientId
|
|
470
|
+
},
|
|
471
|
+
recipient: {
|
|
472
|
+
type: 'care-team',
|
|
473
|
+
name: 'Care Team'
|
|
474
|
+
},
|
|
475
|
+
subject,
|
|
476
|
+
body,
|
|
477
|
+
sentAt: new Date(),
|
|
478
|
+
priority,
|
|
479
|
+
category,
|
|
480
|
+
requiresResponse: true,
|
|
481
|
+
responseDeadline: priority === 'urgent'
|
|
482
|
+
? new Date(Date.now() + 4 * 60 * 60 * 1000) // 4 hours for urgent
|
|
483
|
+
: new Date(Date.now() + 48 * 60 * 60 * 1000), // 48 hours for routine
|
|
484
|
+
status: 'unread'
|
|
485
|
+
};
|
|
486
|
+
const patientMessages = this.messages.get(patientId) || [];
|
|
487
|
+
patientMessages.push(message);
|
|
488
|
+
this.messages.set(patientId, patientMessages);
|
|
489
|
+
this.emit('message-sent', { patientId, messageId: id, category, priority });
|
|
490
|
+
return message;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Get messages for patient
|
|
494
|
+
*/
|
|
495
|
+
getMessages(patientId, unreadOnly = false) {
|
|
496
|
+
const messages = this.messages.get(patientId) || [];
|
|
497
|
+
if (unreadOnly) {
|
|
498
|
+
return messages.filter(m => m.status === 'unread');
|
|
499
|
+
}
|
|
500
|
+
return messages.sort((a, b) => b.sentAt.getTime() - a.sentAt.getTime());
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Mark message as read
|
|
504
|
+
*/
|
|
505
|
+
markMessageRead(patientId, messageId) {
|
|
506
|
+
const messages = this.messages.get(patientId) || [];
|
|
507
|
+
const message = messages.find(m => m.id === messageId);
|
|
508
|
+
if (message) {
|
|
509
|
+
message.status = 'read';
|
|
510
|
+
message.readAt = new Date();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
514
|
+
// EDUCATIONAL CONTENT
|
|
515
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
516
|
+
/**
|
|
517
|
+
* Get recommended educational content
|
|
518
|
+
*/
|
|
519
|
+
getRecommendedContent(patientId) {
|
|
520
|
+
const summary = this.treatmentSummaries.get(patientId);
|
|
521
|
+
if (!summary) {
|
|
522
|
+
return Array.from(this.educationalContent.values()).slice(0, 5);
|
|
523
|
+
}
|
|
524
|
+
// Match content based on patient's diagnosis and treatment
|
|
525
|
+
const diagnosis = summary.diagnosis.condition.toLowerCase();
|
|
526
|
+
const treatment = summary.currentTreatment.regimen.toLowerCase();
|
|
527
|
+
const relevantContent = Array.from(this.educationalContent.values()).filter(content => {
|
|
528
|
+
const matchesCancer = content.cancerTypes?.some(ct => diagnosis.includes(ct.toLowerCase()));
|
|
529
|
+
const matchesTreatment = content.treatmentTypes?.some(tt => treatment.includes(tt.toLowerCase()));
|
|
530
|
+
return matchesCancer || matchesTreatment;
|
|
531
|
+
});
|
|
532
|
+
return relevantContent.slice(0, 10);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Search educational content
|
|
536
|
+
*/
|
|
537
|
+
searchContent(query) {
|
|
538
|
+
const lowerQuery = query.toLowerCase();
|
|
539
|
+
return Array.from(this.educationalContent.values()).filter(content => content.title.toLowerCase().includes(lowerQuery) ||
|
|
540
|
+
content.description.toLowerCase().includes(lowerQuery) ||
|
|
541
|
+
content.topics.some(t => t.toLowerCase().includes(lowerQuery)));
|
|
542
|
+
}
|
|
543
|
+
initializeEducationalContent() {
|
|
544
|
+
const content = [
|
|
545
|
+
{
|
|
546
|
+
id: 'edu-1',
|
|
547
|
+
title: 'Understanding Your Cancer Treatment',
|
|
548
|
+
description: 'An overview of how cancer treatments work and what to expect.',
|
|
549
|
+
contentType: 'article',
|
|
550
|
+
targetAudience: ['new-patients', 'caregivers'],
|
|
551
|
+
topics: ['treatment', 'basics', 'getting-started'],
|
|
552
|
+
readingLevel: 'basic',
|
|
553
|
+
language: 'en',
|
|
554
|
+
content: '# Understanding Your Cancer Treatment\n\nThis guide explains the different types of cancer treatment...',
|
|
555
|
+
lastUpdated: new Date()
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: 'edu-2',
|
|
559
|
+
title: 'Managing Side Effects',
|
|
560
|
+
description: 'Tips for managing common side effects from cancer treatment.',
|
|
561
|
+
contentType: 'article',
|
|
562
|
+
targetAudience: ['patients', 'caregivers'],
|
|
563
|
+
topics: ['side-effects', 'self-care', 'management'],
|
|
564
|
+
readingLevel: 'basic',
|
|
565
|
+
language: 'en',
|
|
566
|
+
content: '# Managing Side Effects\n\nMany cancer treatments can cause side effects...',
|
|
567
|
+
lastUpdated: new Date()
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
id: 'edu-3',
|
|
571
|
+
title: 'What is Immunotherapy?',
|
|
572
|
+
description: 'Learn how immunotherapy helps your immune system fight cancer.',
|
|
573
|
+
contentType: 'video',
|
|
574
|
+
targetAudience: ['patients'],
|
|
575
|
+
treatmentTypes: ['immunotherapy'],
|
|
576
|
+
topics: ['immunotherapy', 'treatment-types'],
|
|
577
|
+
readingLevel: 'basic',
|
|
578
|
+
language: 'en',
|
|
579
|
+
content: '',
|
|
580
|
+
videoUrl: 'https://example.com/immunotherapy-video',
|
|
581
|
+
duration: 5,
|
|
582
|
+
lastUpdated: new Date()
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
id: 'edu-4',
|
|
586
|
+
title: 'Nutrition During Treatment',
|
|
587
|
+
description: 'Healthy eating tips while receiving cancer treatment.',
|
|
588
|
+
contentType: 'article',
|
|
589
|
+
targetAudience: ['patients', 'caregivers'],
|
|
590
|
+
topics: ['nutrition', 'self-care', 'wellness'],
|
|
591
|
+
readingLevel: 'basic',
|
|
592
|
+
language: 'en',
|
|
593
|
+
content: '# Nutrition During Treatment\n\nGood nutrition is important during cancer treatment...',
|
|
594
|
+
lastUpdated: new Date()
|
|
595
|
+
}
|
|
596
|
+
];
|
|
597
|
+
for (const c of content) {
|
|
598
|
+
this.educationalContent.set(c.id, c);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
602
|
+
// APPOINTMENTS
|
|
603
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
604
|
+
/**
|
|
605
|
+
* Request an appointment
|
|
606
|
+
*/
|
|
607
|
+
async requestAppointment(request) {
|
|
608
|
+
const id = `APT-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
609
|
+
const fullRequest = {
|
|
610
|
+
...request,
|
|
611
|
+
id,
|
|
612
|
+
status: 'pending',
|
|
613
|
+
submittedAt: new Date()
|
|
614
|
+
};
|
|
615
|
+
const patientRequests = this.appointmentRequests.get(request.patientId) || [];
|
|
616
|
+
patientRequests.push(fullRequest);
|
|
617
|
+
this.appointmentRequests.set(request.patientId, patientRequests);
|
|
618
|
+
this.emit('appointment-requested', {
|
|
619
|
+
patientId: request.patientId,
|
|
620
|
+
requestId: id,
|
|
621
|
+
type: request.appointmentType,
|
|
622
|
+
urgency: request.urgency
|
|
623
|
+
});
|
|
624
|
+
return id;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Get appointment requests for patient
|
|
628
|
+
*/
|
|
629
|
+
getAppointmentRequests(patientId) {
|
|
630
|
+
return this.appointmentRequests.get(patientId) || [];
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Get dashboard data for patient
|
|
634
|
+
*/
|
|
635
|
+
getDashboard(patientId) {
|
|
636
|
+
const summary = this.getTreatmentSummary(patientId);
|
|
637
|
+
const recentSymptoms = this.getSymptomHistory(patientId, 7);
|
|
638
|
+
const adherence = this.getAdherenceSummary(patientId);
|
|
639
|
+
const messages = this.getMessages(patientId, true);
|
|
640
|
+
const content = this.getRecommendedContent(patientId);
|
|
641
|
+
const alerts = [];
|
|
642
|
+
// Check for concerning symptoms
|
|
643
|
+
const latestSymptom = recentSymptoms[recentSymptoms.length - 1];
|
|
644
|
+
if (latestSymptom?.requiresFollowUp) {
|
|
645
|
+
alerts.push('Please contact your care team about your recent symptoms');
|
|
646
|
+
}
|
|
647
|
+
// Check adherence
|
|
648
|
+
if (adherence.overallAdherence < 80) {
|
|
649
|
+
alerts.push('Your medication adherence has been below target - remember to take medications as prescribed');
|
|
650
|
+
}
|
|
651
|
+
// Check unread messages
|
|
652
|
+
if (messages.length > 0) {
|
|
653
|
+
alerts.push(`You have ${messages.length} unread message(s) from your care team`);
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
summary,
|
|
657
|
+
recentSymptoms: recentSymptoms.slice(-5),
|
|
658
|
+
adherence,
|
|
659
|
+
unreadMessages: messages.length,
|
|
660
|
+
upcomingAppointments: summary?.upcomingAppointments || [],
|
|
661
|
+
recommendedContent: content.slice(0, 3),
|
|
662
|
+
alerts
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
export default PatientPortalService;
|
|
667
|
+
//# sourceMappingURL=patientPortal.js.map
|