UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

563 lines (504 loc) 24.4 kB
module.exports = function(data, options) { const protocol = data.protocol || data.ITEM; if (!protocol) { return '<div style="padding: 20px; color: #dc3545;">Error: No protocol data available</div>'; } // Helper functions function getRef(refId) { if (!refId) return null; try { return $J.get(refId); } catch(e) { console.error('Error getting reference:', refId, e); return null; } } function extractText(rendering) { if (!rendering) return ''; if (typeof rendering === 'string') { return rendering.replace(/<[^>]*>/g, '').trim(); } return String(rendering); } function preserveHtml(html) { if (!html) return ''; if (typeof html === 'string') { return html.trim(); } return String(html); } function escapeHtml(text) { if (!text) return ''; return String(text) .replace(/&/g, '&' + 'amp;') .replace(/</g, '&' + 'lt;') .replace(/>/g, '&' + 'gt;') .replace(/"/g, '&' + 'quot;') .replace(/'/g, '&' + '#039;'); } function formatDate(dateString) { if (!dateString) return ''; try { const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); } catch(e) { return ''; } } function splitList(text) { if (!text) return []; return text.split(/\n|•|·|-/).map(s => s.trim()).filter(Boolean); } // Get client (handle both array and string formats) let clientId = protocol.client; if (Array.isArray(clientId)) { clientId = clientId.length > 0 ? clientId[0] : null; } const client = clientId ? getRef(clientId) : null; // Build report data const reportData = { protocol_name: protocol.name || '', protocol_headline: protocol.info || '', name: client ? (client.name || '') : '', email: client ? (client.email || '') : '', phone_number: client ? (client.phone || '') : '', generated_on: formatDate(protocol.joeUpdated || protocol.created), protocol_conditions: [], main_concerns: [], top_outcomes: [], client_symptoms: '', urine_ph: protocol.urine_analysis_ph ? String(protocol.urine_analysis_ph) : '', urine_status: protocol.urine_status || '', blood_pressure_analysis_headline: protocol.blood_pressure_analysis_headline || '', urine_analysis_headline: protocol.urine_analysis_headline || '', condition_summaries: [] }; // Client data if (client) { if (client.main_concerns) { reportData.main_concerns = splitList(extractText(client.main_concerns)); } if (client.top_outcomes) { reportData.top_outcomes = splitList(extractText(client.top_outcomes)); } if (client.client_symptoms) { reportData.client_symptoms = extractText(client.client_symptoms); } } // Protocol conditions if (protocol.conditions && Array.isArray(protocol.conditions)) { reportData.protocol_conditions = protocol.conditions.map(function(condId) { const cond = getRef(condId); return cond ? cond.name : null; }).filter(Boolean); } // Build phases const phases = []; for (let i = 1; i <= 5; i++) { const phaseHeadline = protocol[`phase_${i}_headline`] || ''; const phaseDesc = protocol[`phase_${i}_description`] ? preserveHtml(protocol[`phase_${i}_description`]) : ''; const phaseRecIds = protocol[`phase_${i}_recommendations`] || []; const phaseOverrides = protocol[`phase_${i}_overrides`] || []; // Handle both array and single value formats const recIds = Array.isArray(phaseRecIds) ? phaseRecIds : (phaseRecIds ? [phaseRecIds] : []); // Build override lookup map - match rec _id on recToOverride const overrideMap = {}; if (Array.isArray(phaseOverrides)) { phaseOverrides.forEach(function(override) { const recIdField = `phase_${i}_recToOverride`; const dosageField = `phase_${i}_dosage`; const instructionsField = `phase_${i}_instructions`; const recId = override[recIdField]; if (recId) { overrideMap[recId] = { dosage: override[dosageField] || '', instructions: override[instructionsField] || '' }; } }); } const recommendations = recIds.map(function(recId) { if (!recId || typeof recId !== 'string') return null; try { const rec = getRef(recId); if (!rec || !rec.name) return null; const override = overrideMap[recId] || {}; return { _id: recId, name: rec.name || '', short_description: rec.info || '', long_description: extractText(rec.description) || '', dosage: override.dosage || rec.standard_dosage || '', overrideInstructions: override.instructions || '', domain: rec.recommendation_domain || 'product' }; } catch(e) { console.error('Error processing recommendation:', recId, e); return null; } }).filter(Boolean); // Always add phase if it has headline or description, even without recommendations if (phaseHeadline || phaseDesc) { phases.push({ headline: phaseHeadline, description: phaseDesc, recommendations: recommendations }); } } // Collect all unique recommendations from all phases const allRecommendationsMap = new Map(); phases.forEach(function(phase) { phase.recommendations.forEach(function(rec) { if (rec._id && !allRecommendationsMap.has(rec._id)) { allRecommendationsMap.set(rec._id, rec); } }); }); // Group recommendations by domain const allRecommendations = Array.from(allRecommendationsMap.values()); const recommendationsByDomain = { product: [], dietary: [], activity: [] }; allRecommendations.forEach(function(rec) { const domain = rec.domain || 'product'; if (recommendationsByDomain[domain]) { recommendationsByDomain[domain].push(rec); } else { recommendationsByDomain.product.push(rec); // Fallback } }); // Condition summaries if (protocol.conditions && Array.isArray(protocol.conditions)) { reportData.condition_summaries = protocol.conditions.map(function(condId) { const cond = getRef(condId); if (!cond) return null; return { name: cond.name || '', description: extractText(cond.long_description || cond.description || '') }; }).filter(Boolean); } // HTML generation helpers function renderPill(label, value) { if (!value) return ''; return `<span class="pill"><small>${escapeHtml(label)}</small> ${escapeHtml(value)}</span>`; } function renderPhaseRecommendationLink(rec) { const recId = rec._id || rec.name.toLowerCase().replace(/\s+/g, '-'); const dosageHtml = rec.dosage ? `<div class="dose">${escapeHtml(rec.dosage)}</div>` : ''; const instructionsHtml = rec.overrideInstructions ? `<div style="font-size: 11px; color: #666; margin-top: 6px; font-style: italic; padding-left: 4px;">${escapeHtml(rec.overrideInstructions)}</div>` : ''; return ` <div style="margin-bottom: 8px;"> <div style="display:flex; justify-content:space-between; gap:10px; align-items:flex-start;"> <div style="flex:1;"> <a href="#rec-${recId}" class="rec-link">${escapeHtml(rec.name)}</a> ${instructionsHtml} </div> ${dosageHtml} </div> </div> `; } function renderFullRecommendation(rec) { const recId = rec._id || rec.name.toLowerCase().replace(/\s+/g, '-'); return ` <div class="rec" id="rec-${recId}"> <div class="rec-top"> <div> <div class="rec-name">${escapeHtml(rec.name)}</div> ${rec.short_description ? `<div class="muted" style="margin-top:6px"> ${escapeHtml(rec.short_description)} <button class="toggle" type="button" data-toggle="more" aria-expanded="false">+</button> </div>` : ''} </div> ${rec.dosage ? `<div class="dose">${escapeHtml(rec.dosage)}</div>` : ''} </div> <div class="more"> ${rec.long_description ? `<div class="muted">${escapeHtml(rec.long_description)}</div>` : ''} </div> </div> `; } function renderPhase(phase, i) { if (!phase.headline && !phase.description && phase.recommendations.length === 0) { return ''; } var pmap = ["Phase 1: Stabilization", "Phase 2: Foundations", "Phase 3: Repair", "Phase 4: Optimization", "Phase 5: Integration & Maintenance"]; const phaseInstructions = protocol[`phase_${i + 1}_instructions`] || ''; const instructionsHtml = phaseInstructions ? preserveHtml(phaseInstructions) : ''; return ` <div class="phase"> <div class="head"> <div class="phase-label">${pmap[i]} ${phase.headline ? `<div><small><i>${escapeHtml(phase.headline)}</i></small></div>` : ''} </div> ${phase.description ? `<div class="muted">${phase.description}</div>` : ''} </div> ${phase.recommendations.length > 0 || instructionsHtml ? ` <div class="recs" style="display:grid; grid-template-columns: 1fr 1fr; gap: 16px; padding:12px;"> <div> <h4 style="margin:0 0 8px; font-size:13px; color:var(--plum);">Recommendations</h4> <div style="display:flex; flex-direction:column; gap:6px;"> ${phase.recommendations.map(rec => renderPhaseRecommendationLink(rec)).join('')} </div> </div> ${instructionsHtml ? ` <div> <h4 style="margin:0 0 8px; font-size:13px; color:var(--plum);">Instructions</h4> <div class="muted">${instructionsHtml}</div> </div> ` : '<div></div>'} </div> ` : ''} </div> `; } // Generate HTML const html = ` <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Harmonious Wellness — Protocol Report</title> <style> :root{ --plum:#3a1430; --gold:#c6a44d; --olive:#6c7b3b; --ink:#0f172a; --muted:#475569; --bg:#0b0810; --paper:#ffffff; --soft:#f8fafc; --line:#e5e7eb; --max: 980px; --radius: 16px; --font: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; } html,body{height:100%;} body{margin:0; font-family:var(--font); background:var(--bg); color:var(--ink);} .topbar{position:sticky; top:0; z-index:10; background:rgba(11,8,16,.92); border-bottom:1px solid rgba(255,255,255,.10);} .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;} .brand{display:flex; align-items:center; gap:10px; color:#f1f5f9;} .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);} .brand b{letter-spacing:.2px;} .actions{display:flex; gap:8px; flex-wrap:wrap;} 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;} button:hover{opacity:0.8;} button.primary{border-color:rgba(198,164,77,.55); background:rgba(198,164,77,.18); color:#fff7e6;} .wrap{max-width:var(--max); margin:18px auto 52px; padding:0 14px;} .paper{background:var(--paper); border-radius:22px; overflow:hidden; border:1px solid rgba(255,255,255,.06);} .hero{padding:18px; border-bottom:1px solid var(--line); background:linear-gradient(180deg,#fff,#fbfdff);} .hero h1{margin:0; font-size:22px; line-height:1.2;} .hero .sub{margin-top:6px; color:var(--muted); font-weight:700;} .meta{display:flex; gap:10px; flex-wrap:wrap; margin-top:12px;} .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;} .pill small{color:var(--muted); font-weight:800;} .section{padding:16px 18px; border-bottom:1px solid var(--line);} .section:last-child{border-bottom:none;} .section h2{margin:0 0 10px; font-size:16px;} .grid{display:grid; gap:10px;} .two{grid-template-columns: repeat(2, minmax(0,1fr));} @media (max-width:780px){.two{grid-template-columns:1fr;}} .card{border:1px solid var(--line); border-radius:var(--radius); padding:12px; background:#fff;} .card h3{margin:0 0 8px; font-size:13px; color:var(--plum);} .muted{color:var(--muted); line-height:1.55;} ul{margin:0; padding-left:18px; color:var(--muted); font-weight:650;} li{margin:6px 0;} .phase-label{font-weight:bold;} .phase{border:1px solid var(--line); border-radius:18px; overflow:hidden; margin-bottom:12px;} .phase .head{padding:12px; background:linear-gradient(180deg,#fff,#f9fbff); border-bottom:1px solid var(--line);} .phase .head b{color:var(--plum);} .phase .head p{margin:6px 0 0;} .phase .recs{padding:12px; display:grid; gap:10px;} .rec{border:1px solid var(--line); border-radius:14px; padding:10px;} .rec-top{display:flex; justify-content:space-between; gap:10px; align-items:flex-start;} .rec-name{font-weight:900;} .rec-link{color:var(--plum); text-decoration:none; font-weight:600; padding:4px 0; display:block; transition:opacity 0.2s;} .rec-link:hover{text-decoration:underline; opacity:0.8;} .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;} .more{display:none; margin-top:8px; padding-top:8px; border-top:1px dashed rgba(15,23,42,.18);} .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;} .toggle:hover{background:#f0f0f0;} body.pdf-mode .toggle{display:none;} body.pdf-mode .more{display:block;} @media print{ body{background:#fff;} .topbar{display:none;} .wrap{max-width:100%; margin:0; padding:0;} .paper{border-radius:0;} .more{display:block !important;} .toggle{display:none !important;} .phase{break-inside:avoid; page-break-inside:avoid;} } </style> </head> <body> <div class="topbar"> <div class="inner"> <div class="brand"> <div class="mark" aria-hidden="true"></div> <div> <b>Harmonious Wellness</b> <div style="font-size:12px; color:#cbd5e1; font-weight:700;">Protocol Report</div> </div> </div> <div class="actions"> <button id="pdfBtn" aria-pressed="false">📄 PDF Mode</button> <button class="primary" onclick="window.print()">🖨️ Print / Save</button> </div> </div> </div> <main class="wrap"> <article class="paper" id="report"> <section class="hero"> <h1>${escapeHtml(reportData.protocol_name)}</h1> ${reportData.protocol_headline ? `<div class="sub">${escapeHtml(reportData.protocol_headline)}</div>` : ''} <div class="meta"> ${reportData.name ? renderPill('Client', reportData.name) : ''} ${reportData.generated_on ? renderPill('Generated', reportData.generated_on) : ''} ${reportData.email ? renderPill('Email', reportData.email) : ''} ${reportData.phone_number ? renderPill('Phone', reportData.phone_number) : ''} </div> </section> <section class="section"> <h2>Intentions and Goals</h2> <div class="grid two"> <div class="card"> <h3>Conditions of focus</h3> <div class="muted">${reportData.protocol_conditions.length > 0 ? escapeHtml(reportData.protocol_conditions.join(', ')) : 'None specified'}</div> </div> <div class="card"> <h3>Main Glands, Organs, or Systems Involved</h3> <div class="muted">${reportData.client_symptoms || 'Not specified'}</div> </div> </div> <div class="grid two" style="margin-top:10px"> <div class="card"> <h3>Top Client Concerns</h3> ${reportData.main_concerns.length > 0 ? `<ul>${reportData.main_concerns.map(c => `<li>${escapeHtml(c)}</li>`).join('')}</ul>` : '<div class="muted">Not specified</div>'} </div> <div class="card"> <h3>Other Key Notes from Consult</h3> ${reportData.top_outcomes.length > 0 ? `<ul>${reportData.top_outcomes.map(o => `<li>${escapeHtml(o)}</li>`).join('')}</ul>` : '<div class="muted">Not specified</div>'} </div> </div> <div class="grid two" style="margin-top:10px"> <div class="card"> <h3>Kidney Filtration</h3> <div class="muted">Urine pH: ${reportData.urine_ph || 'N/A'}</div> <div class="muted">Status: ${reportData.urine_status || 'N/A'}</div> </div> <div class="card"> <h3>Analysis</h3> ${reportData.blood_pressure_analysis_headline ? `<div class="muted">${escapeHtml(reportData.blood_pressure_analysis_headline)}</div>` : ''} ${reportData.urine_analysis_headline ? `<div class="muted" style="margin-top:8px">${escapeHtml(reportData.urine_analysis_headline)}</div>` : ''} </div> </div> </section> <section class="section"> <h2>Protocol Overview</h2> <div class="card"> <h2>${protocol.info}</h2> ${protocol.description} </div> </section> ${phases.length > 0 ? ` <section class="section"> <h2>Protocol Phases & Recommendations</h2> ${phases.map((phase, i) => renderPhase(phase, i)).join('')} </section> ` : ''} ${allRecommendations.length > 0 ? ` <section class="section" id="recommendations"> <h2>Recommendations</h2> ${Object.keys(recommendationsByDomain).map(function(domain) { const recs = recommendationsByDomain[domain]; if (recs.length === 0) return ''; const domainLabel = domain.charAt(0).toUpperCase() + domain.slice(1); return ` <div style="margin-bottom:20px"> <h3 style="margin:0 0 12px; font-size:14px; color:var(--plum); text-transform:capitalize;">${domainLabel}</h3> <div style="display:grid; gap:10px;"> ${recs.map(renderFullRecommendation).join('')} </div> </div> `; }).filter(Boolean).join('')} </section> ` : ''} <section class="section"> <h2>Education & Resources</h2> ${reportData.condition_summaries.length > 0 ? ` <div class="card"> <h3>Key Conditions We're Supporting</h3> ${reportData.condition_summaries.map(c => ` <div style="margin-top:10px"> <div style="font-weight:900; color:var(--plum)">${escapeHtml(c.name)}</div> ${c.description ? `<div class="muted">${escapeHtml(c.description)}</div>` : ''} </div> `).join('')} </div> ` : ''} <div class="card" style="margin-top:10px"> <h3>Products Referenced in Protocol</h3> <div class="muted">List and links for products referenced in this protocol will be emailed separately.</div> </div> </section> <section class="section"> <h2>Closing Note</h2> <div class="muted"> ${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 </div> </section> </article> </main> <script> (function() { // PDF Mode toggle const pdfBtn = document.getElementById('pdfBtn'); if (pdfBtn) { pdfBtn.addEventListener('click', function() { const enabled = !document.body.classList.contains('pdf-mode'); document.body.classList.toggle('pdf-mode', enabled); pdfBtn.setAttribute('aria-pressed', String(enabled)); }); } // Expand/collapse recommendations (only in recommendations section) document.addEventListener('click', function(e) { const toggle = e.target.closest('[data-toggle="more"]'); if (!toggle || document.body.classList.contains('pdf-mode')) return; const rec = toggle.closest('.rec'); if (!rec) return; // Only expand if in recommendations section if (!rec.closest('#recommendations')) return; const more = rec.querySelector('.more'); if (!more) return; const expanded = toggle.getAttribute('aria-expanded') === 'true'; toggle.setAttribute('aria-expanded', String(!expanded)); toggle.textContent = expanded ? '+' : '−'; more.style.display = expanded ? 'none' : 'block'; }); })(); </script> </body> </html> `; // Use renderHTMLFramework if available, otherwise return raw HTML if (options && options.renderHTMLFramework) { return options.renderHTMLFramework(data.REPORT, protocol, html); } return html; };