@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.
Files changed (69) hide show
  1. package/README.md +46 -94
  2. package/dist/bin/cure.d.ts +2 -5
  3. package/dist/bin/cure.d.ts.map +1 -1
  4. package/dist/bin/cure.js +285 -124
  5. package/dist/bin/cure.js.map +1 -1
  6. package/dist/clinician/decisionSupport.d.ts +325 -0
  7. package/dist/clinician/decisionSupport.d.ts.map +1 -0
  8. package/dist/clinician/decisionSupport.js +604 -0
  9. package/dist/clinician/decisionSupport.js.map +1 -0
  10. package/dist/clinician/index.d.ts +5 -0
  11. package/dist/clinician/index.d.ts.map +1 -0
  12. package/dist/clinician/index.js +5 -0
  13. package/dist/clinician/index.js.map +1 -0
  14. package/dist/compliance/index.d.ts +5 -0
  15. package/dist/compliance/index.d.ts.map +1 -0
  16. package/dist/compliance/index.js +5 -0
  17. package/dist/compliance/index.js.map +1 -0
  18. package/dist/integrations/clinicalTrials/index.d.ts +5 -0
  19. package/dist/integrations/clinicalTrials/index.d.ts.map +1 -0
  20. package/dist/integrations/clinicalTrials/index.js +5 -0
  21. package/dist/integrations/clinicalTrials/index.js.map +1 -0
  22. package/dist/integrations/ehr/index.d.ts +5 -0
  23. package/dist/integrations/ehr/index.d.ts.map +1 -0
  24. package/dist/integrations/ehr/index.js +5 -0
  25. package/dist/integrations/ehr/index.js.map +1 -0
  26. package/dist/integrations/genomics/index.d.ts +5 -0
  27. package/dist/integrations/genomics/index.d.ts.map +1 -0
  28. package/dist/integrations/genomics/index.js +5 -0
  29. package/dist/integrations/genomics/index.js.map +1 -0
  30. package/dist/integrations/index.d.ts +7 -0
  31. package/dist/integrations/index.d.ts.map +1 -0
  32. package/dist/integrations/index.js +10 -0
  33. package/dist/integrations/index.js.map +1 -0
  34. package/dist/ml/index.d.ts +5 -0
  35. package/dist/ml/index.d.ts.map +1 -0
  36. package/dist/ml/index.js +5 -0
  37. package/dist/ml/index.js.map +1 -0
  38. package/dist/ml/outcomePredictor.d.ts +297 -0
  39. package/dist/ml/outcomePredictor.d.ts.map +1 -0
  40. package/dist/ml/outcomePredictor.js +823 -0
  41. package/dist/ml/outcomePredictor.js.map +1 -0
  42. package/dist/patient/index.d.ts +5 -0
  43. package/dist/patient/index.d.ts.map +1 -0
  44. package/dist/patient/index.js +5 -0
  45. package/dist/patient/index.js.map +1 -0
  46. package/dist/patient/patientPortal.d.ts +337 -0
  47. package/dist/patient/patientPortal.d.ts.map +1 -0
  48. package/dist/patient/patientPortal.js +667 -0
  49. package/dist/patient/patientPortal.js.map +1 -0
  50. package/dist/safety/drugInteractions.d.ts +230 -0
  51. package/dist/safety/drugInteractions.d.ts.map +1 -0
  52. package/dist/safety/drugInteractions.js +697 -0
  53. package/dist/safety/drugInteractions.js.map +1 -0
  54. package/dist/safety/index.d.ts +5 -0
  55. package/dist/safety/index.d.ts.map +1 -0
  56. package/dist/safety/index.js +5 -0
  57. package/dist/safety/index.js.map +1 -0
  58. package/dist/validation/index.d.ts +5 -0
  59. package/dist/validation/index.d.ts.map +1 -0
  60. package/dist/validation/index.js +5 -0
  61. package/dist/validation/index.js.map +1 -0
  62. package/dist/validation/retrospectiveValidator.d.ts +246 -0
  63. package/dist/validation/retrospectiveValidator.d.ts.map +1 -0
  64. package/dist/validation/retrospectiveValidator.js +602 -0
  65. package/dist/validation/retrospectiveValidator.js.map +1 -0
  66. package/package.json +1 -1
  67. package/src/bin/cure.ts +331 -140
  68. package/src/clinician/decisionSupport.ts +949 -0
  69. package/src/patient/patientPortal.ts +1039 -0
@@ -0,0 +1,1039 @@
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
+
16
+ import { EventEmitter } from 'events';
17
+
18
+ // ═══════════════════════════════════════════════════════════════════════════════
19
+ // PATIENT PORTAL TYPES
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+
22
+ export interface PatientAccount {
23
+ id: string;
24
+ mrn: string;
25
+ email: string;
26
+ phone?: string;
27
+ preferredLanguage: string;
28
+ preferredContactMethod: 'email' | 'sms' | 'phone' | 'portal';
29
+ accessLevel: 'full' | 'limited';
30
+ proxyAccess?: {
31
+ proxyId: string;
32
+ proxyName: string;
33
+ relationship: string;
34
+ permissions: ('view' | 'message' | 'schedule')[];
35
+ }[];
36
+ createdAt: Date;
37
+ lastLoginAt?: Date;
38
+ notificationPreferences: {
39
+ appointments: boolean;
40
+ labResults: boolean;
41
+ messages: boolean;
42
+ medications: boolean;
43
+ educational: boolean;
44
+ };
45
+ }
46
+
47
+ export interface PatientTreatmentSummary {
48
+ patientId: string;
49
+ diagnosis: {
50
+ condition: string;
51
+ diagnosisDate: Date;
52
+ stage?: string;
53
+ simpleExplanation: string;
54
+ };
55
+ currentTreatment: {
56
+ regimen: string;
57
+ medications: {
58
+ name: string;
59
+ purpose: string;
60
+ dose: string;
61
+ frequency: string;
62
+ instructions: string;
63
+ sideEffectsToWatch: string[];
64
+ }[];
65
+ startDate: Date;
66
+ cycleInfo?: {
67
+ currentCycle: number;
68
+ totalPlannedCycles?: number;
69
+ nextCycleDate?: Date;
70
+ };
71
+ whatToExpect: string[];
72
+ whenToCallDoctor: string[];
73
+ };
74
+ upcomingAppointments: {
75
+ date: Date;
76
+ type: string;
77
+ provider: string;
78
+ location: string;
79
+ preparation?: string;
80
+ }[];
81
+ careTeam: {
82
+ role: string;
83
+ name: string;
84
+ phone?: string;
85
+ messageable: boolean;
86
+ }[];
87
+ goals: {
88
+ goal: string;
89
+ progress?: number;
90
+ lastUpdated?: Date;
91
+ }[];
92
+ }
93
+
94
+ export interface SymptomReport {
95
+ id: string;
96
+ patientId: string;
97
+ reportedAt: Date;
98
+ symptoms: {
99
+ symptom: string;
100
+ severity: 1 | 2 | 3 | 4 | 5; // 1=Mild, 5=Severe
101
+ duration?: string;
102
+ interference: 'none' | 'little' | 'somewhat' | 'quite-a-bit' | 'very-much';
103
+ notes?: string;
104
+ }[];
105
+ overallFeeling: 1 | 2 | 3 | 4 | 5;
106
+ concernsForDoctor?: string;
107
+ requiresFollowUp: boolean;
108
+ followUpReason?: string;
109
+ }
110
+
111
+ export interface MedicationAdherenceLog {
112
+ patientId: string;
113
+ medication: string;
114
+ logs: {
115
+ scheduledTime: Date;
116
+ takenTime?: Date;
117
+ taken: boolean;
118
+ skippedReason?: string;
119
+ sideEffects?: string[];
120
+ notes?: string;
121
+ }[];
122
+ adherenceRate: number; // 0-100%
123
+ missedDoses: number;
124
+ streakDays: number;
125
+ }
126
+
127
+ export interface QualityOfLifeAssessment {
128
+ id: string;
129
+ patientId: string;
130
+ assessmentType: 'FACT-G' | 'EORTC-QLQ-C30' | 'PRO-CTCAE' | 'custom';
131
+ completedAt: Date;
132
+ responses: {
133
+ question: string;
134
+ category: 'physical' | 'emotional' | 'social' | 'functional' | 'symptoms';
135
+ response: number | string;
136
+ score?: number;
137
+ }[];
138
+ summaryScores: {
139
+ domain: string;
140
+ score: number;
141
+ maxScore: number;
142
+ interpretation: string;
143
+ changeFromLast?: number;
144
+ }[];
145
+ alerts: {
146
+ domain: string;
147
+ concern: string;
148
+ severity: 'low' | 'moderate' | 'high';
149
+ recommendation: string;
150
+ }[];
151
+ }
152
+
153
+ export interface PatientMessage {
154
+ id: string;
155
+ threadId: string;
156
+ patientId: string;
157
+ direction: 'inbound' | 'outbound';
158
+ sender: {
159
+ type: 'patient' | 'provider' | 'nurse' | 'system';
160
+ name: string;
161
+ id?: string;
162
+ };
163
+ recipient?: {
164
+ type: 'patient' | 'provider' | 'nurse' | 'care-team';
165
+ name: string;
166
+ id?: string;
167
+ };
168
+ subject?: string;
169
+ body: string;
170
+ attachments?: {
171
+ name: string;
172
+ type: string;
173
+ url: string;
174
+ }[];
175
+ sentAt: Date;
176
+ readAt?: Date;
177
+ priority: 'routine' | 'urgent';
178
+ category: 'question' | 'symptoms' | 'medication' | 'appointment' | 'billing' | 'other';
179
+ requiresResponse: boolean;
180
+ responseDeadline?: Date;
181
+ status: 'unread' | 'read' | 'responded' | 'closed';
182
+ }
183
+
184
+ export interface EducationalContent {
185
+ id: string;
186
+ title: string;
187
+ description: string;
188
+ contentType: 'article' | 'video' | 'infographic' | 'checklist' | 'faq';
189
+ targetAudience: string[];
190
+ cancerTypes?: string[];
191
+ treatmentTypes?: string[];
192
+ topics: string[];
193
+ readingLevel: 'basic' | 'intermediate' | 'advanced';
194
+ language: string;
195
+ content: string; // HTML or markdown
196
+ videoUrl?: string;
197
+ duration?: number; // minutes for video
198
+ author?: string;
199
+ reviewedBy?: string;
200
+ lastUpdated: Date;
201
+ relatedContent?: string[];
202
+ }
203
+
204
+ export interface AppointmentRequest {
205
+ id: string;
206
+ patientId: string;
207
+ requestType: 'new' | 'reschedule' | 'cancel';
208
+ appointmentType: string;
209
+ preferredDates: Date[];
210
+ preferredTimes: ('morning' | 'afternoon' | 'evening')[];
211
+ reason: string;
212
+ urgency: 'routine' | 'soon' | 'urgent';
213
+ notes?: string;
214
+ status: 'pending' | 'scheduled' | 'declined';
215
+ scheduledAppointment?: {
216
+ date: Date;
217
+ provider: string;
218
+ location: string;
219
+ };
220
+ submittedAt: Date;
221
+ processedAt?: Date;
222
+ processedBy?: string;
223
+ }
224
+
225
+ // ═══════════════════════════════════════════════════════════════════════════════
226
+ // PATIENT PORTAL SERVICE
227
+ // ═══════════════════════════════════════════════════════════════════════════════
228
+
229
+ export class PatientPortalService extends EventEmitter {
230
+ private accounts: Map<string, PatientAccount> = new Map();
231
+ private treatmentSummaries: Map<string, PatientTreatmentSummary> = new Map();
232
+ private symptomReports: Map<string, SymptomReport[]> = new Map();
233
+ private adherenceLogs: Map<string, MedicationAdherenceLog[]> = new Map();
234
+ private qolAssessments: Map<string, QualityOfLifeAssessment[]> = new Map();
235
+ private messages: Map<string, PatientMessage[]> = new Map();
236
+ private educationalContent: Map<string, EducationalContent> = new Map();
237
+ private appointmentRequests: Map<string, AppointmentRequest[]> = new Map();
238
+
239
+ constructor() {
240
+ super();
241
+ this.initializeEducationalContent();
242
+ }
243
+
244
+ // ═══════════════════════════════════════════════════════════════════════════════
245
+ // ACCOUNT MANAGEMENT
246
+ // ═══════════════════════════════════════════════════════════════════════════════
247
+
248
+ /**
249
+ * Create a patient portal account
250
+ */
251
+ async createAccount(accountData: Omit<PatientAccount, 'id' | 'createdAt'>): Promise<PatientAccount> {
252
+ const id = `PAT-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
253
+ const account: PatientAccount = {
254
+ ...accountData,
255
+ id,
256
+ createdAt: new Date()
257
+ };
258
+
259
+ this.accounts.set(id, account);
260
+ this.emit('account-created', { accountId: id, mrn: account.mrn });
261
+
262
+ return account;
263
+ }
264
+
265
+ /**
266
+ * Get patient account
267
+ */
268
+ getAccount(patientId: string): PatientAccount | undefined {
269
+ return this.accounts.get(patientId);
270
+ }
271
+
272
+ /**
273
+ * Record patient login
274
+ */
275
+ recordLogin(patientId: string): void {
276
+ const account = this.accounts.get(patientId);
277
+ if (account) {
278
+ account.lastLoginAt = new Date();
279
+ this.emit('patient-login', { patientId });
280
+ }
281
+ }
282
+
283
+ // ═══════════════════════════════════════════════════════════════════════════════
284
+ // TREATMENT SUMMARY
285
+ // ═══════════════════════════════════════════════════════════════════════════════
286
+
287
+ /**
288
+ * Set patient treatment summary
289
+ */
290
+ setTreatmentSummary(summary: PatientTreatmentSummary): void {
291
+ this.treatmentSummaries.set(summary.patientId, summary);
292
+ this.emit('treatment-summary-updated', { patientId: summary.patientId });
293
+ }
294
+
295
+ /**
296
+ * Get patient-friendly treatment summary
297
+ */
298
+ getTreatmentSummary(patientId: string): PatientTreatmentSummary | undefined {
299
+ return this.treatmentSummaries.get(patientId);
300
+ }
301
+
302
+ /**
303
+ * Generate simple language summary
304
+ */
305
+ generateSimpleSummary(
306
+ diagnosis: string,
307
+ stage: string,
308
+ treatment: string
309
+ ): {
310
+ diagnosisExplanation: string;
311
+ treatmentExplanation: string;
312
+ whatItMeans: string[];
313
+ } {
314
+ // Convert medical terminology to patient-friendly language
315
+ const diagnosisExplanation = this.simplifyDiagnosis(diagnosis, stage);
316
+ const treatmentExplanation = this.simplifyTreatment(treatment);
317
+
318
+ const whatItMeans: string[] = [
319
+ 'Your care team has created a treatment plan specifically for you',
320
+ 'This plan is based on the specific characteristics of your cancer',
321
+ 'Your doctors will monitor how well the treatment is working',
322
+ 'You can ask questions at any time - your care team is here to help'
323
+ ];
324
+
325
+ return {
326
+ diagnosisExplanation,
327
+ treatmentExplanation,
328
+ whatItMeans
329
+ };
330
+ }
331
+
332
+ private simplifyDiagnosis(diagnosis: string, stage: string): string {
333
+ // Simplified explanations for common diagnoses
334
+ const simplifications: Record<string, string> = {
335
+ 'NSCLC': 'non-small cell lung cancer, the most common type of lung cancer',
336
+ 'SCLC': 'small cell lung cancer',
337
+ 'Breast Cancer': 'breast cancer',
338
+ 'Adenocarcinoma': 'a type of cancer that starts in gland cells',
339
+ 'Squamous Cell Carcinoma': 'a type of cancer that starts in flat cells'
340
+ };
341
+
342
+ let explanation = `You have been diagnosed with ${simplifications[diagnosis] || diagnosis}. `;
343
+
344
+ // Stage explanation
345
+ if (stage) {
346
+ const stageNum = stage.replace(/[^IViv0-4]/g, '').toUpperCase();
347
+ if (stageNum.includes('I') && !stageNum.includes('V')) {
348
+ explanation += 'This is an early stage, which often has more treatment options.';
349
+ } else if (stageNum.includes('II')) {
350
+ explanation += 'This stage means the cancer has grown but is still considered treatable.';
351
+ } else if (stageNum.includes('III')) {
352
+ explanation += 'This is a more advanced stage, but many effective treatments are available.';
353
+ } else if (stageNum.includes('IV')) {
354
+ explanation += 'This stage means the cancer has spread, and treatment focuses on controlling the disease and maintaining quality of life.';
355
+ }
356
+ }
357
+
358
+ return explanation;
359
+ }
360
+
361
+ private simplifyTreatment(treatment: string): string {
362
+ const explanations: Record<string, string> = {
363
+ 'immunotherapy': 'a treatment that helps your immune system fight the cancer',
364
+ 'chemotherapy': 'medicine that kills fast-growing cells, including cancer cells',
365
+ 'targeted therapy': 'medicine that targets specific features of cancer cells',
366
+ 'radiation': 'high-energy beams that destroy cancer cells in a specific area',
367
+ 'surgery': 'an operation to remove the cancer'
368
+ };
369
+
370
+ let explanation = treatment;
371
+ for (const [term, simple] of Object.entries(explanations)) {
372
+ if (treatment.toLowerCase().includes(term)) {
373
+ explanation = `Your treatment includes ${simple}.`;
374
+ break;
375
+ }
376
+ }
377
+
378
+ return explanation;
379
+ }
380
+
381
+ // ═══════════════════════════════════════════════════════════════════════════════
382
+ // SYMPTOM TRACKING
383
+ // ═══════════════════════════════════════════════════════════════════════════════
384
+
385
+ /**
386
+ * Submit a symptom report
387
+ */
388
+ async submitSymptomReport(
389
+ patientId: string,
390
+ symptoms: SymptomReport['symptoms'],
391
+ overallFeeling: SymptomReport['overallFeeling'],
392
+ concerns?: string
393
+ ): Promise<SymptomReport> {
394
+ const id = `SYM-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
395
+
396
+ // Analyze symptoms for follow-up needs
397
+ const { requiresFollowUp, followUpReason } = this.analyzeSymptoms(symptoms, overallFeeling);
398
+
399
+ const report: SymptomReport = {
400
+ id,
401
+ patientId,
402
+ reportedAt: new Date(),
403
+ symptoms,
404
+ overallFeeling,
405
+ concernsForDoctor: concerns,
406
+ requiresFollowUp,
407
+ followUpReason
408
+ };
409
+
410
+ const patientReports = this.symptomReports.get(patientId) || [];
411
+ patientReports.push(report);
412
+ this.symptomReports.set(patientId, patientReports);
413
+
414
+ this.emit('symptom-report-submitted', { patientId, reportId: id, requiresFollowUp });
415
+
416
+ // Alert care team if needed
417
+ if (requiresFollowUp) {
418
+ this.emit('symptom-alert', {
419
+ patientId,
420
+ reportId: id,
421
+ reason: followUpReason,
422
+ severity: this.determineSeverity(symptoms)
423
+ });
424
+ }
425
+
426
+ return report;
427
+ }
428
+
429
+ /**
430
+ * Get symptom history for patient
431
+ */
432
+ getSymptomHistory(patientId: string, days?: number): SymptomReport[] {
433
+ const reports = this.symptomReports.get(patientId) || [];
434
+ if (!days) return reports;
435
+
436
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
437
+ return reports.filter(r => r.reportedAt >= cutoff);
438
+ }
439
+
440
+ /**
441
+ * Get symptom trends
442
+ */
443
+ getSymptomTrends(patientId: string): {
444
+ symptom: string;
445
+ trend: 'improving' | 'stable' | 'worsening';
446
+ averageSeverity: number;
447
+ occurrences: number;
448
+ }[] {
449
+ const reports = this.getSymptomHistory(patientId, 30);
450
+ const symptomData = new Map<string, number[]>();
451
+
452
+ for (const report of reports) {
453
+ for (const symptom of report.symptoms) {
454
+ const data = symptomData.get(symptom.symptom) || [];
455
+ data.push(symptom.severity);
456
+ symptomData.set(symptom.symptom, data);
457
+ }
458
+ }
459
+
460
+ const trends: ReturnType<typeof this.getSymptomTrends> = [];
461
+
462
+ for (const [symptom, severities] of symptomData) {
463
+ const avg = severities.reduce((a, b) => a + b, 0) / severities.length;
464
+
465
+ // Determine trend
466
+ let trend: 'improving' | 'stable' | 'worsening' = 'stable';
467
+ if (severities.length >= 3) {
468
+ const recent = severities.slice(-3);
469
+ const earlier = severities.slice(0, Math.max(1, severities.length - 3));
470
+ const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length;
471
+ const earlierAvg = earlier.reduce((a, b) => a + b, 0) / earlier.length;
472
+
473
+ if (recentAvg < earlierAvg - 0.5) trend = 'improving';
474
+ else if (recentAvg > earlierAvg + 0.5) trend = 'worsening';
475
+ }
476
+
477
+ trends.push({
478
+ symptom,
479
+ trend,
480
+ averageSeverity: Math.round(avg * 10) / 10,
481
+ occurrences: severities.length
482
+ });
483
+ }
484
+
485
+ return trends.sort((a, b) => b.averageSeverity - a.averageSeverity);
486
+ }
487
+
488
+ private analyzeSymptoms(
489
+ symptoms: SymptomReport['symptoms'],
490
+ overallFeeling: number
491
+ ): { requiresFollowUp: boolean; followUpReason?: string } {
492
+ // Check for concerning symptoms
493
+ const concerningSymptoms = [
494
+ 'fever', 'chest pain', 'shortness of breath', 'severe pain',
495
+ 'bleeding', 'confusion', 'fainting', 'seizure'
496
+ ];
497
+
498
+ const severeSymptomsCount = symptoms.filter(s => s.severity >= 4).length;
499
+ const concerningFound = symptoms.find(s =>
500
+ concerningSymptoms.some(cs => s.symptom.toLowerCase().includes(cs))
501
+ );
502
+
503
+ if (concerningFound && concerningFound.severity >= 3) {
504
+ return {
505
+ requiresFollowUp: true,
506
+ followUpReason: `Patient reported ${concerningFound.symptom} with severity ${concerningFound.severity}/5`
507
+ };
508
+ }
509
+
510
+ if (severeSymptomsCount >= 2) {
511
+ return {
512
+ requiresFollowUp: true,
513
+ followUpReason: `Multiple severe symptoms reported (${severeSymptomsCount})`
514
+ };
515
+ }
516
+
517
+ if (overallFeeling <= 2) {
518
+ return {
519
+ requiresFollowUp: true,
520
+ followUpReason: 'Patient overall feeling is very poor'
521
+ };
522
+ }
523
+
524
+ return { requiresFollowUp: false };
525
+ }
526
+
527
+ private determineSeverity(symptoms: SymptomReport['symptoms']): 'low' | 'moderate' | 'high' {
528
+ const maxSeverity = Math.max(...symptoms.map(s => s.severity));
529
+ if (maxSeverity >= 4) return 'high';
530
+ if (maxSeverity >= 3) return 'moderate';
531
+ return 'low';
532
+ }
533
+
534
+ // ═══════════════════════════════════════════════════════════════════════════════
535
+ // MEDICATION ADHERENCE
536
+ // ═══════════════════════════════════════════════════════════════════════════════
537
+
538
+ /**
539
+ * Log medication taken
540
+ */
541
+ logMedicationTaken(
542
+ patientId: string,
543
+ medication: string,
544
+ scheduledTime: Date,
545
+ taken: boolean,
546
+ takenTime?: Date,
547
+ skippedReason?: string,
548
+ sideEffects?: string[]
549
+ ): void {
550
+ const logs = this.adherenceLogs.get(patientId) || [];
551
+ let medLog = logs.find(l => l.medication === medication);
552
+
553
+ if (!medLog) {
554
+ medLog = {
555
+ patientId,
556
+ medication,
557
+ logs: [],
558
+ adherenceRate: 100,
559
+ missedDoses: 0,
560
+ streakDays: 0
561
+ };
562
+ logs.push(medLog);
563
+ }
564
+
565
+ medLog.logs.push({
566
+ scheduledTime,
567
+ takenTime,
568
+ taken,
569
+ skippedReason,
570
+ sideEffects
571
+ });
572
+
573
+ // Update adherence metrics
574
+ const totalDoses = medLog.logs.length;
575
+ const takenDoses = medLog.logs.filter(l => l.taken).length;
576
+ medLog.adherenceRate = Math.round((takenDoses / totalDoses) * 100);
577
+ medLog.missedDoses = totalDoses - takenDoses;
578
+
579
+ // Calculate streak
580
+ const sortedLogs = [...medLog.logs].sort((a, b) =>
581
+ b.scheduledTime.getTime() - a.scheduledTime.getTime()
582
+ );
583
+ let streak = 0;
584
+ for (const log of sortedLogs) {
585
+ if (log.taken) streak++;
586
+ else break;
587
+ }
588
+ medLog.streakDays = streak;
589
+
590
+ this.adherenceLogs.set(patientId, logs);
591
+ this.emit('medication-logged', { patientId, medication, taken });
592
+
593
+ // Alert for poor adherence
594
+ if (medLog.adherenceRate < 80) {
595
+ this.emit('adherence-concern', {
596
+ patientId,
597
+ medication,
598
+ adherenceRate: medLog.adherenceRate
599
+ });
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Get adherence summary
605
+ */
606
+ getAdherenceSummary(patientId: string): {
607
+ medications: MedicationAdherenceLog[];
608
+ overallAdherence: number;
609
+ concerns: string[];
610
+ encouragement: string;
611
+ } {
612
+ const medications = this.adherenceLogs.get(patientId) || [];
613
+
614
+ if (medications.length === 0) {
615
+ return {
616
+ medications: [],
617
+ overallAdherence: 100,
618
+ concerns: [],
619
+ encouragement: 'Great start! Remember to log your medications each day.'
620
+ };
621
+ }
622
+
623
+ const overallAdherence = Math.round(
624
+ medications.reduce((sum, m) => sum + m.adherenceRate, 0) / medications.length
625
+ );
626
+
627
+ const concerns: string[] = [];
628
+ for (const med of medications) {
629
+ if (med.adherenceRate < 80) {
630
+ concerns.push(`${med.medication} adherence is ${med.adherenceRate}% - please talk to your care team if you're having trouble`);
631
+ }
632
+ }
633
+
634
+ let encouragement: string;
635
+ if (overallAdherence >= 95) {
636
+ encouragement = 'Excellent! You\'re doing a great job staying on track with your medications.';
637
+ } else if (overallAdherence >= 80) {
638
+ encouragement = 'Good work! Try to take your medications at the same time each day to build a routine.';
639
+ } else {
640
+ encouragement = 'Remember, taking your medications as prescribed gives them the best chance to work. We\'re here to help if you\'re having difficulties.';
641
+ }
642
+
643
+ return { medications, overallAdherence, concerns, encouragement };
644
+ }
645
+
646
+ // ═══════════════════════════════════════════════════════════════════════════════
647
+ // QUALITY OF LIFE ASSESSMENTS
648
+ // ═══════════════════════════════════════════════════════════════════════════════
649
+
650
+ /**
651
+ * Submit quality of life assessment
652
+ */
653
+ async submitQoLAssessment(
654
+ patientId: string,
655
+ assessmentType: QualityOfLifeAssessment['assessmentType'],
656
+ responses: QualityOfLifeAssessment['responses']
657
+ ): Promise<QualityOfLifeAssessment> {
658
+ const id = `QOL-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
659
+
660
+ // Score the assessment
661
+ const summaryScores = this.scoreAssessment(assessmentType, responses, patientId);
662
+ const alerts = this.identifyQoLAlerts(summaryScores);
663
+
664
+ const assessment: QualityOfLifeAssessment = {
665
+ id,
666
+ patientId,
667
+ assessmentType,
668
+ completedAt: new Date(),
669
+ responses,
670
+ summaryScores,
671
+ alerts
672
+ };
673
+
674
+ const patientAssessments = this.qolAssessments.get(patientId) || [];
675
+ patientAssessments.push(assessment);
676
+ this.qolAssessments.set(patientId, patientAssessments);
677
+
678
+ this.emit('qol-assessment-completed', { patientId, assessmentId: id });
679
+
680
+ // Alert for concerning scores
681
+ if (alerts.some(a => a.severity === 'high')) {
682
+ this.emit('qol-alert', { patientId, assessmentId: id, alerts });
683
+ }
684
+
685
+ return assessment;
686
+ }
687
+
688
+ /**
689
+ * Get QoL assessment history
690
+ */
691
+ getQoLHistory(patientId: string): QualityOfLifeAssessment[] {
692
+ return this.qolAssessments.get(patientId) || [];
693
+ }
694
+
695
+ private scoreAssessment(
696
+ type: QualityOfLifeAssessment['assessmentType'],
697
+ responses: QualityOfLifeAssessment['responses'],
698
+ patientId: string
699
+ ): QualityOfLifeAssessment['summaryScores'] {
700
+ // Group responses by category
701
+ const categories = new Map<string, { scores: number[]; maxScore: number }>();
702
+
703
+ for (const response of responses) {
704
+ const cat = response.category;
705
+ const data = categories.get(cat) || { scores: [], maxScore: 0 };
706
+ data.scores.push(response.score || 0);
707
+ data.maxScore = Math.max(data.maxScore, 5); // Assuming 5-point scale
708
+ categories.set(cat, data);
709
+ }
710
+
711
+ // Get previous assessment for comparison
712
+ const history = this.qolAssessments.get(patientId) || [];
713
+ const previous = history.length > 0 ? history[history.length - 1] : null;
714
+
715
+ const summaryScores: QualityOfLifeAssessment['summaryScores'] = [];
716
+
717
+ for (const [domain, data] of categories) {
718
+ const score = Math.round(data.scores.reduce((a, b) => a + b, 0) / data.scores.length * 10) / 10;
719
+ const maxScore = data.maxScore;
720
+
721
+ // Get previous score for this domain
722
+ let changeFromLast: number | undefined;
723
+ if (previous) {
724
+ const prevDomain = previous.summaryScores.find(s => s.domain === domain);
725
+ if (prevDomain) {
726
+ changeFromLast = Math.round((score - prevDomain.score) * 10) / 10;
727
+ }
728
+ }
729
+
730
+ summaryScores.push({
731
+ domain,
732
+ score,
733
+ maxScore,
734
+ interpretation: this.interpretScore(score, maxScore),
735
+ changeFromLast
736
+ });
737
+ }
738
+
739
+ return summaryScores;
740
+ }
741
+
742
+ private interpretScore(score: number, maxScore: number): string {
743
+ const percentage = (score / maxScore) * 100;
744
+ if (percentage >= 80) return 'Good - minimal impact on quality of life';
745
+ if (percentage >= 60) return 'Moderate - some impact on daily activities';
746
+ if (percentage >= 40) return 'Below average - noticeable impact on quality of life';
747
+ return 'Concerning - significant impact, please discuss with care team';
748
+ }
749
+
750
+ private identifyQoLAlerts(scores: QualityOfLifeAssessment['summaryScores']): QualityOfLifeAssessment['alerts'] {
751
+ const alerts: QualityOfLifeAssessment['alerts'] = [];
752
+
753
+ for (const score of scores) {
754
+ const percentage = (score.score / score.maxScore) * 100;
755
+
756
+ if (percentage < 40) {
757
+ alerts.push({
758
+ domain: score.domain,
759
+ concern: `${score.domain} score is below 40%`,
760
+ severity: 'high',
761
+ recommendation: 'Discuss with your care team at your next appointment'
762
+ });
763
+ } else if (percentage < 60 && score.changeFromLast !== undefined && score.changeFromLast < -1) {
764
+ alerts.push({
765
+ domain: score.domain,
766
+ concern: `${score.domain} has declined since last assessment`,
767
+ severity: 'moderate',
768
+ recommendation: 'Monitor and report if continues to decline'
769
+ });
770
+ }
771
+ }
772
+
773
+ return alerts;
774
+ }
775
+
776
+ // ═══════════════════════════════════════════════════════════════════════════════
777
+ // SECURE MESSAGING
778
+ // ═══════════════════════════════════════════════════════════════════════════════
779
+
780
+ /**
781
+ * Send a message
782
+ */
783
+ async sendMessage(
784
+ patientId: string,
785
+ body: string,
786
+ category: PatientMessage['category'],
787
+ priority: PatientMessage['priority'] = 'routine',
788
+ subject?: string
789
+ ): Promise<PatientMessage> {
790
+ const id = `MSG-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
791
+ const threadId = `THR-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`;
792
+
793
+ const message: PatientMessage = {
794
+ id,
795
+ threadId,
796
+ patientId,
797
+ direction: 'inbound',
798
+ sender: {
799
+ type: 'patient',
800
+ name: 'Patient', // Would be fetched from account
801
+ id: patientId
802
+ },
803
+ recipient: {
804
+ type: 'care-team',
805
+ name: 'Care Team'
806
+ },
807
+ subject,
808
+ body,
809
+ sentAt: new Date(),
810
+ priority,
811
+ category,
812
+ requiresResponse: true,
813
+ responseDeadline: priority === 'urgent'
814
+ ? new Date(Date.now() + 4 * 60 * 60 * 1000) // 4 hours for urgent
815
+ : new Date(Date.now() + 48 * 60 * 60 * 1000), // 48 hours for routine
816
+ status: 'unread'
817
+ };
818
+
819
+ const patientMessages = this.messages.get(patientId) || [];
820
+ patientMessages.push(message);
821
+ this.messages.set(patientId, patientMessages);
822
+
823
+ this.emit('message-sent', { patientId, messageId: id, category, priority });
824
+
825
+ return message;
826
+ }
827
+
828
+ /**
829
+ * Get messages for patient
830
+ */
831
+ getMessages(patientId: string, unreadOnly: boolean = false): PatientMessage[] {
832
+ const messages = this.messages.get(patientId) || [];
833
+ if (unreadOnly) {
834
+ return messages.filter(m => m.status === 'unread');
835
+ }
836
+ return messages.sort((a, b) => b.sentAt.getTime() - a.sentAt.getTime());
837
+ }
838
+
839
+ /**
840
+ * Mark message as read
841
+ */
842
+ markMessageRead(patientId: string, messageId: string): void {
843
+ const messages = this.messages.get(patientId) || [];
844
+ const message = messages.find(m => m.id === messageId);
845
+ if (message) {
846
+ message.status = 'read';
847
+ message.readAt = new Date();
848
+ }
849
+ }
850
+
851
+ // ═══════════════════════════════════════════════════════════════════════════════
852
+ // EDUCATIONAL CONTENT
853
+ // ═══════════════════════════════════════════════════════════════════════════════
854
+
855
+ /**
856
+ * Get recommended educational content
857
+ */
858
+ getRecommendedContent(patientId: string): EducationalContent[] {
859
+ const summary = this.treatmentSummaries.get(patientId);
860
+ if (!summary) {
861
+ return Array.from(this.educationalContent.values()).slice(0, 5);
862
+ }
863
+
864
+ // Match content based on patient's diagnosis and treatment
865
+ const diagnosis = summary.diagnosis.condition.toLowerCase();
866
+ const treatment = summary.currentTreatment.regimen.toLowerCase();
867
+
868
+ const relevantContent = Array.from(this.educationalContent.values()).filter(content => {
869
+ const matchesCancer = content.cancerTypes?.some(ct =>
870
+ diagnosis.includes(ct.toLowerCase())
871
+ );
872
+ const matchesTreatment = content.treatmentTypes?.some(tt =>
873
+ treatment.includes(tt.toLowerCase())
874
+ );
875
+ return matchesCancer || matchesTreatment;
876
+ });
877
+
878
+ return relevantContent.slice(0, 10);
879
+ }
880
+
881
+ /**
882
+ * Search educational content
883
+ */
884
+ searchContent(query: string): EducationalContent[] {
885
+ const lowerQuery = query.toLowerCase();
886
+ return Array.from(this.educationalContent.values()).filter(content =>
887
+ content.title.toLowerCase().includes(lowerQuery) ||
888
+ content.description.toLowerCase().includes(lowerQuery) ||
889
+ content.topics.some(t => t.toLowerCase().includes(lowerQuery))
890
+ );
891
+ }
892
+
893
+ private initializeEducationalContent(): void {
894
+ const content: EducationalContent[] = [
895
+ {
896
+ id: 'edu-1',
897
+ title: 'Understanding Your Cancer Treatment',
898
+ description: 'An overview of how cancer treatments work and what to expect.',
899
+ contentType: 'article',
900
+ targetAudience: ['new-patients', 'caregivers'],
901
+ topics: ['treatment', 'basics', 'getting-started'],
902
+ readingLevel: 'basic',
903
+ language: 'en',
904
+ content: '# Understanding Your Cancer Treatment\n\nThis guide explains the different types of cancer treatment...',
905
+ lastUpdated: new Date()
906
+ },
907
+ {
908
+ id: 'edu-2',
909
+ title: 'Managing Side Effects',
910
+ description: 'Tips for managing common side effects from cancer treatment.',
911
+ contentType: 'article',
912
+ targetAudience: ['patients', 'caregivers'],
913
+ topics: ['side-effects', 'self-care', 'management'],
914
+ readingLevel: 'basic',
915
+ language: 'en',
916
+ content: '# Managing Side Effects\n\nMany cancer treatments can cause side effects...',
917
+ lastUpdated: new Date()
918
+ },
919
+ {
920
+ id: 'edu-3',
921
+ title: 'What is Immunotherapy?',
922
+ description: 'Learn how immunotherapy helps your immune system fight cancer.',
923
+ contentType: 'video',
924
+ targetAudience: ['patients'],
925
+ treatmentTypes: ['immunotherapy'],
926
+ topics: ['immunotherapy', 'treatment-types'],
927
+ readingLevel: 'basic',
928
+ language: 'en',
929
+ content: '',
930
+ videoUrl: 'https://example.com/immunotherapy-video',
931
+ duration: 5,
932
+ lastUpdated: new Date()
933
+ },
934
+ {
935
+ id: 'edu-4',
936
+ title: 'Nutrition During Treatment',
937
+ description: 'Healthy eating tips while receiving cancer treatment.',
938
+ contentType: 'article',
939
+ targetAudience: ['patients', 'caregivers'],
940
+ topics: ['nutrition', 'self-care', 'wellness'],
941
+ readingLevel: 'basic',
942
+ language: 'en',
943
+ content: '# Nutrition During Treatment\n\nGood nutrition is important during cancer treatment...',
944
+ lastUpdated: new Date()
945
+ }
946
+ ];
947
+
948
+ for (const c of content) {
949
+ this.educationalContent.set(c.id, c);
950
+ }
951
+ }
952
+
953
+ // ═══════════════════════════════════════════════════════════════════════════════
954
+ // APPOINTMENTS
955
+ // ═══════════════════════════════════════════════════════════════════════════════
956
+
957
+ /**
958
+ * Request an appointment
959
+ */
960
+ async requestAppointment(request: Omit<AppointmentRequest, 'id' | 'status' | 'submittedAt'>): Promise<string> {
961
+ const id = `APT-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
962
+
963
+ const fullRequest: AppointmentRequest = {
964
+ ...request,
965
+ id,
966
+ status: 'pending',
967
+ submittedAt: new Date()
968
+ };
969
+
970
+ const patientRequests = this.appointmentRequests.get(request.patientId) || [];
971
+ patientRequests.push(fullRequest);
972
+ this.appointmentRequests.set(request.patientId, patientRequests);
973
+
974
+ this.emit('appointment-requested', {
975
+ patientId: request.patientId,
976
+ requestId: id,
977
+ type: request.appointmentType,
978
+ urgency: request.urgency
979
+ });
980
+
981
+ return id;
982
+ }
983
+
984
+ /**
985
+ * Get appointment requests for patient
986
+ */
987
+ getAppointmentRequests(patientId: string): AppointmentRequest[] {
988
+ return this.appointmentRequests.get(patientId) || [];
989
+ }
990
+
991
+ /**
992
+ * Get dashboard data for patient
993
+ */
994
+ getDashboard(patientId: string): {
995
+ summary: PatientTreatmentSummary | undefined;
996
+ recentSymptoms: SymptomReport[];
997
+ adherence: ReturnType<typeof this.getAdherenceSummary>;
998
+ unreadMessages: number;
999
+ upcomingAppointments: PatientTreatmentSummary['upcomingAppointments'];
1000
+ recommendedContent: EducationalContent[];
1001
+ alerts: string[];
1002
+ } {
1003
+ const summary = this.getTreatmentSummary(patientId);
1004
+ const recentSymptoms = this.getSymptomHistory(patientId, 7);
1005
+ const adherence = this.getAdherenceSummary(patientId);
1006
+ const messages = this.getMessages(patientId, true);
1007
+ const content = this.getRecommendedContent(patientId);
1008
+
1009
+ const alerts: string[] = [];
1010
+
1011
+ // Check for concerning symptoms
1012
+ const latestSymptom = recentSymptoms[recentSymptoms.length - 1];
1013
+ if (latestSymptom?.requiresFollowUp) {
1014
+ alerts.push('Please contact your care team about your recent symptoms');
1015
+ }
1016
+
1017
+ // Check adherence
1018
+ if (adherence.overallAdherence < 80) {
1019
+ alerts.push('Your medication adherence has been below target - remember to take medications as prescribed');
1020
+ }
1021
+
1022
+ // Check unread messages
1023
+ if (messages.length > 0) {
1024
+ alerts.push(`You have ${messages.length} unread message(s) from your care team`);
1025
+ }
1026
+
1027
+ return {
1028
+ summary,
1029
+ recentSymptoms: recentSymptoms.slice(-5),
1030
+ adherence,
1031
+ unreadMessages: messages.length,
1032
+ upcomingAppointments: summary?.upcomingAppointments || [],
1033
+ recommendedContent: content.slice(0, 3),
1034
+ alerts
1035
+ };
1036
+ }
1037
+ }
1038
+
1039
+ export default PatientPortalService;