@claudebernard/node-fhir-mapper 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -0
- package/README.md +512 -0
- package/dist/index.d.ts +327 -0
- package/dist/index.js +1751 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1751 @@
|
|
|
1
|
+
const unitsOfTime = {
|
|
2
|
+
's': { value: 0, label: 'seconde' },
|
|
3
|
+
'min': { value: 10, label: 'minute' },
|
|
4
|
+
'h': { value: 20, label: 'heure' },
|
|
5
|
+
'd': { value: 40, label: 'jour' },
|
|
6
|
+
'wk': { value: 50, label: 'semaine' },
|
|
7
|
+
'mo': { value: 70, label: 'mois' },
|
|
8
|
+
'a': { value: 100, label: 'an' }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Converts a CB Posology Bean to BCB Posologie Structuree2 format
|
|
13
|
+
* Based on the Java constructor BCBPosologieStructuree2(PosologyBean poso)
|
|
14
|
+
* Maps all fields from the Java constructor
|
|
15
|
+
*/
|
|
16
|
+
function convertCBToBCB(poso) {
|
|
17
|
+
return {
|
|
18
|
+
// Direct mappings from the Java constructor - exact field mapping
|
|
19
|
+
idProduit: poso.productId,
|
|
20
|
+
codeTerrain: poso.code,
|
|
21
|
+
codeIndication: poso.indicationCode,
|
|
22
|
+
codeNature: poso.typeCode,
|
|
23
|
+
codeVoie: poso.routeCode,
|
|
24
|
+
codeProfil: poso.profilCode,
|
|
25
|
+
quantite1: poso.quantity1,
|
|
26
|
+
quantite2: poso.quantity2,
|
|
27
|
+
codeUnitePrise: poso.intakeUnitCode,
|
|
28
|
+
parKilo: poso.perKilo,
|
|
29
|
+
adequationUP: poso.adequacy,
|
|
30
|
+
codePar: poso.byCode,
|
|
31
|
+
combien1: poso.howMuch1,
|
|
32
|
+
combien2: poso.howMuch2,
|
|
33
|
+
tousLes: poso.every,
|
|
34
|
+
codeDuree1: poso.duration1Code,
|
|
35
|
+
codeDuree2: poso.duration2Code,
|
|
36
|
+
codeDuree3: poso.duration3Code,
|
|
37
|
+
codeMoment: poso.momentCode,
|
|
38
|
+
pendant1: poso.during1,
|
|
39
|
+
pendant2: poso.during2,
|
|
40
|
+
maximum: poso.maximum,
|
|
41
|
+
maximumPoids: poso.weightMaximum,
|
|
42
|
+
codeSpecifPrise1: poso.intakeSpecification1Code,
|
|
43
|
+
codeSpecifPrise2: poso.intakeSpecification2Code,
|
|
44
|
+
nbUnites: poso.numberOfUnits,
|
|
45
|
+
coeffMoment: poso.momentCoeff,
|
|
46
|
+
implicite: poso.implicit,
|
|
47
|
+
// Additional label fields for completeness (not in Java constructor but useful)
|
|
48
|
+
noPosologie: poso.patientPosology,
|
|
49
|
+
libellePosologie: poso.posologyLabel,
|
|
50
|
+
libIndication: poso.indicationLabel,
|
|
51
|
+
libVoie: poso.routeLabel,
|
|
52
|
+
libUnitePrise: poso.intakeUnitLabel,
|
|
53
|
+
libDuree1: poso.duration1Label,
|
|
54
|
+
libDuree2: poso.duration2Label,
|
|
55
|
+
libDuree3: poso.duration3Label,
|
|
56
|
+
libSpecifPrise1: poso.intakeSpecification1Label
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Converts BCB Posologie Structuree2 back to CB Posology Bean format.
|
|
61
|
+
* This is the reverse of the convertCBToBCB function.
|
|
62
|
+
* @param bcbPosology BCB Posologie Structuree2 object
|
|
63
|
+
* @returns CB Posology Bean object
|
|
64
|
+
*/
|
|
65
|
+
function convertBCBToCB(bcbPosology) {
|
|
66
|
+
return {
|
|
67
|
+
// Reverse mapping from BCB to CB (Java constructor mapping in reverse)
|
|
68
|
+
productId: bcbPosology.idProduit ?? undefined,
|
|
69
|
+
code: bcbPosology.codeTerrain ?? undefined,
|
|
70
|
+
indicationCode: bcbPosology.codeIndication ?? undefined,
|
|
71
|
+
typeCode: bcbPosology.codeNature ?? undefined,
|
|
72
|
+
routeCode: bcbPosology.codeVoie ?? undefined,
|
|
73
|
+
profilCode: bcbPosology.codeProfil ?? undefined,
|
|
74
|
+
quantity1: bcbPosology.quantite1 ?? undefined,
|
|
75
|
+
quantity2: bcbPosology.quantite2 ?? undefined,
|
|
76
|
+
intakeUnitCode: bcbPosology.codeUnitePrise ?? undefined,
|
|
77
|
+
perKilo: bcbPosology.parKilo ?? undefined,
|
|
78
|
+
adequacy: bcbPosology.adequationUP ?? undefined,
|
|
79
|
+
byCode: bcbPosology.codePar ?? undefined,
|
|
80
|
+
howMuch1: bcbPosology.combien1 ?? undefined,
|
|
81
|
+
howMuch2: bcbPosology.combien2 ?? undefined,
|
|
82
|
+
every: bcbPosology.tousLes ?? undefined,
|
|
83
|
+
duration1Code: bcbPosology.codeDuree1 ?? undefined,
|
|
84
|
+
duration2Code: bcbPosology.codeDuree2 ?? undefined,
|
|
85
|
+
duration3Code: bcbPosology.codeDuree3 ?? undefined,
|
|
86
|
+
momentCode: bcbPosology.codeMoment ?? undefined,
|
|
87
|
+
during1: bcbPosology.pendant1 ?? undefined,
|
|
88
|
+
during2: bcbPosology.pendant2 ?? undefined,
|
|
89
|
+
maximum: bcbPosology.maximum ?? undefined,
|
|
90
|
+
weightMaximum: bcbPosology.maximumPoids ?? undefined,
|
|
91
|
+
intakeSpecification1Code: bcbPosology.codeSpecifPrise1 ?? undefined,
|
|
92
|
+
intakeSpecification2Code: bcbPosology.codeSpecifPrise2 ?? undefined,
|
|
93
|
+
numberOfUnits: bcbPosology.nbUnites ?? undefined,
|
|
94
|
+
momentCoeff: bcbPosology.coeffMoment ?? undefined,
|
|
95
|
+
implicit: bcbPosology.implicite ?? undefined,
|
|
96
|
+
// Label fields (reverse mapping)
|
|
97
|
+
patientPosology: bcbPosology.noPosologie ?? undefined,
|
|
98
|
+
posologyLabel: bcbPosology.libellePosologie ?? undefined,
|
|
99
|
+
indicationLabel: bcbPosology.libIndication ?? undefined,
|
|
100
|
+
routeLabel: bcbPosology.libVoie ?? undefined,
|
|
101
|
+
intakeUnitLabel: bcbPosology.libUnitePrise ?? undefined,
|
|
102
|
+
duration1Label: bcbPosology.libDuree1 ?? undefined,
|
|
103
|
+
duration2Label: bcbPosology.libDuree2 ?? undefined,
|
|
104
|
+
duration3Label: bcbPosology.libDuree3 ?? undefined,
|
|
105
|
+
intakeSpecification1Label: bcbPosology.libSpecifPrise1 ?? undefined
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildDoseAndRate(dosage) {
|
|
110
|
+
const coding = {
|
|
111
|
+
system: dosage.codeUnitePrise ? 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units' : undefined,
|
|
112
|
+
code: dosage.codeUnitePrise ?? undefined,
|
|
113
|
+
unit: dosage.codeUnitePrise ? dosage.libUnitePrise : undefined
|
|
114
|
+
};
|
|
115
|
+
const doseAndRate = dosage.quantite2 ? {
|
|
116
|
+
doseRange: {
|
|
117
|
+
low: {
|
|
118
|
+
value: dosage.quantite1,
|
|
119
|
+
...coding
|
|
120
|
+
},
|
|
121
|
+
high: {
|
|
122
|
+
value: dosage.quantite2,
|
|
123
|
+
...coding
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} : {
|
|
127
|
+
doseQuantity: {
|
|
128
|
+
value: dosage.quantite1,
|
|
129
|
+
...coding
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return [doseAndRate];
|
|
133
|
+
}
|
|
134
|
+
// The formula used here is count = dose * frequency * (duration / period)
|
|
135
|
+
function calculateCount(dose, frequency, period, periodUnit, duration, durationUnit) {
|
|
136
|
+
// Provide default values for null or undefined parameters
|
|
137
|
+
const actualDose = dose ?? 0;
|
|
138
|
+
const actualFrequency = frequency ?? 0;
|
|
139
|
+
const actualPeriod = period ?? 1;
|
|
140
|
+
const actualDuration = duration ?? 0;
|
|
141
|
+
const actualPeriodUnit = periodUnit || 'd'; // Default to 'd' (days) if not provided
|
|
142
|
+
const actualDurationUnit = durationUnit || 'd'; // Default to 'd' (days) if not provided
|
|
143
|
+
// Convert duration to the same unit as period
|
|
144
|
+
let convertedDuration = actualDuration;
|
|
145
|
+
if (actualDurationUnit !== actualPeriodUnit && actualDuration && actualDurationUnit && actualPeriodUnit) {
|
|
146
|
+
convertedDuration = convertTime(actualDuration, actualDurationUnit, actualPeriodUnit);
|
|
147
|
+
}
|
|
148
|
+
// Check if dose, frequency, convertedDuration, or period is null or 0 and compute count accordingly
|
|
149
|
+
if (!actualDose) {
|
|
150
|
+
return actualFrequency * (convertedDuration / actualPeriod);
|
|
151
|
+
}
|
|
152
|
+
else if (!actualFrequency) {
|
|
153
|
+
return actualDose * (convertedDuration / actualPeriod);
|
|
154
|
+
}
|
|
155
|
+
else if (!convertedDuration) {
|
|
156
|
+
return actualDose * actualFrequency * actualPeriod;
|
|
157
|
+
}
|
|
158
|
+
else if (!actualPeriod) {
|
|
159
|
+
return actualDose * actualFrequency * convertedDuration;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
return actualDose * actualFrequency * (convertedDuration / actualPeriod);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function convertTime(value, fromUnit, toUnit) {
|
|
166
|
+
const conversionFactors = {
|
|
167
|
+
's': 1,
|
|
168
|
+
'min': 60,
|
|
169
|
+
'h': 3600,
|
|
170
|
+
'd': 86400,
|
|
171
|
+
'wk': 604800,
|
|
172
|
+
'mo': 2628000,
|
|
173
|
+
'a': 31536000
|
|
174
|
+
};
|
|
175
|
+
if (!conversionFactors[fromUnit]) {
|
|
176
|
+
throw new Error(`Invalid fromUnit: ${fromUnit}`);
|
|
177
|
+
}
|
|
178
|
+
if (!conversionFactors[toUnit]) {
|
|
179
|
+
throw new Error(`Invalid toUnit: ${toUnit}`);
|
|
180
|
+
}
|
|
181
|
+
// Convert the input value to seconds
|
|
182
|
+
const valueInSeconds = value * conversionFactors[fromUnit];
|
|
183
|
+
// Convert from seconds to the desired unit
|
|
184
|
+
const convertedValue = valueInSeconds / conversionFactors[toUnit];
|
|
185
|
+
return convertedValue;
|
|
186
|
+
}
|
|
187
|
+
async function handleCodifications(dosage, indicationMapper, routeMapper, intakeMapper) {
|
|
188
|
+
let conditionCode;
|
|
189
|
+
let conditionLabel;
|
|
190
|
+
let intakeCode;
|
|
191
|
+
let intakeLabel;
|
|
192
|
+
let routeCode;
|
|
193
|
+
let routeLabel;
|
|
194
|
+
let errors = [];
|
|
195
|
+
if (dosage.asNeededFor && dosage.asNeededFor[0].coding && dosage.asNeededFor[0].coding.length > 0) {
|
|
196
|
+
if (dosage.asNeededFor[0]?.coding?.[0]?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies') {
|
|
197
|
+
conditionCode = dosage.asNeededFor?.[0]?.coding?.[0]?.code;
|
|
198
|
+
conditionLabel = dosage.asNeededFor?.[0]?.coding?.[0]?.display;
|
|
199
|
+
}
|
|
200
|
+
else if (indicationMapper) {
|
|
201
|
+
const result = await indicationMapper(dosage?.asNeededFor?.[0]?.coding?.[0]);
|
|
202
|
+
let indication = undefined;
|
|
203
|
+
if (Array.isArray(result)) {
|
|
204
|
+
if (result.length > 0)
|
|
205
|
+
indication = result[0];
|
|
206
|
+
}
|
|
207
|
+
else if (result) {
|
|
208
|
+
indication = result;
|
|
209
|
+
}
|
|
210
|
+
if (indication) {
|
|
211
|
+
conditionCode = indication.code;
|
|
212
|
+
conditionLabel = indication.label;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
conditionCode = undefined;
|
|
216
|
+
conditionLabel = undefined;
|
|
217
|
+
errors.push({
|
|
218
|
+
field: 'codeIndication',
|
|
219
|
+
message: 'No mapping found for indication code'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
errors.push({
|
|
225
|
+
field: 'codeIndication',
|
|
226
|
+
message: 'The system used in the asNeededFor coding field is not supported and no custom mapper was provided'
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (dosage.route && dosage.route.coding && dosage.route.coding.length > 0) {
|
|
231
|
+
if (dosage.route?.coding?.[0]?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-routes') {
|
|
232
|
+
routeCode = parseInt(dosage?.route?.coding?.[0]?.code ?? '');
|
|
233
|
+
routeLabel = dosage.route?.coding?.[0]?.display;
|
|
234
|
+
}
|
|
235
|
+
else if (dosage.route && routeMapper) {
|
|
236
|
+
const result = await routeMapper(dosage.route?.coding?.[0]);
|
|
237
|
+
let route = undefined;
|
|
238
|
+
if (Array.isArray(result)) {
|
|
239
|
+
if (result.length > 0)
|
|
240
|
+
route = result[0];
|
|
241
|
+
}
|
|
242
|
+
else if (result) {
|
|
243
|
+
route = result;
|
|
244
|
+
}
|
|
245
|
+
if (route) {
|
|
246
|
+
routeCode = parseInt(route.code);
|
|
247
|
+
routeLabel = route.label;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
routeCode = undefined;
|
|
251
|
+
routeLabel = undefined;
|
|
252
|
+
errors.push({
|
|
253
|
+
field: 'codeVoie',
|
|
254
|
+
message: 'No mapping found for route code'
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
errors.push({
|
|
260
|
+
field: 'codeVoie',
|
|
261
|
+
message: 'The system used in the route coding field is not supported and no custom mapper was provided'
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (dosage.doseAndRate && dosage.doseAndRate.length > 0) {
|
|
266
|
+
if (dosage.doseAndRate?.[0]?.doseQuantity?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units') {
|
|
267
|
+
intakeCode = parseInt(dosage.doseAndRate?.[0]?.doseQuantity.code ?? '');
|
|
268
|
+
intakeLabel = dosage.doseAndRate?.[0]?.doseQuantity.unit;
|
|
269
|
+
}
|
|
270
|
+
else if (dosage.doseAndRate && intakeMapper) {
|
|
271
|
+
const result = await intakeMapper(dosage.doseAndRate?.[0]?.doseQuantity);
|
|
272
|
+
let intake = undefined;
|
|
273
|
+
if (Array.isArray(result)) {
|
|
274
|
+
if (result.length > 0)
|
|
275
|
+
intake = result[0];
|
|
276
|
+
}
|
|
277
|
+
else if (result) {
|
|
278
|
+
intake = result;
|
|
279
|
+
}
|
|
280
|
+
if (intake) {
|
|
281
|
+
intakeCode = parseInt(intake.code);
|
|
282
|
+
intakeLabel = intake.label;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
intakeCode = undefined;
|
|
286
|
+
intakeLabel = undefined;
|
|
287
|
+
errors.push({
|
|
288
|
+
field: 'codeUnitePrise',
|
|
289
|
+
message: 'No mapping found for intake code'
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
errors.push({
|
|
295
|
+
field: 'codeUnitePrise',
|
|
296
|
+
message: 'The system used in the doseQuantity coding field is not supported and no custom mapper was provided'
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
conditionCode,
|
|
302
|
+
conditionLabel,
|
|
303
|
+
intakeCode,
|
|
304
|
+
intakeLabel,
|
|
305
|
+
routeCode,
|
|
306
|
+
routeLabel,
|
|
307
|
+
codificationErrors: errors
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// adding this function to support new naming convention without breaking changes
|
|
312
|
+
async function fhirToBcb$2(dosageInstructions, indicationMapper, routeMapper, intakeMapper) {
|
|
313
|
+
return await mapToBcb(dosageInstructions, indicationMapper, routeMapper, intakeMapper);
|
|
314
|
+
}
|
|
315
|
+
async function mapToBcb(dosageInstructions, indicationMapper, routeMapper, intakeMapper) {
|
|
316
|
+
if (!dosageInstructions || dosageInstructions.length === 0) {
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
result: undefined,
|
|
320
|
+
errors: [{
|
|
321
|
+
field: 'root',
|
|
322
|
+
message: 'Invalid Dosage array'
|
|
323
|
+
}]
|
|
324
|
+
}
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
const safeIndicationMapper = wrapMaybeAsync$1(indicationMapper);
|
|
328
|
+
const safeRouteMapper = wrapMaybeAsync$1(routeMapper);
|
|
329
|
+
const safeIntakeMapper = wrapMaybeAsync$1(intakeMapper);
|
|
330
|
+
const results = [];
|
|
331
|
+
for (const dosage of dosageInstructions) {
|
|
332
|
+
let errors = [];
|
|
333
|
+
const periodUnit = dosage.timing?.repeat?.periodUnit;
|
|
334
|
+
const quantite1 = dosage?.doseAndRate?.[0]?.doseQuantity?.value ?? dosage?.doseAndRate?.[0]?.doseRange?.low?.value;
|
|
335
|
+
const quantite2 = dosage?.doseAndRate?.[0]?.doseRange?.high?.value ?? null;
|
|
336
|
+
let maximum = null;
|
|
337
|
+
let codeDuree3 = null;
|
|
338
|
+
let maxDosePerDay = null;
|
|
339
|
+
if (dosage.maxDosePerPeriod) {
|
|
340
|
+
maxDosePerDay = dosage.maxDosePerPeriod.find(period => period?.denominator?.unit === 'd' && period?.denominator?.value === 1);
|
|
341
|
+
if (maxDosePerDay) {
|
|
342
|
+
maximum = maxDosePerDay?.numerator?.value;
|
|
343
|
+
codeDuree3 = maxDosePerDay?.denominator?.unit;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
let libSpecifPrise1 = dosage.asNeeded ? 'selon besoin' : null;
|
|
347
|
+
const doseSpacing = dosage.maxDosePerPeriod?.find(period => period?.denominator?.unit === 'h');
|
|
348
|
+
if (doseSpacing) {
|
|
349
|
+
const spacingText = `en espaçant les prises de ${doseSpacing?.denominator?.value}h minimum`;
|
|
350
|
+
libSpecifPrise1 = libSpecifPrise1 ? `${libSpecifPrise1}, ${spacingText}` : spacingText;
|
|
351
|
+
}
|
|
352
|
+
// handleCodifications must be updated to support async mappers
|
|
353
|
+
const { conditionCode, conditionLabel, intakeCode, intakeLabel, routeCode, routeLabel, codificationErrors } = await handleCodifications(dosage, safeIndicationMapper, safeRouteMapper, safeIntakeMapper);
|
|
354
|
+
const codeDuree2 = dosage?.timing?.repeat?.boundsDuration?.code;
|
|
355
|
+
const bcbDosage = {
|
|
356
|
+
noPosologie: dosage.sequence ?? undefined,
|
|
357
|
+
libellePosologie: dosage.text ?? undefined,
|
|
358
|
+
codeIndication: conditionCode,
|
|
359
|
+
libIndication: conditionLabel,
|
|
360
|
+
codeDuree1: periodUnit ? unitsOfTime[periodUnit]?.value : undefined,
|
|
361
|
+
libDuree1: periodUnit ? unitsOfTime[periodUnit]?.label : undefined,
|
|
362
|
+
quantite1: quantite1 ?? undefined,
|
|
363
|
+
quantite2: quantite2 ?? undefined,
|
|
364
|
+
tousLes: dosage.timing?.repeat?.period ?? undefined,
|
|
365
|
+
combien1: dosage.timing?.repeat?.frequency ?? undefined,
|
|
366
|
+
combien2: dosage.timing?.repeat?.frequencyMax ?? undefined,
|
|
367
|
+
pendant1: dosage.timing?.repeat?.boundsDuration?.value ?? undefined,
|
|
368
|
+
codeUnitePrise: intakeCode,
|
|
369
|
+
libUnitePrise: intakeLabel,
|
|
370
|
+
codeVoie: routeCode,
|
|
371
|
+
libVoie: routeLabel,
|
|
372
|
+
codeDuree2: codeDuree2 ? unitsOfTime[codeDuree2]?.value : undefined,
|
|
373
|
+
libDuree2: codeDuree2 ? unitsOfTime[codeDuree2]?.label : undefined,
|
|
374
|
+
maximum: maximum ?? undefined,
|
|
375
|
+
codeDuree3: codeDuree3 ? unitsOfTime[codeDuree3]?.value : undefined,
|
|
376
|
+
libDuree3: codeDuree3 ? unitsOfTime[codeDuree3]?.label : undefined,
|
|
377
|
+
libSpecifPrise1: libSpecifPrise1 ?? undefined
|
|
378
|
+
};
|
|
379
|
+
results.push({
|
|
380
|
+
result: bcbDosage,
|
|
381
|
+
errors: codificationErrors.concat(errors)
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return results;
|
|
385
|
+
}
|
|
386
|
+
// adding this function to support new naming convention without breaking changes
|
|
387
|
+
function bcbToFhir$2(bcbDosages) {
|
|
388
|
+
return mapToFhir(bcbDosages);
|
|
389
|
+
}
|
|
390
|
+
function mapToFhir(bcbDosages) {
|
|
391
|
+
if (!bcbDosages || bcbDosages.length === 0) {
|
|
392
|
+
throw new Error('Invalid BCBPosologieStructuree2 array');
|
|
393
|
+
}
|
|
394
|
+
return bcbDosages.map((dosage) => {
|
|
395
|
+
const keys = Object.keys(unitsOfTime);
|
|
396
|
+
const periodUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree1) ?? undefined;
|
|
397
|
+
const durationUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree2) ?? undefined;
|
|
398
|
+
const maximumUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree3) ?? undefined;
|
|
399
|
+
const route = dosage.codeVoie ? {
|
|
400
|
+
coding: [{
|
|
401
|
+
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-routes',
|
|
402
|
+
code: dosage.codeVoie.toString(),
|
|
403
|
+
display: dosage.libVoie
|
|
404
|
+
}]
|
|
405
|
+
} : undefined;
|
|
406
|
+
const intake = dosage.codeUnitePrise ? {
|
|
407
|
+
coding: [{
|
|
408
|
+
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units',
|
|
409
|
+
code: dosage.codeUnitePrise.toString(),
|
|
410
|
+
display: dosage.libUnitePrise
|
|
411
|
+
}]
|
|
412
|
+
} : undefined;
|
|
413
|
+
const indication = dosage.codeIndication ? [{
|
|
414
|
+
coding: [{
|
|
415
|
+
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies',
|
|
416
|
+
code: dosage.codeIndication,
|
|
417
|
+
display: dosage.libIndication
|
|
418
|
+
}]
|
|
419
|
+
}] : undefined;
|
|
420
|
+
const maxDosePerPeriod = dosage.maximum ? [{
|
|
421
|
+
numerator: {
|
|
422
|
+
value: dosage.maximum,
|
|
423
|
+
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units',
|
|
424
|
+
code: intake?.coding?.[0]?.code,
|
|
425
|
+
unit: intake?.coding?.[0]?.display
|
|
426
|
+
},
|
|
427
|
+
denominator: {
|
|
428
|
+
value: 1,
|
|
429
|
+
unit: maximumUnit
|
|
430
|
+
}
|
|
431
|
+
}] : undefined;
|
|
432
|
+
const doseAndRate = buildDoseAndRate(dosage);
|
|
433
|
+
const boundsDuration = dosage.pendant1 ? { value: dosage.pendant1, unit: durationUnit } : undefined;
|
|
434
|
+
const asNeeded = dosage.libSpecifPrise1?.includes('besoin');
|
|
435
|
+
return {
|
|
436
|
+
result: {
|
|
437
|
+
text: dosage.libellePosologie,
|
|
438
|
+
sequence: dosage.noPosologie,
|
|
439
|
+
doseAndRate: doseAndRate,
|
|
440
|
+
maxDosePerPeriod: dosage.maximum ? maxDosePerPeriod : undefined,
|
|
441
|
+
route: route,
|
|
442
|
+
asNeeded: asNeeded,
|
|
443
|
+
asNeededFor: indication,
|
|
444
|
+
timing: {
|
|
445
|
+
repeat: {
|
|
446
|
+
frequency: dosage.combien1,
|
|
447
|
+
frequencyMax: dosage.combien2,
|
|
448
|
+
period: dosage.tousLes,
|
|
449
|
+
periodUnit: periodUnit,
|
|
450
|
+
boundsDuration: boundsDuration,
|
|
451
|
+
count: calculateCount(dosage.quantite1, dosage.combien1, dosage.tousLes, periodUnit, dosage.pendant1, durationUnit),
|
|
452
|
+
countMax: calculateCount(dosage.quantite2 ?? dosage.quantite1, dosage.combien2 ?? dosage.combien1, dosage.tousLes, periodUnit, dosage.pendant2 ?? dosage.pendant1, durationUnit)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
errors: []
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
// Utility to wrap a possibly sync or async function so it always returns a Promise
|
|
461
|
+
function wrapMaybeAsync$1(fn) {
|
|
462
|
+
if (!fn)
|
|
463
|
+
return undefined;
|
|
464
|
+
return ((...args) => Promise.resolve(fn(...args)));
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Converts FHIR Dosage instructions to CB Posology Bean format.
|
|
468
|
+
* Uses the existing fhirToBcb function and then converts BCB to CB format.
|
|
469
|
+
* @param dosageInstructions Array of FHIR Dosage resources
|
|
470
|
+
* @param indicationMapper Optional mapper function for indication codes
|
|
471
|
+
* @param routeMapper Optional mapper function for route codes
|
|
472
|
+
* @param intakeMapper Optional mapper function for intake codes
|
|
473
|
+
* @returns Promise of array of CB Posology Beans with mapping responses
|
|
474
|
+
*/
|
|
475
|
+
async function fhirToCb$1(dosageInstructions, indicationMapper, routeMapper, intakeMapper) {
|
|
476
|
+
// First convert FHIR to BCB using existing function
|
|
477
|
+
const bcbResults = await fhirToBcb$2(dosageInstructions, indicationMapper, routeMapper, intakeMapper);
|
|
478
|
+
// Convert each BCB result to CB format
|
|
479
|
+
const cbResults = [];
|
|
480
|
+
for (const bcbResult of bcbResults) {
|
|
481
|
+
if (bcbResult.result) {
|
|
482
|
+
// Convert BCB to CB format (reverse conversion)
|
|
483
|
+
const cbPosology = convertBCBToCB(bcbResult.result);
|
|
484
|
+
cbResults.push({
|
|
485
|
+
result: cbPosology,
|
|
486
|
+
errors: bcbResult.errors || []
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
// If BCB conversion failed, pass through the errors
|
|
491
|
+
cbResults.push({
|
|
492
|
+
result: undefined,
|
|
493
|
+
errors: bcbResult.errors || [{
|
|
494
|
+
field: 'conversion',
|
|
495
|
+
message: 'Failed to convert FHIR to BCB format'
|
|
496
|
+
}]
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return cbResults;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Converts CB Posology Bean format to FHIR Dosage instructions.
|
|
504
|
+
* Converts CB to BCB format first, then uses existing bcbToFhir function.
|
|
505
|
+
* @param cbPosologies Array of CB Posology Beans
|
|
506
|
+
* @returns Array of FHIR Dosage resources with mapping responses
|
|
507
|
+
*/
|
|
508
|
+
function cbToFhir$1(cbPosologies) {
|
|
509
|
+
if (!cbPosologies || cbPosologies.length === 0) {
|
|
510
|
+
return [{
|
|
511
|
+
result: undefined,
|
|
512
|
+
errors: [{
|
|
513
|
+
field: 'root',
|
|
514
|
+
message: 'Invalid CB Posology array'
|
|
515
|
+
}]
|
|
516
|
+
}];
|
|
517
|
+
}
|
|
518
|
+
// Convert each CB to BCB format using the converter
|
|
519
|
+
const bcbPosologies = [];
|
|
520
|
+
const conversionErrors = [];
|
|
521
|
+
for (const [index, cbPosology] of cbPosologies.entries()) {
|
|
522
|
+
try {
|
|
523
|
+
const bcbPosology = convertCBToBCB(cbPosology);
|
|
524
|
+
bcbPosologies.push(bcbPosology);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
conversionErrors.push({
|
|
528
|
+
field: `cbPosology[${index}]`,
|
|
529
|
+
message: `Failed to convert CB to BCB format: ${error}`
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// If there were conversion errors, return them
|
|
534
|
+
if (conversionErrors.length > 0) {
|
|
535
|
+
return [{
|
|
536
|
+
result: undefined,
|
|
537
|
+
errors: conversionErrors
|
|
538
|
+
}];
|
|
539
|
+
}
|
|
540
|
+
// Use existing BCB to FHIR conversion
|
|
541
|
+
return bcbToFhir$2(bcbPosologies);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
var dosageMapper = /*#__PURE__*/Object.freeze({
|
|
545
|
+
__proto__: null,
|
|
546
|
+
bcbToFhir: bcbToFhir$2,
|
|
547
|
+
cbToFhir: cbToFhir$1,
|
|
548
|
+
fhirToBcb: fhirToBcb$2,
|
|
549
|
+
fhirToCb: fhirToCb$1,
|
|
550
|
+
mapToBcb: mapToBcb,
|
|
551
|
+
mapToFhir: mapToFhir
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Simple hash function compatible with both Node.js and browser environments
|
|
556
|
+
* This replaces the Node.js crypto module to avoid browser compatibility issues
|
|
557
|
+
*/
|
|
558
|
+
const simpleHash = (str) => {
|
|
559
|
+
let hash = 0;
|
|
560
|
+
if (str.length === 0)
|
|
561
|
+
return hash.toString(16).padStart(8, '0');
|
|
562
|
+
for (let i = 0; i < str.length; i++) {
|
|
563
|
+
const char = str.charCodeAt(i);
|
|
564
|
+
hash = ((hash << 5) - hash) + char;
|
|
565
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
566
|
+
}
|
|
567
|
+
return Math.abs(hash).toString(16).padStart(8, '0').substring(0, 8);
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* Generate a deterministic 8-character hash from input data
|
|
571
|
+
* Compatible with browser and Node.js environments
|
|
572
|
+
*/
|
|
573
|
+
const generateHash = (data) => {
|
|
574
|
+
const dataString = typeof data === 'string' ? data : JSON.stringify(data);
|
|
575
|
+
return simpleHash(dataString);
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
/* ================== System URLs ================== */
|
|
579
|
+
const LOINC_SYSTEM_URL = 'http://loinc.org';
|
|
580
|
+
const SNOMED_SYSTEM_URL = 'http://snomed.info/sct';
|
|
581
|
+
const ICD10_SYSTEM_URL = 'http://hl7.org/fhir/sid/icd-10';
|
|
582
|
+
const AMM_SYSTEM_URL = 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies';
|
|
583
|
+
const INGREDIENTS_SYSTEM_URL = 'https://platform.claudebernard.fr/fhir/CodeSystem/products-ingredients';
|
|
584
|
+
const UCUM_SYSTEM_URL = 'http://unitsofmeasure.org';
|
|
585
|
+
/* ================== LOINC codes ================== */
|
|
586
|
+
const WEIGHT_LOINC_CODE = '29463-7'; // Body weight
|
|
587
|
+
const HEIGHT_LOINC_CODE = '8302-2'; // Body height
|
|
588
|
+
const PREGNANCY_LOINC_CODE = '82810-3'; // Pregnancy status
|
|
589
|
+
const BREASTFEEDING_LOINC_CODE = '63895-7'; // Breastfeeding
|
|
590
|
+
const AMENORRHEA_LOINC_CODE = '18185-9'; // Gestational age
|
|
591
|
+
const CHILD_PUGH_LOINC_CODE = '98152-2'; // Child-Pugh score
|
|
592
|
+
const CREATININEMIE_MOL_LOINC_CODE = '14682-9'; // Creatinine [Moles/volume] in Serum or Plasma
|
|
593
|
+
const CREATININEMIE_MG_LOINC_CODE = '2160-0'; // Creatinine [Mass/volume] in Serum or Plasma
|
|
594
|
+
const GFR_LOINC_CODE = '69405-9'; // Glomerular filtration rate (absolute, used when writing)
|
|
595
|
+
// LOINC codes for renal function - kept in sync with mvn-fhir-mapper (PatientMapperUtils)
|
|
596
|
+
const ACCEPTED_LOINC_CODES_FOR_RENAL_CLEARANCE = [
|
|
597
|
+
'2163-4', // Creatinine renal clearance
|
|
598
|
+
'2164-2', // Creatinine clearance
|
|
599
|
+
'13441-1', // Creatinine renal clearance/1.73 sq M.predicted
|
|
600
|
+
'13442-9', // Creatinine renal clearance/BSA
|
|
601
|
+
'13443-7', // Creatinine renal clearance/1.73 sq M.predicted by Cockcroft-Gault formula
|
|
602
|
+
'13446-0', // Creatinine renal clearance by 24 hour urine collection
|
|
603
|
+
'13447-8', // Creatinine renal clearance/1.73 sq M.predicted by 24 hour urine collection
|
|
604
|
+
'13449-4', // Creatinine renal clearance/BSA by 24 hour urine collection
|
|
605
|
+
'13450-2', // Creatinine renal clearance by Cockcroft-Gault formula
|
|
606
|
+
'26752-6', // Creatinine renal clearance by MDRD equation
|
|
607
|
+
'35591-7', // Creatinine renal clearance/1.73 sq M.predicted by MDRD equation
|
|
608
|
+
'35594-1', // Creatinine renal clearance/BSA by MDRD equation
|
|
609
|
+
'50380-5', // Creatinine renal clearance/1.73 sq M.predicted by CKD-EPI creatinine equation
|
|
610
|
+
'50381-3', // Creatinine renal clearance/BSA by CKD-EPI creatinine equation
|
|
611
|
+
'104805-7' // Creatinine renal clearance by CKD-EPI creatinine-cystatin C equation
|
|
612
|
+
];
|
|
613
|
+
const ACCEPTED_LOINC_CODES_FOR_RENAL_GFR = [
|
|
614
|
+
'69405-9', // Glomerular filtration rate
|
|
615
|
+
'70969-1' // Glomerular filtration rate by creatinine-based formula
|
|
616
|
+
];
|
|
617
|
+
const ACCEPTED_LOINC_CODES_FOR_RENAL_GFR_PER_BSA = [
|
|
618
|
+
'48642-3', // Glomerular filtration rate/1.73 sq M.predicted by CKD-EPI creatinine equation
|
|
619
|
+
'48643-1', // Glomerular filtration rate/1.73 sq M.predicted by MDRD equation
|
|
620
|
+
'50044-7', // Glomerular filtration rate/1.73 sq M.predicted
|
|
621
|
+
'50210-4', // Glomerular filtration rate/1.73 sq M.predicted by CKD-EPI creatinine-cystatin C equation
|
|
622
|
+
'50384-7', // Glomerular filtration rate/BSA predicted
|
|
623
|
+
'62238-1', // Glomerular filtration rate/1.73 sq M.predicted by Schwartz equation
|
|
624
|
+
'77147-7', // Glomerular filtration rate/1.73 sq M.predicted by CKD-EPI cystatin C equation
|
|
625
|
+
'78006-4', // Glomerular filtration rate/BSA by CKD-EPI creatinine equation
|
|
626
|
+
'88293-6', // Glomerular filtration rate/1.73 sq M.predicted by CKD-EPI creatinine-cystatin C equation
|
|
627
|
+
'88294-4', // Glomerular filtration rate/1.73 sq M.predicted by Full Age Spectrum creatinine equation
|
|
628
|
+
'94677-2', // Glomerular filtration rate/1.73 sq M.predicted by Berlin Initiative Study equation
|
|
629
|
+
'96591-3', // Glomerular filtration rate/1.73 sq M.predicted by European Kidney Function Consortium creatinine equation
|
|
630
|
+
'96592-1', // Glomerular filtration rate/1.73 sq M.predicted by European Kidney Function Consortium creatinine-cystatin C equation
|
|
631
|
+
'98979-8', // Glomerular filtration rate/1.73 sq M.predicted by creatinine-based formula
|
|
632
|
+
'98980-6', // Glomerular filtration rate/BSA by creatinine-based formula
|
|
633
|
+
'102097-3' // Glomerular filtration rate/1.73 sq M.predicted by CKD-EPI creatinine equation (2021)
|
|
634
|
+
];
|
|
635
|
+
/* ================== Child-Pugh ================== */
|
|
636
|
+
const CHILD_PUGH_STAGE_A_CODE = '710065009'; // Child-Pugh score class A
|
|
637
|
+
const CHILD_PUGH_STAGE_B_CODE = '710066005'; // Child-Pugh score class B
|
|
638
|
+
const CHILD_PUGH_STAGE_C_CODE = '710067001'; // Child-Pugh score class C
|
|
639
|
+
/**
|
|
640
|
+
* Child-Pugh stage details, keyed by BCB stage letter.
|
|
641
|
+
* Scores are the representative value of each class (A: < 7, B: 7-9, C: >= 10).
|
|
642
|
+
*/
|
|
643
|
+
const childPughStageMap = {
|
|
644
|
+
'A': { score: 6, snomedCode: CHILD_PUGH_STAGE_A_CODE, snomedDisplay: 'Child-Pugh score class A', stageText: 'Stade A' },
|
|
645
|
+
'B': { score: 8, snomedCode: CHILD_PUGH_STAGE_B_CODE, snomedDisplay: 'Child-Pugh score class B', stageText: 'Stade B' },
|
|
646
|
+
'C': { score: 11, snomedCode: CHILD_PUGH_STAGE_C_CODE, snomedDisplay: 'Child-Pugh score class C', stageText: 'Stade C' }
|
|
647
|
+
};
|
|
648
|
+
const childPughSnomedReverseMap = {
|
|
649
|
+
[CHILD_PUGH_STAGE_A_CODE]: 'A',
|
|
650
|
+
[CHILD_PUGH_STAGE_B_CODE]: 'B',
|
|
651
|
+
[CHILD_PUGH_STAGE_C_CODE]: 'C'
|
|
652
|
+
};
|
|
653
|
+
const genderMap = {
|
|
654
|
+
male: 'M',
|
|
655
|
+
female: 'F'
|
|
656
|
+
};
|
|
657
|
+
const fhirGenderMap = {
|
|
658
|
+
M: 'male',
|
|
659
|
+
F: 'female'
|
|
660
|
+
};
|
|
661
|
+
/** @deprecated See {@link hepaticInsufficiencyMap}. */
|
|
662
|
+
const hepaticInsufficiencyReverseMap = {
|
|
663
|
+
'K72.90': 'A',
|
|
664
|
+
'K72.91': 'B',
|
|
665
|
+
'K72.92': 'C'
|
|
666
|
+
};
|
|
667
|
+
const LEGACY_HEPATIC_ICD10_CODES = Object.keys(hepaticInsufficiencyReverseMap);
|
|
668
|
+
/* ================== Clinical helpers ================== */
|
|
669
|
+
/** Check whether a code belongs to a list of accepted LOINC codes. */
|
|
670
|
+
const isAcceptedLoincCode = (code, acceptedCodes) => !!code && acceptedCodes.includes(code);
|
|
671
|
+
/** True when any coding of the concept carries one of the accepted codes. */
|
|
672
|
+
const hasAcceptedCode = (concept, acceptedCodes) => !!concept?.coding?.some(coding => isAcceptedLoincCode(coding.code, acceptedCodes));
|
|
673
|
+
/**
|
|
674
|
+
* Body Surface Area, Du Bois formula.
|
|
675
|
+
*
|
|
676
|
+
* @param height Height in centimeters
|
|
677
|
+
* @param weight Weight in kilograms
|
|
678
|
+
* @returns BSA in square meters
|
|
679
|
+
*/
|
|
680
|
+
const calculateBSA = (height, weight) => Math.sqrt((height * weight) / 3600);
|
|
681
|
+
/**
|
|
682
|
+
* Convert a GFR normalized to 1.73 m² into an absolute GFR.
|
|
683
|
+
*
|
|
684
|
+
* @param normalizedGFR GFR normalized to 1.73 m²
|
|
685
|
+
* @param bsa Body surface area in m²
|
|
686
|
+
*/
|
|
687
|
+
const normalizedGFRtoAbsolute = (normalizedGFR, bsa) => Math.round((normalizedGFR * (bsa / 1.73)) * 100) / 100;
|
|
688
|
+
/**
|
|
689
|
+
* Convert a Child-Pugh score into its stage letter.
|
|
690
|
+
*
|
|
691
|
+
* @param score Child-Pugh total score
|
|
692
|
+
* @returns 'A' (mild), 'B' (moderate) or 'C' (severe)
|
|
693
|
+
*/
|
|
694
|
+
const childPughScoreToStage = (score) => {
|
|
695
|
+
if (score < 7) {
|
|
696
|
+
return 'A';
|
|
697
|
+
}
|
|
698
|
+
if (score < 10) {
|
|
699
|
+
return 'B';
|
|
700
|
+
}
|
|
701
|
+
return 'C';
|
|
702
|
+
};
|
|
703
|
+
/** Convert a SNOMED CT Child-Pugh class code into its stage letter, or '' when unknown. */
|
|
704
|
+
const snomedCodeToChildPughStage = (snomedCode) => snomedCode ? childPughSnomedReverseMap[snomedCode] ?? '' : '';
|
|
705
|
+
const calculateAgeInMonths = (birthDate) => {
|
|
706
|
+
const birth = new Date(birthDate);
|
|
707
|
+
const today = new Date();
|
|
708
|
+
const yearsDifference = today.getFullYear() - birth.getFullYear();
|
|
709
|
+
const monthsDifference = today.getMonth() - birth.getMonth();
|
|
710
|
+
const daysDifference = today.getDate() - birth.getDate();
|
|
711
|
+
let ageInMonths = yearsDifference * 12 + monthsDifference;
|
|
712
|
+
if (daysDifference < 0) {
|
|
713
|
+
ageInMonths--;
|
|
714
|
+
}
|
|
715
|
+
return ageInMonths;
|
|
716
|
+
};
|
|
717
|
+
const calculateBirthDateFromAgeInMonths = (ageInMonths) => {
|
|
718
|
+
const today = new Date();
|
|
719
|
+
const birthDate = new Date(today.setMonth(today.getMonth() - ageInMonths));
|
|
720
|
+
const year = birthDate.getFullYear();
|
|
721
|
+
const month = (birthDate.getMonth() + 1).toString().padStart(2, '0');
|
|
722
|
+
const day = birthDate.getDate().toString().padStart(2, '0');
|
|
723
|
+
return `${year}-${month}-${day}`;
|
|
724
|
+
};
|
|
725
|
+
const generatePatientId = (patientData) => {
|
|
726
|
+
// Create a consistent hash based on patient data
|
|
727
|
+
const dataToHash = {
|
|
728
|
+
age: patientData.age,
|
|
729
|
+
gender: 'sexe' in patientData ? patientData.sexe : patientData.gender,
|
|
730
|
+
weight: 'poids' in patientData ? patientData.poids : patientData.weight,
|
|
731
|
+
height: 'taille' in patientData ? patientData.taille : patientData.height,
|
|
732
|
+
pregnancy: 'grossesse' in patientData ? patientData.grossesse : patientData.pregnancy,
|
|
733
|
+
breastfeeding: 'allaitement' in patientData ? patientData.allaitement : patientData.breastfeeding
|
|
734
|
+
};
|
|
735
|
+
return generateHash(dataToHash);
|
|
736
|
+
};
|
|
737
|
+
const convertPatient = (bcbPatient) => {
|
|
738
|
+
return {
|
|
739
|
+
firstName: '',
|
|
740
|
+
lastName: '',
|
|
741
|
+
age: bcbPatient.age,
|
|
742
|
+
gender: bcbPatient.sexe,
|
|
743
|
+
weight: bcbPatient.poids,
|
|
744
|
+
pregnancy: bcbPatient.grossesse === 1,
|
|
745
|
+
breastfeeding: bcbPatient.allaitement === 1,
|
|
746
|
+
ammPathologies: bcbPatient.lstPathologiesAMM.map(code => ({
|
|
747
|
+
type: 'AMM',
|
|
748
|
+
code: code,
|
|
749
|
+
label: ''
|
|
750
|
+
})),
|
|
751
|
+
allergies: bcbPatient.lstIdComposantAllergie.map(code => ({
|
|
752
|
+
code: code,
|
|
753
|
+
label: ''
|
|
754
|
+
})),
|
|
755
|
+
hepaticStage: bcbPatient.insuffisanceHepatique,
|
|
756
|
+
height: bcbPatient.taille,
|
|
757
|
+
weeksOfPregnancy: 0,
|
|
758
|
+
creatinineClearance: bcbPatient.clairanceCreatinine,
|
|
759
|
+
molCreatinine: bcbPatient.creatininemieMol,
|
|
760
|
+
mglCreatinine: bcbPatient.creatininemieMg,
|
|
761
|
+
gfr: 0,
|
|
762
|
+
medicalTeam: {
|
|
763
|
+
firstNameDoctor: "",
|
|
764
|
+
lastNameDoctor: "",
|
|
765
|
+
firstNameSpecialist: "",
|
|
766
|
+
lastNameSpecialist: "",
|
|
767
|
+
firstNamePharmacist: "",
|
|
768
|
+
lastNamePharmacist: "",
|
|
769
|
+
firstNameNurse: "",
|
|
770
|
+
lastNameNurse: ""
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
};
|
|
774
|
+
const sortEntries = async (target, entries, allergiesMapper, snomedPathologiesMapper, errors = []) => {
|
|
775
|
+
const patientEntry = entries.find(entry => entry?.resource?.resourceType === "Patient")?.resource;
|
|
776
|
+
const observationEntries = entries.filter(entry => entry?.resource?.resourceType === "Observation").map(entry => entry?.resource);
|
|
777
|
+
const conditionEntries = entries.filter(entry => entry?.resource?.resourceType === "Condition").map(entry => entry?.resource);
|
|
778
|
+
const allergyIntoleranceEntries = entries.filter(entry => entry?.resource?.resourceType === "AllergyIntolerance").map(entry => entry?.resource);
|
|
779
|
+
const weightObservation = observationEntries.find(entry => entry?.code?.coding?.[0]?.code === WEIGHT_LOINC_CODE);
|
|
780
|
+
const heightObservation = observationEntries.find(entry => entry?.code?.coding?.[0]?.code === HEIGHT_LOINC_CODE);
|
|
781
|
+
const pregnancyObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === PREGNANCY_LOINC_CODE));
|
|
782
|
+
const breastfeedingObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === BREASTFEEDING_LOINC_CODE));
|
|
783
|
+
const amenorrheaObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === AMENORRHEA_LOINC_CODE));
|
|
784
|
+
const childPughObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === CHILD_PUGH_LOINC_CODE));
|
|
785
|
+
// Hepatic insufficiency used to be carried by an ICD-10 Condition; still read it as a fallback
|
|
786
|
+
// so that bundles produced by previous versions of the library keep working.
|
|
787
|
+
const legacyHepaticCondition = conditionEntries.find(entry => hasAcceptedCode(entry?.code, LEGACY_HEPATIC_ICD10_CODES));
|
|
788
|
+
const insuffisanceHepatique = resolveHepaticStage(childPughObservation, legacyHepaticCondition);
|
|
789
|
+
let lstIdComposantAllergie = [];
|
|
790
|
+
let errorField = target === 'bcb' ? 'lstIdComposantAllergie' : 'allergies';
|
|
791
|
+
for (const allergyEntry of allergyIntoleranceEntries) {
|
|
792
|
+
if (allergiesMapper) {
|
|
793
|
+
const mappingResult = await allergiesMapper(allergyEntry?.code?.coding?.[0]);
|
|
794
|
+
if (Array.isArray(mappingResult)) {
|
|
795
|
+
if (mappingResult.length > 0) {
|
|
796
|
+
lstIdComposantAllergie.push(...mappingResult);
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
errors.push({
|
|
800
|
+
field: errorField,
|
|
801
|
+
message: `No mapping found for allergy code ${allergyEntry?.code?.coding?.[0]?.code}`
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else if (mappingResult) {
|
|
806
|
+
lstIdComposantAllergie.push(mappingResult);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
errors.push({
|
|
810
|
+
field: errorField,
|
|
811
|
+
message: `No mapping found for allergy code ${allergyEntry?.code?.coding?.[0]?.code}`
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
errors.push({
|
|
817
|
+
field: errorField,
|
|
818
|
+
message: 'Allergy Intolerance values were found but no allergies mapper was provided, the output field is thus left empty.'
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// The legacy hepatic Condition is already consumed as the hepatic status, keep it out of the
|
|
823
|
+
// ICD-10 pathologies so it is not reported twice.
|
|
824
|
+
const cim10PathologiesEntries = conditionEntries.filter(entry => entry !== legacyHepaticCondition && entry?.code?.coding?.some(coding => coding.system === ICD10_SYSTEM_URL)).map(pathology => ({ code: pathology?.code?.coding?.[0]?.code || '', label: '' }));
|
|
825
|
+
const snomedPathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === SNOMED_SYSTEM_URL));
|
|
826
|
+
const ammPathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === AMM_SYSTEM_URL)).map(pathology => ({ code: pathology?.code?.coding?.[0]?.code || '', label: '' }));
|
|
827
|
+
let lstPathologies = [];
|
|
828
|
+
lstPathologies.push(...ammPathologiesEntries);
|
|
829
|
+
let lstCim10Pathologies = [];
|
|
830
|
+
lstCim10Pathologies.push(...cim10PathologiesEntries);
|
|
831
|
+
errorField = target === 'bcb' ? 'lstPathologiesAMM' : 'ammPathologies';
|
|
832
|
+
for (const pathology of snomedPathologiesEntries) {
|
|
833
|
+
if (snomedPathologiesMapper) {
|
|
834
|
+
const mappingResult = await snomedPathologiesMapper(pathology?.code?.coding?.[0]);
|
|
835
|
+
if (Array.isArray(mappingResult)) {
|
|
836
|
+
if (mappingResult.length > 0) {
|
|
837
|
+
lstPathologies.push(...mappingResult);
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
errors.push({
|
|
841
|
+
field: errorField,
|
|
842
|
+
message: `No mapping found for snomed code ${pathology?.code?.coding?.[0]?.code}`
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
else if (mappingResult) {
|
|
847
|
+
lstPathologies.push(mappingResult);
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
errors.push({
|
|
851
|
+
field: errorField,
|
|
852
|
+
message: `No mapping found for snomed code ${pathology?.code?.coding?.[0]?.code}`
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
errors.push({
|
|
858
|
+
field: errorField,
|
|
859
|
+
message: "Snomed pathologies were found but no mapper was provided, the output field is thus left empty."
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const clairanceCreatinineObservation = observationEntries.find(entry => hasAcceptedCode(entry?.code, ACCEPTED_LOINC_CODES_FOR_RENAL_CLEARANCE));
|
|
864
|
+
const gfrObservation = observationEntries.find(entry => hasAcceptedCode(entry?.code, ACCEPTED_LOINC_CODES_FOR_RENAL_GFR));
|
|
865
|
+
const gfrPerBsaObservation = observationEntries.find(entry => hasAcceptedCode(entry?.code, ACCEPTED_LOINC_CODES_FOR_RENAL_GFR_PER_BSA));
|
|
866
|
+
const creatininemieMolObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === CREATININEMIE_MOL_LOINC_CODE));
|
|
867
|
+
const creatininemieMgObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === CREATININEMIE_MG_LOINC_CODE));
|
|
868
|
+
const renalStatus = getPatientRenalStatus({
|
|
869
|
+
weightObservation,
|
|
870
|
+
heightObservation,
|
|
871
|
+
clairanceCreatinineObservation,
|
|
872
|
+
gfrObservation,
|
|
873
|
+
gfrPerBsaObservation
|
|
874
|
+
});
|
|
875
|
+
return {
|
|
876
|
+
patientEntry,
|
|
877
|
+
weightObservation,
|
|
878
|
+
heightObservation,
|
|
879
|
+
pregnancyObservation,
|
|
880
|
+
breastfeedingObservation,
|
|
881
|
+
amenorrheaObservation,
|
|
882
|
+
childPughObservation,
|
|
883
|
+
legacyHepaticCondition,
|
|
884
|
+
insuffisanceHepatique,
|
|
885
|
+
lstIdComposantAllergie,
|
|
886
|
+
lstCim10Pathologies,
|
|
887
|
+
lstPathologies,
|
|
888
|
+
clairanceCreatinineObservation,
|
|
889
|
+
gfrObservation,
|
|
890
|
+
gfrPerBsaObservation,
|
|
891
|
+
renalStatus,
|
|
892
|
+
creatininemieMolObservation,
|
|
893
|
+
creatininemieMgObservation
|
|
894
|
+
};
|
|
895
|
+
};
|
|
896
|
+
/**
|
|
897
|
+
* Resolve the BCB hepatic insufficiency stage from a bundle.
|
|
898
|
+
*
|
|
899
|
+
* The Child-Pugh `Observation` is authoritative and is inspected in the same order as
|
|
900
|
+
* mvn-fhir-mapper: a component holding a SNOMED class, then a direct `valueCodeableConcept`,
|
|
901
|
+
* then the numeric score. The legacy ICD-10 `Condition` is only used when none of those yield
|
|
902
|
+
* a stage.
|
|
903
|
+
*
|
|
904
|
+
* @returns 'A', 'B', 'C' or '' when the bundle carries no hepatic information
|
|
905
|
+
*/
|
|
906
|
+
const resolveHepaticStage = (childPughObservation, legacyHepaticCondition) => {
|
|
907
|
+
if (childPughObservation) {
|
|
908
|
+
// First priority: a component carrying the stage as a codeable concept
|
|
909
|
+
for (const component of childPughObservation.component ?? []) {
|
|
910
|
+
const stage = snomedCodeToChildPughStage(component.valueCodeableConcept?.coding?.[0]?.code);
|
|
911
|
+
if (stage) {
|
|
912
|
+
return stage;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// Second priority: the stage as a direct valueCodeableConcept
|
|
916
|
+
const directStage = snomedCodeToChildPughStage(childPughObservation.valueCodeableConcept?.coding?.[0]?.code);
|
|
917
|
+
if (directStage) {
|
|
918
|
+
return directStage;
|
|
919
|
+
}
|
|
920
|
+
// Third priority: derive the stage from the numeric score
|
|
921
|
+
const score = childPughObservation.valueQuantity?.value;
|
|
922
|
+
if (score !== undefined && score !== null) {
|
|
923
|
+
return childPughScoreToStage(score);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const legacyCode = legacyHepaticCondition?.code?.coding?.find(coding => hepaticInsufficiencyReverseMap[coding.code ?? ''])?.code;
|
|
927
|
+
return legacyCode ? hepaticInsufficiencyReverseMap[legacyCode] : '';
|
|
928
|
+
};
|
|
929
|
+
/**
|
|
930
|
+
* Compute the advanced renal status from the renal observations of a bundle.
|
|
931
|
+
*
|
|
932
|
+
* An absolute GFR observation wins. Otherwise a GFR normalized to 1.73 m² is converted to an
|
|
933
|
+
* absolute value, which requires both weight and height — without them the GFR stays undefined.
|
|
934
|
+
*/
|
|
935
|
+
const getPatientRenalStatus = (entries) => {
|
|
936
|
+
const clearance = entries.clairanceCreatinineObservation?.valueQuantity;
|
|
937
|
+
if (entries.gfrObservation?.valueQuantity) {
|
|
938
|
+
return { clearance, gfr: entries.gfrObservation.valueQuantity };
|
|
939
|
+
}
|
|
940
|
+
const normalizedGfrValue = entries.gfrPerBsaObservation?.valueQuantity?.value;
|
|
941
|
+
if (normalizedGfrValue !== undefined && normalizedGfrValue !== null) {
|
|
942
|
+
const weight = entries.weightObservation?.valueQuantity?.value ?? 0;
|
|
943
|
+
const height = entries.heightObservation?.valueQuantity?.value ?? 0;
|
|
944
|
+
if (weight > 0 && height > 0) {
|
|
945
|
+
const bsa = calculateBSA(height, weight);
|
|
946
|
+
return {
|
|
947
|
+
clearance,
|
|
948
|
+
gfr: {
|
|
949
|
+
value: normalizedGFRtoAbsolute(normalizedGfrValue, bsa),
|
|
950
|
+
unit: 'mL/min',
|
|
951
|
+
system: UCUM_SYSTEM_URL,
|
|
952
|
+
code: 'mL/min'
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return { clearance, gfr: undefined };
|
|
958
|
+
};
|
|
959
|
+
const patientToFhir = (patientData) => {
|
|
960
|
+
if (!patientData) {
|
|
961
|
+
return {
|
|
962
|
+
result: undefined,
|
|
963
|
+
errors: [{
|
|
964
|
+
field: 'root',
|
|
965
|
+
message: 'Invalid patient object'
|
|
966
|
+
}]
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
let errors = [];
|
|
970
|
+
const bundleEntries = [];
|
|
971
|
+
// Generate a consistent patient ID based on patient data
|
|
972
|
+
const patientId = generatePatientId(patientData);
|
|
973
|
+
const patientReference = `Patient/${patientId}`;
|
|
974
|
+
const patient = {
|
|
975
|
+
resourceType: 'Patient',
|
|
976
|
+
id: patientId,
|
|
977
|
+
birthDate: calculateBirthDateFromAgeInMonths(patientData.age),
|
|
978
|
+
gender: fhirGenderMap[patientData.gender] || 'unknown'
|
|
979
|
+
};
|
|
980
|
+
const weightObservation = {
|
|
981
|
+
resourceType: 'Observation',
|
|
982
|
+
id: `${patientId}-weight`,
|
|
983
|
+
code: {
|
|
984
|
+
coding: [{
|
|
985
|
+
code: '29463-7',
|
|
986
|
+
display: 'Body Weight'
|
|
987
|
+
}]
|
|
988
|
+
},
|
|
989
|
+
status: 'final',
|
|
990
|
+
valueQuantity: {
|
|
991
|
+
value: patientData.weight,
|
|
992
|
+
unit: 'kg'
|
|
993
|
+
},
|
|
994
|
+
subject: {
|
|
995
|
+
reference: patientReference
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
const heightObservation = {
|
|
999
|
+
resourceType: 'Observation',
|
|
1000
|
+
id: `${patientId}-height`,
|
|
1001
|
+
code: {
|
|
1002
|
+
coding: [{
|
|
1003
|
+
code: '8302-2',
|
|
1004
|
+
display: 'Body Height'
|
|
1005
|
+
}]
|
|
1006
|
+
},
|
|
1007
|
+
status: 'final',
|
|
1008
|
+
valueQuantity: {
|
|
1009
|
+
value: patientData.height,
|
|
1010
|
+
unit: 'cm'
|
|
1011
|
+
},
|
|
1012
|
+
subject: {
|
|
1013
|
+
reference: patientReference
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
let pregnancyObservation = undefined;
|
|
1017
|
+
if (patientData.pregnancy) {
|
|
1018
|
+
pregnancyObservation = {
|
|
1019
|
+
resourceType: 'Observation',
|
|
1020
|
+
id: `${patientId}-pregnancy`,
|
|
1021
|
+
code: {
|
|
1022
|
+
coding: [{
|
|
1023
|
+
code: '82810-3',
|
|
1024
|
+
display: 'Pregnancy Status'
|
|
1025
|
+
}]
|
|
1026
|
+
},
|
|
1027
|
+
status: 'final',
|
|
1028
|
+
subject: {
|
|
1029
|
+
reference: patientReference
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
if (pregnancyObservation && patientData.weeksOfPregnancy > 0) {
|
|
1034
|
+
pregnancyObservation.valueQuantity = {
|
|
1035
|
+
value: patientData.weeksOfPregnancy,
|
|
1036
|
+
unit: 'weeks',
|
|
1037
|
+
system: 'http://unitsofmeasure.org',
|
|
1038
|
+
code: 'wk'
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
let breastfeedingObservation = undefined;
|
|
1042
|
+
if (patientData.breastfeeding) {
|
|
1043
|
+
breastfeedingObservation = {
|
|
1044
|
+
resourceType: 'Observation',
|
|
1045
|
+
id: `${patientId}-breastfeeding`,
|
|
1046
|
+
code: {
|
|
1047
|
+
coding: [{
|
|
1048
|
+
code: '63895-7',
|
|
1049
|
+
display: 'Breastfeeding Status'
|
|
1050
|
+
}]
|
|
1051
|
+
},
|
|
1052
|
+
status: 'final',
|
|
1053
|
+
subject: {
|
|
1054
|
+
reference: patientReference
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
let creatinineClearanceObservation;
|
|
1059
|
+
if (patientData.creatinineClearance > 0) {
|
|
1060
|
+
creatinineClearanceObservation = {
|
|
1061
|
+
resourceType: 'Observation',
|
|
1062
|
+
id: `${patientId}-creatinine-clearance`,
|
|
1063
|
+
code: {
|
|
1064
|
+
coding: [{
|
|
1065
|
+
system: 'http://loinc.org',
|
|
1066
|
+
code: '2164-2',
|
|
1067
|
+
display: 'Creatinine clearance'
|
|
1068
|
+
}]
|
|
1069
|
+
},
|
|
1070
|
+
status: 'final',
|
|
1071
|
+
valueQuantity: {
|
|
1072
|
+
value: patientData.creatinineClearance,
|
|
1073
|
+
unit: 'mL/min'
|
|
1074
|
+
},
|
|
1075
|
+
subject: {
|
|
1076
|
+
reference: patientReference
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
let gfrObservation;
|
|
1081
|
+
if (patientData.gfr > 0) {
|
|
1082
|
+
gfrObservation = {
|
|
1083
|
+
resourceType: 'Observation',
|
|
1084
|
+
id: `${patientId}-gfr`,
|
|
1085
|
+
code: {
|
|
1086
|
+
coding: [{
|
|
1087
|
+
system: LOINC_SYSTEM_URL,
|
|
1088
|
+
code: GFR_LOINC_CODE,
|
|
1089
|
+
display: 'Glomerular filtration rate'
|
|
1090
|
+
}]
|
|
1091
|
+
},
|
|
1092
|
+
status: 'final',
|
|
1093
|
+
valueQuantity: {
|
|
1094
|
+
value: patientData.gfr,
|
|
1095
|
+
unit: 'mL/min',
|
|
1096
|
+
system: UCUM_SYSTEM_URL,
|
|
1097
|
+
code: 'mL/min'
|
|
1098
|
+
},
|
|
1099
|
+
subject: {
|
|
1100
|
+
reference: patientReference
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
let creatinineMolObservation;
|
|
1105
|
+
if (patientData.molCreatinine > 0) {
|
|
1106
|
+
creatinineMolObservation = {
|
|
1107
|
+
resourceType: 'Observation',
|
|
1108
|
+
id: `${patientId}-creatinine-mol`,
|
|
1109
|
+
code: {
|
|
1110
|
+
coding: [{
|
|
1111
|
+
system: 'http://loinc.org',
|
|
1112
|
+
code: '14682-9',
|
|
1113
|
+
display: 'Creatinine [Moles/volume] in Serum or Plasma'
|
|
1114
|
+
}]
|
|
1115
|
+
},
|
|
1116
|
+
status: 'final',
|
|
1117
|
+
valueQuantity: {
|
|
1118
|
+
value: patientData.molCreatinine,
|
|
1119
|
+
unit: 'mmol/L'
|
|
1120
|
+
},
|
|
1121
|
+
subject: {
|
|
1122
|
+
reference: patientReference
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
let creatinineMgObservation;
|
|
1127
|
+
if (patientData.mglCreatinine > 0) {
|
|
1128
|
+
creatinineMgObservation = {
|
|
1129
|
+
resourceType: 'Observation',
|
|
1130
|
+
id: `${patientId}-creatinine-mg`,
|
|
1131
|
+
code: {
|
|
1132
|
+
coding: [{
|
|
1133
|
+
system: 'http://loinc.org',
|
|
1134
|
+
code: '2160-0',
|
|
1135
|
+
display: 'Creatinine [Mass/volume] in Serum or Plasma'
|
|
1136
|
+
}]
|
|
1137
|
+
},
|
|
1138
|
+
status: 'final',
|
|
1139
|
+
valueQuantity: {
|
|
1140
|
+
value: patientData.mglCreatinine,
|
|
1141
|
+
unit: 'mg/dL'
|
|
1142
|
+
},
|
|
1143
|
+
subject: {
|
|
1144
|
+
reference: patientReference
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
let conditions = [];
|
|
1149
|
+
if (patientData.ammPathologies) {
|
|
1150
|
+
conditions = patientData.ammPathologies.map((pathology, index) => {
|
|
1151
|
+
return {
|
|
1152
|
+
resourceType: 'Condition',
|
|
1153
|
+
id: `${patientId}-condition-${index}`,
|
|
1154
|
+
code: {
|
|
1155
|
+
coding: [{
|
|
1156
|
+
system: pathology.type === 'AMM' ? AMM_SYSTEM_URL : ICD10_SYSTEM_URL,
|
|
1157
|
+
code: pathology.code,
|
|
1158
|
+
display: pathology.label || pathology.code
|
|
1159
|
+
}]
|
|
1160
|
+
},
|
|
1161
|
+
clinicalStatus: {
|
|
1162
|
+
coding: [{
|
|
1163
|
+
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
|
|
1164
|
+
code: 'active',
|
|
1165
|
+
display: 'Active'
|
|
1166
|
+
}]
|
|
1167
|
+
},
|
|
1168
|
+
subject: {
|
|
1169
|
+
reference: patientReference
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
// Hepatic insufficiency is expressed as a Child-Pugh observation: the numeric score in
|
|
1175
|
+
// valueQuantity, the class as a SNOMED coding in a component.
|
|
1176
|
+
let childPughObservation;
|
|
1177
|
+
if (patientData.hepaticStage && childPughStageMap[patientData.hepaticStage]) {
|
|
1178
|
+
const stageInfo = childPughStageMap[patientData.hepaticStage];
|
|
1179
|
+
childPughObservation = {
|
|
1180
|
+
resourceType: 'Observation',
|
|
1181
|
+
id: `${patientId}-child-pugh-score`,
|
|
1182
|
+
status: 'final',
|
|
1183
|
+
code: {
|
|
1184
|
+
coding: [{
|
|
1185
|
+
system: LOINC_SYSTEM_URL,
|
|
1186
|
+
code: CHILD_PUGH_LOINC_CODE,
|
|
1187
|
+
display: 'Total score [Child-Pugh]'
|
|
1188
|
+
}],
|
|
1189
|
+
text: 'Score total Child-Pugh'
|
|
1190
|
+
},
|
|
1191
|
+
subject: {
|
|
1192
|
+
reference: patientReference
|
|
1193
|
+
},
|
|
1194
|
+
valueQuantity: {
|
|
1195
|
+
value: stageInfo.score,
|
|
1196
|
+
unit: 'score',
|
|
1197
|
+
system: UCUM_SYSTEM_URL,
|
|
1198
|
+
code: '{score}'
|
|
1199
|
+
},
|
|
1200
|
+
component: [{
|
|
1201
|
+
code: {
|
|
1202
|
+
coding: [{
|
|
1203
|
+
system: LOINC_SYSTEM_URL,
|
|
1204
|
+
code: CHILD_PUGH_LOINC_CODE,
|
|
1205
|
+
display: 'Child-Pugh class'
|
|
1206
|
+
}],
|
|
1207
|
+
text: 'Classe Child-Pugh'
|
|
1208
|
+
},
|
|
1209
|
+
valueCodeableConcept: {
|
|
1210
|
+
coding: [{
|
|
1211
|
+
system: SNOMED_SYSTEM_URL,
|
|
1212
|
+
code: stageInfo.snomedCode,
|
|
1213
|
+
display: stageInfo.snomedDisplay
|
|
1214
|
+
}],
|
|
1215
|
+
text: stageInfo.stageText
|
|
1216
|
+
}
|
|
1217
|
+
}]
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
let allergies = [];
|
|
1221
|
+
if (patientData.allergies) {
|
|
1222
|
+
allergies = patientData.allergies.map((allergy, index) => {
|
|
1223
|
+
return {
|
|
1224
|
+
resourceType: 'AllergyIntolerance',
|
|
1225
|
+
id: `${patientId}-allergy-${index}`,
|
|
1226
|
+
patient: {
|
|
1227
|
+
reference: patientReference
|
|
1228
|
+
},
|
|
1229
|
+
code: {
|
|
1230
|
+
coding: [{
|
|
1231
|
+
system: INGREDIENTS_SYSTEM_URL,
|
|
1232
|
+
code: allergy.code.toString(),
|
|
1233
|
+
display: allergy.label?.toString() || allergy.code.toString()
|
|
1234
|
+
}]
|
|
1235
|
+
},
|
|
1236
|
+
clinicalStatus: {
|
|
1237
|
+
coding: [{
|
|
1238
|
+
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
|
|
1239
|
+
code: 'active',
|
|
1240
|
+
display: 'Active'
|
|
1241
|
+
}]
|
|
1242
|
+
},
|
|
1243
|
+
};
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
bundleEntries.push(...[
|
|
1247
|
+
{ resource: patient },
|
|
1248
|
+
...(weightObservation ? [{ resource: weightObservation }] : []),
|
|
1249
|
+
...(heightObservation ? [{ resource: heightObservation }] : []),
|
|
1250
|
+
...(pregnancyObservation ? [{ resource: pregnancyObservation }] : []),
|
|
1251
|
+
...(breastfeedingObservation ? [{ resource: breastfeedingObservation }] : []),
|
|
1252
|
+
...(creatinineClearanceObservation ? [{ resource: creatinineClearanceObservation }] : []),
|
|
1253
|
+
...(gfrObservation ? [{ resource: gfrObservation }] : []),
|
|
1254
|
+
...(creatinineMolObservation ? [{ resource: creatinineMolObservation }] : []),
|
|
1255
|
+
...(creatinineMgObservation ? [{ resource: creatinineMgObservation }] : []),
|
|
1256
|
+
...(childPughObservation ? [{ resource: childPughObservation }] : []),
|
|
1257
|
+
...conditions.map(condition => ({ resource: condition })),
|
|
1258
|
+
...allergies.map(allergy => ({ resource: allergy })),
|
|
1259
|
+
]);
|
|
1260
|
+
const bundle = {
|
|
1261
|
+
resourceType: "Bundle",
|
|
1262
|
+
id: "patient-bundle",
|
|
1263
|
+
type: "collection",
|
|
1264
|
+
entry: bundleEntries
|
|
1265
|
+
};
|
|
1266
|
+
return {
|
|
1267
|
+
result: bundle,
|
|
1268
|
+
errors: errors
|
|
1269
|
+
};
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
/* ================== FHIR <=> BCB ================== */
|
|
1273
|
+
function bcbToFhir$1(bcbPatient) {
|
|
1274
|
+
return patientToFhir(convertPatient(bcbPatient));
|
|
1275
|
+
}
|
|
1276
|
+
async function fhirToBcb$1(fhirBundle, allergiesMapper, snomedPathologiesMapper) {
|
|
1277
|
+
if (!fhirBundle || !fhirBundle.entry || fhirBundle.entry.length === 0) {
|
|
1278
|
+
return {
|
|
1279
|
+
result: undefined,
|
|
1280
|
+
errors: [{
|
|
1281
|
+
field: 'root',
|
|
1282
|
+
message: 'Invalid Bundle or empty entries'
|
|
1283
|
+
}]
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
let errors = [];
|
|
1287
|
+
const { patientEntry, weightObservation, heightObservation, pregnancyObservation, breastfeedingObservation, insuffisanceHepatique, lstIdComposantAllergie, lstCim10Pathologies, lstPathologies, renalStatus, creatininemieMolObservation, creatininemieMgObservation } = await sortEntries('bcb', fhirBundle.entry, wrapMaybeAsync(allergiesMapper), wrapMaybeAsync(snomedPathologiesMapper), errors);
|
|
1288
|
+
const bcbPatient = {
|
|
1289
|
+
lstIdComposantAllergie: lstIdComposantAllergie.map(codification => Number(codification.code)),
|
|
1290
|
+
lstPathologiesCIM10: lstCim10Pathologies.map(codification => codification.code),
|
|
1291
|
+
lstPathologiesAMM: lstPathologies.map(codification => codification.code),
|
|
1292
|
+
age: patientEntry && patientEntry.birthDate ? calculateAgeInMonths(patientEntry.birthDate) : 0,
|
|
1293
|
+
poids: weightObservation && weightObservation?.valueQuantity?.value ? weightObservation?.valueQuantity?.value : 0,
|
|
1294
|
+
taille: heightObservation && heightObservation?.valueQuantity?.value ? heightObservation?.valueQuantity?.value : 0,
|
|
1295
|
+
grossesse: pregnancyObservation ? 1 : 0,
|
|
1296
|
+
allaitement: breastfeedingObservation ? 1 : 0,
|
|
1297
|
+
sexe: patientEntry?.gender ? genderMap[patientEntry.gender] || '' : '',
|
|
1298
|
+
clairanceCreatinine: renalStatus.clearance?.value ?? 0,
|
|
1299
|
+
creatininemieMol: creatininemieMolObservation && creatininemieMolObservation?.valueQuantity?.value ? creatininemieMolObservation?.valueQuantity?.value : 0,
|
|
1300
|
+
creatininemieMg: creatininemieMgObservation && creatininemieMgObservation?.valueQuantity?.value ? creatininemieMgObservation?.valueQuantity?.value : 0,
|
|
1301
|
+
insuffisanceHepatique,
|
|
1302
|
+
};
|
|
1303
|
+
return {
|
|
1304
|
+
result: bcbPatient,
|
|
1305
|
+
errors: errors
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
/* ================== FHIR <=> CB ================== */
|
|
1309
|
+
function cbToFhir(cbPatient) {
|
|
1310
|
+
return patientToFhir(cbPatient);
|
|
1311
|
+
}
|
|
1312
|
+
async function fhirToCb(fhirBundle, allergiesMapper, snomedPathologiesMapper) {
|
|
1313
|
+
if (!fhirBundle?.entry?.length) {
|
|
1314
|
+
return {
|
|
1315
|
+
result: undefined,
|
|
1316
|
+
errors: [{
|
|
1317
|
+
field: 'root',
|
|
1318
|
+
message: 'Invalid Bundle or empty entries'
|
|
1319
|
+
}]
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
let errors = [];
|
|
1323
|
+
const { patientEntry, weightObservation, heightObservation, pregnancyObservation, breastfeedingObservation, amenorrheaObservation, insuffisanceHepatique, lstIdComposantAllergie, lstCim10Pathologies, lstPathologies, renalStatus, creatininemieMolObservation, creatininemieMgObservation } = await sortEntries('cb', fhirBundle.entry, wrapMaybeAsync(allergiesMapper), wrapMaybeAsync(snomedPathologiesMapper), errors);
|
|
1324
|
+
const pathologies = [
|
|
1325
|
+
...lstPathologies.map(codification => ({ type: 'AMM', code: codification.code, label: codification.label })),
|
|
1326
|
+
...lstCim10Pathologies.map(codification => ({ type: 'CIM10', code: codification.code, label: codification.label }))
|
|
1327
|
+
];
|
|
1328
|
+
const patientName = patientEntry?.name?.[0];
|
|
1329
|
+
return {
|
|
1330
|
+
result: {
|
|
1331
|
+
firstName: patientName?.given?.join(' ') ?? '',
|
|
1332
|
+
lastName: patientName?.family ?? '',
|
|
1333
|
+
age: patientEntry && patientEntry.birthDate ? calculateAgeInMonths(patientEntry.birthDate) : 0,
|
|
1334
|
+
gender: patientEntry?.gender ? genderMap[patientEntry.gender] || '' : '',
|
|
1335
|
+
weight: weightObservation && weightObservation?.valueQuantity?.value ? weightObservation?.valueQuantity?.value : 0,
|
|
1336
|
+
pregnancy: pregnancyObservation ? true : false,
|
|
1337
|
+
breastfeeding: breastfeedingObservation ? true : false,
|
|
1338
|
+
ammPathologies: pathologies,
|
|
1339
|
+
allergies: lstIdComposantAllergie.map(codification => ({ code: Number(codification.code), label: codification.label })),
|
|
1340
|
+
hepaticStage: insuffisanceHepatique,
|
|
1341
|
+
height: heightObservation && heightObservation?.valueQuantity?.value ? heightObservation?.valueQuantity?.value : 0,
|
|
1342
|
+
weeksOfPregnancy: pregnancyObservation?.valueQuantity?.value ?? amenorrheaObservation?.valueQuantity?.value ?? 0,
|
|
1343
|
+
creatinineClearance: renalStatus.clearance?.value ?? 0,
|
|
1344
|
+
molCreatinine: creatininemieMolObservation && creatininemieMolObservation?.valueQuantity?.value ? creatininemieMolObservation?.valueQuantity?.value : 0,
|
|
1345
|
+
mglCreatinine: creatininemieMgObservation && creatininemieMgObservation?.valueQuantity?.value ? creatininemieMgObservation?.valueQuantity?.value : 0,
|
|
1346
|
+
gfr: renalStatus.gfr?.value ?? 0,
|
|
1347
|
+
medicalTeam: {
|
|
1348
|
+
firstNameDoctor: "",
|
|
1349
|
+
lastNameDoctor: "",
|
|
1350
|
+
firstNameSpecialist: "",
|
|
1351
|
+
lastNameSpecialist: "",
|
|
1352
|
+
firstNamePharmacist: "",
|
|
1353
|
+
lastNamePharmacist: "",
|
|
1354
|
+
firstNameNurse: "",
|
|
1355
|
+
lastNameNurse: ""
|
|
1356
|
+
}
|
|
1357
|
+
},
|
|
1358
|
+
errors: errors
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
// Utility to wrap a possibly sync or async function so it always returns a Promise
|
|
1362
|
+
function wrapMaybeAsync(fn) {
|
|
1363
|
+
if (!fn)
|
|
1364
|
+
return undefined;
|
|
1365
|
+
return ((...args) => Promise.resolve(fn(...args)));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
var patientMapper = /*#__PURE__*/Object.freeze({
|
|
1369
|
+
__proto__: null,
|
|
1370
|
+
bcbToFhir: bcbToFhir$1,
|
|
1371
|
+
cbToFhir: cbToFhir,
|
|
1372
|
+
fhirToBcb: fhirToBcb$1,
|
|
1373
|
+
fhirToCb: fhirToCb
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Generate a hash-based ID for a medication based on its codes
|
|
1378
|
+
*/
|
|
1379
|
+
const generateMedicationId = (codes, system) => {
|
|
1380
|
+
const sortedCodes = [...codes].sort((a, b) => a.localeCompare(b));
|
|
1381
|
+
const dataToHash = {
|
|
1382
|
+
codes: sortedCodes,
|
|
1383
|
+
system
|
|
1384
|
+
};
|
|
1385
|
+
return generateHash(dataToHash);
|
|
1386
|
+
};
|
|
1387
|
+
/**
|
|
1388
|
+
* Helper function to extract codes from a single medication
|
|
1389
|
+
*/
|
|
1390
|
+
const extractCodesFromMedication = (medication, system) => {
|
|
1391
|
+
const codes = [];
|
|
1392
|
+
if (medication.code?.coding) {
|
|
1393
|
+
for (const coding of medication.code.coding) {
|
|
1394
|
+
if (coding.system === system && coding.code) {
|
|
1395
|
+
codes.push(coding.code);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return codes;
|
|
1400
|
+
};
|
|
1401
|
+
/**
|
|
1402
|
+
* Extracts a list of codes from a Bundle containing Medication resources based on the specified system.
|
|
1403
|
+
* @param bundle Bundle containing Medication resources to extract codes from
|
|
1404
|
+
* @param system The coding system to filter by
|
|
1405
|
+
* @returns List of codes that match the specified system
|
|
1406
|
+
*/
|
|
1407
|
+
function extractCodesFromMedications(bundle, system) {
|
|
1408
|
+
const codes = [];
|
|
1409
|
+
if (!bundle.entry) {
|
|
1410
|
+
return codes;
|
|
1411
|
+
}
|
|
1412
|
+
for (const entry of bundle.entry) {
|
|
1413
|
+
if (entry.resource?.resourceType === 'Medication') {
|
|
1414
|
+
const medication = entry.resource;
|
|
1415
|
+
codes.push(...extractCodesFromMedication(medication, system));
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return codes;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Extracts a list of bcb-code strings from a Bundle containing Medication resources.
|
|
1422
|
+
* Only codes with system "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code" are included.
|
|
1423
|
+
* @param bundle Bundle containing Medication resources
|
|
1424
|
+
* @returns List of BCB codes
|
|
1425
|
+
*/
|
|
1426
|
+
function extractBcbCodesFromMedications(bundle) {
|
|
1427
|
+
return extractCodesFromMedications(bundle, "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code");
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Extracts a list of cip13-code strings from a Bundle containing Medication resources.
|
|
1431
|
+
* Only codes with system "http://terminology.hl7.org/CodeSystem/cip13-code" are included.
|
|
1432
|
+
* @param bundle Bundle containing Medication resources
|
|
1433
|
+
* @returns List of CIP13 codes
|
|
1434
|
+
*/
|
|
1435
|
+
function extractCIP13CodesFromMedications(bundle) {
|
|
1436
|
+
return extractCodesFromMedications(bundle, "http://terminology.hl7.org/CodeSystem/cip13-code");
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Extracts a list of cis-code strings from a Bundle containing Medication resources.
|
|
1440
|
+
* Only codes with system "http://terminology.hl7.org/CodeSystem/cis-code" are included.
|
|
1441
|
+
* @param bundle Bundle containing Medication resources
|
|
1442
|
+
* @returns List of CIS codes
|
|
1443
|
+
*/
|
|
1444
|
+
function extractCISCodesFromMedications(bundle) {
|
|
1445
|
+
return extractCodesFromMedications(bundle, "http://terminology.hl7.org/CodeSystem/cis-code");
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Creates a Bundle containing Medication resources from a list of bcb-code strings.
|
|
1449
|
+
* Each Medication will have a CodeableConcept with a Coding for the bcb-code system.
|
|
1450
|
+
* @param bcbCodes List of BCB codes to convert to Medication resources
|
|
1451
|
+
* @returns MappingResponse containing a Bundle with Medication resources
|
|
1452
|
+
*/
|
|
1453
|
+
function createMedicationsFromBcbCodes(bcbCodes) {
|
|
1454
|
+
if (!bcbCodes || bcbCodes.length === 0) {
|
|
1455
|
+
return {
|
|
1456
|
+
result: {
|
|
1457
|
+
resourceType: "Bundle",
|
|
1458
|
+
id: "empty-medications-bundle",
|
|
1459
|
+
type: "collection",
|
|
1460
|
+
entry: []
|
|
1461
|
+
},
|
|
1462
|
+
errors: []
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
const bundleId = generateMedicationId(bcbCodes, "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code");
|
|
1466
|
+
const medications = bcbCodes.map((code, index) => {
|
|
1467
|
+
const medicationId = `${bundleId}-med-${index}`;
|
|
1468
|
+
return {
|
|
1469
|
+
resourceType: 'Medication',
|
|
1470
|
+
id: medicationId,
|
|
1471
|
+
code: {
|
|
1472
|
+
coding: [{
|
|
1473
|
+
system: "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code",
|
|
1474
|
+
code: code
|
|
1475
|
+
}]
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
});
|
|
1479
|
+
const bundle = {
|
|
1480
|
+
resourceType: "Bundle",
|
|
1481
|
+
id: `medications-bundle-${bundleId}`,
|
|
1482
|
+
type: "collection",
|
|
1483
|
+
entry: medications.map(medication => ({ resource: medication }))
|
|
1484
|
+
};
|
|
1485
|
+
return {
|
|
1486
|
+
result: bundle,
|
|
1487
|
+
errors: []
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Creates a Bundle containing Medication resources from a list of CIP13 codes.
|
|
1492
|
+
* Each Medication will have a CodeableConcept with a Coding for the CIP13 system.
|
|
1493
|
+
* @param cip13Codes List of CIP13 codes to convert to Medication resources
|
|
1494
|
+
* @returns MappingResponse containing a Bundle with Medication resources
|
|
1495
|
+
*/
|
|
1496
|
+
function createMedicationsFromCIP13Codes(cip13Codes) {
|
|
1497
|
+
if (!cip13Codes || cip13Codes.length === 0) {
|
|
1498
|
+
return {
|
|
1499
|
+
result: {
|
|
1500
|
+
resourceType: "Bundle",
|
|
1501
|
+
id: "empty-medications-bundle",
|
|
1502
|
+
type: "collection",
|
|
1503
|
+
entry: []
|
|
1504
|
+
},
|
|
1505
|
+
errors: []
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
const bundleId = generateMedicationId(cip13Codes, "http://terminology.hl7.org/CodeSystem/cip13-code");
|
|
1509
|
+
const medications = cip13Codes.map((code, index) => {
|
|
1510
|
+
const medicationId = `${bundleId}-med-${index}`;
|
|
1511
|
+
return {
|
|
1512
|
+
resourceType: 'Medication',
|
|
1513
|
+
id: medicationId,
|
|
1514
|
+
code: {
|
|
1515
|
+
coding: [{
|
|
1516
|
+
system: "http://terminology.hl7.org/CodeSystem/cip13-code",
|
|
1517
|
+
code: code
|
|
1518
|
+
}]
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
});
|
|
1522
|
+
const bundle = {
|
|
1523
|
+
resourceType: "Bundle",
|
|
1524
|
+
id: `medications-bundle-${bundleId}`,
|
|
1525
|
+
type: "collection",
|
|
1526
|
+
entry: medications.map(medication => ({ resource: medication }))
|
|
1527
|
+
};
|
|
1528
|
+
return {
|
|
1529
|
+
result: bundle,
|
|
1530
|
+
errors: []
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Creates a Bundle containing Medication resources from a list of CIS codes.
|
|
1535
|
+
* Each Medication will have a CodeableConcept with a Coding for the CIS system.
|
|
1536
|
+
* @param cisCodes List of CIS codes to convert to Medication resources
|
|
1537
|
+
* @returns MappingResponse containing a Bundle with Medication resources
|
|
1538
|
+
*/
|
|
1539
|
+
function createMedicationsFromCISCodes(cisCodes) {
|
|
1540
|
+
if (!cisCodes || cisCodes.length === 0) {
|
|
1541
|
+
return {
|
|
1542
|
+
result: {
|
|
1543
|
+
resourceType: "Bundle",
|
|
1544
|
+
id: "empty-medications-bundle",
|
|
1545
|
+
type: "collection",
|
|
1546
|
+
entry: []
|
|
1547
|
+
},
|
|
1548
|
+
errors: []
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
const bundleId = generateMedicationId(cisCodes, "http://terminology.hl7.org/CodeSystem/cis-code");
|
|
1552
|
+
const medications = cisCodes.map((code, index) => {
|
|
1553
|
+
const medicationId = `${bundleId}-med-${index}`;
|
|
1554
|
+
return {
|
|
1555
|
+
resourceType: 'Medication',
|
|
1556
|
+
id: medicationId,
|
|
1557
|
+
code: {
|
|
1558
|
+
coding: [{
|
|
1559
|
+
system: "http://terminology.hl7.org/CodeSystem/cis-code",
|
|
1560
|
+
code: code
|
|
1561
|
+
}]
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
});
|
|
1565
|
+
const bundle = {
|
|
1566
|
+
resourceType: "Bundle",
|
|
1567
|
+
id: `medications-bundle-${bundleId}`,
|
|
1568
|
+
type: "collection",
|
|
1569
|
+
entry: medications.map(medication => ({ resource: medication }))
|
|
1570
|
+
};
|
|
1571
|
+
return {
|
|
1572
|
+
result: bundle,
|
|
1573
|
+
errors: []
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
var medicationMapper = /*#__PURE__*/Object.freeze({
|
|
1578
|
+
__proto__: null,
|
|
1579
|
+
createMedicationsFromBcbCodes: createMedicationsFromBcbCodes,
|
|
1580
|
+
createMedicationsFromCIP13Codes: createMedicationsFromCIP13Codes,
|
|
1581
|
+
createMedicationsFromCISCodes: createMedicationsFromCISCodes,
|
|
1582
|
+
extractBcbCodesFromMedications: extractBcbCodesFromMedications,
|
|
1583
|
+
extractCIP13CodesFromMedications: extractCIP13CodesFromMedications,
|
|
1584
|
+
extractCISCodesFromMedications: extractCISCodesFromMedications,
|
|
1585
|
+
extractCodesFromMedications: extractCodesFromMedications,
|
|
1586
|
+
generateMedicationId: generateMedicationId
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Generate a hash-based ID for a medication request based on its key properties
|
|
1591
|
+
*/
|
|
1592
|
+
const generateMedicationRequestId = (medicationCode, patientId) => {
|
|
1593
|
+
const dataToHash = {
|
|
1594
|
+
medicationCode: medicationCode || 'unknown',
|
|
1595
|
+
patientId: patientId || 'unknown'
|
|
1596
|
+
};
|
|
1597
|
+
return generateHash(dataToHash);
|
|
1598
|
+
};
|
|
1599
|
+
/**
|
|
1600
|
+
* Converts dosage instructions from MedicationRequest resources in a Bundle to BCB format.
|
|
1601
|
+
* Uses the dosage-mapper for dosageInstruction field conversion.
|
|
1602
|
+
* @param bundle Bundle containing MedicationRequest resources
|
|
1603
|
+
* @returns Array of BCB dosage instructions from all MedicationRequest resources
|
|
1604
|
+
*/
|
|
1605
|
+
async function fhirToBcb(bundle) {
|
|
1606
|
+
const results = [];
|
|
1607
|
+
if (!bundle.entry) {
|
|
1608
|
+
return [{
|
|
1609
|
+
result: undefined,
|
|
1610
|
+
errors: [{ field: 'bundle.entry', message: 'Bundle has no entries' }]
|
|
1611
|
+
}];
|
|
1612
|
+
}
|
|
1613
|
+
for (const entry of bundle.entry) {
|
|
1614
|
+
if (entry.resource?.resourceType === 'MedicationRequest') {
|
|
1615
|
+
const medicationRequest = entry.resource;
|
|
1616
|
+
// Convert dosage instructions using the dosage-mapper
|
|
1617
|
+
if (medicationRequest.dosageInstruction && medicationRequest.dosageInstruction.length > 0) {
|
|
1618
|
+
try {
|
|
1619
|
+
const dosageResults = await fhirToBcb$2(medicationRequest.dosageInstruction);
|
|
1620
|
+
results.push(...dosageResults);
|
|
1621
|
+
}
|
|
1622
|
+
catch (error) {
|
|
1623
|
+
results.push({
|
|
1624
|
+
result: undefined,
|
|
1625
|
+
errors: [{
|
|
1626
|
+
field: 'dosageInstruction',
|
|
1627
|
+
message: `Error converting dosage instructions: ${error}`
|
|
1628
|
+
}]
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return results;
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Creates a Bundle containing MedicationRequest resources from BCB dosage data.
|
|
1638
|
+
* Uses the dosage-mapper for converting BCB dosage to FHIR dosage instructions.
|
|
1639
|
+
* @param bcbDosages Array of BCB dosage data
|
|
1640
|
+
* @returns MappingResponse containing a Bundle with MedicationRequest resources
|
|
1641
|
+
*/
|
|
1642
|
+
function bcbToFhir(bcbDosages) {
|
|
1643
|
+
if (!bcbDosages || bcbDosages.length === 0) {
|
|
1644
|
+
return {
|
|
1645
|
+
result: {
|
|
1646
|
+
resourceType: "Bundle",
|
|
1647
|
+
id: "empty-medication-requests-bundle",
|
|
1648
|
+
type: "collection",
|
|
1649
|
+
entry: []
|
|
1650
|
+
},
|
|
1651
|
+
errors: []
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
const bundleId = generateMedicationRequestId();
|
|
1655
|
+
const medicationRequests = [];
|
|
1656
|
+
const allErrors = [];
|
|
1657
|
+
// Convert BCB dosages to FHIR dosages using dosage-mapper
|
|
1658
|
+
const dosageResponses = bcbToFhir$2(bcbDosages);
|
|
1659
|
+
const dosageInstructions = [];
|
|
1660
|
+
for (const response of dosageResponses) {
|
|
1661
|
+
if (response.result) {
|
|
1662
|
+
dosageInstructions.push(response.result);
|
|
1663
|
+
}
|
|
1664
|
+
if (response.errors && response.errors.length > 0) {
|
|
1665
|
+
allErrors.push(...response.errors);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
// Create a single MedicationRequest with all converted dosage instructions
|
|
1669
|
+
const medicationRequest = {
|
|
1670
|
+
resourceType: 'MedicationRequest',
|
|
1671
|
+
id: `medication-request-${bundleId}`,
|
|
1672
|
+
status: 'active',
|
|
1673
|
+
intent: 'order',
|
|
1674
|
+
medication: {
|
|
1675
|
+
concept: {
|
|
1676
|
+
coding: [{
|
|
1677
|
+
code: 'CONVERTED_FROM_BCB'
|
|
1678
|
+
}]
|
|
1679
|
+
}
|
|
1680
|
+
},
|
|
1681
|
+
subject: {
|
|
1682
|
+
reference: 'Patient/unknown'
|
|
1683
|
+
},
|
|
1684
|
+
dosageInstruction: dosageInstructions
|
|
1685
|
+
};
|
|
1686
|
+
medicationRequests.push(medicationRequest);
|
|
1687
|
+
const bundle = {
|
|
1688
|
+
resourceType: "Bundle",
|
|
1689
|
+
id: `medication-requests-bundle-${bundleId}`,
|
|
1690
|
+
type: "collection",
|
|
1691
|
+
entry: medicationRequests.map(medicationRequest => ({ resource: medicationRequest }))
|
|
1692
|
+
};
|
|
1693
|
+
return {
|
|
1694
|
+
result: bundle,
|
|
1695
|
+
errors: allErrors
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Extracts medication codes from MedicationRequest resources in a Bundle.
|
|
1700
|
+
* @param bundle Bundle containing MedicationRequest resources
|
|
1701
|
+
* @returns Array of medication codes
|
|
1702
|
+
*/
|
|
1703
|
+
function extractMedicationCodes(bundle) {
|
|
1704
|
+
const codes = [];
|
|
1705
|
+
if (!bundle.entry) {
|
|
1706
|
+
return codes;
|
|
1707
|
+
}
|
|
1708
|
+
for (const entry of bundle.entry) {
|
|
1709
|
+
if (entry.resource?.resourceType === 'MedicationRequest') {
|
|
1710
|
+
const medicationRequest = entry.resource;
|
|
1711
|
+
const medicationCode = medicationRequest.medication?.concept?.coding?.[0]?.code;
|
|
1712
|
+
if (medicationCode) {
|
|
1713
|
+
codes.push(medicationCode);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return codes;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Extracts patient references from MedicationRequest resources in a Bundle.
|
|
1721
|
+
* @param bundle Bundle containing MedicationRequest resources
|
|
1722
|
+
* @returns Array of patient references
|
|
1723
|
+
*/
|
|
1724
|
+
function extractPatientReferences(bundle) {
|
|
1725
|
+
const references = [];
|
|
1726
|
+
if (!bundle.entry) {
|
|
1727
|
+
return references;
|
|
1728
|
+
}
|
|
1729
|
+
for (const entry of bundle.entry) {
|
|
1730
|
+
if (entry.resource?.resourceType === 'MedicationRequest') {
|
|
1731
|
+
const medicationRequest = entry.resource;
|
|
1732
|
+
const patientReference = medicationRequest.subject?.reference;
|
|
1733
|
+
if (patientReference) {
|
|
1734
|
+
references.push(patientReference);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return references;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
var medicationRequestMapper = /*#__PURE__*/Object.freeze({
|
|
1742
|
+
__proto__: null,
|
|
1743
|
+
bcbToFhir: bcbToFhir,
|
|
1744
|
+
extractMedicationCodes: extractMedicationCodes,
|
|
1745
|
+
extractPatientReferences: extractPatientReferences,
|
|
1746
|
+
fhirToBcb: fhirToBcb,
|
|
1747
|
+
generateMedicationRequestId: generateMedicationRequestId
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
export { dosageMapper, generateHash, medicationMapper, medicationRequestMapper, patientMapper, simpleHash };
|
|
1751
|
+
//# sourceMappingURL=index.js.map
|