@mieweb/ui 0.11.0-dev.29 → 0.11.0-dev.30
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/MIGRATION.md +67 -0
- package/README.md +10 -0
- package/dist/EncounterMdyEditor-3SQY4MK2.js +286 -0
- package/dist/EncounterMdyEditor-3SQY4MK2.js.map +1 -0
- package/dist/EncounterMdyEditor-73PTTIRY.cjs +288 -0
- package/dist/EncounterMdyEditor-73PTTIRY.cjs.map +1 -0
- package/dist/chunk-25MPUVTM.js +339 -0
- package/dist/chunk-25MPUVTM.js.map +1 -0
- package/dist/{chunk-2VISTX7O.js → chunk-3NOVISM6.js} +3 -3
- package/dist/{chunk-2VISTX7O.js.map → chunk-3NOVISM6.js.map} +1 -1
- package/dist/{chunk-6GKIISNZ.cjs → chunk-3Y3ZCDNF.cjs} +176 -63
- package/dist/chunk-3Y3ZCDNF.cjs.map +1 -0
- package/dist/{chunk-AK4LHJPC.js → chunk-4JXO5JTE.js} +84 -3
- package/dist/chunk-4JXO5JTE.js.map +1 -0
- package/dist/chunk-4NB4WOGU.cjs +666 -0
- package/dist/chunk-4NB4WOGU.cjs.map +1 -0
- package/dist/{chunk-434L5RYF.js → chunk-4P27Y52M.js} +3 -3
- package/dist/{chunk-434L5RYF.js.map → chunk-4P27Y52M.js.map} +1 -1
- package/dist/{chunk-PRA2V5BP.cjs → chunk-5KGYTNOA.cjs} +7 -7
- package/dist/{chunk-PRA2V5BP.cjs.map → chunk-5KGYTNOA.cjs.map} +1 -1
- package/dist/chunk-5LNLB2XT.js +634 -0
- package/dist/chunk-5LNLB2XT.js.map +1 -0
- package/dist/chunk-5YOT4DVH.js +31 -0
- package/dist/chunk-5YOT4DVH.js.map +1 -0
- package/dist/chunk-7HFZL5V5.js +2425 -0
- package/dist/chunk-7HFZL5V5.js.map +1 -0
- package/dist/{chunk-XZNHS7YQ.cjs → chunk-7XC6E4BT.cjs} +3 -3
- package/dist/{chunk-XZNHS7YQ.cjs.map → chunk-7XC6E4BT.cjs.map} +1 -1
- package/dist/{chunk-JM4YFLO4.js → chunk-AIJYZP7M.js} +3 -3
- package/dist/{chunk-JM4YFLO4.js.map → chunk-AIJYZP7M.js.map} +1 -1
- package/dist/chunk-BBLEGF5L.js +66 -0
- package/dist/chunk-BBLEGF5L.js.map +1 -0
- package/dist/{chunk-MBDT2MGR.js → chunk-BIQIE3VW.js} +4 -4
- package/dist/{chunk-MBDT2MGR.js.map → chunk-BIQIE3VW.js.map} +1 -1
- package/dist/chunk-FSLI5IKP.cjs +54 -0
- package/dist/chunk-FSLI5IKP.cjs.map +1 -0
- package/dist/chunk-GADJASST.js +3 -0
- package/dist/chunk-GADJASST.js.map +1 -0
- package/dist/chunk-IXUIOR4K.cjs +2457 -0
- package/dist/chunk-IXUIOR4K.cjs.map +1 -0
- package/dist/chunk-K2BY7YPJ.cjs +4 -0
- package/dist/chunk-K2BY7YPJ.cjs.map +1 -0
- package/dist/{chunk-H3P7POFC.cjs → chunk-MA3Y6AYA.cjs} +3 -3
- package/dist/{chunk-H3P7POFC.cjs.map → chunk-MA3Y6AYA.cjs.map} +1 -1
- package/dist/chunk-QO4A6U73.cjs +342 -0
- package/dist/chunk-QO4A6U73.cjs.map +1 -0
- package/dist/{chunk-43QBEF6N.js → chunk-QR7B6UPR.js} +176 -63
- package/dist/chunk-QR7B6UPR.js.map +1 -0
- package/dist/chunk-SMPLM6K3.js +4107 -0
- package/dist/chunk-SMPLM6K3.js.map +1 -0
- package/dist/chunk-SV6YGK4U.cjs +88 -0
- package/dist/chunk-SV6YGK4U.cjs.map +1 -0
- package/dist/{chunk-MQ5WJ3NO.js → chunk-UDUYZDTA.js} +14 -72
- package/dist/chunk-UDUYZDTA.js.map +1 -0
- package/dist/{chunk-ZLBNYCYT.cjs → chunk-XPVQI3MM.cjs} +84 -3
- package/dist/chunk-XPVQI3MM.cjs.map +1 -0
- package/dist/{chunk-AQUHEKXL.cjs → chunk-XQHDQ64G.cjs} +4 -4
- package/dist/{chunk-AQUHEKXL.cjs.map → chunk-XQHDQ64G.cjs.map} +1 -1
- package/dist/chunk-Z2OOJGHL.cjs +4155 -0
- package/dist/chunk-Z2OOJGHL.cjs.map +1 -0
- package/dist/{chunk-QKW2QCGI.cjs → chunk-ZK2R6UGF.cjs} +13 -72
- package/dist/chunk-ZK2R6UGF.cjs.map +1 -0
- package/dist/components/ActionPlan/index.cjs +4 -4
- package/dist/components/ActionPlan/index.js +2 -2
- package/dist/components/AlertDialog/index.d.cts +1 -1
- package/dist/components/AlertDialog/index.d.ts +1 -1
- package/dist/components/EmailEditor/index.cjs +13 -12
- package/dist/components/EmailEditor/index.js +4 -3
- package/dist/components/MediaEditor/index.cjs +7 -7
- package/dist/components/MediaEditor/index.js +2 -2
- package/dist/components/RichTextEditor/index.cjs +5 -4
- package/dist/components/RichTextEditor/index.d.cts +34 -0
- package/dist/components/RichTextEditor/index.d.ts +34 -0
- package/dist/components/RichTextEditor/index.js +2 -1
- package/dist/components/SuperChat/index.cjs +10 -9
- package/dist/components/SuperChat/index.cjs.map +1 -1
- package/dist/components/SuperChat/index.js +2 -1
- package/dist/components/SuperChat/index.js.map +1 -1
- package/dist/components/Textarea/index.cjs +6 -5
- package/dist/components/Textarea/index.js +3 -2
- package/dist/esheet.cjs +4237 -25
- package/dist/esheet.cjs.map +1 -1
- package/dist/esheet.d.cts +753 -6
- package/dist/esheet.d.ts +753 -6
- package/dist/esheet.js +4099 -21
- package/dist/esheet.js.map +1 -1
- package/dist/index.cjs +204 -3072
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -9
- package/dist/index.d.ts +71 -9
- package/dist/index.js +38 -2968
- package/dist/index.js.map +1 -1
- package/dist/kerebron.cjs +7 -268
- package/dist/kerebron.cjs.map +1 -1
- package/dist/kerebron.d.cts +7 -1
- package/dist/kerebron.d.ts +7 -1
- package/dist/kerebron.js +6 -270
- package/dist/kerebron.js.map +1 -1
- package/dist/model-WFFZRQMY.js +18 -0
- package/dist/model-WFFZRQMY.js.map +1 -0
- package/dist/model-Y7WRIGGA.cjs +79 -0
- package/dist/model-Y7WRIGGA.cjs.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-preset.cjs +4 -4
- package/dist/tailwind-preset.js +1 -1
- package/package.json +30 -9
- package/dist/chunk-43QBEF6N.js.map +0 -1
- package/dist/chunk-6GKIISNZ.cjs.map +0 -1
- package/dist/chunk-AK4LHJPC.js.map +0 -1
- package/dist/chunk-MQ5WJ3NO.js.map +0 -1
- package/dist/chunk-QKW2QCGI.cjs.map +0 -1
- package/dist/chunk-SFSBYC46.js +0 -1596
- package/dist/chunk-SFSBYC46.js.map +0 -1
- package/dist/chunk-ZIJMLRI3.cjs +0 -1633
- package/dist/chunk-ZIJMLRI3.cjs.map +0 -1
- package/dist/chunk-ZLBNYCYT.cjs.map +0 -1
|
@@ -0,0 +1,2425 @@
|
|
|
1
|
+
import { currentAssertion } from './chunk-5LNLB2XT.js';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import { fieldDisplay, render } from '@mieweb/templit';
|
|
4
|
+
import { markdownToHtml } from '@mieweb/templit/markdown';
|
|
5
|
+
import { parseDocument, isMap, isSeq } from 'yaml';
|
|
6
|
+
|
|
7
|
+
// src/components/EncounterVisit/date.ts
|
|
8
|
+
function isEncounterDate(value) {
|
|
9
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
10
|
+
return false;
|
|
11
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
12
|
+
if (year < 1 || month < 1 || month > 12 || day < 1) return false;
|
|
13
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
14
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
15
|
+
return day <= days[month - 1];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/components/EncounterVisit/definition.ts
|
|
19
|
+
function getEncounterFieldId(sectionId, observationId) {
|
|
20
|
+
const prefix = `encounter:${encodeURIComponent(sectionId)}`;
|
|
21
|
+
return observationId === void 0 ? `${prefix}:narrative` : `${prefix}:observation:${encodeURIComponent(observationId)}`;
|
|
22
|
+
}
|
|
23
|
+
function getEncounterSectionFieldId(sectionId) {
|
|
24
|
+
return `encounter:${encodeURIComponent(sectionId)}:section`;
|
|
25
|
+
}
|
|
26
|
+
function getEncounterSectionFieldIds(section) {
|
|
27
|
+
if (section.kind === "narrative") return [getEncounterFieldId(section.id)];
|
|
28
|
+
if (section.kind !== "observations") {
|
|
29
|
+
return [
|
|
30
|
+
...section.narrative ? [getEncounterFieldId(section.id)] : [],
|
|
31
|
+
getEncounterFieldId(section.id, "value")
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
return [
|
|
35
|
+
...section.narrative ? [getEncounterFieldId(section.id)] : [],
|
|
36
|
+
...(section.observations ?? []).map(
|
|
37
|
+
(observation) => getEncounterFieldId(section.id, observation.id)
|
|
38
|
+
)
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
var SECTION_KINDS = [
|
|
42
|
+
"narrative",
|
|
43
|
+
"observations",
|
|
44
|
+
"vitals",
|
|
45
|
+
"medications",
|
|
46
|
+
"allergies",
|
|
47
|
+
"conditions",
|
|
48
|
+
"plan",
|
|
49
|
+
"assessment"
|
|
50
|
+
];
|
|
51
|
+
function nonempty(value) {
|
|
52
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
53
|
+
}
|
|
54
|
+
function optionalString(value) {
|
|
55
|
+
return value === void 0 || typeof value === "string";
|
|
56
|
+
}
|
|
57
|
+
function invalid(message) {
|
|
58
|
+
throw new Error(`Invalid encounter definition: ${message}`);
|
|
59
|
+
}
|
|
60
|
+
function validateFindings(findings, location) {
|
|
61
|
+
if (findings === void 0) return;
|
|
62
|
+
if (!Array.isArray(findings))
|
|
63
|
+
invalid(`findings must be an array in ${location}`);
|
|
64
|
+
const ids = /* @__PURE__ */ new Set();
|
|
65
|
+
for (const finding of findings) {
|
|
66
|
+
if (!finding || typeof finding !== "object" || !nonempty(finding.id) || !nonempty(finding.label) || !nonempty(finding.text))
|
|
67
|
+
invalid(`findings require id, label and text in ${location}`);
|
|
68
|
+
if (ids.has(finding.id))
|
|
69
|
+
invalid(`duplicate finding id ${finding.id} in ${location}`);
|
|
70
|
+
ids.add(finding.id);
|
|
71
|
+
if (finding.code !== void 0 && (!finding.code || typeof finding.code !== "object" || !nonempty(finding.code.system) || !nonempty(finding.code.code) || !optionalString(finding.code.display)))
|
|
72
|
+
invalid(`invalid finding code in ${location}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function validateEncounterDefinition(definition) {
|
|
76
|
+
if (!definition || typeof definition !== "object")
|
|
77
|
+
invalid("expected an object");
|
|
78
|
+
if (!nonempty(definition.id) || !nonempty(definition.title)) {
|
|
79
|
+
invalid("id and title must be nonempty strings");
|
|
80
|
+
}
|
|
81
|
+
if (definition.encounterDate !== void 0 && !isEncounterDate(definition.encounterDate))
|
|
82
|
+
invalid("encounterDate must be a valid YYYY-MM-DD date");
|
|
83
|
+
if (!Array.isArray(definition.sections) || definition.sections.length === 0) {
|
|
84
|
+
invalid("at least one section is required");
|
|
85
|
+
}
|
|
86
|
+
const sectionIds = /* @__PURE__ */ new Set();
|
|
87
|
+
for (const section of definition.sections) {
|
|
88
|
+
if (!section || typeof section !== "object")
|
|
89
|
+
invalid("expected a section object");
|
|
90
|
+
if (!nonempty(section.id) || !nonempty(section.title)) {
|
|
91
|
+
invalid("each section requires an id and title");
|
|
92
|
+
}
|
|
93
|
+
if (sectionIds.has(section.id))
|
|
94
|
+
invalid(`duplicate section id ${section.id}`);
|
|
95
|
+
sectionIds.add(section.id);
|
|
96
|
+
if (!SECTION_KINDS.includes(section.kind))
|
|
97
|
+
invalid(`unknown section kind ${section.kind}`);
|
|
98
|
+
if (!optionalString(section.description))
|
|
99
|
+
invalid(`invalid description in ${section.id}`);
|
|
100
|
+
if (section.required !== void 0 && typeof section.required !== "boolean") {
|
|
101
|
+
invalid(`required must be boolean in ${section.id}`);
|
|
102
|
+
}
|
|
103
|
+
if (section.narrative !== void 0 && typeof section.narrative !== "boolean") {
|
|
104
|
+
invalid(`narrative must be boolean in ${section.id}`);
|
|
105
|
+
}
|
|
106
|
+
if (section.observations !== void 0 && !Array.isArray(section.observations)) {
|
|
107
|
+
invalid(`observations must be an array in ${section.id}`);
|
|
108
|
+
}
|
|
109
|
+
if (section.kind !== "observations" && section.observations?.length) {
|
|
110
|
+
invalid(
|
|
111
|
+
`only observations sections accept individual observations (${section.id})`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (section.kind === "observations" && !section.narrative && !section.observations?.length) {
|
|
115
|
+
invalid(`observations section ${section.id} has no fields`);
|
|
116
|
+
}
|
|
117
|
+
if (section.requireCoding !== void 0 && typeof section.requireCoding !== "boolean")
|
|
118
|
+
invalid(`requireCoding must be boolean in ${section.id}`);
|
|
119
|
+
validateFindings(section.findings, section.id);
|
|
120
|
+
if (section.findings?.length && section.kind !== "narrative" && !section.narrative)
|
|
121
|
+
invalid(`section findings require a narrative in ${section.id}`);
|
|
122
|
+
const observationIds = /* @__PURE__ */ new Set();
|
|
123
|
+
for (const observation of section.observations ?? []) {
|
|
124
|
+
if (!observation || typeof observation !== "object" || !nonempty(observation.id) || !nonempty(observation.label)) {
|
|
125
|
+
invalid(`observations in ${section.id} require an id and label`);
|
|
126
|
+
}
|
|
127
|
+
if (observationIds.has(observation.id))
|
|
128
|
+
invalid(`duplicate observation id ${observation.id} in ${section.id}`);
|
|
129
|
+
observationIds.add(observation.id);
|
|
130
|
+
if (observation.type !== void 0 && !["text", "number", "choice"].includes(observation.type)) {
|
|
131
|
+
invalid(`unknown observation type in ${section.id}.${observation.id}`);
|
|
132
|
+
}
|
|
133
|
+
if (!optionalString(observation.unit))
|
|
134
|
+
invalid(`invalid unit in ${section.id}.${observation.id}`);
|
|
135
|
+
if (observation.required !== void 0 && typeof observation.required !== "boolean") {
|
|
136
|
+
invalid(`required must be boolean in ${section.id}.${observation.id}`);
|
|
137
|
+
}
|
|
138
|
+
if (observation.code !== void 0 && (!observation.code || typeof observation.code !== "object" || !nonempty(observation.code.system) || !nonempty(observation.code.code) || !optionalString(observation.code.display))) {
|
|
139
|
+
invalid(`invalid code in ${section.id}.${observation.id}`);
|
|
140
|
+
}
|
|
141
|
+
if (observation.options !== void 0 && (!Array.isArray(observation.options) || observation.options.some((option) => !nonempty(option)) || new Set(observation.options).size !== observation.options.length)) {
|
|
142
|
+
invalid(
|
|
143
|
+
`options must be distinct nonempty strings in ${section.id}.${observation.id}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
validateFindings(observation.findings, `${section.id}.${observation.id}`);
|
|
147
|
+
if (observation.findings?.length && observation.type && observation.type !== "text")
|
|
148
|
+
invalid(
|
|
149
|
+
`findings require a text observation in ${section.id}.${observation.id}`
|
|
150
|
+
);
|
|
151
|
+
if (observation.type === "choice" && !observation.options?.length) {
|
|
152
|
+
invalid(
|
|
153
|
+
`choice observation ${section.id}.${observation.id} requires options`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function observationField(sectionId, observation) {
|
|
160
|
+
const base = {
|
|
161
|
+
id: getEncounterFieldId(sectionId, observation.id),
|
|
162
|
+
question: observation.label,
|
|
163
|
+
required: observation.required,
|
|
164
|
+
width: "full",
|
|
165
|
+
_sourceData: { ...observation, encounterFindings: observation.findings }
|
|
166
|
+
};
|
|
167
|
+
if (observation.type === "choice") {
|
|
168
|
+
return {
|
|
169
|
+
...base,
|
|
170
|
+
fieldType: "dropdown",
|
|
171
|
+
options: observation.options?.map((value) => ({
|
|
172
|
+
id: encodeURIComponent(value),
|
|
173
|
+
value,
|
|
174
|
+
text: value
|
|
175
|
+
}))
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (observation.type === "number") {
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
fieldType: "text",
|
|
182
|
+
inputType: "number",
|
|
183
|
+
unit: observation.unit,
|
|
184
|
+
validators: [
|
|
185
|
+
{ type: "number", message: "Enter a finite nonnegative number." }
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
...base,
|
|
191
|
+
fieldType: "encounterNarrative",
|
|
192
|
+
unit: observation.unit
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
var CUSTOM_FIELD_TYPES = {
|
|
196
|
+
vitals: "encounterVitals",
|
|
197
|
+
medications: "encounterMedications",
|
|
198
|
+
allergies: "encounterAllergies",
|
|
199
|
+
conditions: "encounterConditions",
|
|
200
|
+
plan: "encounterPlan",
|
|
201
|
+
assessment: "encounterAssessment"
|
|
202
|
+
};
|
|
203
|
+
function createEncounterFormDefinition(definition) {
|
|
204
|
+
validateEncounterDefinition(definition);
|
|
205
|
+
return {
|
|
206
|
+
id: definition.id,
|
|
207
|
+
title: definition.title,
|
|
208
|
+
pages: [
|
|
209
|
+
{
|
|
210
|
+
id: `${definition.id}:visit`,
|
|
211
|
+
title: definition.title,
|
|
212
|
+
fields: definition.sections.map((section) => {
|
|
213
|
+
const fields = [];
|
|
214
|
+
if (section.description) {
|
|
215
|
+
fields.push({
|
|
216
|
+
id: `${getEncounterSectionFieldId(section.id)}:description`,
|
|
217
|
+
fieldType: "display",
|
|
218
|
+
content: section.description,
|
|
219
|
+
width: "full"
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (section.kind === "narrative" || section.narrative) {
|
|
223
|
+
fields.push({
|
|
224
|
+
id: getEncounterFieldId(section.id),
|
|
225
|
+
fieldType: "encounterNarrative",
|
|
226
|
+
question: section.kind === "narrative" ? section.title : `${section.title} narrative`,
|
|
227
|
+
required: section.kind === "narrative" ? section.required : void 0,
|
|
228
|
+
width: "full",
|
|
229
|
+
_sourceData: {
|
|
230
|
+
encounterHideLabel: true,
|
|
231
|
+
encounterFindings: section.findings
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (section.kind === "observations") {
|
|
236
|
+
fields.push(
|
|
237
|
+
...(section.observations ?? []).map(
|
|
238
|
+
(observation) => observationField(section.id, observation)
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
} else if (section.kind !== "narrative") {
|
|
242
|
+
fields.push({
|
|
243
|
+
id: getEncounterFieldId(section.id, "value"),
|
|
244
|
+
fieldType: CUSTOM_FIELD_TYPES[section.kind],
|
|
245
|
+
question: section.title,
|
|
246
|
+
required: section.required,
|
|
247
|
+
_sourceData: {
|
|
248
|
+
encounterRequireCoding: section.requireCoding ?? true,
|
|
249
|
+
encounterDate: definition.encounterDate
|
|
250
|
+
},
|
|
251
|
+
width: "full"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
id: getEncounterSectionFieldId(section.id),
|
|
256
|
+
fieldType: "section",
|
|
257
|
+
title: section.title,
|
|
258
|
+
sectionCollapse: "expanded",
|
|
259
|
+
width: "full",
|
|
260
|
+
fields
|
|
261
|
+
};
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
var DEFAULT_ENCOUNTER_VISIT_DEFINITION = {
|
|
268
|
+
id: "anonymous-encounter",
|
|
269
|
+
title: "Encounter visit",
|
|
270
|
+
sections: [
|
|
271
|
+
{
|
|
272
|
+
id: "hpi",
|
|
273
|
+
title: "History of present illness",
|
|
274
|
+
kind: "narrative",
|
|
275
|
+
findings: [
|
|
276
|
+
{ id: "no-fever", label: "No fever", text: "Denies fever." },
|
|
277
|
+
{ id: "no-trauma", label: "No trauma", text: "Denies recent trauma." },
|
|
278
|
+
{
|
|
279
|
+
id: "no-radiation",
|
|
280
|
+
label: "No radiation",
|
|
281
|
+
text: "Pain does not radiate."
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "history",
|
|
287
|
+
title: "Patient history",
|
|
288
|
+
kind: "observations",
|
|
289
|
+
narrative: true,
|
|
290
|
+
findings: [
|
|
291
|
+
{
|
|
292
|
+
id: "patient-source",
|
|
293
|
+
label: "Patient provided history",
|
|
294
|
+
text: "History provided by the patient."
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: "records-reviewed",
|
|
298
|
+
label: "Records reviewed",
|
|
299
|
+
text: "History supplemented by available records."
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
observations: [
|
|
303
|
+
{
|
|
304
|
+
id: "medical",
|
|
305
|
+
label: "Medical history",
|
|
306
|
+
findings: [
|
|
307
|
+
{
|
|
308
|
+
id: "reviewed",
|
|
309
|
+
label: "History reviewed",
|
|
310
|
+
text: "Medical history reviewed."
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: "no-additional-history",
|
|
314
|
+
label: "No additional history",
|
|
315
|
+
text: "No additional medical history reported."
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "surgical",
|
|
321
|
+
label: "Surgical history",
|
|
322
|
+
findings: [
|
|
323
|
+
{
|
|
324
|
+
id: "no-surgeries",
|
|
325
|
+
label: "No prior surgery",
|
|
326
|
+
text: "No previous surgery reported."
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "reviewed",
|
|
330
|
+
label: "History reviewed",
|
|
331
|
+
text: "Surgical history reviewed."
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: "family",
|
|
337
|
+
label: "Family history",
|
|
338
|
+
findings: [
|
|
339
|
+
{
|
|
340
|
+
id: "unknown",
|
|
341
|
+
label: "History unknown",
|
|
342
|
+
text: "Family history unknown."
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "noncontributory",
|
|
346
|
+
label: "No pertinent history",
|
|
347
|
+
text: "No pertinent family history reported."
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: "social",
|
|
353
|
+
label: "Social history",
|
|
354
|
+
findings: [
|
|
355
|
+
{
|
|
356
|
+
id: "never-smoked",
|
|
357
|
+
label: "Never smoked",
|
|
358
|
+
text: "Never smoked."
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "no-alcohol",
|
|
362
|
+
label: "No alcohol use",
|
|
363
|
+
text: "Denies alcohol use."
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
id: "no-recreational-drugs",
|
|
367
|
+
label: "No recreational drug use",
|
|
368
|
+
text: "Denies recreational drug use."
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: "ros",
|
|
376
|
+
title: "Review of systems",
|
|
377
|
+
kind: "observations",
|
|
378
|
+
narrative: true,
|
|
379
|
+
findings: [
|
|
380
|
+
{
|
|
381
|
+
id: "pertinent-hpi",
|
|
382
|
+
label: "Additional symptoms in HPI",
|
|
383
|
+
text: "Additional symptoms are documented in the history of present illness."
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "limited-review",
|
|
387
|
+
label: "Limited review",
|
|
388
|
+
text: "Review of systems limited by patient condition."
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
observations: [
|
|
392
|
+
{
|
|
393
|
+
id: "constitutional",
|
|
394
|
+
label: "Constitutional",
|
|
395
|
+
findings: [
|
|
396
|
+
{
|
|
397
|
+
id: "no-fever-chills",
|
|
398
|
+
label: "No fever or chills",
|
|
399
|
+
text: "Denies fever or chills."
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: "cardiovascular",
|
|
405
|
+
label: "Cardiovascular",
|
|
406
|
+
findings: [
|
|
407
|
+
{
|
|
408
|
+
id: "no-chest-pain",
|
|
409
|
+
label: "No chest pain",
|
|
410
|
+
text: "Denies chest pain."
|
|
411
|
+
}
|
|
412
|
+
]
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: "respiratory",
|
|
416
|
+
label: "Respiratory",
|
|
417
|
+
findings: [
|
|
418
|
+
{
|
|
419
|
+
id: "no-dyspnea",
|
|
420
|
+
label: "No dyspnea",
|
|
421
|
+
text: "Denies shortness of breath."
|
|
422
|
+
},
|
|
423
|
+
{ id: "no-cough", label: "No cough", text: "Denies cough." }
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: "musculoskeletal",
|
|
428
|
+
label: "Musculoskeletal",
|
|
429
|
+
findings: [
|
|
430
|
+
{ id: "back-pain", label: "Back pain", text: "Reports back pain." },
|
|
431
|
+
{
|
|
432
|
+
id: "no-joint-swelling",
|
|
433
|
+
label: "No joint swelling",
|
|
434
|
+
text: "Denies joint swelling."
|
|
435
|
+
}
|
|
436
|
+
]
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: "neurological",
|
|
440
|
+
label: "Neurological",
|
|
441
|
+
findings: [
|
|
442
|
+
{
|
|
443
|
+
id: "no-weakness",
|
|
444
|
+
label: "No weakness",
|
|
445
|
+
text: "Denies weakness."
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: "no-numbness-tingling",
|
|
449
|
+
label: "No numbness / tingling",
|
|
450
|
+
text: "Denies numbness or tingling."
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: "no-bowel-bladder-changes",
|
|
454
|
+
label: "No bowel / bladder changes",
|
|
455
|
+
text: "Denies bowel or bladder changes."
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "vitals",
|
|
463
|
+
title: "Vitals",
|
|
464
|
+
kind: "vitals"
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: "exam",
|
|
468
|
+
title: "Physical examination",
|
|
469
|
+
kind: "observations",
|
|
470
|
+
narrative: true,
|
|
471
|
+
findings: [
|
|
472
|
+
{
|
|
473
|
+
id: "focused",
|
|
474
|
+
label: "Focused examination",
|
|
475
|
+
text: "Focused examination performed."
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: "limited-by-pain",
|
|
479
|
+
label: "Limited by pain",
|
|
480
|
+
text: "Examination limited by pain."
|
|
481
|
+
}
|
|
482
|
+
],
|
|
483
|
+
observations: [
|
|
484
|
+
{
|
|
485
|
+
id: "general",
|
|
486
|
+
label: "General appearance",
|
|
487
|
+
findings: [
|
|
488
|
+
{
|
|
489
|
+
id: "no-distress",
|
|
490
|
+
label: "No acute distress",
|
|
491
|
+
text: "No acute distress."
|
|
492
|
+
},
|
|
493
|
+
{ id: "alert", label: "Alert", text: "Alert and conversant." }
|
|
494
|
+
]
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: "head-neck",
|
|
498
|
+
label: "Head and neck",
|
|
499
|
+
findings: [
|
|
500
|
+
{
|
|
501
|
+
id: "no-cervical-nodes",
|
|
502
|
+
label: "No cervical lymphadenopathy",
|
|
503
|
+
text: "No palpable cervical lymphadenopathy."
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
id: "no-oral-lesions",
|
|
507
|
+
label: "No oral lesions",
|
|
508
|
+
text: "No visible oral lesions."
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
id: "cardiovascular",
|
|
514
|
+
label: "Cardiovascular",
|
|
515
|
+
findings: [
|
|
516
|
+
{
|
|
517
|
+
id: "regular",
|
|
518
|
+
label: "Regular rhythm",
|
|
519
|
+
text: "Regular rate and rhythm."
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: "respiratory",
|
|
525
|
+
label: "Respiratory",
|
|
526
|
+
findings: [
|
|
527
|
+
{
|
|
528
|
+
id: "clear",
|
|
529
|
+
label: "Clear lungs",
|
|
530
|
+
text: "Lungs clear to auscultation bilaterally."
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
id: "nonlabored",
|
|
534
|
+
label: "Nonlabored",
|
|
535
|
+
text: "Breathing is nonlabored."
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: "abdomen",
|
|
541
|
+
label: "Abdomen",
|
|
542
|
+
findings: [
|
|
543
|
+
{
|
|
544
|
+
id: "soft",
|
|
545
|
+
label: "Soft / nontender",
|
|
546
|
+
text: "Abdomen soft and nontender."
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
id: "musculoskeletal",
|
|
552
|
+
label: "Musculoskeletal / back",
|
|
553
|
+
findings: [
|
|
554
|
+
{
|
|
555
|
+
id: "no-midline-tenderness",
|
|
556
|
+
label: "No midline tenderness",
|
|
557
|
+
text: "No midline spinal tenderness."
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
id: "paraspinal-tenderness",
|
|
561
|
+
label: "Paraspinal tenderness",
|
|
562
|
+
text: "Paraspinal tenderness is present."
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: "range-of-motion",
|
|
566
|
+
label: "Preserved range of motion",
|
|
567
|
+
text: "Spinal range of motion is preserved."
|
|
568
|
+
}
|
|
569
|
+
]
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
id: "neurological",
|
|
573
|
+
label: "Neurological",
|
|
574
|
+
findings: [
|
|
575
|
+
{
|
|
576
|
+
id: "symmetric-strength",
|
|
577
|
+
label: "Symmetric leg strength",
|
|
578
|
+
text: "Strength symmetric in both lower extremities."
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: "light-touch",
|
|
582
|
+
label: "Intact light touch",
|
|
583
|
+
text: "Sensation intact to light touch in both lower extremities."
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
id: "steady-gait",
|
|
587
|
+
label: "Steady gait",
|
|
588
|
+
text: "Gait is steady."
|
|
589
|
+
}
|
|
590
|
+
]
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
id: "skin",
|
|
594
|
+
label: "Skin",
|
|
595
|
+
findings: [
|
|
596
|
+
{ id: "warm-dry", label: "Warm / dry", text: "Skin warm and dry." }
|
|
597
|
+
]
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
id: "conditions",
|
|
603
|
+
title: "Conditions",
|
|
604
|
+
kind: "conditions",
|
|
605
|
+
requireCoding: true
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "medications",
|
|
609
|
+
title: "Presenting medications",
|
|
610
|
+
kind: "medications",
|
|
611
|
+
requireCoding: true
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
id: "allergies",
|
|
615
|
+
title: "Allergies and intolerances",
|
|
616
|
+
kind: "allergies",
|
|
617
|
+
requireCoding: true
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
id: "assessment",
|
|
621
|
+
title: "Assessment and orders",
|
|
622
|
+
kind: "assessment",
|
|
623
|
+
requireCoding: true
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
id: "plan",
|
|
627
|
+
title: "Plan",
|
|
628
|
+
kind: "plan",
|
|
629
|
+
narrative: true,
|
|
630
|
+
requireCoding: true,
|
|
631
|
+
findings: [
|
|
632
|
+
{
|
|
633
|
+
id: "questions",
|
|
634
|
+
label: "Questions answered",
|
|
635
|
+
text: "Questions were answered."
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: "understanding",
|
|
639
|
+
label: "Understanding confirmed",
|
|
640
|
+
text: "Patient verbalized understanding of the plan."
|
|
641
|
+
}
|
|
642
|
+
]
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: "follow-up",
|
|
646
|
+
title: "Follow-up and appointments",
|
|
647
|
+
kind: "observations",
|
|
648
|
+
narrative: true,
|
|
649
|
+
findings: [
|
|
650
|
+
{
|
|
651
|
+
id: "arrangements-reviewed",
|
|
652
|
+
label: "Arrangements reviewed",
|
|
653
|
+
text: "Follow-up arrangements reviewed with the patient."
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
id: "patient-to-schedule",
|
|
657
|
+
label: "Patient will schedule",
|
|
658
|
+
text: "Patient will contact the office to schedule follow-up."
|
|
659
|
+
}
|
|
660
|
+
],
|
|
661
|
+
observations: [
|
|
662
|
+
{
|
|
663
|
+
id: "interval",
|
|
664
|
+
label: "Return interval",
|
|
665
|
+
findings: [
|
|
666
|
+
{ id: "one-week", label: "1 week", text: "1 week" },
|
|
667
|
+
{ id: "one-month", label: "1 month", text: "1 month" },
|
|
668
|
+
{ id: "three-months", label: "3 months", text: "3 months" }
|
|
669
|
+
]
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
id: "appointment",
|
|
673
|
+
label: "Appointment details",
|
|
674
|
+
findings: [
|
|
675
|
+
{
|
|
676
|
+
id: "scheduled",
|
|
677
|
+
label: "Appointment scheduled",
|
|
678
|
+
text: "Appointment scheduled."
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
id: "patient-to-schedule",
|
|
682
|
+
label: "Patient to schedule",
|
|
683
|
+
text: "Patient to schedule appointment."
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: "scheduling-pending",
|
|
687
|
+
label: "Scheduling pending",
|
|
688
|
+
text: "Appointment scheduling pending."
|
|
689
|
+
}
|
|
690
|
+
]
|
|
691
|
+
}
|
|
692
|
+
]
|
|
693
|
+
}
|
|
694
|
+
]
|
|
695
|
+
};
|
|
696
|
+
var EXAMPLE_ENCOUNTER_VISIT_RESPONSES = {
|
|
697
|
+
[getEncounterFieldId("hpi")]: {
|
|
698
|
+
answer: "45-year-old male with pre-diabetes, back pain and hypertension."
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
var ENCOUNTER_DOCUMENT_FIELD_ID = "__encounter_document__";
|
|
702
|
+
function record(value) {
|
|
703
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
704
|
+
}
|
|
705
|
+
function hasValue(value) {
|
|
706
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
707
|
+
if (typeof value === "number" || typeof value === "boolean") return true;
|
|
708
|
+
if (Array.isArray(value)) return value.some(hasValue);
|
|
709
|
+
if (record(value)) return Object.values(value).some(hasValue);
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
function hasSelectedValue(selected) {
|
|
713
|
+
if (Array.isArray(selected)) return selected.some(hasSelectedValue);
|
|
714
|
+
if (!record(selected)) return false;
|
|
715
|
+
if ("value" in selected) return hasValue(selected.value);
|
|
716
|
+
return Object.values(selected).some(hasSelectedValue);
|
|
717
|
+
}
|
|
718
|
+
function hasNativeValue(response, parseAnswer = false) {
|
|
719
|
+
return response?.answer !== void 0 && hasValue(parseAnswer ? canonicalValue(response) : response.answer) || hasSelectedValue(response?.selected);
|
|
720
|
+
}
|
|
721
|
+
function linkId(fieldId) {
|
|
722
|
+
return `visit_${Array.from(
|
|
723
|
+
new TextEncoder().encode(fieldId),
|
|
724
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
725
|
+
).join("")}`;
|
|
726
|
+
}
|
|
727
|
+
function unitDisplay(unit) {
|
|
728
|
+
return unit === "Cel" ? "\xB0C" : unit === "mm[Hg]" ? "mmHg" : unit === "{score}" ? "/10" : unit;
|
|
729
|
+
}
|
|
730
|
+
function observationDisplay(observation) {
|
|
731
|
+
const unit = unitDisplay(observation.unit);
|
|
732
|
+
return `${observation.value}${unit ? ` ${unit}` : ""}`;
|
|
733
|
+
}
|
|
734
|
+
function groupedDisplay(observations, showReadingId) {
|
|
735
|
+
const groups = /* @__PURE__ */ new Map();
|
|
736
|
+
for (const observation of observations) {
|
|
737
|
+
const key = observation.groupId ?? "";
|
|
738
|
+
const group = groups.get(key) ?? [];
|
|
739
|
+
group.push(observation);
|
|
740
|
+
groups.set(key, group);
|
|
741
|
+
}
|
|
742
|
+
return Array.from(groups, ([groupId, values]) => {
|
|
743
|
+
const first = values[0];
|
|
744
|
+
const metadata = [
|
|
745
|
+
showReadingId ? groupId : void 0,
|
|
746
|
+
first.recordedAt,
|
|
747
|
+
first.position,
|
|
748
|
+
first.site
|
|
749
|
+
].filter(Boolean);
|
|
750
|
+
const summary = values.map((value) => `${value.label}: ${observationDisplay(value)}`).join("\n");
|
|
751
|
+
return metadata.length ? `${summary} (${metadata.join("; ")})` : summary;
|
|
752
|
+
}).join("\n\n");
|
|
753
|
+
}
|
|
754
|
+
function getEncounterMdyFields(snapshot) {
|
|
755
|
+
return snapshot.definition.sections.flatMap(
|
|
756
|
+
(section) => getEncounterSectionFieldIds(section).map((fieldId) => {
|
|
757
|
+
const observations = snapshot.observations.filter(
|
|
758
|
+
(observation) => observation.fieldId === fieldId
|
|
759
|
+
);
|
|
760
|
+
const observationDefinition = section.observations?.find(
|
|
761
|
+
(observation) => getEncounterFieldId(section.id, observation.id) === fieldId
|
|
762
|
+
);
|
|
763
|
+
const narrative = fieldId === getEncounterFieldId(section.id);
|
|
764
|
+
const label = narrative ? section.kind === "narrative" ? section.title : `${section.title} narrative` : observationDefinition?.label ?? section.title;
|
|
765
|
+
const structured = !narrative && section.kind !== "observations";
|
|
766
|
+
let display;
|
|
767
|
+
if (structured) {
|
|
768
|
+
display = groupedDisplay(observations, section.kind === "vitals");
|
|
769
|
+
} else if (observations.length) {
|
|
770
|
+
display = observations.map(observationDisplay).join("; ");
|
|
771
|
+
} else {
|
|
772
|
+
display = fieldDisplay({ ...snapshot.responses[fieldId] });
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
id: linkId(fieldId),
|
|
776
|
+
fieldId,
|
|
777
|
+
sectionId: section.id,
|
|
778
|
+
label,
|
|
779
|
+
display: display.trim() ? display : "\u2014",
|
|
780
|
+
empty: structured ? observations.length === 0 || !display.trim() : !hasNativeValue(snapshot.responses[fieldId])
|
|
781
|
+
};
|
|
782
|
+
})
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
function markdownText(value) {
|
|
786
|
+
return value.replace(/&/g, "&").replace(/\r?\n/g, " \xB7 ").replace(/[\\[\]`*_~<>]/g, "\\$&");
|
|
787
|
+
}
|
|
788
|
+
function fieldLink(field) {
|
|
789
|
+
return `[${markdownDisplay(field.display)}](mdy:${field.id})`;
|
|
790
|
+
}
|
|
791
|
+
function markdownDisplay(value) {
|
|
792
|
+
return value.split(/\r?\n/).map(markdownText).join("<br>");
|
|
793
|
+
}
|
|
794
|
+
function initialBody(snapshot, fields) {
|
|
795
|
+
return [
|
|
796
|
+
`# ${markdownText(snapshot.definition.title)}`,
|
|
797
|
+
...snapshot.definition.sections.map(
|
|
798
|
+
(section) => [
|
|
799
|
+
`## ${markdownText(section.title)}`,
|
|
800
|
+
fields.filter((field) => field.sectionId === section.id).map(
|
|
801
|
+
(field) => section.kind === "narrative" ? fieldLink(field) : `- **${markdownText(field.label)}:** ${fieldLink(field)}`
|
|
802
|
+
).join("\n")
|
|
803
|
+
].join("\n\n")
|
|
804
|
+
)
|
|
805
|
+
].join("\n\n");
|
|
806
|
+
}
|
|
807
|
+
function fieldLinkSpans(body) {
|
|
808
|
+
const links = [];
|
|
809
|
+
const closingBrackets = /* @__PURE__ */ new Map();
|
|
810
|
+
const openingBrackets = [];
|
|
811
|
+
for (let cursor = 0; cursor < body.length; cursor += 1) {
|
|
812
|
+
if (body[cursor] === "\\") cursor += 1;
|
|
813
|
+
else if (body[cursor] === "[") openingBrackets.push(cursor);
|
|
814
|
+
else if (body[cursor] === "]") {
|
|
815
|
+
const opening = openingBrackets.pop();
|
|
816
|
+
if (opening !== void 0) closingBrackets.set(opening, cursor);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
const ticksByLength = /* @__PURE__ */ new Map();
|
|
820
|
+
for (let cursor = 0; cursor < body.length; cursor += 1) {
|
|
821
|
+
if (body[cursor] !== "`") continue;
|
|
822
|
+
const start = cursor;
|
|
823
|
+
while (body[cursor + 1] === "`") cursor += 1;
|
|
824
|
+
const length = cursor - start + 1;
|
|
825
|
+
const runs = ticksByLength.get(length) ?? [];
|
|
826
|
+
runs.push(start);
|
|
827
|
+
ticksByLength.set(length, runs);
|
|
828
|
+
}
|
|
829
|
+
const tickCursors = /* @__PURE__ */ new Map();
|
|
830
|
+
let noMoreTagEnds = false;
|
|
831
|
+
let index = 0;
|
|
832
|
+
while (index < body.length) {
|
|
833
|
+
const lineStart = index === 0 || body[index - 1] === "\n";
|
|
834
|
+
if (lineStart) {
|
|
835
|
+
const rest = body.slice(index);
|
|
836
|
+
const fence = /^ {0,3}(`{3,}|~{3,})[^\n]*(?:\n|$)/.exec(rest);
|
|
837
|
+
if (fence) {
|
|
838
|
+
const marker = fence[1][0];
|
|
839
|
+
const close = new RegExp(
|
|
840
|
+
`^ {0,3}${marker}{${fence[1].length},}[ \\t]*\\r?$`,
|
|
841
|
+
"m"
|
|
842
|
+
).exec(rest.slice(fence[0].length));
|
|
843
|
+
index += close ? fence[0].length + close.index + close[0].length : rest.length;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (/^(?: {4}|\t)/.test(rest)) {
|
|
847
|
+
const end = body.indexOf("\n", index);
|
|
848
|
+
index = end < 0 ? body.length : end + 1;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (body.startsWith("<!--", index)) {
|
|
853
|
+
const end = body.indexOf("-->", index + 4);
|
|
854
|
+
index = end < 0 ? body.length : end + 3;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const htmlPrefix = !noMoreTagEnds && body[index] === "<" ? /^<(pre|code|script|style)\b/i.exec(body.slice(index, index + 8)) : null;
|
|
858
|
+
if (htmlPrefix) {
|
|
859
|
+
const tagEnd = body.indexOf(">", index + htmlPrefix[0].length);
|
|
860
|
+
if (tagEnd < 0) noMoreTagEnds = true;
|
|
861
|
+
else {
|
|
862
|
+
const contentStart = tagEnd + 1;
|
|
863
|
+
const end = new RegExp(`</${htmlPrefix[1]}\\s*>`, "i").exec(
|
|
864
|
+
body.slice(contentStart)
|
|
865
|
+
);
|
|
866
|
+
index = end ? contentStart + end.index + end[0].length : body.length;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (body[index] === "\\") {
|
|
871
|
+
index += 2;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (body[index] === "`") {
|
|
875
|
+
let ticksEnd = index + 1;
|
|
876
|
+
while (body[ticksEnd] === "`") ticksEnd += 1;
|
|
877
|
+
const length = ticksEnd - index;
|
|
878
|
+
const runs = ticksByLength.get(length) ?? [];
|
|
879
|
+
let cursor = tickCursors.get(length) ?? 0;
|
|
880
|
+
while (cursor < runs.length && runs[cursor] < ticksEnd) cursor += 1;
|
|
881
|
+
tickCursors.set(length, cursor);
|
|
882
|
+
const closing2 = runs[cursor];
|
|
883
|
+
index = closing2 === void 0 ? ticksEnd : closing2 + length;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (body[index] !== "[" || body[index - 1] === "!") {
|
|
887
|
+
index += 1;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const labelEnd = closingBrackets.get(index);
|
|
891
|
+
if (labelEnd === void 0) {
|
|
892
|
+
index += 1;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const closing = labelEnd + 1;
|
|
896
|
+
const destination = /^\([ \t]*(?:<(?:mdy:|#)([a-z0-9_-]+)>|(?:mdy:|#)([a-z0-9_-]+))(?:[ \t]+(?:"[^"\n]*"|'[^'\n]*'))?[ \t]*\)/.exec(
|
|
897
|
+
body.slice(closing)
|
|
898
|
+
);
|
|
899
|
+
if (destination) {
|
|
900
|
+
const end = closing + destination[0].length;
|
|
901
|
+
links.push({
|
|
902
|
+
id: destination[1] ?? destination[2],
|
|
903
|
+
start: index,
|
|
904
|
+
labelEnd,
|
|
905
|
+
end
|
|
906
|
+
});
|
|
907
|
+
index = end;
|
|
908
|
+
} else {
|
|
909
|
+
index += 1;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return links;
|
|
913
|
+
}
|
|
914
|
+
function refreshEncounterMdyLinks(body, fields) {
|
|
915
|
+
const byId = new Map(fields.map((field) => [field.id, field]));
|
|
916
|
+
let result = body;
|
|
917
|
+
for (const link of fieldLinkSpans(body).reverse()) {
|
|
918
|
+
const field = byId.get(link.id);
|
|
919
|
+
if (!field) continue;
|
|
920
|
+
result = result.slice(0, link.start + 1) + markdownDisplay(field.display) + result.slice(link.labelEnd);
|
|
921
|
+
}
|
|
922
|
+
return result;
|
|
923
|
+
}
|
|
924
|
+
function splitSource(source) {
|
|
925
|
+
const opening = /^\uFEFF?---[ \t]*\r?\n/.exec(source);
|
|
926
|
+
if (!opening) return void 0;
|
|
927
|
+
const remainder = source.slice(opening[0].length);
|
|
928
|
+
const closing = /(^|\r?\n)---[ \t]*(\r?\n|$)/.exec(remainder);
|
|
929
|
+
if (!closing) return void 0;
|
|
930
|
+
const yamlEnd = closing.index + closing[1].length;
|
|
931
|
+
const end = opening[0].length + closing.index + closing[0].length;
|
|
932
|
+
return {
|
|
933
|
+
frontMatterSource: source.slice(0, end),
|
|
934
|
+
yamlSource: remainder.slice(0, yamlEnd),
|
|
935
|
+
body: source.slice(end),
|
|
936
|
+
opening: opening[0],
|
|
937
|
+
closing: closing[0].slice(closing[1].length)
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function fieldsFromFrontMatter(frontMatter) {
|
|
941
|
+
return Object.entries(frontMatter).flatMap(
|
|
942
|
+
([id, value]) => /^[a-z0-9_-]+$/.test(id) && record(value) && typeof value.fieldId === "string" && typeof value.sectionId === "string" && typeof value.display === "string" ? [
|
|
943
|
+
{
|
|
944
|
+
id,
|
|
945
|
+
fieldId: value.fieldId,
|
|
946
|
+
sectionId: value.sectionId,
|
|
947
|
+
label: typeof value.label === "string" ? value.label : value.fieldId,
|
|
948
|
+
display: fieldDisplay(value)
|
|
949
|
+
}
|
|
950
|
+
] : []
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
function validNativeForm(value) {
|
|
954
|
+
if (!record(value) || typeof value.id !== "string" || !Array.isArray(value.pages))
|
|
955
|
+
return false;
|
|
956
|
+
const ids = /* @__PURE__ */ new Set();
|
|
957
|
+
const fieldsValid = (fields) => Array.isArray(fields) && fields.every((field) => {
|
|
958
|
+
if (!record(field) || typeof field.id !== "string" || !field.id || typeof field.fieldType !== "string" || ids.has(field.id))
|
|
959
|
+
return false;
|
|
960
|
+
ids.add(field.id);
|
|
961
|
+
return field.fields === void 0 || fieldsValid(field.fields);
|
|
962
|
+
});
|
|
963
|
+
return value.pages.every(
|
|
964
|
+
(page) => record(page) && typeof page.id === "string" && fieldsValid(page.fields)
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
function validNativeResponses(value) {
|
|
968
|
+
return record(value) && Object.values(value).every(
|
|
969
|
+
(response) => record(response) && (response.answer === void 0 || typeof response.answer === "string") && (response.selected === void 0 || record(response.selected) || Array.isArray(response.selected))
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
function hasTemplateSyntax(body) {
|
|
973
|
+
for (const [opener, closer] of [
|
|
974
|
+
["{{", "}}"],
|
|
975
|
+
["{%", "%}"]
|
|
976
|
+
]) {
|
|
977
|
+
const start = body.indexOf(opener);
|
|
978
|
+
if (start >= 0 && body.indexOf(closer, start + opener.length) >= 0)
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
function parseEncounterMdy(source) {
|
|
984
|
+
const split = splitSource(source);
|
|
985
|
+
const result = {
|
|
986
|
+
source,
|
|
987
|
+
frontMatterSource: split?.frontMatterSource ?? "",
|
|
988
|
+
body: split?.body ?? source,
|
|
989
|
+
frontMatter: null,
|
|
990
|
+
diagnostics: []
|
|
991
|
+
};
|
|
992
|
+
const error = (code, message) => result.diagnostics.push({ severity: "error", code, message });
|
|
993
|
+
if (!split) {
|
|
994
|
+
error(
|
|
995
|
+
"frontmatter",
|
|
996
|
+
"An encounter MDY document requires YAML front matter."
|
|
997
|
+
);
|
|
998
|
+
return result;
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const document = parseDocument(split.yamlSource, { uniqueKeys: true });
|
|
1002
|
+
if (document.errors.length || document.warnings.length) {
|
|
1003
|
+
throw new Error("Front matter must contain valid safe YAML.");
|
|
1004
|
+
}
|
|
1005
|
+
const data2 = document.toJS({ maxAliasCount: 100 });
|
|
1006
|
+
if (!record(data2)) throw new Error("Front matter must be a YAML mapping.");
|
|
1007
|
+
result.frontMatter = data2;
|
|
1008
|
+
} catch (cause) {
|
|
1009
|
+
error(
|
|
1010
|
+
"frontmatter",
|
|
1011
|
+
cause instanceof Error ? cause.message : "Invalid YAML front matter."
|
|
1012
|
+
);
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|
|
1015
|
+
const data = result.frontMatter;
|
|
1016
|
+
if (validNativeForm(data.form)) result.form = data.form;
|
|
1017
|
+
else error("form", "Front matter requires a native eSheet form definition.");
|
|
1018
|
+
if (validNativeResponses(data.response)) result.response = data.response;
|
|
1019
|
+
else error("response", "Front matter requires a native eSheet response map.");
|
|
1020
|
+
if (data.encounterDefinition !== void 0) {
|
|
1021
|
+
try {
|
|
1022
|
+
validateEncounterDefinition(
|
|
1023
|
+
data.encounterDefinition
|
|
1024
|
+
);
|
|
1025
|
+
result.definition = data.encounterDefinition;
|
|
1026
|
+
} catch {
|
|
1027
|
+
error(
|
|
1028
|
+
"definition",
|
|
1029
|
+
"Front matter contains an invalid encounterDefinition."
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const fields = fieldsFromFrontMatter(data);
|
|
1034
|
+
const byId = new Map(fields.map((field) => [field.id, field]));
|
|
1035
|
+
const linked = /* @__PURE__ */ new Set();
|
|
1036
|
+
for (const link of fieldLinkSpans(result.body)) {
|
|
1037
|
+
if (!byId.has(link.id)) {
|
|
1038
|
+
if (link.id.startsWith("visit_") || result.body.slice(link.labelEnd, link.end).includes("mdy:"))
|
|
1039
|
+
result.diagnostics.push({
|
|
1040
|
+
severity: "error",
|
|
1041
|
+
code: "dangling-link",
|
|
1042
|
+
message: `Field link ${link.id} has no indexed data.`
|
|
1043
|
+
});
|
|
1044
|
+
} else linked.add(link.id);
|
|
1045
|
+
}
|
|
1046
|
+
for (const field of fields) {
|
|
1047
|
+
if (!linked.has(field.id) && field.display !== "\u2014" && field.display.trim())
|
|
1048
|
+
result.diagnostics.push({
|
|
1049
|
+
severity: "warning",
|
|
1050
|
+
code: "orphan",
|
|
1051
|
+
message: `${field.label} is recorded without a narrative field link.`,
|
|
1052
|
+
fieldId: field.fieldId
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
if (hasTemplateSyntax(result.body))
|
|
1056
|
+
result.diagnostics.push({
|
|
1057
|
+
severity: "warning",
|
|
1058
|
+
code: "template-syntax",
|
|
1059
|
+
message: "Template syntax belongs in .mdyt; document prose is displayed literally."
|
|
1060
|
+
});
|
|
1061
|
+
return result;
|
|
1062
|
+
}
|
|
1063
|
+
function getEncounterDocumentBody(responses) {
|
|
1064
|
+
return responses[ENCOUNTER_DOCUMENT_FIELD_ID]?.answer;
|
|
1065
|
+
}
|
|
1066
|
+
function updateEncounterDocumentBody(responses, body, options = {}) {
|
|
1067
|
+
return {
|
|
1068
|
+
...responses,
|
|
1069
|
+
[ENCOUNTER_DOCUMENT_FIELD_ID]: {
|
|
1070
|
+
...responses[ENCOUNTER_DOCUMENT_FIELD_ID],
|
|
1071
|
+
answer: body,
|
|
1072
|
+
...options
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function encounterDocumentResponse(source) {
|
|
1077
|
+
const parsed = parseEncounterMdy(source);
|
|
1078
|
+
return {
|
|
1079
|
+
answer: parsed.body,
|
|
1080
|
+
attributes: { mdyFrontMatterSource: parsed.frontMatterSource }
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function canonicalValue(response) {
|
|
1084
|
+
if (response?.answer !== void 0) {
|
|
1085
|
+
try {
|
|
1086
|
+
return JSON.parse(response.answer);
|
|
1087
|
+
} catch {
|
|
1088
|
+
return response.answer;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return response?.selected ?? null;
|
|
1092
|
+
}
|
|
1093
|
+
var FORM_PROPERTIES = /* @__PURE__ */ new Set(["id", "title", "pages"]);
|
|
1094
|
+
var PAGE_PROPERTIES = /* @__PURE__ */ new Set(["id", "title", "fields"]);
|
|
1095
|
+
var FIELD_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1096
|
+
"id",
|
|
1097
|
+
"fieldType",
|
|
1098
|
+
"title",
|
|
1099
|
+
"question",
|
|
1100
|
+
"required",
|
|
1101
|
+
"width",
|
|
1102
|
+
"content",
|
|
1103
|
+
"fields",
|
|
1104
|
+
"sectionCollapse",
|
|
1105
|
+
"options",
|
|
1106
|
+
"inputType",
|
|
1107
|
+
"unit",
|
|
1108
|
+
"validators",
|
|
1109
|
+
"_sourceData"
|
|
1110
|
+
]);
|
|
1111
|
+
var SOURCE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1112
|
+
"id",
|
|
1113
|
+
"label",
|
|
1114
|
+
"type",
|
|
1115
|
+
"unit",
|
|
1116
|
+
"code",
|
|
1117
|
+
"options",
|
|
1118
|
+
"required",
|
|
1119
|
+
"encounterHideLabel",
|
|
1120
|
+
"findings",
|
|
1121
|
+
"encounterFindings",
|
|
1122
|
+
"encounterRequireCoding",
|
|
1123
|
+
"encounterDate"
|
|
1124
|
+
]);
|
|
1125
|
+
function mergeMetadata(previous, next, owned) {
|
|
1126
|
+
const merged = { ...record(previous) ? previous : {}, ...next };
|
|
1127
|
+
for (const property of owned) {
|
|
1128
|
+
if (!(property in next)) delete merged[property];
|
|
1129
|
+
}
|
|
1130
|
+
return merged;
|
|
1131
|
+
}
|
|
1132
|
+
function mergeNativeFormMetadata(previous, next) {
|
|
1133
|
+
if (!record(previous) || previous.id !== next.id) return next;
|
|
1134
|
+
const mergeFields = (oldFields, newFields) => {
|
|
1135
|
+
const oldById = new Map(
|
|
1136
|
+
(Array.isArray(oldFields) ? oldFields : []).filter(record).map((field) => [field.id, field])
|
|
1137
|
+
);
|
|
1138
|
+
return newFields.filter(record).map((field) => {
|
|
1139
|
+
const oldField = oldById.get(field.id);
|
|
1140
|
+
const merged2 = mergeMetadata(oldField, field, FIELD_PROPERTIES);
|
|
1141
|
+
if (Array.isArray(field.fields))
|
|
1142
|
+
merged2.fields = mergeFields(oldField?.fields, field.fields);
|
|
1143
|
+
if (record(field._sourceData))
|
|
1144
|
+
merged2._sourceData = mergeMetadata(
|
|
1145
|
+
oldField?._sourceData,
|
|
1146
|
+
field._sourceData,
|
|
1147
|
+
SOURCE_PROPERTIES
|
|
1148
|
+
);
|
|
1149
|
+
return merged2;
|
|
1150
|
+
});
|
|
1151
|
+
};
|
|
1152
|
+
const oldPages = new Map(
|
|
1153
|
+
(Array.isArray(previous.pages) ? previous.pages : []).filter(record).map((page) => [page.id, page])
|
|
1154
|
+
);
|
|
1155
|
+
const merged = mergeMetadata(
|
|
1156
|
+
previous,
|
|
1157
|
+
next,
|
|
1158
|
+
FORM_PROPERTIES
|
|
1159
|
+
);
|
|
1160
|
+
merged.pages = next.pages.map((page) => {
|
|
1161
|
+
const oldPage = oldPages.get(page.id);
|
|
1162
|
+
return {
|
|
1163
|
+
...mergeMetadata(
|
|
1164
|
+
oldPage,
|
|
1165
|
+
page,
|
|
1166
|
+
PAGE_PROPERTIES
|
|
1167
|
+
),
|
|
1168
|
+
fields: mergeFields(oldPage?.fields, page.fields ?? [])
|
|
1169
|
+
};
|
|
1170
|
+
});
|
|
1171
|
+
return merged;
|
|
1172
|
+
}
|
|
1173
|
+
function setPreserving(document, path, value) {
|
|
1174
|
+
const node = document.getIn(path, true);
|
|
1175
|
+
const previous = document.getIn(path);
|
|
1176
|
+
if (JSON.stringify(previous) === JSON.stringify(value)) return false;
|
|
1177
|
+
if (record(value) && isMap(node)) {
|
|
1178
|
+
let changed = false;
|
|
1179
|
+
const keys = new Set(
|
|
1180
|
+
Object.keys(value).filter((key) => value[key] !== void 0)
|
|
1181
|
+
);
|
|
1182
|
+
for (const pair of [...node.items]) {
|
|
1183
|
+
const key = String(pair.key);
|
|
1184
|
+
if (!keys.has(key)) {
|
|
1185
|
+
document.deleteIn([...path, key]);
|
|
1186
|
+
changed = true;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
for (const key of keys) {
|
|
1190
|
+
changed = setPreserving(document, [...path, key], value[key]) || changed;
|
|
1191
|
+
}
|
|
1192
|
+
return changed;
|
|
1193
|
+
}
|
|
1194
|
+
if (Array.isArray(value) && isSeq(node)) {
|
|
1195
|
+
if (value.every((item) => record(item) && typeof item.id === "string") && node.items.every(
|
|
1196
|
+
(item) => isMap(item) && typeof item.get("id") === "string"
|
|
1197
|
+
)) {
|
|
1198
|
+
const byId = new Map(
|
|
1199
|
+
node.items.map((item) => [
|
|
1200
|
+
isMap(item) ? item.get("id") : void 0,
|
|
1201
|
+
item
|
|
1202
|
+
])
|
|
1203
|
+
);
|
|
1204
|
+
node.items = value.map((item) => {
|
|
1205
|
+
const id = item.id;
|
|
1206
|
+
return byId.get(id) ?? document.createNode(item);
|
|
1207
|
+
});
|
|
1208
|
+
value.forEach(
|
|
1209
|
+
(item, index) => setPreserving(document, [...path, index], item)
|
|
1210
|
+
);
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
if (node.items.length === value.length) {
|
|
1214
|
+
return value.reduce(
|
|
1215
|
+
(changed, item, index) => setPreserving(document, [...path, index], item) || changed,
|
|
1216
|
+
false
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
document.setIn(path, value);
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
function createEncounterMdy(snapshot, body, source) {
|
|
1224
|
+
const reserved = snapshot.responses[ENCOUNTER_DOCUMENT_FIELD_ID];
|
|
1225
|
+
const previousSource = source ?? reserved?.attributes?.mdyFrontMatterSource ?? "";
|
|
1226
|
+
const split = splitSource(previousSource);
|
|
1227
|
+
const fields = getEncounterMdyFields(snapshot);
|
|
1228
|
+
const authoredBody = body ?? reserved?.answer ?? split?.body ?? initialBody(snapshot, fields);
|
|
1229
|
+
let refreshedBody = refreshEncounterMdyLinks(authoredBody, fields);
|
|
1230
|
+
const responses = Object.fromEntries(
|
|
1231
|
+
Object.entries(snapshot.responses).filter(
|
|
1232
|
+
([id]) => id !== ENCOUNTER_DOCUMENT_FIELD_ID
|
|
1233
|
+
)
|
|
1234
|
+
);
|
|
1235
|
+
const document = split ? parseDocument(split.yamlSource, { uniqueKeys: true }) : parseDocument("{}\n");
|
|
1236
|
+
if (document.errors.length || document.warnings.length || !isMap(document.contents))
|
|
1237
|
+
throw new Error("Cannot update invalid MDY front matter.");
|
|
1238
|
+
if (!split) document.contents.flow = false;
|
|
1239
|
+
let changed = false;
|
|
1240
|
+
const existing = document.toJS({ maxAliasCount: 100 });
|
|
1241
|
+
const activeIds = new Set(fields.map((field) => field.id));
|
|
1242
|
+
const removedFields = [];
|
|
1243
|
+
if (record(existing)) {
|
|
1244
|
+
for (const [id, value] of Object.entries(existing)) {
|
|
1245
|
+
if (!activeIds.has(id) && record(value) && typeof value.fieldId === "string" && id === linkId(value.fieldId)) {
|
|
1246
|
+
document.delete(id);
|
|
1247
|
+
changed = true;
|
|
1248
|
+
removedFields.push({
|
|
1249
|
+
id,
|
|
1250
|
+
fieldId: value.fieldId,
|
|
1251
|
+
sectionId: typeof value.sectionId === "string" ? value.sectionId : "",
|
|
1252
|
+
label: "Field no longer configured",
|
|
1253
|
+
display: "Field no longer configured"
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
refreshedBody = refreshEncounterMdyLinks(refreshedBody, removedFields);
|
|
1259
|
+
const oldMdy = record(existing) && record(existing.mdy) ? existing.mdy : {};
|
|
1260
|
+
const metadata = {
|
|
1261
|
+
...oldMdy,
|
|
1262
|
+
kind: "document",
|
|
1263
|
+
schema: "esheet",
|
|
1264
|
+
template: oldMdy.template ?? {
|
|
1265
|
+
name: snapshot.definition.title,
|
|
1266
|
+
version: "1.0.0"
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
for (const [key, value] of Object.entries({
|
|
1270
|
+
mdy: metadata,
|
|
1271
|
+
form: mergeNativeFormMetadata(
|
|
1272
|
+
record(existing) ? existing.form : void 0,
|
|
1273
|
+
createEncounterFormDefinition(snapshot.definition)
|
|
1274
|
+
),
|
|
1275
|
+
response: responses,
|
|
1276
|
+
encounterDefinition: snapshot.definition
|
|
1277
|
+
})) {
|
|
1278
|
+
changed = setPreserving(document, [key], value) || changed;
|
|
1279
|
+
}
|
|
1280
|
+
for (const field of fields) {
|
|
1281
|
+
const oldField = record(existing) ? existing[field.id] : void 0;
|
|
1282
|
+
changed = setPreserving(document, [field.id], {
|
|
1283
|
+
...record(oldField) ? oldField : {},
|
|
1284
|
+
fieldId: field.fieldId,
|
|
1285
|
+
sectionId: field.sectionId,
|
|
1286
|
+
label: field.label,
|
|
1287
|
+
value: canonicalValue(responses[field.fieldId]),
|
|
1288
|
+
display: field.display
|
|
1289
|
+
}) || changed;
|
|
1290
|
+
}
|
|
1291
|
+
const newline = split?.opening.endsWith("\r\n") ? "\r\n" : "\n";
|
|
1292
|
+
if (split && !changed) {
|
|
1293
|
+
const boundary = refreshedBody && !split.frontMatterSource.endsWith("\n") ? newline : "";
|
|
1294
|
+
return `${split.frontMatterSource}${boundary}${refreshedBody}`;
|
|
1295
|
+
}
|
|
1296
|
+
const yaml = document.toString({ lineWidth: 0 }).replace(/\n/g, newline);
|
|
1297
|
+
let closing = split?.closing || `---${newline}`;
|
|
1298
|
+
if (refreshedBody && !closing.endsWith("\n")) closing += newline;
|
|
1299
|
+
return `${split?.opening ?? `---${newline}`}${yaml}${closing}${refreshedBody}`;
|
|
1300
|
+
}
|
|
1301
|
+
async function renderEncounterMdy(source) {
|
|
1302
|
+
let parsed = parseEncounterMdy(source);
|
|
1303
|
+
let fields = fieldsFromFrontMatter(parsed.frontMatter ?? {});
|
|
1304
|
+
if (parsed.definition && parsed.response) {
|
|
1305
|
+
const { createEncounterSnapshot: createEncounterSnapshot2 } = await import('./model-WFFZRQMY.js');
|
|
1306
|
+
const snapshot = createEncounterSnapshot2(parsed.definition, {
|
|
1307
|
+
...parsed.response,
|
|
1308
|
+
[ENCOUNTER_DOCUMENT_FIELD_ID]: encounterDocumentResponse(source)
|
|
1309
|
+
});
|
|
1310
|
+
parsed = parseEncounterMdy(snapshot.mdy);
|
|
1311
|
+
const canonical = new Map(
|
|
1312
|
+
getEncounterMdyFields(snapshot).map((field) => [field.fieldId, field])
|
|
1313
|
+
);
|
|
1314
|
+
fields = fieldsFromFrontMatter(parsed.frontMatter ?? {}).map((field) => ({
|
|
1315
|
+
...field,
|
|
1316
|
+
display: canonical.get(field.fieldId)?.display ?? field.display,
|
|
1317
|
+
empty: canonical.get(field.fieldId)?.empty ?? !hasNativeValue(parsed.response?.[field.fieldId], true)
|
|
1318
|
+
}));
|
|
1319
|
+
} else if (parsed.response) {
|
|
1320
|
+
const units = /* @__PURE__ */ new Map();
|
|
1321
|
+
const indexUnits = (nativeFields) => {
|
|
1322
|
+
if (!Array.isArray(nativeFields)) return;
|
|
1323
|
+
for (const field of nativeFields) {
|
|
1324
|
+
if (!record(field)) continue;
|
|
1325
|
+
if (typeof field.id === "string" && typeof field.unit === "string")
|
|
1326
|
+
units.set(field.id, field.unit);
|
|
1327
|
+
indexUnits(field.fields);
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
parsed.form?.pages.forEach((page) => indexUnits(page.fields));
|
|
1331
|
+
const response = parsed.response;
|
|
1332
|
+
fields = fields.map((field) => {
|
|
1333
|
+
const cached = parsed.frontMatter?.[field.id];
|
|
1334
|
+
const native = response[field.fieldId];
|
|
1335
|
+
const empty = !hasNativeValue(native, true);
|
|
1336
|
+
if (!native || !record(cached) || JSON.stringify(cached.value) === JSON.stringify(canonicalValue(native)))
|
|
1337
|
+
return { ...field, empty };
|
|
1338
|
+
const display = fieldDisplay({
|
|
1339
|
+
...native,
|
|
1340
|
+
unit: units.get(field.fieldId)
|
|
1341
|
+
});
|
|
1342
|
+
parsed.diagnostics.push({
|
|
1343
|
+
severity: "warning",
|
|
1344
|
+
code: "response",
|
|
1345
|
+
fieldId: field.fieldId,
|
|
1346
|
+
message: `${field.label}'s cached projection differed from its canonical response and was refreshed.`
|
|
1347
|
+
});
|
|
1348
|
+
return { ...field, display: display.trim() ? display : "\u2014", empty };
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
const body = refreshEncounterMdyLinks(parsed.body, fields);
|
|
1352
|
+
const envelope = parsed.frontMatterSource || "---\n{}\n---\n";
|
|
1353
|
+
const { raw, html } = await render(
|
|
1354
|
+
`${envelope}${body}`,
|
|
1355
|
+
{},
|
|
1356
|
+
{
|
|
1357
|
+
engine: { name: "encounter-mdy-document", render: (content) => content },
|
|
1358
|
+
markdown: markdownToHtml,
|
|
1359
|
+
fieldLinks: false
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
return { raw, html, fields, diagnostics: parsed.diagnostics };
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/components/EncounterVisit/model.ts
|
|
1366
|
+
function record2(value) {
|
|
1367
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1368
|
+
}
|
|
1369
|
+
function nonempty2(value) {
|
|
1370
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1371
|
+
}
|
|
1372
|
+
function measurement(value) {
|
|
1373
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
1374
|
+
}
|
|
1375
|
+
function parseJson(answer) {
|
|
1376
|
+
if (typeof answer !== "string" || !answer.trim()) return void 0;
|
|
1377
|
+
try {
|
|
1378
|
+
return JSON.parse(answer);
|
|
1379
|
+
} catch {
|
|
1380
|
+
return void 0;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
var VITALS = [
|
|
1384
|
+
{ key: "pulse", label: "Pulse", unit: "/min", code: "8867-4" },
|
|
1385
|
+
{
|
|
1386
|
+
key: "respiratoryRate",
|
|
1387
|
+
label: "Respiratory rate",
|
|
1388
|
+
unit: "/min",
|
|
1389
|
+
code: "9279-1"
|
|
1390
|
+
},
|
|
1391
|
+
{ key: "temperature", label: "Temperature", unit: "Cel", code: "8310-5" },
|
|
1392
|
+
{
|
|
1393
|
+
key: "oxygenSaturation",
|
|
1394
|
+
label: "Oxygen saturation",
|
|
1395
|
+
unit: "%",
|
|
1396
|
+
code: "2708-6"
|
|
1397
|
+
},
|
|
1398
|
+
{ key: "height", label: "Height", unit: "cm", code: "8302-2" },
|
|
1399
|
+
{ key: "weight", label: "Weight", unit: "kg", code: "29463-7" },
|
|
1400
|
+
{ key: "pain", label: "Pain score", unit: "{score}", code: "72514-3" }
|
|
1401
|
+
];
|
|
1402
|
+
var VITAL_KEYS = [
|
|
1403
|
+
"systolic",
|
|
1404
|
+
"diastolic",
|
|
1405
|
+
...VITALS.map(({ key }) => key)
|
|
1406
|
+
];
|
|
1407
|
+
function loinc(code, display) {
|
|
1408
|
+
return { system: "http://loinc.org", code, display };
|
|
1409
|
+
}
|
|
1410
|
+
function parseEncounterVitals(answer) {
|
|
1411
|
+
const parsed = parseJson(answer);
|
|
1412
|
+
if (!record2(parsed) || !Array.isArray(parsed.readings))
|
|
1413
|
+
return { readings: [] };
|
|
1414
|
+
const readings = parsed.readings.filter(
|
|
1415
|
+
(reading) => record2(reading) && nonempty2(reading.id)
|
|
1416
|
+
).map((reading) => {
|
|
1417
|
+
const safe = { id: reading.id };
|
|
1418
|
+
for (const key of VITAL_KEYS) {
|
|
1419
|
+
if (typeof reading[key] === "number" && Number.isFinite(reading[key]))
|
|
1420
|
+
safe[key] = reading[key];
|
|
1421
|
+
}
|
|
1422
|
+
for (const key of ["recordedAt", "position", "site"]) {
|
|
1423
|
+
if (typeof reading[key] === "string") safe[key] = reading[key];
|
|
1424
|
+
}
|
|
1425
|
+
return safe;
|
|
1426
|
+
});
|
|
1427
|
+
const inputDrafts = Array.isArray(parsed.inputDrafts) ? parsed.inputDrafts.filter(
|
|
1428
|
+
(draft) => record2(draft) && nonempty2(draft.readingId) && nonempty2(draft.text) && VITAL_KEYS.includes(draft.key)
|
|
1429
|
+
) : void 0;
|
|
1430
|
+
for (const draft of Array.isArray(parsed.inputDrafts) ? parsed.inputDrafts : []) {
|
|
1431
|
+
if (!record2(draft) || typeof draft.readingId !== "string" || !VITAL_KEYS.includes(draft.key))
|
|
1432
|
+
continue;
|
|
1433
|
+
const reading = readings.find(({ id }) => id === draft.readingId);
|
|
1434
|
+
if (reading) delete reading[draft.key];
|
|
1435
|
+
}
|
|
1436
|
+
return inputDrafts ? { readings, inputDrafts } : { readings };
|
|
1437
|
+
}
|
|
1438
|
+
function validateEncounterVitals(value, fieldId = "", sectionId = "") {
|
|
1439
|
+
const issues = [];
|
|
1440
|
+
const add = (message, path, readingId, code = "invalid-value") => {
|
|
1441
|
+
issues.push({ sectionId, fieldId, code, message, path, readingId });
|
|
1442
|
+
};
|
|
1443
|
+
if (!record2(value) || !Array.isArray(value.readings)) {
|
|
1444
|
+
add("Vitals must contain a readings array.", "readings");
|
|
1445
|
+
return issues;
|
|
1446
|
+
}
|
|
1447
|
+
for (const key of Object.keys(value)) {
|
|
1448
|
+
if (!["readings", "inputDrafts"].includes(key))
|
|
1449
|
+
add(`Unknown vitals property ${key}.`, key);
|
|
1450
|
+
}
|
|
1451
|
+
const readings = value.readings;
|
|
1452
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1453
|
+
readings.forEach((reading, index) => {
|
|
1454
|
+
const path = `readings.${index}`;
|
|
1455
|
+
if (!record2(reading) || !nonempty2(reading.id)) {
|
|
1456
|
+
add("Each vital reading requires a nonempty id.", `${path}.id`);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const id = reading.id;
|
|
1460
|
+
if (ids.has(id)) add("Vital reading ids must be unique.", `${path}.id`, id);
|
|
1461
|
+
ids.add(id);
|
|
1462
|
+
const allowedKeys = /* @__PURE__ */ new Set([
|
|
1463
|
+
"id",
|
|
1464
|
+
"recordedAt",
|
|
1465
|
+
"position",
|
|
1466
|
+
"site",
|
|
1467
|
+
...VITAL_KEYS
|
|
1468
|
+
]);
|
|
1469
|
+
for (const key of Object.keys(reading)) {
|
|
1470
|
+
if (!allowedKeys.has(key))
|
|
1471
|
+
add(`Unknown vital property ${key}.`, `${path}.${key}`, id);
|
|
1472
|
+
}
|
|
1473
|
+
if (!VITAL_KEYS.some((key) => reading[key] !== void 0)) {
|
|
1474
|
+
add("Record at least one measurement in a vital reading.", path, id);
|
|
1475
|
+
}
|
|
1476
|
+
for (const key of VITAL_KEYS) {
|
|
1477
|
+
if (reading[key] !== void 0 && !measurement(reading[key])) {
|
|
1478
|
+
add(
|
|
1479
|
+
`${key} must be a finite nonnegative number.`,
|
|
1480
|
+
`${path}.${key}`,
|
|
1481
|
+
id,
|
|
1482
|
+
"invalid-number"
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (reading.systolic !== void 0 !== (reading.diastolic !== void 0)) {
|
|
1487
|
+
add(
|
|
1488
|
+
"Record both systolic and diastolic blood pressure, or leave both blank.",
|
|
1489
|
+
path,
|
|
1490
|
+
id,
|
|
1491
|
+
"incomplete-blood-pressure"
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
if (measurement(reading.oxygenSaturation) && reading.oxygenSaturation > 100) {
|
|
1495
|
+
add(
|
|
1496
|
+
"Oxygen saturation must be between 0 and 100 percent.",
|
|
1497
|
+
`${path}.oxygenSaturation`,
|
|
1498
|
+
id,
|
|
1499
|
+
"invalid-number"
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
if (measurement(reading.pain) && reading.pain > 10) {
|
|
1503
|
+
add(
|
|
1504
|
+
"Pain score must be between 0 and 10.",
|
|
1505
|
+
`${path}.pain`,
|
|
1506
|
+
id,
|
|
1507
|
+
"invalid-number"
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
for (const key of ["recordedAt", "position", "site"]) {
|
|
1511
|
+
if (reading[key] !== void 0 && typeof reading[key] !== "string") {
|
|
1512
|
+
add(`${key} must be a string.`, `${path}.${key}`, id);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (nonempty2(reading.recordedAt) && (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(reading.recordedAt) || !DateTime.fromISO(reading.recordedAt, { setZone: true }).isValid)) {
|
|
1516
|
+
add(
|
|
1517
|
+
"Recorded time must be a valid date and time.",
|
|
1518
|
+
`${path}.recordedAt`,
|
|
1519
|
+
id
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
if (value.inputDrafts !== void 0) {
|
|
1524
|
+
if (!Array.isArray(value.inputDrafts)) {
|
|
1525
|
+
add(
|
|
1526
|
+
"Unfinished vital inputs must be an inputDrafts array.",
|
|
1527
|
+
"inputDrafts"
|
|
1528
|
+
);
|
|
1529
|
+
} else {
|
|
1530
|
+
const draftKeys = /* @__PURE__ */ new Set();
|
|
1531
|
+
value.inputDrafts.forEach((draft, index) => {
|
|
1532
|
+
const path = `inputDrafts.${index}`;
|
|
1533
|
+
if (!record2(draft) || !nonempty2(draft.readingId) || !nonempty2(draft.text) || !VITAL_KEYS.includes(draft.key)) {
|
|
1534
|
+
add(
|
|
1535
|
+
"Each unfinished input requires a reading id, numeric measurement key and nonempty text.",
|
|
1536
|
+
path,
|
|
1537
|
+
record2(draft) && typeof draft.readingId === "string" ? draft.readingId : void 0
|
|
1538
|
+
);
|
|
1539
|
+
add(
|
|
1540
|
+
"Finish or clear the malformed numeric input before saving.",
|
|
1541
|
+
path,
|
|
1542
|
+
record2(draft) && typeof draft.readingId === "string" ? draft.readingId : void 0,
|
|
1543
|
+
"invalid-number"
|
|
1544
|
+
);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const readingId = draft.readingId;
|
|
1548
|
+
if (!ids.has(readingId))
|
|
1549
|
+
add(
|
|
1550
|
+
"Unfinished input must reference an existing reading.",
|
|
1551
|
+
`${path}.readingId`,
|
|
1552
|
+
readingId
|
|
1553
|
+
);
|
|
1554
|
+
for (const key of Object.keys(draft)) {
|
|
1555
|
+
if (!["readingId", "key", "text"].includes(key))
|
|
1556
|
+
add(
|
|
1557
|
+
`Unknown unfinished input property ${key}.`,
|
|
1558
|
+
`${path}.${key}`,
|
|
1559
|
+
readingId
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
const draftKey = JSON.stringify([readingId, draft.key]);
|
|
1563
|
+
if (draftKeys.has(draftKey))
|
|
1564
|
+
add(
|
|
1565
|
+
"Each measurement can have only one unfinished input.",
|
|
1566
|
+
path,
|
|
1567
|
+
readingId
|
|
1568
|
+
);
|
|
1569
|
+
draftKeys.add(draftKey);
|
|
1570
|
+
const readingIndex = readings.findIndex(
|
|
1571
|
+
(reading) => record2(reading) && reading.id === readingId
|
|
1572
|
+
);
|
|
1573
|
+
add(
|
|
1574
|
+
`Finish or clear the ${draft.key} input before saving.`,
|
|
1575
|
+
`readings.${readingIndex}.${draft.key}`,
|
|
1576
|
+
readingId,
|
|
1577
|
+
"invalid-number"
|
|
1578
|
+
);
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return issues;
|
|
1583
|
+
}
|
|
1584
|
+
function optionalStrings(value, keys) {
|
|
1585
|
+
return keys.every(
|
|
1586
|
+
(key) => value[key] === void 0 || typeof value[key] === "string"
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
function validCode(value) {
|
|
1590
|
+
return value === void 0 || record2(value) && nonempty2(value.system) && nonempty2(value.code) && optionalStrings(value, ["display"]);
|
|
1591
|
+
}
|
|
1592
|
+
function validFieldUncertainty(value) {
|
|
1593
|
+
return record2(value) && typeof value.known === "boolean" && (value.reason === void 0 || ["asked-unknown", "not-asked", "masked", "unavailable"].includes(
|
|
1594
|
+
value.reason
|
|
1595
|
+
)) && (value.confidence === void 0 || ["low", "medium", "high"].includes(value.confidence)) && optionalStrings(value, ["note"]);
|
|
1596
|
+
}
|
|
1597
|
+
function validUncertainty(value) {
|
|
1598
|
+
if (!record2(value)) return false;
|
|
1599
|
+
if (value.overall !== void 0 && !["confirmed", "low", "medium", "high"].includes(value.overall)) {
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
if (value.fields === void 0) return true;
|
|
1603
|
+
if (!record2(value.fields)) return false;
|
|
1604
|
+
return Object.entries(value.fields).every(
|
|
1605
|
+
([field, fieldUncertainty]) => ["coding", "onset", "severity", "laterality", "bodySite"].includes(
|
|
1606
|
+
field
|
|
1607
|
+
) && validFieldUncertainty(fieldUncertainty)
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
function validMedication(value) {
|
|
1611
|
+
return record2(value) && nonempty2(value.id) && nonempty2(value.name) && [
|
|
1612
|
+
"unreconciled",
|
|
1613
|
+
"taking",
|
|
1614
|
+
"taking-noncompliant",
|
|
1615
|
+
"not-taking",
|
|
1616
|
+
"unknown"
|
|
1617
|
+
].includes(value.status) && optionalStrings(value, [
|
|
1618
|
+
"sig",
|
|
1619
|
+
"discontinuedDate",
|
|
1620
|
+
"note",
|
|
1621
|
+
"task",
|
|
1622
|
+
"strength",
|
|
1623
|
+
"doseForm",
|
|
1624
|
+
"quantity",
|
|
1625
|
+
"quantityUnit",
|
|
1626
|
+
"daysSupply",
|
|
1627
|
+
"route",
|
|
1628
|
+
"frequency",
|
|
1629
|
+
"refills",
|
|
1630
|
+
"startDate",
|
|
1631
|
+
"endDate",
|
|
1632
|
+
"indication",
|
|
1633
|
+
"pharmacyNotes"
|
|
1634
|
+
]) && (value.expired === void 0 || typeof value.expired === "boolean") && (value.prn === void 0 || typeof value.prn === "boolean") && (value.substitution === void 0 || ["0", "1"].includes(value.substitution)) && validCode(value.code);
|
|
1635
|
+
}
|
|
1636
|
+
function validAllergy(value) {
|
|
1637
|
+
return record2(value) && nonempty2(value.id) && nonempty2(value.allergen) && optionalStrings(value, ["reaction", "onsetDate", "note"]) && (value.type === void 0 || ["drug", "food", "environmental", "other"].includes(
|
|
1638
|
+
value.type
|
|
1639
|
+
)) && (value.kind === void 0 || ["allergy", "intolerance"].includes(value.kind)) && (value.severity === void 0 || ["mild", "moderate", "severe"].includes(value.severity)) && (value.inactive === void 0 || typeof value.inactive === "boolean") && validCode(value.code);
|
|
1640
|
+
}
|
|
1641
|
+
function duplicateIds(values, key) {
|
|
1642
|
+
return new Set(values.map((value) => value[key])).size !== values.length;
|
|
1643
|
+
}
|
|
1644
|
+
function validAssessmentItem(value) {
|
|
1645
|
+
return record2(value) && nonempty2(value.concernId) && nonempty2(value.assertionId) && optionalStrings(value, ["note"]);
|
|
1646
|
+
}
|
|
1647
|
+
function validateEncounterAssessment(value, fieldId = "", sectionId = "", externalConcerns = []) {
|
|
1648
|
+
const issues = [];
|
|
1649
|
+
const add = (message, path) => issues.push({
|
|
1650
|
+
sectionId,
|
|
1651
|
+
fieldId,
|
|
1652
|
+
code: "invalid-value",
|
|
1653
|
+
message,
|
|
1654
|
+
path
|
|
1655
|
+
});
|
|
1656
|
+
if (!record2(value) || !Array.isArray(value.concerns) || !Array.isArray(value.items) || !Array.isArray(value.orders)) {
|
|
1657
|
+
add("Assessment must contain concerns, items and orders arrays.");
|
|
1658
|
+
return issues;
|
|
1659
|
+
}
|
|
1660
|
+
const concerns = new Map(
|
|
1661
|
+
externalConcerns.map((concern) => [
|
|
1662
|
+
concern.concernId,
|
|
1663
|
+
new Set(concern.assertions.map((assertion) => assertion.id))
|
|
1664
|
+
])
|
|
1665
|
+
);
|
|
1666
|
+
const localConcernIds = /* @__PURE__ */ new Set();
|
|
1667
|
+
value.concerns.forEach((concern, index) => {
|
|
1668
|
+
const path = `concerns.${index}`;
|
|
1669
|
+
if (!record2(concern) || !nonempty2(concern.concernId) || !Array.isArray(concern.assertions) || ![
|
|
1670
|
+
"active",
|
|
1671
|
+
"recurrence",
|
|
1672
|
+
"relapse",
|
|
1673
|
+
"inactive",
|
|
1674
|
+
"remission",
|
|
1675
|
+
"resolved"
|
|
1676
|
+
].includes(concern.clinicalStatus)) {
|
|
1677
|
+
add(
|
|
1678
|
+
"Each concern requires an id, clinical status and assertions array.",
|
|
1679
|
+
path
|
|
1680
|
+
);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (localConcernIds.has(concern.concernId))
|
|
1684
|
+
add("Concern ids must be unique.", `${path}.concernId`);
|
|
1685
|
+
localConcernIds.add(concern.concernId);
|
|
1686
|
+
if (concern.relationships !== void 0 && (!Array.isArray(concern.relationships) || concern.relationships.some(
|
|
1687
|
+
(relationship) => !record2(relationship) || !nonempty2(relationship.concernId) || ![
|
|
1688
|
+
"caused-by",
|
|
1689
|
+
"evolved-from",
|
|
1690
|
+
"differential-sibling",
|
|
1691
|
+
"complication-of"
|
|
1692
|
+
].includes(relationship.type) || !optionalStrings(relationship, ["display"])
|
|
1693
|
+
))) {
|
|
1694
|
+
add(
|
|
1695
|
+
"Concern relationships must be an array of records with a supported type and a concernId.",
|
|
1696
|
+
`${path}.relationships`
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
if (concern.observations !== void 0 && (!Array.isArray(concern.observations) || concern.observations.some(
|
|
1700
|
+
(observation) => !record2(observation) || !nonempty2(observation.id) || !nonempty2(observation.text) || !isEncounterDate(observation.date) || !optionalStrings(observation, ["author"])
|
|
1701
|
+
))) {
|
|
1702
|
+
add(
|
|
1703
|
+
"Concern observations must be an array of records with an id, text and a valid YYYY-MM-DD date.",
|
|
1704
|
+
`${path}.observations`
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
const assertionIds = /* @__PURE__ */ new Set();
|
|
1708
|
+
concerns.set(concern.concernId, assertionIds);
|
|
1709
|
+
concern.assertions.forEach((assertion, assertionIndex) => {
|
|
1710
|
+
const assertionPath = `${path}.assertions.${assertionIndex}`;
|
|
1711
|
+
if (!record2(assertion) || !nonempty2(assertion.id) || !nonempty2(assertion.text) || !isEncounterDate(assertion.date) || ![
|
|
1712
|
+
"unconfirmed",
|
|
1713
|
+
"provisional",
|
|
1714
|
+
"differential",
|
|
1715
|
+
"confirmed",
|
|
1716
|
+
"refuted",
|
|
1717
|
+
"entered-in-error"
|
|
1718
|
+
].includes(assertion.verificationStatus) || !optionalStrings(assertion, ["note"])) {
|
|
1719
|
+
add(
|
|
1720
|
+
"Each assertion requires an id, text, a valid YYYY-MM-DD date and verification status.",
|
|
1721
|
+
assertionPath
|
|
1722
|
+
);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (assertionIds.has(assertion.id))
|
|
1726
|
+
add(
|
|
1727
|
+
"Assertion ids must be unique within a concern.",
|
|
1728
|
+
`${assertionPath}.id`
|
|
1729
|
+
);
|
|
1730
|
+
assertionIds.add(assertion.id);
|
|
1731
|
+
if (assertion.changeType !== void 0 && !["refinement", "revision", "progression", "reattribution"].includes(
|
|
1732
|
+
assertion.changeType
|
|
1733
|
+
)) {
|
|
1734
|
+
add(
|
|
1735
|
+
"Assertion changeType must be refinement, revision, progression or reattribution.",
|
|
1736
|
+
`${assertionPath}.changeType`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
if (assertion.coding !== void 0 && (!Array.isArray(assertion.coding) || assertion.coding.some((coding) => !validCode(coding)))) {
|
|
1740
|
+
add(
|
|
1741
|
+
"Assertion coding must contain system and code strings.",
|
|
1742
|
+
`${assertionPath}.coding`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
if (assertion.uncertainty !== void 0 && !validUncertainty(assertion.uncertainty)) {
|
|
1746
|
+
add(
|
|
1747
|
+
"Assertion uncertainty must use supported overall confidence and per-field known/reason/confidence records.",
|
|
1748
|
+
`${assertionPath}.uncertainty`
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
1754
|
+
value.items.forEach((item, index) => {
|
|
1755
|
+
const path = `items.${index}`;
|
|
1756
|
+
if (!validAssessmentItem(item)) {
|
|
1757
|
+
add("Assessment items require concern and assertion ids.", path);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (itemIds.has(item.concernId))
|
|
1761
|
+
add("Each concern may be assessed once.", `${path}.concernId`);
|
|
1762
|
+
itemIds.add(item.concernId);
|
|
1763
|
+
if (!concerns.get(item.concernId)?.has(item.assertionId)) {
|
|
1764
|
+
add(
|
|
1765
|
+
"An assessment item must reference an existing concern and assertion.",
|
|
1766
|
+
path
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
const orderIds = /* @__PURE__ */ new Set();
|
|
1771
|
+
value.orders.forEach((order, index) => {
|
|
1772
|
+
const path = `orders.${index}`;
|
|
1773
|
+
if (!record2(order) || !nonempty2(order.orderId) || !nonempty2(order.display) || !["medication", "lab", "imaging", "procedure", "referral"].includes(
|
|
1774
|
+
order.type
|
|
1775
|
+
) || !optionalStrings(order, [
|
|
1776
|
+
"concernId",
|
|
1777
|
+
"detail",
|
|
1778
|
+
"timing",
|
|
1779
|
+
"indication",
|
|
1780
|
+
"notes",
|
|
1781
|
+
"bodySite",
|
|
1782
|
+
"referTo"
|
|
1783
|
+
]) || order.priority !== void 0 && !["routine", "urgent", "stat"].includes(order.priority)) {
|
|
1784
|
+
add(
|
|
1785
|
+
"Each order requires an id, display text and supported order type.",
|
|
1786
|
+
path
|
|
1787
|
+
);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (orderIds.has(order.orderId))
|
|
1791
|
+
add("Order ids must be unique.", `${path}.orderId`);
|
|
1792
|
+
orderIds.add(order.orderId);
|
|
1793
|
+
if (order.concernId !== void 0 && !concerns.has(order.concernId)) {
|
|
1794
|
+
add(
|
|
1795
|
+
"An order indication must reference an existing concern.",
|
|
1796
|
+
`${path}.concernId`
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
if (order.code !== void 0 && (!record2(order.code) || !nonempty2(order.code.fullid) || !nonempty2(order.code.codetype) || !nonempty2(order.code.fullcode))) {
|
|
1800
|
+
add(
|
|
1801
|
+
"Order coding requires fullid, codetype and fullcode.",
|
|
1802
|
+
`${path}.code`
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
return issues;
|
|
1807
|
+
}
|
|
1808
|
+
function parseEncounterAssessment(answer) {
|
|
1809
|
+
const parsed = parseJson(answer);
|
|
1810
|
+
return validateEncounterAssessment(parsed).length === 0 ? parsed : { concerns: [], items: [], orders: [] };
|
|
1811
|
+
}
|
|
1812
|
+
function validateClinicalList(parsed, key, validItem, title, fieldId, sectionId) {
|
|
1813
|
+
const issues = [];
|
|
1814
|
+
const add = (message, path) => issues.push({
|
|
1815
|
+
sectionId,
|
|
1816
|
+
fieldId,
|
|
1817
|
+
code: "invalid-value",
|
|
1818
|
+
message,
|
|
1819
|
+
path
|
|
1820
|
+
});
|
|
1821
|
+
if (!record2(parsed) || !Array.isArray(parsed[key])) {
|
|
1822
|
+
add(`${title} must contain a ${key} array.`, key);
|
|
1823
|
+
return issues;
|
|
1824
|
+
}
|
|
1825
|
+
const values = parsed[key];
|
|
1826
|
+
values.forEach((item, index) => {
|
|
1827
|
+
if (!validItem(item))
|
|
1828
|
+
add(
|
|
1829
|
+
`Invalid ${key === "medications" ? "medication" : "allergy"} record.`,
|
|
1830
|
+
`${key}.${index}`
|
|
1831
|
+
);
|
|
1832
|
+
});
|
|
1833
|
+
if (values.every(record2) && duplicateIds(values, "id"))
|
|
1834
|
+
add(`${title} ids must be unique.`, key);
|
|
1835
|
+
if (key === "allergies") {
|
|
1836
|
+
if (parsed.noKnownAllergies !== void 0 && typeof parsed.noKnownAllergies !== "boolean") {
|
|
1837
|
+
add(
|
|
1838
|
+
"No-known-allergies must be an explicit boolean.",
|
|
1839
|
+
"noKnownAllergies"
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
if (parsed.noKnownAllergies === true && values.length > 0) {
|
|
1843
|
+
add(
|
|
1844
|
+
"Recorded allergies conflict with the no-known-allergies flag.",
|
|
1845
|
+
"noKnownAllergies"
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return issues;
|
|
1850
|
+
}
|
|
1851
|
+
function validateEncounterMedicationValue(value, fieldId = "", sectionId = "") {
|
|
1852
|
+
return validateClinicalList(
|
|
1853
|
+
value,
|
|
1854
|
+
"medications",
|
|
1855
|
+
validMedication,
|
|
1856
|
+
"Medications",
|
|
1857
|
+
fieldId,
|
|
1858
|
+
sectionId
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
function validateEncounterAllergyValue(value, fieldId = "", sectionId = "") {
|
|
1862
|
+
return validateClinicalList(
|
|
1863
|
+
value,
|
|
1864
|
+
"allergies",
|
|
1865
|
+
validAllergy,
|
|
1866
|
+
"Allergies",
|
|
1867
|
+
fieldId,
|
|
1868
|
+
sectionId
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
function validateEncounterConditions(value, fieldId = "", sectionId = "") {
|
|
1872
|
+
return validateEncounterAssessment(
|
|
1873
|
+
{
|
|
1874
|
+
concerns: record2(value) ? value.concerns : void 0,
|
|
1875
|
+
items: [],
|
|
1876
|
+
orders: []
|
|
1877
|
+
},
|
|
1878
|
+
fieldId,
|
|
1879
|
+
sectionId
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
function validateEncounterPlan(value, fieldId = "", sectionId = "") {
|
|
1883
|
+
if (!record2(value) || !Array.isArray(value.orders))
|
|
1884
|
+
return [
|
|
1885
|
+
{
|
|
1886
|
+
sectionId,
|
|
1887
|
+
fieldId,
|
|
1888
|
+
code: "invalid-value",
|
|
1889
|
+
message: "Plan must contain an orders array.",
|
|
1890
|
+
path: "orders"
|
|
1891
|
+
}
|
|
1892
|
+
];
|
|
1893
|
+
const ids = new Set(
|
|
1894
|
+
value.orders.flatMap(
|
|
1895
|
+
(order) => record2(order) && nonempty2(order.concernId) ? [order.concernId] : []
|
|
1896
|
+
)
|
|
1897
|
+
);
|
|
1898
|
+
return validateEncounterAssessment(
|
|
1899
|
+
{
|
|
1900
|
+
concerns: Array.from(ids, (concernId) => ({
|
|
1901
|
+
concernId,
|
|
1902
|
+
clinicalStatus: "active",
|
|
1903
|
+
assertions: []
|
|
1904
|
+
})),
|
|
1905
|
+
items: [],
|
|
1906
|
+
orders: value.orders
|
|
1907
|
+
},
|
|
1908
|
+
fieldId,
|
|
1909
|
+
sectionId
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
function parseEncounterConditions(answer) {
|
|
1913
|
+
const parsed = parseJson(answer);
|
|
1914
|
+
return validateEncounterConditions(parsed).length === 0 ? parsed : { concerns: [] };
|
|
1915
|
+
}
|
|
1916
|
+
function parseEncounterPlan(answer) {
|
|
1917
|
+
const parsed = parseJson(answer);
|
|
1918
|
+
return validateEncounterPlan(parsed).length === 0 ? parsed : { orders: [] };
|
|
1919
|
+
}
|
|
1920
|
+
function validateCoding(section, parsed, fieldId, externalConcerns = []) {
|
|
1921
|
+
if (!(section.requireCoding ?? true)) return [];
|
|
1922
|
+
const issues = [];
|
|
1923
|
+
const add = (path) => issues.push({
|
|
1924
|
+
sectionId: section.id,
|
|
1925
|
+
fieldId,
|
|
1926
|
+
code: "invalid-value",
|
|
1927
|
+
path,
|
|
1928
|
+
message: `${section.title} entries require a selected clinical code.`
|
|
1929
|
+
});
|
|
1930
|
+
const coded = (code) => record2(code) && validCode(code);
|
|
1931
|
+
if (section.kind === "medications" || section.kind === "allergies") {
|
|
1932
|
+
const key = section.kind;
|
|
1933
|
+
if (Array.isArray(parsed[key]))
|
|
1934
|
+
parsed[key].forEach((entry, index) => {
|
|
1935
|
+
if (record2(entry) && !coded(entry.code)) add(`${key}.${index}.code`);
|
|
1936
|
+
});
|
|
1937
|
+
} else if (section.kind === "conditions" || section.kind === "assessment") {
|
|
1938
|
+
if (Array.isArray(parsed.concerns))
|
|
1939
|
+
parsed.concerns.forEach((entry, index) => {
|
|
1940
|
+
if (!record2(entry) || !Array.isArray(entry.assertions)) return;
|
|
1941
|
+
const assertion = currentAssertion(
|
|
1942
|
+
entry
|
|
1943
|
+
);
|
|
1944
|
+
if (!assertion?.coding?.some(coded)) add(`concerns.${index}.coding`);
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
if (section.kind === "assessment" && Array.isArray(parsed.items)) {
|
|
1948
|
+
const concerns = [
|
|
1949
|
+
...Array.isArray(parsed.concerns) ? parsed.concerns : [],
|
|
1950
|
+
...externalConcerns
|
|
1951
|
+
];
|
|
1952
|
+
parsed.items.forEach((item, index) => {
|
|
1953
|
+
if (!record2(item)) return;
|
|
1954
|
+
const concern = concerns.find(
|
|
1955
|
+
(entry) => record2(entry) && entry.concernId === item.concernId
|
|
1956
|
+
);
|
|
1957
|
+
const assertion = concern && Array.isArray(concern.assertions) ? concern.assertions.find(
|
|
1958
|
+
(entry) => record2(entry) && entry.id === item.assertionId
|
|
1959
|
+
) : void 0;
|
|
1960
|
+
if (!record2(assertion) || !Array.isArray(assertion.coding) || !assertion.coding.some(coded))
|
|
1961
|
+
add(`items.${index}.coding`);
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
if (section.kind === "plan" || section.kind === "assessment") {
|
|
1965
|
+
if (Array.isArray(parsed.orders))
|
|
1966
|
+
parsed.orders.forEach((order, index) => {
|
|
1967
|
+
if (record2(order) && (!record2(order.code) || !nonempty2(order.code.fullid) || !nonempty2(order.code.codetype) || !nonempty2(order.code.fullcode)))
|
|
1968
|
+
add(`orders.${index}.code`);
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
return issues;
|
|
1972
|
+
}
|
|
1973
|
+
function validateCustom(section, answer, fieldId, externalConcerns = []) {
|
|
1974
|
+
let parsed;
|
|
1975
|
+
try {
|
|
1976
|
+
parsed = JSON.parse(answer);
|
|
1977
|
+
} catch {
|
|
1978
|
+
return [
|
|
1979
|
+
{
|
|
1980
|
+
sectionId: section.id,
|
|
1981
|
+
fieldId,
|
|
1982
|
+
code: "invalid-json",
|
|
1983
|
+
message: `${section.title} contains malformed JSON.`
|
|
1984
|
+
}
|
|
1985
|
+
];
|
|
1986
|
+
}
|
|
1987
|
+
const shape = section.kind === "vitals" ? validateEncounterVitals(parsed, fieldId, section.id) : section.kind === "assessment" ? validateEncounterAssessment(
|
|
1988
|
+
parsed,
|
|
1989
|
+
fieldId,
|
|
1990
|
+
section.id,
|
|
1991
|
+
externalConcerns
|
|
1992
|
+
) : section.kind === "conditions" ? validateEncounterConditions(parsed, fieldId, section.id) : section.kind === "plan" ? validateEncounterPlan(parsed, fieldId, section.id) : section.kind === "medications" ? validateEncounterMedicationValue(parsed, fieldId, section.id) : validateEncounterAllergyValue(parsed, fieldId, section.id);
|
|
1993
|
+
return shape.length || !record2(parsed) ? shape : validateCoding(section, parsed, fieldId, externalConcerns);
|
|
1994
|
+
}
|
|
1995
|
+
function selectedValue(response) {
|
|
1996
|
+
return record2(response?.selected) && typeof response.selected.value === "string" ? response.selected.value : void 0;
|
|
1997
|
+
}
|
|
1998
|
+
function responseHasContent(response) {
|
|
1999
|
+
return nonempty2(response?.answer) || nonempty2(selectedValue(response));
|
|
2000
|
+
}
|
|
2001
|
+
function customHasContent(section, answer) {
|
|
2002
|
+
const parsed = parseJson(answer);
|
|
2003
|
+
if (!record2(parsed)) return false;
|
|
2004
|
+
if (section.kind === "vitals")
|
|
2005
|
+
return parseEncounterVitals(answer).readings.some(
|
|
2006
|
+
(reading) => VITAL_KEYS.some((key) => measurement(reading[key]))
|
|
2007
|
+
);
|
|
2008
|
+
if (section.kind === "medications")
|
|
2009
|
+
return Array.isArray(parsed.medications) && parsed.medications.length > 0;
|
|
2010
|
+
if (section.kind === "allergies")
|
|
2011
|
+
return Array.isArray(parsed.allergies) && parsed.allergies.length > 0 || parsed.noKnownAllergies === true;
|
|
2012
|
+
if (section.kind === "conditions")
|
|
2013
|
+
return Array.isArray(parsed.concerns) && parsed.concerns.length > 0;
|
|
2014
|
+
return Array.isArray(parsed.items) && parsed.items.length > 0 || Array.isArray(parsed.orders) && parsed.orders.length > 0;
|
|
2015
|
+
}
|
|
2016
|
+
function getEncounterConcerns(definition, responses) {
|
|
2017
|
+
const concerns = /* @__PURE__ */ new Map();
|
|
2018
|
+
const sections = [...definition.sections].sort(
|
|
2019
|
+
(a, b) => Number(b.kind === "conditions") - Number(a.kind === "conditions")
|
|
2020
|
+
);
|
|
2021
|
+
for (const section of sections) {
|
|
2022
|
+
if (section.kind !== "conditions" && section.kind !== "assessment")
|
|
2023
|
+
continue;
|
|
2024
|
+
const value = parseJson(
|
|
2025
|
+
responses[getEncounterFieldId(section.id, "value")]?.answer
|
|
2026
|
+
);
|
|
2027
|
+
if (!record2(value) || validateEncounterConditions({ concerns: value.concerns }).length)
|
|
2028
|
+
continue;
|
|
2029
|
+
for (const concern of value.concerns)
|
|
2030
|
+
if (!concerns.has(concern.concernId))
|
|
2031
|
+
concerns.set(concern.concernId, concern);
|
|
2032
|
+
}
|
|
2033
|
+
return [...concerns.values()];
|
|
2034
|
+
}
|
|
2035
|
+
function validateEncounterResponses(definition, responses) {
|
|
2036
|
+
validateEncounterDefinition(definition);
|
|
2037
|
+
const externalConcerns = getEncounterConcerns(definition, responses);
|
|
2038
|
+
const issues = [];
|
|
2039
|
+
const add = (sectionId, fieldId, code, message) => issues.push({ sectionId, fieldId, code, message });
|
|
2040
|
+
for (const section of definition.sections) {
|
|
2041
|
+
const ids = getEncounterSectionFieldIds(section);
|
|
2042
|
+
for (const fieldId of ids) {
|
|
2043
|
+
const response = responses[fieldId];
|
|
2044
|
+
if (response !== void 0 && (!record2(response) || response.answer !== void 0 && typeof response.answer !== "string")) {
|
|
2045
|
+
add(
|
|
2046
|
+
section.id,
|
|
2047
|
+
fieldId,
|
|
2048
|
+
"invalid-response",
|
|
2049
|
+
"An eSheet response must be an object with a string answer."
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
if (section.kind === "narrative" || section.narrative) {
|
|
2054
|
+
const fieldId = getEncounterFieldId(section.id);
|
|
2055
|
+
const response = responses[fieldId];
|
|
2056
|
+
if (response?.selected !== void 0)
|
|
2057
|
+
add(
|
|
2058
|
+
section.id,
|
|
2059
|
+
fieldId,
|
|
2060
|
+
"invalid-response",
|
|
2061
|
+
"Narrative fields store text in answer."
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
if (section.kind === "observations") {
|
|
2065
|
+
for (const observation of section.observations ?? []) {
|
|
2066
|
+
const fieldId = getEncounterFieldId(section.id, observation.id);
|
|
2067
|
+
const response = responses[fieldId];
|
|
2068
|
+
const hasContent = responseHasContent(response);
|
|
2069
|
+
if (observation.required && !hasContent)
|
|
2070
|
+
add(
|
|
2071
|
+
section.id,
|
|
2072
|
+
fieldId,
|
|
2073
|
+
"required",
|
|
2074
|
+
`${observation.label} is required.`
|
|
2075
|
+
);
|
|
2076
|
+
if (observation.type === "choice") {
|
|
2077
|
+
if (response?.selected !== void 0 && (!record2(response.selected) || typeof response.selected.id !== "string" || typeof response.selected.value !== "string" || !observation.options?.includes(response.selected.value) || response.selected.id !== encodeURIComponent(response.selected.value))) {
|
|
2078
|
+
add(
|
|
2079
|
+
section.id,
|
|
2080
|
+
fieldId,
|
|
2081
|
+
"invalid-choice",
|
|
2082
|
+
`${observation.label} must use a configured option.`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
if (nonempty2(response?.answer))
|
|
2086
|
+
add(
|
|
2087
|
+
section.id,
|
|
2088
|
+
fieldId,
|
|
2089
|
+
"invalid-response",
|
|
2090
|
+
"Choice observations store the selected option in selected."
|
|
2091
|
+
);
|
|
2092
|
+
} else {
|
|
2093
|
+
if (response?.selected !== void 0)
|
|
2094
|
+
add(
|
|
2095
|
+
section.id,
|
|
2096
|
+
fieldId,
|
|
2097
|
+
"invalid-response",
|
|
2098
|
+
"Text and numeric observations store their value in answer."
|
|
2099
|
+
);
|
|
2100
|
+
if (observation.type === "number" && nonempty2(response?.answer) && !measurement(Number(response.answer.trim()))) {
|
|
2101
|
+
add(
|
|
2102
|
+
section.id,
|
|
2103
|
+
fieldId,
|
|
2104
|
+
"invalid-number",
|
|
2105
|
+
`${observation.label} must be a finite nonnegative number.`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
} else if (section.kind !== "narrative") {
|
|
2111
|
+
const fieldId = getEncounterFieldId(section.id, "value");
|
|
2112
|
+
const response = responses[fieldId];
|
|
2113
|
+
if (response?.selected !== void 0)
|
|
2114
|
+
add(
|
|
2115
|
+
section.id,
|
|
2116
|
+
fieldId,
|
|
2117
|
+
"invalid-response",
|
|
2118
|
+
"Structured sections store serialized JSON in answer."
|
|
2119
|
+
);
|
|
2120
|
+
if (nonempty2(response?.answer))
|
|
2121
|
+
issues.push(
|
|
2122
|
+
...validateCustom(section, response.answer, fieldId, externalConcerns)
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
if (section.required) {
|
|
2126
|
+
const hasContent = section.kind === "narrative" || section.kind === "observations" ? ids.some((id) => responseHasContent(responses[id])) : section.narrative && responseHasContent(responses[getEncounterFieldId(section.id)]) || customHasContent(
|
|
2127
|
+
section,
|
|
2128
|
+
responses[getEncounterFieldId(section.id, "value")]?.answer
|
|
2129
|
+
);
|
|
2130
|
+
if (!hasContent)
|
|
2131
|
+
add(
|
|
2132
|
+
section.id,
|
|
2133
|
+
ids[0],
|
|
2134
|
+
"required",
|
|
2135
|
+
`${section.title} requires at least one recorded answer.`
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
const concernIds = new Set(
|
|
2140
|
+
externalConcerns.map((concern) => concern.concernId)
|
|
2141
|
+
);
|
|
2142
|
+
for (const section of definition.sections) {
|
|
2143
|
+
if (section.kind !== "plan") continue;
|
|
2144
|
+
const fieldId = getEncounterFieldId(section.id, "value");
|
|
2145
|
+
const parsed = parseJson(responses[fieldId]?.answer);
|
|
2146
|
+
if (!record2(parsed) || !Array.isArray(parsed.orders)) continue;
|
|
2147
|
+
parsed.orders.forEach((order, index) => {
|
|
2148
|
+
if (record2(order) && nonempty2(order.concernId) && !concernIds.has(order.concernId))
|
|
2149
|
+
issues.push({
|
|
2150
|
+
sectionId: section.id,
|
|
2151
|
+
fieldId,
|
|
2152
|
+
code: "invalid-value",
|
|
2153
|
+
path: `orders.${index}.concernId`,
|
|
2154
|
+
message: "A plan order must link to a condition recorded in this visit."
|
|
2155
|
+
});
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
return issues;
|
|
2159
|
+
}
|
|
2160
|
+
function exportVitals(section, answer) {
|
|
2161
|
+
const fieldId = getEncounterFieldId(section.id, "value");
|
|
2162
|
+
return parseEncounterVitals(answer).readings.flatMap((reading) => {
|
|
2163
|
+
const base = {
|
|
2164
|
+
sectionId: section.id,
|
|
2165
|
+
fieldId,
|
|
2166
|
+
groupId: reading.id,
|
|
2167
|
+
recordedAt: reading.recordedAt,
|
|
2168
|
+
position: reading.position,
|
|
2169
|
+
site: reading.site
|
|
2170
|
+
};
|
|
2171
|
+
const observations = [];
|
|
2172
|
+
if (measurement(reading.systolic) || measurement(reading.diastolic)) {
|
|
2173
|
+
observations.push({
|
|
2174
|
+
...base,
|
|
2175
|
+
label: "Blood pressure",
|
|
2176
|
+
value: `${measurement(reading.systolic) ? reading.systolic : "?"}/${measurement(reading.diastolic) ? reading.diastolic : "?"}`,
|
|
2177
|
+
unit: "mmHg",
|
|
2178
|
+
code: loinc("85354-9", "Blood pressure panel"),
|
|
2179
|
+
components: [
|
|
2180
|
+
...measurement(reading.systolic) ? [
|
|
2181
|
+
{
|
|
2182
|
+
label: "Systolic",
|
|
2183
|
+
value: reading.systolic,
|
|
2184
|
+
unit: "mmHg",
|
|
2185
|
+
code: loinc("8480-6", "Systolic blood pressure")
|
|
2186
|
+
}
|
|
2187
|
+
] : [],
|
|
2188
|
+
...measurement(reading.diastolic) ? [
|
|
2189
|
+
{
|
|
2190
|
+
label: "Diastolic",
|
|
2191
|
+
value: reading.diastolic,
|
|
2192
|
+
unit: "mmHg",
|
|
2193
|
+
code: loinc("8462-4", "Diastolic blood pressure")
|
|
2194
|
+
}
|
|
2195
|
+
] : []
|
|
2196
|
+
]
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
for (const vital of VITALS) {
|
|
2200
|
+
const value = reading[vital.key];
|
|
2201
|
+
if (measurement(value) && (vital.key !== "pain" || value <= 10) && (vital.key !== "oxygenSaturation" || value <= 100))
|
|
2202
|
+
observations.push({
|
|
2203
|
+
...base,
|
|
2204
|
+
label: vital.label,
|
|
2205
|
+
value,
|
|
2206
|
+
unit: vital.unit,
|
|
2207
|
+
code: loinc(vital.code, vital.label)
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
return observations;
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
function exportCustom(section, answer, externalConcerns = []) {
|
|
2214
|
+
if (section.kind === "vitals") return exportVitals(section, answer);
|
|
2215
|
+
if (!nonempty2(answer) || validateCustom(
|
|
2216
|
+
{ ...section, requireCoding: false },
|
|
2217
|
+
answer,
|
|
2218
|
+
getEncounterFieldId(section.id, "value"),
|
|
2219
|
+
externalConcerns
|
|
2220
|
+
).length > 0)
|
|
2221
|
+
return [];
|
|
2222
|
+
const parsed = parseJson(answer);
|
|
2223
|
+
const base = {
|
|
2224
|
+
sectionId: section.id,
|
|
2225
|
+
fieldId: getEncounterFieldId(section.id, "value")
|
|
2226
|
+
};
|
|
2227
|
+
if (section.kind === "medications") {
|
|
2228
|
+
return parsed.medications.map((medication) => ({
|
|
2229
|
+
...base,
|
|
2230
|
+
label: "Medication",
|
|
2231
|
+
value: [
|
|
2232
|
+
medication.name,
|
|
2233
|
+
medication.sig,
|
|
2234
|
+
`Status: ${medication.status}`,
|
|
2235
|
+
medication.note,
|
|
2236
|
+
(section.requireCoding ?? true) && !medication.code ? "Coding required" : void 0
|
|
2237
|
+
].filter(Boolean).join("; "),
|
|
2238
|
+
code: medication.code,
|
|
2239
|
+
groupId: medication.id
|
|
2240
|
+
}));
|
|
2241
|
+
}
|
|
2242
|
+
if (section.kind === "allergies") {
|
|
2243
|
+
if (parsed.noKnownAllergies === true)
|
|
2244
|
+
return [
|
|
2245
|
+
{
|
|
2246
|
+
...base,
|
|
2247
|
+
label: "Allergies",
|
|
2248
|
+
value: "No known allergies (explicitly recorded)",
|
|
2249
|
+
// HL7 IPA NKA example: AllergyIntolerance/716186003 (explicit absence).
|
|
2250
|
+
code: {
|
|
2251
|
+
system: "http://snomed.info/sct",
|
|
2252
|
+
code: "716186003",
|
|
2253
|
+
display: "No known allergy"
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
];
|
|
2257
|
+
return parsed.allergies.map((allergy) => ({
|
|
2258
|
+
...base,
|
|
2259
|
+
label: allergy.kind === "intolerance" ? "Intolerance" : "Allergy",
|
|
2260
|
+
value: [
|
|
2261
|
+
allergy.allergen,
|
|
2262
|
+
allergy.reaction,
|
|
2263
|
+
allergy.severity,
|
|
2264
|
+
allergy.inactive ? "inactive" : void 0,
|
|
2265
|
+
allergy.note,
|
|
2266
|
+
(section.requireCoding ?? true) && !allergy.code ? "Coding required" : void 0
|
|
2267
|
+
].filter(Boolean).join("; "),
|
|
2268
|
+
code: allergy.code,
|
|
2269
|
+
groupId: allergy.id
|
|
2270
|
+
}));
|
|
2271
|
+
}
|
|
2272
|
+
if (section.kind === "conditions") {
|
|
2273
|
+
return parsed.concerns.flatMap((concern) => {
|
|
2274
|
+
const assertion = currentAssertion(concern);
|
|
2275
|
+
if (!assertion) return [];
|
|
2276
|
+
return [
|
|
2277
|
+
{
|
|
2278
|
+
...base,
|
|
2279
|
+
label: "Condition",
|
|
2280
|
+
value: [
|
|
2281
|
+
assertion.text,
|
|
2282
|
+
`Status: ${concern.clinicalStatus}`,
|
|
2283
|
+
`Verification: ${assertion.verificationStatus}`,
|
|
2284
|
+
assertion.note,
|
|
2285
|
+
(section.requireCoding ?? true) && !assertion.coding?.length ? "Coding required" : void 0
|
|
2286
|
+
].filter(Boolean).join("; "),
|
|
2287
|
+
groupId: concern.concernId,
|
|
2288
|
+
code: assertion.coding?.find((coding) => coding.primary) ?? assertion.coding?.[0]
|
|
2289
|
+
}
|
|
2290
|
+
];
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
const assessment = section.kind === "plan" ? {
|
|
2294
|
+
concerns: [],
|
|
2295
|
+
items: [],
|
|
2296
|
+
orders: parsed.orders
|
|
2297
|
+
} : parsed;
|
|
2298
|
+
const observations = [];
|
|
2299
|
+
for (const item of assessment.items) {
|
|
2300
|
+
const assertion = [...assessment.concerns, ...externalConcerns].find((concern) => concern.concernId === item.concernId)?.assertions.find((assertion2) => assertion2.id === item.assertionId);
|
|
2301
|
+
if (assertion)
|
|
2302
|
+
observations.push({
|
|
2303
|
+
...base,
|
|
2304
|
+
label: "Assessment",
|
|
2305
|
+
value: [
|
|
2306
|
+
assertion.text,
|
|
2307
|
+
`Verification: ${assertion.verificationStatus}`,
|
|
2308
|
+
assertion.note,
|
|
2309
|
+
item.note,
|
|
2310
|
+
(section.requireCoding ?? true) && !assertion.coding?.length ? "Coding required" : void 0
|
|
2311
|
+
].filter(Boolean).join("; "),
|
|
2312
|
+
groupId: item.concernId,
|
|
2313
|
+
code: assertion.coding?.find((coding) => coding.primary) ?? assertion.coding?.[0]
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
for (const order of assessment.orders) {
|
|
2317
|
+
observations.push({
|
|
2318
|
+
...base,
|
|
2319
|
+
label: `${order.type} order`,
|
|
2320
|
+
value: [
|
|
2321
|
+
order.display,
|
|
2322
|
+
order.detail,
|
|
2323
|
+
order.priority,
|
|
2324
|
+
order.timing,
|
|
2325
|
+
order.indication,
|
|
2326
|
+
order.bodySite,
|
|
2327
|
+
order.referTo,
|
|
2328
|
+
order.notes,
|
|
2329
|
+
(section.requireCoding ?? true) && !order.code ? "Coding required" : void 0
|
|
2330
|
+
].filter(Boolean).join("; "),
|
|
2331
|
+
groupId: order.concernId ?? order.orderId,
|
|
2332
|
+
code: order.code ? {
|
|
2333
|
+
system: order.code.codetype,
|
|
2334
|
+
code: order.code.fullcode,
|
|
2335
|
+
display: order.display
|
|
2336
|
+
} : void 0
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
return observations;
|
|
2340
|
+
}
|
|
2341
|
+
function createEncounterSnapshot(definition, responses) {
|
|
2342
|
+
const errors = validateEncounterResponses(definition, responses);
|
|
2343
|
+
const externalConcerns = getEncounterConcerns(definition, responses);
|
|
2344
|
+
const observations = [];
|
|
2345
|
+
for (const section of definition.sections) {
|
|
2346
|
+
if (section.kind === "narrative" || section.narrative) {
|
|
2347
|
+
const fieldId = getEncounterFieldId(section.id);
|
|
2348
|
+
const answer = responses[fieldId]?.answer;
|
|
2349
|
+
if (nonempty2(answer))
|
|
2350
|
+
observations.push({
|
|
2351
|
+
sectionId: section.id,
|
|
2352
|
+
fieldId,
|
|
2353
|
+
label: section.kind === "narrative" ? section.title : `${section.title} narrative`,
|
|
2354
|
+
value: answer
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
if (section.kind === "observations") {
|
|
2358
|
+
for (const observation of section.observations ?? []) {
|
|
2359
|
+
const fieldId = getEncounterFieldId(section.id, observation.id);
|
|
2360
|
+
const response = responses[fieldId];
|
|
2361
|
+
const raw = observation.type === "choice" ? selectedValue(response) : response?.answer;
|
|
2362
|
+
if (!nonempty2(raw)) continue;
|
|
2363
|
+
if (observation.type === "choice" && (!observation.options?.includes(raw) || errors.some(
|
|
2364
|
+
(issue) => issue.fieldId === fieldId && issue.code === "invalid-choice"
|
|
2365
|
+
)))
|
|
2366
|
+
continue;
|
|
2367
|
+
const value = observation.type === "number" ? Number(raw) : raw;
|
|
2368
|
+
if (typeof value === "number" && !measurement(value)) continue;
|
|
2369
|
+
observations.push({
|
|
2370
|
+
sectionId: section.id,
|
|
2371
|
+
fieldId,
|
|
2372
|
+
label: observation.label,
|
|
2373
|
+
value,
|
|
2374
|
+
unit: observation.unit,
|
|
2375
|
+
code: observation.code
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
} else if (section.kind !== "narrative") {
|
|
2379
|
+
observations.push(
|
|
2380
|
+
...exportCustom(
|
|
2381
|
+
section,
|
|
2382
|
+
responses[getEncounterFieldId(section.id, "value")]?.answer,
|
|
2383
|
+
externalConcerns
|
|
2384
|
+
)
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
const note = [
|
|
2389
|
+
definition.title,
|
|
2390
|
+
...definition.sections.flatMap((section) => {
|
|
2391
|
+
const values = observations.filter(
|
|
2392
|
+
(observation) => observation.sectionId === section.id
|
|
2393
|
+
);
|
|
2394
|
+
if (values.length === 0) return [];
|
|
2395
|
+
return [
|
|
2396
|
+
`${section.title}
|
|
2397
|
+
${values.map((observation) => {
|
|
2398
|
+
const metadata = [
|
|
2399
|
+
section.kind === "vitals" && observation.groupId && `Reading ${observation.groupId}`,
|
|
2400
|
+
observation.recordedAt,
|
|
2401
|
+
observation.position,
|
|
2402
|
+
observation.site
|
|
2403
|
+
].filter(Boolean);
|
|
2404
|
+
const unit = observation.unit === "Cel" ? "\xB0C" : observation.unit === "mm[Hg]" ? "mmHg" : observation.unit === "{score}" ? "/10" : observation.unit;
|
|
2405
|
+
return `${observation.label}: ${observation.value}${unit ? ` ${unit}` : ""}${metadata.length ? ` (${metadata.join("; ")})` : ""}`;
|
|
2406
|
+
}).join("\n")}`
|
|
2407
|
+
];
|
|
2408
|
+
})
|
|
2409
|
+
].join("\n\n");
|
|
2410
|
+
const snapshot = {
|
|
2411
|
+
definition,
|
|
2412
|
+
responses,
|
|
2413
|
+
observations,
|
|
2414
|
+
note,
|
|
2415
|
+
errors
|
|
2416
|
+
};
|
|
2417
|
+
return globalThis.structuredClone({
|
|
2418
|
+
...snapshot,
|
|
2419
|
+
mdy: createEncounterMdy(snapshot)
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
export { DEFAULT_ENCOUNTER_VISIT_DEFINITION, ENCOUNTER_DOCUMENT_FIELD_ID, EXAMPLE_ENCOUNTER_VISIT_RESPONSES, createEncounterFormDefinition, createEncounterMdy, createEncounterSnapshot, encounterDocumentResponse, getEncounterConcerns, getEncounterDocumentBody, getEncounterFieldId, getEncounterMdyFields, getEncounterSectionFieldId, getEncounterSectionFieldIds, isEncounterDate, parseEncounterAssessment, parseEncounterConditions, parseEncounterMdy, parseEncounterPlan, parseEncounterVitals, refreshEncounterMdyLinks, renderEncounterMdy, updateEncounterDocumentBody, validAssessmentItem, validateEncounterAllergyValue, validateEncounterAssessment, validateEncounterConditions, validateEncounterDefinition, validateEncounterMedicationValue, validateEncounterPlan, validateEncounterResponses, validateEncounterVitals };
|
|
2424
|
+
//# sourceMappingURL=chunk-7HFZL5V5.js.map
|
|
2425
|
+
//# sourceMappingURL=chunk-7HFZL5V5.js.map
|