UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

479 lines (430 loc) 20.6 kB
<!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 (Mockup)</title> <style> :root{ /* Brand-inspired (lightweight) */ --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 */ .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;} button.primary{border-color:rgba(198,164,77,.55); background:rgba(198,164,77,.18); color:#fff7e6;} /* Page */ .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); font-weight:650; line-height:1.55;} ul{margin:0; padding-left:18px; color:var(--muted); font-weight:650;} li{margin:6px 0;} /* Phases */ .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;} .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;} /* PDF Mode */ body.pdf-mode .toggle{display:none;} body.pdf-mode .more{display:block;} /* Print */ @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 (Mockup)</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"></article> </main> <script> /* PROTOCOL REPORT TEMPLATE - Uses data from PROTOCOL (passed via reportbuilder) - Resolves client, recommendations, and conditions via $J.get() - Lightweight styling + small JS to avoid performance issues - Web mode: expandable long descriptions - PDF/Print: single-column, expanded */ // --------------------------- // DATA TRANSFORMATION // --------------------------- const protocol = typeof PROTOCOL !== 'undefined' ? PROTOCOL : null; function getRef(refId) { if (!refId) return null; try { if (typeof $J !== 'undefined' && $J.get) { 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 buildData() { const data = { protocol_name: '', protocol_headline: '', name: '', email: '', phone_number: '', generated_on: '', protocol_conditions: [], main_concerns: [], top_outcomes: [], daily_diet_breakfast_lunch_dinner_snacks: '', urine_ph: '', urine_status: '', sleep_quality: '', daily_energy_level: '', current_supplements: '', current_medications: '', client_symptoms: '', phase_1_headline: '', phase_1_description: '', phase_1_recommendations: [], phase_2_headline: '', phase_2_description: '', phase_2_recommendations: [], phase_3_headline: '', phase_3_description: '', phase_3_recommendations: [], phase_4_headline: '', phase_4_description: '', phase_4_recommendations: [], phase_5_headline: '', phase_5_description: '', phase_5_recommendations: [], blood_pressure_analysis_headline: '', urine_analysis_headline: '', condition_summaries: [] }; if (!protocol) { console.warn('No protocol data available'); return data; } const client = protocol.client ? getRef(protocol.client) : null; data.protocol_name = protocol.name || ''; data.protocol_headline = protocol.info || ''; if (client) { data.name = client.name || ''; data.email = client.email || ''; data.phone_number = client.phone || ''; if (client.main_concerns) { const concernsText = extractText(client.main_concerns); data.main_concerns = concernsText ? concernsText.split(/\n|•|·|-/).map(s => s.trim()).filter(Boolean) : []; } if (client.top_outcomes) { const outcomesText = extractText(client.top_outcomes); data.top_outcomes = outcomesText ? outcomesText.split(/\n|•|·|-/).map(s => s.trim()).filter(Boolean) : []; } if (client.client_symptoms) { data.client_symptoms = extractText(client.client_symptoms); } } if (protocol.joeUpdated || protocol.created) { const date = new Date(protocol.joeUpdated || protocol.created); data.generated_on = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); } if (protocol.conditions && Array.isArray(protocol.conditions)) { data.protocol_conditions = protocol.conditions.map(function(condId) { const cond = getRef(condId); return cond ? cond.name : null; }).filter(Boolean); } if (protocol.urine_analysis_ph) { data.urine_ph = String(protocol.urine_analysis_ph); } if (protocol.urine_status) { data.urine_status = protocol.urine_status; } if (protocol.blood_pressure_analysis_headline) { data.blood_pressure_analysis_headline = protocol.blood_pressure_analysis_headline; } if (protocol.urine_analysis_headline) { data.urine_analysis_headline = protocol.urine_analysis_headline; } for (let i = 1; i <= 5; i++) { const phaseHeadline = protocol[`phase_${i}_headline`] || ''; const phaseDesc = protocol[`phase_${i}_description`] ? extractText(protocol[`phase_${i}_description`]) : ''; const phaseRecIds = protocol[`phase_${i}_recommendations`] || []; data[`phase_${i}_headline`] = phaseHeadline; data[`phase_${i}_description`] = phaseDesc; data[`phase_${i}_recommendations`] = phaseRecIds.map(function(recId) { const rec = getRef(recId); if (!rec) return null; return { recommendation_name: rec.name || '', short_description: rec.info || '', long_description: extractText(rec.description) || '', standard_dosage: rec.standard_dosage || '' }; }).filter(Boolean); } if (protocol.conditions && Array.isArray(protocol.conditions)) { data.condition_summaries = protocol.conditions.map(function(condId) { const cond = getRef(condId); if (!cond) return null; return { protocol_conditions: cond.name || '', long_description: extractText(cond.long_description || cond.description || '') || '' }; }).filter(Boolean); } return data; } const data = buildData(); // --------------------------- // Render helpers // --------------------------- const el = (tag, attrs={}, children=[]) => { const n = document.createElement(tag); Object.entries(attrs).forEach(([k,v]) => { if(k === 'class') n.className = v; else n.setAttribute(k, v); }); (Array.isArray(children) ? children : [children]).forEach(c => { if(c === null || c === undefined) return; if(typeof c === 'string') n.appendChild(document.createTextNode(c)); else n.appendChild(c); }); return n; }; const pill = (label, value) => el('span', {class:'pill'}, [el('small', {}, label), ' ' + value]); function recCard(rec){ const more = el('div', {class:'more'}, [el('div', {class:'muted'}, rec.long_description || '')]); const toggle = el('button', {class:'toggle', type:'button', 'data-toggle':'more', 'aria-expanded':'false'}, '+'); return el('div', {class:'rec'}, [ el('div', {class:'rec-top'}, [ el('div', {}, [ el('div', {class:'rec-name'}, rec.recommendation_name), el('div', {class:'muted', style:'margin-top:6px'}, [rec.short_description, ' ', toggle]) ]), el('div', {class:'dose'}, rec.standard_dosage || '') ]), more ]); } function phase(n){ const headline = data[`phase_${n}_headline`]; const desc = data[`phase_${n}_description`]; const recs = data[`phase_${n}_recommendations`] || []; return el('div', {class:'phase'}, [ el('div', {class:'head'}, [ el('div', {}, [ el('b', {}, headline), el('p', {class:'muted'}, desc) ]) ]), el('div', {class:'recs'}, recs.map(recCard)) ]); } // --------------------------- // Render page (with DOM ready check) // --------------------------- function initReport() { const report = document.getElementById('report'); if (!report) { console.error('Report element not found'); return; } report.appendChild(el('section', {class:'hero'}, [ el('h1', {}, data.protocol_name), el('div', {class:'sub'}, data.protocol_headline), el('div', {class:'meta'}, [ pill('Client', data.name), pill('Generated', data.generated_on), pill('Email', data.email), pill('Phone', data.phone_number) ]) ])); // Intentions and Goals report.appendChild(el('section', {class:'section'}, [ el('h2', {}, 'Intentions and Goals'), el('div', {class:'grid two'}, [ el('div', {class:'card'}, [ el('h3', {}, 'Conditions of focus'), el('div', {class:'muted'}, data.protocol_conditions.join(', ')), el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: protocol_conditions') ]), el('div', {class:'card'}, [ el('h3', {}, 'Main Glands, Organs, or Systems Involved'), el('div', {class:'muted'}, data.client_symptoms), el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: client_symptoms') ]) ]), el('div', {class:'grid two', style:'margin-top:10px'}, [ el('div', {class:'card'}, [ el('h3', {}, 'Top Client Concerns'), el('ul', {}, data.main_concerns.map(x => el('li', {}, x))), el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: main_concerns') ]), el('div', {class:'card'}, [ el('h3', {}, 'Other Key Notes from Consult'), el('ul', {}, data.top_outcomes.map(x => el('li', {}, x))), el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: top_outcomes') ]) ]), el('div', {class:'grid two', style:'margin-top:10px'}, [ el('div', {class:'card'}, [ el('h3', {}, 'Current Diet'), el('div', {class:'muted'}, data.daily_diet_breakfast_lunch_dinner_snacks || 'Not specified'), el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: daily_diet_breakfast_lunch_dinner_snacks') ]), el('div', {class:'card'}, [ el('h3', {}, 'Kidney Filtration / Sleep / Energy'), el('div', {class:'muted'}, `urine_ph: ${data.urine_ph || 'N/A'} · urine_status: ${data.urine_status || 'N/A'}`), el('div', {class:'muted'}, `sleep_quality: ${data.sleep_quality || 'N/A'} · daily_energy_level: ${data.daily_energy_level || 'N/A'}`), el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped Fields: urine_ph, urine_status, sleep_quality, daily_energy_level') ]) ]), el('div', {class:'grid two', style:'margin-top:10px'}, [ el('div', {class:'card'}, [ el('h3', {}, 'Current Supplements'), el('div', {class:'muted'}, data.current_supplements || 'Not specified'), el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: current_supplements') ]), el('div', {class:'card'}, [ el('h3', {}, 'Current Medications'), el('div', {class:'muted'}, data.current_medications || 'Not specified'), el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: current_medications') ]) ]) ])); // Phases & Recommendations report.appendChild(el('section', {class:'section'}, [ el('h2', {}, 'Protocol Phases & Recommendations'), phase(1), phase(2), phase(3), phase(4), phase(5) ])); // Education & Resources report.appendChild(el('section', {class:'section'}, [ el('h2', {}, 'Education & Resources'), el('div', {class:'card'}, [ el('h3', {}, 'Understanding Your Body Signals'), el('div', {class:'muted'}, `blood_pressure_analysis_headline: ${data.blood_pressure_analysis_headline || 'N/A'}`), el('div', {class:'muted', style:'margin-top:8px'}, `urine_analysis_headline: ${data.urine_analysis_headline || 'N/A'}`) ]), el('div', {class:'card', style:'margin-top:10px'}, [ el('h3', {}, "Key Conditions We’re Supporting"), ...data.condition_summaries.map(c => el('div', {style:'margin-top:10px'}, [ el('div', {style:'font-weight:900; color:var(--plum)'}, c.protocol_conditions), el('div', {class:'muted'}, c.long_description) ])) ]), el('div', {class:'card', style:'margin-top:10px'}, [ el('h3', {}, 'Products Referenced in 20-Week Protocol'), el('div', {class:'muted'}, 'List and links for products referenced in this protocol will be emailed separately.') ]) ])); // Closing Note (static rendered content) report.appendChild(el('section', {class:'section'}, [ el('h2', {}, 'Closing Note'), el('div', {class:'muted'}, `${data.name ? data.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 ${data.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` ) ])); // --------------------------- // Interactions // --------------------------- const pdfBtn = document.getElementById('pdfBtn'); if (pdfBtn) { pdfBtn.addEventListener('click', () => { const enabled = !document.body.classList.contains('pdf-mode'); document.body.classList.toggle('pdf-mode', enabled); pdfBtn.setAttribute('aria-pressed', String(enabled)); }); } document.addEventListener('click', (e) => { const t = e.target.closest('[data-toggle="more"]'); if(!t) return; if(document.body.classList.contains('pdf-mode')) return; const rec = t.closest('.rec'); if (!rec) return; const more = rec.querySelector('.more'); if (!more) return; const expanded = t.getAttribute('aria-expanded') === 'true'; t.setAttribute('aria-expanded', String(!expanded)); more.style.display = expanded ? 'none' : 'block'; }); } // Wait for DOM to be ready before initializing if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initReport); } else { // DOM already ready, but wait a tick to ensure elements exist setTimeout(initReport, 0); } </script> </body> </html>