|
173 | 173 | .res-console.copied{background:var(--teal-bg);color:var(--teal)} |
174 | 174 | .res-preview{display:block;margin-top:.3rem;padding:.5rem .65rem;background:var(--bg);border:1px solid var(--line);border-radius:4px;font-family:var(--mono);font-size:0.6rem;line-height:1.45;color:var(--fg2);max-height:320px;overflow:auto;white-space:pre-wrap} |
175 | 175 | .cte-tag{display:inline-block;margin-left:.4rem;padding:.05rem .35rem;background:var(--bg3);color:var(--fg3);font-family:var(--mono);font-size:0.55rem;border-radius:3px;letter-spacing:.04em;vertical-align:middle} |
| 176 | +.p360-everything-wrap{margin:.5rem 0 .8rem} |
| 177 | +.p360-everything-mount{margin-top:.4rem;padding:.5rem .65rem;background:var(--card);border:1px solid var(--line);border-radius:6px;font-size:0.7rem} |
| 178 | +.evt-group{margin:.2rem 0} |
| 179 | +.evt-head{cursor:pointer;padding:.18rem .4rem;background:var(--bg2);border-radius:4px;display:inline-block;color:var(--fg2);font-size:0.66rem;user-select:none;font-weight:500} |
| 180 | +.evt-head:hover{background:var(--bg3);color:var(--fg)} |
| 181 | +.evt-head .evt-count{font-family:var(--mono);font-size:0.55rem;color:var(--fg3);margin-left:.3rem;font-weight:400} |
| 182 | +.evt-body{padding:.25rem 0 .25rem .85rem;border-left:1px solid var(--bg3);margin-left:.4rem;margin-top:.2rem} |
| 183 | +.evt-row{padding:.08rem 0;font-size:0.64rem;color:var(--fg2);line-height:1.55} |
| 184 | +.evt-row .res-link{font-family:var(--mono);font-size:0.6rem} |
| 185 | +.evt-summary{color:var(--fg3);margin-left:.2rem} |
176 | 186 |
|
177 | 187 | /* ── Care gap specific ─────────────── */ |
178 | 188 | .cat-badge{font-size:0.5rem;font-weight:600;padding:.08rem .3rem;border-radius:3px;display:inline-block;margin-right:.15rem} |
|
613 | 623 | const name=patientLabel(pid);const open=measures.filter(m=>m.status==='open');const closed=measures.filter(m=>m.status==='closed'); |
614 | 624 | const initials=name.split(' ').map(w=>w[0]).join('').substring(0,2).toUpperCase(); |
615 | 625 | const order={open:0,closed:1,excl:2,noip:3,error:9};const sorted=[...measures].sort((a,b)=>(order[a.status]??9)-(order[b.status]??9)); |
616 | | - let html=`<button class="p360-back" onclick="selectedP360=null;renderP360List()">← All Patients</button><div class="p360-header"><div class="p360-avatar">${initials}</div><div class="p360-info"><h2>${name}</h2><div class="p360-meta">${pid}</div><div class="p360-summary"><span class="p360-chip total">Age ${patientAge(pid)}</span>${open.length>0?`<span class="p360-chip open">${open.length} open</span>`:''}<span class="p360-chip closed">${closed.length} closed</span><span class="p360-chip total">${measures.length} measures</span></div></div></div><div class="p360-list">`; |
| 626 | + let html=`<button class="p360-back" onclick="selectedP360=null;renderP360List()">← All Patients</button><div class="p360-header"><div class="p360-avatar">${initials}</div><div class="p360-info"><h2>${name}</h2><div class="p360-meta">${pid}</div><div class="p360-summary"><span class="p360-chip total">Age ${patientAge(pid)}</span>${open.length>0?`<span class="p360-chip open">${open.length} open</span>`:''}<span class="p360-chip closed">${closed.length} closed</span><span class="p360-chip total">${measures.length} measures</span></div></div></div><div class="p360-everything-wrap"><button class="p360-ev-toggle" onclick="loadEverything('${pid}',this,'p360-ever-${pid}')">▸ Patient $everything</button><div id="p360-ever-${pid}" class="p360-everything-mount" style="display:none"></div></div><div class="p360-list">`; |
617 | 627 | for(let i=0;i<sorted.length;i++){const m=sorted[i];const meta=META[m.mid]||{short:m.mid,name:m.mid};const act=ACTIONS[m.mid]||{action:'',category:''};const evId='p360-ev-'+m.mid; |
618 | 628 | html+=`<div class="p360-card card-${m.status}" style="animation-delay:${i*35}ms"><div class="p360-card-head"><div class="p360-card-title">${meta.short} — ${meta.name}</div><span class="badge ${m.cls}">${m.label}</span></div>${m.status==='open'?`<div class="p360-action-box"><div class="act-label"><span class="cat-badge cat-${act.category}">${act.category}</span> Recommended Action</div><div class="act-text">${act.action}</div></div>`:''}<button class="p360-ev-toggle" onclick="event.stopPropagation();toggleP360Ev('${m.mid}','${pid}','${evId}',this)">▸ Evidence</button><div id="${evId}"></div></div>`} |
619 | 629 | html+='</div>';document.getElementById('main').innerHTML=html; |
|
666 | 676 | html+='<div class="evidence-box"><div class="evidence-box-title">Evidence</div>'; |
667 | 677 | const shown=new Set(); |
668 | 678 | for(const r of ipItems){ |
669 | | - const rt=r.ip_resource_type||'Encounter',rid=r.ip_resource_id||'',src=r.source_cte||r.ip_pathway||r.pathway||''; |
| 679 | + const rt=r.ip_resource_type||'Encounter',rid=r.ip_resource_id||'',src=r.ip_source_cte||r.ip_pathway||''; |
670 | 680 | const key=rid?`${rt}/${rid}|${src}`:`ip|${r.ip_code_display}|${r.ip_event_date}|${src}`; |
671 | 681 | if(shown.has(key))continue;shown.add(key); |
672 | 682 | const date=r.ip_event_date?' — '+r.ip_event_date.substring(0,10):''; |
|
680 | 690 | } |
681 | 691 | for(const r of numItems){ |
682 | 692 | if(r.code_display){ |
683 | | - const rt=r.resource_type||'Resource',rid=r.resource_id||'',src=r.source_cte||r.pathway||''; |
| 693 | + const rt=r.resource_type||'Resource',rid=r.resource_id||'',src=(r.source_cte&&r.source_cte!=='none'?r.source_cte:'')||(r.pathway&&r.pathway!=='none'?r.pathway:'')||''; |
684 | 694 | const key=rid?`${rt}/${rid}|${src}`:`num|${r.code_display}|${r.event_date}|${src}`; |
685 | 695 | if(shown.has(key))continue;shown.add(key); |
686 | 696 | const date=r.event_date?' — '+r.event_date.substring(0,10):''; |
|
693 | 703 | html+=`<div class="evidence-box-item"><strong>${escapeHtml(rt)}:</strong> ${escapeHtml(r.code_display)}${codeStr}${date}${tag}</div>`; |
694 | 704 | } |
695 | 705 | }else if(r.exclusion_pathway){ |
696 | | - const src=r.source_cte||r.pathway||''; |
697 | | - const key=`exc|${r.exclusion_pathway}|${src}`; |
| 706 | + // Exception row: body already shows the pathway, no separate cte-tag needed. |
| 707 | + const key=`exc|${r.exclusion_pathway}`; |
698 | 708 | if(shown.has(key))continue;shown.add(key); |
699 | | - const tag=src?` <span class="cte-tag">${escapeHtml(src)}</span>`:''; |
700 | | - html+=`<div class="evidence-box-item"><strong>Exception:</strong> ${escapeHtml(r.exclusion_pathway)}${tag}</div>`; |
| 709 | + html+=`<div class="evidence-box-item"><strong>Exception:</strong> ${escapeHtml(r.exclusion_pathway)}</div>`; |
701 | 710 | } |
702 | 711 | } |
703 | 712 | html+='</div>'; |
|
743 | 752 | return false; |
744 | 753 | } |
745 | 754 |
|
| 755 | +// ── Patient $everything tree ──────────────── |
| 756 | +const everythingCache={}; |
| 757 | +const resourceStash=new Map(); |
| 758 | + |
| 759 | +async function loadEverything(pid,btn,mountId){ |
| 760 | + const mount=document.getElementById(mountId); |
| 761 | + if(!mount)return; |
| 762 | + if(mount.dataset.loaded==='1'){ |
| 763 | + const hidden=mount.style.display==='none'; |
| 764 | + mount.style.display=hidden?'':'none'; |
| 765 | + btn.innerHTML=(hidden?'▾':'▸')+' Patient $everything'; |
| 766 | + return; |
| 767 | + } |
| 768 | + btn.innerHTML='… loading Patient $everything'; |
| 769 | + try{ |
| 770 | + let bundle=everythingCache[pid]; |
| 771 | + if(!bundle){ |
| 772 | + const resp=await fetch(`${AIDBOX_URL}/fhir/Patient/${pid}/$everything`,{headers:{'Authorization':AUTH,'Accept':'application/fhir+json'}}); |
| 773 | + if(!resp.ok)throw new Error(`HTTP ${resp.status}`); |
| 774 | + bundle=await resp.json(); |
| 775 | + everythingCache[pid]=bundle; |
| 776 | + } |
| 777 | + mount.innerHTML=renderEverything(bundle); |
| 778 | + mount.dataset.loaded='1'; |
| 779 | + mount.style.display=''; |
| 780 | + btn.innerHTML='▾ Patient $everything'; |
| 781 | + }catch(e){ |
| 782 | + mount.innerHTML=`<span class="ev-none" style="color:var(--rose)">Error: ${escapeHtml(e.message)}</span>`; |
| 783 | + mount.style.display=''; |
| 784 | + btn.innerHTML='▸ Patient $everything (error — click to retry)'; |
| 785 | + mount.dataset.loaded=''; |
| 786 | + } |
| 787 | +} |
| 788 | + |
| 789 | +function renderEverything(bundle){ |
| 790 | + const entries=bundle.entry||[]; |
| 791 | + if(!entries.length)return '<span class="ev-none">No resources returned by $everything</span>'; |
| 792 | + const byType={}; |
| 793 | + for(const e of entries){ |
| 794 | + const r=e.resource; |
| 795 | + if(!r||!r.resourceType)continue; |
| 796 | + (byType[r.resourceType]=byType[r.resourceType]||[]).push(r); |
| 797 | + if(r.id)resourceStash.set(`${r.resourceType}/${r.id}`,r); |
| 798 | + } |
| 799 | + const types=Object.keys(byType).sort((a,b)=>byType[b].length-byType[a].length||a.localeCompare(b)); |
| 800 | + let html=`<div class="evidence-box-title">All resources <span class="cte-tag">${entries.length} total</span></div>`; |
| 801 | + for(const t of types){ |
| 802 | + const list=byType[t]; |
| 803 | + const bodyId=`evt-body-${t}-${Math.random().toString(36).slice(2,8)}`; |
| 804 | + html+=`<div class="evt-group"><div class="evt-head" onclick="toggleEvtGroup('${bodyId}',this)">▸ ${escapeHtml(t)}<span class="evt-count">(${list.length})</span></div><div id="${bodyId}" class="evt-body" style="display:none">`; |
| 805 | + for(const r of list){ |
| 806 | + const summary=summarizeResource(r); |
| 807 | + const sRt=escapeHtml(t),sRid=escapeHtml(r.id||'?'); |
| 808 | + html+=`<div class="evt-row"><a class="res-link" href="#" onclick="return showInlineJson(event,this,'${sRt}','${sRid}')">${sRid}</a> <a class="res-console" href="#" onclick="return openConsole(event,'${sRt}','${sRid}')" title="Open in Aidbox REST console (copies GET to clipboard)">↗</a>${summary?`<span class="evt-summary">— ${escapeHtml(summary)}</span>`:''}<pre class="res-preview" style="display:none"></pre></div>`; |
| 809 | + } |
| 810 | + html+='</div></div>'; |
| 811 | + } |
| 812 | + return html; |
| 813 | +} |
| 814 | + |
| 815 | +function toggleEvtGroup(bodyId,head){ |
| 816 | + const body=document.getElementById(bodyId); |
| 817 | + if(!body)return; |
| 818 | + const hidden=body.style.display==='none'; |
| 819 | + body.style.display=hidden?'':'none'; |
| 820 | + head.innerHTML=head.innerHTML.replace(/^ɛ[8E];|^[▸▾]/,hidden?'▾':'▸'); |
| 821 | +} |
| 822 | + |
| 823 | +function showInlineJson(ev,linkEl,type,id){ |
| 824 | + ev.preventDefault();ev.stopPropagation(); |
| 825 | + const pre=linkEl.parentElement.querySelector('pre.res-preview'); |
| 826 | + if(!pre)return false; |
| 827 | + if(pre.style.display!=='none'){pre.style.display='none';return false} |
| 828 | + if(pre.dataset.loaded!=='1'){ |
| 829 | + const r=resourceStash.get(`${type}/${id}`); |
| 830 | + pre.textContent=r?JSON.stringify(r,null,2):'Resource not found in $everything cache'; |
| 831 | + pre.dataset.loaded='1'; |
| 832 | + } |
| 833 | + pre.style.display=''; |
| 834 | + return false; |
| 835 | +} |
| 836 | + |
| 837 | +function summarizeResource(r){ |
| 838 | + const t=r.resourceType; |
| 839 | + const cd=cc=>cc&&((cc.coding&&cc.coding[0]&&(cc.coding[0].display||cc.coding[0].code))||cc.text)||''; |
| 840 | + let s=''; |
| 841 | + if(t==='Encounter')s=(r.class&&(r.class.display||r.class.code))||(r.type&&cd(r.type[0]))||''; |
| 842 | + else if(t==='Patient')s=(r.name&&r.name[0]&&[(r.name[0].given||[]).join(' '),r.name[0].family].filter(Boolean).join(' '))||''; |
| 843 | + else if(r.medicationCodeableConcept)s=cd(r.medicationCodeableConcept); |
| 844 | + else if(r.code)s=cd(r.code); |
| 845 | + const date=r.effectiveDateTime |
| 846 | + ||(r.effectivePeriod&&r.effectivePeriod.start) |
| 847 | + ||r.performedDateTime |
| 848 | + ||(r.performedPeriod&&r.performedPeriod.start) |
| 849 | + ||(r.period&&r.period.start) |
| 850 | + ||r.onsetDateTime |
| 851 | + ||r.authoredOn |
| 852 | + ||r.recordedDate |
| 853 | + ||r.birthDate |
| 854 | + ||r.issued |
| 855 | + ||''; |
| 856 | + return [s,date&&date.substring(0,10)].filter(Boolean).join(' — '); |
| 857 | +} |
| 858 | + |
746 | 859 | // ══════════════════════════════════ |
747 | 860 | // EXPORTS |
748 | 861 | // ══════════════════════════════════ |
|
0 commit comments