@imranq2/fhirpatientsummary 1.0.22 → 1.0.23
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/dist/index.cjs +180 -124
- package/dist/index.js +180 -124
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -109,6 +109,14 @@ var BLOOD_PRESSURE_LOINC_CODES = {
|
|
|
109
109
|
DIASTOLIC: "8462-4"
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
// src/structures/ips_section_constants.ts
|
|
113
|
+
var VITAL_SIGNS_SUMMARY_COMPONENT_MAP = {
|
|
114
|
+
"Systolic Blood Pressure": "valueRatio.numerator.value",
|
|
115
|
+
"Diastolic Blood Pressure": "valueRatio.denominator.value",
|
|
116
|
+
"Default": "valueString"
|
|
117
|
+
};
|
|
118
|
+
var IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM = "https://fhir.icanbwell.com/4_0_0/CodeSystem/composition/";
|
|
119
|
+
|
|
112
120
|
// src/structures/ips_section_resource_map.ts
|
|
113
121
|
var IPSSectionResourceFilters = {
|
|
114
122
|
// Patient section: only Patient resource
|
|
@@ -116,7 +124,7 @@ var IPSSectionResourceFilters = {
|
|
|
116
124
|
// Only include allergies
|
|
117
125
|
["AllergyIntoleranceSection" /* ALLERGIES */]: (resource) => resource.resourceType === "AllergyIntolerance",
|
|
118
126
|
// includes MedicationRequest, MedicationStatement. Medication is needed for medication names
|
|
119
|
-
["MedicationSummarySection" /* MEDICATIONS */]: (resource) => ["MedicationRequest", "MedicationStatement"
|
|
127
|
+
["MedicationSummarySection" /* MEDICATIONS */]: (resource) => ["MedicationRequest", "MedicationStatement"].includes(resource.resourceType) && resource.status === "active" || resource.resourceType === "Medication",
|
|
120
128
|
// Only include active conditions
|
|
121
129
|
["ProblemSection" /* PROBLEMS */]: (resource) => resource.resourceType === "Condition" && resource.clinicalStatus?.coding?.some((c) => !["inactive", "resolved"].includes(c.code)),
|
|
122
130
|
// Only include completed immunizations
|
|
@@ -143,10 +151,11 @@ var IPSSectionResourceFilters = {
|
|
|
143
151
|
["AdvanceDirectivesSection" /* ADVANCE_DIRECTIVES */]: (resource) => resource.resourceType === "Consent" && resource.status === "active"
|
|
144
152
|
};
|
|
145
153
|
var IPSSectionSummaryCompositionFilter = {
|
|
146
|
-
["AllergyIntoleranceSection" /* ALLERGIES */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system ===
|
|
147
|
-
["VitalSignsSection" /* VITAL_SIGNS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system ===
|
|
148
|
-
["PlanOfCareSection" /* CARE_PLAN */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system ===
|
|
149
|
-
["ImmunizationSection" /* IMMUNIZATIONS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system ===
|
|
154
|
+
["AllergyIntoleranceSection" /* ALLERGIES */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "allergy_summary_document"),
|
|
155
|
+
["VitalSignsSection" /* VITAL_SIGNS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "vital_summary_document"),
|
|
156
|
+
["PlanOfCareSection" /* CARE_PLAN */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "careplan_summary_document"),
|
|
157
|
+
["ImmunizationSection" /* IMMUNIZATIONS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "immunization_summary_document"),
|
|
158
|
+
["MedicationSummarySection" /* MEDICATIONS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "medication_summary_document")
|
|
150
159
|
};
|
|
151
160
|
var IPSSectionResourceHelper = class {
|
|
152
161
|
static getResourceFilterForSection(section) {
|
|
@@ -850,9 +859,11 @@ var TemplateUtilities = class {
|
|
|
850
859
|
if (dateValue instanceof Date) {
|
|
851
860
|
dateTime = import_luxon.DateTime.fromJSDate(dateValue);
|
|
852
861
|
} else if (typeof dateValue === "string") {
|
|
853
|
-
dateTime = import_luxon.DateTime.fromISO(dateValue);
|
|
854
862
|
if (!dateValue.includes("T")) {
|
|
855
863
|
dateOnly = true;
|
|
864
|
+
dateTime = import_luxon.DateTime.fromISO(dateValue, { zone: "utc" });
|
|
865
|
+
} else {
|
|
866
|
+
dateTime = import_luxon.DateTime.fromISO(dateValue);
|
|
856
867
|
}
|
|
857
868
|
} else {
|
|
858
869
|
dateTime = import_luxon.DateTime.fromISO(String(dateValue));
|
|
@@ -954,6 +965,9 @@ var TemplateUtilities = class {
|
|
|
954
965
|
}
|
|
955
966
|
};
|
|
956
967
|
|
|
968
|
+
// src/constants.ts
|
|
969
|
+
var ADDRESS_SIMILARITY_THRESHOLD = 70;
|
|
970
|
+
|
|
957
971
|
// src/narratives/templates/typescript/PatientTemplate.ts
|
|
958
972
|
var PatientTemplate = class _PatientTemplate {
|
|
959
973
|
/**
|
|
@@ -980,7 +994,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
980
994
|
<li><strong>Name(s):</strong>${this.renderNames(combinedPatient)}</li>
|
|
981
995
|
<li><strong>Gender:</strong>${combinedPatient.gender ? this.capitalize(combinedPatient.gender) : ""}</li>
|
|
982
996
|
<li><strong>Date of Birth:</strong>${combinedPatient.birthDate || ""}</li>
|
|
983
|
-
<li><strong>Identifier(s):</strong>${this.renderIdentifiers(combinedPatient)}</li>
|
|
984
997
|
<li><strong>Telecom:</strong><ul>${this.renderTelecom(combinedPatient)}</ul></li>
|
|
985
998
|
<li><strong>Address(es):</strong>${this.renderAddresses(combinedPatient)}</li>
|
|
986
999
|
<li><strong>Marital Status:</strong> ${combinedPatient.maritalStatus?.text || ""}</li>
|
|
@@ -1001,7 +1014,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
1001
1014
|
}
|
|
1002
1015
|
const combined = patients[0];
|
|
1003
1016
|
const allNames = [];
|
|
1004
|
-
const allIdentifiers = [];
|
|
1005
1017
|
const allTelecom = [];
|
|
1006
1018
|
const allAddresses = [];
|
|
1007
1019
|
const allCommunication = [];
|
|
@@ -1009,9 +1021,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
1009
1021
|
if (patient.name) {
|
|
1010
1022
|
allNames.push(...patient.name);
|
|
1011
1023
|
}
|
|
1012
|
-
if (patient.identifier) {
|
|
1013
|
-
allIdentifiers.push(...patient.identifier);
|
|
1014
|
-
}
|
|
1015
1024
|
if (patient.telecom) {
|
|
1016
1025
|
allTelecom.push(...patient.telecom);
|
|
1017
1026
|
}
|
|
@@ -1038,7 +1047,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
1038
1047
|
}
|
|
1039
1048
|
});
|
|
1040
1049
|
combined.name = allNames;
|
|
1041
|
-
combined.identifier = allIdentifiers;
|
|
1042
1050
|
combined.telecom = allTelecom;
|
|
1043
1051
|
combined.address = allAddresses;
|
|
1044
1052
|
combined.communication = allCommunication;
|
|
@@ -1064,21 +1072,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
1064
1072
|
});
|
|
1065
1073
|
return Array.from(uniqueNames).map((nameText) => `<ul><li>${nameText}</li></ul>`).join("");
|
|
1066
1074
|
}
|
|
1067
|
-
/**
|
|
1068
|
-
* Renders patient identifiers as HTML list items
|
|
1069
|
-
* @param patient - Patient resources
|
|
1070
|
-
* @returns HTML string of list items
|
|
1071
|
-
*/
|
|
1072
|
-
static renderIdentifiers(patient) {
|
|
1073
|
-
if (!patient.identifier || patient.identifier.length === 0) {
|
|
1074
|
-
return "";
|
|
1075
|
-
}
|
|
1076
|
-
return patient.identifier.map((id) => {
|
|
1077
|
-
const system = id.system || "";
|
|
1078
|
-
const value = id.value || "";
|
|
1079
|
-
return `<ul><li>${system}: ${value}</li></ul>`;
|
|
1080
|
-
}).join("");
|
|
1081
|
-
}
|
|
1082
1075
|
/**
|
|
1083
1076
|
* Renders patient telecom information grouped by system
|
|
1084
1077
|
* @param patient - Patient resources
|
|
@@ -1180,7 +1173,89 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
1180
1173
|
uniqueAddresses.add(addressText);
|
|
1181
1174
|
}
|
|
1182
1175
|
});
|
|
1183
|
-
|
|
1176
|
+
const deduplicatedAddresses = this.deduplicateSimilarAddresses(Array.from(uniqueAddresses));
|
|
1177
|
+
return deduplicatedAddresses.map((addressText) => `<ul><li>${addressText}</li></ul>`).join("");
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Calculates the similarity between two strings using Levenshtein distance
|
|
1181
|
+
* Returns a percentage (0-100) indicating how similar the strings are
|
|
1182
|
+
* @param str1 - First string
|
|
1183
|
+
* @param str2 - Second string
|
|
1184
|
+
* @returns Similarity percentage (0-100)
|
|
1185
|
+
*/
|
|
1186
|
+
static calculateStringSimilarity(str1, str2) {
|
|
1187
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
1188
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
1189
|
+
if (longer.length === 0) {
|
|
1190
|
+
return 100;
|
|
1191
|
+
}
|
|
1192
|
+
const editDistance = this.levenshteinDistance(longer.toLowerCase(), shorter.toLowerCase());
|
|
1193
|
+
return (longer.length - editDistance) / longer.length * 100;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Calculates the Levenshtein distance between two strings
|
|
1197
|
+
* @param str1 - First string
|
|
1198
|
+
* @param str2 - Second string
|
|
1199
|
+
* @returns The Levenshtein distance
|
|
1200
|
+
*/
|
|
1201
|
+
static levenshteinDistance(str1, str2) {
|
|
1202
|
+
const matrix = [];
|
|
1203
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
1204
|
+
matrix[i] = [i];
|
|
1205
|
+
}
|
|
1206
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
1207
|
+
matrix[0][j] = j;
|
|
1208
|
+
}
|
|
1209
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
1210
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
1211
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
1212
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
1213
|
+
} else {
|
|
1214
|
+
matrix[i][j] = Math.min(
|
|
1215
|
+
matrix[i - 1][j - 1] + 1,
|
|
1216
|
+
// substitution
|
|
1217
|
+
matrix[i][j - 1] + 1,
|
|
1218
|
+
// insertion
|
|
1219
|
+
matrix[i - 1][j] + 1
|
|
1220
|
+
// deletion
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return matrix[str2.length][str1.length];
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Deduplicates addresses that are more than ADDRESS_SIMILARITY_THRESHOLD% similar
|
|
1229
|
+
* @param addresses - Array of address strings
|
|
1230
|
+
* @returns Array of deduplicated addresses
|
|
1231
|
+
*/
|
|
1232
|
+
static deduplicateSimilarAddresses(addresses) {
|
|
1233
|
+
if (addresses.length <= 1) {
|
|
1234
|
+
return addresses;
|
|
1235
|
+
}
|
|
1236
|
+
const deduplicated = [];
|
|
1237
|
+
const processed = /* @__PURE__ */ new Set();
|
|
1238
|
+
for (let i = 0; i < addresses.length; i++) {
|
|
1239
|
+
if (processed.has(i)) {
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
let keepAddress = addresses[i];
|
|
1243
|
+
processed.add(i);
|
|
1244
|
+
for (let j = i + 1; j < addresses.length; j++) {
|
|
1245
|
+
if (processed.has(j)) {
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const similarity = this.calculateStringSimilarity(addresses[i], addresses[j]);
|
|
1249
|
+
if (similarity > ADDRESS_SIMILARITY_THRESHOLD) {
|
|
1250
|
+
processed.add(j);
|
|
1251
|
+
if (addresses[j].length > keepAddress.length) {
|
|
1252
|
+
keepAddress = addresses[j];
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
deduplicated.push(keepAddress);
|
|
1257
|
+
}
|
|
1258
|
+
return deduplicated;
|
|
1184
1259
|
}
|
|
1185
1260
|
/**
|
|
1186
1261
|
* Renders patient deceased status
|
|
@@ -1428,6 +1503,75 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1428
1503
|
generateNarrative(resources, timezone) {
|
|
1429
1504
|
return _MedicationSummaryTemplate.generateStaticNarrative(resources, timezone);
|
|
1430
1505
|
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Generate HTML narrative for Medication resources using summary
|
|
1508
|
+
* @param resources - FHIR Composition resources
|
|
1509
|
+
* @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
|
|
1510
|
+
* @returns HTML string for rendering
|
|
1511
|
+
*/
|
|
1512
|
+
generateSummaryNarrative(resources, timezone) {
|
|
1513
|
+
const templateUtilities = new TemplateUtilities(resources);
|
|
1514
|
+
let html = `
|
|
1515
|
+
<div>
|
|
1516
|
+
<table>
|
|
1517
|
+
<thead>
|
|
1518
|
+
<tr>
|
|
1519
|
+
<th>Medication</th>
|
|
1520
|
+
<th>Sig</th>
|
|
1521
|
+
<th>Days of Supply</th>
|
|
1522
|
+
<th>Refills</th>
|
|
1523
|
+
<th>Start Date</th>
|
|
1524
|
+
</tr>
|
|
1525
|
+
</thead>
|
|
1526
|
+
<tbody>`;
|
|
1527
|
+
for (const resourceItem of resources) {
|
|
1528
|
+
for (const rowData of resourceItem.section ?? []) {
|
|
1529
|
+
const data = {};
|
|
1530
|
+
for (const columnData of rowData.section ?? []) {
|
|
1531
|
+
switch (columnData.title) {
|
|
1532
|
+
case "Medication Name":
|
|
1533
|
+
data["medication"] = columnData.text?.div ?? "";
|
|
1534
|
+
break;
|
|
1535
|
+
case "Status":
|
|
1536
|
+
data["status"] = columnData.text?.div ?? "";
|
|
1537
|
+
break;
|
|
1538
|
+
case "Prescriber Instruction":
|
|
1539
|
+
data["sig-prescriber"] = columnData.text?.div ?? "";
|
|
1540
|
+
break;
|
|
1541
|
+
case "Pharmacy Instruction":
|
|
1542
|
+
data["sig-pharmacy"] = columnData.text?.div ?? "";
|
|
1543
|
+
break;
|
|
1544
|
+
case "Days Of Supply":
|
|
1545
|
+
data["daysOfSupply"] = columnData.text?.div ?? "";
|
|
1546
|
+
break;
|
|
1547
|
+
case "Refills Remaining":
|
|
1548
|
+
data["refills"] = columnData.text?.div ?? "";
|
|
1549
|
+
break;
|
|
1550
|
+
case "Authored On Date":
|
|
1551
|
+
data["startDate"] = columnData.text?.div ?? "";
|
|
1552
|
+
break;
|
|
1553
|
+
default:
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (data["status"] === "active") {
|
|
1558
|
+
html += `
|
|
1559
|
+
<tr>
|
|
1560
|
+
<td>${data["medication"]}</td>
|
|
1561
|
+
<td>${data["sig-prescriber"] || data["sig-pharmacy"]}</td>
|
|
1562
|
+
<td>${data["daysOfSupply"]}</td>
|
|
1563
|
+
<td>${data["refills"]}</td>
|
|
1564
|
+
<td>${templateUtilities.renderTime(data["startDate"], timezone)}</td>
|
|
1565
|
+
</tr>`;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
html += `
|
|
1570
|
+
</tbody>
|
|
1571
|
+
</table>
|
|
1572
|
+
</div>`;
|
|
1573
|
+
return html;
|
|
1574
|
+
}
|
|
1431
1575
|
/**
|
|
1432
1576
|
* Safely parse a date string and return a valid Date object or null
|
|
1433
1577
|
* @param dateString - The date string to parse
|
|
@@ -1440,55 +1584,6 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1440
1584
|
const date = new Date(dateString);
|
|
1441
1585
|
return !isNaN(date.getTime()) ? date : null;
|
|
1442
1586
|
}
|
|
1443
|
-
/**
|
|
1444
|
-
* Determine if a MedicationRequest is active
|
|
1445
|
-
* @param medicationRequest - The MedicationRequest resource
|
|
1446
|
-
* @returns boolean indicating if the medication request is active
|
|
1447
|
-
*/
|
|
1448
|
-
static isActiveMedicationRequest(medicationRequest) {
|
|
1449
|
-
const status = medicationRequest.status?.toLowerCase();
|
|
1450
|
-
if (status === "active" || status === "unknown") {
|
|
1451
|
-
return true;
|
|
1452
|
-
}
|
|
1453
|
-
if (status === "completed" || status === "cancelled" || status === "stopped" || status === "draft") {
|
|
1454
|
-
return false;
|
|
1455
|
-
}
|
|
1456
|
-
const endDate = medicationRequest.dispenseRequest?.validityPeriod?.end;
|
|
1457
|
-
if (!endDate) {
|
|
1458
|
-
return true;
|
|
1459
|
-
}
|
|
1460
|
-
const parsedEndDate = this.parseDate(endDate);
|
|
1461
|
-
if (!parsedEndDate) {
|
|
1462
|
-
return true;
|
|
1463
|
-
}
|
|
1464
|
-
return parsedEndDate.getTime() > Date.now();
|
|
1465
|
-
}
|
|
1466
|
-
/**
|
|
1467
|
-
* Determine if a MedicationStatement is active
|
|
1468
|
-
* @param medicationStatement - The MedicationStatement resource
|
|
1469
|
-
* @returns boolean indicating if the medication statement is active
|
|
1470
|
-
*/
|
|
1471
|
-
static isActiveMedicationStatement(medicationStatement) {
|
|
1472
|
-
const status = medicationStatement.status?.toLowerCase();
|
|
1473
|
-
if (status === "active" || status === "intended" || status === "unknown") {
|
|
1474
|
-
return true;
|
|
1475
|
-
}
|
|
1476
|
-
if (status === "completed" || status === "stopped" || status === "not-taken") {
|
|
1477
|
-
return false;
|
|
1478
|
-
}
|
|
1479
|
-
let endDate;
|
|
1480
|
-
if (medicationStatement.effectivePeriod?.end) {
|
|
1481
|
-
endDate = medicationStatement.effectivePeriod.end;
|
|
1482
|
-
}
|
|
1483
|
-
if (!endDate) {
|
|
1484
|
-
return true;
|
|
1485
|
-
}
|
|
1486
|
-
const parsedEndDate = this.parseDate(endDate);
|
|
1487
|
-
if (!parsedEndDate) {
|
|
1488
|
-
return true;
|
|
1489
|
-
}
|
|
1490
|
-
return parsedEndDate.getTime() > Date.now();
|
|
1491
|
-
}
|
|
1492
1587
|
/**
|
|
1493
1588
|
* Internal static implementation that actually generates the narrative
|
|
1494
1589
|
* @param resources - FHIR Medication resources
|
|
@@ -1502,20 +1597,11 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1502
1597
|
const medicationRequests = this.getMedicationRequests(templateUtilities, resources);
|
|
1503
1598
|
const medicationStatements = this.getMedicationStatements(templateUtilities, resources);
|
|
1504
1599
|
const allActiveMedications = [];
|
|
1505
|
-
const allInactiveMedications = [];
|
|
1506
1600
|
medicationRequests.forEach((mr) => {
|
|
1507
|
-
|
|
1508
|
-
allActiveMedications.push({ type: "request", resource: mr.resource, extension: mr.extension });
|
|
1509
|
-
} else {
|
|
1510
|
-
allInactiveMedications.push({ type: "request", resource: mr.resource, extension: mr.extension });
|
|
1511
|
-
}
|
|
1601
|
+
allActiveMedications.push({ type: "request", resource: mr.resource, extension: mr.extension });
|
|
1512
1602
|
});
|
|
1513
1603
|
medicationStatements.forEach((ms) => {
|
|
1514
|
-
|
|
1515
|
-
allActiveMedications.push({ type: "statement", resource: ms.resource, extension: ms.extension });
|
|
1516
|
-
} else {
|
|
1517
|
-
allInactiveMedications.push({ type: "statement", resource: ms.resource, extension: ms.extension });
|
|
1518
|
-
}
|
|
1604
|
+
allActiveMedications.push({ type: "statement", resource: ms.resource, extension: ms.extension });
|
|
1519
1605
|
});
|
|
1520
1606
|
const sortMedications = (medications) => {
|
|
1521
1607
|
medications.sort((a, b) => {
|
|
@@ -1545,11 +1631,7 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1545
1631
|
};
|
|
1546
1632
|
if (allActiveMedications.length > 0) {
|
|
1547
1633
|
sortMedications(allActiveMedications);
|
|
1548
|
-
html += this.renderCombinedMedications(templateUtilities, allActiveMedications
|
|
1549
|
-
}
|
|
1550
|
-
if (allInactiveMedications.length > 0) {
|
|
1551
|
-
sortMedications(allInactiveMedications);
|
|
1552
|
-
html += this.renderCombinedMedications(templateUtilities, allInactiveMedications, false);
|
|
1634
|
+
html += this.renderCombinedMedications(templateUtilities, allActiveMedications);
|
|
1553
1635
|
}
|
|
1554
1636
|
return html;
|
|
1555
1637
|
}
|
|
@@ -1587,12 +1669,10 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1587
1669
|
* Render HTML table for combined MedicationRequest and MedicationStatement resources
|
|
1588
1670
|
* @param templateUtilities - Instance of TemplateUtilities for utility functions
|
|
1589
1671
|
* @param medications - Array of combined medication resources
|
|
1590
|
-
* @param sectionTitle - Title for the section
|
|
1591
1672
|
* @returns HTML string for rendering
|
|
1592
1673
|
*/
|
|
1593
|
-
static renderCombinedMedications(templateUtilities, medications
|
|
1674
|
+
static renderCombinedMedications(templateUtilities, medications) {
|
|
1594
1675
|
let html = `
|
|
1595
|
-
<h3>${isActiveSection ? "Active Medications" : "Inactive Medications"}</h3>
|
|
1596
1676
|
<table>
|
|
1597
1677
|
<thead>
|
|
1598
1678
|
<tr>
|
|
@@ -1601,9 +1681,7 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1601
1681
|
<th>Sig</th>
|
|
1602
1682
|
<th>Dispense Quantity</th>
|
|
1603
1683
|
<th>Refills</th>
|
|
1604
|
-
<th>Start Date</th
|
|
1605
|
-
<th>End Date</th>`}
|
|
1606
|
-
<th>Status</th>
|
|
1684
|
+
<th>Start Date</th>
|
|
1607
1685
|
</tr>
|
|
1608
1686
|
</thead>
|
|
1609
1687
|
<tbody>`;
|
|
@@ -1615,12 +1693,9 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1615
1693
|
let dispenseQuantity = "-";
|
|
1616
1694
|
let refills = "-";
|
|
1617
1695
|
let startDate = "-";
|
|
1618
|
-
let endDate = "-";
|
|
1619
|
-
let status;
|
|
1620
1696
|
if (medication.type === "request") {
|
|
1621
1697
|
const mr = medication.resource;
|
|
1622
1698
|
type = "Request";
|
|
1623
|
-
status = mr.status ? String(mr.status) : "-";
|
|
1624
1699
|
medicationName = templateUtilities.getMedicationName(
|
|
1625
1700
|
mr.medicationReference || mr.medicationCodeableConcept
|
|
1626
1701
|
);
|
|
@@ -1634,14 +1709,12 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1634
1709
|
refills = mr.dispenseRequest?.numberOfRepeatsAllowed?.toString() || "-";
|
|
1635
1710
|
if (mr.dispenseRequest?.validityPeriod) {
|
|
1636
1711
|
startDate = mr.dispenseRequest.validityPeriod.start || "-";
|
|
1637
|
-
endDate = mr.dispenseRequest.validityPeriod.end || "-";
|
|
1638
1712
|
} else {
|
|
1639
1713
|
startDate = mr.authoredOn || "-";
|
|
1640
1714
|
}
|
|
1641
1715
|
} else {
|
|
1642
1716
|
const ms = medication.resource;
|
|
1643
1717
|
type = "Statement";
|
|
1644
|
-
status = ms.status ? String(ms.status) : "-";
|
|
1645
1718
|
medicationName = templateUtilities.getMedicationName(
|
|
1646
1719
|
ms.medicationReference || ms.medicationCodeableConcept
|
|
1647
1720
|
);
|
|
@@ -1650,7 +1723,6 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1650
1723
|
startDate = ms.effectiveDateTime;
|
|
1651
1724
|
} else if (ms.effectivePeriod) {
|
|
1652
1725
|
startDate = ms.effectivePeriod.start || "-";
|
|
1653
|
-
endDate = ms.effectivePeriod.end || "-";
|
|
1654
1726
|
}
|
|
1655
1727
|
}
|
|
1656
1728
|
html += `
|
|
@@ -1660,9 +1732,7 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1660
1732
|
<td>${sig}</td>
|
|
1661
1733
|
<td>${dispenseQuantity}</td>
|
|
1662
1734
|
<td>${refills}</td>
|
|
1663
|
-
<td>${startDate}</td
|
|
1664
|
-
<td>${endDate}</td>`}
|
|
1665
|
-
<td>${status}</td>
|
|
1735
|
+
<td>${startDate}</td>
|
|
1666
1736
|
</tr>`;
|
|
1667
1737
|
}
|
|
1668
1738
|
html += `
|
|
@@ -1803,6 +1873,7 @@ var ProblemListTemplate = class _ProblemListTemplate {
|
|
|
1803
1873
|
* @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
|
|
1804
1874
|
* @returns HTML string for rendering
|
|
1805
1875
|
*/
|
|
1876
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1806
1877
|
static generateStaticNarrative(resources, timezone) {
|
|
1807
1878
|
const templateUtilities = new TemplateUtilities(resources);
|
|
1808
1879
|
let html = ``;
|
|
@@ -1819,7 +1890,6 @@ var ProblemListTemplate = class _ProblemListTemplate {
|
|
|
1819
1890
|
<th>Problem</th>
|
|
1820
1891
|
<th>Onset Date</th>
|
|
1821
1892
|
<th>Recorded Date</th>
|
|
1822
|
-
<th>Notes</th>
|
|
1823
1893
|
</tr>
|
|
1824
1894
|
</thead>
|
|
1825
1895
|
<tbody>`;
|
|
@@ -1828,7 +1898,6 @@ var ProblemListTemplate = class _ProblemListTemplate {
|
|
|
1828
1898
|
<td class="Name">${templateUtilities.codeableConcept(cond.code)}</td>
|
|
1829
1899
|
<td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
|
|
1830
1900
|
<td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
|
|
1831
|
-
<td class="Notes">${templateUtilities.renderNotes(cond.note, timezone)}</td>
|
|
1832
1901
|
</tr>`;
|
|
1833
1902
|
}
|
|
1834
1903
|
html += `</tbody>
|
|
@@ -1837,13 +1906,6 @@ var ProblemListTemplate = class _ProblemListTemplate {
|
|
|
1837
1906
|
}
|
|
1838
1907
|
};
|
|
1839
1908
|
|
|
1840
|
-
// src/structures/ips_section_constants.ts
|
|
1841
|
-
var VITAL_SIGNS_SUMMARY_COMPONENT_MAP = {
|
|
1842
|
-
"Systolic Blood Pressure": "valueRatio.numerator.value",
|
|
1843
|
-
"Diastolic Blood Pressure": "valueRatio.denominator.value",
|
|
1844
|
-
"Default": "valueString"
|
|
1845
|
-
};
|
|
1846
|
-
|
|
1847
1909
|
// src/narratives/templates/typescript/VitalSignsTemplate.ts
|
|
1848
1910
|
var VitalSignsTemplate = class _VitalSignsTemplate {
|
|
1849
1911
|
/**
|
|
@@ -2263,6 +2325,7 @@ var PastHistoryOfIllnessTemplate = class {
|
|
|
2263
2325
|
* @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
|
|
2264
2326
|
* @returns HTML string for rendering
|
|
2265
2327
|
*/
|
|
2328
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2266
2329
|
generateNarrative(resources, timezone) {
|
|
2267
2330
|
const templateUtilities = new TemplateUtilities(resources);
|
|
2268
2331
|
let html = ``;
|
|
@@ -2280,7 +2343,6 @@ var PastHistoryOfIllnessTemplate = class {
|
|
|
2280
2343
|
<th>Onset Date</th>
|
|
2281
2344
|
<th>Recorded Date</th>
|
|
2282
2345
|
<th>Resolved Date</th>
|
|
2283
|
-
<th>Notes</th>
|
|
2284
2346
|
</tr>
|
|
2285
2347
|
</thead>
|
|
2286
2348
|
<tbody>`;
|
|
@@ -2290,7 +2352,6 @@ var PastHistoryOfIllnessTemplate = class {
|
|
|
2290
2352
|
<td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
|
|
2291
2353
|
<td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
|
|
2292
2354
|
<td class="ResolvedDate">${templateUtilities.renderDate(cond.abatementDateTime)}</td>
|
|
2293
|
-
<td class="Notes">${templateUtilities.renderNotes(cond.note, timezone)}</td>
|
|
2294
2355
|
</tr>`;
|
|
2295
2356
|
}
|
|
2296
2357
|
html += `</tbody>
|
|
@@ -2443,7 +2504,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2443
2504
|
<th>Problem</th>
|
|
2444
2505
|
<th>Onset Date</th>
|
|
2445
2506
|
<th>Recorded Date</th>
|
|
2446
|
-
<th>Notes</th>
|
|
2447
2507
|
</tr>
|
|
2448
2508
|
</thead>
|
|
2449
2509
|
<tbody>`;
|
|
@@ -2452,7 +2512,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2452
2512
|
<td class="Name">${templateUtilities.codeableConcept(cond.code)}</td>
|
|
2453
2513
|
<td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
|
|
2454
2514
|
<td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
|
|
2455
|
-
<td class="Notes">${templateUtilities.renderNotes(cond.note, timezone, { styled: true, warning: true })}</td>
|
|
2456
2515
|
</tr>`;
|
|
2457
2516
|
}
|
|
2458
2517
|
html += `</tbody>
|
|
@@ -2468,7 +2527,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2468
2527
|
<th>Description</th>
|
|
2469
2528
|
<th>Summary</th>
|
|
2470
2529
|
<th>Findings</th>
|
|
2471
|
-
<th>Notes</th>
|
|
2472
2530
|
</tr>
|
|
2473
2531
|
</thead>
|
|
2474
2532
|
<tbody>`;
|
|
@@ -2497,7 +2555,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2497
2555
|
}
|
|
2498
2556
|
findingsHtml += "</ul>";
|
|
2499
2557
|
}
|
|
2500
|
-
const notes = templateUtilities.renderNotes(impression.note, timezone);
|
|
2501
2558
|
html += `
|
|
2502
2559
|
<tr id="${templateUtilities.narrativeLinkId(impression)}">
|
|
2503
2560
|
<td>${formattedDate}</td>
|
|
@@ -2505,7 +2562,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2505
2562
|
<td>${impression.description || ""}</td>
|
|
2506
2563
|
<td>${impression.summary || ""}</td>
|
|
2507
2564
|
<td>${findingsHtml}</td>
|
|
2508
|
-
<td>${notes}</td>
|
|
2509
2565
|
</tr>`;
|
|
2510
2566
|
}
|
|
2511
2567
|
html += `</tbody>
|
package/dist/index.js
CHANGED
|
@@ -81,6 +81,14 @@ var BLOOD_PRESSURE_LOINC_CODES = {
|
|
|
81
81
|
DIASTOLIC: "8462-4"
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
// src/structures/ips_section_constants.ts
|
|
85
|
+
var VITAL_SIGNS_SUMMARY_COMPONENT_MAP = {
|
|
86
|
+
"Systolic Blood Pressure": "valueRatio.numerator.value",
|
|
87
|
+
"Diastolic Blood Pressure": "valueRatio.denominator.value",
|
|
88
|
+
"Default": "valueString"
|
|
89
|
+
};
|
|
90
|
+
var IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM = "https://fhir.icanbwell.com/4_0_0/CodeSystem/composition/";
|
|
91
|
+
|
|
84
92
|
// src/structures/ips_section_resource_map.ts
|
|
85
93
|
var IPSSectionResourceFilters = {
|
|
86
94
|
// Patient section: only Patient resource
|
|
@@ -88,7 +96,7 @@ var IPSSectionResourceFilters = {
|
|
|
88
96
|
// Only include allergies
|
|
89
97
|
["AllergyIntoleranceSection" /* ALLERGIES */]: (resource) => resource.resourceType === "AllergyIntolerance",
|
|
90
98
|
// includes MedicationRequest, MedicationStatement. Medication is needed for medication names
|
|
91
|
-
["MedicationSummarySection" /* MEDICATIONS */]: (resource) => ["MedicationRequest", "MedicationStatement"
|
|
99
|
+
["MedicationSummarySection" /* MEDICATIONS */]: (resource) => ["MedicationRequest", "MedicationStatement"].includes(resource.resourceType) && resource.status === "active" || resource.resourceType === "Medication",
|
|
92
100
|
// Only include active conditions
|
|
93
101
|
["ProblemSection" /* PROBLEMS */]: (resource) => resource.resourceType === "Condition" && resource.clinicalStatus?.coding?.some((c) => !["inactive", "resolved"].includes(c.code)),
|
|
94
102
|
// Only include completed immunizations
|
|
@@ -115,10 +123,11 @@ var IPSSectionResourceFilters = {
|
|
|
115
123
|
["AdvanceDirectivesSection" /* ADVANCE_DIRECTIVES */]: (resource) => resource.resourceType === "Consent" && resource.status === "active"
|
|
116
124
|
};
|
|
117
125
|
var IPSSectionSummaryCompositionFilter = {
|
|
118
|
-
["AllergyIntoleranceSection" /* ALLERGIES */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system ===
|
|
119
|
-
["VitalSignsSection" /* VITAL_SIGNS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system ===
|
|
120
|
-
["PlanOfCareSection" /* CARE_PLAN */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system ===
|
|
121
|
-
["ImmunizationSection" /* IMMUNIZATIONS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system ===
|
|
126
|
+
["AllergyIntoleranceSection" /* ALLERGIES */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "allergy_summary_document"),
|
|
127
|
+
["VitalSignsSection" /* VITAL_SIGNS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "vital_summary_document"),
|
|
128
|
+
["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
|
+
["ImmunizationSection" /* IMMUNIZATIONS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "immunization_summary_document"),
|
|
130
|
+
["MedicationSummarySection" /* MEDICATIONS */]: (resource) => resource.resourceType === "Composition" && resource.type?.coding?.some((c) => c.system === IPS_SUMMARY_COMPOSITION_TYPE_SYSTEM && c.code === "medication_summary_document")
|
|
122
131
|
};
|
|
123
132
|
var IPSSectionResourceHelper = class {
|
|
124
133
|
static getResourceFilterForSection(section) {
|
|
@@ -822,9 +831,11 @@ var TemplateUtilities = class {
|
|
|
822
831
|
if (dateValue instanceof Date) {
|
|
823
832
|
dateTime = DateTime.fromJSDate(dateValue);
|
|
824
833
|
} else if (typeof dateValue === "string") {
|
|
825
|
-
dateTime = DateTime.fromISO(dateValue);
|
|
826
834
|
if (!dateValue.includes("T")) {
|
|
827
835
|
dateOnly = true;
|
|
836
|
+
dateTime = DateTime.fromISO(dateValue, { zone: "utc" });
|
|
837
|
+
} else {
|
|
838
|
+
dateTime = DateTime.fromISO(dateValue);
|
|
828
839
|
}
|
|
829
840
|
} else {
|
|
830
841
|
dateTime = DateTime.fromISO(String(dateValue));
|
|
@@ -926,6 +937,9 @@ var TemplateUtilities = class {
|
|
|
926
937
|
}
|
|
927
938
|
};
|
|
928
939
|
|
|
940
|
+
// src/constants.ts
|
|
941
|
+
var ADDRESS_SIMILARITY_THRESHOLD = 70;
|
|
942
|
+
|
|
929
943
|
// src/narratives/templates/typescript/PatientTemplate.ts
|
|
930
944
|
var PatientTemplate = class _PatientTemplate {
|
|
931
945
|
/**
|
|
@@ -952,7 +966,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
952
966
|
<li><strong>Name(s):</strong>${this.renderNames(combinedPatient)}</li>
|
|
953
967
|
<li><strong>Gender:</strong>${combinedPatient.gender ? this.capitalize(combinedPatient.gender) : ""}</li>
|
|
954
968
|
<li><strong>Date of Birth:</strong>${combinedPatient.birthDate || ""}</li>
|
|
955
|
-
<li><strong>Identifier(s):</strong>${this.renderIdentifiers(combinedPatient)}</li>
|
|
956
969
|
<li><strong>Telecom:</strong><ul>${this.renderTelecom(combinedPatient)}</ul></li>
|
|
957
970
|
<li><strong>Address(es):</strong>${this.renderAddresses(combinedPatient)}</li>
|
|
958
971
|
<li><strong>Marital Status:</strong> ${combinedPatient.maritalStatus?.text || ""}</li>
|
|
@@ -973,7 +986,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
973
986
|
}
|
|
974
987
|
const combined = patients[0];
|
|
975
988
|
const allNames = [];
|
|
976
|
-
const allIdentifiers = [];
|
|
977
989
|
const allTelecom = [];
|
|
978
990
|
const allAddresses = [];
|
|
979
991
|
const allCommunication = [];
|
|
@@ -981,9 +993,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
981
993
|
if (patient.name) {
|
|
982
994
|
allNames.push(...patient.name);
|
|
983
995
|
}
|
|
984
|
-
if (patient.identifier) {
|
|
985
|
-
allIdentifiers.push(...patient.identifier);
|
|
986
|
-
}
|
|
987
996
|
if (patient.telecom) {
|
|
988
997
|
allTelecom.push(...patient.telecom);
|
|
989
998
|
}
|
|
@@ -1010,7 +1019,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
1010
1019
|
}
|
|
1011
1020
|
});
|
|
1012
1021
|
combined.name = allNames;
|
|
1013
|
-
combined.identifier = allIdentifiers;
|
|
1014
1022
|
combined.telecom = allTelecom;
|
|
1015
1023
|
combined.address = allAddresses;
|
|
1016
1024
|
combined.communication = allCommunication;
|
|
@@ -1036,21 +1044,6 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
1036
1044
|
});
|
|
1037
1045
|
return Array.from(uniqueNames).map((nameText) => `<ul><li>${nameText}</li></ul>`).join("");
|
|
1038
1046
|
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Renders patient identifiers as HTML list items
|
|
1041
|
-
* @param patient - Patient resources
|
|
1042
|
-
* @returns HTML string of list items
|
|
1043
|
-
*/
|
|
1044
|
-
static renderIdentifiers(patient) {
|
|
1045
|
-
if (!patient.identifier || patient.identifier.length === 0) {
|
|
1046
|
-
return "";
|
|
1047
|
-
}
|
|
1048
|
-
return patient.identifier.map((id) => {
|
|
1049
|
-
const system = id.system || "";
|
|
1050
|
-
const value = id.value || "";
|
|
1051
|
-
return `<ul><li>${system}: ${value}</li></ul>`;
|
|
1052
|
-
}).join("");
|
|
1053
|
-
}
|
|
1054
1047
|
/**
|
|
1055
1048
|
* Renders patient telecom information grouped by system
|
|
1056
1049
|
* @param patient - Patient resources
|
|
@@ -1152,7 +1145,89 @@ var PatientTemplate = class _PatientTemplate {
|
|
|
1152
1145
|
uniqueAddresses.add(addressText);
|
|
1153
1146
|
}
|
|
1154
1147
|
});
|
|
1155
|
-
|
|
1148
|
+
const deduplicatedAddresses = this.deduplicateSimilarAddresses(Array.from(uniqueAddresses));
|
|
1149
|
+
return deduplicatedAddresses.map((addressText) => `<ul><li>${addressText}</li></ul>`).join("");
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Calculates the similarity between two strings using Levenshtein distance
|
|
1153
|
+
* Returns a percentage (0-100) indicating how similar the strings are
|
|
1154
|
+
* @param str1 - First string
|
|
1155
|
+
* @param str2 - Second string
|
|
1156
|
+
* @returns Similarity percentage (0-100)
|
|
1157
|
+
*/
|
|
1158
|
+
static calculateStringSimilarity(str1, str2) {
|
|
1159
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
1160
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
1161
|
+
if (longer.length === 0) {
|
|
1162
|
+
return 100;
|
|
1163
|
+
}
|
|
1164
|
+
const editDistance = this.levenshteinDistance(longer.toLowerCase(), shorter.toLowerCase());
|
|
1165
|
+
return (longer.length - editDistance) / longer.length * 100;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Calculates the Levenshtein distance between two strings
|
|
1169
|
+
* @param str1 - First string
|
|
1170
|
+
* @param str2 - Second string
|
|
1171
|
+
* @returns The Levenshtein distance
|
|
1172
|
+
*/
|
|
1173
|
+
static levenshteinDistance(str1, str2) {
|
|
1174
|
+
const matrix = [];
|
|
1175
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
1176
|
+
matrix[i] = [i];
|
|
1177
|
+
}
|
|
1178
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
1179
|
+
matrix[0][j] = j;
|
|
1180
|
+
}
|
|
1181
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
1182
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
1183
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
1184
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
1185
|
+
} else {
|
|
1186
|
+
matrix[i][j] = Math.min(
|
|
1187
|
+
matrix[i - 1][j - 1] + 1,
|
|
1188
|
+
// substitution
|
|
1189
|
+
matrix[i][j - 1] + 1,
|
|
1190
|
+
// insertion
|
|
1191
|
+
matrix[i - 1][j] + 1
|
|
1192
|
+
// deletion
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return matrix[str2.length][str1.length];
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Deduplicates addresses that are more than ADDRESS_SIMILARITY_THRESHOLD% similar
|
|
1201
|
+
* @param addresses - Array of address strings
|
|
1202
|
+
* @returns Array of deduplicated addresses
|
|
1203
|
+
*/
|
|
1204
|
+
static deduplicateSimilarAddresses(addresses) {
|
|
1205
|
+
if (addresses.length <= 1) {
|
|
1206
|
+
return addresses;
|
|
1207
|
+
}
|
|
1208
|
+
const deduplicated = [];
|
|
1209
|
+
const processed = /* @__PURE__ */ new Set();
|
|
1210
|
+
for (let i = 0; i < addresses.length; i++) {
|
|
1211
|
+
if (processed.has(i)) {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
let keepAddress = addresses[i];
|
|
1215
|
+
processed.add(i);
|
|
1216
|
+
for (let j = i + 1; j < addresses.length; j++) {
|
|
1217
|
+
if (processed.has(j)) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const similarity = this.calculateStringSimilarity(addresses[i], addresses[j]);
|
|
1221
|
+
if (similarity > ADDRESS_SIMILARITY_THRESHOLD) {
|
|
1222
|
+
processed.add(j);
|
|
1223
|
+
if (addresses[j].length > keepAddress.length) {
|
|
1224
|
+
keepAddress = addresses[j];
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
deduplicated.push(keepAddress);
|
|
1229
|
+
}
|
|
1230
|
+
return deduplicated;
|
|
1156
1231
|
}
|
|
1157
1232
|
/**
|
|
1158
1233
|
* Renders patient deceased status
|
|
@@ -1400,6 +1475,75 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1400
1475
|
generateNarrative(resources, timezone) {
|
|
1401
1476
|
return _MedicationSummaryTemplate.generateStaticNarrative(resources, timezone);
|
|
1402
1477
|
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Generate HTML narrative for Medication resources using summary
|
|
1480
|
+
* @param resources - FHIR Composition resources
|
|
1481
|
+
* @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
|
|
1482
|
+
* @returns HTML string for rendering
|
|
1483
|
+
*/
|
|
1484
|
+
generateSummaryNarrative(resources, timezone) {
|
|
1485
|
+
const templateUtilities = new TemplateUtilities(resources);
|
|
1486
|
+
let html = `
|
|
1487
|
+
<div>
|
|
1488
|
+
<table>
|
|
1489
|
+
<thead>
|
|
1490
|
+
<tr>
|
|
1491
|
+
<th>Medication</th>
|
|
1492
|
+
<th>Sig</th>
|
|
1493
|
+
<th>Days of Supply</th>
|
|
1494
|
+
<th>Refills</th>
|
|
1495
|
+
<th>Start Date</th>
|
|
1496
|
+
</tr>
|
|
1497
|
+
</thead>
|
|
1498
|
+
<tbody>`;
|
|
1499
|
+
for (const resourceItem of resources) {
|
|
1500
|
+
for (const rowData of resourceItem.section ?? []) {
|
|
1501
|
+
const data = {};
|
|
1502
|
+
for (const columnData of rowData.section ?? []) {
|
|
1503
|
+
switch (columnData.title) {
|
|
1504
|
+
case "Medication Name":
|
|
1505
|
+
data["medication"] = columnData.text?.div ?? "";
|
|
1506
|
+
break;
|
|
1507
|
+
case "Status":
|
|
1508
|
+
data["status"] = columnData.text?.div ?? "";
|
|
1509
|
+
break;
|
|
1510
|
+
case "Prescriber Instruction":
|
|
1511
|
+
data["sig-prescriber"] = columnData.text?.div ?? "";
|
|
1512
|
+
break;
|
|
1513
|
+
case "Pharmacy Instruction":
|
|
1514
|
+
data["sig-pharmacy"] = columnData.text?.div ?? "";
|
|
1515
|
+
break;
|
|
1516
|
+
case "Days Of Supply":
|
|
1517
|
+
data["daysOfSupply"] = columnData.text?.div ?? "";
|
|
1518
|
+
break;
|
|
1519
|
+
case "Refills Remaining":
|
|
1520
|
+
data["refills"] = columnData.text?.div ?? "";
|
|
1521
|
+
break;
|
|
1522
|
+
case "Authored On Date":
|
|
1523
|
+
data["startDate"] = columnData.text?.div ?? "";
|
|
1524
|
+
break;
|
|
1525
|
+
default:
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (data["status"] === "active") {
|
|
1530
|
+
html += `
|
|
1531
|
+
<tr>
|
|
1532
|
+
<td>${data["medication"]}</td>
|
|
1533
|
+
<td>${data["sig-prescriber"] || data["sig-pharmacy"]}</td>
|
|
1534
|
+
<td>${data["daysOfSupply"]}</td>
|
|
1535
|
+
<td>${data["refills"]}</td>
|
|
1536
|
+
<td>${templateUtilities.renderTime(data["startDate"], timezone)}</td>
|
|
1537
|
+
</tr>`;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
html += `
|
|
1542
|
+
</tbody>
|
|
1543
|
+
</table>
|
|
1544
|
+
</div>`;
|
|
1545
|
+
return html;
|
|
1546
|
+
}
|
|
1403
1547
|
/**
|
|
1404
1548
|
* Safely parse a date string and return a valid Date object or null
|
|
1405
1549
|
* @param dateString - The date string to parse
|
|
@@ -1412,55 +1556,6 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1412
1556
|
const date = new Date(dateString);
|
|
1413
1557
|
return !isNaN(date.getTime()) ? date : null;
|
|
1414
1558
|
}
|
|
1415
|
-
/**
|
|
1416
|
-
* Determine if a MedicationRequest is active
|
|
1417
|
-
* @param medicationRequest - The MedicationRequest resource
|
|
1418
|
-
* @returns boolean indicating if the medication request is active
|
|
1419
|
-
*/
|
|
1420
|
-
static isActiveMedicationRequest(medicationRequest) {
|
|
1421
|
-
const status = medicationRequest.status?.toLowerCase();
|
|
1422
|
-
if (status === "active" || status === "unknown") {
|
|
1423
|
-
return true;
|
|
1424
|
-
}
|
|
1425
|
-
if (status === "completed" || status === "cancelled" || status === "stopped" || status === "draft") {
|
|
1426
|
-
return false;
|
|
1427
|
-
}
|
|
1428
|
-
const endDate = medicationRequest.dispenseRequest?.validityPeriod?.end;
|
|
1429
|
-
if (!endDate) {
|
|
1430
|
-
return true;
|
|
1431
|
-
}
|
|
1432
|
-
const parsedEndDate = this.parseDate(endDate);
|
|
1433
|
-
if (!parsedEndDate) {
|
|
1434
|
-
return true;
|
|
1435
|
-
}
|
|
1436
|
-
return parsedEndDate.getTime() > Date.now();
|
|
1437
|
-
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Determine if a MedicationStatement is active
|
|
1440
|
-
* @param medicationStatement - The MedicationStatement resource
|
|
1441
|
-
* @returns boolean indicating if the medication statement is active
|
|
1442
|
-
*/
|
|
1443
|
-
static isActiveMedicationStatement(medicationStatement) {
|
|
1444
|
-
const status = medicationStatement.status?.toLowerCase();
|
|
1445
|
-
if (status === "active" || status === "intended" || status === "unknown") {
|
|
1446
|
-
return true;
|
|
1447
|
-
}
|
|
1448
|
-
if (status === "completed" || status === "stopped" || status === "not-taken") {
|
|
1449
|
-
return false;
|
|
1450
|
-
}
|
|
1451
|
-
let endDate;
|
|
1452
|
-
if (medicationStatement.effectivePeriod?.end) {
|
|
1453
|
-
endDate = medicationStatement.effectivePeriod.end;
|
|
1454
|
-
}
|
|
1455
|
-
if (!endDate) {
|
|
1456
|
-
return true;
|
|
1457
|
-
}
|
|
1458
|
-
const parsedEndDate = this.parseDate(endDate);
|
|
1459
|
-
if (!parsedEndDate) {
|
|
1460
|
-
return true;
|
|
1461
|
-
}
|
|
1462
|
-
return parsedEndDate.getTime() > Date.now();
|
|
1463
|
-
}
|
|
1464
1559
|
/**
|
|
1465
1560
|
* Internal static implementation that actually generates the narrative
|
|
1466
1561
|
* @param resources - FHIR Medication resources
|
|
@@ -1474,20 +1569,11 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1474
1569
|
const medicationRequests = this.getMedicationRequests(templateUtilities, resources);
|
|
1475
1570
|
const medicationStatements = this.getMedicationStatements(templateUtilities, resources);
|
|
1476
1571
|
const allActiveMedications = [];
|
|
1477
|
-
const allInactiveMedications = [];
|
|
1478
1572
|
medicationRequests.forEach((mr) => {
|
|
1479
|
-
|
|
1480
|
-
allActiveMedications.push({ type: "request", resource: mr.resource, extension: mr.extension });
|
|
1481
|
-
} else {
|
|
1482
|
-
allInactiveMedications.push({ type: "request", resource: mr.resource, extension: mr.extension });
|
|
1483
|
-
}
|
|
1573
|
+
allActiveMedications.push({ type: "request", resource: mr.resource, extension: mr.extension });
|
|
1484
1574
|
});
|
|
1485
1575
|
medicationStatements.forEach((ms) => {
|
|
1486
|
-
|
|
1487
|
-
allActiveMedications.push({ type: "statement", resource: ms.resource, extension: ms.extension });
|
|
1488
|
-
} else {
|
|
1489
|
-
allInactiveMedications.push({ type: "statement", resource: ms.resource, extension: ms.extension });
|
|
1490
|
-
}
|
|
1576
|
+
allActiveMedications.push({ type: "statement", resource: ms.resource, extension: ms.extension });
|
|
1491
1577
|
});
|
|
1492
1578
|
const sortMedications = (medications) => {
|
|
1493
1579
|
medications.sort((a, b) => {
|
|
@@ -1517,11 +1603,7 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1517
1603
|
};
|
|
1518
1604
|
if (allActiveMedications.length > 0) {
|
|
1519
1605
|
sortMedications(allActiveMedications);
|
|
1520
|
-
html += this.renderCombinedMedications(templateUtilities, allActiveMedications
|
|
1521
|
-
}
|
|
1522
|
-
if (allInactiveMedications.length > 0) {
|
|
1523
|
-
sortMedications(allInactiveMedications);
|
|
1524
|
-
html += this.renderCombinedMedications(templateUtilities, allInactiveMedications, false);
|
|
1606
|
+
html += this.renderCombinedMedications(templateUtilities, allActiveMedications);
|
|
1525
1607
|
}
|
|
1526
1608
|
return html;
|
|
1527
1609
|
}
|
|
@@ -1559,12 +1641,10 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1559
1641
|
* Render HTML table for combined MedicationRequest and MedicationStatement resources
|
|
1560
1642
|
* @param templateUtilities - Instance of TemplateUtilities for utility functions
|
|
1561
1643
|
* @param medications - Array of combined medication resources
|
|
1562
|
-
* @param sectionTitle - Title for the section
|
|
1563
1644
|
* @returns HTML string for rendering
|
|
1564
1645
|
*/
|
|
1565
|
-
static renderCombinedMedications(templateUtilities, medications
|
|
1646
|
+
static renderCombinedMedications(templateUtilities, medications) {
|
|
1566
1647
|
let html = `
|
|
1567
|
-
<h3>${isActiveSection ? "Active Medications" : "Inactive Medications"}</h3>
|
|
1568
1648
|
<table>
|
|
1569
1649
|
<thead>
|
|
1570
1650
|
<tr>
|
|
@@ -1573,9 +1653,7 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1573
1653
|
<th>Sig</th>
|
|
1574
1654
|
<th>Dispense Quantity</th>
|
|
1575
1655
|
<th>Refills</th>
|
|
1576
|
-
<th>Start Date</th
|
|
1577
|
-
<th>End Date</th>`}
|
|
1578
|
-
<th>Status</th>
|
|
1656
|
+
<th>Start Date</th>
|
|
1579
1657
|
</tr>
|
|
1580
1658
|
</thead>
|
|
1581
1659
|
<tbody>`;
|
|
@@ -1587,12 +1665,9 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1587
1665
|
let dispenseQuantity = "-";
|
|
1588
1666
|
let refills = "-";
|
|
1589
1667
|
let startDate = "-";
|
|
1590
|
-
let endDate = "-";
|
|
1591
|
-
let status;
|
|
1592
1668
|
if (medication.type === "request") {
|
|
1593
1669
|
const mr = medication.resource;
|
|
1594
1670
|
type = "Request";
|
|
1595
|
-
status = mr.status ? String(mr.status) : "-";
|
|
1596
1671
|
medicationName = templateUtilities.getMedicationName(
|
|
1597
1672
|
mr.medicationReference || mr.medicationCodeableConcept
|
|
1598
1673
|
);
|
|
@@ -1606,14 +1681,12 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1606
1681
|
refills = mr.dispenseRequest?.numberOfRepeatsAllowed?.toString() || "-";
|
|
1607
1682
|
if (mr.dispenseRequest?.validityPeriod) {
|
|
1608
1683
|
startDate = mr.dispenseRequest.validityPeriod.start || "-";
|
|
1609
|
-
endDate = mr.dispenseRequest.validityPeriod.end || "-";
|
|
1610
1684
|
} else {
|
|
1611
1685
|
startDate = mr.authoredOn || "-";
|
|
1612
1686
|
}
|
|
1613
1687
|
} else {
|
|
1614
1688
|
const ms = medication.resource;
|
|
1615
1689
|
type = "Statement";
|
|
1616
|
-
status = ms.status ? String(ms.status) : "-";
|
|
1617
1690
|
medicationName = templateUtilities.getMedicationName(
|
|
1618
1691
|
ms.medicationReference || ms.medicationCodeableConcept
|
|
1619
1692
|
);
|
|
@@ -1622,7 +1695,6 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1622
1695
|
startDate = ms.effectiveDateTime;
|
|
1623
1696
|
} else if (ms.effectivePeriod) {
|
|
1624
1697
|
startDate = ms.effectivePeriod.start || "-";
|
|
1625
|
-
endDate = ms.effectivePeriod.end || "-";
|
|
1626
1698
|
}
|
|
1627
1699
|
}
|
|
1628
1700
|
html += `
|
|
@@ -1632,9 +1704,7 @@ var MedicationSummaryTemplate = class _MedicationSummaryTemplate {
|
|
|
1632
1704
|
<td>${sig}</td>
|
|
1633
1705
|
<td>${dispenseQuantity}</td>
|
|
1634
1706
|
<td>${refills}</td>
|
|
1635
|
-
<td>${startDate}</td
|
|
1636
|
-
<td>${endDate}</td>`}
|
|
1637
|
-
<td>${status}</td>
|
|
1707
|
+
<td>${startDate}</td>
|
|
1638
1708
|
</tr>`;
|
|
1639
1709
|
}
|
|
1640
1710
|
html += `
|
|
@@ -1775,6 +1845,7 @@ var ProblemListTemplate = class _ProblemListTemplate {
|
|
|
1775
1845
|
* @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
|
|
1776
1846
|
* @returns HTML string for rendering
|
|
1777
1847
|
*/
|
|
1848
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1778
1849
|
static generateStaticNarrative(resources, timezone) {
|
|
1779
1850
|
const templateUtilities = new TemplateUtilities(resources);
|
|
1780
1851
|
let html = ``;
|
|
@@ -1791,7 +1862,6 @@ var ProblemListTemplate = class _ProblemListTemplate {
|
|
|
1791
1862
|
<th>Problem</th>
|
|
1792
1863
|
<th>Onset Date</th>
|
|
1793
1864
|
<th>Recorded Date</th>
|
|
1794
|
-
<th>Notes</th>
|
|
1795
1865
|
</tr>
|
|
1796
1866
|
</thead>
|
|
1797
1867
|
<tbody>`;
|
|
@@ -1800,7 +1870,6 @@ var ProblemListTemplate = class _ProblemListTemplate {
|
|
|
1800
1870
|
<td class="Name">${templateUtilities.codeableConcept(cond.code)}</td>
|
|
1801
1871
|
<td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
|
|
1802
1872
|
<td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
|
|
1803
|
-
<td class="Notes">${templateUtilities.renderNotes(cond.note, timezone)}</td>
|
|
1804
1873
|
</tr>`;
|
|
1805
1874
|
}
|
|
1806
1875
|
html += `</tbody>
|
|
@@ -1809,13 +1878,6 @@ var ProblemListTemplate = class _ProblemListTemplate {
|
|
|
1809
1878
|
}
|
|
1810
1879
|
};
|
|
1811
1880
|
|
|
1812
|
-
// src/structures/ips_section_constants.ts
|
|
1813
|
-
var VITAL_SIGNS_SUMMARY_COMPONENT_MAP = {
|
|
1814
|
-
"Systolic Blood Pressure": "valueRatio.numerator.value",
|
|
1815
|
-
"Diastolic Blood Pressure": "valueRatio.denominator.value",
|
|
1816
|
-
"Default": "valueString"
|
|
1817
|
-
};
|
|
1818
|
-
|
|
1819
1881
|
// src/narratives/templates/typescript/VitalSignsTemplate.ts
|
|
1820
1882
|
var VitalSignsTemplate = class _VitalSignsTemplate {
|
|
1821
1883
|
/**
|
|
@@ -2235,6 +2297,7 @@ var PastHistoryOfIllnessTemplate = class {
|
|
|
2235
2297
|
* @param timezone - Optional timezone to use for date formatting (e.g., 'America/New_York', 'Europe/London')
|
|
2236
2298
|
* @returns HTML string for rendering
|
|
2237
2299
|
*/
|
|
2300
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2238
2301
|
generateNarrative(resources, timezone) {
|
|
2239
2302
|
const templateUtilities = new TemplateUtilities(resources);
|
|
2240
2303
|
let html = ``;
|
|
@@ -2252,7 +2315,6 @@ var PastHistoryOfIllnessTemplate = class {
|
|
|
2252
2315
|
<th>Onset Date</th>
|
|
2253
2316
|
<th>Recorded Date</th>
|
|
2254
2317
|
<th>Resolved Date</th>
|
|
2255
|
-
<th>Notes</th>
|
|
2256
2318
|
</tr>
|
|
2257
2319
|
</thead>
|
|
2258
2320
|
<tbody>`;
|
|
@@ -2262,7 +2324,6 @@ var PastHistoryOfIllnessTemplate = class {
|
|
|
2262
2324
|
<td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
|
|
2263
2325
|
<td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
|
|
2264
2326
|
<td class="ResolvedDate">${templateUtilities.renderDate(cond.abatementDateTime)}</td>
|
|
2265
|
-
<td class="Notes">${templateUtilities.renderNotes(cond.note, timezone)}</td>
|
|
2266
2327
|
</tr>`;
|
|
2267
2328
|
}
|
|
2268
2329
|
html += `</tbody>
|
|
@@ -2415,7 +2476,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2415
2476
|
<th>Problem</th>
|
|
2416
2477
|
<th>Onset Date</th>
|
|
2417
2478
|
<th>Recorded Date</th>
|
|
2418
|
-
<th>Notes</th>
|
|
2419
2479
|
</tr>
|
|
2420
2480
|
</thead>
|
|
2421
2481
|
<tbody>`;
|
|
@@ -2424,7 +2484,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2424
2484
|
<td class="Name">${templateUtilities.codeableConcept(cond.code)}</td>
|
|
2425
2485
|
<td class="OnsetDate">${templateUtilities.renderDate(cond.onsetDateTime)}</td>
|
|
2426
2486
|
<td class="RecordedDate">${templateUtilities.renderDate(cond.recordedDate)}</td>
|
|
2427
|
-
<td class="Notes">${templateUtilities.renderNotes(cond.note, timezone, { styled: true, warning: true })}</td>
|
|
2428
2487
|
</tr>`;
|
|
2429
2488
|
}
|
|
2430
2489
|
html += `</tbody>
|
|
@@ -2440,7 +2499,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2440
2499
|
<th>Description</th>
|
|
2441
2500
|
<th>Summary</th>
|
|
2442
2501
|
<th>Findings</th>
|
|
2443
|
-
<th>Notes</th>
|
|
2444
2502
|
</tr>
|
|
2445
2503
|
</thead>
|
|
2446
2504
|
<tbody>`;
|
|
@@ -2469,7 +2527,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2469
2527
|
}
|
|
2470
2528
|
findingsHtml += "</ul>";
|
|
2471
2529
|
}
|
|
2472
|
-
const notes = templateUtilities.renderNotes(impression.note, timezone);
|
|
2473
2530
|
html += `
|
|
2474
2531
|
<tr id="${templateUtilities.narrativeLinkId(impression)}">
|
|
2475
2532
|
<td>${formattedDate}</td>
|
|
@@ -2477,7 +2534,6 @@ var FunctionalStatusTemplate = class _FunctionalStatusTemplate {
|
|
|
2477
2534
|
<td>${impression.description || ""}</td>
|
|
2478
2535
|
<td>${impression.summary || ""}</td>
|
|
2479
2536
|
<td>${findingsHtml}</td>
|
|
2480
|
-
<td>${notes}</td>
|
|
2481
2537
|
</tr>`;
|
|
2482
2538
|
}
|
|
2483
2539
|
html += `</tbody>
|