v14: дашборд, аналитика, фикс загрузки, скачивание CSV-отчёта
This commit is contained in:
parent
871282be97
commit
007676595c
610
index.html
610
index.html
@ -3,23 +3,22 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>ИИ-агент ПБ — Казахтелеком</title>
|
||||
<title>ИИ-агент мониторинга ПБ — АО «Казахтелеком»</title>
|
||||
<style>
|
||||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--gray-200:#E5E7EB;--green:#10B981;--red:#EF4444;--amber:#F59E0B;--blue:#3B82F6}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--gray-100);min-height:100vh}
|
||||
input,select,textarea,button{font:inherit}
|
||||
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:12px 24px;border-radius:8px;font-weight:700;font-size:15px;border:none;cursor:pointer}
|
||||
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:12px 24px;border-radius:8px;font-weight:700;font-size:15px;border:none;cursor:pointer;text-decoration:none}
|
||||
.btn:hover{background:#1be5ff}
|
||||
.btn-sm{padding:6px 12px;font-size:13px}
|
||||
.btn-outline{background:transparent;border:2px solid var(--ink);color:var(--ink)}
|
||||
.btn-sm{padding:7px 14px;font-size:13px}
|
||||
.btn-outline{background:transparent;border:2px solid var(--ink);color:var(--ink)}.btn-outline:hover{background:var(--ink);color:var(--white)}
|
||||
.badge{display:inline-block;padding:4px 10px;border-radius:100px;font-size:12px;font-weight:600}
|
||||
.badge.green{background:#D1FAE5;color:#065F46}.badge.amber{background:#FEF3C7;color:#92400E}.badge.red{background:#FEE2E2;color:#991B1B}.badge.blue{background:#DBEAFE;color:#1E40AF}.badge.gray{background:var(--gray-100);color:var(--gray-700)}
|
||||
.panel{background:var(--white);border-radius:12px;border:1px solid var(--gray-200);padding:24px;margin-bottom:20px}
|
||||
.panel h3{font-size:17px;font-weight:700;margin-bottom:12px}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{padding:10px 14px;text-align:left;font-size:13px}
|
||||
th{font-weight:600;color:var(--gray-500);font-size:11px;text-transform:uppercase;letter-spacing:.5px;border-bottom:2px solid var(--gray-200)}
|
||||
table{width:100%;border-collapse:collapse}th,td{padding:10px 14px;text-align:left;font-size:13px}
|
||||
th{font-weight:600;color:var(--gray-500);font-size:11px;text-transform:uppercase;border-bottom:2px solid var(--gray-200)}
|
||||
td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)}
|
||||
.pct-bar{display:flex;align-items:center;gap:8px;font-size:13px}
|
||||
.pct-bar .track{width:80px;height:7px;background:var(--gray-200);border-radius:10px;overflow:hidden}
|
||||
@ -32,6 +31,7 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
|
||||
.login-box label{display:block;text-align:left;font-size:13px;font-weight:600;margin-bottom:6px}
|
||||
.login-box input[type=email],.login-box input[type=password]{width:100%;padding:12px 16px;border:1px solid var(--gray-200);border-radius:8px;font-size:15px;margin-bottom:20px}
|
||||
.login-box .hint{font-size:12px;color:var(--gray-500);margin-top:-12px;margin-bottom:16px;text-align:left}
|
||||
.login-box .err{color:var(--red);font-size:13px;margin-bottom:12px;display:none}
|
||||
|
||||
#app{display:none}
|
||||
.topbar{background:var(--white);border-bottom:1px solid var(--gray-200);padding:0 32px;height:60px;display:flex;align-items:center;justify-content:space-between}
|
||||
@ -42,20 +42,27 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
|
||||
.notif-btn .badge-count{position:absolute;top:-2px;right:-4px;background:var(--red);color:#fff;border-radius:100px;font-size:10px;padding:1px 6px;font-weight:700}
|
||||
.logout-btn{font-size:13px;color:var(--gray-500);cursor:pointer;border:none;background:none}.logout-btn:hover{color:var(--red)}
|
||||
.notif-drop{position:absolute;top:56px;right:32px;width:380px;max-width:90vw;background:var(--white);border:1px solid var(--gray-200);border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.12);z-index:300;display:none;max-height:400px;overflow-y:auto}
|
||||
.notif-drop.open{display:block}
|
||||
.notif-drop .item{padding:14px 18px;border-bottom:1px solid var(--gray-100);font-size:13px}
|
||||
.notif-drop .item .title{font-weight:600;margin-bottom:2px}
|
||||
.notif-drop .item .time{font-size:11px;color:var(--gray-500)}
|
||||
.notif-drop.open{display:block}.notif-drop .item{padding:14px 18px;border-bottom:1px solid var(--gray-100);font-size:13px}
|
||||
.notif-drop .item .title{font-weight:600;margin-bottom:2px}.notif-drop .item .time{font-size:11px;color:var(--gray-500)}
|
||||
.notif-drop .empty{padding:24px;text-align:center;color:var(--gray-500)}
|
||||
|
||||
.tabs{display:flex;gap:0;margin-bottom:0;background:var(--white);border-radius:12px 12px 0 0;overflow:hidden}
|
||||
.tab-btn{padding:12px 24px;border:none;background:transparent;cursor:pointer;font-size:14px;font-weight:600;color:var(--gray-500);border-bottom:3px solid transparent}
|
||||
.tab-btn.active{color:var(--ink);border-bottom-color:var(--cyan)}
|
||||
.tab-content{display:none}.tab-content.active{display:block}
|
||||
|
||||
.main{padding:24px 32px;max-width:1400px}
|
||||
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}
|
||||
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:16px;margin-bottom:24px}
|
||||
.stat-card{background:var(--white);border-radius:12px;padding:20px 24px;border:1px solid var(--gray-200)}
|
||||
.stat-card .lbl{font-size:13px;color:var(--gray-500)}.stat-card .num{font-size:28px;font-weight:800;line-height:1.2}
|
||||
.stat-card.red .num{color:var(--red)}.stat-card.green .num{color:var(--green)}.stat-card.amber .num{color:var(--amber)}.stat-card.blue .num{color:var(--blue)}
|
||||
.filters{display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center}
|
||||
.filters select,.filters input{padding:10px 14px;border:1px solid var(--gray-200);border-radius:8px;font-size:14px;background:var(--white);min-width:160px}
|
||||
.filters input{min-width:260px}
|
||||
.chart-stub{height:180px;background:var(--gray-100);border-radius:8px;display:flex;align-items:flex-end;gap:8px;padding:16px 16px 8px}
|
||||
.chart-stub .bar{flex:1;background:var(--cyan);border-radius:4px 4px 0 0;min-height:4px}
|
||||
.chart-labels{display:flex;gap:8px;padding:8px 16px 0;font-size:11px;color:var(--gray-500);text-align:center}
|
||||
.chart-labels span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:200;display:none;align-items:center;justify-content:center}
|
||||
.modal-overlay.open{display:flex}
|
||||
@ -67,10 +74,8 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
|
||||
.modal .field input,.modal .field select,.modal .field textarea{width:100%;padding:10px 14px;border:1px solid var(--gray-200);border-radius:8px;font-size:14px}
|
||||
.modal .field textarea{min-height:70px;resize:vertical}
|
||||
.modal .meta-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
|
||||
.modal .meta-row .fld{font-size:13px;color:var(--gray-500)}
|
||||
.modal .meta-row .fld strong{display:block;font-size:14px;color:var(--ink);margin-top:2px}
|
||||
.ai-block{background:var(--cyan-50);border-radius:8px;padding:14px;margin:16px 0;font-size:14px}
|
||||
.ai-block h4{font-size:14px;margin-bottom:4px}
|
||||
.modal .meta-row .fld{font-size:13px;color:var(--gray-500)}.modal .meta-row .fld strong{display:block;font-size:14px;color:var(--ink);margin-top:2px}
|
||||
.ai-block{background:var(--cyan-50);border-radius:8px;padding:14px;margin:16px 0;font-size:14px}.ai-block h4{font-size:14px;margin-bottom:4px}
|
||||
.history-item{display:flex;gap:8px;font-size:12px;padding:3px 0;color:var(--gray-500);align-items:baseline}
|
||||
.history-item .dot{width:7px;height:7px;border-radius:50%;background:var(--cyan);flex-shrink:0;margin-top:5px}
|
||||
.sub-items{margin:12px 0}
|
||||
@ -79,26 +84,17 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
|
||||
.sub-item .sub-text{flex:1;color:var(--gray-500);font-size:12px;line-height:1.3}
|
||||
.sub-item input[type=checkbox]{width:18px;height:18px;cursor:pointer;accent-color:var(--cyan)}
|
||||
.month-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
|
||||
.month-tab{padding:6px 14px;border:1px solid var(--gray-200);border-radius:100px;font-size:13px;font-weight:600;cursor:pointer;background:var(--white);color:var(--gray-500);transition:.15s}
|
||||
.month-tab:hover{border-color:var(--cyan)}
|
||||
.month-tab.active{background:var(--cyan);color:var(--ink);border-color:var(--cyan)}
|
||||
.month-tab{padding:6px 14px;border:1px solid var(--gray-200);border-radius:100px;font-size:13px;font-weight:600;cursor:pointer;background:var(--white);color:var(--gray-500)}.month-tab:hover{border-color:var(--cyan)}.month-tab.active{background:var(--cyan);color:var(--ink)}
|
||||
.file-row{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--gray-100);border-radius:8px;margin-bottom:6px;font-size:13px}
|
||||
.file-row .file-info{flex:1;min-width:0}
|
||||
.file-row .file-name{font-weight:600;cursor:pointer;color:var(--ink)}.file-row .file-name:hover{color:var(--cyan)}
|
||||
.file-row .file-desc{font-size:11px;color:var(--gray-500)}
|
||||
.file-row .file-meta{font-size:11px;color:var(--gray-500);white-space:nowrap}
|
||||
.file-row .file-info{flex:1;min-width:0}.file-row .file-name{font-weight:600;cursor:pointer;color:var(--ink)}.file-row .file-name:hover{color:var(--cyan)}
|
||||
.file-row .file-desc{font-size:11px;color:var(--gray-500)}.file-row .file-meta{font-size:11px;color:var(--gray-500);white-space:nowrap}
|
||||
.file-row .file-del{background:none;border:none;color:var(--red);cursor:pointer;font-size:16px;padding:2px}
|
||||
.upload-row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;padding:12px;border:2px dashed var(--gray-200);border-radius:8px}
|
||||
.upload-row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;padding:12px;border:2px dashed var(--gray-200);border-radius:8px;margin-top:8px}
|
||||
.upload-row input[type=text]{flex:1;min-width:150px;padding:8px 12px;border:1px solid var(--gray-200);border-radius:6px;font-size:13px}
|
||||
.upload-row input[type=file]{font-size:12px;max-width:220px}
|
||||
|
||||
@media(max-width:768px){
|
||||
.main{padding:16px}.topbar{padding:0 16px}
|
||||
.modal .meta-row{grid-template-columns:1fr}
|
||||
.stats-row{grid-template-columns:1fr 1fr}
|
||||
.notif-drop{right:8px;width:calc(100vw - 16px)}
|
||||
.modal{padding:20px}
|
||||
}
|
||||
.report-bar{display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-wrap:wrap}
|
||||
.report-bar select,.report-bar input{padding:8px 14px;border:1px solid var(--gray-200);border-radius:8px;font-size:13px}
|
||||
@media(max-width:768px){.main{padding:16px}.topbar{padding:0 16px}.modal{padding:20px}.stats-row{grid-template-columns:1fr 1fr}.notif-drop{right:8px;width:calc(100vw-16px)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -112,7 +108,7 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
|
||||
<p class="hint">admin@telecom.kz / ahmetov@telecom.kz / serikov@telecom.kz — пароль любой</p>
|
||||
<label>Пароль</label>
|
||||
<input type="password" id="loginPass" placeholder="••••••••" required>
|
||||
<p style="color:var(--red);font-size:13px;margin-bottom:12px;display:none" id="loginErr">Неверная почта или пароль</p>
|
||||
<p class="err" id="loginErr">Неверная почта или пароль</p>
|
||||
<button type="submit" class="btn" style="width:100%;margin-top:8px">Войти</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -129,415 +125,221 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
|
||||
<span class="logout-btn" onclick="doLogout()">Выйти</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main" id="mainContent"></div>
|
||||
<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="analytics">📈 Аналитика</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>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="editModalOverlay"><div class="modal" id="editModalContent"></div></div>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
var sections=["I. Люди","II. Оборудование","III. Аварии и ЧС","IV. Информ. работа","V. ИИ и цифровизация"];
|
||||
var branches=["Дирекция производственной безопасности","Дивизион «Сеть»","Дивизион по корпоративному бизнесу","Дивизион по розничному бизнесу","Сервисная фабрика","Дирекция «Телеком Комплект»","Корпоративный университет","Дирекция управления проектами","Дивизион цифрового бизнеса"];
|
||||
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=["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"];
|
||||
function M(idx){return monthNames[parseInt(months[idx].split("-")[1])-1]+" "+months[idx].split("-")[0]}
|
||||
function esc(s){return s.replace(/&/g,"&").replace(/</g,"<").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 sections = [
|
||||
"I. Люди. Повышение культуры безопасности",
|
||||
"II. Безопасность при эксплуатации оборудования",
|
||||
"III. Предупреждение и готовность к ликвидации аварий и ЧС",
|
||||
"IV. Информационно-разъяснительная работа",
|
||||
"V. Внедрение ИИ и цифровизации"
|
||||
];
|
||||
var branches = [
|
||||
"Дирекция производственной безопасности",
|
||||
"Объединение «Дивизион «Сеть»»",
|
||||
"Дивизион по корпоративному бизнесу",
|
||||
"Дивизион по розничному бизнесу",
|
||||
"Сервисная фабрика",
|
||||
"Дирекция «Телеком Комплект»",
|
||||
"Корпоративный университет",
|
||||
"Дирекция управления проектами",
|
||||
"Дивизион цифрового бизнеса"
|
||||
];
|
||||
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 = ["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"];
|
||||
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 curUser=null,curMonth=5;
|
||||
|
||||
function M(idx) { var parts = months[idx].split("-"); return monthNames[parseInt(parts[1])-1] + " " + parts[0]; }
|
||||
|
||||
var users = {
|
||||
"dpp@telecom.kz": {name: "Директор ДПБ", branch: 0, role: "director"},
|
||||
"ahmetov@telecom.kz": {name: "Ахметов К.Т.", branch: 6, role: "responsible"},
|
||||
"serikov@telecom.kz": {name: "Сериков А.М.", branch: 1, role: "responsible"},
|
||||
"nurlanov@telecom.kz": {name: "Нурланов Д.С.", branch: 8, role: "responsible"},
|
||||
"aliev@telecom.kz": {name: "Алиев Г.С.", branch: 4, role: "responsible"},
|
||||
"tulegenov@telecom.kz": {name: "Тулегенов Е.А.", branch: 2, role: "responsible"},
|
||||
"saparov@telecom.kz": {name: "Сапаров А.Д.", branch: 3, role: "responsible"},
|
||||
"maratov@telecom.kz": {name: "Маратов Ж.К.", branch: 5, role: "responsible"},
|
||||
"iskakov@telecom.kz": {name: "Искаков Р.Н.", branch: 7, role: "responsible"},
|
||||
"admin@telecom.kz": {name: "Администратор", branch: 0, role: "admin"}
|
||||
};
|
||||
|
||||
var currentUser = null;
|
||||
var editMonthIdx = 5;
|
||||
|
||||
// Load events from localStorage or use defaults
|
||||
var eventsStr = localStorage.getItem("samruk_events");
|
||||
var events = eventsStr ? JSON.parse(eventsStr) : null;
|
||||
|
||||
// inline events data (35 items)
|
||||
function getDefaultEvents() { return [
|
||||
{id:1,sec:0,b:6,s:"warn",p:45,due:"31.12.2026",done:"\u2014",dname:"Протоколы обучения / Электронная ведомость",
|
||||
r:"Генеральный директор КУ, Генеральные директора филиалов и ДАО",
|
||||
t:"Продолжить проведение обучения и повышения квалификации руководителей и работников компании в соответствии с лучшими международными практиками, ориентированными на специфику условий труда, работы повышенной опасности и требований промышленной безопасности, а также развитие культуры безопасности, включая обучение производственного персонала по курсу \u00abКультура безопасного труда\u00bb, в том числе с применением VR, AR \u2013 технологий и цифровых симуляторов аварийных ситуаций по различным направлениям производственной безопасности (с правом выдачи сертификатов).",
|
||||
ai:"Обучение ведётся по графику. Охвачено 45% персонала. VR-тренажёры развёрнуты в 3 филиалах.",
|
||||
h:["15.01 — Мероприятие создано","01.03 — Запущено обучение","15.05 — VR-симуляторы установлены"]},
|
||||
{id:2,sec:0,b:0,s:"done",p:100,due:"31.03.2026",done:"28.03.2026",dname:"Отчёт о проведённом анализе / Утверждённый ВНД",
|
||||
r:"Директор ДПБ, Генеральный директор ДИТ, Генеральные директора филиалов и ДАО",
|
||||
t:"Провести анализ, в том числе с использованием аналитических платформ (Microsoft Teams, Power BI, Tableau, Qlik и др.), и в случае необходимости, осуществить пересмотр внутренних нормативных документов филиалов/ДАО Общества в соответствии со \u00abСтратегией развития производственной безопасности АО \u00abСамрук-Қазына\u00bb на 2024-2028 гг.\u00bb, включая установку значений ключевых показателей производственной безопасности.",
|
||||
ai:"Анализ завершён в срок. ВНД пересмотрены. Ключевые показатели ПБ установлены.",
|
||||
h:["10.01 — Мероприятие создано","15.02 — Проведён анализ","28.03 — Отчёт утверждён"]},
|
||||
{id:3,sec:0,b:0,s:"warn",p:50,due:"31.12.2026",done:"\u2014",dname:"Протоколы совещаний (a, b, c)",
|
||||
r:"Главный административный директор, Директор ДПБ / Генеральные директора филиалов и ДАО",
|
||||
t:"Организовывать тематические совещания по вопросам производственной безопасности.",
|
||||
ai:"Проведено 2 квартальных совещания. Ежемесячные совещания — выполняется.",
|
||||
h:["10.01 — Мероприятие создано","15.02 — Совещание Q1","15.05 — Совещание Q2"],
|
||||
sub:[{l:"a",t:"Руководство Общества с филиалами/ДАО, не менее 1 раза в квартал, с личным мониторингом показателей эффективности ПБ и статуса исполнения Плана"},
|
||||
{l:"b",t:"Руководство филиалов/ДАО со структурными подразделениями, не менее 1 раза в месяц"},
|
||||
{l:"c",t:"Руководство региональных подразделений/филиалов/ДАО с подрядными организациями, не менее 1 раза в квартал"}]},
|
||||
{id:4,sec:0,b:6,s:"warn",p:55,due:"31.12.2026",done:"\u2014",dname:"Отчёты о проделанной работе / Тесты",
|
||||
r:"Генеральные директора филиалов и ДАО",
|
||||
t:"Продолжить практику проверки знаний в формате тестирования после проведения инструктажей по охране труда в филиалах/ДАО Общества.",
|
||||
ai:"Тестирование внедрено в 6 филиалах. Средний результат — 82%.",h:["01.02 — Создано","01.04 — Внедрено","01.06 — Отчёт"]},
|
||||
{id:5,sec:0,b:0,s:"done",p:100,due:"31.03.2026",done:"25.03.2026",dname:"Информация о нематериальном поощрении",
|
||||
r:"Директор ДПБ, Генеральные директора филиалов и ДАО",
|
||||
t:"Рассмотреть возможность нематериального поощрения филиалов и ДАО Общества, демонстрирующих устойчивое снижение количества несчастных случаев, пожаров и аварий по итогам нескольких и более лет.",
|
||||
ai:"Положение утверждено. Определены 3 филиала-лидера.",h:["15.01 — Проект","01.03 — Согласование","25.03 — Утверждено"]}
|
||||
]; } // end getDefaultEvents — real data loaded from data.json on server
|
||||
|
||||
// Load events: try data.json first, fallback to inline defaults
|
||||
(function loadEvents() {
|
||||
if (events) return; // already loaded from localStorage
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "data.json", true);
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
try { events = JSON.parse(xhr.responseText); } catch(e) { events = getDefaultEvents(); }
|
||||
} else { events = getDefaultEvents(); }
|
||||
localStorage.setItem("samruk_events", JSON.stringify(events));
|
||||
if (currentUser) renderMain();
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
events = getDefaultEvents();
|
||||
localStorage.setItem("samruk_events", JSON.stringify(events));
|
||||
if (currentUser) renderMain();
|
||||
};
|
||||
xhr.send();
|
||||
// Load events
|
||||
var events=null;
|
||||
(function(){
|
||||
var s=localStorage.getItem("samruk_ev");if(s){events=JSON.parse(s);return}
|
||||
var x=new XMLHttpRequest();x.open("GET","data.json",true);
|
||||
x.onload=function(){if(x.status===200){try{events=JSON.parse(x.responseText)}catch(e){}}if(!events)events=[];localStorage.setItem("samruk_ev",JSON.stringify(events));if(curUser)renderAll()};
|
||||
x.onerror=function(){events=[];localStorage.setItem("samruk_ev",JSON.stringify(events));if(curUser)renderAll()};
|
||||
x.send();
|
||||
})();
|
||||
function doLogin(e) {
|
||||
e.preventDefault();
|
||||
var email = document.getElementById("loginEmail").value.trim().toLowerCase();
|
||||
var pass = document.getElementById("loginPass").value;
|
||||
if (users[email] && pass.length >= 1) {
|
||||
currentUser = {email: email, name: users[email].name, branch: users[email].branch, role: users[email].role};
|
||||
localStorage.setItem("samruk_user", JSON.stringify(currentUser));
|
||||
showApp();
|
||||
} else {
|
||||
document.getElementById("loginErr").style.display = "block";
|
||||
}
|
||||
return false;
|
||||
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})}
|
||||
|
||||
// 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()}
|
||||
|
||||
// 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 toggleNotif(){notifsUpdate();document.getElementById("notifDrop").classList.toggle("open")}
|
||||
|
||||
// Tabs
|
||||
function switchTab(name){
|
||||
document.querySelectorAll(".tab-btn").forEach(function(b){b.classList.remove("active")});
|
||||
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();
|
||||
}
|
||||
|
||||
function doLogout() {
|
||||
localStorage.removeItem("samruk_user");
|
||||
currentUser = null;
|
||||
document.getElementById("loginScreen").style.display = "flex";
|
||||
document.getElementById("app").style.display = "none";
|
||||
}
|
||||
// ===== DASHBOARD =====
|
||||
function renderDashboard(){
|
||||
var my=getMy(),done=my.filter(function(e){return e.s==="done"}).length,late=my.filter(function(e){return e.s==="late"}).length,warn=my.filter(function(e){return e.s==="warn"}).length,wait=my.filter(function(e){return e.s==="wait"}).length,donePct=my.length?Math.round(done/my.length*100):0;
|
||||
var h='<div class="stats-row">';
|
||||
h+='<div class="stat-card"><div class="lbl">Мероприятий ('+(curUser.role==="admin"?"все":"дивизион")+')</div><div class="num">'+my.length+'</div></div>';
|
||||
h+='<div class="stat-card green"><div class="lbl">Исполнено</div><div class="num">'+done+'</div></div>';
|
||||
h+='<div class="stat-card amber"><div class="lbl">На контроле</div><div class="num">'+warn+'</div></div>';
|
||||
h+='<div class="stat-card red"><div class="lbl">Просрочено</div><div class="num">'+late+'</div></div>';
|
||||
h+='<div class="stat-card blue"><div class="lbl">В процессе</div><div class="num">'+wait+'</div></div>';
|
||||
h+='</div>';
|
||||
|
||||
function showApp() {
|
||||
document.getElementById("loginScreen").style.display = "none";
|
||||
document.getElementById("app").style.display = "block";
|
||||
document.getElementById("userLabel").innerHTML = "<strong>" + currentUser.name + "</strong> · " + branches[currentUser.branch];
|
||||
renderMain();
|
||||
}
|
||||
|
||||
// === MAIN VIEW ===
|
||||
function renderMain() {
|
||||
var myEvs = getMyEvents();
|
||||
var done = 0, late = 0, warn = 0, wait = 0;
|
||||
myEvs.forEach(function(e) {
|
||||
if (e.s === "done") done++;
|
||||
else if (e.s === "late") late++;
|
||||
else if (e.s === "warn") warn++;
|
||||
else wait++;
|
||||
// Section breakdown
|
||||
h+='<div class="panel"><h3>Исполнение по разделам</h3>';
|
||||
var secCounts=[0,0,0,0,0],secDone=[0,0,0,0,0];
|
||||
my.forEach(function(e){secCounts[e.sec]++;if(e.s==="done")secDone[e.sec]++});
|
||||
sections.forEach(function(s,i){var pct=secCounts[i]?Math.round(secDone[i]/secCounts[i]*100):0;
|
||||
h+='<div class="pct-bar" style="margin-bottom:6px"><span style="width:160px;font-size:13px;font-weight:600">'+s+'</span><div class="track" style="flex:1"><div class="fill" style="width:'+pct+'%;background:var(--cyan)"></div></div><span style="font-weight:700;font-size:13px">'+pct+'%</span></div>';
|
||||
});
|
||||
h+='</div>';
|
||||
|
||||
var html = "";
|
||||
html += '<div class="stats-row">';
|
||||
html += '<div class="stat-card"><div class="lbl">Мои мероприятия</div><div class="num">'+myEvs.length+'</div></div>';
|
||||
html += '<div class="stat-card green"><div class="lbl">Исполнено</div><div class="num">'+done+'</div></div>';
|
||||
html += '<div class="stat-card amber"><div class="lbl">На контроле</div><div class="num">'+warn+'</div></div>';
|
||||
html += '<div class="stat-card red"><div class="lbl">Просрочено</div><div class="num">'+late+'</div></div>';
|
||||
html += '<div class="stat-card blue"><div class="lbl">В процессе</div><div class="num">'+wait+'</div></div>';
|
||||
html += '</div>';
|
||||
// Progress chart
|
||||
h+='<div class="panel"><h3>Динамика по кварталам</h3><div class="chart-stub">';
|
||||
h+='<div class="bar" style="height:50%;background:var(--green)"></div><div class="bar" style="height:65%;background:var(--green)"></div><div class="bar" style="height:75%"></div><div class="bar" style="height:'+donePct+'%"></div>';
|
||||
h+='</div><div class="chart-labels"><span>Q1 (факт)</span><span>Q2 (прогноз)</span><span>Q3 (план)</span><span>Q4 (цель)</span></div></div>';
|
||||
|
||||
html += '<div class="panel"><h3>Реестр мероприятий</h3>';
|
||||
html += '<table><tr><th>№</th><th>Мероприятие</th><th>Раздел</th><th>Срок</th><th>Прогресс</th><th>Статус</th><th></th></tr>';
|
||||
myEvs.forEach(function(e) {
|
||||
var pctColor = e.p >= 80 ? "var(--green)" : (e.p >= 40 ? "var(--amber)" : "var(--red)");
|
||||
var sClass = {done:"green",warn:"amber",late:"red",wait:"gray"}[e.s];
|
||||
html += '<tr><td>'+e.id+'</td>';
|
||||
html += '<td style="font-size:12px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+escHtml(e.t)+'">'+escHtml(e.t)+'</td>';
|
||||
html += '<td><span class="badge blue">'+["I","II","III","IV","V"][e.sec]+'</span></td>';
|
||||
html += '<td style="font-size:12px">'+e.due+'</td>';
|
||||
html += '<td><div class="pct-bar"><div class="track"><div class="fill" style="width:'+e.p+'%;background:'+pctColor+'"></div></div>'+e.p+'%</div></td>';
|
||||
html += '<td><span class="badge '+sClass+'">'+statusMap[e.s]+'</span></td>';
|
||||
html += '<td><button class="btn btn-sm" onclick="openEdit('+e.id+')">📝</button></td></tr>';
|
||||
});
|
||||
html += '</table></div>';
|
||||
// Report download
|
||||
h+='<div class="panel"><h3>📥 Скачать сводный отчёт</h3><div class="report-bar">';
|
||||
h+='<select id="rptFrom">'+months.map(function(m,i){return'<option value="'+i+'">'+M(i)+'</option>'}).join("")+'</select>';
|
||||
h+='<span>—</span><select id="rptTo">'+months.map(function(m,i){return'<option value="'+i+'"'+(i===11?" selected":"")+'>'+M(i)+'</option>'}).join("")+'</select>';
|
||||
h+='<button class="btn btn-sm" onclick="downloadReport()">Скачать CSV</button></div></div>';
|
||||
|
||||
document.getElementById("mainContent").innerHTML = html;
|
||||
updateNotifs();
|
||||
document.getElementById("tab-dashboard").innerHTML=h;
|
||||
}
|
||||
|
||||
function getMyEvents() {
|
||||
if (!currentUser || !events) return [];
|
||||
if (currentUser.role === "admin" || currentUser.role === "director") return events;
|
||||
return events.filter(function(e) { return e.b === currentUser.branch; });
|
||||
function downloadReport(){
|
||||
var from=parseInt(document.getElementById("rptFrom").value),to=parseInt(document.getElementById("rptTo").value);
|
||||
var my=getMy(),csv="№;Мероприятие;Раздел;Дивизион;Статус;Прогресс;Срок;Факт;Отчёт (текст)\n";
|
||||
my.forEach(function(e){
|
||||
var rep="";for(var i=from;i<=to;i++){var m=months[i],d=getMD(e.id);if(d[m]&&d[m].report)rep+=M(i)+": "+d[m].report.replace(/"/g,'""')+"; "}
|
||||
csv+=e.id+';"'+e.t.replace(/"/g,'""')+'";'+sections[e.sec]+';'+branches[e.b]+';'+statusMap[e.s]+';'+e.p+'%;'+e.due+';'+(e.done||"—")+';"'+rep+'"\n';
|
||||
});
|
||||
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()
|
||||
}
|
||||
|
||||
function escHtml(s) { return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); }
|
||||
|
||||
// === NOTIFICATIONS ===
|
||||
function updateNotifs() {
|
||||
var myEvs = getMyEvents();
|
||||
var notifs = [];
|
||||
myEvs.forEach(function(e) {
|
||||
if (e.s === "late") notifs.push({type:"danger", msg:"Просрочено: "+e.t.slice(0,60)+"...", time: e.due});
|
||||
if (e.s === "warn" && e.p < 30) notifs.push({type:"warn", msg:"Низкий прогресс ("+e.p+"%): "+e.t.slice(0,50)+"...", time:"Сейчас"});
|
||||
});
|
||||
var el = document.getElementById("notifDrop");
|
||||
var cnt = document.getElementById("notifCount");
|
||||
cnt.textContent = notifs.length;
|
||||
cnt.style.display = notifs.length ? "inline-block" : "none";
|
||||
el.innerHTML = notifs.length
|
||||
? notifs.map(function(n) { return '<div class="item"><div class="title">'+(n.type==="danger"?"🔴":"🟡")+' '+escHtml(n.msg)+'</div><div class="time">'+n.time+'</div></div>'; }).join("")
|
||||
: '<div class="empty">Новых уведомлений нет</div>';
|
||||
// ===== MY EVENTS =====
|
||||
function renderMyEvents(){
|
||||
var my=getMy(),h='<div class="panel" style="border-radius:0 0 12px 12px">';
|
||||
h+='<div class="filters"><select id="mySF" onchange="renderMyEvents()"><option value="">Все</option><option value="done">Исполнено</option><option value="warn">На контроле</option><option value="late">Просрочено</option><option value="wait">В процессе</option></select></div>';
|
||||
var sf=document.getElementById("mySF");sf=sf?sf.value:"";
|
||||
var list=my;if(sf)list=list.filter(function(e){return e.s===sf});
|
||||
h+='<table><tr><th>№</th><th>Мероприятие</th><th>Раздел</th><th>Срок</th><th>Прогресс</th><th>Статус</th><th></th></tr>';
|
||||
list.forEach(function(e){h+='<tr><td>'+e.id+'</td><td style="font-size:12px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+esc(e.t)+'">'+esc(e.t)+'</td><td><span class="badge blue">'+["I","II","III","IV","V"][e.sec]+'</span></td><td>'+e.due+'</td><td>'+pct(e.p)+'</td><td>'+sb(e.s)+'</td><td><button class="btn btn-sm" onclick="openEdit('+e.id+')">📝</button></td></tr>'});
|
||||
h+='</table></div>';
|
||||
document.getElementById("tab-myevents").innerHTML=h;
|
||||
}
|
||||
|
||||
function toggleNotif() {
|
||||
updateNotifs();
|
||||
document.getElementById("notifDrop").classList.toggle("open");
|
||||
// ===== ANALYTICS =====
|
||||
function renderAnalytics(){
|
||||
var h='<div class="stats-row">';
|
||||
var all=events||[],done=all.filter(function(e){return e.s==="done"}).length,total=all.length;
|
||||
h+='<div class="stat-card"><div class="lbl">Всего</div><div class="num">'+total+'</div></div>';
|
||||
h+='<div class="stat-card green"><div class="lbl">Исполнено</div><div class="num">'+done+'</div></div>';
|
||||
h+='<div class="stat-card amber"><div class="lbl">На контроле</div><div class="num">'+all.filter(function(e){return e.s==="warn"}).length+'</div></div>';
|
||||
h+='<div class="stat-card red"><div class="lbl">Просрочено</div><div class="num">'+all.filter(function(e){return e.s==="late"}).length+'</div></div>';
|
||||
h+='</div>';
|
||||
|
||||
h+='<div class="panel"><h3>Рейтинг дивизионов</h3>';
|
||||
branches.forEach(function(b,i){var items=all.filter(function(e){return e.b===i}),d=items.filter(function(e){return e.s==="done"}).length,pct=items.length?Math.round(d/items.length*100):0;
|
||||
h+='<div class="pct-bar" style="margin-bottom:6px"><span style="width:240px;font-size:13px;font-weight:600">'+b+'</span><div class="track" style="flex:1"><div class="fill" style="width:'+pct+'%;background:'+(pct>=50?"var(--green)":pct>=25?"var(--amber)":"var(--red)")+'"></div></div><span style="font-weight:700;font-size:13px">'+pct+'% ('+d+'/'+items.length+')</span></div>';
|
||||
});
|
||||
h+='</div>';
|
||||
|
||||
h+='<div class="panel"><h3>Просроченные мероприятия</h3><table><tr><th>№</th><th>Мероприятие</th><th>Дивизион</th><th>Срок</th></tr>';
|
||||
all.filter(function(e){return e.s==="late"}).forEach(function(e){h+='<tr><td>'+e.id+'</td><td style="font-size:12px">'+esc(e.t.slice(0,80))+'...</td><td>'+branches[e.b]+'</td><td style="color:var(--red);font-weight:700">'+e.due+'</td></tr>'});
|
||||
h+='</table></div>';
|
||||
document.getElementById("tab-analytics").innerHTML=h;
|
||||
}
|
||||
|
||||
// === EDIT MODAL ===
|
||||
function openEdit(id, monthIdx) {
|
||||
if (typeof monthIdx === "number") editMonthIdx = monthIdx;
|
||||
var e = null;
|
||||
for (var i = 0; i < events.length; i++) { if (events[i].id === id) { e = events[i]; break; } }
|
||||
if (!e) return;
|
||||
// ===== EDIT MODAL =====
|
||||
function openEdit(id,mi){
|
||||
if(typeof mi==="number")curMonth=mi;
|
||||
var e=null;for(var i=0;i<events.length;i++){if(events[i].id===id){e=events[i];break}}if(!e)return;
|
||||
var ad=getMD(e.id),sc=getSC(e.id),cm=months[curMonth],cd=ad[cm]||{report:"",files:[]},cfs=cd.files||[],tf=0;
|
||||
for(var k in ad){if(ad.hasOwnProperty(k))tf+=(ad[k].files||[]).length}
|
||||
|
||||
var allData = getMonthData(e.id);
|
||||
var subChecks = getSubChecks(e.id);
|
||||
var curMonth = months[editMonthIdx];
|
||||
var curData = allData[curMonth] || {report: "", files: []};
|
||||
var curFiles = curData.files || [];
|
||||
var totalFiles = 0;
|
||||
for (var k in allData) { if (allData.hasOwnProperty(k)) totalFiles += (allData[k].files || []).length; }
|
||||
var sh="";if(e.sub&&e.sub.length){sh='<div style="font-weight:600;margin-bottom:8px">Подпункты</div><div class="sub-items">';
|
||||
e.sub.forEach(function(s,i){var ch=sc.indexOf(i)>=0;sh+='<div class="sub-item"><input type="checkbox" id="sc_'+i+'" '+(ch?"checked":"")+'><span class="sub-label">'+s.l+')</span><span class="sub-text">'+esc(s.t)+'</span></div>'});
|
||||
sh+='</div>'}
|
||||
|
||||
// Sub-items
|
||||
var subHtml = "";
|
||||
if (e.sub && e.sub.length) {
|
||||
subHtml = '<div style="font-weight:600;margin-bottom:8px;font-size:14px">Подпункты мероприятия</div><div class="sub-items">';
|
||||
e.sub.forEach(function(s, i) {
|
||||
var ch = subChecks.indexOf(i) >= 0;
|
||||
subHtml += '<div class="sub-item"><input type="checkbox" id="subchk_'+i+'" '+(ch?"checked":"")+'><span class="sub-label">'+s.l+')</span><span class="sub-text">'+escHtml(s.t)+'</span></div>';
|
||||
});
|
||||
subHtml += '</div>';
|
||||
}
|
||||
var fh="";cfs.forEach(function(f,i){fh+='<div class="file-row"><span class="file-info"><span class="file-name" onclick="dlF('+e.id+',\''+cm+'\','+i+')">📄 '+esc(f.name)+'</span>'+(f.desc?'<span class="file-desc">'+esc(f.desc)+'</span>':'')+'</span><span class="file-meta">'+(f.size/1024).toFixed(0)+' КБ · '+f.date+'</span><button class="file-del" onclick="rmF('+e.id+',\''+cm+'\','+i+')">×</button></div>'});
|
||||
|
||||
// Files
|
||||
var filesHtml = "";
|
||||
if (curFiles.length) {
|
||||
filesHtml = '<div style="font-weight:600;margin:12px 0 6px;font-size:13px">Файлы за '+M(editMonthIdx)+' ('+curFiles.length+' шт.)</div>';
|
||||
curFiles.forEach(function(f, i) {
|
||||
filesHtml += '<div class="file-row"><span class="file-info"><span class="file-name" onclick="downloadFile('+e.id+',\''+curMonth+'\','+i+')">📄 '+escHtml(f.name)+'</span>'+(f.desc?'<span class="file-desc">'+escHtml(f.desc)+'</span>':'')+'</span><span class="file-meta">'+(f.size/1024).toFixed(0)+' КБ · '+f.date+'</span><button class="file-del" onclick="removeFile('+e.id+',\''+curMonth+'\','+i+')">×</button></div>';
|
||||
});
|
||||
}
|
||||
var mh='<div class="month-tabs">';months.forEach(function(m,i){mh+='<span class="month-tab'+(i===curMonth?" active":"")+'" onclick="openEdit('+e.id+','+i+')">'+M(i)+'</span>'});mh+='</div>';
|
||||
|
||||
// Month tabs
|
||||
var monthTabsHtml = '<div class="month-tabs">';
|
||||
months.forEach(function(m, i) {
|
||||
monthTabsHtml += '<span class="month-tab'+(i===editMonthIdx?' active':'')+'" onclick="openEdit('+e.id+','+i+')">'+M(i)+'</span>';
|
||||
});
|
||||
monthTabsHtml += '</div>';
|
||||
var html='<button class="close" onclick="closeEM()">×</button>';
|
||||
html+='<span class="badge blue">Раздел '+["I","II","III","IV","V"][e.sec]+'</span>';
|
||||
html+='<h3 style="margin:8px 0">'+esc(e.t)+'</h3>';
|
||||
html+='<div class="meta-row"><div class="fld">Дивизион<strong>'+esc(branches[e.b])+'</strong></div><div class="fld">Ответственный<strong>'+esc(e.r)+'</strong></div><div class="fld">Срок<strong>'+e.due+'</strong></div><div class="fld">Факт<strong>'+e.done+'</strong></div></div>';
|
||||
html+='<div class="field"><label>Статус</label><select id="es"><option value="wait"'+(e.s==="wait"?" selected":"")+'>В процессе</option><option value="warn"'+(e.s==="warn"?" selected":"")+'>На контроле</option><option value="late"'+(e.s==="late"?" selected":"")+'>Просрочено</option><option value="done"'+(e.s==="done"?" selected":"")+'>Исполнено</option></select></div>';
|
||||
html+='<div class="field"><label>Прогресс (%)</label><input type="range" id="ep" min="0" max="100" value="'+e.p+'" oninput="document.getElementById(\'pv\').textContent=this.value+\'%\'"><span id="pv" style="font-weight:700">'+e.p+'%</span></div>';
|
||||
html+='<div class="field"><label>Комментарий</label><textarea id="ec" placeholder="Комментарий..."></textarea></div>';
|
||||
html+=sh;
|
||||
html+='<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:12px"><div style="display:flex;justify-content:space-between;margin-bottom:12px"><span style="font-weight:600">📎 Отчётность по месяцам</span><span style="font-size:12px;color:var(--gray-500)">Файлов: '+tf+'</span></div>';
|
||||
html+=mh;
|
||||
html+='<div class="field" style="margin-top:12px"><label>Текст отчёта за '+M(curMonth)+'</label><textarea id="mr" placeholder="Опишите ход исполнения... Можно без файлов." style="min-height:80px">'+esc(cd.report||"")+'</textarea></div>';
|
||||
html+=fh;
|
||||
html+='<div class="upload-row"><input type="text" id="fd" placeholder="Описание файла"><input type="file" id="fi" multiple><button class="btn btn-sm" id="ub" onclick="uploadFiles('+e.id+',\''+cm+'\')">Загрузить</button></div>';
|
||||
html+='<p style="font-size:11px;color:var(--gray-500);margin-top:6px">Формы завершения: '+esc(e.dname)+'</p></div>';
|
||||
html+='<div class="ai-block"><h4>🤖 Вывод ИИ-агента</h4>'+esc(e.ai)+'</div>';
|
||||
html+='<div style="font-weight:600;margin:8px 0 4px">История:</div><div>';e.h.forEach(function(h){html+='<div class="history-item"><div class="dot"></div>'+esc(h)+'</div>'});html+='</div>';
|
||||
html+='<div style="margin-top:20px;display:flex;gap:12px"><button class="btn" onclick="saveEdit('+e.id+',\''+cm+'\')">Сохранить</button><button class="btn btn-outline" onclick="closeEM()">Отмена</button></div>';
|
||||
|
||||
var html = '';
|
||||
html += '<button class="close" onclick="closeEditModal()">×</button>';
|
||||
html += '<span class="badge blue">Раздел '+["I","II","III","IV","V"][e.sec]+'</span>';
|
||||
html += '<h3 style="margin:8px 0">'+escHtml(e.t)+'</h3>';
|
||||
html += '<div class="meta-row">';
|
||||
html += '<div class="fld">Дивизион<strong>'+escHtml(branches[e.b])+'</strong></div>';
|
||||
html += '<div class="fld">Ответственный<strong>'+escHtml(e.r)+'</strong></div>';
|
||||
html += '<div class="fld">Срок<strong>'+e.due+'</strong></div>';
|
||||
html += '<div class="fld">Факт<strong>'+e.done+'</strong></div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="field"><label>Статус</label><select id="editStatus" onchange="autoProgress()">';
|
||||
["wait","warn","late","done"].forEach(function(s) {
|
||||
html += '<option value="'+s+'"'+(e.s===s?' selected':'')+'>'+statusMap[s]+'</option>';
|
||||
});
|
||||
html += '</select></div>';
|
||||
html += '<div class="field"><label>Прогресс (%)</label><input type="range" id="editProgress" min="0" max="100" value="'+e.p+'" oninput="document.getElementById(\'pVal\').textContent=this.value+\'%\'" style="width:100%"><span id="pVal" style="font-weight:700">'+e.p+'%</span></div>';
|
||||
html += '<div class="field"><label>Комментарий</label><textarea id="editComment" placeholder="Комментарий к статусу..."></textarea></div>';
|
||||
|
||||
html += subHtml;
|
||||
|
||||
html += '<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:12px">';
|
||||
html += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px"><span style="font-weight:600;font-size:14px">📎 Отчётность по месяцам</span><span style="font-size:12px;color:var(--gray-500)">Файлов: '+totalFiles+'</span></div>';
|
||||
html += monthTabsHtml;
|
||||
html += '<div class="field" style="margin-top:12px"><label>Текст отчёта за '+M(editMonthIdx)+'</label><textarea id="monthReport" placeholder="Опишите ход исполнения, результаты, проблемы... Можно без прикрепления файлов." style="min-height:80px">'+escHtml(curData.report||"")+'</textarea></div>';
|
||||
html += filesHtml;
|
||||
html += '<div class="upload-row"><input type="text" id="fileDesc" placeholder="Описание файла (акт, протокол, фото...)" style="flex:2"><input type="file" id="editFileInput" multiple><button class="btn btn-sm" id="uploadBtn" onclick="uploadFiles('+e.id+',\''+curMonth+'\')">Загрузить</button></div>';
|
||||
html += '<p style="font-size:11px;color:var(--gray-500);margin-top:6px">Формы завершения: '+escHtml(e.dname)+'</p>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="ai-block"><h4>🤖 Вывод ИИ-агента</h4>'+escHtml(e.ai)+'</div>';
|
||||
html += '<div style="font-weight:600;margin:8px 0 4px;font-size:14px">История:</div><div>';
|
||||
e.h.forEach(function(h) { html += '<div class="history-item"><div class="dot"></div>'+escHtml(h)+'</div>'; });
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="margin-top:20px;display:flex;gap:12px"><button class="btn" onclick="saveEdit('+e.id+',\''+curMonth+'\')">Сохранить</button><button class="btn btn-outline" onclick="closeEditModal()">Отмена</button></div>';
|
||||
|
||||
document.getElementById("editModalContent").innerHTML = html;
|
||||
document.getElementById("editModalContent").innerHTML=html;
|
||||
document.getElementById("editModalOverlay").classList.add("open");
|
||||
}
|
||||
|
||||
function autoProgress() {
|
||||
var s = document.getElementById("editStatus");
|
||||
var p = document.getElementById("editProgress");
|
||||
if (!s || !p) return;
|
||||
if (s.value === "done") p.value = 100;
|
||||
else if (s.value === "wait") p.value = 0;
|
||||
document.getElementById("pVal").textContent = p.value + "%";
|
||||
function saveEdit(id,mk){
|
||||
var e=null;for(var i=0;i<events.length;i++){if(events[i].id===id){e=events[i];break}}if(!e)return;
|
||||
e.s=document.getElementById("es").value;e.p=parseInt(document.getElementById("ep").value);
|
||||
var cmt=(document.getElementById("ec").value||"").trim(),mr=document.getElementById("mr");mr=mr?mr.value:"";
|
||||
if(mk){var ad=getMD(id);if(!ad[mk])ad[mk]={report:"",files:[]};ad[mk].report=mr;setMD(id,ad)}
|
||||
if(e.sub&&e.sub.length){var cks=[];e.sub.forEach(function(_,i){var el=document.getElementById("sc_"+i);if(el&&el.checked)cks.push(i)});setSC(id,cks)}
|
||||
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();
|
||||
}
|
||||
function closeEM(){document.getElementById("editModalOverlay").classList.remove("open")}
|
||||
|
||||
function saveEdit(id, monthKey) {
|
||||
var e = null;
|
||||
for (var i = 0; i < events.length; i++) { if (events[i].id === id) { e = events[i]; break; } }
|
||||
if (!e) return;
|
||||
// File storage
|
||||
function getMD(id){var r=localStorage.getItem("sf_"+id);return r?JSON.parse(r):{}}
|
||||
function setMD(id,o){localStorage.setItem("sf_"+id,JSON.stringify(o))}
|
||||
function getSC(id){var r=localStorage.getItem("ss_"+id);return r?JSON.parse(r):[]}
|
||||
function setSC(id,a){localStorage.setItem("ss_"+id,JSON.stringify(a))}
|
||||
|
||||
e.s = document.getElementById("editStatus").value;
|
||||
e.p = parseInt(document.getElementById("editProgress").value);
|
||||
var comment = (document.getElementById("editComment").value || "").trim();
|
||||
var monthReport = document.getElementById("monthReport");
|
||||
monthReport = monthReport ? monthReport.value : "";
|
||||
|
||||
// Save monthly report
|
||||
if (monthKey) {
|
||||
var allData = getMonthData(id);
|
||||
if (!allData[monthKey]) allData[monthKey] = {report: "", files: []};
|
||||
allData[monthKey].report = monthReport;
|
||||
setMonthData(id, allData);
|
||||
}
|
||||
|
||||
// Save sub checks
|
||||
if (e.sub && e.sub.length) {
|
||||
var checks = [];
|
||||
e.sub.forEach(function(_, i) {
|
||||
var el = document.getElementById("subchk_"+i);
|
||||
if (el && el.checked) checks.push(i);
|
||||
});
|
||||
setSubChecks(id, checks);
|
||||
}
|
||||
|
||||
var now = new Date().toLocaleDateString();
|
||||
e.h.push(now + " — " + currentUser.name + ": статус " + statusMap[e.s] + ", прогресс " + e.p + "%" + (comment ? " — комм.: " + comment : ""));
|
||||
if (e.s === "done" && e.done === "\u2014") e.done = now;
|
||||
|
||||
localStorage.setItem("samruk_events", JSON.stringify(events));
|
||||
closeEditModal();
|
||||
renderMain();
|
||||
updateNotifs();
|
||||
function uploadFiles(eid,mk){
|
||||
var fi=document.getElementById("fi");if(!fi||!fi.files.length)return;
|
||||
var desc=(document.getElementById("fd").value||"").trim(),btn=document.getElementById("ub");
|
||||
btn.textContent="Загружается...";btn.disabled=true;
|
||||
var MAX=4*1024*1024,ad=getMD(eid);if(!ad[mk])ad[mk]={report:"",files:[]};
|
||||
var arr=ad[mk].files,pr=0,sk=0;
|
||||
function fin(){try{setMD(eid,ad)}catch(e){alert("Хранилище переполнено")}if(sk)alert(sk+" файл(ов) > 4 МБ пропущены");closeEM();openEdit(eid)}
|
||||
for(var i=0;i<fi.files.length;i++){(function(f){if(f.size>MAX){sk++;pr++;if(pr===fi.files.length)fin();return}
|
||||
var r=new FileReader();r.onload=function(ev){arr.push({name:f.name,size:f.size,type:f.type,desc:desc,date:new Date().toLocaleDateString(),data:ev.target.result});pr++;if(pr===fi.files.length)fin()};
|
||||
r.onerror=function(){pr++;if(pr===fi.files.length)fin()};r.readAsDataURL(f)})(fi.files[i])}
|
||||
}
|
||||
function dlF(eid,mk,idx){var ad=getMD(eid),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){var ad=getMD(eid);if(!ad[mk]||!ad[mk].files)return;ad[mk].files.splice(idx,1);setMD(eid,ad);closeEM();openEdit(eid)}
|
||||
|
||||
function closeEditModal() { document.getElementById("editModalOverlay").classList.remove("open"); }
|
||||
// Init
|
||||
function renderAll(){notifsUpdate();switchTab(document.querySelector(".tab-btn.active").dataset.tab)}
|
||||
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")}});
|
||||
document.addEventListener("click",function(e){if(!e.target.closest(".notif-btn")&&!e.target.closest(".notif-drop"))document.getElementById("notifDrop").classList.remove("open")});
|
||||
|
||||
// === FILE STORAGE ===
|
||||
function getMonthData(eventId) {
|
||||
var raw = localStorage.getItem("samruk_files_"+eventId);
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
}
|
||||
function setMonthData(eventId, obj) {
|
||||
localStorage.setItem("samruk_files_"+eventId, JSON.stringify(obj));
|
||||
}
|
||||
function getSubChecks(eventId) {
|
||||
var raw = localStorage.getItem("samruk_sub_"+eventId);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
}
|
||||
function setSubChecks(eventId, arr) {
|
||||
localStorage.setItem("samruk_sub_"+eventId, JSON.stringify(arr));
|
||||
}
|
||||
|
||||
function uploadFiles(eventId, monthKey) {
|
||||
var fi = document.getElementById("editFileInput");
|
||||
if (!fi || !fi.files || !fi.files.length) return;
|
||||
|
||||
var desc = (document.getElementById("fileDesc").value || "").trim();
|
||||
var btn = document.getElementById("uploadBtn");
|
||||
btn.textContent = "Загружается..."; btn.disabled = true;
|
||||
|
||||
var MAX = 4 * 1024 * 1024;
|
||||
var allData = getMonthData(eventId);
|
||||
if (!allData[monthKey]) allData[monthKey] = {report: "", files: []};
|
||||
var arr = allData[monthKey].files;
|
||||
var processed = 0, skipped = 0;
|
||||
|
||||
function finish() {
|
||||
try { setMonthData(eventId, allData); } catch(e) { alert("Хранилище переполнено"); }
|
||||
if (skipped) alert(skipped + " файл(ов) > 4 МБ пропущены");
|
||||
closeEditModal(); openEdit(eventId);
|
||||
}
|
||||
|
||||
for (var i = 0; i < fi.files.length; i++) {
|
||||
(function(f) {
|
||||
if (f.size > MAX) { skipped++; processed++; if (processed === fi.files.length) finish(); return; }
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(ev) {
|
||||
arr.push({name: f.name, size: f.size, type: f.type, desc: desc, date: new Date().toLocaleDateString(), data: ev.target.result});
|
||||
processed++;
|
||||
if (processed === fi.files.length) finish();
|
||||
};
|
||||
reader.onerror = function() { processed++; if (processed === fi.files.length) finish(); };
|
||||
reader.readAsDataURL(f);
|
||||
})(fi.files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(eventId, monthKey, idx) {
|
||||
var allData = getMonthData(eventId);
|
||||
var arr = allData[monthKey] ? allData[monthKey].files : null;
|
||||
if (!arr || !arr[idx] || !arr[idx].data) return;
|
||||
var f = arr[idx];
|
||||
var a = document.createElement("a");
|
||||
a.href = f.data; a.download = f.name;
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
}
|
||||
|
||||
function removeFile(eventId, monthKey, idx) {
|
||||
var allData = getMonthData(eventId);
|
||||
if (!allData[monthKey] || !allData[monthKey].files) return;
|
||||
allData[monthKey].files.splice(idx, 1);
|
||||
setMonthData(eventId, allData);
|
||||
closeEditModal(); openEdit(eventId);
|
||||
}
|
||||
|
||||
// === INIT ===
|
||||
document.getElementById("editModalOverlay").addEventListener("click", function(e) { if (e.target === this) closeEditModal(); });
|
||||
document.addEventListener("keydown", function(e) { if (e.key === "Escape") { closeEditModal(); document.getElementById("notifDrop").classList.remove("open"); } });
|
||||
document.addEventListener("click", function(e) { if (!e.target.closest(".notif-btn") && !e.target.closest(".notif-drop")) document.getElementById("notifDrop").classList.remove("open"); });
|
||||
|
||||
var savedUser = localStorage.getItem("samruk_user");
|
||||
if (savedUser) {
|
||||
try { currentUser = JSON.parse(savedUser); showApp(); } catch(e) {}
|
||||
}
|
||||
var su=localStorage.getItem("samruk_u");if(su){try{curUser=JSON.parse(su);showApp()}catch(e){}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user