@imranq2/fhirpatientsummary 1.0.17 → 1.0.19

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 CHANGED
@@ -18,7 +18,7 @@ npm install
18
18
 
19
19
  You can use the `ComprehensiveIPSCompositionBuilder` in your TypeScript or JavaScript project to generate an IPS-compliant FHIR Bundle. The builder supports both fluent section addition and a convenient `read_bundle()` method to extract all supported sections from a FHIR Bundle.
20
20
 
21
- ### Example: Using setPatient and addSection
21
+ ### Example: Using setPatient and makeSectionAsync
22
22
 
23
23
  ```typescript
24
24
  import { ComprehensiveIPSCompositionBuilder } from './src/generators/fhir_summary_generator';
@@ -26,10 +26,10 @@ import { IPSSections } from './src/structures/ips_sections';
26
26
 
27
27
  const builder = new ComprehensiveIPSCompositionBuilder()
28
28
  .setPatient(patientResource)
29
- .addSection(IPSSections.ALLERGIES, allergiesArray, 'America/New_York')
30
- .addSection(IPSSections.MEDICATIONS, medicationsArray, 'America/New_York')
31
- .addSection(IPSSections.PROBLEMS, problemsArray, 'America/New_York')
32
- .addSection(IPSSections.IMMUNIZATIONS, immunizationsArray, 'America/New_York');
29
+ .makeSectionAsync(IPSSections.ALLERGIES, allergiesArray, 'America/New_York')
30
+ .makeSectionAsync(IPSSections.MEDICATIONS, medicationsArray, 'America/New_York')
31
+ .makeSectionAsync(IPSSections.PROBLEMS, problemsArray, 'America/New_York')
32
+ .makeSectionAsync(IPSSections.IMMUNIZATIONS, immunizationsArray, 'America/New_York');
33
33
 
34
34
  const bundle = builder.build_bundle(
35
35
  'example-organization',
@@ -61,7 +61,7 @@ console.log(JSON.stringify(bundle, null, 2));
61
61
  ```
62
62
 
63
63
  - Use `setPatient(patientResource)` to set the patient.
64
- - Use `addSection(sectionType, resources, timezone)` to add each IPS section, or use `read_bundle(fhirBundle, timezone)` to extract all supported sections from a FHIR Bundle.
64
+ - Use `makeSectionAsync(sectionType, resources, timezone)` to add each IPS section, or use `read_bundle(fhirBundle, timezone)` to extract all supported sections from a FHIR Bundle.
65
65
  - Use `build_bundle` to generate the final FHIR Bundle.
66
66
 
67
67
  ## Running Tests
package/dist/index.cjs CHANGED
@@ -142,10 +142,17 @@ var IPSSectionResourceFilters = {
142
142
  // Only include active advance directives (Consent resources)
143
143
  ["AdvanceDirectivesSection" /* ADVANCE_DIRECTIVES */]: (resource) => resource.resourceType === "Consent" && resource.status === "active"
144
144
  };
145
+ var IPSSectionSummaryCompositionFilter = {
146
+ ["AllergyIntoleranceSection" /* ALLERGIES */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === "https://fhir.icanbwell.com/4_0_0/CodeSystem/composition/" && c.code === "allergy_summary_document"),
147
+ ["VitalSignsSection" /* VITAL_SIGNS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === "https://fhir.icanbwell.com/4_0_0/CodeSystem/composition/" && c.code === "vital_summary_document")
148
+ };
145
149
  var IPSSectionResourceHelper = class {
146
150
  static getResourceFilterForSection(section) {
147
151
  return IPSSectionResourceFilters[section];
148
152
  }
153
+ static getSummaryCompositionFilterForSection(section) {
154
+ return IPSSectionSummaryCompositionFilter[section];
155
+ }
149
156
  };
150
157
 
151
158
  // src/narratives/templates/typescript/TemplateUtilities.ts
@@ -595,6 +602,137 @@ var TemplateUtilities = class {
595
602
  }
596
603
  return status;
597
604
  }
605
+ extractObservationSummaryValue(data, timezone) {
606
+ if (data["valueQuantity.value"] !== void 0) {
607
+ const value = data["valueQuantity.value"];
608
+ const unit = data["valueQuantity.unit"];
609
+ return unit ? `${value} ${unit}` : `${value}`;
610
+ }
611
+ if (data["valueCodeableConcept.text"] !== void 0) {
612
+ return data["valueCodeableConcept.text"];
613
+ }
614
+ if (data["valueCodeableConcept.coding.display"] !== void 0) {
615
+ return data["valueCodeableConcept.coding.display"];
616
+ }
617
+ if (data["valueString"] !== void 0) {
618
+ return data["valueString"];
619
+ }
620
+ if (data["valueBoolean"] !== void 0) {
621
+ return String(data["valueBoolean"]);
622
+ }
623
+ if (data["valueInteger"] !== void 0) {
624
+ return String(data["valueInteger"]);
625
+ }
626
+ if (data["valueDateTime"] !== void 0) {
627
+ return this.renderTime(data["valueDateTime"], timezone);
628
+ }
629
+ if (data["valuePeriod.start"] !== void 0 || data["valuePeriod.end"] !== void 0) {
630
+ const start = this.renderTime(data["valuePeriod.start"], timezone);
631
+ const end = this.renderTime(data["valuePeriod.end"], timezone);
632
+ if (start && end) {
633
+ return `${start} - ${end}`;
634
+ } else if (start) {
635
+ return `${start}`;
636
+ } else if (end) {
637
+ return `${end}`;
638
+ }
639
+ }
640
+ if (data["valueTime"] !== void 0) {
641
+ return this.renderTime(data["valueTime"], timezone);
642
+ }
643
+ if (data["valueSampledData.origin.value"] !== void 0 || data["valueSampledData.origin.unit"] !== void 0) {
644
+ const originValue = data["valueSampledData.origin.value"];
645
+ const originUnit = data["valueSampledData.origin.unit"];
646
+ let result = "";
647
+ if (originValue !== void 0 && originUnit !== void 0) {
648
+ result = `${originValue} ${originUnit}`;
649
+ } else if (originValue !== void 0) {
650
+ result = `${originValue}`;
651
+ } else if (originUnit !== void 0) {
652
+ result = `${originUnit}`;
653
+ }
654
+ const period = data["valueSampledData.period"];
655
+ const factor = data["valueSampledData.factor"];
656
+ const lowerLimit = data["valueSampledData.lowerLimit"];
657
+ const upperLimit = data["valueSampledData.upperLimit"];
658
+ const sampledData = data["valueSampledData.data"];
659
+ const extras = [];
660
+ if (period !== void 0) extras.push(`period: ${period}`);
661
+ if (factor !== void 0) extras.push(`factor: ${factor}`);
662
+ if (lowerLimit !== void 0) extras.push(`lowerLimit: ${lowerLimit}`);
663
+ if (upperLimit !== void 0) extras.push(`upperLimit: ${upperLimit}`);
664
+ if (sampledData !== void 0) extras.push(`data: ${sampledData}`);
665
+ if (extras.length > 0) {
666
+ result += ` (${extras.join(", ")})`;
667
+ }
668
+ return result;
669
+ }
670
+ if (data["valueRange.low.value"] !== void 0 || data["valueRange.high.value"] !== void 0) {
671
+ let referenceRange = "";
672
+ if (data["valueRange.low.value"] !== void 0) {
673
+ referenceRange += `${data["valueRange.low.value"]}`;
674
+ if (data["valueRange.low.unit"] !== void 0) {
675
+ referenceRange += ` ${data["valueRange.low.unit"]}`;
676
+ }
677
+ referenceRange = referenceRange.trim();
678
+ if (data["valueRange.high.value"] !== void 0) {
679
+ referenceRange += " - ";
680
+ }
681
+ }
682
+ if (data["valueRange.high.value"] !== void 0) {
683
+ referenceRange += `${data["valueRange.high.value"]}`;
684
+ if (data["valueRange.high.unit"] !== void 0) {
685
+ referenceRange += ` ${data["valueRange.high.unit"]}`;
686
+ }
687
+ }
688
+ return referenceRange.trim();
689
+ }
690
+ if (data["valueRatio.numerator.value"] !== void 0 || data["valueRatio.denominator.value"] !== void 0) {
691
+ let ratio = "";
692
+ if (data["valueRatio.numerator.value"] !== void 0) {
693
+ ratio += `${data["valueRatio.numerator.value"]}`;
694
+ if (data["valueRatio.numerator.unit"] !== void 0) {
695
+ ratio += ` ${data["valueRatio.numerator.unit"]}`;
696
+ }
697
+ }
698
+ if (data["valueRatio.denominator.value"] !== void 0) {
699
+ ratio += " / ";
700
+ ratio += `${data["valueRatio.denominator.value"]}`;
701
+ if (data["valueRatio.denominator.unit"] !== void 0) {
702
+ ratio += ` ${data["valueRatio.denominator.unit"]}`;
703
+ }
704
+ }
705
+ return ratio.trim();
706
+ }
707
+ return "";
708
+ }
709
+ extractObservationSummaryReferenceRange(data) {
710
+ let referenceRange = "";
711
+ if (data["referenceRange.low.value"]) {
712
+ referenceRange += `${data["referenceRange.low.value"]} ${data["referenceRange.low.unit"]}`;
713
+ referenceRange.trim();
714
+ if (data["referenceRange.high.value"]) {
715
+ referenceRange += " - ";
716
+ }
717
+ }
718
+ if (data["referenceRange.high.value"]) {
719
+ referenceRange += `${data["referenceRange.high.value"]} ${data["referenceRange.high.unit"]}`;
720
+ }
721
+ return referenceRange.trim();
722
+ }
723
+ extractObservationSummaryEffectiveTime(data, timezone) {
724
+ if (data["effectiveDateTime"]) {
725
+ return this.renderTime(data["effectiveDateTime"], timezone);
726
+ }
727
+ let effectiveTimePeriod = "";
728
+ if (data["effectivePeriod.start"]) {
729
+ effectiveTimePeriod += this.renderTime(data["effectivePeriod.start"], timezone);
730
+ }
731
+ if (data["effectivePeriod.end"]) {
732
+ effectiveTimePeriod += ` - ${this.renderTime(data["effectivePeriod.end"], timezone)}`;
733
+ }
734
+ return effectiveTimePeriod.trim();
735
+ }
598
736
  formatQuantityValue(quantity) {
599
737
  if (!quantity) return "";
600
738
  const parts = [];
@@ -1094,6 +1232,57 @@ var AllergyIntoleranceTemplate = class _AllergyIntoleranceTemplate {
1094
1232
  generateNarrative(resources, timezone) {
1095
1233
  return _AllergyIntoleranceTemplate.generateStaticNarrative(resources, timezone);
1096
1234
  }
1235
+ /**
1236
+ * Generate HTML narrative for AllergyIntolerance resources using summary
1237
+ * @param resources - FHIR Composition resources
1238
+ * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
1239
+ * @returns HTML string for rendering
1240
+ */
1241
+ generateSummaryNarrative(resources, timezone) {
1242
+ const templateUtilities = new TemplateUtilities(resources);
1243
+ let html = `
1244
+ <div>
1245
+ <table>
1246
+ <thead>
1247
+ <tr>
1248
+ <th>Allergen</th>
1249
+ <th>Criticality</th>
1250
+ <th>Recorded Date</th>
1251
+ </tr>
1252
+ </thead>
1253
+ <tbody>`;
1254
+ for (const resourceItem of resources) {
1255
+ for (const rowData of resourceItem.section ?? []) {
1256
+ const data = {};
1257
+ for (const columnData of rowData.section ?? []) {
1258
+ switch (columnData.title) {
1259
+ case "Allergen Name":
1260
+ data["allergen"] = columnData.text?.div ?? "";
1261
+ break;
1262
+ case "Criticality":
1263
+ data["criticality"] = columnData.text?.div ?? "";
1264
+ break;
1265
+ case "Recorded Date":
1266
+ data["recordedDate"] = columnData.text?.div ?? "";
1267
+ break;
1268
+ default:
1269
+ break;
1270
+ }
1271
+ }
1272
+ html += `
1273
+ <tr>
1274
+ <td>${data["allergen"] ?? "-"}</td>
1275
+ <td>${data["criticality"] ?? "-"}</td>
1276
+ <td>${templateUtilities.renderTime(data["recordedDate"], timezone) ?? "-"}</td>
1277
+ </tr>`;
1278
+ }
1279
+ }
1280
+ html += `
1281
+ </tbody>
1282
+ </table>
1283
+ </div>`;
1284
+ return html;
1285
+ }
1097
1286
  /**
1098
1287
  * Internal static implementation that actually generates the narrative
1099
1288
  * @param resources - FHIR resources array containing AllergyIntolerance resources
@@ -1586,6 +1775,13 @@ var ProblemListTemplate = class _ProblemListTemplate {
1586
1775
  }
1587
1776
  };
1588
1777
 
1778
+ // src/structures/ips_section_constants.ts
1779
+ var VITAL_SIGNS_SUMMARY_COMPONENT_MAP = {
1780
+ "Systolic Blood Pressure": "valueRatio.numerator.value",
1781
+ "Diastolic Blood Pressure": "valueRatio.denominator.value",
1782
+ "Default": "valueString"
1783
+ };
1784
+
1589
1785
  // src/narratives/templates/typescript/VitalSignsTemplate.ts
1590
1786
  var VitalSignsTemplate = class _VitalSignsTemplate {
1591
1787
  /**
@@ -1597,6 +1793,68 @@ var VitalSignsTemplate = class _VitalSignsTemplate {
1597
1793
  generateNarrative(resources, timezone) {
1598
1794
  return _VitalSignsTemplate.generateStaticNarrative(resources, timezone);
1599
1795
  }
1796
+ /**
1797
+ * Generate HTML narrative for vital signs using summary
1798
+ * @param resources - FHIR Composition resources
1799
+ * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
1800
+ * @returns HTML string for rendering
1801
+ */
1802
+ generateSummaryNarrative(resources, timezone) {
1803
+ const templateUtilities = new TemplateUtilities(resources);
1804
+ let html = `
1805
+ <div>
1806
+ <table>
1807
+ <thead>
1808
+ <tr>
1809
+ <th>Vital Name</th>
1810
+ <th>Result</th>
1811
+ <th>Reference Range</th>
1812
+ <th>Date</th>
1813
+ </tr>
1814
+ </thead>
1815
+ <tbody>`;
1816
+ for (const resourceItem of resources) {
1817
+ for (const rowData of resourceItem.section ?? []) {
1818
+ const data = {};
1819
+ for (const columnData of rowData.section ?? []) {
1820
+ const columnTitle = columnData.title;
1821
+ if (columnTitle) {
1822
+ if (Object.keys(VITAL_SIGNS_SUMMARY_COMPONENT_MAP).includes(
1823
+ columnTitle
1824
+ )) {
1825
+ const vitalData = {};
1826
+ for (const component of columnData.section?.[0]?.section ?? []) {
1827
+ if (component.title) {
1828
+ vitalData[component.title] = component.text?.div ?? "";
1829
+ }
1830
+ }
1831
+ const vitalValue = templateUtilities.extractObservationSummaryValue(
1832
+ vitalData,
1833
+ timezone
1834
+ );
1835
+ if (vitalValue) {
1836
+ const dataKey = VITAL_SIGNS_SUMMARY_COMPONENT_MAP[columnTitle] ?? VITAL_SIGNS_SUMMARY_COMPONENT_MAP["Default"];
1837
+ data[dataKey] = vitalValue;
1838
+ }
1839
+ }
1840
+ data[columnTitle] = columnData.text?.div ?? "";
1841
+ }
1842
+ }
1843
+ html += `
1844
+ <tr>
1845
+ <td>${data["Vital Name"] ?? "-"}</td>
1846
+ <td>${templateUtilities.extractObservationSummaryValue(data, timezone) ?? "-"}</td>
1847
+ <td>${templateUtilities.extractObservationSummaryReferenceRange(data) ?? "-"}</td>
1848
+ <td>${templateUtilities.extractObservationSummaryEffectiveTime(data, timezone) ?? "-"}</td>
1849
+ </tr>`;
1850
+ }
1851
+ }
1852
+ html += `
1853
+ </tbody>
1854
+ </table>
1855
+ </div>`;
1856
+ return html;
1857
+ }
1600
1858
  /**
1601
1859
  * Internal static implementation that actually generates the narrative
1602
1860
  * @param resources - FHIR Observation resources
@@ -1615,7 +1873,7 @@ var VitalSignsTemplate = class _VitalSignsTemplate {
1615
1873
  <table>
1616
1874
  <thead>
1617
1875
  <tr>
1618
- <th>Code</th>
1876
+ <th>Vital Name</th>
1619
1877
  <th>Result</th>
1620
1878
  <th>Unit</th>
1621
1879
  <th>Interpretation</th>
@@ -2260,14 +2518,18 @@ var TypeScriptTemplateMapper = class {
2260
2518
  * @param section - The IPS section
2261
2519
  * @param resources - FHIR resources
2262
2520
  * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
2521
+ * @param useSectionSummary - Whether to use the section summary for narrative generation
2263
2522
  * @returns HTML string for rendering
2264
2523
  */
2265
- static generateNarrative(section, resources, timezone) {
2524
+ static generateNarrative(section, resources, timezone, useSectionSummary = false) {
2266
2525
  const templateClass = this.sectionToTemplate[section];
2267
2526
  if (!templateClass) {
2268
2527
  throw new Error(`No template found for section: ${section}`);
2269
2528
  }
2270
- return templateClass.generateNarrative(resources, timezone);
2529
+ return useSectionSummary ? templateClass.generateSummaryNarrative(
2530
+ resources,
2531
+ timezone
2532
+ ) : templateClass.generateNarrative(resources, timezone);
2271
2533
  }
2272
2534
  };
2273
2535
  // Map of section types to their template classes
@@ -2323,14 +2585,15 @@ var NarrativeGenerator = class {
2323
2585
  * @param section - IPS section type
2324
2586
  * @param resources - Array of domain resources
2325
2587
  * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
2588
+ * @param useSectionSummary - Whether to use section summary for narrative generation (default: false)
2326
2589
  * @returns Generated HTML content or undefined if no resources
2327
2590
  */
2328
- static async generateNarrativeContentAsync(section, resources, timezone) {
2591
+ static async generateNarrativeContentAsync(section, resources, timezone, useSectionSummary = false) {
2329
2592
  if (!resources || resources.length === 0) {
2330
2593
  return void 0;
2331
2594
  }
2332
2595
  try {
2333
- const content = TypeScriptTemplateMapper.generateNarrative(section, resources, timezone);
2596
+ const content = TypeScriptTemplateMapper.generateNarrative(section, resources, timezone, useSectionSummary);
2334
2597
  if (!content) {
2335
2598
  return void 0;
2336
2599
  }
@@ -2381,10 +2644,11 @@ var NarrativeGenerator = class {
2381
2644
  * @param resources - Array of domain resources
2382
2645
  * @param timezone - Optional timezone to use for date formatting
2383
2646
  * @param minify - Whether to minify the HTML content (default: true)
2647
+ * @param useSectionSummary - Whether to use section summary for narrative generation (default: false)
2384
2648
  * @returns Promise that resolves to a FHIR Narrative object or undefined if no resources
2385
2649
  */
2386
- static async generateNarrativeAsync(section, resources, timezone, minify = true) {
2387
- const content = await this.generateNarrativeContentAsync(section, resources, timezone);
2650
+ static async generateNarrativeAsync(section, resources, timezone, minify = true, useSectionSummary = false) {
2651
+ const content = await this.generateNarrativeContentAsync(section, resources, timezone, useSectionSummary);
2388
2652
  if (!content) {
2389
2653
  return void 0;
2390
2654
  }
@@ -2429,11 +2693,37 @@ var ComprehensiveIPSCompositionBuilder = class {
2429
2693
  }
2430
2694
  /**
2431
2695
  * Adds a section to the composition with async HTML minification
2696
+ * @param narrative - Narrative content for the section
2697
+ * @param sectionType - IPS section type
2698
+ * @param validResources - Array of domain resources
2699
+ */
2700
+ addSectionAsync(narrative, sectionType, validResources) {
2701
+ const sectionEntry = {
2702
+ title: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType,
2703
+ code: {
2704
+ coding: [{
2705
+ system: "http://loinc.org",
2706
+ code: IPS_SECTION_LOINC_CODES[sectionType],
2707
+ display: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType
2708
+ }],
2709
+ text: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType
2710
+ },
2711
+ text: narrative,
2712
+ entry: validResources.map((resource) => ({
2713
+ reference: `${resource.resourceType}/${resource.id}`,
2714
+ display: resource.resourceType
2715
+ }))
2716
+ };
2717
+ this.sections.push(sectionEntry);
2718
+ return this;
2719
+ }
2720
+ /**
2721
+ * Make and adds a section to the composition with async HTML minification
2432
2722
  * @param sectionType - IPS section type
2433
2723
  * @param validResources - Array of domain resources
2434
2724
  * @param timezone - Optional timezone to use for date formatting
2435
2725
  */
2436
- async addSectionAsync(sectionType, validResources, timezone) {
2726
+ async makeSectionAsync(sectionType, validResources, timezone) {
2437
2727
  for (const resource of validResources) {
2438
2728
  this.resources.add(resource);
2439
2729
  }
@@ -2453,32 +2743,38 @@ var ComprehensiveIPSCompositionBuilder = class {
2453
2743
  } else {
2454
2744
  return this;
2455
2745
  }
2456
- const sectionEntry = {
2457
- title: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType,
2458
- code: {
2459
- coding: [{
2460
- system: "http://loinc.org",
2461
- code: IPS_SECTION_LOINC_CODES[sectionType],
2462
- display: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType
2463
- }],
2464
- text: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType
2465
- },
2466
- text: narrative,
2467
- entry: validResources.map((resource) => ({
2468
- reference: `${resource.resourceType}/${resource.id}`,
2469
- display: resource.resourceType
2470
- }))
2471
- };
2472
- this.sections.push(sectionEntry);
2746
+ this.addSectionAsync(narrative, sectionType, validResources);
2473
2747
  }
2474
2748
  return this;
2475
2749
  }
2750
+ async makeSectionFromSummaryAsync(sectionType, summaryCompositions, resources, timezone) {
2751
+ const sectionResources = [];
2752
+ for (const summaryComposition of summaryCompositions) {
2753
+ const resourceEntries = summaryComposition?.section?.flatMap((sec) => sec.entry || []) ?? [];
2754
+ resources.forEach((resource) => {
2755
+ if (resourceEntries?.some((entry) => entry.reference === `${resource.resourceType}/${resource.id}`)) {
2756
+ this.resources.add(resource);
2757
+ sectionResources.push(resource);
2758
+ }
2759
+ });
2760
+ }
2761
+ const narrative = await NarrativeGenerator.generateNarrativeAsync(
2762
+ sectionType,
2763
+ summaryCompositions,
2764
+ timezone,
2765
+ true,
2766
+ true
2767
+ );
2768
+ this.addSectionAsync(narrative, sectionType, sectionResources);
2769
+ return this;
2770
+ }
2476
2771
  /**
2477
2772
  * Reads a FHIR Bundle and extracts resources for each section defined in IPSSections.
2478
2773
  * @param bundle - FHIR Bundle containing resources
2479
2774
  * @param timezone - Optional timezone to use for date formatting
2775
+ * @param useSummaryCompositions - Whether to use summary compositions (default: false)
2480
2776
  */
2481
- async readBundleAsync(bundle, timezone) {
2777
+ async readBundleAsync(bundle, timezone, useSummaryCompositions = false) {
2482
2778
  if (!bundle.entry) {
2483
2779
  return this;
2484
2780
  }
@@ -2500,9 +2796,15 @@ var ComprehensiveIPSCompositionBuilder = class {
2500
2796
  if (sectionType === "Patient" /* PATIENT */) {
2501
2797
  continue;
2502
2798
  }
2503
- const sectionFilter = IPSSectionResourceHelper.getResourceFilterForSection(sectionType);
2504
- const sectionResources = resources.filter((resource) => sectionFilter(resource));
2505
- await this.addSectionAsync(sectionType, sectionResources, timezone);
2799
+ const summaryCompositionFilter = useSummaryCompositions ? IPSSectionResourceHelper.getSummaryCompositionFilterForSection(sectionType) : void 0;
2800
+ const sectionSummary = summaryCompositionFilter ? resources.filter((resource) => summaryCompositionFilter(resource)) : [];
2801
+ if (sectionSummary.length > 0) {
2802
+ await this.makeSectionFromSummaryAsync(sectionType, sectionSummary, resources, timezone);
2803
+ } else {
2804
+ const sectionFilter = IPSSectionResourceHelper.getResourceFilterForSection(sectionType);
2805
+ const sectionResources = resources.filter((resource) => sectionFilter(resource));
2806
+ await this.makeSectionAsync(sectionType, sectionResources, timezone);
2807
+ }
2506
2808
  }
2507
2809
  return this;
2508
2810
  }
package/dist/index.d.cts CHANGED
@@ -630,6 +630,60 @@ type TBundle = {
630
630
  signature?: TSignature;
631
631
  };
632
632
 
633
+ type TCompositionAttester = {
634
+ id?: string;
635
+ extension?: TExtension[];
636
+ modifierExtension?: TExtension[];
637
+ mode: string;
638
+ time?: TDateTime;
639
+ party?: TReference;
640
+ };
641
+
642
+ type TCompositionRelatesTo = {
643
+ id?: string;
644
+ extension?: TExtension[];
645
+ modifierExtension?: TExtension[];
646
+ code: string;
647
+ targetIdentifier?: TIdentifier;
648
+ targetReference?: TReference;
649
+ };
650
+
651
+ type TCompositionEvent = {
652
+ id?: string;
653
+ extension?: TExtension[];
654
+ modifierExtension?: TExtension[];
655
+ code?: TCodeableConcept[];
656
+ period?: TPeriod;
657
+ detail?: TReference[];
658
+ };
659
+
660
+ type TComposition = {
661
+ resourceType?: string;
662
+ id?: string;
663
+ meta?: TMeta;
664
+ implicitRules?: TUri;
665
+ language?: string;
666
+ text?: TNarrative;
667
+ contained?: TResourceContainer[];
668
+ extension?: TExtension[];
669
+ modifierExtension?: TExtension[];
670
+ identifier?: TIdentifier;
671
+ status: string;
672
+ type: TCodeableConcept;
673
+ category?: TCodeableConcept[];
674
+ subject?: TReference;
675
+ encounter?: TReference;
676
+ date: TDateTime;
677
+ author: TReference[];
678
+ title: string;
679
+ confidentiality?: string;
680
+ attester?: TCompositionAttester[];
681
+ custodian?: TReference;
682
+ relatesTo?: TCompositionRelatesTo[];
683
+ event?: TCompositionEvent[];
684
+ section?: TCompositionSection[];
685
+ };
686
+
633
687
  declare class ComprehensiveIPSCompositionBuilder {
634
688
  private patients;
635
689
  private sections;
@@ -642,17 +696,26 @@ declare class ComprehensiveIPSCompositionBuilder {
642
696
  setPatient(patients: TPatient | TPatient[]): this;
643
697
  /**
644
698
  * Adds a section to the composition with async HTML minification
699
+ * @param narrative - Narrative content for the section
700
+ * @param sectionType - IPS section type
701
+ * @param validResources - Array of domain resources
702
+ */
703
+ addSectionAsync<T extends TDomainResource>(narrative: TNarrative, sectionType: IPSSections, validResources: T[]): this;
704
+ /**
705
+ * Make and adds a section to the composition with async HTML minification
645
706
  * @param sectionType - IPS section type
646
707
  * @param validResources - Array of domain resources
647
708
  * @param timezone - Optional timezone to use for date formatting
648
709
  */
649
- addSectionAsync<T extends TDomainResource>(sectionType: IPSSections, validResources: T[], timezone: string | undefined): Promise<this>;
710
+ makeSectionAsync<T extends TDomainResource>(sectionType: IPSSections, validResources: T[], timezone: string | undefined): Promise<this>;
711
+ makeSectionFromSummaryAsync(sectionType: IPSSections, summaryCompositions: TComposition[], resources: TDomainResource[], timezone: string | undefined): Promise<this>;
650
712
  /**
651
713
  * Reads a FHIR Bundle and extracts resources for each section defined in IPSSections.
652
714
  * @param bundle - FHIR Bundle containing resources
653
715
  * @param timezone - Optional timezone to use for date formatting
716
+ * @param useSummaryCompositions - Whether to use summary compositions (default: false)
654
717
  */
655
- readBundleAsync(bundle: TBundle, timezone: string | undefined): Promise<this>;
718
+ readBundleAsync(bundle: TBundle, timezone: string | undefined, useSummaryCompositions?: boolean): Promise<this>;
656
719
  /**
657
720
  * Builds a complete FHIR Bundle containing the Composition and all resources.
658
721
  * @param authorOrganizationId - ID of the authoring organization (e.g., hospital or clinic)
@@ -683,9 +746,10 @@ declare class NarrativeGenerator {
683
746
  * @param section - IPS section type
684
747
  * @param resources - Array of domain resources
685
748
  * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
749
+ * @param useSectionSummary - Whether to use section summary for narrative generation (default: false)
686
750
  * @returns Generated HTML content or undefined if no resources
687
751
  */
688
- static generateNarrativeContentAsync<T extends TDomainResource>(section: IPSSections, resources: T[], timezone: string | undefined): Promise<string | undefined>;
752
+ static generateNarrativeContentAsync<T extends TDomainResource>(section: IPSSections, resources: T[], timezone: string | undefined, useSectionSummary?: boolean): Promise<string | undefined>;
689
753
  /**
690
754
  * Minifies HTML content asynchronously using html-minifier-terser
691
755
  * @param html - HTML content to minify
@@ -706,9 +770,10 @@ declare class NarrativeGenerator {
706
770
  * @param resources - Array of domain resources
707
771
  * @param timezone - Optional timezone to use for date formatting
708
772
  * @param minify - Whether to minify the HTML content (default: true)
773
+ * @param useSectionSummary - Whether to use section summary for narrative generation (default: false)
709
774
  * @returns Promise that resolves to a FHIR Narrative object or undefined if no resources
710
775
  */
711
- static generateNarrativeAsync<T extends TDomainResource>(section: IPSSections, resources: T[], timezone: string | undefined, minify?: boolean): Promise<Narrative | undefined>;
776
+ static generateNarrativeAsync<T extends TDomainResource>(section: IPSSections, resources: T[], timezone: string | undefined, minify?: boolean, useSectionSummary?: boolean): Promise<Narrative | undefined>;
712
777
  }
713
778
 
714
779
  declare const myPackage: (taco?: string) => string;
package/dist/index.d.ts CHANGED
@@ -630,6 +630,60 @@ type TBundle = {
630
630
  signature?: TSignature;
631
631
  };
632
632
 
633
+ type TCompositionAttester = {
634
+ id?: string;
635
+ extension?: TExtension[];
636
+ modifierExtension?: TExtension[];
637
+ mode: string;
638
+ time?: TDateTime;
639
+ party?: TReference;
640
+ };
641
+
642
+ type TCompositionRelatesTo = {
643
+ id?: string;
644
+ extension?: TExtension[];
645
+ modifierExtension?: TExtension[];
646
+ code: string;
647
+ targetIdentifier?: TIdentifier;
648
+ targetReference?: TReference;
649
+ };
650
+
651
+ type TCompositionEvent = {
652
+ id?: string;
653
+ extension?: TExtension[];
654
+ modifierExtension?: TExtension[];
655
+ code?: TCodeableConcept[];
656
+ period?: TPeriod;
657
+ detail?: TReference[];
658
+ };
659
+
660
+ type TComposition = {
661
+ resourceType?: string;
662
+ id?: string;
663
+ meta?: TMeta;
664
+ implicitRules?: TUri;
665
+ language?: string;
666
+ text?: TNarrative;
667
+ contained?: TResourceContainer[];
668
+ extension?: TExtension[];
669
+ modifierExtension?: TExtension[];
670
+ identifier?: TIdentifier;
671
+ status: string;
672
+ type: TCodeableConcept;
673
+ category?: TCodeableConcept[];
674
+ subject?: TReference;
675
+ encounter?: TReference;
676
+ date: TDateTime;
677
+ author: TReference[];
678
+ title: string;
679
+ confidentiality?: string;
680
+ attester?: TCompositionAttester[];
681
+ custodian?: TReference;
682
+ relatesTo?: TCompositionRelatesTo[];
683
+ event?: TCompositionEvent[];
684
+ section?: TCompositionSection[];
685
+ };
686
+
633
687
  declare class ComprehensiveIPSCompositionBuilder {
634
688
  private patients;
635
689
  private sections;
@@ -642,17 +696,26 @@ declare class ComprehensiveIPSCompositionBuilder {
642
696
  setPatient(patients: TPatient | TPatient[]): this;
643
697
  /**
644
698
  * Adds a section to the composition with async HTML minification
699
+ * @param narrative - Narrative content for the section
700
+ * @param sectionType - IPS section type
701
+ * @param validResources - Array of domain resources
702
+ */
703
+ addSectionAsync<T extends TDomainResource>(narrative: TNarrative, sectionType: IPSSections, validResources: T[]): this;
704
+ /**
705
+ * Make and adds a section to the composition with async HTML minification
645
706
  * @param sectionType - IPS section type
646
707
  * @param validResources - Array of domain resources
647
708
  * @param timezone - Optional timezone to use for date formatting
648
709
  */
649
- addSectionAsync<T extends TDomainResource>(sectionType: IPSSections, validResources: T[], timezone: string | undefined): Promise<this>;
710
+ makeSectionAsync<T extends TDomainResource>(sectionType: IPSSections, validResources: T[], timezone: string | undefined): Promise<this>;
711
+ makeSectionFromSummaryAsync(sectionType: IPSSections, summaryCompositions: TComposition[], resources: TDomainResource[], timezone: string | undefined): Promise<this>;
650
712
  /**
651
713
  * Reads a FHIR Bundle and extracts resources for each section defined in IPSSections.
652
714
  * @param bundle - FHIR Bundle containing resources
653
715
  * @param timezone - Optional timezone to use for date formatting
716
+ * @param useSummaryCompositions - Whether to use summary compositions (default: false)
654
717
  */
655
- readBundleAsync(bundle: TBundle, timezone: string | undefined): Promise<this>;
718
+ readBundleAsync(bundle: TBundle, timezone: string | undefined, useSummaryCompositions?: boolean): Promise<this>;
656
719
  /**
657
720
  * Builds a complete FHIR Bundle containing the Composition and all resources.
658
721
  * @param authorOrganizationId - ID of the authoring organization (e.g., hospital or clinic)
@@ -683,9 +746,10 @@ declare class NarrativeGenerator {
683
746
  * @param section - IPS section type
684
747
  * @param resources - Array of domain resources
685
748
  * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
749
+ * @param useSectionSummary - Whether to use section summary for narrative generation (default: false)
686
750
  * @returns Generated HTML content or undefined if no resources
687
751
  */
688
- static generateNarrativeContentAsync<T extends TDomainResource>(section: IPSSections, resources: T[], timezone: string | undefined): Promise<string | undefined>;
752
+ static generateNarrativeContentAsync<T extends TDomainResource>(section: IPSSections, resources: T[], timezone: string | undefined, useSectionSummary?: boolean): Promise<string | undefined>;
689
753
  /**
690
754
  * Minifies HTML content asynchronously using html-minifier-terser
691
755
  * @param html - HTML content to minify
@@ -706,9 +770,10 @@ declare class NarrativeGenerator {
706
770
  * @param resources - Array of domain resources
707
771
  * @param timezone - Optional timezone to use for date formatting
708
772
  * @param minify - Whether to minify the HTML content (default: true)
773
+ * @param useSectionSummary - Whether to use section summary for narrative generation (default: false)
709
774
  * @returns Promise that resolves to a FHIR Narrative object or undefined if no resources
710
775
  */
711
- static generateNarrativeAsync<T extends TDomainResource>(section: IPSSections, resources: T[], timezone: string | undefined, minify?: boolean): Promise<Narrative | undefined>;
776
+ static generateNarrativeAsync<T extends TDomainResource>(section: IPSSections, resources: T[], timezone: string | undefined, minify?: boolean, useSectionSummary?: boolean): Promise<Narrative | undefined>;
712
777
  }
713
778
 
714
779
  declare const myPackage: (taco?: string) => string;
package/dist/index.js CHANGED
@@ -114,10 +114,17 @@ var IPSSectionResourceFilters = {
114
114
  // Only include active advance directives (Consent resources)
115
115
  ["AdvanceDirectivesSection" /* ADVANCE_DIRECTIVES */]: (resource) => resource.resourceType === "Consent" && resource.status === "active"
116
116
  };
117
+ var IPSSectionSummaryCompositionFilter = {
118
+ ["AllergyIntoleranceSection" /* ALLERGIES */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === "https://fhir.icanbwell.com/4_0_0/CodeSystem/composition/" && c.code === "allergy_summary_document"),
119
+ ["VitalSignsSection" /* VITAL_SIGNS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === "https://fhir.icanbwell.com/4_0_0/CodeSystem/composition/" && c.code === "vital_summary_document")
120
+ };
117
121
  var IPSSectionResourceHelper = class {
118
122
  static getResourceFilterForSection(section) {
119
123
  return IPSSectionResourceFilters[section];
120
124
  }
125
+ static getSummaryCompositionFilterForSection(section) {
126
+ return IPSSectionSummaryCompositionFilter[section];
127
+ }
121
128
  };
122
129
 
123
130
  // src/narratives/templates/typescript/TemplateUtilities.ts
@@ -567,6 +574,137 @@ var TemplateUtilities = class {
567
574
  }
568
575
  return status;
569
576
  }
577
+ extractObservationSummaryValue(data, timezone) {
578
+ if (data["valueQuantity.value"] !== void 0) {
579
+ const value = data["valueQuantity.value"];
580
+ const unit = data["valueQuantity.unit"];
581
+ return unit ? `${value} ${unit}` : `${value}`;
582
+ }
583
+ if (data["valueCodeableConcept.text"] !== void 0) {
584
+ return data["valueCodeableConcept.text"];
585
+ }
586
+ if (data["valueCodeableConcept.coding.display"] !== void 0) {
587
+ return data["valueCodeableConcept.coding.display"];
588
+ }
589
+ if (data["valueString"] !== void 0) {
590
+ return data["valueString"];
591
+ }
592
+ if (data["valueBoolean"] !== void 0) {
593
+ return String(data["valueBoolean"]);
594
+ }
595
+ if (data["valueInteger"] !== void 0) {
596
+ return String(data["valueInteger"]);
597
+ }
598
+ if (data["valueDateTime"] !== void 0) {
599
+ return this.renderTime(data["valueDateTime"], timezone);
600
+ }
601
+ if (data["valuePeriod.start"] !== void 0 || data["valuePeriod.end"] !== void 0) {
602
+ const start = this.renderTime(data["valuePeriod.start"], timezone);
603
+ const end = this.renderTime(data["valuePeriod.end"], timezone);
604
+ if (start && end) {
605
+ return `${start} - ${end}`;
606
+ } else if (start) {
607
+ return `${start}`;
608
+ } else if (end) {
609
+ return `${end}`;
610
+ }
611
+ }
612
+ if (data["valueTime"] !== void 0) {
613
+ return this.renderTime(data["valueTime"], timezone);
614
+ }
615
+ if (data["valueSampledData.origin.value"] !== void 0 || data["valueSampledData.origin.unit"] !== void 0) {
616
+ const originValue = data["valueSampledData.origin.value"];
617
+ const originUnit = data["valueSampledData.origin.unit"];
618
+ let result = "";
619
+ if (originValue !== void 0 && originUnit !== void 0) {
620
+ result = `${originValue} ${originUnit}`;
621
+ } else if (originValue !== void 0) {
622
+ result = `${originValue}`;
623
+ } else if (originUnit !== void 0) {
624
+ result = `${originUnit}`;
625
+ }
626
+ const period = data["valueSampledData.period"];
627
+ const factor = data["valueSampledData.factor"];
628
+ const lowerLimit = data["valueSampledData.lowerLimit"];
629
+ const upperLimit = data["valueSampledData.upperLimit"];
630
+ const sampledData = data["valueSampledData.data"];
631
+ const extras = [];
632
+ if (period !== void 0) extras.push(`period: ${period}`);
633
+ if (factor !== void 0) extras.push(`factor: ${factor}`);
634
+ if (lowerLimit !== void 0) extras.push(`lowerLimit: ${lowerLimit}`);
635
+ if (upperLimit !== void 0) extras.push(`upperLimit: ${upperLimit}`);
636
+ if (sampledData !== void 0) extras.push(`data: ${sampledData}`);
637
+ if (extras.length > 0) {
638
+ result += ` (${extras.join(", ")})`;
639
+ }
640
+ return result;
641
+ }
642
+ if (data["valueRange.low.value"] !== void 0 || data["valueRange.high.value"] !== void 0) {
643
+ let referenceRange = "";
644
+ if (data["valueRange.low.value"] !== void 0) {
645
+ referenceRange += `${data["valueRange.low.value"]}`;
646
+ if (data["valueRange.low.unit"] !== void 0) {
647
+ referenceRange += ` ${data["valueRange.low.unit"]}`;
648
+ }
649
+ referenceRange = referenceRange.trim();
650
+ if (data["valueRange.high.value"] !== void 0) {
651
+ referenceRange += " - ";
652
+ }
653
+ }
654
+ if (data["valueRange.high.value"] !== void 0) {
655
+ referenceRange += `${data["valueRange.high.value"]}`;
656
+ if (data["valueRange.high.unit"] !== void 0) {
657
+ referenceRange += ` ${data["valueRange.high.unit"]}`;
658
+ }
659
+ }
660
+ return referenceRange.trim();
661
+ }
662
+ if (data["valueRatio.numerator.value"] !== void 0 || data["valueRatio.denominator.value"] !== void 0) {
663
+ let ratio = "";
664
+ if (data["valueRatio.numerator.value"] !== void 0) {
665
+ ratio += `${data["valueRatio.numerator.value"]}`;
666
+ if (data["valueRatio.numerator.unit"] !== void 0) {
667
+ ratio += ` ${data["valueRatio.numerator.unit"]}`;
668
+ }
669
+ }
670
+ if (data["valueRatio.denominator.value"] !== void 0) {
671
+ ratio += " / ";
672
+ ratio += `${data["valueRatio.denominator.value"]}`;
673
+ if (data["valueRatio.denominator.unit"] !== void 0) {
674
+ ratio += ` ${data["valueRatio.denominator.unit"]}`;
675
+ }
676
+ }
677
+ return ratio.trim();
678
+ }
679
+ return "";
680
+ }
681
+ extractObservationSummaryReferenceRange(data) {
682
+ let referenceRange = "";
683
+ if (data["referenceRange.low.value"]) {
684
+ referenceRange += `${data["referenceRange.low.value"]} ${data["referenceRange.low.unit"]}`;
685
+ referenceRange.trim();
686
+ if (data["referenceRange.high.value"]) {
687
+ referenceRange += " - ";
688
+ }
689
+ }
690
+ if (data["referenceRange.high.value"]) {
691
+ referenceRange += `${data["referenceRange.high.value"]} ${data["referenceRange.high.unit"]}`;
692
+ }
693
+ return referenceRange.trim();
694
+ }
695
+ extractObservationSummaryEffectiveTime(data, timezone) {
696
+ if (data["effectiveDateTime"]) {
697
+ return this.renderTime(data["effectiveDateTime"], timezone);
698
+ }
699
+ let effectiveTimePeriod = "";
700
+ if (data["effectivePeriod.start"]) {
701
+ effectiveTimePeriod += this.renderTime(data["effectivePeriod.start"], timezone);
702
+ }
703
+ if (data["effectivePeriod.end"]) {
704
+ effectiveTimePeriod += ` - ${this.renderTime(data["effectivePeriod.end"], timezone)}`;
705
+ }
706
+ return effectiveTimePeriod.trim();
707
+ }
570
708
  formatQuantityValue(quantity) {
571
709
  if (!quantity) return "";
572
710
  const parts = [];
@@ -1066,6 +1204,57 @@ var AllergyIntoleranceTemplate = class _AllergyIntoleranceTemplate {
1066
1204
  generateNarrative(resources, timezone) {
1067
1205
  return _AllergyIntoleranceTemplate.generateStaticNarrative(resources, timezone);
1068
1206
  }
1207
+ /**
1208
+ * Generate HTML narrative for AllergyIntolerance resources using summary
1209
+ * @param resources - FHIR Composition resources
1210
+ * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
1211
+ * @returns HTML string for rendering
1212
+ */
1213
+ generateSummaryNarrative(resources, timezone) {
1214
+ const templateUtilities = new TemplateUtilities(resources);
1215
+ let html = `
1216
+ <div>
1217
+ <table>
1218
+ <thead>
1219
+ <tr>
1220
+ <th>Allergen</th>
1221
+ <th>Criticality</th>
1222
+ <th>Recorded Date</th>
1223
+ </tr>
1224
+ </thead>
1225
+ <tbody>`;
1226
+ for (const resourceItem of resources) {
1227
+ for (const rowData of resourceItem.section ?? []) {
1228
+ const data = {};
1229
+ for (const columnData of rowData.section ?? []) {
1230
+ switch (columnData.title) {
1231
+ case "Allergen Name":
1232
+ data["allergen"] = columnData.text?.div ?? "";
1233
+ break;
1234
+ case "Criticality":
1235
+ data["criticality"] = columnData.text?.div ?? "";
1236
+ break;
1237
+ case "Recorded Date":
1238
+ data["recordedDate"] = columnData.text?.div ?? "";
1239
+ break;
1240
+ default:
1241
+ break;
1242
+ }
1243
+ }
1244
+ html += `
1245
+ <tr>
1246
+ <td>${data["allergen"] ?? "-"}</td>
1247
+ <td>${data["criticality"] ?? "-"}</td>
1248
+ <td>${templateUtilities.renderTime(data["recordedDate"], timezone) ?? "-"}</td>
1249
+ </tr>`;
1250
+ }
1251
+ }
1252
+ html += `
1253
+ </tbody>
1254
+ </table>
1255
+ </div>`;
1256
+ return html;
1257
+ }
1069
1258
  /**
1070
1259
  * Internal static implementation that actually generates the narrative
1071
1260
  * @param resources - FHIR resources array containing AllergyIntolerance resources
@@ -1558,6 +1747,13 @@ var ProblemListTemplate = class _ProblemListTemplate {
1558
1747
  }
1559
1748
  };
1560
1749
 
1750
+ // src/structures/ips_section_constants.ts
1751
+ var VITAL_SIGNS_SUMMARY_COMPONENT_MAP = {
1752
+ "Systolic Blood Pressure": "valueRatio.numerator.value",
1753
+ "Diastolic Blood Pressure": "valueRatio.denominator.value",
1754
+ "Default": "valueString"
1755
+ };
1756
+
1561
1757
  // src/narratives/templates/typescript/VitalSignsTemplate.ts
1562
1758
  var VitalSignsTemplate = class _VitalSignsTemplate {
1563
1759
  /**
@@ -1569,6 +1765,68 @@ var VitalSignsTemplate = class _VitalSignsTemplate {
1569
1765
  generateNarrative(resources, timezone) {
1570
1766
  return _VitalSignsTemplate.generateStaticNarrative(resources, timezone);
1571
1767
  }
1768
+ /**
1769
+ * Generate HTML narrative for vital signs using summary
1770
+ * @param resources - FHIR Composition resources
1771
+ * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
1772
+ * @returns HTML string for rendering
1773
+ */
1774
+ generateSummaryNarrative(resources, timezone) {
1775
+ const templateUtilities = new TemplateUtilities(resources);
1776
+ let html = `
1777
+ <div>
1778
+ <table>
1779
+ <thead>
1780
+ <tr>
1781
+ <th>Vital Name</th>
1782
+ <th>Result</th>
1783
+ <th>Reference Range</th>
1784
+ <th>Date</th>
1785
+ </tr>
1786
+ </thead>
1787
+ <tbody>`;
1788
+ for (const resourceItem of resources) {
1789
+ for (const rowData of resourceItem.section ?? []) {
1790
+ const data = {};
1791
+ for (const columnData of rowData.section ?? []) {
1792
+ const columnTitle = columnData.title;
1793
+ if (columnTitle) {
1794
+ if (Object.keys(VITAL_SIGNS_SUMMARY_COMPONENT_MAP).includes(
1795
+ columnTitle
1796
+ )) {
1797
+ const vitalData = {};
1798
+ for (const component of columnData.section?.[0]?.section ?? []) {
1799
+ if (component.title) {
1800
+ vitalData[component.title] = component.text?.div ?? "";
1801
+ }
1802
+ }
1803
+ const vitalValue = templateUtilities.extractObservationSummaryValue(
1804
+ vitalData,
1805
+ timezone
1806
+ );
1807
+ if (vitalValue) {
1808
+ const dataKey = VITAL_SIGNS_SUMMARY_COMPONENT_MAP[columnTitle] ?? VITAL_SIGNS_SUMMARY_COMPONENT_MAP["Default"];
1809
+ data[dataKey] = vitalValue;
1810
+ }
1811
+ }
1812
+ data[columnTitle] = columnData.text?.div ?? "";
1813
+ }
1814
+ }
1815
+ html += `
1816
+ <tr>
1817
+ <td>${data["Vital Name"] ?? "-"}</td>
1818
+ <td>${templateUtilities.extractObservationSummaryValue(data, timezone) ?? "-"}</td>
1819
+ <td>${templateUtilities.extractObservationSummaryReferenceRange(data) ?? "-"}</td>
1820
+ <td>${templateUtilities.extractObservationSummaryEffectiveTime(data, timezone) ?? "-"}</td>
1821
+ </tr>`;
1822
+ }
1823
+ }
1824
+ html += `
1825
+ </tbody>
1826
+ </table>
1827
+ </div>`;
1828
+ return html;
1829
+ }
1572
1830
  /**
1573
1831
  * Internal static implementation that actually generates the narrative
1574
1832
  * @param resources - FHIR Observation resources
@@ -1587,7 +1845,7 @@ var VitalSignsTemplate = class _VitalSignsTemplate {
1587
1845
  <table>
1588
1846
  <thead>
1589
1847
  <tr>
1590
- <th>Code</th>
1848
+ <th>Vital Name</th>
1591
1849
  <th>Result</th>
1592
1850
  <th>Unit</th>
1593
1851
  <th>Interpretation</th>
@@ -2232,14 +2490,18 @@ var TypeScriptTemplateMapper = class {
2232
2490
  * @param section - The IPS section
2233
2491
  * @param resources - FHIR resources
2234
2492
  * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
2493
+ * @param useSectionSummary - Whether to use the section summary for narrative generation
2235
2494
  * @returns HTML string for rendering
2236
2495
  */
2237
- static generateNarrative(section, resources, timezone) {
2496
+ static generateNarrative(section, resources, timezone, useSectionSummary = false) {
2238
2497
  const templateClass = this.sectionToTemplate[section];
2239
2498
  if (!templateClass) {
2240
2499
  throw new Error(`No template found for section: ${section}`);
2241
2500
  }
2242
- return templateClass.generateNarrative(resources, timezone);
2501
+ return useSectionSummary ? templateClass.generateSummaryNarrative(
2502
+ resources,
2503
+ timezone
2504
+ ) : templateClass.generateNarrative(resources, timezone);
2243
2505
  }
2244
2506
  };
2245
2507
  // Map of section types to their template classes
@@ -2295,14 +2557,15 @@ var NarrativeGenerator = class {
2295
2557
  * @param section - IPS section type
2296
2558
  * @param resources - Array of domain resources
2297
2559
  * @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
2560
+ * @param useSectionSummary - Whether to use section summary for narrative generation (default: false)
2298
2561
  * @returns Generated HTML content or undefined if no resources
2299
2562
  */
2300
- static async generateNarrativeContentAsync(section, resources, timezone) {
2563
+ static async generateNarrativeContentAsync(section, resources, timezone, useSectionSummary = false) {
2301
2564
  if (!resources || resources.length === 0) {
2302
2565
  return void 0;
2303
2566
  }
2304
2567
  try {
2305
- const content = TypeScriptTemplateMapper.generateNarrative(section, resources, timezone);
2568
+ const content = TypeScriptTemplateMapper.generateNarrative(section, resources, timezone, useSectionSummary);
2306
2569
  if (!content) {
2307
2570
  return void 0;
2308
2571
  }
@@ -2353,10 +2616,11 @@ var NarrativeGenerator = class {
2353
2616
  * @param resources - Array of domain resources
2354
2617
  * @param timezone - Optional timezone to use for date formatting
2355
2618
  * @param minify - Whether to minify the HTML content (default: true)
2619
+ * @param useSectionSummary - Whether to use section summary for narrative generation (default: false)
2356
2620
  * @returns Promise that resolves to a FHIR Narrative object or undefined if no resources
2357
2621
  */
2358
- static async generateNarrativeAsync(section, resources, timezone, minify = true) {
2359
- const content = await this.generateNarrativeContentAsync(section, resources, timezone);
2622
+ static async generateNarrativeAsync(section, resources, timezone, minify = true, useSectionSummary = false) {
2623
+ const content = await this.generateNarrativeContentAsync(section, resources, timezone, useSectionSummary);
2360
2624
  if (!content) {
2361
2625
  return void 0;
2362
2626
  }
@@ -2401,11 +2665,37 @@ var ComprehensiveIPSCompositionBuilder = class {
2401
2665
  }
2402
2666
  /**
2403
2667
  * Adds a section to the composition with async HTML minification
2668
+ * @param narrative - Narrative content for the section
2669
+ * @param sectionType - IPS section type
2670
+ * @param validResources - Array of domain resources
2671
+ */
2672
+ addSectionAsync(narrative, sectionType, validResources) {
2673
+ const sectionEntry = {
2674
+ title: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType,
2675
+ code: {
2676
+ coding: [{
2677
+ system: "http://loinc.org",
2678
+ code: IPS_SECTION_LOINC_CODES[sectionType],
2679
+ display: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType
2680
+ }],
2681
+ text: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType
2682
+ },
2683
+ text: narrative,
2684
+ entry: validResources.map((resource) => ({
2685
+ reference: `${resource.resourceType}/${resource.id}`,
2686
+ display: resource.resourceType
2687
+ }))
2688
+ };
2689
+ this.sections.push(sectionEntry);
2690
+ return this;
2691
+ }
2692
+ /**
2693
+ * Make and adds a section to the composition with async HTML minification
2404
2694
  * @param sectionType - IPS section type
2405
2695
  * @param validResources - Array of domain resources
2406
2696
  * @param timezone - Optional timezone to use for date formatting
2407
2697
  */
2408
- async addSectionAsync(sectionType, validResources, timezone) {
2698
+ async makeSectionAsync(sectionType, validResources, timezone) {
2409
2699
  for (const resource of validResources) {
2410
2700
  this.resources.add(resource);
2411
2701
  }
@@ -2425,32 +2715,38 @@ var ComprehensiveIPSCompositionBuilder = class {
2425
2715
  } else {
2426
2716
  return this;
2427
2717
  }
2428
- const sectionEntry = {
2429
- title: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType,
2430
- code: {
2431
- coding: [{
2432
- system: "http://loinc.org",
2433
- code: IPS_SECTION_LOINC_CODES[sectionType],
2434
- display: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType
2435
- }],
2436
- text: IPS_SECTION_DISPLAY_NAMES[sectionType] || sectionType
2437
- },
2438
- text: narrative,
2439
- entry: validResources.map((resource) => ({
2440
- reference: `${resource.resourceType}/${resource.id}`,
2441
- display: resource.resourceType
2442
- }))
2443
- };
2444
- this.sections.push(sectionEntry);
2718
+ this.addSectionAsync(narrative, sectionType, validResources);
2445
2719
  }
2446
2720
  return this;
2447
2721
  }
2722
+ async makeSectionFromSummaryAsync(sectionType, summaryCompositions, resources, timezone) {
2723
+ const sectionResources = [];
2724
+ for (const summaryComposition of summaryCompositions) {
2725
+ const resourceEntries = summaryComposition?.section?.flatMap((sec) => sec.entry || []) ?? [];
2726
+ resources.forEach((resource) => {
2727
+ if (resourceEntries?.some((entry) => entry.reference === `${resource.resourceType}/${resource.id}`)) {
2728
+ this.resources.add(resource);
2729
+ sectionResources.push(resource);
2730
+ }
2731
+ });
2732
+ }
2733
+ const narrative = await NarrativeGenerator.generateNarrativeAsync(
2734
+ sectionType,
2735
+ summaryCompositions,
2736
+ timezone,
2737
+ true,
2738
+ true
2739
+ );
2740
+ this.addSectionAsync(narrative, sectionType, sectionResources);
2741
+ return this;
2742
+ }
2448
2743
  /**
2449
2744
  * Reads a FHIR Bundle and extracts resources for each section defined in IPSSections.
2450
2745
  * @param bundle - FHIR Bundle containing resources
2451
2746
  * @param timezone - Optional timezone to use for date formatting
2747
+ * @param useSummaryCompositions - Whether to use summary compositions (default: false)
2452
2748
  */
2453
- async readBundleAsync(bundle, timezone) {
2749
+ async readBundleAsync(bundle, timezone, useSummaryCompositions = false) {
2454
2750
  if (!bundle.entry) {
2455
2751
  return this;
2456
2752
  }
@@ -2472,9 +2768,15 @@ var ComprehensiveIPSCompositionBuilder = class {
2472
2768
  if (sectionType === "Patient" /* PATIENT */) {
2473
2769
  continue;
2474
2770
  }
2475
- const sectionFilter = IPSSectionResourceHelper.getResourceFilterForSection(sectionType);
2476
- const sectionResources = resources.filter((resource) => sectionFilter(resource));
2477
- await this.addSectionAsync(sectionType, sectionResources, timezone);
2771
+ const summaryCompositionFilter = useSummaryCompositions ? IPSSectionResourceHelper.getSummaryCompositionFilterForSection(sectionType) : void 0;
2772
+ const sectionSummary = summaryCompositionFilter ? resources.filter((resource) => summaryCompositionFilter(resource)) : [];
2773
+ if (sectionSummary.length > 0) {
2774
+ await this.makeSectionFromSummaryAsync(sectionType, sectionSummary, resources, timezone);
2775
+ } else {
2776
+ const sectionFilter = IPSSectionResourceHelper.getResourceFilterForSection(sectionType);
2777
+ const sectionResources = resources.filter((resource) => sectionFilter(resource));
2778
+ await this.makeSectionAsync(sectionType, sectionResources, timezone);
2779
+ }
2478
2780
  }
2479
2781
  return this;
2480
2782
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imranq2/fhirpatientsummary",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "A template for creating npm packages using TypeScript and VSCode",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",