Skip to content

Commit 47d12ba

Browse files
author
Alexandra Pavlyshina
committed
measure-evaluate/demo: Patient $everything tree on P360 page + fix Evidence cte-tag fallback to prefer ip_*-side fields and drop 'none' sentinel
1 parent 84f4657 commit 47d12ba

1 file changed

Lines changed: 120 additions & 7 deletions

File tree

  • aidbox-custom-operations/measure-evaluate/demo

aidbox-custom-operations/measure-evaluate/demo/app.html

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,16 @@
173173
.res-console.copied{background:var(--teal-bg);color:var(--teal)}
174174
.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}
175175
.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}
176186

177187
/* ── Care gap specific ─────────────── */
178188
.cat-badge{font-size:0.5rem;font-weight:600;padding:.08rem .3rem;border-radius:3px;display:inline-block;margin-right:.15rem}
@@ -613,7 +623,7 @@
613623
const name=patientLabel(pid);const open=measures.filter(m=>m.status==='open');const closed=measures.filter(m=>m.status==='closed');
614624
const initials=name.split(' ').map(w=>w[0]).join('').substring(0,2).toUpperCase();
615625
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()">&larr; 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()">&larr; 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}')">&#x25B8; Patient $everything</button><div id="p360-ever-${pid}" class="p360-everything-mount" style="display:none"></div></div><div class="p360-list">`;
617627
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;
618628
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} &mdash; ${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)">&#x25B8; Evidence</button><div id="${evId}"></div></div>`}
619629
html+='</div>';document.getElementById('main').innerHTML=html;
@@ -666,7 +676,7 @@
666676
html+='<div class="evidence-box"><div class="evidence-box-title">Evidence</div>';
667677
const shown=new Set();
668678
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||'';
670680
const key=rid?`${rt}/${rid}|${src}`:`ip|${r.ip_code_display}|${r.ip_event_date}|${src}`;
671681
if(shown.has(key))continue;shown.add(key);
672682
const date=r.ip_event_date?' — '+r.ip_event_date.substring(0,10):'';
@@ -680,7 +690,7 @@
680690
}
681691
for(const r of numItems){
682692
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:'')||'';
684694
const key=rid?`${rt}/${rid}|${src}`:`num|${r.code_display}|${r.event_date}|${src}`;
685695
if(shown.has(key))continue;shown.add(key);
686696
const date=r.event_date?' — '+r.event_date.substring(0,10):'';
@@ -693,11 +703,10 @@
693703
html+=`<div class="evidence-box-item"><strong>${escapeHtml(rt)}:</strong> ${escapeHtml(r.code_display)}${codeStr}${date}${tag}</div>`;
694704
}
695705
}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}`;
698708
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>`;
701710
}
702711
}
703712
html+='</div>';
@@ -743,6 +752,110 @@
743752
return false;
744753
}
745754

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?'&#x25BE;':'&#x25B8;')+' Patient $everything';
766+
return;
767+
}
768+
btn.innerHTML='&hellip; 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='&#x25BE; 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='&#x25B8; 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)">&#x25B8; ${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)">&#8599;</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(/^&#x25B[8E];|^[]/,hidden?'&#x25BE;':'&#x25B8;');
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+
746859
// ══════════════════════════════════
747860
// EXPORTS
748861
// ══════════════════════════════════

0 commit comments

Comments
 (0)