Дашборд с Chart.js: столбчатые и линейные графики

This commit is contained in:
Dauren777 2026-06-04 12:14:19 +00:00
parent 82d0fe1161
commit 45b182c87b

View File

@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ПАБ — Система</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#0F1218;background:#F2F4F7;min-height:100vh}
@ -133,8 +134,18 @@ function rDB(){
var c=document.getElementById("dbc");if(!c)return;
var a=getA(),all=allU();var t=a.length,sf=a.filter(function(x){return x.overallSafe}).length,wd=a.filter(function(x){return !x.overallSafe}).length,tv=a.reduce(function(s,x){return s+(x.totalViolations||0)},0);
var ot=0,bh=0,ov=0;for(var k in all){if(k==="admin")continue;var u=all[k];var q=getUserQuota(u);if(!q.p)continue;var p=gp(q.p);var d=a.filter(function(x){return x.createdBy===k&&new Date(x.date)>=p.s}).length;if(d>q.c)ov++;else if(d>=q.c)ot++;else bh++}
var adb=isA()?"<div style=\"margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap\"><button class=\"btn bp\" onclick=\"downloadFullCSV()\">📥 CSV</button><button class=\"btn bo\" onclick=\"showAllUsers()\">👥 Пользователи</button></div>":"";
c.innerHTML=adb+"<div class=\"stats\"><div class=\"st\"><div class=\"n\">"+t+"</div><div class=\"l\">Всего аудитов</div></div><div class=\"st gr\"><div class=\"n\">"+sf+"</div><div class=\"l\">Безопасно</div></div><div class=\"st rd\"><div class=\"n\">"+wd+"</div><div class=\"l\">С нарушениями</div></div><div class=\"st rd\"><div class=\"n\">"+tv+"</div><div class=\"l\">Нарушений</div></div><div class=\"st bl\"><div class=\"n\">"+ov+"</div><div class=\"l\">Перевыполняют</div></div><div class=\"st gr\"><div class=\"n\">"+ot+"</div><div class=\"l\">Выполняют</div></div><div class=\"st rd\"><div class=\"n\">"+bh+"</div><div class=\"l\">Отстают</div></div></div>"+(t>0?"<div class=\"card\"><h3>📂 Нарушения по категориям</h3>"+CATS.map(function(cat){var cnt=a.reduce(function(s,x){var c=x.categories&&x.categories[cat.id];return s+(c?c.items.length:0)},0);return"<div style=\"display:flex;align-items:center;gap:10px;margin-bottom:6px\"><div style=\"width:160px;font-size:12px\">"+cat.title.split(". ")[1]+"</div><div style=\"flex:1;height:10px;background:#E2E6EB;border-radius:5px;overflow:hidden\"><div style=\"height:100%;background:#E63946;border-radius:5px;width:"+Math.min(100,cnt*10)+"%\"></div></div><div style=\"font-size:12px;font-weight:700;width:30px\">"+cnt+"</div></div>"}).join("")+"</div>":"");
var adb=isA()?"<div style=\"margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap\"><button class=\"btn bp\" onclick=\"downloadFullCSV()\">📥 CSV данные</button><button class=\"btn bo\" onclick=\"downloadSummaryHTML()\">📊 HTML отчёт</button><button class=\"btn bo\" onclick=\"showAllUsers()\">👥 Пользователи</button></div>":"";
c.innerHTML=adb+"<div class=\"stats\"><div class=\"st\"><div class=\"n\">"+t+"</div><div class=\"l\">Всего аудитов</div></div><div class=\"st gr\"><div class=\"n\">"+sf+"</div><div class=\"l\">Безопасно</div></div><div class=\"st rd\"><div class=\"n\">"+wd+"</div><div class=\"l\">С нарушениями</div></div><div class=\"st rd\"><div class=\"n\">"+tv+"</div><div class=\"l\">Нарушений</div></div><div class=\"st bl\"><div class=\"n\">"+ov+"</div><div class=\"l\">Перевыполняют</div></div><div class=\"st gr\"><div class=\"n\">"+ot+"</div><div class=\"l\">Выполняют</div></div><div class=\"st rd\"><div class=\"n\">"+bh+"</div><div class=\"l\">Отстают</div></div></div>"+
"<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px\"><div class=\"card\"><h3>📂 Нарушения по категориям</h3><canvas id=\"ch1\"></canvas></div><div class=\"card\"><h3>📅 Динамика по датам</h3><canvas id=\"ch2\"></canvas></div></div>"+
"<div class=\"card\"><h3>🔝 Топ-10 нарушений</h3><canvas id=\"ch3\"></canvas></div>";
setTimeout(function(){
if(window._charts){for(var ck in window._charts)try{window._charts[ck].destroy()}catch(e){}}window._charts={};
var ctx1=document.getElementById("ch1");if(ctx1)window._charts.cat=new Chart(ctx1,{type:"bar",data:{labels:CATS.map(function(c){return c.title.split(". ")[1]}),datasets:[{label:"Нарушений",data:CATS.map(function(cat){return a.reduce(function(s,x){var c2=x.categories&&x.categories[cat.id];return s+(c2?c2.items.length:0)},0)}),backgroundColor:"#E63946",borderRadius:4}]},options:{responsive:true,plugins:{legend:{display:false}}}});
var dates={};a.forEach(function(x){if(!dates[x.date])dates[x.date]=0;dates[x.date]+=(x.totalViolations||0)});var sd=Object.keys(dates).sort();
var ctx2=document.getElementById("ch2");if(ctx2)window._charts.tl=new Chart(ctx2,{type:"line",data:{labels:sd,datasets:[{label:"Нарушений",data:sd.map(function(d){return dates[d]}),borderColor:"#E63946",tension:0.3,pointRadius:4}]},options:{responsive:true,plugins:{legend:{display:false}}}});
var ic={};a.forEach(function(x){if(x.categories){Object.values(x.categories).forEach(function(cat){if(cat.items)cat.items.forEach(function(it){ic[it.item]=(ic[it.item]||0)+1})})}});var ti=Object.entries(ic).sort(function(a,b){return b[1]-a[1]}).slice(0,10);
var ctx3=document.getElementById("ch3");if(ctx3)window._charts.top=new Chart(ctx3,{type:"bar",data:{labels:ti.map(function(i){return i[0]}),datasets:[{label:"Раз",data:ti.map(function(i){return i[1]}),backgroundColor:["#E63946","#E76F51","#F4A261","#E9C46A","#2A9D8F","#264653","#00B4D8","#0077B6","#023E8A","#6C757D"],borderRadius:4}]},options:{indexAxis:"y",responsive:true,plugins:{legend:{display:false}}}});
},300);
}
function rVL(){var c=document.getElementById("vlc");if(!c)return;var a=getA(),td=new Date().toISOString().split("T")[0],av=[];a.forEach(function(x){if(!x.violations)return;x.violations.forEach(function(v){var dd=v.date||"",dn=v.done&&v.done.trim();var st="pending";if(dn)st="done";else if(dd&&dd<td)st="overdue";av.push({nc:v.nc,ex:v.executor,tp:v.type,ms:v.measure,rs:v.responsible,dt:dd,dn:v.done||"",st:st,ad:x.date,an:x.number||"—"})})});c.innerHTML="<div class=\"stats\"><div class=\"st\"><div class=\"n\">"+av.length+"</div><div class=\"l\">Всего</div></div><div class=\"st gr\"><div class=\"n\">"+av.filter(function(v){return v.st==="done"}).length+"</div><div class=\"l\">Устранено</div></div><div class=\"st rd\"><div class=\"n\">"+av.filter(function(v){return v.st==="overdue"}).length+"</div><div class=\"l\">Просрочено</div></div><div class=\"st\"><div class=\"n\" style=\"color:#E76F51\">"+av.filter(function(v){return v.st==="pending"}).length+"</div><div class=\"l\">В работе</div></div></div>"+(av.length>0?"<table><thead><tr><th></th><th>Несоответствие</th><th>Аудит</th><th>Исполнитель</th><th>Меры</th><th>Срок</th><th>Статус</th></tr></thead><tbody>"+av.map(function(v,i){var sc=v.st==="done"?"bs":v.st==="overdue"?"bd2":"bw";var sl=v.st==="done"?"Устранено":v.st==="overdue"?"Просрочено":"В работе";return"<tr><td>"+(i+1)+"</td><td>"+v.nc+"</td><td>"+v.ad+"</td><td>"+v.ex+"</td><td>"+(v.ms||"—")+"</td><td>"+(v.dt||"—")+"</td><td><span class=\"badge "+sc+"\">"+sl+"</span></td></tr>"}).join("")+"</tbody></table>":"<p style=\"color:#5B6573;padding:20px\">Несоответствий не найдено</p>")}
@ -163,6 +174,7 @@ function editA(id){if(!isA()){alert("Только админ");return}alert("Р
function delA(id){if(!isA()){alert("Только админ");return}if(!confirm("Удалить?"))return;saveA(getA().filter(function(a){return a.id!==id}));rHS()}
function downloadFullCSV(){var a=getA();if(a.length===0){alert("Нет данных");return}var all=allU(),h="Бланк №;Дата;Место;Наблюдатель;Филиал;Регион;Статус;Нарушений",rs=a.map(function(x){var u=all[x.createdBy]||{};return(x.number||"")+";"+x.date+";"+x.location+";"+x.observer+";"+(u.branch||"")+";"+(u.region||"")+";"+(x.overallSafe?"Безопасно":"Нарушения")+";"+(x.totalViolations||0)}),csv="\uFEFF"+h+"\n"+rs.join("\n"),bl=new Blob([csv],{type:"text/csv"}),ur=URL.createObjectURL(bl),dl=document.createElement("a");dl.href=ur;dl.download="pab-full.csv";dl.click();URL.revokeObjectURL(ur)}
function showAllUsers(){if(!isA())return;var all=allU(),h="<h2>👥 Пользователи</h2><table style=\"width:100%;border-collapse:collapse;font-size:13px\"><tr style=\"background:#0F1218;color:#fff\"><th>Логин</th><th>ФИО</th><th>Должность</th><th>Филиал</th><th>Регион</th><th>Город</th></tr>";for(var k in all){var u=all[k];h+="<tr><td>"+k+(k==="admin"?" ⭐":"")+"</td><td>"+u.name+"</td><td>"+u.role+"</td><td>"+(u.branch||"—")+"</td><td>"+(u.region||"—")+"</td><td>"+(u.city||"—")+"</td></tr>"}h+="</table>";var w=window.open("","_blank","width=800,height=500");w.document.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Пользователи</title><style>body{font:14px/1.5 Arial;max-width:800px;margin:20px auto;padding:20px}</style></head><body>"+h+"</body></html>");w.document.close()}
function downloadSummaryHTML(){var a=getA(),all=allU(),t=a.length;var w=window.open("","_blank","width=800,height=600");w.document.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Отчёт ПАБ</title><style>body{font:14px/1.5 Arial;max-width:900px;margin:20px auto;padding:20px}h1{font-size:22px}h2{font-size:16px;margin-top:20px}table{width:100%;border-collapse:collapse;font-size:12px}th{background:#0F1218;color:#fff;padding:8px}td{padding:6px 8px;border-bottom:1px solid #E2E6EB}@media print{button{display:none}}</style></head><body><button onclick=\"window.print()\" style=\"padding:8px 16px;margin-bottom:16px\">🖨️ Печать</button><h1>📊 Сводный отчёт ПАБ</h1><p>Сформирован: "+new Date().toLocaleString("ru")+" | Всего аудитов: "+t+"</p><h2>📋 Аудиты</h2><table><tr><th></th><th>Дата</th><th>Место</th><th>Наблюдатель</th><th>Статус</th><th>Нарушений</th></tr>"+a.map(function(x){return"<tr><td>"+(x.number||"—")+"</td><td>"+x.date+"</td><td>"+x.location+"</td><td>"+x.observer+"</td><td>"+(x.overallSafe?"Безопасно":"Нарушения")+"</td><td>"+(x.totalViolations||0)+"</td></tr>"}).join("")+"</table></body></html>");w.document.close()}
rHS();
</script>