@imranq2/fhirpatientsummary 1.0.25 → 1.0.27

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 (3) hide show
  1. package/dist/index.cjs +512 -108
  2. package/dist/index.js +512 -108
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -87,6 +87,8 @@ var VITAL_SIGNS_SUMMARY_COMPONENT_MAP = {
87
87
  "Diastolic Blood Pressure": "valueRatio.denominator.value",
88
88
  "Default": "valueString"
89
89
  };
90
+ var RESULT_SUMMARY_OBSERVATION_CATEGORIES = ["laboratory", "Lab", "LAB"];
91
+ var RESULT_SUMMARY_OBSERVATION_DATE_FILTER = 2 * 365 * 24 * 60 * 60 * 1e3;
90
92
  var IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM = "https://fhir.icanbwell.com/4_0_0/CodeSystem/composition/";
91
93
 
92
94
  // src/structures/ips_section_resource_map.ts
@@ -106,7 +108,7 @@ var IPSSectionResourceFilters = {
106
108
  // Includes DeviceUseStatement. Device is needed for linked device details
107
109
  ["MedicalDeviceSection" /* MEDICAL_DEVICES */]: (resource) => ["DeviceUseStatement", "Device"].includes(resource.resourceType),
108
110
  // Only include finalized diagnostic reports
109
- ["ResultsSection" /* DIAGNOSTIC_REPORTS */]: (resource) => ["DiagnosticReport", "Observation"].includes(resource.resourceType) && resource.status === "final",
111
+ ["ResultsSection" /* DIAGNOSTIC_REPORTS */]: (resource) => resource.resourceType === "Observation" && resource.category?.some((cat) => cat.coding?.some((c) => RESULT_SUMMARY_OBSERVATION_CATEGORIES.includes(c.code))),
110
112
  // Only include completed procedures
111
113
  ["HistoryOfProceduresSection" /* PROCEDURES */]: (resource) => resource.resourceType === "Procedure" && resource.status === "completed",
112
114
  // Only include social history Observations
@@ -114,7 +116,7 @@ var IPSSectionResourceFilters = {
114
116
  // Only include pregnancy history Observations
115
117
  ["HistoryOfPregnancySection" /* PREGNANCY_HISTORY */]: (resource) => resource.resourceType === "Observation" && (resource.code?.coding?.some((c) => Object.keys(PREGNANCY_LOINC_CODES.PREGNANCY_STATUS).includes(c.code)) || resource.valueCodeableConcept?.coding?.some((c) => Object.keys(PREGNANCY_LOINC_CODES.PREGNANCY_OUTCOME).includes(c.code))),
116
118
  // Only include Conditions or completed ClinicalImpressions
117
- ["FunctionalStatusSection" /* FUNCTIONAL_STATUS */]: (resource) => resource.resourceType === "Condition" || resource.resourceType === "ClinicalImpression" && resource.status === "completed",
119
+ ["FunctionalStatusSection" /* FUNCTIONAL_STATUS */]: (resource) => resource.resourceType === "Condition" && resource.clinicalStatus?.coding?.some((c) => !["inactive", "resolved"].includes(c.code)) || resource.resourceType === "ClinicalImpression" && resource.status === "completed",
118
120
  // Only include resolved medical history Conditions
119
121
  ["HistoryOfPastIllnessSection" /* MEDICAL_HISTORY */]: (resource) => resource.resourceType === "Condition" && resource.clinicalStatus?.coding?.some((c) => ["inactive", "resolved"].includes(c.code)),
120
122
  // Only include active care plans
@@ -128,7 +130,7 @@ var IPSSectionSummaryCompositionFilter = {
128
130
  ["PlanOfCareSection" /* CARE_PLAN */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "careplan_summary_document"),
129
131
  ["ImmunizationSection" /* IMMUNIZATIONS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "immunization_summary_document"),
130
132
  ["MedicationSummarySection" /* MEDICATIONS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "medication_summary_document"),
131
- ["ResultsSection" /* DIAGNOSTIC_REPORTS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && ["lab_summary_document", "diagnosticreportlab_summary_document"].includes(c.code)),
133
+ // [IPSSections.DIAGNOSTIC_REPORTS]: (resource) => resource.resourceType === 'Composition' && resource.type?.coding?.some((c: any) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && ["lab_summary_document", "diagnosticreportlab_summary_document"].includes(c.code)),
132
134
  ["HistoryOfProceduresSection" /* PROCEDURES */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "procedure_summary_document")
133
135
  };
134
136
  var IPSSectionResourceHelper = class {
@@ -592,9 +594,18 @@ var TemplateUtilities = class {
592
594
  }
593
595
  return status;
594
596
  }
597
+ formatFloatValue(value) {
598
+ if (typeof value === "number") {
599
+ return value.toFixed(2).replace(/\.?0+$/, "");
600
+ } else if (typeof value === "string" && !isNaN(Number(value))) {
601
+ return parseFloat(value).toFixed(2).replace(/\.?0+$/, "");
602
+ }
603
+ return value;
604
+ }
595
605
  extractObservationSummaryValue(data, timezone) {
596
606
  if (data["valueQuantity.value"] !== void 0) {
597
- const value = data["valueQuantity.value"];
607
+ let value = data["valueQuantity.value"];
608
+ value = this.formatFloatValue(value);
598
609
  const unit = data["valueQuantity.unit"];
599
610
  return unit ? `${value} ${unit}` : `${value}`;
600
611
  }
@@ -611,7 +622,9 @@ var TemplateUtilities = class {
611
622
  return String(data["valueBoolean"]);
612
623
  }
613
624
  if (data["valueInteger"] !== void 0) {
614
- return String(data["valueInteger"]);
625
+ let value = String(data["valueInteger"]);
626
+ value = this.formatFloatValue(value);
627
+ return value;
615
628
  }
616
629
  if (data["valueDateTime"] !== void 0) {
617
630
  return this.renderTime(data["valueDateTime"], timezone);
@@ -631,7 +644,8 @@ var TemplateUtilities = class {
631
644
  return this.renderTime(data["valueTime"], timezone);
632
645
  }
633
646
  if (data["valueSampledData.origin.value"] !== void 0 || data["valueSampledData.origin.unit"] !== void 0) {
634
- const originValue = data["valueSampledData.origin.value"];
647
+ let originValue = data["valueSampledData.origin.value"];
648
+ originValue = this.formatFloatValue(originValue);
635
649
  const originUnit = data["valueSampledData.origin.unit"];
636
650
  let result = "";
637
651
  if (originValue !== void 0 && originUnit !== void 0) {
@@ -641,10 +655,10 @@ var TemplateUtilities = class {
641
655
  } else if (originUnit !== void 0) {
642
656
  result = `${originUnit}`;
643
657
  }
644
- const period = data["valueSampledData.period"];
645
- const factor = data["valueSampledData.factor"];
646
- const lowerLimit = data["valueSampledData.lowerLimit"];
647
- const upperLimit = data["valueSampledData.upperLimit"];
658
+ const period = this.formatFloatValue(data["valueSampledData.period"]);
659
+ const factor = this.formatFloatValue(data["valueSampledData.factor"]);
660
+ const lowerLimit = this.formatFloatValue(data["valueSampledData.lowerLimit"]);
661
+ const upperLimit = this.formatFloatValue(data["valueSampledData.upperLimit"]);
648
662
  const sampledData = data["valueSampledData.data"];
649
663
  const extras = [];
650
664
  if (period !== void 0) extras.push(`period: ${period}`);
@@ -680,14 +694,14 @@ var TemplateUtilities = class {
680
694
  if (data["valueRatio.numerator.value"] !== void 0 || data["valueRatio.denominator.value"] !== void 0) {
681
695
  let ratio = "";
682
696
  if (data["valueRatio.numerator.value"] !== void 0) {
683
- ratio += `${data["valueRatio.numerator.value"]}`;
697
+ ratio += `${this.formatFloatValue(data["valueRatio.numerator.value"])}`;
684
698
  if (data["valueRatio.numerator.unit"] !== void 0) {
685
699
  ratio += ` ${data["valueRatio.numerator.unit"]}`;
686
700
  }
687
701
  }
688
702
  if (data["valueRatio.denominator.value"] !== void 0) {
689
703
  ratio += " / ";
690
- ratio += `${data["valueRatio.denominator.value"]}`;
704
+ ratio += `${this.formatFloatValue(data["valueRatio.denominator.value"])}`;
691
705
  if (data["valueRatio.denominator.unit"] !== void 0) {
692
706
  ratio += ` ${data["valueRatio.denominator.unit"]}`;
693
707
  }
@@ -941,6 +955,313 @@ var TemplateUtilities = class {
941
955
 
942
956
  // src/constants.ts
943
957
  var ADDRESS_SIMILARITY_THRESHOLD = 70;
958
+ var LAB_LOINC_MAP = {
959
+ // METABOLIC PANELS (VERY COMMONLY ORDERED)
960
+ "Basic Metabolic Panel": [
961
+ "24321-2",
962
+ // Basic metabolic 2000 panel - Serum or Plasma
963
+ "51990-0"
964
+ // Basic metabolic 2000 panel - Blood
965
+ ],
966
+ "Comprehensive Metabolic Panel": [
967
+ "24323-8"
968
+ // Comprehensive metabolic 2000 panel - Serum or Plasma
969
+ ],
970
+ // CBC COMPONENTS
971
+ Hemoglobin: [
972
+ "718-7"
973
+ // Hemoglobin [Mass/volume] in Blood
974
+ ],
975
+ Hematocrit: [
976
+ "4544-3"
977
+ // Hematocrit [Volume Fraction] of Blood by Automated count
978
+ ],
979
+ "White Blood Cell Count": [
980
+ "6690-2"
981
+ // Leukocytes [///volume] in Blood by Automated count
982
+ ],
983
+ "Platelet Count": [
984
+ "777-3"
985
+ // Platelets [///volume] in Blood by Automated count
986
+ ],
987
+ "Complete Blood Count": [
988
+ "58410-2",
989
+ // CBC panel - Blood by Automated count
990
+ "57021-8",
991
+ // CBC W Auto Differential panel - Blood
992
+ "69738-3"
993
+ // CBC W Auto Differential panel - Blood by Automated count
994
+ ],
995
+ // CHEMISTRY - GLUCOSE
996
+ Glucose: [
997
+ "2345-7",
998
+ // Glucose [Mass/volume] in Serum or Plasma
999
+ "1558-6",
1000
+ // Fasting glucose [Mass/volume] in Serum or Plasma
1001
+ "2339-0"
1002
+ // Glucose [Mass/volume] in Blood
1003
+ ],
1004
+ // RENAL FUNCTION
1005
+ Creatinine: [
1006
+ "2160-0"
1007
+ // Creatinine [Mass/volume] in Serum or Plasma
1008
+ ],
1009
+ "Blood Urea Nitrogen": [
1010
+ "3094-0",
1011
+ // Urea nitrogen [Mass/volume] in Serum or Plasma
1012
+ "6299-2"
1013
+ // Urea nitrogen [Mass/volume] in Blood
1014
+ ],
1015
+ // ELECTROLYTES
1016
+ Sodium: [
1017
+ "2951-2"
1018
+ // Sodium [Moles/volume] in Serum or Plasma
1019
+ ],
1020
+ Potassium: [
1021
+ "2823-3"
1022
+ // Potassium [Moles/volume] in Serum or Plasma
1023
+ ],
1024
+ Chloride: [
1025
+ "2075-0"
1026
+ // Chloride [Moles/volume] in Serum or Plasma
1027
+ ],
1028
+ Calcium: [
1029
+ "17861-6",
1030
+ // Calcium [Mass/volume] in Serum or Plasma
1031
+ "1994-3"
1032
+ // Calcium.ionized [Moles/volume] in Serum or Plasma
1033
+ ],
1034
+ Magnesium: [
1035
+ "19123-9"
1036
+ // Magnesium [Mass/volume] in Serum or Plasma
1037
+ ],
1038
+ Phosphate: [
1039
+ "14879-1"
1040
+ // Phosphate [Mass/volume] in Serum or Plasma
1041
+ ],
1042
+ // PROTEINS
1043
+ Albumin: [
1044
+ "1751-7"
1045
+ // Albumin [Mass/volume] in Serum or Plasma
1046
+ ],
1047
+ "Total Protein": [
1048
+ "2885-2"
1049
+ // Protein [Mass/volume] in Serum or Plasma
1050
+ ],
1051
+ // LIVER FUNCTION
1052
+ Bilirubin: [
1053
+ "1975-2",
1054
+ // Bilirubin.total [Mass/volume] in Serum or Plasma
1055
+ "1968-7",
1056
+ // Bilirubin.direct [Mass/volume] in Serum or Plasma
1057
+ "1971-1"
1058
+ // Bilirubin.indirect [Mass/volume] in Serum or Plasma
1059
+ ],
1060
+ "Alkaline Phosphatase": [
1061
+ "6768-6"
1062
+ // Alkaline phosphatase [Enzymatic activity/volume] in Serum or Plasma
1063
+ ],
1064
+ AST: [
1065
+ "1920-8"
1066
+ // Aspartate aminotransferase [Enzymatic activity/volume] in Serum or Plasma
1067
+ ],
1068
+ ALT: [
1069
+ "1742-6"
1070
+ // Alanine aminotransferase [Enzymatic activity/volume] in Serum or Plasma
1071
+ ],
1072
+ GGT: [
1073
+ "2324-2"
1074
+ // Gamma glutamyl transferase [Enzymatic activity/volume] in Serum or Plasma
1075
+ ],
1076
+ // ENDOCRINE
1077
+ TSH: [
1078
+ "3016-3"
1079
+ // Thyrotropin [Units/volume] in Serum or Plasma
1080
+ ],
1081
+ "Free T4": [
1082
+ "3024-7"
1083
+ // Thyroxine (T4) free [Mass/volume] in Serum or Plasma
1084
+ ],
1085
+ "Total T4": [
1086
+ "3026-2"
1087
+ // Thyroxine (T4) [Mass/volume] in Serum or Plasma
1088
+ ],
1089
+ "Free T3": [
1090
+ "3051-0"
1091
+ // Triiodothyronine (T3) free [Mass/volume] in Serum or Plasma
1092
+ ],
1093
+ "Total T3": [
1094
+ "3053-6"
1095
+ // Triiodothyronine (T3) [Mass/volume] in Serum or Plasma
1096
+ ],
1097
+ HbA1c: [
1098
+ "4548-4",
1099
+ // Hemoglobin A1c/Hemoglobin.total in Blood
1100
+ "17856-6"
1101
+ // Hemoglobin A1c/Hemoglobin.total in Blood by HPLC
1102
+ ],
1103
+ // LIPID PANEL
1104
+ "Lipid Panel": [
1105
+ "24331-1",
1106
+ // Lipid 1996 panel - Serum or Plasma
1107
+ "57698-3"
1108
+ // Lipid panel with direct LDL - Serum or Plasma
1109
+ ],
1110
+ "Cholesterol Total": [
1111
+ "2093-3"
1112
+ // Cholesterol [Mass/volume] in Serum or Plasma
1113
+ ],
1114
+ "HDL Cholesterol": [
1115
+ "2085-9"
1116
+ // Cholesterol in HDL [Mass/volume] in Serum or Plasma
1117
+ ],
1118
+ "LDL Cholesterol": [
1119
+ "13457-7",
1120
+ // Cholesterol in LDL [Mass/volume] in Serum or Plasma by calculation
1121
+ "18262-6"
1122
+ // Cholesterol in LDL [Mass/volume] in Serum or Plasma by Direct assay
1123
+ ],
1124
+ Triglycerides: [
1125
+ "2571-8"
1126
+ // Triglyceride [Mass/volume] in Serum or Plasma
1127
+ ],
1128
+ // COAGULATION STUDIES (COMMONLY MISSING!)
1129
+ PT: [
1130
+ "5902-2"
1131
+ // Prothrombin time (PT)
1132
+ ],
1133
+ INR: [
1134
+ "6301-6"
1135
+ // INR in Platelet poor plasma by Coagulation assay
1136
+ ],
1137
+ PTT: [
1138
+ "3173-2",
1139
+ // aPTT in Blood by Coagulation assay
1140
+ "14979-9"
1141
+ // aPTT in Platelet poor plasma by Coagulation assay
1142
+ ],
1143
+ Fibrinogen: [
1144
+ "3255-7"
1145
+ // Fibrinogen [Mass/volume] in Platelet poor plasma by Coagulation assay
1146
+ ],
1147
+ "D-Dimer": [
1148
+ "48065-7",
1149
+ // D-dimer FEU [Mass/volume] in Platelet poor plasma
1150
+ "48066-5"
1151
+ // D-dimer DDU [Mass/volume] in Platelet poor plasma
1152
+ ],
1153
+ // CARDIAC MARKERS (CRITICAL FOR ER!)
1154
+ "Troponin I": [
1155
+ "10839-9",
1156
+ // Troponin I.cardiac [Mass/volume] in Serum or Plasma
1157
+ "42757-5",
1158
+ // Troponin I.cardiac [Mass/volume] in Blood
1159
+ "89579-7"
1160
+ // Troponin I.cardiac [Mass/volume] in Serum or Plasma by High sensitivity method
1161
+ ],
1162
+ "Troponin T": [
1163
+ "6598-7",
1164
+ // Troponin T.cardiac [Mass/volume] in Serum or Plasma
1165
+ "48425-3"
1166
+ // Troponin T.cardiac [Mass/volume] in Serum or Plasma by High sensitivity method
1167
+ ],
1168
+ BNP: [
1169
+ "30934-4"
1170
+ // BNP [Mass/volume] in Serum or Plasma
1171
+ ],
1172
+ "NT-proBNP": [
1173
+ "33762-6"
1174
+ // NT-proBNP [Mass/volume] in Serum or Plasma
1175
+ ],
1176
+ "CK-MB": [
1177
+ "13969-1"
1178
+ // Creatine kinase.MB [Mass/volume] in Serum or Plasma
1179
+ ],
1180
+ // INFLAMMATORY MARKERS
1181
+ CRP: [
1182
+ "1988-5",
1183
+ // C reactive protein [Mass/volume] in Serum or Plasma
1184
+ "30522-7"
1185
+ // C reactive protein [Mass/volume] in Serum or Plasma by High sensitivity method
1186
+ ],
1187
+ ESR: [
1188
+ "30341-2",
1189
+ // Erythrocyte sedimentation rate by Westergren method
1190
+ "4537-7"
1191
+ // Erythrocyte sedimentation rate
1192
+ ],
1193
+ // VITAMINS & MINERALS (VERY HIGH VOLUME!)
1194
+ "Vitamin D": [
1195
+ "1990-1",
1196
+ // Vitamin D [Mass/volume] in Serum or Plasma (obsolete, but still used)
1197
+ "14635-7",
1198
+ // 25-Hydroxyvitamin D3 [Mass/volume] in Serum or Plasma
1199
+ "62292-8"
1200
+ // 25-Hydroxyvitamin D2+D3 [Mass/volume] in Serum or Plasma
1201
+ ],
1202
+ "Vitamin B12": [
1203
+ "2132-9"
1204
+ // Cobalamin (Vitamin B12) [Mass/volume] in Serum or Plasma
1205
+ ],
1206
+ Folate: [
1207
+ "2284-8",
1208
+ // Folate [Mass/volume] in Serum or Plasma
1209
+ "15152-2"
1210
+ // Folate [Mass/volume] in Red Blood Cells
1211
+ ],
1212
+ Iron: [
1213
+ "2498-4"
1214
+ // Iron [Mass/volume] in Serum or Plasma
1215
+ ],
1216
+ Ferritin: [
1217
+ "2276-4"
1218
+ // Ferritin [Mass/volume] in Serum or Plasma
1219
+ ],
1220
+ TIBC: [
1221
+ "2500-7"
1222
+ // Iron binding capacity [Mass/volume] in Serum or Plasma
1223
+ ],
1224
+ // OTHER COMMON TESTS
1225
+ PSA: [
1226
+ "2857-1",
1227
+ // Prostate specific Ag [Mass/volume] in Serum or Plasma
1228
+ "10886-0"
1229
+ // Prostate specific Ag Free [Mass/volume] in Serum or Plasma
1230
+ ],
1231
+ "Uric Acid": [
1232
+ "3084-1"
1233
+ // Urate [Mass/volume] in Serum or Plasma
1234
+ ],
1235
+ LDH: [
1236
+ "2532-0",
1237
+ // Lactate dehydrogenase [Enzymatic activity/volume] in Serum or Plasma
1238
+ "14804-9"
1239
+ // Lactate dehydrogenase [Enzymatic activity/volume] in Serum or Plasma by Lactate to pyruvate reaction
1240
+ ],
1241
+ Amylase: [
1242
+ "1798-8"
1243
+ // Amylase [Enzymatic activity/volume] in Serum or Plasma
1244
+ ],
1245
+ Lipase: [
1246
+ "3040-3"
1247
+ // Lipase [Enzymatic activity/volume] in Serum or Plasma
1248
+ ],
1249
+ hCG: [
1250
+ "21198-7",
1251
+ // Choriogonadotropin.beta subunit [Units/volume] in Serum or Plasma
1252
+ "2118-8",
1253
+ // Choriogonadotropin (pregnancy test) [Presence] in Serum or Plasma
1254
+ "2106-3"
1255
+ // Choriogonadotropin (pregnancy test) [Presence] in Urine
1256
+ ],
1257
+ // URINALYSIS
1258
+ Urinalysis: [
1259
+ "24357-6",
1260
+ // Urinalysis macro (dipstick) panel - Urine
1261
+ "24356-8"
1262
+ // Urinalysis complete panel - Urine
1263
+ ]
1264
+ };
944
1265
 
945
1266
  // src/narratives/templates/typescript/PatientTemplate.ts
946
1267
  var PatientTemplate = class _PatientTemplate {
@@ -1859,8 +2180,8 @@ var ProblemListTemplate = class _ProblemListTemplate {
1859
2180
  let html = ``;
1860
2181
  const activeConditions = resources.map((entry) => entry) || [];
1861
2182
  activeConditions.sort((a, b) => {
1862
- const dateA = a.onsetDateTime ? new Date(a.onsetDateTime).getTime() : 0;
1863
- const dateB = b.onsetDateTime ? new Date(b.onsetDateTime).getTime() : 0;
2183
+ const dateA = a.recordedDate ? new Date(a.recordedDate).getTime() : 0;
2184
+ const dateB = b.recordedDate ? new Date(b.recordedDate).getTime() : 0;
1864
2185
  return dateB - dateA;
1865
2186
  });
1866
2187
  html += `
@@ -1873,12 +2194,17 @@ var ProblemListTemplate = class _ProblemListTemplate {
1873
2194
  </tr>
1874
2195
  </thead>
1875
2196
  <tbody>`;
2197
+ const addedConditionCodes = /* @__PURE__ */ new Set();
1876
2198
  for (const cond of activeConditions) {
1877
- html += `<tr id="${templateUtilities.narrativeLinkId(cond)}">
1878
- <td class="Name">${templateUtilities.codeableConcept(cond.code)}</td>
1879
- <td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
1880
- <td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
1881
- </tr>`;
2199
+ const conditionCode = templateUtilities.codeableConcept(cond.code);
2200
+ if (!addedConditionCodes.has(conditionCode)) {
2201
+ addedConditionCodes.add(conditionCode);
2202
+ html += `<tr id="${templateUtilities.narrativeLinkId(cond)}">
2203
+ <td class="Name">${conditionCode}</td>
2204
+ <td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
2205
+ <td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
2206
+ </tr>`;
2207
+ }
1882
2208
  }
1883
2209
  html += `</tbody>
1884
2210
  </table>`;
@@ -2060,6 +2386,12 @@ var MedicalDevicesTemplate = class _MedicalDevicesTemplate {
2060
2386
  };
2061
2387
 
2062
2388
  // src/narratives/templates/typescript/DiagnosticResultsTemplate.ts
2389
+ var loincToLabName = {};
2390
+ for (const [labName, loincCodes] of Object.entries(LAB_LOINC_MAP)) {
2391
+ for (const code of loincCodes) {
2392
+ loincToLabName[code] = labName;
2393
+ }
2394
+ }
2063
2395
  var DiagnosticResultsTemplate = class _DiagnosticResultsTemplate {
2064
2396
  /**
2065
2397
  * Generate HTML narrative for Diagnostic Results
@@ -2362,8 +2694,8 @@ var DiagnosticResultsTemplate = class _DiagnosticResultsTemplate {
2362
2694
  </tr>
2363
2695
  </thead>
2364
2696
  <tbody>`;
2365
- let observationExists = false;
2366
- let diagnosticReportExists = false;
2697
+ const observationAdded = /* @__PURE__ */ new Set();
2698
+ const diagnosticReportAdded = /* @__PURE__ */ new Set();
2367
2699
  for (const resourceItem of resources) {
2368
2700
  for (const rowData of resourceItem.section ?? []) {
2369
2701
  const data = {};
@@ -2403,7 +2735,6 @@ var DiagnosticResultsTemplate = class _DiagnosticResultsTemplate {
2403
2735
  }
2404
2736
  }
2405
2737
  if (resourceItem.title === "Observation|Labs Summary Grouped by Lab Code") {
2406
- observationExists = true;
2407
2738
  let date = data["effectiveDateTime"] ? templateUtilities.renderTime(data["effectiveDateTime"], timezone) : "";
2408
2739
  if (!date && data["effectivePeriodStart"]) {
2409
2740
  date = templateUtilities.renderTime(data["effectivePeriodStart"], timezone);
@@ -2412,48 +2743,59 @@ var DiagnosticResultsTemplate = class _DiagnosticResultsTemplate {
2412
2743
  }
2413
2744
  }
2414
2745
  if (components.length > 0) {
2415
- const groupName = data["code"] ?? "";
2746
+ const groupName = data["code"] ? `${data["code"]} - ` : "";
2416
2747
  for (const component of components) {
2417
- this.formatSummaryObservationData(component);
2418
- observationhtml += `
2419
- <tr>
2420
- <td>${groupName ? groupName + " - " : ""}${component["code"] ?? "-"}</td>
2421
- <td>${component["formattedValue"] ?? "-"}</td>
2422
- <td>${component["referenceRange"]?.trim() ?? "-"}</td>
2423
- <td>${date ?? "-"}</td>
2424
- </tr>`;
2748
+ const componentCode = `${groupName}${component["code"] ?? ""}`;
2749
+ if (componentCode && !observationAdded.has(componentCode)) {
2750
+ observationAdded.add(componentCode);
2751
+ this.formatSummaryObservationData(component);
2752
+ observationhtml += `
2753
+ <tr>
2754
+ <td>${componentCode}</td>
2755
+ <td>${component["formattedValue"] ?? "-"}</td>
2756
+ <td>${component["referenceRange"]?.trim() ?? "-"}</td>
2757
+ <td>${date ?? "-"}</td>
2758
+ </tr>`;
2759
+ }
2425
2760
  }
2426
2761
  } else {
2427
- this.formatSummaryObservationData(data);
2428
- observationhtml += `
2429
- <tr>
2430
- <td>${data["code"] ?? "-"}</td>
2431
- <td>${data["formattedValue"] ?? "-"}</td>
2432
- <td>${data["referenceRange"]?.trim() ?? "-"}</td>
2433
- <td>${date ?? "-"}</td>
2434
- </tr>`;
2762
+ const code = data["code"] ?? "";
2763
+ if (code && !observationAdded.has(code)) {
2764
+ observationAdded.add(code);
2765
+ this.formatSummaryObservationData(data);
2766
+ observationhtml += `
2767
+ <tr>
2768
+ <td>${data["code"] ?? "-"}</td>
2769
+ <td>${data["formattedValue"] ?? "-"}</td>
2770
+ <td>${data["referenceRange"]?.trim() ?? "-"}</td>
2771
+ <td>${date ?? "-"}</td>
2772
+ </tr>`;
2773
+ }
2435
2774
  }
2436
2775
  } else if (resourceItem.title === "DiagnosticReportLab Summary Grouped by DiagnosticReport|Lab Code") {
2437
2776
  if (data["status"] === "final") {
2438
- diagnosticReportExists = true;
2439
- diagnosticReporthtml += `
2440
- <tr>
2441
- <td>${data["report"] ?? "-"}</td>
2442
- <td>${data["performer"] ?? "-"}</td>
2443
- <td>${templateUtilities.renderTime(data["issued"], timezone) ?? "-"}</td>
2444
- </tr>`;
2777
+ const reportName = data["report"] ?? "";
2778
+ if (reportName && !diagnosticReportAdded.has(reportName)) {
2779
+ diagnosticReportAdded.add(reportName);
2780
+ diagnosticReporthtml += `
2781
+ <tr>
2782
+ <td>${data["report"] ?? "-"}</td>
2783
+ <td>${data["performer"] ?? "-"}</td>
2784
+ <td>${templateUtilities.renderTime(data["issued"], timezone) ?? "-"}</td>
2785
+ </tr>`;
2786
+ }
2445
2787
  }
2446
2788
  }
2447
2789
  }
2448
2790
  }
2449
- if (observationExists) {
2791
+ if (observationAdded.size > 0) {
2450
2792
  html += observationhtml;
2451
2793
  html += `
2452
2794
  </tbody>
2453
2795
  </table>
2454
2796
  </div>`;
2455
2797
  }
2456
- if (diagnosticReportExists) {
2798
+ if (diagnosticReportAdded.size > 0) {
2457
2799
  html += diagnosticReporthtml;
2458
2800
  html += `
2459
2801
  </tbody>
@@ -2462,7 +2804,7 @@ var DiagnosticResultsTemplate = class _DiagnosticResultsTemplate {
2462
2804
  }
2463
2805
  html += `
2464
2806
  </div>`;
2465
- return observationExists || diagnosticReportExists ? html : void 0;
2807
+ return observationAdded.size > 0 || diagnosticReportAdded.size > 0 ? html : void 0;
2466
2808
  }
2467
2809
  /**
2468
2810
  * Internal static implementation that actually generates the narrative
@@ -2476,30 +2818,80 @@ var DiagnosticResultsTemplate = class _DiagnosticResultsTemplate {
2476
2818
  const observations = this.getObservations(resources);
2477
2819
  if (observations.length > 0) {
2478
2820
  observations.sort((a, b) => {
2479
- const dateA = a.effectiveDateTime || a.effectivePeriod?.start;
2480
- const dateB = b.effectiveDateTime || b.effectivePeriod?.start;
2481
- return dateA && dateB ? new Date(dateB).getTime() - new Date(dateA).getTime() : 0;
2821
+ const dateA = this.getObservationDate(a);
2822
+ const dateB = this.getObservationDate(b);
2823
+ return dateA && dateB ? dateB.getTime() - dateA.getTime() : 0;
2482
2824
  });
2825
+ this.filterObservationForLoincCodes(observations);
2483
2826
  html += this.renderObservations(templateUtilities, observations, timezone);
2484
2827
  }
2485
- const diagnosticReports = this.getDiagnosticReports(resources);
2486
- if (diagnosticReports.length > 0) {
2487
- diagnosticReports.sort((a, b) => {
2488
- const dateA = a.issued;
2489
- const dateB = b.issued;
2490
- return dateA && dateB ? new Date(dateB).getTime() - new Date(dateA).getTime() : 0;
2491
- });
2492
- html += this.renderDiagnosticReports(templateUtilities, diagnosticReports, timezone);
2828
+ if (process.env.ENABLE_DIAGNOSTIC_REPORTS_IN_SUMMARY === "true") {
2829
+ const diagnosticReports = this.getDiagnosticReports(resources);
2830
+ if (diagnosticReports.length > 0) {
2831
+ diagnosticReports.sort((a, b) => {
2832
+ const dateA = a.issued;
2833
+ const dateB = b.issued;
2834
+ return dateA && dateB ? new Date(dateB).getTime() - new Date(dateA).getTime() : 0;
2835
+ });
2836
+ html += this.renderDiagnosticReports(templateUtilities, diagnosticReports, timezone);
2837
+ }
2493
2838
  }
2494
2839
  return html;
2495
2840
  }
2841
+ static filterObservationForLoincCodes(observations) {
2842
+ const labsAdded = /* @__PURE__ */ new Set();
2843
+ const filteredObservations = [];
2844
+ for (const obs of observations) {
2845
+ const loincCode = this.getObservationLoincCode(obs);
2846
+ if (loincCode && loincToLabName[loincCode]) {
2847
+ const labName = loincToLabName[loincCode];
2848
+ if (!labsAdded.has(labName)) {
2849
+ labsAdded.add(labName);
2850
+ filteredObservations.push(obs);
2851
+ }
2852
+ }
2853
+ }
2854
+ observations.length = 0;
2855
+ observations.push(...filteredObservations);
2856
+ }
2857
+ static getObservationLoincCode(obs) {
2858
+ if (obs.code && obs.code.coding) {
2859
+ for (const coding of obs.code.coding) {
2860
+ if (coding.system && coding.system.toLowerCase().includes("loinc") && coding.code) {
2861
+ return coding.code;
2862
+ }
2863
+ }
2864
+ }
2865
+ return void 0;
2866
+ }
2867
+ static getObservationDate(obs) {
2868
+ let obsDate = void 0;
2869
+ if (obs.effectiveDateTime) {
2870
+ obsDate = new Date(obs.effectiveDateTime);
2871
+ } else if (obs.effectivePeriod) {
2872
+ if (obs.effectivePeriod.start) {
2873
+ obsDate = new Date(obs.effectivePeriod.start);
2874
+ } else if (obs.effectivePeriod.end) {
2875
+ obsDate = new Date(obs.effectivePeriod.end);
2876
+ }
2877
+ }
2878
+ return obsDate;
2879
+ }
2496
2880
  /**
2497
2881
  * Get all Observation resources from the resource array
2498
2882
  * @param resources - FHIR resources array
2499
2883
  * @returns Array of Observation resources
2500
2884
  */
2501
2885
  static getObservations(resources) {
2502
- return resources.filter((resourceItem) => resourceItem.resourceType === "Observation").map((resourceItem) => resourceItem);
2886
+ return resources.filter((resourceItem) => {
2887
+ if (resourceItem.resourceType === "Observation") {
2888
+ const obsDate = this.getObservationDate(resourceItem);
2889
+ if (obsDate && obsDate >= new Date(Date.now() - RESULT_SUMMARY_OBSERVATION_DATE_FILTER)) {
2890
+ return true;
2891
+ }
2892
+ }
2893
+ return false;
2894
+ }).map((resourceItem) => resourceItem);
2503
2895
  }
2504
2896
  /**
2505
2897
  * Get all DiagnosticReport resources from the resource array
@@ -2517,32 +2909,35 @@ var DiagnosticResultsTemplate = class _DiagnosticResultsTemplate {
2517
2909
  * @returns HTML string for rendering
2518
2910
  */
2519
2911
  static renderObservations(templateUtilities, observations, timezone) {
2520
- let html = `
2521
- <h3>Observations</h3>
2912
+ let html = "";
2913
+ if (process.env.ENABLE_DIAGNOSTIC_REPORTS_IN_SUMMARY === "true") {
2914
+ html += `
2915
+ <h3>Observations</h3>`;
2916
+ }
2917
+ html += `
2522
2918
  <table>
2523
2919
  <thead>
2524
2920
  <tr>
2525
2921
  <th>Code</th>
2526
2922
  <th>Result</th>
2527
- <th>Unit</th>
2528
- <th>Interpretation</th>
2529
2923
  <th>Reference Range</th>
2530
- <th>Comments</th>
2531
2924
  <th>Date</th>
2532
2925
  </tr>
2533
2926
  </thead>
2534
2927
  <tbody>`;
2928
+ const observationAdded = /* @__PURE__ */ new Set();
2535
2929
  for (const obs of observations) {
2536
- html += `
2537
- <tr id="${templateUtilities.narrativeLinkId(obs)}">
2538
- <td>${templateUtilities.codeableConcept(obs.code)}</td>
2539
- <td>${templateUtilities.extractObservationValue(obs)}</td>
2540
- <td>${templateUtilities.extractObservationValueUnit(obs)}</td>
2541
- <td>${templateUtilities.firstFromCodeableConceptList(obs.interpretation)}</td>
2542
- <td>${templateUtilities.concatReferenceRange(obs.referenceRange)}</td>
2543
- <td>${templateUtilities.renderNotes(obs.note, timezone)}</td>
2544
- <td>${obs.effectiveDateTime ? templateUtilities.renderTime(obs.effectiveDateTime, timezone) : obs.effectivePeriod ? templateUtilities.renderPeriod(obs.effectivePeriod, timezone) : ""}</td>
2545
- </tr>`;
2930
+ const obsCode = templateUtilities.codeableConcept(obs.code);
2931
+ if (!observationAdded.has(obsCode)) {
2932
+ observationAdded.add(obsCode);
2933
+ html += `
2934
+ <tr id="${templateUtilities.narrativeLinkId(obs)}">
2935
+ <td>${obsCode}</td>
2936
+ <td>${templateUtilities.extractObservationValue(obs)}</td>
2937
+ <td>${templateUtilities.concatReferenceRange(obs.referenceRange)}</td>
2938
+ <td>${obs.effectiveDateTime ? templateUtilities.renderTime(obs.effectiveDateTime, timezone) : obs.effectivePeriod ? templateUtilities.renderPeriod(obs.effectivePeriod, timezone) : ""}</td>
2939
+ </tr>`;
2940
+ }
2546
2941
  }
2547
2942
  html += `
2548
2943
  </tbody>
@@ -2569,18 +2964,23 @@ var DiagnosticResultsTemplate = class _DiagnosticResultsTemplate {
2569
2964
  </tr>
2570
2965
  </thead>
2571
2966
  <tbody>`;
2967
+ const diagnosticReportAdded = /* @__PURE__ */ new Set();
2572
2968
  for (const report of reports) {
2573
- let resultCount = "";
2574
- if (report.result && Array.isArray(report.result)) {
2575
- resultCount = `${report.result.length} result${report.result.length !== 1 ? "s" : ""}`;
2969
+ const reportName = templateUtilities.codeableConcept(report.code);
2970
+ if (!diagnosticReportAdded.has(reportName)) {
2971
+ diagnosticReportAdded.add(reportName);
2972
+ let resultCount = "";
2973
+ if (report.result && Array.isArray(report.result)) {
2974
+ resultCount = `${report.result.length} result${report.result.length !== 1 ? "s" : ""}`;
2975
+ }
2976
+ html += `
2977
+ <tr id="${templateUtilities.narrativeLinkId(report)}">
2978
+ <td>${reportName}</td>
2979
+ <td>${templateUtilities.firstFromCodeableConceptList(report.category)}</td>
2980
+ <td>${resultCount}</td>
2981
+ <td>${report.issued ? templateUtilities.renderTime(report.issued, timezone) : ""}</td>
2982
+ </tr>`;
2576
2983
  }
2577
- html += `
2578
- <tr id="${templateUtilities.narrativeLinkId(report)}">
2579
- <td>${templateUtilities.codeableConcept(report.code)}</td>
2580
- <td>${templateUtilities.firstFromCodeableConceptList(report.category)}</td>
2581
- <td>${resultCount}</td>
2582
- <td>${report.issued ? templateUtilities.renderTime(report.issued, timezone) : ""}</td>
2583
- </tr>`;
2584
2984
  }
2585
2985
  html += `
2586
2986
  </tbody>
@@ -2760,8 +3160,8 @@ var PastHistoryOfIllnessTemplate = class {
2760
3160
  let html = ``;
2761
3161
  const resolvedConditions = resources.map((entry) => entry) || [];
2762
3162
  resolvedConditions.sort((a, b) => {
2763
- const dateA = a.onsetDateTime ? new Date(a.onsetDateTime).getTime() : 0;
2764
- const dateB = b.onsetDateTime ? new Date(b.onsetDateTime).getTime() : 0;
3163
+ const dateA = a.recordedDate ? new Date(a.recordedDate).getTime() : 0;
3164
+ const dateB = b.recordedDate ? new Date(b.recordedDate).getTime() : 0;
2765
3165
  return dateB - dateA;
2766
3166
  });
2767
3167
  html += `
@@ -2775,13 +3175,18 @@ var PastHistoryOfIllnessTemplate = class {
2775
3175
  </tr>
2776
3176
  </thead>
2777
3177
  <tbody>`;
3178
+ const addedConditionCodes = /* @__PURE__ */ new Set();
2778
3179
  for (const cond of resolvedConditions) {
2779
- html += `<tr id="${templateUtilities.narrativeLinkId(cond)}">
2780
- <td class="Name">${templateUtilities.codeableConcept(cond.code)}</td>
2781
- <td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
2782
- <td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
2783
- <td class="ResolvedDate">${templateUtilities.renderDate(cond.abatementDateTime)}</td>
2784
- </tr>`;
3180
+ const conditionCode = templateUtilities.codeableConcept(cond.code);
3181
+ if (!addedConditionCodes.has(conditionCode)) {
3182
+ addedConditionCodes.add(conditionCode);
3183
+ html += `<tr id="${templateUtilities.narrativeLinkId(cond)}">
3184
+ <td class="Name">${conditionCode}</td>
3185
+ <td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
3186
+ <td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
3187
+ <td class="ResolvedDate">${templateUtilities.renderDate(cond.abatementDateTime)}</td>
3188
+ </tr>`;
3189
+ }
2785
3190
  }
2786
3191
  html += `</tbody>
2787
3192
  </table>`;
@@ -2906,20 +3311,14 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
2906
3311
  const clinicalImpressions = [];
2907
3312
  for (const resourceItem of resources) {
2908
3313
  if (resourceItem.resourceType === "Condition") {
2909
- const cond = resourceItem;
2910
- const isResolved = cond.clinicalStatus?.coding?.some(
2911
- (c) => c.code === "resolved" || c.code === "inactive" || c.display?.toLowerCase().includes("resolved")
2912
- );
2913
- if (!isResolved) {
2914
- activeConditions.push(cond);
2915
- }
3314
+ activeConditions.push(resourceItem);
2916
3315
  } else if (resourceItem.resourceType === "ClinicalImpression") {
2917
3316
  clinicalImpressions.push(resourceItem);
2918
3317
  }
2919
3318
  }
2920
3319
  activeConditions.sort((a, b) => {
2921
- const dateA = a.onsetDateTime ? new Date(a.onsetDateTime).getTime() : 0;
2922
- const dateB = b.onsetDateTime ? new Date(b.onsetDateTime).getTime() : 0;
3320
+ const dateA = a.recordedDate ? new Date(a.recordedDate).getTime() : 0;
3321
+ const dateB = b.recordedDate ? new Date(b.recordedDate).getTime() : 0;
2923
3322
  return dateB - dateA;
2924
3323
  });
2925
3324
  clinicalImpressions.sort((a, b) => {
@@ -2938,12 +3337,17 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
2938
3337
  </tr>
2939
3338
  </thead>
2940
3339
  <tbody>`;
3340
+ const addedConditionCodes = /* @__PURE__ */ new Set();
2941
3341
  for (const cond of activeConditions) {
2942
- html += `<tr id="${templateUtilities.narrativeLinkId(cond)}">
2943
- <td class="Name">${templateUtilities.codeableConcept(cond.code)}</td>
2944
- <td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
2945
- <td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
2946
- </tr>`;
3342
+ const conditionCode = templateUtilities.codeableConcept(cond.code);
3343
+ if (!addedConditionCodes.has(conditionCode)) {
3344
+ addedConditionCodes.add(conditionCode);
3345
+ html += `<tr id="${templateUtilities.narrativeLinkId(cond)}">
3346
+ <td class="Name">${conditionCode}</td>
3347
+ <td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
3348
+ <td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
3349
+ </tr>`;
3350
+ }
2947
3351
  }
2948
3352
  html += `</tbody>
2949
3353
  </table>`;