@medicus.ai/medicus-report-pdf-generator 1.3.19 → 1.3.22
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/lib/big_integral_questionnaire.js +152 -66
- package/lib/template.js +20 -3
- package/lib/wellbeing_report_generator.js +105 -10
- package/locales/wellbeing/ar-AE.json +105 -1
- package/locales/wellbeing/ar-SA.json +105 -1
- package/locales/wellbeing/en.json +106 -10
- package/package.json +1 -1
- package/preview-big-integral.js +1 -1
- package/templates/maisonsante/rtl_no_pages.html +46 -0
|
@@ -98,7 +98,8 @@ function getDummyData() {
|
|
|
98
98
|
|
|
99
99
|
// Section labels of the reference design, keyed by cerbo's stable symbol. Keeps
|
|
100
100
|
// the table wording stable even though the metadata element names differ
|
|
101
|
-
// (e.g. "IMMUNE SYSTEM", "LIFESTYLE").
|
|
101
|
+
// (e.g. "IMMUNE SYSTEM", "LIFESTYLE"). These are the English fallbacks; the
|
|
102
|
+
// translated label comes from the locale key "bigIntegralSystem_<symbol>".
|
|
102
103
|
var SYSTEM_LABELS = {
|
|
103
104
|
"stomach-and-gi-tract": "STOMACH and GI (Gastrointestinal) Tract",
|
|
104
105
|
"small-intestine": "Small Intestine",
|
|
@@ -112,12 +113,53 @@ var SYSTEM_LABELS = {
|
|
|
112
113
|
"lifestyle": "Lifestyle",
|
|
113
114
|
};
|
|
114
115
|
|
|
116
|
+
// English fallbacks for every static label of the section, keyed by locale key.
|
|
117
|
+
var STATIC_LABELS = {
|
|
118
|
+
bigIntegralTitle: "Key to the Big Integral Questionnaire",
|
|
119
|
+
bigIntegralPatientName: "Client/Patient Full Name",
|
|
120
|
+
bigIntegralAge: "Age",
|
|
121
|
+
bigIntegralBmi: "BMI",
|
|
122
|
+
bigIntegralSystemScores: "SYSTEM SCORES",
|
|
123
|
+
bigIntegralSystem: "SYSTEM",
|
|
124
|
+
bigIntegralHigh: "High",
|
|
125
|
+
bigIntegralQuestions: "Questions",
|
|
126
|
+
bigIntegralResult: "Result",
|
|
127
|
+
bigIntegralMaximum: "Maximum",
|
|
128
|
+
bigIntegralHormonalFemale: "Hormonal Health Female",
|
|
129
|
+
bigIntegralHormonalMale: "Hormonal Health Male",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Placeholder rows (getDummyData) carry only the English label; map it back to
|
|
133
|
+
// the symbol so the preview can be translated too.
|
|
134
|
+
var SYMBOL_BY_LABEL = Object.keys(SYSTEM_LABELS).reduce(function (acc, symbol) {
|
|
135
|
+
acc[SYSTEM_LABELS[symbol]] = symbol;
|
|
136
|
+
return acc;
|
|
137
|
+
}, {});
|
|
138
|
+
|
|
139
|
+
// localeService.t() returns the entry's `.message`, which is undefined for a key
|
|
140
|
+
// that is missing from the locale file (i18n then stores the key as a bare
|
|
141
|
+
// string). Fall back to the English label in that case so a missing translation
|
|
142
|
+
// can never print "undefined" or the raw key.
|
|
143
|
+
function makeTranslator(localeService) {
|
|
144
|
+
return function (key, fallback) {
|
|
145
|
+
var value;
|
|
146
|
+
try {
|
|
147
|
+
value = localeService && typeof localeService.t === "function" ? localeService.t(key) : undefined;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
value = undefined;
|
|
150
|
+
}
|
|
151
|
+
return typeof value === "string" && value.trim() && value !== key ? value : fallback;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
115
155
|
function buildRowsFromDoctorScores(doctorScores, meta) {
|
|
116
156
|
var groups = (doctorScores && doctorScores.groups) || [];
|
|
117
157
|
if (!groups.length) return null;
|
|
118
158
|
|
|
119
159
|
var rows = groups.map(function (group) {
|
|
120
160
|
return {
|
|
161
|
+
key: "system:" + group.symbol,
|
|
162
|
+
symbol: group.symbol,
|
|
121
163
|
system: SYSTEM_LABELS[group.symbol] || group.name || group.symbol,
|
|
122
164
|
high:
|
|
123
165
|
group.highThreshold === null || group.highThreshold === undefined
|
|
@@ -132,29 +174,44 @@ function buildRowsFromDoctorScores(doctorScores, meta) {
|
|
|
132
174
|
|
|
133
175
|
// Hormonal Health is never scored (no maximum in the spreadsheet either); the
|
|
134
176
|
// rows exist so the doctor can see the questions were asked.
|
|
135
|
-
rows.push(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
177
|
+
rows.push(hormonalRow("female"));
|
|
178
|
+
rows.push(hormonalRow("male"));
|
|
179
|
+
|
|
180
|
+
return { rows: rows, meta: meta || {} };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function hormonalRow(sex) {
|
|
184
|
+
return {
|
|
185
|
+
key: "hormonal:" + sex,
|
|
186
|
+
symbol: null,
|
|
187
|
+
system: sex === "female" ? STATIC_LABELS.bigIntegralHormonalFemale : STATIC_LABELS.bigIntegralHormonalMale,
|
|
144
188
|
high: null,
|
|
145
189
|
questions: null,
|
|
146
190
|
result: 0,
|
|
147
191
|
maximum: null,
|
|
148
|
-
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
149
194
|
|
|
150
|
-
|
|
195
|
+
// Gives placeholder rows the same identity fields as real rows.
|
|
196
|
+
function normalizeRow(row) {
|
|
197
|
+
if (row.key) return row;
|
|
198
|
+
if (row.system === STATIC_LABELS.bigIntegralHormonalFemale) return hormonalRow("female");
|
|
199
|
+
if (row.system === STATIC_LABELS.bigIntegralHormonalMale) return hormonalRow("male");
|
|
200
|
+
var symbol = SYMBOL_BY_LABEL[row.system] || null;
|
|
201
|
+
return Object.assign({}, row, { key: "system:" + (symbol || row.system), symbol: symbol });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function translateSystem(row, tr) {
|
|
205
|
+
if (row.key === "hormonal:female") return tr("bigIntegralHormonalFemale", row.system);
|
|
206
|
+
if (row.key === "hormonal:male") return tr("bigIntegralHormonalMale", row.system);
|
|
207
|
+
return row.symbol ? tr("bigIntegralSystem_" + row.symbol, row.system) : row.system;
|
|
151
208
|
}
|
|
152
209
|
|
|
153
210
|
// ---------------------------------------------------------------------------
|
|
154
211
|
// Helpers
|
|
155
212
|
// ---------------------------------------------------------------------------
|
|
156
213
|
function fmt(val, decimals) {
|
|
157
|
-
if (val === null || val === undefined) return "
|
|
214
|
+
if (val === null || val === undefined) return "—"; // em dash
|
|
158
215
|
if (typeof val === "number") {
|
|
159
216
|
const rounded =
|
|
160
217
|
Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
@@ -163,6 +220,20 @@ function fmt(val, decimals) {
|
|
|
163
220
|
return String(val);
|
|
164
221
|
}
|
|
165
222
|
|
|
223
|
+
function escapeHtml(value) {
|
|
224
|
+
return String(value === null || value === undefined ? "" : value)
|
|
225
|
+
.replace(/&/g, "&")
|
|
226
|
+
.replace(/</g, "<")
|
|
227
|
+
.replace(/>/g, ">")
|
|
228
|
+
.replace(/"/g, """);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Numbers, thresholds (">8") and "/3" must keep left-to-right order inside an
|
|
232
|
+
// Arabic (RTL) table, otherwise they render mirrored ("8<", "3/").
|
|
233
|
+
function ltr(content) {
|
|
234
|
+
return `<span dir="ltr" style="direction:ltr;unicode-bidi:isolate;">${content}</span>`;
|
|
235
|
+
}
|
|
236
|
+
|
|
166
237
|
// ---------------------------------------------------------------------------
|
|
167
238
|
// Single render — faithful to the reference image, Maison Sante colours.
|
|
168
239
|
//
|
|
@@ -178,28 +249,38 @@ function fmt(val, decimals) {
|
|
|
178
249
|
// ├────────────┼──────┼─────────┼────────┼───────┼──┼───┤
|
|
179
250
|
// │ ...rows... │
|
|
180
251
|
// └──────────────────────────────────────────────────────┘
|
|
252
|
+
// In Arabic the whole block is laid out right-to-left (dir="rtl"): the SYSTEM
|
|
253
|
+
// column moves to the right edge and every label comes from the locale files.
|
|
181
254
|
// ---------------------------------------------------------------------------
|
|
182
|
-
function renderTable(rows, meta, cfg) {
|
|
255
|
+
function renderTable(rows, meta, cfg, tr, isRtl) {
|
|
183
256
|
const c = cfg.colors;
|
|
184
|
-
const title = cfg.title ||
|
|
257
|
+
const title = tr("bigIntegralTitle", cfg.title || STATIC_LABELS.bigIntegralTitle);
|
|
258
|
+
const dir = isRtl ? "rtl" : "ltr";
|
|
259
|
+
const start = isRtl ? "right" : "left";
|
|
260
|
+
// Letter-spacing breaks the joining of Arabic letters, so drop it in RTL.
|
|
261
|
+
const titleSpacing = isRtl ? "0" : "0.3px";
|
|
262
|
+
const bandSpacing = isRtl ? "0" : "0.8px";
|
|
263
|
+
const fontFamily = isRtl ? "'Cairo','Open Sans',sans-serif" : "'Open Sans',sans-serif";
|
|
264
|
+
|
|
265
|
+
// Padding with the "start" side first, mirrored for RTL.
|
|
266
|
+
const pad = (top, startPx, bottom, endPx) =>
|
|
267
|
+
isRtl
|
|
268
|
+
? `padding:${top}px ${startPx}px ${bottom}px ${endPx}px;`
|
|
269
|
+
: `padding:${top}px ${endPx}px ${bottom}px ${startPx}px;`;
|
|
185
270
|
|
|
186
271
|
// ── patient info bar ──────────────────────────────────────────────────
|
|
272
|
+
const infoCell = (label, value, startPx, endPx) => `
|
|
273
|
+
<td style="${pad(7, startPx, 7, endPx)}font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;text-align:${start};">
|
|
274
|
+
<span style="color:${c.headerBg};font-weight:700;">${label}</span>
|
|
275
|
+
<bdi>${escapeHtml(value)}</bdi>
|
|
276
|
+
</td>`;
|
|
187
277
|
const infoBar = `
|
|
188
278
|
<div style="background:#ffffff;border-bottom:1px solid ${c.rowBorder};">
|
|
189
|
-
<table style="border-collapse:collapse;">
|
|
279
|
+
<table dir="${dir}" style="border-collapse:collapse;">
|
|
190
280
|
<tr>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
</td>
|
|
195
|
-
<td style="padding:7px 40px 7px 0;font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;">
|
|
196
|
-
<span style="color:${c.headerBg};font-weight:700;">Age</span>
|
|
197
|
-
${meta.age}
|
|
198
|
-
</td>
|
|
199
|
-
<td style="padding:7px 0 7px 0;font-size:9px;color:#1a1a1a;white-space:nowrap;vertical-align:middle;">
|
|
200
|
-
<span style="color:${c.headerBg};font-weight:700;">BMI</span>
|
|
201
|
-
${meta.bmi}
|
|
202
|
-
</td>
|
|
281
|
+
${infoCell(tr("bigIntegralPatientName", STATIC_LABELS.bigIntegralPatientName), meta.patientName, 14, 40)}
|
|
282
|
+
${infoCell(tr("bigIntegralAge", STATIC_LABELS.bigIntegralAge), meta.age, 0, 40)}
|
|
283
|
+
${infoCell(tr("bigIntegralBmi", STATIC_LABELS.bigIntegralBmi), meta.bmi, 0, 0)}
|
|
203
284
|
</tr>
|
|
204
285
|
</table>
|
|
205
286
|
</div>`;
|
|
@@ -213,13 +294,14 @@ function renderTable(rows, meta, cfg) {
|
|
|
213
294
|
color: #ffffff;
|
|
214
295
|
font-size: 8.5px;
|
|
215
296
|
font-weight: 700;
|
|
216
|
-
letter-spacing:
|
|
297
|
+
letter-spacing: ${bandSpacing};
|
|
217
298
|
text-transform: uppercase;
|
|
218
|
-
|
|
299
|
+
text-align: ${start};
|
|
300
|
+
">${tr("bigIntegralSystemScores", STATIC_LABELS.bigIntegralSystemScores)}</div>`;
|
|
219
301
|
|
|
220
302
|
// ── column widths ──────────────────────────────────────────────────────
|
|
221
303
|
const COL = {
|
|
222
|
-
system:
|
|
304
|
+
system: `width:45%;text-align:${start};`,
|
|
223
305
|
high: "width:10%;text-align:center;",
|
|
224
306
|
questions: "width:12%;text-align:center;",
|
|
225
307
|
result: "width:10%;text-align:center;",
|
|
@@ -235,12 +317,12 @@ function renderTable(rows, meta, cfg) {
|
|
|
235
317
|
|
|
236
318
|
const tableHeader = `
|
|
237
319
|
<tr>
|
|
238
|
-
${th("
|
|
239
|
-
${th("
|
|
240
|
-
${th("
|
|
241
|
-
${th("
|
|
242
|
-
${th("
|
|
243
|
-
${th("/3", "perThree")}
|
|
320
|
+
${th(tr("bigIntegralSystem", STATIC_LABELS.bigIntegralSystem), "system")}
|
|
321
|
+
${th(tr("bigIntegralHigh", STATIC_LABELS.bigIntegralHigh), "high")}
|
|
322
|
+
${th(tr("bigIntegralQuestions", STATIC_LABELS.bigIntegralQuestions), "questions")}
|
|
323
|
+
${th(tr("bigIntegralResult", STATIC_LABELS.bigIntegralResult), "result")}
|
|
324
|
+
${th(tr("bigIntegralMaximum", STATIC_LABELS.bigIntegralMaximum), "maximum")}
|
|
325
|
+
${th(ltr("/3"), "perThree")}
|
|
244
326
|
</tr>`;
|
|
245
327
|
|
|
246
328
|
// ── data rows ─────────────────────────────────────────────────────────
|
|
@@ -254,19 +336,19 @@ function renderTable(rows, meta, cfg) {
|
|
|
254
336
|
const hasMaximum = row.maximum !== null && row.maximum !== undefined;
|
|
255
337
|
|
|
256
338
|
const highDisplay = hasThreshold
|
|
257
|
-
? `<span style="color:${c.elevatedText};font-weight:600;">${row.high}</span>`
|
|
258
|
-
: "
|
|
339
|
+
? `<span style="color:${c.elevatedText};font-weight:600;">${ltr(escapeHtml(row.high))}</span>`
|
|
340
|
+
: "—";
|
|
259
341
|
|
|
260
|
-
const questionsDisplay = hasQuantity ? String(row.questions) : "
|
|
261
|
-
const resultDisplay = String(row.result);
|
|
262
|
-
const maximumDisplay = hasMaximum ? fmt(row.maximum, 0) : "
|
|
263
|
-
const perThreeDisplay = hasMaximum ? fmt(row.maximum / 3, 2) : "
|
|
342
|
+
const questionsDisplay = hasQuantity ? ltr(String(row.questions)) : "—";
|
|
343
|
+
const resultDisplay = ltr(String(row.result));
|
|
344
|
+
const maximumDisplay = hasMaximum ? ltr(fmt(row.maximum, 0)) : "—";
|
|
345
|
+
const perThreeDisplay = hasMaximum ? ltr(fmt(row.maximum / 3, 2)) : "—";
|
|
264
346
|
|
|
265
347
|
const rowBg = i % 2 === 0 ? "#ffffff" : "rgba(15, 32, 98, 0.03)";
|
|
266
348
|
|
|
267
349
|
return `
|
|
268
350
|
<tr style="background:${rowBg};">
|
|
269
|
-
${TD(`<span style="color:#1a1a1a;">${row
|
|
351
|
+
${TD(`<span style="color:#1a1a1a;">${escapeHtml(translateSystem(row, tr))}</span>`, "system")}
|
|
270
352
|
${TD(highDisplay, "high")}
|
|
271
353
|
${TD(questionsDisplay, "questions")}
|
|
272
354
|
${TD(resultDisplay, "result", `font-weight:600;color:#444;`)}
|
|
@@ -281,9 +363,9 @@ function renderTable(rows, meta, cfg) {
|
|
|
281
363
|
// empty divs with only `height` get collapsed by wkhtmltopdf. Padding
|
|
282
364
|
// always renders.
|
|
283
365
|
return `
|
|
284
|
-
<div style="padding-top:25px;font-family
|
|
366
|
+
<div dir="${dir}" style="padding-top:25px;font-family:${fontFamily};direction:${dir};text-align:${start};">
|
|
285
367
|
<div class="biq-wrapper" style="
|
|
286
|
-
font-family:
|
|
368
|
+
font-family: ${fontFamily};
|
|
287
369
|
page-break-inside: avoid;
|
|
288
370
|
border-radius: 4px;
|
|
289
371
|
overflow: hidden;
|
|
@@ -297,13 +379,14 @@ function renderTable(rows, meta, cfg) {
|
|
|
297
379
|
padding: 9px 14px;
|
|
298
380
|
font-size: 11px;
|
|
299
381
|
font-weight: 700;
|
|
300
|
-
letter-spacing:
|
|
382
|
+
letter-spacing: ${titleSpacing};
|
|
383
|
+
text-align: ${start};
|
|
301
384
|
">${title}</div>
|
|
302
385
|
|
|
303
386
|
${infoBar}
|
|
304
387
|
${subheaderBand}
|
|
305
388
|
|
|
306
|
-
<table style="width:100%;border-collapse:collapse;table-layout:fixed;">
|
|
389
|
+
<table dir="${dir}" style="width:100%;border-collapse:collapse;table-layout:fixed;">
|
|
307
390
|
<thead>${tableHeader}</thead>
|
|
308
391
|
<tbody>${dataRows}</tbody>
|
|
309
392
|
</table>
|
|
@@ -318,11 +401,16 @@ function renderTable(rows, meta, cfg) {
|
|
|
318
401
|
* Render the Big Integral Questionnaire section for Maison Sante doctor PDFs.
|
|
319
402
|
*
|
|
320
403
|
* @param {object} opts
|
|
321
|
-
* @param {object} opts.clientConfig
|
|
322
|
-
* @param {object} opts.
|
|
323
|
-
* @
|
|
404
|
+
* @param {object} opts.clientConfig - The full maisonsante.json config object
|
|
405
|
+
* @param {object} opts.doctorScores - The `doctorScores` payload from cerbo
|
|
406
|
+
* @param {object} opts.meta - { patientName, age, bmi }
|
|
407
|
+
* @param {object} opts.localeService - localeService (already set to the report language) with .t(key)
|
|
408
|
+
* @param {boolean} opts.isRtl - true for Arabic reports
|
|
409
|
+
* @param {boolean} opts.usePlaceholder - render the design placeholder when doctorScores is absent
|
|
410
|
+
* (preview script only; real PDFs never show dummy data)
|
|
411
|
+
* @returns {string} HTML string to inject into #big-integral-section ('' when there is nothing to show)
|
|
324
412
|
*/
|
|
325
|
-
function renderBigIntegralQuestionnaire({ clientConfig, doctorScores, meta }) {
|
|
413
|
+
function renderBigIntegralQuestionnaire({ clientConfig, doctorScores, meta, localeService, isRtl = false, usePlaceholder = false }) {
|
|
326
414
|
const cfg = (clientConfig && clientConfig.big_integral_questionnaire) || {};
|
|
327
415
|
|
|
328
416
|
const colors = Object.assign(
|
|
@@ -338,30 +426,28 @@ function renderBigIntegralQuestionnaire({ clientConfig, doctorScores, meta }) {
|
|
|
338
426
|
|
|
339
427
|
const resolvedCfg = Object.assign({}, cfg, { colors });
|
|
340
428
|
|
|
341
|
-
//
|
|
342
|
-
//
|
|
429
|
+
// Cerbo only sends doctorScores for a FINISHED quiz. Without it there is no real
|
|
430
|
+
// data: the section is omitted rather than filled with the placeholder patient
|
|
431
|
+
// ("Final Test 3", age 37, BMI 23.67) that getDummyData() carries for the preview.
|
|
343
432
|
const real = buildRowsFromDoctorScores(doctorScores, meta);
|
|
344
|
-
if (!real) {
|
|
345
|
-
console.log("[BigIntegral] no doctorScores in payload —
|
|
346
|
-
|
|
433
|
+
if (!real && !usePlaceholder) {
|
|
434
|
+
console.log("[BigIntegral] no doctorScores in payload — section omitted");
|
|
435
|
+
return "";
|
|
436
|
+
}
|
|
437
|
+
if (real) {
|
|
347
438
|
console.log("[BigIntegral] rendering " + real.rows.length + " rows from doctorScores");
|
|
348
439
|
}
|
|
349
440
|
const source = real || getDummyData();
|
|
350
|
-
const allRows = source.rows;
|
|
441
|
+
const allRows = source.rows.map(normalizeRow);
|
|
351
442
|
const resolvedMeta = source.meta;
|
|
352
443
|
|
|
353
444
|
const rows = allRows.filter((row) => {
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
cfg.showHormonalFemale === false
|
|
357
|
-
)
|
|
358
|
-
return false;
|
|
359
|
-
if (row.system === "Hormonal Health Male" && cfg.showHormonalMale === false)
|
|
360
|
-
return false;
|
|
445
|
+
if (row.key === "hormonal:female" && cfg.showHormonalFemale === false) return false;
|
|
446
|
+
if (row.key === "hormonal:male" && cfg.showHormonalMale === false) return false;
|
|
361
447
|
return true;
|
|
362
448
|
});
|
|
363
449
|
|
|
364
|
-
return renderTable(rows, resolvedMeta, resolvedCfg);
|
|
450
|
+
return renderTable(rows, resolvedMeta, resolvedCfg, makeTranslator(localeService), !!isRtl);
|
|
365
451
|
}
|
|
366
452
|
|
|
367
453
|
module.exports = { renderBigIntegralQuestionnaire };
|
package/lib/template.js
CHANGED
|
@@ -1184,7 +1184,9 @@ function renderPanelV3(panel, nestingLevel, isCompactView, counter = null, isRec
|
|
|
1184
1184
|
|
|
1185
1185
|
|
|
1186
1186
|
const panelCssId = `panel-${panel.panelId}db${Math.floor(Math.random() * 3)}`;
|
|
1187
|
-
|
|
1187
|
+
// Each panel note is a separate line from the lab; joining with '' glued them
|
|
1188
|
+
// into one run of text ("...Association:Glucose Level70-99 Normal...").
|
|
1189
|
+
const notes = panel.notes ? panel.notes.join('<br/>') : '';
|
|
1188
1190
|
let classPanel = '';
|
|
1189
1191
|
if (panel.doctorNotes && isCompactView) {
|
|
1190
1192
|
classPanel = 'panel-with-notes';
|
|
@@ -1324,10 +1326,25 @@ function renderDetailsListV3(detailsList, nestingLevel, isCompactView) {
|
|
|
1324
1326
|
if (nestingLevel > 0) {
|
|
1325
1327
|
let extraPadding = ((nestingLevel + 10 % 10) + 1);
|
|
1326
1328
|
let levelNesting = extraPadding + nestingLevel * INDENTATION.BIOMARKER_DETAILS + plusPadding;
|
|
1327
|
-
blockStyle = `padding-left: ${levelNesting}px
|
|
1329
|
+
blockStyle = `padding-left: ${levelNesting}px;`;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// The shared stylesheet sizes details tables at 70% of the panel and lets the
|
|
1333
|
+
// browser size columns to their content. A wide table (e.g. a 12-column
|
|
1334
|
+
// prescription) then overflows the panel card and gets cut at the page edge.
|
|
1335
|
+
// Wide tables take the full panel width with fixed column layout so every
|
|
1336
|
+
// column stays inside the card; the header row wraps instead of overflowing.
|
|
1337
|
+
const WIDE_TABLE_COLUMNS = 6;
|
|
1338
|
+
let tableStyle = '';
|
|
1339
|
+
let cellStyle = '';
|
|
1340
|
+
if (colspanColumn >= WIDE_TABLE_COLUMNS) {
|
|
1341
|
+
blockStyle += 'width:100%;max-width:100%;box-sizing:border-box;';
|
|
1342
|
+
tableStyle = 'width:100%;table-layout:fixed;';
|
|
1343
|
+
cellStyle = 'word-wrap:break-word;overflow-wrap:break-word;padding:6px 4px;font-size:10px;line-height:1.3;';
|
|
1344
|
+
content = content.replace(/<td colspan="1">/g, `<td colspan="1" style="${cellStyle}">`);
|
|
1328
1345
|
}
|
|
1329
1346
|
|
|
1330
|
-
return `<div style="${blockStyle}" class="biomarker-details-table" style="
|
|
1347
|
+
return `<div style="${blockStyle}padding:0 ${plusPadding}px" class="biomarker-details-table"><table style="${tableStyle}">${content}</table></div>`;
|
|
1331
1348
|
}
|
|
1332
1349
|
|
|
1333
1350
|
function renderNoteV3(note, nestingLevel, isCompactView, showInCompactView, showInDetailsView) {
|
|
@@ -13,6 +13,39 @@ const { cli } = require("webpack");
|
|
|
13
13
|
|
|
14
14
|
var localeService = new LocaleService(i18n);
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Translate `key` for the current report language, falling back to `fallback`.
|
|
18
|
+
* localeService.t() returns undefined for a key missing from the locale file
|
|
19
|
+
* (i18n stores it as a bare string without `.message`), which would otherwise be
|
|
20
|
+
* printed as "undefined".
|
|
21
|
+
*/
|
|
22
|
+
const tOr = (key, fallback) => {
|
|
23
|
+
let value;
|
|
24
|
+
try {
|
|
25
|
+
value = localeService.t(key);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
value = undefined;
|
|
28
|
+
}
|
|
29
|
+
return typeof value === 'string' && value.trim() && value !== key ? value : fallback;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Score statements are null for a score that has not been calculated yet. */
|
|
33
|
+
const textOr = (value, fallback = '') => (value === null || value === undefined ? fallback : value);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Metrics for a score that is not available yet (null in the payload, e.g. a
|
|
37
|
+
* wellbeing quiz still in progress). parseFloat(null) is NaN, which the gauge
|
|
38
|
+
* used to print as "NaNaN"; show an em dash with the needle at the start instead.
|
|
39
|
+
*/
|
|
40
|
+
const missingScoreMetrics = (client = 'default') => ({
|
|
41
|
+
score: null,
|
|
42
|
+
amount: 214,
|
|
43
|
+
color: client === 'nasco' ? config.second_level_color : config.first_level_color,
|
|
44
|
+
rotateAmount: 0,
|
|
45
|
+
scoreInteger: '\u2014',
|
|
46
|
+
scoreDecimal: ''
|
|
47
|
+
});
|
|
48
|
+
|
|
16
49
|
|
|
17
50
|
let OUT_FILE = 'output/nasco-pdf.html';
|
|
18
51
|
let Pdf_file = '';
|
|
@@ -280,6 +313,9 @@ const wrapLTR = (text) => {
|
|
|
280
313
|
*/
|
|
281
314
|
const calculateScoreMetrics = (score, client = 'default') => {
|
|
282
315
|
const scoreValue = parseFloat(score);
|
|
316
|
+
if (!Number.isFinite(scoreValue)) {
|
|
317
|
+
return missingScoreMetrics(client);
|
|
318
|
+
}
|
|
283
319
|
|
|
284
320
|
// Determine amount based on score range
|
|
285
321
|
let amount;
|
|
@@ -413,6 +449,9 @@ const calculateExtendedScoreMetrics = (score, scoreType, client = 'default') =>
|
|
|
413
449
|
*/
|
|
414
450
|
const calculatePhysicalScoreMetrics = (score, client = 'default') => {
|
|
415
451
|
const scoreValue = parseFloat(score);
|
|
452
|
+
if (!Number.isFinite(scoreValue)) {
|
|
453
|
+
return missingScoreMetrics(client);
|
|
454
|
+
}
|
|
416
455
|
|
|
417
456
|
let amount;
|
|
418
457
|
if (scoreValue > 40 && scoreValue < 70) {
|
|
@@ -475,14 +514,14 @@ const renderFirstSection = (template, calculationData, physicalMetrics, psycholo
|
|
|
475
514
|
return template
|
|
476
515
|
.replace("{{physicalScore}}", physicalMetrics.scoreInteger)
|
|
477
516
|
.replace("{{physicalScoreDecimalPart}}", physicalMetrics.scoreDecimal)
|
|
478
|
-
.replace("{{physicalScoreStatement}}", calculationData.physicalScoreStatement)
|
|
517
|
+
.replace("{{physicalScoreStatement}}", textOr(calculationData.physicalScoreStatement))
|
|
479
518
|
.replace("{{PhysicalWord}}", localeService.t('physical'))
|
|
480
519
|
.replace(/{{rotatePhysicalAmount}}/gi, physicalMetrics.rotateAmount)
|
|
481
520
|
.replace(/{{physicalColor}}/gi, physicalMetrics.color)
|
|
482
521
|
.replace("{{psychologicalScore}}", psychologicalMetrics.scoreInteger)
|
|
483
522
|
.replace("{{psychologicalScoreDecimalPart}}", psychologicalMetrics.scoreDecimal)
|
|
484
523
|
.replace("{{psychologicalWord}}", localeService.t('psychological'))
|
|
485
|
-
.replace("{{psychologicalScoreStatement}}", calculationData.psychologicalScoreStatement)
|
|
524
|
+
.replace("{{psychologicalScoreStatement}}", textOr(calculationData.psychologicalScoreStatement))
|
|
486
525
|
.replace(/{{rotatePsychologicalAmount}}/gi, psychologicalMetrics.rotateAmount)
|
|
487
526
|
.replace(/{{fifthLevelColor}}/g, config.fifth_level_color)
|
|
488
527
|
.replace(/{{thirdLevelColor}}/g, config.third_level_color)
|
|
@@ -507,19 +546,19 @@ const renderExtendedFirstSection = (template, calculationData, metrics, client)
|
|
|
507
546
|
return template
|
|
508
547
|
.replace("{{depressionRiskScore}}", metrics.depression.scoreInteger)
|
|
509
548
|
.replace("{{depressionRiskScoreDecimalPart}}", metrics.depression.scoreDecimal)
|
|
510
|
-
.replace("{{depressionRiskScoreStatement}}", calculationData.depressionRiskScoreStatement)
|
|
549
|
+
.replace("{{depressionRiskScoreStatement}}", textOr(calculationData.depressionRiskScoreStatement))
|
|
511
550
|
.replace("{{DepressionRiskWord}}", localeService.t('depressionRisk'))
|
|
512
551
|
.replace(/{{rotateDepressionRiskAmount}}/gi, metrics.depression.rotateAmount)
|
|
513
552
|
.replace(/{{depressionRiskColor}}/gi, metrics.depression.color)
|
|
514
553
|
.replace("{{anxietyRiskScore}}", metrics.anxiety.scoreInteger)
|
|
515
554
|
.replace("{{anxietyRiskScoreDecimalPart}}", metrics.anxiety.scoreDecimal)
|
|
516
|
-
.replace("{{anxietyRiskScoreStatement}}", calculationData.anxietyRiskScoreStatement)
|
|
555
|
+
.replace("{{anxietyRiskScoreStatement}}", textOr(calculationData.anxietyRiskScoreStatement))
|
|
517
556
|
.replace("{{AnxietyRiskWord}}", localeService.t('anxietyRisk'))
|
|
518
557
|
.replace(/{{rotateAnxietyRiskAmount}}/gi, metrics.anxiety.rotateAmount)
|
|
519
558
|
.replace(/{{anxietyRiskColor}}/gi, metrics.anxiety.color)
|
|
520
559
|
.replace("{{physicalScore}}", metrics.physical.scoreInteger)
|
|
521
560
|
.replace("{{physicalScoreDecimalPart}}", metrics.physical.scoreDecimal)
|
|
522
|
-
.replace("{{physicalScoreStatement}}", calculationData.physicalScoreStatement)
|
|
561
|
+
.replace("{{physicalScoreStatement}}", textOr(calculationData.physicalScoreStatement))
|
|
523
562
|
.replace("{{PhysicalWord}}", localeService.t('physicalScore'))
|
|
524
563
|
.replace(/{{rotatePhysicalAmount}}/gi, metrics.physical.rotateAmount)
|
|
525
564
|
.replace(/{{physicalColor}}/gi, metrics.physical.color)
|
|
@@ -531,7 +570,7 @@ const renderExtendedFirstSection = (template, calculationData, metrics, client)
|
|
|
531
570
|
.replace("{{psychologicalScore}}", metrics.psychological.scoreInteger)
|
|
532
571
|
.replace("{{psychologicalScoreDecimalPart}}", metrics.psychological.scoreDecimal)
|
|
533
572
|
.replace("{{psychologicalWord}}", localeService.t('psychological'))
|
|
534
|
-
.replace("{{psychologicalScoreStatement}}", calculationData.psychologicalScoreStatement)
|
|
573
|
+
.replace("{{psychologicalScoreStatement}}", textOr(calculationData.psychologicalScoreStatement))
|
|
535
574
|
.replace(/{{rotatePsychologicalAmount}}/gi, metrics.psychological.rotateAmount)
|
|
536
575
|
.replace(/{{fifthLevelColor}}/g, config.fifth_level_color)
|
|
537
576
|
.replace(/{{thirdLevelColor}}/g, config.third_level_color)
|
|
@@ -999,7 +1038,7 @@ const renderDoctorDetails = (doctorData, startQuizAnswersOnNewPage = false) => {
|
|
|
999
1038
|
: columnsHTML;
|
|
1000
1039
|
|
|
1001
1040
|
return `
|
|
1002
|
-
<p class="resume-title"
|
|
1041
|
+
<p class="resume-title">${tOr('quizAnswers', 'Quiz Answers')}</p>
|
|
1003
1042
|
${bodyHTML}
|
|
1004
1043
|
`;
|
|
1005
1044
|
};
|
|
@@ -1415,10 +1454,12 @@ let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clien
|
|
|
1415
1454
|
? profileInfo.find(function (item) { return item && /bmi/i.test(String(item.title || '')); })
|
|
1416
1455
|
: null;
|
|
1417
1456
|
console.log('[BigIntegral] maisonsante doctor PDF — doctorScores '
|
|
1418
|
-
+ (empty(data.doctorScores) ? 'ABSENT (
|
|
1457
|
+
+ (empty(data.doctorScores) ? 'ABSENT (section omitted)' : 'present, groups=' + (data.doctorScores.groups || []).length));
|
|
1419
1458
|
const integralHtml = renderBigIntegralQuestionnaire({
|
|
1420
1459
|
clientConfig: rawClientConfig,
|
|
1421
1460
|
doctorScores: data.doctorScores,
|
|
1461
|
+
localeService,
|
|
1462
|
+
isRtl,
|
|
1422
1463
|
meta: {
|
|
1423
1464
|
patientName: wellbeingData.name || '',
|
|
1424
1465
|
age: wellbeingData.age === undefined || wellbeingData.age === null ? '' : String(wellbeingData.age),
|
|
@@ -1788,15 +1829,69 @@ const cleanStyledHeader = (html, profileInfo, language = 'ar', clientName = 'med
|
|
|
1788
1829
|
`;
|
|
1789
1830
|
};
|
|
1790
1831
|
|
|
1832
|
+
/**
|
|
1833
|
+
* Translate `key` in an explicit locale. The footer is built after the HTML, so it
|
|
1834
|
+
* must not rely on whatever locale the shared localeService was last switched to.
|
|
1835
|
+
*/
|
|
1836
|
+
const tForLocale = (locale, key, fallback) => {
|
|
1837
|
+
let entry;
|
|
1838
|
+
try {
|
|
1839
|
+
entry = i18n.__({ phrase: key, locale });
|
|
1840
|
+
} catch (e) {
|
|
1841
|
+
entry = undefined;
|
|
1842
|
+
}
|
|
1843
|
+
const value = entry && typeof entry === 'object' ? entry.message : undefined;
|
|
1844
|
+
return typeof value === 'string' && value.trim() && value !== key ? value : fallback;
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Maison Sante footer for Arabic reports: Arabic labels, Arabic address, laid out
|
|
1849
|
+
* right-to-left (page number on the right, contact block on the left). Latin
|
|
1850
|
+
* values (e-mail address, page numbers) are isolated so they keep their order.
|
|
1851
|
+
* Cairo is installed in the PDF service image (Dockerfile), Open Sans is the fallback.
|
|
1852
|
+
* @param {string} language - report locale, e.g. 'ar-SA'
|
|
1853
|
+
* @returns {string} Footer template HTML
|
|
1854
|
+
*/
|
|
1855
|
+
const getMaisonSanteArabicPdfFooterTemplate = (language) => {
|
|
1856
|
+
const font = "font-family: 'Cairo', 'Open Sans', sans-serif;";
|
|
1857
|
+
const isolate = "direction: ltr; unicode-bidi: isolate; font-family: 'Open Sans', sans-serif; font-feature-settings: 'tnum';";
|
|
1858
|
+
const page = tForLocale(language, 'Page', 'صفحة');
|
|
1859
|
+
const of = tForLocale(language, 'pageOf', 'من');
|
|
1860
|
+
const addressLabel = tForLocale(language, 'footerAddressLabel', 'العنوان');
|
|
1861
|
+
const address = tForLocale(language, 'maisonSanteFooterAddress', 'ميان مول، أم سقيم 2، دبي، الإمارات العربية المتحدة');
|
|
1862
|
+
const emailLabel = tForLocale(language, 'footerEmailLabel', 'البريد الإلكتروني');
|
|
1863
|
+
return `
|
|
1864
|
+
<div dir="rtl" style="width: 100%; font-size:7.3px; color: ${config.colors.footerText}; padding: 0 20px; box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; direction: rtl; ${font}">
|
|
1865
|
+
<div style="display: flex; align-items: center; ${font}">
|
|
1866
|
+
<span style="${font}">${page}
|
|
1867
|
+
<span class="pageNumber" style="${isolate}"></span>
|
|
1868
|
+
${of}
|
|
1869
|
+
<span class="totalPages" style="${isolate}"></span>
|
|
1870
|
+
</span>
|
|
1871
|
+
</div>
|
|
1872
|
+
<div style="display: flex; flex-direction: column; align-items: flex-end; text-align: left; ${font}">
|
|
1873
|
+
<div style="${font}">${addressLabel}: ${address}</div>
|
|
1874
|
+
<div style="${font}">${emailLabel}: <span style="${isolate}">info@maisonsante.ae</span></div>
|
|
1875
|
+
</div>
|
|
1876
|
+
</div>
|
|
1877
|
+
`;
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1791
1880
|
/**
|
|
1792
1881
|
* Get PDF footer template HTML (client-specific contact info)
|
|
1793
1882
|
* @param {string} [client='mediclinic'] - Client identifier: 'mediclinic' | 'pha' | other
|
|
1794
1883
|
* @param {array} [selectedLabs=[]] - Selected divisions/labs (e.g. ['wp'], ['labcorp'])
|
|
1795
1884
|
* @param {object} [footerFromData] - Footer data from input (e.g. { address: "123 Main St..." }) for PHA lab footers
|
|
1885
|
+
* @param {string} [language='en'] - Report locale; Maison Sante Arabic reports get a translated RTL footer
|
|
1796
1886
|
* @returns {string} Footer template HTML
|
|
1797
1887
|
*/
|
|
1798
|
-
const getPdfFooterTemplate = (client = 'mediclinic', selectedLabs = [], footerFromData = null) => {
|
|
1888
|
+
const getPdfFooterTemplate = (client = 'mediclinic', selectedLabs = [], footerFromData = null, language = 'en') => {
|
|
1799
1889
|
const clientKey = (client || 'mediclinic').toLocaleLowerCase();
|
|
1890
|
+
// Maison Sante Arabic reports get a translated, right-to-left footer. Every other
|
|
1891
|
+
// client/language keeps the footer below exactly as it was.
|
|
1892
|
+
if (clientKey === 'maisonsante' && /^ar/i.test(String(language || ''))) {
|
|
1893
|
+
return getMaisonSanteArabicPdfFooterTemplate(language);
|
|
1894
|
+
}
|
|
1800
1895
|
const isPhaWithWp = clientKey === 'pha' && selectedLabs?.some(lab => lab?.toLowerCase() === 'wp');
|
|
1801
1896
|
const isPhaWithLabcorp = clientKey === 'pha' && selectedLabs?.some(lab => lab?.toLowerCase() === 'labcorp');
|
|
1802
1897
|
const footerAddress =
|
|
@@ -1909,7 +2004,7 @@ const setupExtendedPageContent = async (page, contentHtml, filePath) => {
|
|
|
1909
2004
|
const generatePDFReport = async (data, hideWellbeingUI = false) => {
|
|
1910
2005
|
try {
|
|
1911
2006
|
const contentHtml = data.html;
|
|
1912
|
-
const footerTemplate = getPdfFooterTemplate(data.client, data.selectedLabs, data.footer);
|
|
2007
|
+
const footerTemplate = getPdfFooterTemplate(data.client, data.selectedLabs, data.footer, data.language);
|
|
1913
2008
|
|
|
1914
2009
|
// Launch browser and setup page
|
|
1915
2010
|
const browser = await launchExtendedBrowser();
|
|
@@ -146,5 +146,109 @@
|
|
|
146
146
|
"referenceTitle": {
|
|
147
147
|
"message": "الرقم المرجعي",
|
|
148
148
|
"description": "التسمية الظاهرة فوق الأطباء الذين وقعوا على هذا التقرير"
|
|
149
|
+
},
|
|
150
|
+
"bigIntegralTitle": {
|
|
151
|
+
"message": "مفتاح الاستبيان التكاملي الشامل",
|
|
152
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
153
|
+
},
|
|
154
|
+
"bigIntegralPatientName": {
|
|
155
|
+
"message": "الاسم الكامل للعميل/المريض",
|
|
156
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
157
|
+
},
|
|
158
|
+
"bigIntegralAge": {
|
|
159
|
+
"message": "العمر",
|
|
160
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
161
|
+
},
|
|
162
|
+
"bigIntegralBmi": {
|
|
163
|
+
"message": "منسب كتلة الجسم (BMI)",
|
|
164
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
165
|
+
},
|
|
166
|
+
"bigIntegralSystemScores": {
|
|
167
|
+
"message": "نتائج الأجهزة",
|
|
168
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
169
|
+
},
|
|
170
|
+
"bigIntegralSystem": {
|
|
171
|
+
"message": "الجهاز",
|
|
172
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
173
|
+
},
|
|
174
|
+
"bigIntegralHigh": {
|
|
175
|
+
"message": "مرتفع",
|
|
176
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
177
|
+
},
|
|
178
|
+
"bigIntegralQuestions": {
|
|
179
|
+
"message": "الأسئلة",
|
|
180
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
181
|
+
},
|
|
182
|
+
"bigIntegralResult": {
|
|
183
|
+
"message": "النتيجة",
|
|
184
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
185
|
+
},
|
|
186
|
+
"bigIntegralMaximum": {
|
|
187
|
+
"message": "الحد الأقصى",
|
|
188
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
189
|
+
},
|
|
190
|
+
"bigIntegralHormonalFemale": {
|
|
191
|
+
"message": "الصحة الهرمونية (إناث)",
|
|
192
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
193
|
+
},
|
|
194
|
+
"bigIntegralHormonalMale": {
|
|
195
|
+
"message": "الصحة الهرمونية (ذكور)",
|
|
196
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
197
|
+
},
|
|
198
|
+
"bigIntegralSystem_stomach-and-gi-tract": {
|
|
199
|
+
"message": "المعدة والجهاز الهضمي",
|
|
200
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
201
|
+
},
|
|
202
|
+
"bigIntegralSystem_small-intestine": {
|
|
203
|
+
"message": "الأمعاء الدقيقة",
|
|
204
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
205
|
+
},
|
|
206
|
+
"bigIntegralSystem_infections-invasions-of-the-gi": {
|
|
207
|
+
"message": "العدوى/الغزو في الجهاز الهضمي",
|
|
208
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
209
|
+
},
|
|
210
|
+
"bigIntegralSystem_large-intestine": {
|
|
211
|
+
"message": "الأمعاء الغليظة",
|
|
212
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
213
|
+
},
|
|
214
|
+
"bigIntegralSystem_liver-and-gallbladder": {
|
|
215
|
+
"message": "الكبد والمرارة",
|
|
216
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
217
|
+
},
|
|
218
|
+
"bigIntegralSystem_essential-nutrients": {
|
|
219
|
+
"message": "العناصر الغذائية الأساسية",
|
|
220
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
221
|
+
},
|
|
222
|
+
"bigIntegralSystem_carbohydrate-metabolism": {
|
|
223
|
+
"message": "استقلاب الكربوهيدرات",
|
|
224
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
225
|
+
},
|
|
226
|
+
"bigIntegralSystem_immune-system": {
|
|
227
|
+
"message": "الجهاز المناعي",
|
|
228
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
229
|
+
},
|
|
230
|
+
"bigIntegralSystem_lifestyle": {
|
|
231
|
+
"message": "نمط الحياة",
|
|
232
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
233
|
+
},
|
|
234
|
+
"quizAnswers": {
|
|
235
|
+
"message": "إجابات الاستبيان",
|
|
236
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
237
|
+
},
|
|
238
|
+
"pageOf": {
|
|
239
|
+
"message": "من",
|
|
240
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
241
|
+
},
|
|
242
|
+
"footerAddressLabel": {
|
|
243
|
+
"message": "العنوان",
|
|
244
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
245
|
+
},
|
|
246
|
+
"footerEmailLabel": {
|
|
247
|
+
"message": "البريد الإلكتروني",
|
|
248
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
249
|
+
},
|
|
250
|
+
"maisonSanteFooterAddress": {
|
|
251
|
+
"message": "ميان مول، أم سقيم 2، دبي، الإمارات العربية المتحدة",
|
|
252
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
149
253
|
}
|
|
150
|
-
}
|
|
254
|
+
}
|
|
@@ -254,5 +254,109 @@
|
|
|
254
254
|
"PsychologicalWellbeingText": {
|
|
255
255
|
"message": "الصحة النفسية",
|
|
256
256
|
"description": "التسمية المعروضة فوق الأطباء الذين وقعوا على هذا التقرير."
|
|
257
|
+
},
|
|
258
|
+
"bigIntegralTitle": {
|
|
259
|
+
"message": "مفتاح الاستبيان التكاملي الشامل",
|
|
260
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
261
|
+
},
|
|
262
|
+
"bigIntegralPatientName": {
|
|
263
|
+
"message": "الاسم الكامل للعميل/المريض",
|
|
264
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
265
|
+
},
|
|
266
|
+
"bigIntegralAge": {
|
|
267
|
+
"message": "العمر",
|
|
268
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
269
|
+
},
|
|
270
|
+
"bigIntegralBmi": {
|
|
271
|
+
"message": "منسب كتلة الجسم (BMI)",
|
|
272
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
273
|
+
},
|
|
274
|
+
"bigIntegralSystemScores": {
|
|
275
|
+
"message": "نتائج الأجهزة",
|
|
276
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
277
|
+
},
|
|
278
|
+
"bigIntegralSystem": {
|
|
279
|
+
"message": "الجهاز",
|
|
280
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
281
|
+
},
|
|
282
|
+
"bigIntegralHigh": {
|
|
283
|
+
"message": "مرتفع",
|
|
284
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
285
|
+
},
|
|
286
|
+
"bigIntegralQuestions": {
|
|
287
|
+
"message": "الأسئلة",
|
|
288
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
289
|
+
},
|
|
290
|
+
"bigIntegralResult": {
|
|
291
|
+
"message": "النتيجة",
|
|
292
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
293
|
+
},
|
|
294
|
+
"bigIntegralMaximum": {
|
|
295
|
+
"message": "الحد الأقصى",
|
|
296
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
297
|
+
},
|
|
298
|
+
"bigIntegralHormonalFemale": {
|
|
299
|
+
"message": "الصحة الهرمونية (إناث)",
|
|
300
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
301
|
+
},
|
|
302
|
+
"bigIntegralHormonalMale": {
|
|
303
|
+
"message": "الصحة الهرمونية (ذكور)",
|
|
304
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
305
|
+
},
|
|
306
|
+
"bigIntegralSystem_stomach-and-gi-tract": {
|
|
307
|
+
"message": "المعدة والجهاز الهضمي",
|
|
308
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
309
|
+
},
|
|
310
|
+
"bigIntegralSystem_small-intestine": {
|
|
311
|
+
"message": "الأمعاء الدقيقة",
|
|
312
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
313
|
+
},
|
|
314
|
+
"bigIntegralSystem_infections-invasions-of-the-gi": {
|
|
315
|
+
"message": "العدوى/الغزو في الجهاز الهضمي",
|
|
316
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
317
|
+
},
|
|
318
|
+
"bigIntegralSystem_large-intestine": {
|
|
319
|
+
"message": "الأمعاء الغليظة",
|
|
320
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
321
|
+
},
|
|
322
|
+
"bigIntegralSystem_liver-and-gallbladder": {
|
|
323
|
+
"message": "الكبد والمرارة",
|
|
324
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
325
|
+
},
|
|
326
|
+
"bigIntegralSystem_essential-nutrients": {
|
|
327
|
+
"message": "العناصر الغذائية الأساسية",
|
|
328
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
329
|
+
},
|
|
330
|
+
"bigIntegralSystem_carbohydrate-metabolism": {
|
|
331
|
+
"message": "استقلاب الكربوهيدرات",
|
|
332
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
333
|
+
},
|
|
334
|
+
"bigIntegralSystem_immune-system": {
|
|
335
|
+
"message": "الجهاز المناعي",
|
|
336
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
337
|
+
},
|
|
338
|
+
"bigIntegralSystem_lifestyle": {
|
|
339
|
+
"message": "نمط الحياة",
|
|
340
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
341
|
+
},
|
|
342
|
+
"quizAnswers": {
|
|
343
|
+
"message": "إجابات الاستبيان",
|
|
344
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
345
|
+
},
|
|
346
|
+
"pageOf": {
|
|
347
|
+
"message": "من",
|
|
348
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
349
|
+
},
|
|
350
|
+
"footerAddressLabel": {
|
|
351
|
+
"message": "العنوان",
|
|
352
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
353
|
+
},
|
|
354
|
+
"footerEmailLabel": {
|
|
355
|
+
"message": "البريد الإلكتروني",
|
|
356
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
357
|
+
},
|
|
358
|
+
"maisonSanteFooterAddress": {
|
|
359
|
+
"message": "ميان مول، أم سقيم 2، دبي، الإمارات العربية المتحدة",
|
|
360
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
257
361
|
}
|
|
258
|
-
}
|
|
362
|
+
}
|
|
@@ -407,18 +407,114 @@
|
|
|
407
407
|
"message": "Psychological Wellbeing",
|
|
408
408
|
"description": "The label displayed above the doctors who signed this report."
|
|
409
409
|
},
|
|
410
|
-
"bigIntegralSystem":
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
"
|
|
410
|
+
"bigIntegralSystem": {
|
|
411
|
+
"message": "SYSTEM",
|
|
412
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
413
|
+
},
|
|
414
|
+
"bigIntegralHigh": {
|
|
415
|
+
"message": "High",
|
|
416
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
417
|
+
},
|
|
418
|
+
"bigIntegralQuestions": {
|
|
419
|
+
"message": "Questions",
|
|
420
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
421
|
+
},
|
|
422
|
+
"bigIntegralResult": {
|
|
423
|
+
"message": "Result",
|
|
424
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
425
|
+
},
|
|
426
|
+
"bigIntegralMaximum": {
|
|
427
|
+
"message": "Maximum",
|
|
428
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
429
|
+
},
|
|
415
430
|
"bigIntegralComments": "bigIntegralComments",
|
|
416
431
|
"bigIntegralElevated": "bigIntegralElevated",
|
|
417
432
|
"bigIntegralVariantClinical": "bigIntegralVariantClinical",
|
|
418
433
|
"bigIntegralDateOfCompletion": "bigIntegralDateOfCompletion",
|
|
419
|
-
"bigIntegralPatientName":
|
|
420
|
-
|
|
421
|
-
|
|
434
|
+
"bigIntegralPatientName": {
|
|
435
|
+
"message": "Client/Patient Full Name",
|
|
436
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
437
|
+
},
|
|
438
|
+
"bigIntegralAge": {
|
|
439
|
+
"message": "Age",
|
|
440
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
441
|
+
},
|
|
442
|
+
"bigIntegralBmi": {
|
|
443
|
+
"message": "BMI",
|
|
444
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
445
|
+
},
|
|
422
446
|
"bigIntegralVariantScorecard": "bigIntegralVariantScorecard",
|
|
423
|
-
"bigIntegralVariantHybrid": "bigIntegralVariantHybrid"
|
|
424
|
-
|
|
447
|
+
"bigIntegralVariantHybrid": "bigIntegralVariantHybrid",
|
|
448
|
+
"bigIntegralTitle": {
|
|
449
|
+
"message": "Key to the Big Integral Questionnaire",
|
|
450
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
451
|
+
},
|
|
452
|
+
"bigIntegralSystemScores": {
|
|
453
|
+
"message": "SYSTEM SCORES",
|
|
454
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
455
|
+
},
|
|
456
|
+
"bigIntegralHormonalFemale": {
|
|
457
|
+
"message": "Hormonal Health Female",
|
|
458
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
459
|
+
},
|
|
460
|
+
"bigIntegralHormonalMale": {
|
|
461
|
+
"message": "Hormonal Health Male",
|
|
462
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
463
|
+
},
|
|
464
|
+
"bigIntegralSystem_stomach-and-gi-tract": {
|
|
465
|
+
"message": "STOMACH and GI (Gastrointestinal) Tract",
|
|
466
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
467
|
+
},
|
|
468
|
+
"bigIntegralSystem_small-intestine": {
|
|
469
|
+
"message": "Small Intestine",
|
|
470
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
471
|
+
},
|
|
472
|
+
"bigIntegralSystem_infections-invasions-of-the-gi": {
|
|
473
|
+
"message": "Infections/Invasions of the GI (Gastrointestinal Tract)",
|
|
474
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
475
|
+
},
|
|
476
|
+
"bigIntegralSystem_large-intestine": {
|
|
477
|
+
"message": "Large Intestine",
|
|
478
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
479
|
+
},
|
|
480
|
+
"bigIntegralSystem_liver-and-gallbladder": {
|
|
481
|
+
"message": "Liver and Gallbladder",
|
|
482
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
483
|
+
},
|
|
484
|
+
"bigIntegralSystem_essential-nutrients": {
|
|
485
|
+
"message": "Essential Nutrients",
|
|
486
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
487
|
+
},
|
|
488
|
+
"bigIntegralSystem_carbohydrate-metabolism": {
|
|
489
|
+
"message": "Carbohydrate Metabolism",
|
|
490
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
491
|
+
},
|
|
492
|
+
"bigIntegralSystem_immune-system": {
|
|
493
|
+
"message": "Immune System",
|
|
494
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
495
|
+
},
|
|
496
|
+
"bigIntegralSystem_lifestyle": {
|
|
497
|
+
"message": "Lifestyle",
|
|
498
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
499
|
+
},
|
|
500
|
+
"quizAnswers": {
|
|
501
|
+
"message": "Quiz Answers",
|
|
502
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
503
|
+
},
|
|
504
|
+
"pageOf": {
|
|
505
|
+
"message": "of",
|
|
506
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
507
|
+
},
|
|
508
|
+
"footerAddressLabel": {
|
|
509
|
+
"message": "Address",
|
|
510
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
511
|
+
},
|
|
512
|
+
"footerEmailLabel": {
|
|
513
|
+
"message": "Email",
|
|
514
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
515
|
+
},
|
|
516
|
+
"maisonSanteFooterAddress": {
|
|
517
|
+
"message": "Meyan Mall, Umm Suqeim 2, Dubai, United Arab Emirates",
|
|
518
|
+
"description": "Maison Sante PDF (Big Integral Questionnaire / Quiz Answers / footer)"
|
|
519
|
+
}
|
|
520
|
+
}
|
package/package.json
CHANGED
package/preview-big-integral.js
CHANGED
|
@@ -15,7 +15,7 @@ const clientConfig = JSON.parse(
|
|
|
15
15
|
fs.readFileSync(path.resolve(__dirname, 'config/maisonsante.json'), 'utf8')
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
-
const sectionHtml = renderBigIntegralQuestionnaire({ clientConfig });
|
|
18
|
+
const sectionHtml = renderBigIntegralQuestionnaire({ clientConfig, usePlaceholder: true });
|
|
19
19
|
|
|
20
20
|
const fullHtml = `<!DOCTYPE html>
|
|
21
21
|
<html lang="en">
|
|
@@ -14,6 +14,52 @@
|
|
|
14
14
|
-webkit-font-feature-settings: "zero" off !important;
|
|
15
15
|
-moz-font-feature-settings: "zero" off !important;
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/* Maison Santé Arabic smart report: mixed-direction text.
|
|
19
|
+
The shared RTL sheet forces panel/biomarker titles to direction:ltr and
|
|
20
|
+
lays lab notes out RTL, so Arabic titles get their parentheses mirrored
|
|
21
|
+
("(المعادن (المصل") and English notes get their punctuation displaced
|
|
22
|
+
(".Iron shows", ":Reference"). unicode-bidi:plaintext lets every line
|
|
23
|
+
pick its own direction from its first strong character, so Arabic lines
|
|
24
|
+
read RTL and English lines read LTR, while staying right-aligned. */
|
|
25
|
+
.score-main .panel-title-text,
|
|
26
|
+
.score-main .biom-title,
|
|
27
|
+
.score-main .biomarker-notes,
|
|
28
|
+
.score-main .panel-notes,
|
|
29
|
+
.score-main .biomarker-details-table td {
|
|
30
|
+
unicode-bidi: plaintext !important;
|
|
31
|
+
text-align: right !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Reference ranges are numeric ("4 - 10", "<= 4", "> 50"); rendered RTL they
|
|
35
|
+
come out mirrored ("10 - 4", "4 =>", "50 <"). Keep the text LTR but leave
|
|
36
|
+
the cells themselves in RTL order. */
|
|
37
|
+
.score-main .parent-reference-range,
|
|
38
|
+
.score-main td.biom-range,
|
|
39
|
+
.score-main tr.values td,
|
|
40
|
+
.score-main .biom-value {
|
|
41
|
+
direction: ltr !important;
|
|
42
|
+
unicode-bidi: isolate !important;
|
|
43
|
+
}
|
|
44
|
+
.score-main .parent-reference-range,
|
|
45
|
+
.score-main td.biom-range {
|
|
46
|
+
text-align: right !important;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Quiz Answers (doctor PDF): questions/answers that only exist in English had
|
|
50
|
+
their punctuation displaced ("?Do you have ..."). Same per-line direction. */
|
|
51
|
+
.score-main .doctor-details .question-title,
|
|
52
|
+
.score-main .doctor-details .details-value {
|
|
53
|
+
unicode-bidi: plaintext !important;
|
|
54
|
+
text-align: right !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Wellbeing gauges: the "/100" under the score rendered as "100/" in RTL. */
|
|
58
|
+
.score-main .speedometer-value-down,
|
|
59
|
+
.score-main .speedometer-value-up {
|
|
60
|
+
direction: ltr !important;
|
|
61
|
+
unicode-bidi: isolate !important;
|
|
62
|
+
}
|
|
17
63
|
</style>
|
|
18
64
|
<script>
|
|
19
65
|
var IS_ARABIC = true;
|