json-object-editor 0.10.665 → 0.10.670

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.
@@ -0,0 +1,563 @@
1
+ module.exports = function(data, options) {
2
+ const protocol = data.protocol || data.ITEM;
3
+
4
+ if (!protocol) {
5
+ return '<div style="padding: 20px; color: #dc3545;">Error: No protocol data available</div>';
6
+ }
7
+
8
+ // Helper functions
9
+ function getRef(refId) {
10
+ if (!refId) return null;
11
+ try {
12
+ return $J.get(refId);
13
+ } catch(e) {
14
+ console.error('Error getting reference:', refId, e);
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function extractText(rendering) {
20
+ if (!rendering) return '';
21
+ if (typeof rendering === 'string') {
22
+ return rendering.replace(/<[^>]*>/g, '').trim();
23
+ }
24
+ return String(rendering);
25
+ }
26
+
27
+ function preserveHtml(html) {
28
+ if (!html) return '';
29
+ if (typeof html === 'string') {
30
+ return html.trim();
31
+ }
32
+ return String(html);
33
+ }
34
+
35
+ function escapeHtml(text) {
36
+ if (!text) return '';
37
+ return String(text)
38
+ .replace(/&/g, '&' + 'amp;')
39
+ .replace(/</g, '&' + 'lt;')
40
+ .replace(/>/g, '&' + 'gt;')
41
+ .replace(/"/g, '&' + 'quot;')
42
+ .replace(/'/g, '&' + '#039;');
43
+ }
44
+
45
+ function formatDate(dateString) {
46
+ if (!dateString) return '';
47
+ try {
48
+ const date = new Date(dateString);
49
+ return date.toLocaleDateString('en-US', {
50
+ year: 'numeric',
51
+ month: 'long',
52
+ day: 'numeric'
53
+ });
54
+ } catch(e) {
55
+ return '';
56
+ }
57
+ }
58
+
59
+ function splitList(text) {
60
+ if (!text) return [];
61
+ return text.split(/\n|•|·|-/).map(s => s.trim()).filter(Boolean);
62
+ }
63
+
64
+ // Get client (handle both array and string formats)
65
+ let clientId = protocol.client;
66
+ if (Array.isArray(clientId)) {
67
+ clientId = clientId.length > 0 ? clientId[0] : null;
68
+ }
69
+ const client = clientId ? getRef(clientId) : null;
70
+
71
+ // Build report data
72
+ const reportData = {
73
+ protocol_name: protocol.name || '',
74
+ protocol_headline: protocol.info || '',
75
+ name: client ? (client.name || '') : '',
76
+ email: client ? (client.email || '') : '',
77
+ phone_number: client ? (client.phone || '') : '',
78
+ generated_on: formatDate(protocol.joeUpdated || protocol.created),
79
+ protocol_conditions: [],
80
+ main_concerns: [],
81
+ top_outcomes: [],
82
+ client_symptoms: '',
83
+ urine_ph: protocol.urine_analysis_ph ? String(protocol.urine_analysis_ph) : '',
84
+ urine_status: protocol.urine_status || '',
85
+ blood_pressure_analysis_headline: protocol.blood_pressure_analysis_headline || '',
86
+ urine_analysis_headline: protocol.urine_analysis_headline || '',
87
+ condition_summaries: []
88
+ };
89
+
90
+ // Client data
91
+ if (client) {
92
+ if (client.main_concerns) {
93
+ reportData.main_concerns = splitList(extractText(client.main_concerns));
94
+ }
95
+ if (client.top_outcomes) {
96
+ reportData.top_outcomes = splitList(extractText(client.top_outcomes));
97
+ }
98
+ if (client.client_symptoms) {
99
+ reportData.client_symptoms = extractText(client.client_symptoms);
100
+ }
101
+ }
102
+
103
+ // Protocol conditions
104
+ if (protocol.conditions && Array.isArray(protocol.conditions)) {
105
+ reportData.protocol_conditions = protocol.conditions.map(function(condId) {
106
+ const cond = getRef(condId);
107
+ return cond ? cond.name : null;
108
+ }).filter(Boolean);
109
+ }
110
+
111
+ // Build phases
112
+ const phases = [];
113
+ for (let i = 1; i <= 5; i++) {
114
+ const phaseHeadline = protocol[`phase_${i}_headline`] || '';
115
+ const phaseDesc = protocol[`phase_${i}_description`] ? preserveHtml(protocol[`phase_${i}_description`]) : '';
116
+ const phaseRecIds = protocol[`phase_${i}_recommendations`] || [];
117
+ const phaseOverrides = protocol[`phase_${i}_overrides`] || [];
118
+
119
+ // Handle both array and single value formats
120
+ const recIds = Array.isArray(phaseRecIds) ? phaseRecIds : (phaseRecIds ? [phaseRecIds] : []);
121
+
122
+ // Build override lookup map - match rec _id on recToOverride
123
+ const overrideMap = {};
124
+ if (Array.isArray(phaseOverrides)) {
125
+ phaseOverrides.forEach(function(override) {
126
+ const recIdField = `phase_${i}_recToOverride`;
127
+ const dosageField = `phase_${i}_dosage`;
128
+ const instructionsField = `phase_${i}_instructions`;
129
+
130
+ const recId = override[recIdField];
131
+ if (recId) {
132
+ overrideMap[recId] = {
133
+ dosage: override[dosageField] || '',
134
+ instructions: override[instructionsField] || ''
135
+ };
136
+ }
137
+ });
138
+ }
139
+
140
+ const recommendations = recIds.map(function(recId) {
141
+ if (!recId || typeof recId !== 'string') return null;
142
+ try {
143
+ const rec = getRef(recId);
144
+ if (!rec || !rec.name) return null;
145
+
146
+ const override = overrideMap[recId] || {};
147
+
148
+ return {
149
+ _id: recId,
150
+ name: rec.name || '',
151
+ short_description: rec.info || '',
152
+ long_description: extractText(rec.description) || '',
153
+ dosage: override.dosage || rec.standard_dosage || '',
154
+ overrideInstructions: override.instructions || '',
155
+ domain: rec.recommendation_domain || 'product'
156
+ };
157
+ } catch(e) {
158
+ console.error('Error processing recommendation:', recId, e);
159
+ return null;
160
+ }
161
+ }).filter(Boolean);
162
+
163
+ // Always add phase if it has headline or description, even without recommendations
164
+ if (phaseHeadline || phaseDesc) {
165
+ phases.push({
166
+ headline: phaseHeadline,
167
+ description: phaseDesc,
168
+ recommendations: recommendations
169
+ });
170
+ }
171
+ }
172
+
173
+ // Collect all unique recommendations from all phases
174
+ const allRecommendationsMap = new Map();
175
+ phases.forEach(function(phase) {
176
+ phase.recommendations.forEach(function(rec) {
177
+ if (rec._id && !allRecommendationsMap.has(rec._id)) {
178
+ allRecommendationsMap.set(rec._id, rec);
179
+ }
180
+ });
181
+ });
182
+
183
+ // Group recommendations by domain
184
+ const allRecommendations = Array.from(allRecommendationsMap.values());
185
+ const recommendationsByDomain = {
186
+ product: [],
187
+ dietary: [],
188
+ activity: []
189
+ };
190
+
191
+ allRecommendations.forEach(function(rec) {
192
+ const domain = rec.domain || 'product';
193
+ if (recommendationsByDomain[domain]) {
194
+ recommendationsByDomain[domain].push(rec);
195
+ } else {
196
+ recommendationsByDomain.product.push(rec); // Fallback
197
+ }
198
+ });
199
+
200
+ // Condition summaries
201
+ if (protocol.conditions && Array.isArray(protocol.conditions)) {
202
+ reportData.condition_summaries = protocol.conditions.map(function(condId) {
203
+ const cond = getRef(condId);
204
+ if (!cond) return null;
205
+ return {
206
+ name: cond.name || '',
207
+ description: extractText(cond.long_description || cond.description || '')
208
+ };
209
+ }).filter(Boolean);
210
+ }
211
+
212
+ // HTML generation helpers
213
+ function renderPill(label, value) {
214
+ if (!value) return '';
215
+ return `<span class="pill"><small>${escapeHtml(label)}</small> ${escapeHtml(value)}</span>`;
216
+ }
217
+
218
+ function renderPhaseRecommendationLink(rec) {
219
+ const recId = rec._id || rec.name.toLowerCase().replace(/\s+/g, '-');
220
+ const dosageHtml = rec.dosage ? `<div class="dose">${escapeHtml(rec.dosage)}</div>` : '';
221
+ const instructionsHtml = rec.overrideInstructions ? `<div style="font-size: 11px; color: #666; margin-top: 6px; font-style: italic; padding-left: 4px;">${escapeHtml(rec.overrideInstructions)}</div>` : '';
222
+
223
+ return `
224
+ <div style="margin-bottom: 8px;">
225
+ <div style="display:flex; justify-content:space-between; gap:10px; align-items:flex-start;">
226
+ <div style="flex:1;">
227
+ <a href="#rec-${recId}" class="rec-link">${escapeHtml(rec.name)}</a>
228
+ ${instructionsHtml}
229
+ </div>
230
+ ${dosageHtml}
231
+ </div>
232
+ </div>
233
+ `;
234
+ }
235
+
236
+ function renderFullRecommendation(rec) {
237
+ const recId = rec._id || rec.name.toLowerCase().replace(/\s+/g, '-');
238
+ return `
239
+ <div class="rec" id="rec-${recId}">
240
+ <div class="rec-top">
241
+ <div>
242
+ <div class="rec-name">${escapeHtml(rec.name)}</div>
243
+ ${rec.short_description ? `<div class="muted" style="margin-top:6px">
244
+ ${escapeHtml(rec.short_description)}
245
+ <button class="toggle" type="button" data-toggle="more" aria-expanded="false">+</button>
246
+ </div>` : ''}
247
+ </div>
248
+ ${rec.dosage ? `<div class="dose">${escapeHtml(rec.dosage)}</div>` : ''}
249
+ </div>
250
+ <div class="more">
251
+ ${rec.long_description ? `<div class="muted">${escapeHtml(rec.long_description)}</div>` : ''}
252
+ </div>
253
+ </div>
254
+ `;
255
+ }
256
+
257
+ function renderPhase(phase, i) {
258
+ if (!phase.headline && !phase.description && phase.recommendations.length === 0) {
259
+ return '';
260
+ }
261
+ var pmap = ["Phase 1: Stabilization",
262
+ "Phase 2: Foundations",
263
+ "Phase 3: Repair",
264
+ "Phase 4: Optimization",
265
+ "Phase 5: Integration & Maintenance"];
266
+
267
+ const phaseInstructions = protocol[`phase_${i + 1}_instructions`] || '';
268
+ const instructionsHtml = phaseInstructions ? preserveHtml(phaseInstructions) : '';
269
+
270
+ return `
271
+ <div class="phase">
272
+ <div class="head">
273
+ <div class="phase-label">${pmap[i]}
274
+ ${phase.headline ? `<div><small><i>${escapeHtml(phase.headline)}</i></small></div>` : ''}
275
+ </div>
276
+ ${phase.description ? `<div class="muted">${phase.description}</div>` : ''}
277
+ </div>
278
+ ${phase.recommendations.length > 0 || instructionsHtml ? `
279
+ <div class="recs" style="display:grid; grid-template-columns: 1fr 1fr; gap: 16px; padding:12px;">
280
+ <div>
281
+ <h4 style="margin:0 0 8px; font-size:13px; color:var(--plum);">Recommendations</h4>
282
+ <div style="display:flex; flex-direction:column; gap:6px;">
283
+ ${phase.recommendations.map(rec => renderPhaseRecommendationLink(rec)).join('')}
284
+ </div>
285
+ </div>
286
+ ${instructionsHtml ? `
287
+ <div>
288
+ <h4 style="margin:0 0 8px; font-size:13px; color:var(--plum);">Instructions</h4>
289
+ <div class="muted">${instructionsHtml}</div>
290
+ </div>
291
+ ` : '<div></div>'}
292
+ </div>
293
+ ` : ''}
294
+ </div>
295
+ `;
296
+ }
297
+
298
+ // Generate HTML
299
+ const html = `
300
+ <!doctype html>
301
+ <html lang="en">
302
+ <head>
303
+ <meta charset="utf-8" />
304
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
305
+ <title>Harmonious Wellness — Protocol Report</title>
306
+ <style>
307
+ :root{
308
+ --plum:#3a1430;
309
+ --gold:#c6a44d;
310
+ --olive:#6c7b3b;
311
+ --ink:#0f172a;
312
+ --muted:#475569;
313
+ --bg:#0b0810;
314
+ --paper:#ffffff;
315
+ --soft:#f8fafc;
316
+ --line:#e5e7eb;
317
+ --max: 980px;
318
+ --radius: 16px;
319
+ --font: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
320
+ }
321
+
322
+ html,body{height:100%;}
323
+ body{margin:0; font-family:var(--font); background:var(--bg); color:var(--ink);}
324
+
325
+ .topbar{position:sticky; top:0; z-index:10; background:rgba(11,8,16,.92); border-bottom:1px solid rgba(255,255,255,.10);}
326
+ .topbar .inner{max-width:var(--max); margin:0 auto; padding:10px 14px; display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;}
327
+ .brand{display:flex; align-items:center; gap:10px; color:#f1f5f9;}
328
+ .mark{width:28px; height:28px; border-radius:10px; background:linear-gradient(135deg,var(--olive),var(--gold)); box-shadow:0 0 0 3px rgba(198,164,77,.15);}
329
+ .brand b{letter-spacing:.2px;}
330
+ .actions{display:flex; gap:8px; flex-wrap:wrap;}
331
+ button{border:1px solid rgba(255,255,255,.18); background:rgba(255,255,255,.06); color:#e2e8f0; padding:8px 10px; border-radius:999px; font-weight:700; cursor:pointer; transition:opacity 0.2s;}
332
+ button:hover{opacity:0.8;}
333
+ button.primary{border-color:rgba(198,164,77,.55); background:rgba(198,164,77,.18); color:#fff7e6;}
334
+
335
+ .wrap{max-width:var(--max); margin:18px auto 52px; padding:0 14px;}
336
+ .paper{background:var(--paper); border-radius:22px; overflow:hidden; border:1px solid rgba(255,255,255,.06);}
337
+
338
+ .hero{padding:18px; border-bottom:1px solid var(--line); background:linear-gradient(180deg,#fff,#fbfdff);}
339
+ .hero h1{margin:0; font-size:22px; line-height:1.2;}
340
+ .hero .sub{margin-top:6px; color:var(--muted); font-weight:700;}
341
+ .meta{display:flex; gap:10px; flex-wrap:wrap; margin-top:12px;}
342
+ .pill{display:inline-flex; gap:8px; align-items:center; padding:6px 10px; border:1px solid var(--line); background:var(--soft); border-radius:999px; font-weight:800; font-size:13px;}
343
+ .pill small{color:var(--muted); font-weight:800;}
344
+
345
+ .section{padding:16px 18px; border-bottom:1px solid var(--line);}
346
+ .section:last-child{border-bottom:none;}
347
+ .section h2{margin:0 0 10px; font-size:16px;}
348
+ .grid{display:grid; gap:10px;}
349
+ .two{grid-template-columns: repeat(2, minmax(0,1fr));}
350
+ @media (max-width:780px){.two{grid-template-columns:1fr;}}
351
+
352
+ .card{border:1px solid var(--line); border-radius:var(--radius); padding:12px; background:#fff;}
353
+ .card h3{margin:0 0 8px; font-size:13px; color:var(--plum);}
354
+ .muted{color:var(--muted); line-height:1.55;}
355
+ ul{margin:0; padding-left:18px; color:var(--muted); font-weight:650;}
356
+ li{margin:6px 0;}
357
+ .phase-label{font-weight:bold;}
358
+ .phase{border:1px solid var(--line); border-radius:18px; overflow:hidden; margin-bottom:12px;}
359
+ .phase .head{padding:12px; background:linear-gradient(180deg,#fff,#f9fbff); border-bottom:1px solid var(--line);}
360
+ .phase .head b{color:var(--plum);}
361
+ .phase .head p{margin:6px 0 0;}
362
+ .phase .recs{padding:12px; display:grid; gap:10px;}
363
+
364
+ .rec{border:1px solid var(--line); border-radius:14px; padding:10px;}
365
+ .rec-top{display:flex; justify-content:space-between; gap:10px; align-items:flex-start;}
366
+ .rec-name{font-weight:900;}
367
+ .rec-link{color:var(--plum); text-decoration:none; font-weight:600; padding:4px 0; display:block; transition:opacity 0.2s;}
368
+ .rec-link:hover{text-decoration:underline; opacity:0.8;}
369
+ .dose{font-size:12px; font-weight:900; color:var(--plum); background:rgba(58,20,48,.06); border:1px solid rgba(58,20,48,.12); padding:6px 10px; border-radius:999px; white-space:nowrap;}
370
+ .more{display:none; margin-top:8px; padding-top:8px; border-top:1px dashed rgba(15,23,42,.18);}
371
+ .toggle{margin-left:8px; border:1px solid rgba(15,23,42,.18); background:#fff; color:var(--ink); padding:3px 8px; border-radius:999px; font-weight:900; font-size:12px; cursor:pointer; transition:background 0.2s;}
372
+ .toggle:hover{background:#f0f0f0;}
373
+
374
+ body.pdf-mode .toggle{display:none;}
375
+ body.pdf-mode .more{display:block;}
376
+
377
+ @media print{
378
+ body{background:#fff;}
379
+ .topbar{display:none;}
380
+ .wrap{max-width:100%; margin:0; padding:0;}
381
+ .paper{border-radius:0;}
382
+ .more{display:block !important;}
383
+ .toggle{display:none !important;}
384
+ .phase{break-inside:avoid; page-break-inside:avoid;}
385
+ }
386
+ </style>
387
+ </head>
388
+ <body>
389
+ <div class="topbar">
390
+ <div class="inner">
391
+ <div class="brand">
392
+ <div class="mark" aria-hidden="true"></div>
393
+ <div>
394
+ <b>Harmonious Wellness</b>
395
+ <div style="font-size:12px; color:#cbd5e1; font-weight:700;">Protocol Report</div>
396
+ </div>
397
+ </div>
398
+ <div class="actions">
399
+ <button id="pdfBtn" aria-pressed="false">📄 PDF Mode</button>
400
+ <button class="primary" onclick="window.print()">🖨️ Print / Save</button>
401
+ </div>
402
+ </div>
403
+ </div>
404
+
405
+ <main class="wrap">
406
+ <article class="paper" id="report">
407
+ <section class="hero">
408
+ <h1>${escapeHtml(reportData.protocol_name)}</h1>
409
+ ${reportData.protocol_headline ? `<div class="sub">${escapeHtml(reportData.protocol_headline)}</div>` : ''}
410
+ <div class="meta">
411
+ ${reportData.name ? renderPill('Client', reportData.name) : ''}
412
+ ${reportData.generated_on ? renderPill('Generated', reportData.generated_on) : ''}
413
+ ${reportData.email ? renderPill('Email', reportData.email) : ''}
414
+ ${reportData.phone_number ? renderPill('Phone', reportData.phone_number) : ''}
415
+ </div>
416
+ </section>
417
+
418
+ <section class="section">
419
+ <h2>Intentions and Goals</h2>
420
+ <div class="grid two">
421
+ <div class="card">
422
+ <h3>Conditions of focus</h3>
423
+ <div class="muted">${reportData.protocol_conditions.length > 0 ? escapeHtml(reportData.protocol_conditions.join(', ')) : 'None specified'}</div>
424
+ </div>
425
+ <div class="card">
426
+ <h3>Main Glands, Organs, or Systems Involved</h3>
427
+ <div class="muted">${reportData.client_symptoms || 'Not specified'}</div>
428
+ </div>
429
+ </div>
430
+
431
+ <div class="grid two" style="margin-top:10px">
432
+ <div class="card">
433
+ <h3>Top Client Concerns</h3>
434
+ ${reportData.main_concerns.length > 0 ? `<ul>${reportData.main_concerns.map(c => `<li>${escapeHtml(c)}</li>`).join('')}</ul>` : '<div class="muted">Not specified</div>'}
435
+ </div>
436
+ <div class="card">
437
+ <h3>Other Key Notes from Consult</h3>
438
+ ${reportData.top_outcomes.length > 0 ? `<ul>${reportData.top_outcomes.map(o => `<li>${escapeHtml(o)}</li>`).join('')}</ul>` : '<div class="muted">Not specified</div>'}
439
+ </div>
440
+ </div>
441
+
442
+ <div class="grid two" style="margin-top:10px">
443
+ <div class="card">
444
+ <h3>Kidney Filtration</h3>
445
+ <div class="muted">Urine pH: ${reportData.urine_ph || 'N/A'}</div>
446
+ <div class="muted">Status: ${reportData.urine_status || 'N/A'}</div>
447
+ </div>
448
+ <div class="card">
449
+ <h3>Analysis</h3>
450
+ ${reportData.blood_pressure_analysis_headline ? `<div class="muted">${escapeHtml(reportData.blood_pressure_analysis_headline)}</div>` : ''}
451
+ ${reportData.urine_analysis_headline ? `<div class="muted" style="margin-top:8px">${escapeHtml(reportData.urine_analysis_headline)}</div>` : ''}
452
+ </div>
453
+ </div>
454
+ </section>
455
+ <section class="section">
456
+
457
+ <h2>Protocol Overview</h2>
458
+ <div class="card">
459
+ <h2>${protocol.info}</h2>
460
+ ${protocol.description}
461
+
462
+
463
+ </div>
464
+ </section>
465
+ ${phases.length > 0 ? `
466
+ <section class="section">
467
+ <h2>Protocol Phases & Recommendations</h2>
468
+ ${phases.map((phase, i) => renderPhase(phase, i)).join('')}
469
+ </section>
470
+ ` : ''}
471
+
472
+ ${allRecommendations.length > 0 ? `
473
+ <section class="section" id="recommendations">
474
+ <h2>Recommendations</h2>
475
+ ${Object.keys(recommendationsByDomain).map(function(domain) {
476
+ const recs = recommendationsByDomain[domain];
477
+ if (recs.length === 0) return '';
478
+ const domainLabel = domain.charAt(0).toUpperCase() + domain.slice(1);
479
+ return `
480
+ <div style="margin-bottom:20px">
481
+ <h3 style="margin:0 0 12px; font-size:14px; color:var(--plum); text-transform:capitalize;">${domainLabel}</h3>
482
+ <div style="display:grid; gap:10px;">
483
+ ${recs.map(renderFullRecommendation).join('')}
484
+ </div>
485
+ </div>
486
+ `;
487
+ }).filter(Boolean).join('')}
488
+ </section>
489
+ ` : ''}
490
+
491
+ <section class="section">
492
+ <h2>Education & Resources</h2>
493
+ ${reportData.condition_summaries.length > 0 ? `
494
+ <div class="card">
495
+ <h3>Key Conditions We're Supporting</h3>
496
+ ${reportData.condition_summaries.map(c => `
497
+ <div style="margin-top:10px">
498
+ <div style="font-weight:900; color:var(--plum)">${escapeHtml(c.name)}</div>
499
+ ${c.description ? `<div class="muted">${escapeHtml(c.description)}</div>` : ''}
500
+ </div>
501
+ `).join('')}
502
+ </div>
503
+ ` : ''}
504
+ <div class="card" style="margin-top:10px">
505
+ <h3>Products Referenced in Protocol</h3>
506
+ <div class="muted">List and links for products referenced in this protocol will be emailed separately.</div>
507
+ </div>
508
+ </section>
509
+
510
+ <section class="section">
511
+ <h2>Closing Note</h2>
512
+ <div class="muted">
513
+ ${reportData.name ? escapeHtml(reportData.name.split(' ')[0]) : 'Client'}, This is a ton of information and oftentimes it takes some digestion and processing. If you would like to book a follow up appointment or just hop on the phone to discuss any of this please let me know. I am here to support you! I also make tailor made tinctures and teas so feel free to let me know if that is of interest. Deep blessings on the next step in your health journey! I am available by email or text if you have questions along the way. Please never hesitate to reach out! I would love to at least connect back in 3 months to hear how things are going and recommend adjustments/modifications as needed. You can book your follow-up appointment online here. Select either a Short Follow up or Long Follow up. There is a small fee for these follow ups. However, accessibility is a value of mine and I will never turn anyone away for lack of funds. Please let me know and I will share my sliding scale range. Thank you ${reportData.name ? escapeHtml(reportData.name) : 'you'} for trusting and allowing me to support you. The work works. Keep the faith. Live inspired. Blessings on your wellness journey and enhanced vitality! ~ Kelly Shay
514
+ </div>
515
+ </section>
516
+ </article>
517
+ </main>
518
+
519
+ <script>
520
+ (function() {
521
+ // PDF Mode toggle
522
+ const pdfBtn = document.getElementById('pdfBtn');
523
+ if (pdfBtn) {
524
+ pdfBtn.addEventListener('click', function() {
525
+ const enabled = !document.body.classList.contains('pdf-mode');
526
+ document.body.classList.toggle('pdf-mode', enabled);
527
+ pdfBtn.setAttribute('aria-pressed', String(enabled));
528
+ });
529
+ }
530
+
531
+ // Expand/collapse recommendations (only in recommendations section)
532
+ document.addEventListener('click', function(e) {
533
+ const toggle = e.target.closest('[data-toggle="more"]');
534
+ if (!toggle || document.body.classList.contains('pdf-mode')) return;
535
+
536
+ const rec = toggle.closest('.rec');
537
+ if (!rec) return;
538
+
539
+ // Only expand if in recommendations section
540
+ if (!rec.closest('#recommendations')) return;
541
+
542
+ const more = rec.querySelector('.more');
543
+ if (!more) return;
544
+
545
+ const expanded = toggle.getAttribute('aria-expanded') === 'true';
546
+ toggle.setAttribute('aria-expanded', String(!expanded));
547
+ toggle.textContent = expanded ? '+' : '−';
548
+ more.style.display = expanded ? 'none' : 'block';
549
+ });
550
+ })();
551
+ </script>
552
+ </body>
553
+ </html>
554
+ `;
555
+
556
+ // Use renderHTMLFramework if available, otherwise return raw HTML
557
+ if (options && options.renderHTMLFramework) {
558
+ return options.renderHTMLFramework(data.REPORT, protocol, html);
559
+ }
560
+
561
+ return html;
562
+ };
563
+