diff --git a/index.html b/index.html index 31d6c86..14d72b2 100644 --- a/index.html +++ b/index.html @@ -255,6 +255,7 @@ function seed(){ } return { role:"patient", + staffTab:"patients", currentPatient:"p1", patients:[ { @@ -339,6 +340,75 @@ var ZONE_TXT = { red:{t:"Ухудшение", d:"Срочно! Выполните рекомендации и вызовите скорую при необходимости."} }; +/* ============ ПРОТОКОЛ «СВЕТОФОР» (расширенные действия) ============ */ +var ZONE_PROTOCOL = { + green:{ + title:"🟢 Зелёная зона — стабилен", + crit:"SpO₂ ≥ 95% · ЧДД 12–20/мин · ЧСС 60–100 уд/мин · CAT не растёт · нет гнойной мокроты", + patient:["Продолжать базисную терапию строго по схеме","Вести дневник 2–3 раза в день","Принимать препараты по расписанию, не пропускать","Дыхательная гимнастика и регулярная физическая активность","Вакцинация (грипп, пневмококк) по графику"], + staff:["Рутинное наблюдение, просмотр динамики 1–2 раза в неделю","Контроль adherence и техники ингаляции","Плановый осмотр 1 раз в 1–3 месяца, спирометрия, CAT","Обучение и поддержка самопомощи"], + esc:"Эскалации не требуется" + }, + yellow:{ + title:"🟡 Жёлтая зона — внимание", + crit:"SpO₂ 90–94% · ЧДД 21–24/мин · ЧСС 101–110 уд/мин · CAT вырос на ≥2 балла · нарастание одышки", + patient:["Применить спасательный бронхолитик (сальбутамол 2–4 вдоха / небулайзер)","Проверить и при необходимости усилить О₂ по схеме","Контроль SpO₂ каждые 15–30 минут","Усилить контроль до 3–4 замеров/день","Связаться с медсестрой в течение дня","Начать резервный ГКС/антибиотик по плану врача при гнойной мокроте"], + staff:["Связаться с пациентом в течение дня","Оценить adherence и технику ингаляции","Коррекция терапии: усилить бронхолитик, рассмотреть ИГКС","Контроль через 24–48 часов","Информировать лечащего врача при отсутствии эффекта"], + esc:"Нет эффекта за 24–48ч → красная зона, рассмотреть госпитализацию" + }, + red:{ + title:"🔴 Красная зона — ухудшение", + crit:"SpO₂ < 90% · ЧДД > 24/мин · ЧСС > 110 уд/мин · резкая одышка в покое · гнойная мокрота · лихорадка", + patient:["Увеличить поток О₂ по схеме","Бронхолитик через небулайзер/ДАИ (сальбутамол ± ипратропий)","Положение ортопноэ, дыхание через сжатые губы","Старт системной терапии по плану: ГКС ± антибиотик","Вызвать скорую при SpO₂<88% >5мин или нарастании одышки"], + staff:["Срочный контакт с пациентом, решение о госпитализации/вызове бригады","Назначить/скорректировать системную терапию (ГКС, антибиотики, О₂)","Контроль через 1–4 часа","Передача информации лечащему врачу и оформление вмешательства","Подготовка к госпитализации при нарастании РДС"], + esc:"Экстренная госпитализация: SpO₂<88%, ЧДД>30, спутанность сознания, цианоз, загрудинные боли" + } +}; + +/* ============ КАТАЛОГ БАЗИСНОЙ ТЕРАПИИ ХОБЛ (для назначения врачом) ============ */ +var MED_CATALOG = [ + {grp:"Купирование (короткодействующие)", items:[ + {id:"salbutamol", name:"Сальбутамол (ДАИ/небулайзер)", dt:["по потребности"]}, + {id:"fenoterol", name:"Фенотерол (Беротек)", dt:["по потребности"]}, + {id:"ipratropium", name:"Ипратропия бромид (Атровент)", dt:["по потребности"]}, + {id:"ipra_feno", name:"Ипратропий/Фенотерол (Беродуал)", dt:["по потребности"]} + ]}, + {grp:"ЛАМА (длиннодействующие М-холинолитики)", items:[ + {id:"tiotropium", name:"Тиотропий (Спирива)", dt:["08:00"]}, + {id:"glycopyrronium", name:"Гликопирроний (Сибри)", dt:["08:00"]}, + {id:"aclidinium", name:"Аклидиний (Эклира)", dt:["08:00","20:00"]}, + {id:"umeclidinium", name:"Умеклидиний (Инкруз)", dt:["08:00"]} + ]}, + {grp:"ЛАБА (длиннодействующие β2-агонисты)", items:[ + {id:"indacaterol", name:"Индакатерол (Онбрез)", dt:["08:00"]}, + {id:"olodaterol", name:"Олодатерол (Стриворди)", dt:["08:00"]}, + {id:"salmeterol", name:"Сальметерол (Серевент)", dt:["08:00","20:00"]}, + {id:"formoterol", name:"Формотерол (Форадил)", dt:["08:00","20:00"]} + ]}, + {grp:"ЛАМА/ЛАБА (комбинированные)", items:[ + {id:"gly_ind", name:"Гликопирроний/Индакатерол (Ультибро)", dt:["08:00"]}, + {id:"tio_olo", name:"Тиотропий/Олодатерол (Спиольто)", dt:["08:00"]}, + {id:"ume_vil", name:"Умеклидиний/Вилинтерол (Аноро)", dt:["08:00"]} + ]}, + {grp:"ИГКС/ЛАБА (комбинированные)", items:[ + {id:"bud_fm", name:"Будесонид/Формотерол (Симбикорт)", dt:["08:00","20:00"]}, + {id:"flu_sal", name:"Флутиказон/Сальметерол (Серетид)", dt:["08:00","20:00"]}, + {id:"flu_vil", name:"Флутиказон/Вилантерол (Релвар)", dt:["08:00"]}, + {id:"bec_fm", name:"Беклометазон/Формотерол (Фостер)", dt:["08:00","20:00"]} + ]}, + {grp:"Тройная терапия (ИГКС/ЛАБА/ЛАМА)", items:[ + {id:"flu_ume_vil", name:"Флутиказон/Умеклидиний/Вилантерол (Трео)", dt:["08:00"]}, + {id:"bec_fm_gly", name:"Беклометазон/Формотерол/Гликопирроний (Trimbow)", dt:["08:00","20:00"]} + ]}, + {grp:"Прочее (ФДЭ-4, муколитики, макролиды, О₂)", items:[ + {id:"roflumilast", name:"Рофлумиласт (Даксас) — ФДЭ-4", dt:["21:00"]}, + {id:"nac", name:"N-ацетилцистеин (муколитик)", dt:["08:00","20:00"]}, + {id:"erdosteine", name:"Эрдостеин (муколитик)", dt:["08:00","20:00"]}, + {id:"azithromycin", name:"Азитромицин (профилактика обострений)", dt:["08:00"]}, + {id:"o2_therapy", name:"Длительная О₂-терапия", dt:["15ч/сут 2л/мин"]} + ]} +]; + /* ============ CAT ВОПРОСЫ ============ */ var CAT_Q = [ {q:"Кашель", a:"Я никогда не кашляю", b:"Я постоянно кашляю"}, @@ -354,7 +424,7 @@ function catLevel(s){ if(s<=9) return {t:"низкое влияние",c:"green" /* ============ УТИЛИТЫ ============ */ function $(sel,root){ return (root||document).querySelector(sel); } -function el(tag,cls,html,opts){ var e=document.createElement(tag); if(cls)e.className=cls; if(html!=null && typeof html==="string")e.innerHTML=html; if(opts){ if(opts.style)e.setAttribute("style",opts.style); if(opts.text!=null)e.textContent=opts.text; } return e; } +function el(tag,cls,html,opts){ var e=document.createElement(tag); if(cls)e.className=cls; if(html!=null && typeof html==="string")e.innerHTML=html; else if(html!=null && typeof html==="object" && !opts){opts=html;} if(opts){ if(opts.style)e.setAttribute("style",opts.style); if(opts.text!=null)e.textContent=opts.text; } return e; } function nowHHMM(){ var d=new Date(); return pad(d.getHours())+":"+pad(d.getMinutes()); } function pad(n){ return (n<10?"0":"")+n; } function fmtTime(ts){ var d=new Date(ts); return pad(d.getDate())+"."+pad(d.getMonth()+1)+" "+pad(d.getHours())+":"+pad(d.getMinutes()); } @@ -838,6 +908,68 @@ function saveEntry(p){ toast("Запись сохранена. Зона: "+ZONE_TXT[z].t); } +/* ============ МОДАЛКА: НАЗНАЧЕНИЕ БАЗИСНОЙ ТЕРАПИИ ============ */ +function openPrescribeModal(p){ + var m=$("#modal"); m.innerHTML=""; + var head=el("div","modal-head"); + head.innerHTML='

Назначить базисную терапию · '+p.name+'

'; + var close=el("button","modal-close","×"); close.onclick=closeModal; + head.appendChild(close); m.appendChild(head); + var body=el("div","modal-body"); + body.appendChild(el("div","muted","Выберите препараты из каталога. Время приёма можно изменить. Уже назначенные отмечены.",{"style":"margin-bottom:14px"})); + + MED_CATALOG.forEach(function(grp){ + body.appendChild(el("div","section-title",grp.grp)); + grp.items.forEach(function(it){ + var exists=p.meds.some(function(mm){return mm.id===it.id;}); + var row=el("div","med"); + row.dataset.medId=it.id; row.dataset.medName=it.name; + var left=el("div","med-info"); + left.innerHTML='
'+it.name+(exists?' назначен':'')+'
'; + var te=el("div","med-time"); + var ti=el("input","input"); ti.value=it.dt.join(", "); ti.style.maxWidth="220px"; ti.style.padding="7px 10px"; ti.style.fontSize="13px"; + te.appendChild(el("span","med-pill","время")); + te.appendChild(ti); + left.appendChild(te); + row.appendChild(left); + var cb=el("input"); cb.type="checkbox"; cb.style.width="20px"; cb.style.height="20px"; + if(exists){ cb.checked=true; cb.disabled=true; } + var lab=el("label","",{"style":"display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;cursor:pointer"}); + lab.appendChild(cb); lab.appendChild(el("span","","Назначить")); + row.appendChild(lab); + body.appendChild(row); + }); + }); + + m.appendChild(body); + var foot=el("div","modal-foot"); + var cancel=el("button","btn ghost","Отмена"); cancel.onclick=closeModal; + var saveBtn=el("button","btn","Сохранить назначения"); + saveBtn.onclick=function(){ savePrescription(p, body); }; + foot.appendChild(cancel); foot.appendChild(saveBtn); + m.appendChild(foot); + openModal(); +} + +function savePrescription(p, body){ + var rows=body.querySelectorAll(".med"); + var added=0; + for(var i=0;i'; app.appendChild(banner); - // список пациентов по зонам + // вкладки + var tabs=el("div","tabs"); + [["patients","🩺 Пациенты"],["reports","📊 Отчёты"],["protocol","🚦 Протокол (светофор)"]].forEach(function(t){ + var b=el("button","",t[1]); + if(state.staffTab===t[0]) b.classList.add("active"); + b.onclick=function(){ state.staffTab=t[0]; save(state); render(); }; + tabs.appendChild(b); + }); + app.appendChild(tabs); + + if(state.staffTab==="reports") renderStaffReports(); + else if(state.staffTab==="protocol") renderStaffProtocol(); + else renderStaffList(); +} + +function renderStaffList(){ var list=el("div","card"); list.appendChild(el("h3","","Пациенты по степени риска",{"style":"margin-bottom:14px"})); var sorted=state.patients.slice().sort(function(a,b){ var za=entryZone(a), zb=entryZone(b); return zoneRank(zb)-zoneRank(za); }); - sorted.forEach(function(p){ - list.appendChild(patientRow(p)); - }); + sorted.forEach(function(p){ list.appendChild(patientRow(p)); }); app.appendChild(list); } +/* ---- Отчёт: сегментация + охват ---- */ +function renderStaffReports(){ + var c=zoneCounts(); var n=state.patients.length; + // 1. Сегментация + var seg=el("div","card"); + seg.appendChild(el("h3","","Отчёт по сегментации пациентов",{"style":"margin-bottom:14px"})); + var bars=el("div","",{"style":"margin-bottom:16px"}); + [["red","Красная",c.red,"#EA3943"],["yellow","Жёлтая",c.yellow,"#F5A623"],["green","Зелёная",c.green,"#16C784"]].forEach(function(z){ + var pct=n?Math.round(z[2]/n*100):0; + var row=el("div","",{"style":"margin-bottom:10px"}); + row.innerHTML='
'+z[1]+''+z[2]+' ('+pct+'%)
'; + var track=el("div","",{"style":"background:var(--gray-100);border-radius:6px;height:12px;overflow:hidden"}); + var fill=el("div","",{"style":"background:"+z[3]+";height:100%;width:"+pct+"%"}); + track.appendChild(fill); row.appendChild(track); bars.appendChild(row); + }); + seg.appendChild(bars); + // таблица по зонам + ["red","yellow","green"].forEach(function(z){ + var ps=state.patients.filter(function(p){return entryZone(p)===z;}); + if(!ps.length) return; + seg.appendChild(el("div","zone-chip "+z,''+ZONE_TXT[z].t+" — "+ps.length,{"style":"margin:10px 0 6px"})); + var lst=el("div","",{"style":"font-size:14px;color:var(--gray-500)"}); + lst.innerHTML=ps.map(function(p){return p.name;}).join(" · "); + seg.appendChild(lst); + }); + app.appendChild(seg); + + // 2. Охват мониторингом + var cov=el("div","card"); + cov.appendChild(el("h3","","Отчёт по охвату мониторингом",{"style":"margin-bottom:14px"})); + var now=Date.now(), H24=3600000*24, H48=3600000*48; + var active24=0, stale=0, noData=0, doseTaken=0, doseTotal=0, sumCat=0, catN=0; + var staleList=[]; + state.patients.forEach(function(p){ + var e=lastEntry(p); + if(!e){ noData++; staleList.push({p:p,d:"нет данных"}); return; } + var dt=now-e.ts; + if(dt<=H24) active24++; else { stale++; staleList.push({p:p,d:"последний замер "+fmtTime(e.ts)}); } + sumCat+=e.catTotal; catN++; + p.meds.forEach(function(m){ medStatus(m,e).forEach(function(s){ if(s.status!=="upcoming"){ doseTotal++; if(s.status==="taken") doseTaken++; } }); }); + }); + var tiles=el("div","grid4"); + tiles.appendChild(statTile("Всего под наблюдением", n)); + tiles.appendChild(statTile("Активны (замер ≤24ч)", active24, active24>=Math.ceil(n*0.6)?"green":"yellow")); + tiles.appendChild(statTile("Нет замеров >24ч", stale+noData, (stale+noData)?"red":"green")); + tiles.appendChild(statTile("Adherence препаратов", (doseTotal?Math.round(doseTaken/doseTotal*100):0)+"%", doseTotal&&doseTaken/doseTotal>=0.8?"green":"yellow")); + cov.appendChild(tiles); + var tiles2=el("div","grid4"); + tiles2.appendChild(statTile("Средний CAT", catN?Math.round(sumCat/catN*10)/10:"—")); + tiles2.appendChild(statTile("В красной зоне", c.red, c.red?"red":"green")); + tiles2.appendChild(statTile("В жёлтой зоне", c.yellow, c.yellow?"yellow":"green")); + tiles2.appendChild(statTile("В зелёной зоне", c.green, "green")); + cov.appendChild(tiles2); + // список требующих внимания + if(staleList.length){ + cov.appendChild(el("div","section-title","Требуют внимания (нет свежих замеров)")); + staleList.forEach(function(it){ + var row=el("div","patient-row yellow"); + row.onclick=function(){ openPatientDetail(it.p.id); }; + row.innerHTML='
'+it.p.name+'
'+it.d+'
контакт'; + cov.appendChild(row); + }); + } + app.appendChild(cov); +} +function statTile(lbl,val,zone){ + var cls=zone==="red"?"danger":zone==="yellow"?"warn":zone==="green"?"ok":""; + var t=el("div","vital-tile "+cls); + t.innerHTML='
'+lbl+'
'+val+'
'; + return t; +} + +/* ---- Протокол «светофор» (расширенные действия) ---- */ +function renderStaffProtocol(){ + ["red","yellow","green"].forEach(function(zkey){ + var p=ZONE_PROTOCOL[zkey]; + var cls=zkey==="red"?"r":zkey==="yellow"?"y":"g"; + var card=el("div","zone "+cls); + card.style.marginBottom="18px"; + var badge=zkey==="red"?"Красная зона":zkey==="yellow"?"Жёлтая зона":"Зелёная зона"; + card.innerHTML=''+badge+'

'+p.title+'

'; + card.appendChild(el("div","thr","Критерии: "+p.crit)); + card.appendChild(protoCol("Действия пациента", p.patient, "→", "var(--"+(zkey==="green"?"green":zkey==="yellow"?"amber":"red")+")")); + card.appendChild(protoCol("Действия медперсонала", p.staff, "✓", "var(--cyan)")); + var esc=el("div","act","Эскалация: "+p.esc); + card.appendChild(esc); + app.appendChild(card); + }); +} +function protoCol(title, items, mark, color){ + var wrap=el("div","",{"style":"margin-bottom:12px"}); + wrap.appendChild(el("div","",{style:"font-size:13px;font-weight:700;color:var(--gray-500);text-transform:uppercase;letter-spacing:.04em;margin-bottom:6px","text":title})); + var ul=el("ul","",{"style":"list-style:none;padding:0;margin:0"}); + items.forEach(function(it){ + var li=el("li","",{"style":"font-size:14px;padding:5px 0 5px 22px;position:relative","text":it}); + li.style.setProperty("--mk",color); + li.innerHTML=''+mark+''+it; + ul.appendChild(li); + }); + wrap.appendChild(ul); + return wrap; +} + + function entryZone(p){ var e=lastEntry(p); return e?overallZone(e.spo2,e.hr,e.rr):"green"; } function zoneCounts(){ var c={green:0,yellow:0,red:0}; @@ -933,8 +1182,15 @@ function openPatientDetail(id){ if(e.note){ pc.appendChild(el("div","muted","Заметка пациента: "+e.note,{"style":"margin-top:12px"})); } app.appendChild(pc); - // препараты + // препараты + назначение терапии app.appendChild(renderMedsCard(p, e, false)); + var presc=el("div","card"); + presc.style.display="flex";presc.style.alignItems="center";presc.style.justifyContent="space-between";presc.style.gap="12px";presc.style.flexWrap="wrap"; + presc.innerHTML='

Базисная терапия

Назначьте препараты из каталога — пациент увидит схему в своём кабинете
'; + var pb=el("button","btn dark","+ Назначить терапию"); + pb.onclick=function(){ openPrescribeModal(p); }; + presc.appendChild(pb); + app.appendChild(presc); // флаги / рекомендации var flags=buildFlags(e).concat(buildMedFlags(p));