v29: регионы, роли, журнал действий, уведомления по срокам

This commit is contained in:
Dauren777 2026-06-05 07:26:36 +00:00
parent 28a4aa326c
commit 73b66a2f37

View File

@ -105,7 +105,7 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
<p class="sub">АО «Казахтелеком» — мониторинг производственной безопасности</p>
<label>Корпоративная почта</label>
<input type="email" id="loginEmail" placeholder="surname@telecom.kz" required>
<p class="hint">admin@telecom.kz / ahmetov@telecom.kz / serikov@telecom.kz — пароль любой</p>
<p class="hint">curator@telecom.kz (куратор) / admin@telecom.kz / north@telecom.kz (Север) / almaty@telecom.kz (Алматы) — пароль любой</p>
<label>Пароль</label>
<input type="password" id="loginPass" placeholder="••••••••" required>
<p class="err" id="loginErr">Неверная почта или пароль</p>
@ -128,12 +128,14 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
<div class="main">
<div class="tabs">
<button class="tab-btn active" data-tab="dashboard">📊 Дашборд</button>
<button class="tab-btn" data-tab="myevents">📋 Мои мероприятия</button>
<button class="tab-btn" data-tab="myevents">📋 Мероприятия</button>
<button class="tab-btn" data-tab="analytics">📈 Аналитика</button>
<button class="tab-btn" data-tab="journal">📝 Журнал</button>
</div>
<div class="tab-content active" id="tab-dashboard"></div>
<div class="tab-content" id="tab-myevents"></div>
<div class="tab-content" id="tab-analytics"></div>
<div class="tab-content" id="tab-journal"></div>
</div>
</div>
@ -143,6 +145,8 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
"use strict";
var sections=["I. Люди","II. Оборудование","III. Аварии и ЧС","IV. Информ. работа","V. ИИ и цифровизация"];
var branches=["Дирекция производственной безопасности","Дивизион «Сеть»","Дивизион по корпоративному бизнесу","Дивизион по розничному бизнесу","Сервисная фабрика","Дирекция «Телеком Комплект»","Корпоративный университет","Дирекция управления проектами","Дивизион цифрового бизнеса"];
var regions=["Центральный регион","Алматинский регион","Южный регион","Северный регион","Восточный регион","Западный регион"];
var branchRegion=[0,3,1,1,2,3,4,0,5]; // branch index -> region index
var statusMap={done:"Исполнено",warn:"На контроле",late:"Просрочено",wait:"В процессе"};
var months=["2026-01","2026-02","2026-03","2026-04","2026-05","2026-06","2026-07","2026-08","2026-09","2026-10","2026-11","2026-12"];
var monthNames=["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"];
@ -151,7 +155,24 @@ function esc(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g
function sb(s){var m={done:"green",warn:"amber",late:"red",wait:"gray"};return'<span class="badge '+m[s]+'">'+statusMap[s]+'</span>'}
function pct(p){var c=p>=80?"var(--green)":p>=40?"var(--amber)":"var(--red)";return'<div class="pct-bar"><div class="track"><div class="fill" style="width:'+p+'%;background:'+c+'"></div></div>'+p+'%</div>'}
var users={"dpp@telecom.kz":{name:"Директор ДПБ",branch:0,role:"director"},"ahmetov@telecom.kz":{name:"Ахметов К.Т.",branch:6,role:"resp"},"serikov@telecom.kz":{name:"Сериков А.М.",branch:1,role:"resp"},"nurlanov@telecom.kz":{name:"Нурланов Д.С.",branch:8,role:"resp"},"aliev@telecom.kz":{name:"Алиев Г.С.",branch:4,role:"resp"},"tulegenov@telecom.kz":{name:"Тулегенов Е.А.",branch:2,role:"resp"},"saparov@telecom.kz":{name:"Сапаров А.Д.",branch:3,role:"resp"},"maratov@telecom.kz":{name:"Маратов Ж.К.",branch:5,role:"resp"},"iskakov@telecom.kz":{name:"Искаков Р.Н.",branch:7,role:"resp"},"admin@telecom.kz":{name:"Администратор",branch:0,role:"admin"}};
var users={
"curator@telecom.kz":{name:"Куратор Плана",branch:0,role:"curator",region:-1},
"admin@telecom.kz":{name:"Администратор",branch:0,role:"admin",region:-1},
"ahmetov@telecom.kz":{name:"Ахметов К.Т.",branch:6,role:"branch",region:4},
"serikov@telecom.kz":{name:"Сериков А.М.",branch:1,role:"branch",region:3},
"nurlanov@telecom.kz":{name:"Нурланов Д.С.",branch:8,role:"branch",region:5},
"aliev@telecom.kz":{name:"Алиев Г.С.",branch:4,role:"branch",region:2},
"tulegenov@telecom.kz":{name:"Тулегенов Е.А.",branch:2,role:"branch",region:1},
"saparov@telecom.kz":{name:"Сапаров А.Д.",branch:3,role:"branch",region:1},
"maratov@telecom.kz":{name:"Маратов Ж.К.",branch:5,role:"branch",region:3},
"iskakov@telecom.kz":{name:"Искаков Р.Н.",branch:7,role:"branch",region:0},
"north@telecom.kz":{name:"Отв. Северный регион",branch:1,role:"region",region:3},
"almaty@telecom.kz":{name:"Отв. Алматинский регион",branch:2,role:"region",region:1},
"south@telecom.kz":{name:"Отв. Южный регион",branch:4,role:"region",region:2},
"center@telecom.kz":{name:"Отв. Центральный регион",branch:0,role:"region",region:0},
"east@telecom.kz":{name:"Отв. Восточный регион",branch:6,role:"region",region:4},
"west@telecom.kz":{name:"Отв. Западный регион",branch:8,role:"region",region:5}
};
var curUser=null,curMonth=5;
// Load events
@ -165,15 +186,32 @@ var events=null;
})();
function saveEvents(){localStorage.setItem("samruk_ev",JSON.stringify(events))}
function getMy(){if(!curUser||!events)return[];if(curUser.role==="admin"||curUser.role==="director")return events;return events.filter(function(e){return e.b===curUser.branch})}
// Action log
function addLog(action,eventId,detail){var l=JSON.parse(localStorage.getItem("samruk_log")||"[]");l.push({ts:new Date().toISOString(),user:curUser.name,role:curUser.role,action:action,eid:eventId,detail:detail||""});if(l.length>500)l=l.slice(-500);localStorage.setItem("samruk_log",JSON.stringify(l))}
function getLog(){return JSON.parse(localStorage.getItem("samruk_log")||"[]")}
function getMy(){if(!curUser||!events)return[];if(curUser.role==="admin"||curUser.role==="curator")return events;if(curUser.role==="region")return events.filter(function(e){return branchRegion[e.b]===curUser.region});return events.filter(function(e){return e.b===curUser.branch})}
// Auth
function doLogin(e){e.preventDefault();var em=document.getElementById("loginEmail").value.trim().toLowerCase();if(users[em]){curUser={email:em,name:users[em].name,branch:users[em].branch,role:users[em].role};localStorage.setItem("samruk_u",JSON.stringify(curUser));showApp()}else{document.getElementById("loginErr").style.display="block"}return false}
function doLogout(){localStorage.removeItem("samruk_u");curUser=null;document.getElementById("loginScreen").style.display="flex";document.getElementById("app").style.display="none"}
function showApp(){document.getElementById("loginScreen").style.display="none";document.getElementById("app").style.display="block";document.getElementById("userLabel").innerHTML="<strong>"+curUser.name+"</strong> · "+branches[curUser.branch];renderAll()}
function doLogin(e){e.preventDefault();var em=document.getElementById("loginEmail").value.trim().toLowerCase();if(users[em]){ curUser={email:em,name:users[em].name,branch:users[em].branch,role:users[em].role,region:users[em].region};localStorage.setItem("samruk_u",JSON.stringify(curUser));addLog("вошёл");showApp()}else{document.getElementById("loginErr").style.display="block"}return false}
function doLogout(){addLog("вышел");localStorage.removeItem("samruk_u");curUser=null;document.getElementById("loginScreen").style.display="flex";document.getElementById("app").style.display="none"}
function showApp(){document.getElementById("loginScreen").style.display="none";document.getElementById("app").style.display="block";var label=curUser.name+" · "+(curUser.role==="curator"||curUser.role==="admin"?"Все регионы":curUser.role==="region"?regions[curUser.region]:branches[curUser.branch]);document.getElementById("userLabel").innerHTML="<strong>"+label+"</strong>";renderAll()}
// Notifs
function notifsUpdate(){var my=getMy(),n=[];my.forEach(function(e){if(e.s==="late")n.push({m:"🔴 Просрочено: "+e.t.slice(0,60)+"...",t:e.due});if(e.s==="warn"&&e.p<30)n.push({m:"🟡 Низкий прогресс: "+e.t.slice(0,50)+"...",t:"Сейчас"})});var el=document.getElementById("notifDrop"),c=document.getElementById("notifCount");c.textContent=n.length;c.style.display=n.length?"inline-block":"none";el.innerHTML=n.length?n.map(function(x){return'<div class="item"><div class="title">'+esc(x.m)+'</div><div class="time">'+x.t+'</div></div>'}).join(""):'<div class="empty">Нет уведомлений</div>'}
function notifsUpdate(){
var my=getMy(),n=[],now=new Date();
my.forEach(function(e){
if(e.s==="late")n.push({m:"🔴 Просрочено: "+e.t.slice(0,50)+"...",t:e.due});
if(e.s==="warn"&&e.p<30)n.push({m:"🟡 Низкий прогресс: "+e.t.slice(0,50)+"...",t:"Сейчас"});
// Deadline approaching (within 7 days)
if(e.s!=="done"&&e.s!=="late"){
var parts=e.due.split(".");if(parts.length===3){
var due=new Date(parseInt(parts[2]),parseInt(parts[1])-1,parseInt(parts[0]));
var days=(due-now)/86400000;
if(days>0&&days<=7)n.push({m:"⏰ Срок через "+Math.round(days)+" дн: "+e.t.slice(0,50)+"...",t:e.due});
}
}
});var el=document.getElementById("notifDrop"),c=document.getElementById("notifCount");c.textContent=n.length;c.style.display=n.length?"inline-block":"none";el.innerHTML=n.length?n.map(function(x){return'<div class="item"><div class="title">'+esc(x.m)+'</div><div class="time">'+x.t+'</div></div>'}).join(""):'<div class="empty">Нет уведомлений</div>'}
function toggleNotif(){notifsUpdate();document.getElementById("notifDrop").classList.toggle("open")}
// Tabs
@ -182,7 +220,7 @@ function switchTab(name){
document.querySelector('[data-tab="'+name+'"]').classList.add("active");
document.querySelectorAll(".tab-content").forEach(function(c){c.classList.remove("active")});
document.getElementById("tab-"+name).classList.add("active");
if(name==="dashboard")renderDashboard();else if(name==="myevents")renderMyEvents();else if(name==="analytics")renderAnalytics();
if(name==="dashboard")renderDashboard();else if(name==="myevents")renderMyEvents();else if(name==="analytics")renderAnalytics();else if(name==="journal")renderJournal();
}
// ===== DASHBOARD =====
@ -222,6 +260,15 @@ function renderDashboard(){
h+='<input type="file" id="impF" accept=".json" style="display:none" onchange="importAll(this)">';
h+='<button class="btn btn-sm" style="margin-top:6px;margin-left:4px;background:var(--red);color:#fff" onclick="clearAllFiles()">🗑 Очистить файлы</button></div></div>';
// Regional breakdown (for curator/admin)
if(curUser.role==="curator"||curUser.role==="admin"){
h+='<div class="panel"><h3>🌍 Исполнение по регионам</h3><table><tr><th>Регион</th><th>Всего</th><th>Исполнено</th><th>%</th></tr>';
regions.forEach(function(r,ri){var items=events.filter(function(e){return branchRegion[e.b]===ri}),d=items.filter(function(e){return e.s==="done"}).length;
h+='<tr><td><strong>'+r+'</strong></td><td>'+items.length+'</td><td>'+d+'</td><td>'+pct(items.length?Math.round(d/items.length*100):0)+'</td></tr>';
});
h+='</table></div>';
}
document.getElementById("tab-dashboard").innerHTML=h;
}
@ -235,7 +282,7 @@ function downloadReport(){
addRow("",-1);
if(e.sub) e.sub.forEach(function(s,i){ addRow(s.l,i); });
});
var blob=new Blob(["\uFEFF"+csv],{type:"text/csv;charset=utf-8"}),a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="otchet_pb_"+M(from)+"-"+M(to)+".csv";a.click()
var blob=new Blob(["\uFEFF"+csv],{type:"text/csv;charset=utf-8"}),a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="otchet_pb_"+M(from)+"-"+M(to)+".csv";a.click();addLog("скачал CSV-отчёт",null,M(from)+"-"+M(to))
}
function downloadHTML(){
@ -257,7 +304,7 @@ function downloadHTML(){
h+='</body></html>';
try{
var blob=new Blob(["\uFEFF"+h],{type:"text/html;charset=utf-8"}),a=document.createElement("a");
a.href=URL.createObjectURL(blob);a.download="otchet_pb_"+M(from)+"-"+M(to)+".html";a.click();
a.href=URL.createObjectURL(blob);a.download="otchet_pb_"+M(from)+"-"+M(to)+".html";a.click();addLog("скачал HTML-отчёт",null,M(from)+"-"+M(to));
setTimeout(function(){URL.revokeObjectURL(a.href)},60000);
}catch(e){
alert("⚠️ Отчёт слишком большой для скачивания. Попробуйте выбрать меньший период или очистить часть файлов.");
@ -327,7 +374,7 @@ function toggleSubItem(eid, subIdx, checked) {
else if(sc.length === 0 && e.s === 'done') e.s = 'warn';
e.p = Math.max(e.p, pct);
e.h.push(new Date().toLocaleDateString()+' — '+curUser.name+': подпункты '+sc.length+'/'+e.sub.length);
saveEvents();
saveEvents();addLog("подпункты",eid,sc.length+"/"+e.sub.length);
}
renderMyEvents();
}
@ -444,7 +491,7 @@ function saveEdit(id, mk){
}
var now=new Date().toLocaleDateString();e.h.push(now+" — "+curUser.name+": "+statusMap[e.s]+", "+e.p+"%"+(cmt?" — "+cmt:""));
if(e.s==="done"&&e.done==="\u2014")e.done=now;
saveEvents();closeEM();renderAll();
saveEvents();addLog("изменил статус",id,statusMap[e.s]+" "+e.p+"%");closeEM();renderAll();
}
function closeEM(){document.getElementById("editModalOverlay").classList.remove("open")}
@ -507,6 +554,7 @@ function uploadFiles(eid,mk,si){
var bak=JSON.parse(JSON.stringify(ad));bak[mk].files=bak[mk].files.slice(0,-(pr-sk)||0);try{setMD(eid,bak,si)}catch(e2){}
alert("⚠️ Хранилище заполнено. Удалите старые файлы (кнопка «Очистить файлы» на дашборде).");
}
addLog("загрузил файлы",eid,(pr-sk)+" файл(ов) за "+M(mk));
if(sk)alert(sk+" файл(ов) > 3 МБ пропущены");closeEM();openEdit(eid,curMonth,si>=0?si:undefined)
}
for(var i=0;i<fi.files.length;i++){(function(f){if(f.size>MAX){sk++;pr++;if(pr===fi.files.length)fin();return}
@ -516,8 +564,17 @@ function uploadFiles(eid,mk,si){
function dlF(eid,mk,idx,si){si=si||-1;mk=months[mk];var ad=getMD(eid,si),arr=ad[mk]?ad[mk].files:null;if(!arr||!arr[idx]||!arr[idx].data)return;var f=arr[idx],a=document.createElement("a");a.href=f.data;a.download=f.name;document.body.appendChild(a);a.click();document.body.removeChild(a)}
function rmF(eid,mk,idx,si){si=si||-1;mk=months[mk];var ad=getMD(eid,si);if(!ad[mk]||!ad[mk].files)return;ad[mk].files.splice(idx,1);setMD(eid,ad,si);closeEM();openEdit(eid,curMonth,si>=0?si:undefined)}
// Init
function renderAll(){notifsUpdate();switchTab(document.querySelector(".tab-btn.active").dataset.tab)}
function renderJournal(){
var log=getLog().reverse(),h='<div class="panel"><h3>Журнал действий</h3>';
if(!log.length){h+='<p style="color:var(--gray-500)">Действий пока нет</p>'}else{
h+='<table><tr><th>Время</th><th>Пользователь</th><th>Роль</th><th>Действие</th><th>Мероприятие</th><th>Детали</th></tr>';
log.forEach(function(l){var e=null;if(l.eid)for(var i=0;i<events.length;i++){if(events[i].id===l.eid){e=events[i];break}}
h+='<tr><td style="font-size:11px">'+new Date(l.ts).toLocaleString()+'</td><td>'+esc(l.user)+'</td><td>'+(l.role==="curator"?"Куратор":l.role==="admin"?"Админ":l.role==="region"?"Регион":"Филиал")+'</td><td>'+esc(l.action)+'</td><td>'+(e?e.id+". "+esc(e.t.slice(0,40))+"...":"—")+'</td><td style="font-size:12px">'+esc(l.detail)+'</td></tr>';
});
h+='</table>'}
h+='</div>';
document.getElementById("tab-journal").innerHTML=h;
}
document.querySelectorAll(".tab-btn").forEach(function(b){b.addEventListener("click",function(){switchTab(this.dataset.tab)})});
document.getElementById("editModalOverlay").addEventListener("click",function(e){if(e.target===this)closeEM()});
document.addEventListener("keydown",function(e){if(e.key==="Escape"){closeEM();document.getElementById("notifDrop").classList.remove("open")}});