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='контакт';
+ 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));