v37: полная пересборка — логин через onclick, без strict

This commit is contained in:
Dauren777 2026-06-05 09:13:25 +00:00
parent f5c1fda9cb
commit 02c58b76fc

View File

@ -3,35 +3,29 @@
<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}
body{font:14px/1.5 Arial,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;text-decoration:none}
.btn:hover{background:#1be5ff}
.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)}
.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:hover{background:#1be5ff}
.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)}
.badge.green{background:#D1FAE5;color:#065F46}.badge.amber{background:#FEF3C7;color:#92400E}.badge.red{background:#FEE2E2;color:#991B1B}.badge.blue{background:#DBEAFE;color:#1E40AF}
.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;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}
.pct-bar .fill{height:100%;border-radius:10px}
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}.pct-bar .fill{height:100%;border-radius:10px}
#loginScreen{display:flex;align-items:center;justify-content:center;min-height:100vh;background:var(--ink)}
.login-box{background:var(--white);border-radius:16px;padding:48px 40px;width:440px;max-width:90vw;text-align:center}
.login-box h1{font-size:24px;font-weight:800;margin-bottom:4px}.login-box h1 span{color:var(--cyan)}
.login-box .sub{color:var(--gray-500);font-size:14px;margin-bottom:32px}
.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}
#loginBox{display:flex;align-items:center;justify-content:center;min-height:100vh;background:var(--ink)}
#loginBox>div{background:var(--white);border-radius:16px;padding:48px 40px;width:440px;max-width:90vw;text-align:center}
#loginBox h1{font-size:24px;font-weight:800;margin-bottom:4px}#loginBox h1 span{color:var(--cyan)}
#loginBox .sub{color:var(--gray-500);font-size:14px;margin-bottom:32px}
#loginBox label{display:block;text-align:left;font-size:13px;font-weight:600;margin-bottom:6px}
#loginBox input{padding:12px 16px;border:1px solid var(--gray-200);border-radius:8px;font-size:15px;margin-bottom:20px;width:100%}
#loginBox .hint{font-size:12px;color:var(--gray-500);margin-top:-12px;margin-bottom:16px;text-align:left}
#loginBox .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,75 +36,44 @@ 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 .empty{padding:24px;text-align:center;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-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(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}
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,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}
.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}
.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}
.modal{background:var(--white);border-radius:16px;max-width:760px;width:94vw;max-height:90vh;overflow-y:auto;padding:32px}
.modal h3{font-size:20px;margin-bottom:6px;padding-right:30px}
.modal .close{float:right;background:none;border:none;font-size:28px;cursor:pointer;line-height:1;color:var(--gray-500);margin:-8px -8px 0 0}
.modal .field{margin-bottom:14px}
.modal .field label{display:block;font-size:13px;font-weight:600;margin-bottom:4px;color:var(--gray-500)}
.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}
.modal h3{font-size:20px;margin-bottom:6px;padding-right:30px}.modal .close{float:right;background:none;border:none;font-size:28px;cursor:pointer;color:var(--gray-500);margin:-8px -8px 0 0}
.modal .field{margin-bottom:14px}.modal .field label{display:block;font-size:13px;font-weight:600;margin-bottom:4px;color:var(--gray-500)}.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}
.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}
.sub-item{display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--gray-100);border-radius:8px;margin-bottom:6px;font-size:13px}
.sub-item .sub-label{font-weight:700;color:var(--cyan);font-size:15px;min-width:20px}
.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)}.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-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;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}
.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}
.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-item{display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--gray-100);border-radius:8px;margin-bottom:6px;font-size:13px}.sub-item .sub-label{font-weight:700;color:var(--cyan);font-size:15px;min-width:20px}.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)}.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-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;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{padding:20px}.stats-row{grid-template-columns:1fr 1fr}.notif-drop{right:8px;width:calc(100vw-16px)}}
</style>
</head>
<body>
<div id="loginScreen">
<form class="login-box" onsubmit="return doLogin(event)">
<div id="loginBox">
<div>
<h1><span>ИИ-Агент</span> ПБ</h1>
<p class="sub">АО «Казахтелеком» — мониторинг производственной безопасности</p>
<label>Корпоративная почта</label>
<input type="email" id="loginEmail" placeholder="surname@telecom.kz" required>
<p class="hint">curator@telecom.kz (куратор, всё) / dpp@telecom.kz (ДПБ, ваш филиал) / пароль любой</p>
<input id="loginEmail" placeholder="curator@telecom.kz">
<p class="hint">curator@telecom.kz / dpp@telecom.kz / ahmetov@telecom.kz / serikov@telecom.kz</p>
<label>Пароль</label>
<input type="password" id="loginPass" placeholder="••••••••" required>
<input id="loginPass" type="password" placeholder="любой">
<p class="err" id="loginErr">Неверная почта или пароль</p>
<button type="submit" class="btn" style="width:100%;margin-top:8px">Войти</button>
</form>
<button class="btn" style="width:100%;margin-top:8px" onclick="doLogin()">Войти</button>
</div>
</div>
<div id="app">
@ -122,15 +85,15 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
<button class="notif-btn" onclick="toggleNotif()">🔔<span class="badge-count" id="notifCount">0</span></button>
<div class="notif-drop" id="notifDrop"></div>
</div>
<span class="logout-btn" onclick="doLogout()">Выйти</span>
<button class="logout-btn" onclick="doLogout()">Выйти</button>
</div>
</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>
<button class="tab-btn" data-tab="journal">📝 Журнал</button>
<button class="tab-btn active" data-tab="dashboard" onclick="switchTab('dashboard')">📊 Дашборд</button>
<button class="tab-btn" data-tab="myevents" onclick="switchTab('myevents')">📋 Мероприятия</button>
<button class="tab-btn" data-tab="analytics" onclick="switchTab('analytics')">📈 Аналитика</button>
<button class="tab-btn" data-tab="journal" onclick="switchTab('journal')">📝 Журнал</button>
</div>
<div class="tab-content active" id="tab-dashboard"></div>
<div class="tab-content" id="tab-myevents"></div>
@ -142,21 +105,20 @@ td{border-bottom:1px solid var(--gray-200)}tr:hover td{background:var(--cyan-50)
<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 branches=["Дирекция ПБ","Дивизион «Сеть»","Корпоративный бизнес","Розничный бизнес","Сервисная фабрика","Телеком Комплект","Корпоративный университет","Управление проектами","Цифровой бизнес"];
var regions=["Центральный регион","Алматинский регион","Южный регион","Северный регион","Восточный регион","Западный регион"];
var statusMap={done:"Исполнено",warn:"На контроле",late:"Просрочено",wait:"В процессе"};
var sm={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]}
var mn=["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"];
function M(i){var p=months[i].split("-");return mn[parseInt(p[1])-1]+" "+p[0]}
function esc(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}
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>'}
function sBadge(s){var m={done:"green",warn:"amber",late:"red",wait:"gray"};return'<span class="badge '+m[s]+'">'+sm[s]+'</span>'}
function pctHtml(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={
"curator@telecom.kz":{name:"Куратор Плана",branch:0,role:"curator"},
"dpp@telecom.kz":{name:"Директор ДПБ (Вы)",branch:0,role:"branch"},
"dpp@telecom.kz":{name:"Директор ДПБ",branch:0,role:"branch"},
"admin@telecom.kz":{name:"Администратор",branch:0,role:"admin"},
"ahmetov@telecom.kz":{name:"Ахметов К.Т.",branch:6,role:"branch"},
"serikov@telecom.kz":{name:"Сериков А.М.",branch:1,role:"branch"},
@ -167,61 +129,100 @@ var users={
"maratov@telecom.kz":{name:"Маратов Ж.К.",branch:5,role:"branch"},
"iskakov@telecom.kz":{name:"Искаков Р.Н.",branch:7,role:"branch"}
};
var curUser=null,curMonth=5,curRegion=0,editSubIdx=-1;
// Load events
var expandedEvents={};
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 saveEvents(){localStorage.setItem("samruk_ev",JSON.stringify(events))}
// 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 loadEvents(){
var s=localStorage.getItem("samruk_ev");
if(s){try{events=JSON.parse(s)}catch(e){events=[]}}
if(!events||!events.length){
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){events=[]};if(!events)events=[];saveEv()};
x.onerror=function(){events=[];saveEv()};
x.send();
}
}
function saveEv(){localStorage.setItem("samruk_ev",JSON.stringify(events||[]))}
function getMy(){if(!curUser||!events)return[];if(curUser.role==="admin"||curUser.role==="curator")return events;return events.filter(function(e){return e.b===curUser.branch})}
function getMy(){
if(!curUser||!events)return[];
if(curUser.role==="admin"||curUser.role==="curator")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));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"?"Все регионы":branches[curUser.branch]);document.getElementById("userLabel").innerHTML="<strong>"+label+"</strong>";renderAll()}
// ===== FILE STORAGE =====
function getMD(id,ri,si){ri=ri||0;var k=si>=0?"sf_"+id+"_s"+si+"_r"+ri:"sf_"+id+"_r"+ri;var r=localStorage.getItem(k);return r?JSON.parse(r):{}}
function setMD(id,o,ri,si){ri=ri||0;var k=si>=0?"sf_"+id+"_s"+si+"_r"+ri:"sf_"+id+"_r"+ri;localStorage.setItem(k,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))}
// Notifs
function storageUsed(){var t=0;for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);if(k.indexOf("sf_")===0||k.indexOf("ss_")===0)t+=localStorage.getItem(k).length*2}return t}
function fmtStorage(){var b=storageUsed();return b>1048576?(b/1048576).toFixed(1)+" МБ":(b/1024).toFixed(0)+" КБ"}
// ===== AUTH =====
function doLogin(){
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"}
}
function doLogout(){
localStorage.removeItem("samruk_u");curUser=null;
document.getElementById("loginBox").style.display="flex";
document.getElementById("app").style.display="none";
}
function showApp(){
document.getElementById("loginBox").style.display="none";
document.getElementById("app").style.display="block";
var label=curUser.name+" · "+(curUser.role==="curator"||curUser.role==="admin"?"Все":branches[curUser.branch]);
document.getElementById("userLabel").innerHTML="<strong>"+label+"</strong>";
renderAll();
}
// ===== NOTIFS =====
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==="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:"Сейчас"});
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});
if(days>0&&days<=7)n.push({m:"⏰ "+Math.round(days)+" дн: "+e.t.slice(0,40)+"...",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>'}
});
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){
// ===== TABS =====
function switchTab(n){
document.querySelectorAll(".tab-btn").forEach(function(b){b.classList.remove("active")});
document.querySelector('[data-tab="'+name+'"]').classList.add("active");
document.querySelector('[data-tab="'+n+'"]').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();else if(name==="journal")renderJournal();
document.getElementById("tab-"+n).classList.add("active");
if(n==="dashboard")renderDashboard();else if(n==="myevents")renderMyEvents();else if(n==="analytics")renderAnalytics();else if(n==="journal")renderJournal();
}
// ===== 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 my=getMy(),done=0,late=0,warn=0,wait=0;
my.forEach(function(e){if(e.s==="done")done++;else if(e.s==="late")late++;else if(e.s==="warn")warn++;else wait++});
var dp=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"><div class="lbl">Мероприятий</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>';
@ -229,36 +230,37 @@ function renderDashboard(){
h+='</div>';
// 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 class="panel"><h3>По разделам</h3>';
var sc=[0,0,0,0,0],sd=[0,0,0,0,0];
my.forEach(function(e){sc[e.sec]++;if(e.s==="done")sd[e.sec]++});
sections.forEach(function(s,i){var p=sc[i]?Math.round(sd[i]/sc[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:'+p+'%;background:var(--cyan)"></div></div><span style="font-weight:700;font-size:13px">'+p+'%</span></div>';
});
h+='</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>';
// Chart
h+='<div class="panel"><h3>Динамика</h3><div style="height:140px;background:var(--gray-100);border-radius:8px;display:flex;align-items:flex-end;gap:8px;padding:16px 16px 8px">';
h+='<div style="flex:1;background:var(--green);border-radius:4px 4px 0 0;height:50%"></div><div style="flex:1;background:var(--green);border-radius:4px 4px 0 0;height:65%"></div><div style="flex:1;background:var(--cyan);border-radius:4px 4px 0 0;height:75%"></div><div style="flex:1;background:var(--cyan);border-radius:4px 4px 0 0;height:'+dp+'%"></div>';
h+='</div><div style="display:flex;gap:8px;padding:8px 16px 0;font-size:11px;color:var(--gray-500);text-align:center"><span style="flex:1">Q1</span><span style="flex:1">Q2</span><span style="flex:1">Q3</span><span style="flex:1">Q4</span></div></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>';
h+='<button class="btn btn-sm btn-outline" onclick="downloadHTML()" style="margin-left:8px">Скачать HTML</button>';
h+='<div style="margin-top:12px;font-size:12px;color:var(--gray-500)">Хранилище: '+fmtStorage()+'</div>';
h+='<button class="btn btn-sm" style="margin-top:6px;background:var(--green);color:#fff" onclick="exportAll()">💾 Сохранить все данные</button>';
h+='<button class="btn btn-sm btn-outline" style="margin-top:6px;margin-left:4px" onclick="document.getElementById(\'impF\').click()">📥 Загрузить данные</button>';
h+='<div class="panel"><h3>📥 Сводный отчёт</h3><div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px">';
h+='<select id="rptFrom">'+months.map(function(m,i){return'<option value="'+i+'">'+M(i)+'</option>'}).join("")+'</select><span></span>';
h+='<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="downloadCSV()">Скачать CSV</button>';
h+='<button class="btn btn-sm btn-outline" onclick="downloadHTML()">Скачать HTML</button></div>';
h+='<div style="font-size:12px;color:var(--gray-500);margin-bottom:8px">Хранилище: '+fmtStorage()+'</div>';
h+='<button class="btn btn-sm" style="background:var(--green);color:#fff;margin-right:4px" onclick="exportAll()">💾 Сохранить всё</button>';
h+='<button class="btn btn-sm btn-outline" style="margin-right:4px" onclick="document.getElementById(\'impF\').click()">📥 Загрузить</button>';
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>';
h+='<button class="btn btn-sm" style="background:var(--red);color:#fff" onclick="clearAll()">🗑 Очистить</button></div>';
// Curator branch summary
if(curUser.role==="curator"||curUser.role==="admin"){
h+='<div class="panel"><h3>📋 Сводка по филиалам — что загружено</h3><table><tr><th>Филиал</th><th>Меропр.</th><th>С отчётами</th><th>Файлов</th><th>Прогресс</th></tr>';
branches.forEach(function(b,bi){var items=events.filter(function(e){return e.b===bi}),withRpt=0,totalFiles=0;
items.forEach(function(e){var has=false;regions.forEach(function(r,ri){var d=getMD(e.id,ri,-1);for(var k in d){if(d.hasOwnProperty(k)&&d[k]&&(d[k].report||(d[k].files&&d[k].files.length))){has=true;totalFiles+=(d[k].files||[]).length}}if(e.sub)e.sub.forEach(function(s,si){var sd=getMD(e.id,ri,si);for(var sk in sd){if(sd.hasOwnProperty(sk)&&sd[sk]&&(sd[sk].report||(sd[sk].files&&sd[sk].files.length))){has=true;totalFiles+=(sd[sk].files||[]).length}}})});if(has)withRpt++});
h+='<tr><td><strong>'+b+'</strong></td><td>'+items.length+'</td><td>'+(withRpt?withRpt+' ✅':'—')+'</td><td>'+(totalFiles||'—')+'</td><td>'+pct(items.length?Math.round(withRpt/items.length*100):0)+'</td></tr>';
h+='<div class="panel"><h3>📋 Сводка по филиалам</h3><table><tr><th>Филиал</th><th>Меропр.</th><th>С отчётами</th><th>Файлов</th></tr>';
branches.forEach(function(b,bi){var its=events.filter(function(e){return e.b===bi}),wr=0,tf=0;
its.forEach(function(e){var has=false;regions.forEach(function(r,ri){var d=getMD(e.id,ri,-1);for(var k in d){if(d.hasOwnProperty(k)&&d[k]){if(d[k].report)has=true;tf+=(d[k].files||[]).length}}if(e.sub)e.sub.forEach(function(s,si){var sd=getMD(e.id,ri,si);for(var sk in sd){if(sd.hasOwnProperty(sk)&&sd[sk]){if(sd[sk].report)has=true;tf+=(sd[sk].files||[]).length}}});});if(has)wr++});
h+='<tr><td><strong>'+b+'</strong></td><td>'+its.length+'</td><td>'+(wr?wr+' ✅':"—")+'</td><td>'+(tf||"—")+'</td></tr>';
});
h+='</table></div>';
}
@ -266,288 +268,182 @@ function renderDashboard(){
document.getElementById("tab-dashboard").innerHTML=h;
}
function downloadReport(){
// ===== CSV/HTML DOWNLOAD =====
function downloadCSV(){
var from=parseInt(document.getElementById("rptFrom").value),to=parseInt(document.getElementById("rptTo").value);
var my=getMy(),csv="№;Филиал;Мероприятие;Подпункт;Регион;Раздел;Статус;Прогресс;Срок;Факт;Отчёт (текст);Файлы\n";
var my=getMy(),csv="\uFEFF№;Филиал;Мероприятие;Подпункт;Регион;Раздел;Статус;Прогресс;Срок;Факт;Отчёт;Файлы\n";
my.forEach(function(e){
function addRow(subLabel,subIdx){regions.forEach(function(r,ri){var rep="",fls="",d=getMD(e.id,ri,subIdx);
function add(subL,subI){regions.forEach(function(r,ri){var rep="",fls="",d=getMD(e.id,ri,subI);
for(var i=from;i<=to;i++){var m=months[i];if(d[m]){if(d[m].report)rep+=M(i)+": "+d[m].report.replace(/"/g,'""')+"; ";if(d[m].files&&d[m].files.length)fls+=M(i)+": "+d[m].files.map(function(f){return f.name}).join(", ")+"; "}}
csv+=e.id+';'+branches[e.b]+';"'+e.t.replace(/"/g,'""')+'";'+(subLabel||"общее")+';'+r+';'+sections[e.sec]+';'+statusMap[e.s]+';'+e.p+'%;'+e.due+';'+(e.done||"—")+';"'+rep+'";"'+fls+'"\n'})}
addRow("",-1);
if(e.sub) e.sub.forEach(function(s,i){ addRow(s.l,i); });
csv+=e.id+';'+branches[e.b]+';"'+e.t.replace(/"/g,'""')+'";'+(subL||"общ")+';'+r+';'+sections[e.sec]+';'+sm[e.s]+';'+e.p+'%;'+e.due+';'+(e.done||"—")+';"'+rep+'";"'+fls+'"\n'})}
add("",-1);if(e.sub)e.sub.forEach(function(s,i){add(s.l,i)});
});
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();addLog("скачал CSV-отчёт",null,M(from)+"-"+M(to))
var a=document.createElement("a");a.href=URL.createObjectURL(new Blob([csv],{type:"text/csv"}));a.download="otchet_"+M(from)+"-"+M(to)+".csv";a.click()
}
function downloadHTML(){
var from=parseInt(document.getElementById("rptFrom").value),to=parseInt(document.getElementById("rptTo").value);
var my=getMy(),h='<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8"><title>Сводный отчёт ПБ</title><style>body{font:14px/1.5 Arial,sans-serif;max-width:1100px;margin:0 auto;padding:24px}h1{font-size:22px;margin-bottom:4px}h1 span{color:#00E5FF}h2{font-size:18px;margin:24px 0 8px}.ev{border:1px solid #ddd;border-radius:8px;padding:16px;margin-bottom:16px}.ev h3{font-size:15px;margin:0 0 8px}.meta{display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:#666;margin-bottom:8px}.meta strong{color:#111}.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700}.g{background:#D1FAE5;color:#065F46}.a{background:#FEF3C7;color:#92400E}.r{background:#FEE2E2;color:#991B1B}.w{background:#eee;color:#666}.month{background:#f5f5f5;padding:8px 12px;border-radius:4px;margin:6px 0}.sub-blk{border-left:3px solid #00E5FF;padding-left:12px;margin:8px 0}.sub-blk strong{font-size:12px}.files{font-size:12px;color:#555;margin-top:2px}ul{list-style:"📄 ";margin-left:20px}@media print{.ev{border-color:#999}}</style></head><body>';
h+='<h1><span>ИИ-Агент</span> ПБ — Сводный отчёт</h1><p style="color:#666">Период: '+M(from)+' — '+M(to)+' · Дивизион: '+branches[curUser.branch]+' · Сформирован: '+new Date().toLocaleDateString()+'</p>';
h+='<p style="font-size:12px;color:#888;background:#fffbe6;padding:8px 12px;border-radius:4px;">💡 Файлы перечислены названиями. Чтобы получить сами файлы — используйте кнопку «💾 Сохранить все данные» на дашборде.</p>';
my.forEach(function(e){
var scls={done:"g",warn:"a",late:"r",wait:"w"}[e.s];
h+='<div class="ev"><h3>'+e.id+'. '+esc(e.t)+'</h3>';
h+='<div class="meta"><span>Филиал: <strong>'+branches[e.b]+'</strong></span><span>Раздел: <strong>'+sections[e.sec]+'</strong></span><span>Срок: <strong>'+e.due+'</strong></span><span>Факт: <strong>'+(e.done||"—")+'</strong></span><span>Прогресс: <strong>'+e.p+'%</strong></span><span class="badge '+scls+'">'+statusMap[e.s]+'</span></div>';
h+='<div class="meta"><span>Ответственный: <strong>'+esc(e.r)+'</strong></span></div>';
// Main event data — all regions
regions.forEach(function(r,ri){h+=renderMonthBlock(e.id,ri,-1,r,from,to)});
// Sub-items — all regions
if(e.sub) e.sub.forEach(function(s,i){ h+='<div class="sub-blk"><strong>'+s.l+') '+esc(s.t)+'</strong>';regions.forEach(function(r,ri){h+=renderMonthBlock(e.id,ri,i,"",from,to)});h+='</div>'});
h+='<div style="font-size:11px;color:#666;margin-top:8px">🤖 ИИ: '+esc(e.ai)+'</div></div>';
var my=getMy(),h='<!DOCTYPE html><html><head><meta charset="utf-8"><title>Отчёт ПБ</title><style>body{font:14px/1.5 Arial;max-width:1100px;margin:0 auto;padding:24px}.ev{border:1px solid #ddd;border-radius:8px;padding:16px;margin-bottom:16px}.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700}.g{background:#D1FAE5;color:#065F46}.a{background:#FEF3C7;color:#92400E}.r{background:#FEE2E2;color:#991B1B}.w{background:#eee;color:#666}.month{background:#f5f5f5;padding:8px 12px;border-radius:4px;margin:6px 0}ul{list-style:"📄 ";margin-left:20px}</style></head><body>';
h+='<h1>Сводный отчёт ПБ</h1><p>Период: '+M(from)+' — '+M(to)+'</p>';
my.forEach(function(e){var cl={done:"g",warn:"a",late:"r",wait:"w"}[e.s];
h+='<div class="ev"><h3>'+e.id+'. '+esc(e.t)+'</h3><p>Филиал: '+branches[e.b]+' | Раздел: '+sections[e.sec]+' | Срок: '+e.due+' | Прогресс: '+e.p+'% <span class="badge '+cl+'">'+sm[e.s]+'</span></p>';
regions.forEach(function(r,ri){var d=getMD(e.id,ri,-1);
for(var i=from;i<=to;i++){var m=months[i];if(d[m]&&(d[m].report||(d[m].files&&d[m].files.length))){
h+='<div class="month"><strong>'+M(i)+' — '+r+'</strong>';
if(d[m].report)h+='<p>'+esc(d[m].report)+'</p>';
if(d[m].files&&d[m].files.length)h+='<ul>'+d[m].files.map(function(f){return'<li>'+esc(f.name)+(f.desc?' — '+esc(f.desc):'')+' ('+(f.size/1024).toFixed(0)+' КБ)</li>'}).join("")+'</ul>';
h+='</div>'}}});
if(e.sub)e.sub.forEach(function(s,i){regions.forEach(function(r,ri){var sd=getMD(e.id,ri,i);
for(var j=from;j<=to;j++){var m=months[j];if(sd[m]&&(sd[m].report||(sd[m].files&&sd[m].files.length))){
h+='<div style="border-left:3px solid #00E5FF;padding-left:12px;margin:6px 0"><strong>'+s.l+') '+esc(s.t)+'</strong>';
h+='<div class="month"><strong>'+M(j)+' — '+r+'</strong>';
if(sd[m].report)h+='<p>'+esc(sd[m].report)+'</p>';
if(sd[m].files&&sd[m].files.length)h+='<ul>'+sd[m].files.map(function(f){return'<li>'+esc(f.name)+(f.desc?' — '+esc(f.desc):'')+' ('+(f.size/1024).toFixed(0)+' КБ)</li>'}).join("")+'</ul>';
h+='</div></div>'}}})});
h+='</div>';
});
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();addLog("скачал HTML-отчёт",null,M(from)+"-"+M(to));
setTimeout(function(){URL.revokeObjectURL(a.href)},60000);
}catch(e){
alert("⚠️ Отчёт слишком большой для скачивания. Попробуйте выбрать меньший период или очистить часть файлов.");
}
}
function renderMonthBlock(id,ri,si,label,from,to){
var d=getMD(id,ri,si),has=false,html='';
for(var i=from;i<=to;i++){var m=months[i];if(d[m]&&(d[m].report||(d[m].files&&d[m].files.length))){
has=true;html+='<div class="month"><strong>'+M(i)+(label?' — '+label:'')+'</strong>';
if(d[m].report)html+='<p style="font-size:13px;margin:4px 0">'+esc(d[m].report)+'</p>';
if(d[m].files&&d[m].files.length)html+='<div class="files"><ul>'+d[m].files.map(function(f){return'<li>📄 '+esc(f.name)+(f.desc?' — '+esc(f.desc):'')+' ('+(f.size/1024).toFixed(0)+' КБ)</li>'}).join("")+'</ul></div>';
html+='</div>'}}
if(!has)html='<p style="font-size:12px;color:#999">Нет отчётов</p>';
return html;
try{var a=document.createElement("a");a.href=URL.createObjectURL(new Blob(["\uFEFF"+h],{type:"text/html"}));a.download="otchet_"+M(from)+"-"+M(to)+".html";a.click()}catch(e){alert("Слишком большой отчёт. Попробуйте меньший период.")}
}
// ===== MY EVENTS =====
var expandedEvents = {};
function toggleExpand(eid) { expandedEvents[eid] = !expandedEvents[eid]; renderMyEvents(); }
function toggleExpand(eid){expandedEvents[eid]=!expandedEvents[eid];renderMyEvents()}
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 my=getMy(),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><th></th></tr>';
var h='<div class="panel" style="border-radius:0 0 12px 12px"><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>';
h+='<table><tr><th></th><th>Мероприятие</th><th>Филиал</th><th>Раздел</th><th>Срок</th><th>Прогресс</th><th>Статус</th><th></th></tr>';
list.forEach(function(e){
var hasSub = e.sub && e.sub.length;
var sc = getSC(e.id);
var subDone = hasSub ? sc.length : 0;
var subTotal = hasSub ? e.sub.length : 0;
h+='<tr><td>'+e.id+'</td>';
h+='<td style="font-size:12px;max-width:300px">';
if(hasSub) h+='<span onclick="toggleExpand('+e.id+')" style="cursor:pointer;margin-right:6px">'+(expandedEvents[e.id]?'▼':'▶')+'</span>';
h+='<span title="'+esc(e.t)+'" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px;display:inline-block;vertical-align:middle">'+esc(e.t)+'</span>';
if(hasSub) h+=' <span style="font-size:11px;color:var(--gray-500)">('+subDone+'/'+subTotal+')</span>';
h+='</td>';
h+='<td style="font-size:11px">'+branches[e.b]+'</td>';
h+='<td><span class="badge blue">'+["I","II","III","IV","V"][e.sec]+'</span></td>';
h+='<td>'+e.due+'</td><td>'+pct(e.p)+'</td><td>'+sb(e.s)+'</td>';
h+='<td><button class="btn btn-sm" onclick="openEdit('+e.id+')">📝</button></td></tr>';
// Sub-items
if(hasSub && expandedEvents[e.id]){
e.sub.forEach(function(s,i){
var ch = sc.indexOf(i) >= 0;
h+='<tr style="background:var(--gray-100)"><td></td>';
h+='<td style="font-size:12px;padding-left:44px"><input type="checkbox" id="si_'+e.id+'_'+i+'" '+(ch?'checked':'')+' onchange="toggleSubItem('+e.id+','+i+',this.checked)" style="margin-right:6px"><span style="color:var(--gray-500)">'+s.l+') '+esc(s.t)+'</span></td>';
h+='<td></td><td></td><td></td><td></td><td></td></tr>';
});
}
var hs=e.sub&&e.sub.length,sc=getSC(e.id),sd=hs?sc.length:0,st=hs?e.sub.length:0;
h+='<tr><td>'+e.id+'</td><td style="font-size:12px;max-width:280px">';
if(hs)h+='<span onclick="toggleExpand('+e.id+')" style="cursor:pointer;margin-right:6px">'+(expandedEvents[e.id]?'▼':'▶')+'</span>';
h+='<span title="'+esc(e.t)+'" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px;display:inline-block;vertical-align:middle">'+esc(e.t)+'</span>';
if(hs)h+=' <span style="font-size:11px;color:var(--gray-500)">('+sd+'/'+st+')</span>';
h+='</td><td style="font-size:11px">'+branches[e.b]+'</td><td><span class="badge blue">'+["I","II","III","IV","V"][e.sec]+'</span></td><td>'+e.due+'</td><td>'+pctHtml(e.p)+'</td><td>'+sBadge(e.s)+'</td><td><button class="btn btn-sm" onclick="openEdit('+e.id+')">📝</button></td></tr>';
if(hs&&expandedEvents[e.id])e.sub.forEach(function(s,i){var ch=sc.indexOf(i)>=0;h+='<tr style="background:var(--gray-100)"><td></td><td style="font-size:12px;padding-left:44px"><input type="checkbox" '+(ch?"checked":"")+' onchange="toggleSub('+e.id+','+i+',this.checked)" style="margin-right:6px">'+s.l+') '+esc(s.t)+'</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>'});
});
h+='</table></div>';
document.getElementById("tab-myevents").innerHTML=h;
}
function toggleSubItem(eid, subIdx, checked) {
var sc = getSC(eid);
if(checked) { if(sc.indexOf(subIdx) < 0) sc.push(subIdx); }
else { sc = sc.filter(function(x){ return x !== subIdx; }); }
setSC(eid, sc);
// Update event progress based on sub-items
var e = null; for(var i=0;i<events.length;i++){if(events[i].id===eid){e=events[i];break}}
if(e && e.sub && e.sub.length) {
var pct = Math.round(sc.length / e.sub.length * 100);
if(sc.length === e.sub.length && e.s === 'warn') e.s = 'done';
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();addLog("подпункты",eid,sc.length+"/"+e.sub.length);
}
function toggleSub(eid,si,chk){
var sc=getSC(eid);if(chk&&sc.indexOf(si)<0)sc.push(si);else if(!chk)sc=sc.filter(function(x){return x!==si});setSC(eid,sc);
var e=null;for(var i=0;i<events.length;i++)if(events[i].id===eid){e=events[i];break}
if(e&&e.sub){var p=Math.round(sc.length/e.sub.length*100);if(sc.length===e.sub.length&&e.s!=="done")e.s="done";if(sc.length===0&&e.s==="done")e.s="warn";e.p=Math.max(e.p,p);e.h.push(new Date().toLocaleDateString()+" — подпункты "+sc.length+"/"+e.sub.length);saveEv()}
renderMyEvents();
}
// ===== 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>';
var all=events||[];
var h='<div class="stats-row"><div class="stat-card"><div class="lbl">Всего</div><div class="num">'+all.length+'</div></div><div class="stat-card green"><div class="lbl">Исполнено</div><div class="num">'+all.filter(function(e){return e.s==="done"}).length+'</div></div><div class="stat-card amber"><div class="lbl">На контроле</div><div class="num">'+all.filter(function(e){return e.s==="warn"}).length+'</div></div><div class="stat-card red"><div class="lbl">Просрочено</div><div class="num">'+all.filter(function(e){return e.s==="late"}).length+'</div></div></div>';
h+='<div class="panel"><h3>Дивизионы</h3>';
branches.forEach(function(b,i){var it=all.filter(function(e){return e.b===i}),d=it.filter(function(e){return e.s==="done"}).length,p=it.length?Math.round(d/it.length*100):0;
h+='<div class="pct-bar" style="margin-bottom:6px"><span style="width:200px;font-size:13px;font-weight:600">'+b+'</span><div class="track" style="flex:1"><div class="fill" style="width:'+p+'%;background:'+(p>=50?"var(--green)":p>=25?"var(--amber)":"var(--red)")+'"></div></div><span style="font-weight:700;font-size:13px">'+p+'% ('+d+'/'+it.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+='<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>'+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;
}
// ===== JOURNAL =====
function renderJournal(){
var log=JSON.parse(localStorage.getItem("samruk_log")||"[]").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></tr>';
log.forEach(function(l){h+='<tr><td style="font-size:11px">'+new Date(l.ts).toLocaleString()+'</td><td>'+esc(l.user)+'</td><td>'+esc(l.action)+'</td><td>'+esc(l.detail||"")+'</td></tr>'});
h+='</table>'}
h+='</div>';
document.getElementById("tab-journal").innerHTML=h;
}
function addLog(action,eid,detail){
var l=JSON.parse(localStorage.getItem("samruk_log")||"[]");
l.push({ts:new Date().toISOString(),user:curUser?curUser.name:"?",action:action,eid:eid||0,detail:detail||""});
if(l.length>500)l=l.slice(-500);localStorage.setItem("samruk_log",JSON.stringify(l));
}
// Export/Import/Clear
function exportAll(){
var data={events:events,date:new Date().toISOString(),files:{},subChecks:{}};
for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);if(k.indexOf("sf_")===0)data.files[k]=localStorage.getItem(k);if(k.indexOf("ss_")===0)data.subChecks[k]=localStorage.getItem(k)}
var a=document.createElement("a");a.href=URL.createObjectURL(new Blob([JSON.stringify(data)],{type:"application/json"}));a.download="backup_"+new Date().toISOString().slice(0,10)+".json";a.click();addLog("сохранил бекап")
}
function importAll(inp){if(!inp.files.length)return;var r=new FileReader();r.onload=function(ev){try{var d=JSON.parse(ev.target.result);if(!d.events||!d.files)return alert("Неверный формат");events=d.events;saveEv();for(var k in d.files)localStorage.setItem(k,d.files[k]);for(var k in d.subChecks)localStorage.setItem(k,d.subChecks[k]);alert("Восстановлено. Обновите страницу.");location.reload()}catch(e){alert("Ошибка: "+e.message)}};r.readAsText(inp.files[0])}
function clearAll(){if(!confirm("Удалить все файлы?"))return;var ks=[];for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);if(k.indexOf("sf_")===0||k.indexOf("ss_")===0)ks.push(k)}ks.forEach(function(k){localStorage.removeItem(k)});alert("Очищено");renderDashboard()}
// ===== EDIT MODAL =====
var editSubIdx = -1; // -1=none, click 📎 to expand
function openEdit(id,mi,ri,si){
if(typeof mi==="number")curMonth=mi;if(typeof ri==="number")curRegion=ri;editSubIdx=(typeof si==="number")?si:-1;
var e=null;for(var i=0;i<events.length;i++)if(events[i].id===id){e=events[i];break}if(!e)return;
var cm=months[curMonth],sc=getSC(e.id);
var md=getMD(e.id,curRegion,-1),cd=md[cm]||{report:"",files:[]},cfs=cd.files||[];
function openEdit(id, mi, ri, si){
if(typeof mi==="number")curMonth=mi;
if(typeof ri==="number")curRegion=ri;
editSubIdx = (typeof si==="number") ? si : -1; // reset to -1 if not specified
var e=null;for(var i=0;i<events.length;i++){if(events[i].id===id){e=events[i];break}}if(!e)return;
var hasSub = e.sub && e.sub.length;
var cm=months[curMonth];
var sc=getSC(e.id);
var md=getMD(e.id,curRegion,-1), cd=md[cm]||{report:"",files:[]}, cfs=cd.files||[];
var h='<button class="close" onclick="closeEM()">&times;</button><span class="badge blue">Раздел '+["I","II","III","IV","V"][e.sec]+'</span>';
h+='<h3 style="margin:8px 0">'+esc(e.t)+'</h3>';
h+='<div class="meta-row"><div class="fld">Филиал<strong>'+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>';
h+='<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>';
h+='<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>';
h+='<div class="field"><label>Комментарий</label><textarea id="ec"></textarea></div>';
var html='<button class="close" onclick="closeEM()">&times;</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>';
// Month tabs + Region tabs
// Month + Region tabs
var mh='<div class="month-tabs">';months.forEach(function(m,i){mh+='<span class="month-tab'+(i===curMonth?" active":"")+'" onclick="openEdit('+e.id+','+i+','+curRegion+','+editSubIdx+')">'+M(i)+'</span>'});mh+='</div>';
var rh='<div class="month-tabs" style="margin-bottom:12px"><span style="font-weight:600;font-size:13px;margin-right:8px">Регион:</span>';
var rh='<div class="month-tabs" style="margin-bottom:12px"><b style="font-size:13px;margin-right:8px">Регион:</b>';
regions.forEach(function(r,i){rh+='<span class="month-tab'+(i===curRegion?" active":"")+'" onclick="openEdit('+e.id+','+curMonth+','+i+','+editSubIdx+')">'+r+'</span>'});rh+='</div>';
// Main event files (for selected region)
html+='<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:12px"><div style="font-weight:600;font-size:14px;margin-bottom:8px">📎 Отчётность — '+regions[curRegion]+'</div>';
html+=mh;html+=rh;
html+='<div class="field"><label>Текст отчёта за '+M(curMonth)+' ('+regions[curRegion]+')</label><textarea id="mr" placeholder="Опишите ход исполнения... Можно без файлов." style="min-height:80px">'+esc(cd.report||"")+'</textarea></div>';
cfs.forEach(function(f,i){html+='<div class="file-row"><span class="file-info"><span class="file-name" onclick="dlF('+e.id+','+curMonth+','+i+','+curRegion+',-1)">📄 '+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+','+curMonth+','+i+','+curRegion+',-1)">×</button></div>'});
html+='<div class="upload-row"><input type="text" id="fd" placeholder="Описание файла"><input type="file" id="fi" multiple style="max-width:220px"><button class="btn btn-sm" id="ub" onclick="uploadFiles('+e.id+','+curMonth+','+curRegion+',-1)">📤 Загрузить</button></div>';
html+='<p style="font-size:11px;color:var(--gray-500);margin-top:6px">Формы завершения: '+esc(e.dname)+'</p></div>';
// Main files
h+='<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:12px"><b>📎 '+regions[curRegion]+'</b>';
h+=mh+rh;
h+='<div class="field"><label>Текст за '+M(curMonth)+'</label><textarea id="mr" style="min-height:80px">'+esc(cd.report||"")+'</textarea></div>';
cfs.forEach(function(f,i){h+='<div class="file-row"><span class="file-info"><span class="file-name" onclick="dlF('+e.id+','+curMonth+','+i+','+curRegion+',-1)">📄 '+esc(f.name)+'</span>'+(f.desc?'<span class="file-desc">'+esc(f.desc)+'</span>':'')+'</span><span class="file-meta">'+(f.size/1024).toFixed(0)+' КБ</span><button class="file-del" onclick="rmF('+e.id+','+curMonth+','+i+','+curRegion+',-1)">×</button></div>'});
h+='<div class="upload-row"><input type="text" id="fd" placeholder="Описание"><input type="file" id="fi" multiple><button class="btn btn-sm" id="ub" onclick="upF('+e.id+','+curMonth+','+curRegion+',-1)">📤 Загрузить</button></div>';
h+='<p style="font-size:11px;color:var(--gray-500);margin-top:6px">Формы: '+esc(e.dname)+'</p></div>';
// Sub-items
var hasSub = e.sub && e.sub.length;
if(hasSub){
html+='<div style="border-top:2px solid var(--cyan);padding-top:16px;margin-top:16px"><div style="font-weight:700;font-size:15px;margin-bottom:4px">📋 Подпункты</div><p style="font-size:12px;color:var(--gray-500);margin-bottom:12px">Нажмите 📎 для управления файлами подпункта</p>';
if(e.sub&&e.sub.length){
h+='<div style="border-top:2px solid var(--cyan);padding-top:16px;margin-top:16px"><b>📋 Подпункты</b><p style="font-size:12px;color:var(--gray-500);margin:4px 0 12px">Нажмите 📎 для управления файлами</p>';
e.sub.forEach(function(s,i){
var ch=sc.indexOf(i)>=0,sd=getMD(e.id,curRegion,i),scd=sd[cm]||{report:"",files:[]},scfs=scd.files||[];
var isActive=editSubIdx===i;
html+='<div class="sub-item" style="flex-wrap:wrap;padding:12px 14px;margin-bottom:8px;'+(isActive?'border:2px solid var(--cyan)':'')+'">';
html+='<input type="checkbox" id="sc_'+i+'" '+(ch?"checked":"")+'><span class="sub-label" style="font-size:16px">'+s.l+')</span><span class="sub-text" style="flex:1">'+esc(s.t)+'</span>';
html+='<span style="font-size:11px;color:var(--gray-500);margin-right:8px">Файлов: '+scfs.length+'</span>';
html+='<button class="btn btn-sm" onclick="openEdit('+e.id+','+curMonth+','+curRegion+','+i+')" style="font-size:12px;'+(isActive?'background:var(--cyan);font-weight:700':'')+'">'+(isActive?'📂 Открыто':'📎 Файлы')+'</button>';
html+='</div>';
if(isActive){
html+='<div style="margin-left:20px;margin-bottom:16px;padding:16px;background:var(--cyan-50);border-radius:8px;border:2px solid var(--cyan)">';
html+='<div style="font-weight:700;font-size:14px;margin-bottom:4px">'+s.l+') '+esc(s.t)+'</div>';
html+='<div style="font-size:11px;color:var(--gray-500);margin-bottom:12px">Файлы подпункта: '+regions[curRegion]+' · '+M(curMonth)+'</div>';
html+='<div class="field"><label>Текст отчёта</label><textarea id="mr_s'+i+'" placeholder="Опишите ход исполнения..." style="min-height:60px">'+esc(scd.report||"")+'</textarea></div>';
if(scfs.length)html+='<div style="font-weight:600;font-size:13px;margin:8px 0">Файлы ('+scfs.length+'):</div>';
scfs.forEach(function(f,fi){html+='<div class="file-row"><span class="file-info"><span class="file-name" onclick="dlF('+e.id+','+curMonth+','+fi+','+curRegion+','+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+','+curMonth+','+fi+','+curRegion+','+i+')">×</button></div>'});
html+='<div class="upload-row"><input type="text" id="fd_s'+i+'" placeholder="Описание файла"><input type="file" id="fi_s'+i+'" multiple style="max-width:180px"><button class="btn btn-sm" id="ub_s'+i+'" onclick="uploadFiles('+e.id+','+curMonth+','+curRegion+','+i+')">📤 Загрузить</button></div>';
html+='</div>';
var ch=sc.indexOf(i)>=0,sd=getMD(e.id,curRegion,i),scd=sd[cm]||{report:"",files:[]},scfs=scd.files||[],isA=editSubIdx===i;
h+='<div class="sub-item" style="flex-wrap:wrap;padding:12px 14px;margin-bottom:8px;'+(isA?'border:2px solid var(--cyan)':'')+'"><input type="checkbox" id="sc_'+i+'" '+(ch?"checked":"")+'><span class="sub-label">'+s.l+')</span><span class="sub-text" style="flex:1">'+esc(s.t)+'</span><span style="font-size:11px;color:var(--gray-500);margin-right:8px">Файлов: '+scfs.length+'</span><button class="btn btn-sm" onclick="openEdit('+e.id+','+curMonth+','+curRegion+','+i+')" style="font-size:12px;'+(isA?'background:var(--cyan);font-weight:700':'')+'">'+(isA?'📂 Открыто':'📎')+'</button></div>';
if(isA){
h+='<div style="margin:0 0 16px 20px;padding:16px;background:var(--cyan-50);border-radius:8px;border:2px solid var(--cyan)"><b>'+s.l+') '+esc(s.t)+'</b><p style="font-size:11px;color:var(--gray-500);margin-bottom:8px">'+regions[curRegion]+' · '+M(curMonth)+'</p>';
h+='<div class="field"><label>Текст</label><textarea id="mr_s'+i+'" style="min-height:60px">'+esc(scd.report||"")+'</textarea></div>';
scfs.forEach(function(f,fi){h+='<div class="file-row"><span class="file-name" onclick="dlF('+e.id+','+curMonth+','+fi+','+curRegion+','+i+')">📄 '+esc(f.name)+'</span><span class="file-meta">'+(f.size/1024).toFixed(0)+' КБ</span><button class="file-del" onclick="rmF('+e.id+','+curMonth+','+fi+','+curRegion+','+i+')">×</button></div>'});
h+='<div class="upload-row"><input type="text" id="fd_s'+i+'" placeholder="Описание"><input type="file" id="fi_s'+i+'" multiple><button class="btn btn-sm" onclick="upF('+e.id+','+curMonth+','+curRegion+','+i+')">📤</button></div></div>';
}
});
html+='</div>';
h+='</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+','+curMonth+')">Сохранить</button><button class="btn btn-outline" onclick="closeEM()">Отмена</button></div>';
h+='<div class="ai-block"><h4>🤖 ИИ</h4>'+esc(e.ai)+'</div>';
h+='<div><b>История:</b><div>';e.h.forEach(function(x){h+='<div class="history-item"><div class="dot"></div>'+esc(x)+'</div>'});h+='</div></div>';
h+='<div style="margin-top:20px;display:flex;gap:12px"><button class="btn" onclick="saveEdit('+e.id+','+curMonth+')">Сохранить</button><button class="btn btn-outline" onclick="closeEM()">Отмена</button></div>';
document.getElementById("editModalContent").innerHTML=html;
document.getElementById("editModalContent").innerHTML=h;
document.getElementById("editModalOverlay").classList.add("open");
}
function saveEdit(id, mk){
mk=months[mk]; // convert index to month key
var e=null;for(var i=0;i<events.length;i++){if(events[i].id===id){e=events[i];break}}if(!e)return;
function saveEdit(id,mk){
mk=months[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();
// Save main event report
var mr=document.getElementById("mr");if(mr){var ad=getMD(id,curRegion,-1);if(!ad[mk])ad[mk]={report:"",files:[]};ad[mk].report=mr.value;setMD(id,ad,curRegion,-1)}
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);
var sr=document.getElementById("mr_s"+i);if(sr){var sd=getMD(id,curRegion,i);if(!sd[mk])sd[mk]={report:"",files:[]};sd[mk].report=sr.value;setMD(id,sd,curRegion,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();addLog("изменил статус",id,statusMap[e.s]+" "+e.p+"%");closeEM();renderAll();
var cmt=(document.getElementById("ec").value||"").trim(),mr=document.getElementById("mr");
if(mr){var ad=getMD(id,curRegion,-1);if(!ad[mk])ad[mk]={report:"",files:[]};ad[mk].report=mr.value;setMD(id,ad,curRegion,-1)}
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);var sr=document.getElementById("mr_s"+i);if(sr){var sd=getMD(id,curRegion,i);if(!sd[mk])sd[mk]={report:"",files:[]};sd[mk].report=sr.value;setMD(id,sd,curRegion,i)}});setSC(id,cks)}
var now=new Date().toLocaleDateString();e.h.push(now+" — "+curUser.name+": "+sm[e.s]+", "+e.p+"%");
if(e.s==="done"&&e.done==="—")e.done=now;saveEv();addLog("изменил",id,sm[e.s]);closeEM();renderAll();
}
function closeEM(){document.getElementById("editModalOverlay").classList.remove("open")}
// File storage: sf_<eventId>_r<region> for main, sf_<eventId>_s<sub>_r<region> for sub-item
function getMD(id,ri,si){ri=ri||0;var k=si>=0?'sf_'+id+'_s'+si+'_r'+ri:'sf_'+id+'_r'+ri;var r=localStorage.getItem(k);return r?JSON.parse(r):{}}
function setMD(id,o,ri,si){ri=ri||0;var k=si>=0?'sf_'+id+'_s'+si+'_r'+ri:'sf_'+id+'_r'+ri;localStorage.setItem(k,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))}
function storageUsed(){var t=0;for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);if(k.indexOf('sf_')===0||k.indexOf('ss_')===0)t+=localStorage.getItem(k).length*2}return t}
function fmtStorage(){var b=storageUsed();return b>1048576?(b/1048576).toFixed(1)+' МБ':(b/1024).toFixed(0)+' КБ'}
function clearAllFiles(){if(!confirm('Удалить ВСЕ загруженные файлы и отчёты?'))return;var keys=[];for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);if(k.indexOf('sf_')===0||k.indexOf('ss_')===0)keys.push(k)}keys.forEach(function(k){localStorage.removeItem(k)});alert('Очищено');renderDashboard()}
function exportAll(){
var data={events:events,user:curUser,date:new Date().toISOString(),files:{},subChecks:{}};
for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);if(k.indexOf('sf_')===0)data.files[k]=localStorage.getItem(k);if(k.indexOf('ss_')===0)data.subChecks[k]=localStorage.getItem(k)}
var blob=new Blob([JSON.stringify(data)],{type:"application/json"}),a=document.createElement("a");
a.href=URL.createObjectURL(blob);a.download="backup_pb_"+new Date().toISOString().slice(0,10)+".json";a.click();
alert("✅ Данные сохранены. Файл backup_pb_....json скачан. Его можно загрузить обратно кнопкой «Загрузить данные».")
}
function importAll(input){
if(!input.files.length)return;
var reader=new FileReader();
reader.onload=function(ev){
try{
var data=JSON.parse(ev.target.result);
if(!data.events||!data.files){alert("❌ Неверный формат файла");return}
if(!confirm("Загрузить данные из бекапа? Текущие данные будут заменены."))return;
// Save events
events=data.events;saveEvents();
// Save files and sub-checks
for(var k in data.files){if(data.files.hasOwnProperty(k))localStorage.setItem(k,data.files[k])}
for(var k in data.subChecks){if(data.subChecks.hasOwnProperty(k))localStorage.setItem(k,data.subChecks[k])}
alert("✅ Данные восстановлены. Обновите страницу.");
location.reload();
}catch(e){alert("❌ Ошибка чтения файла")}
};
reader.readAsText(input.files[0]);
}
function countFiles(id){
var total=0,main=getMD(id);
for(var k in main){if(main.hasOwnProperty(k))total+=(main[k].files||[]).length}
var e=null;for(var i=0;i<events.length;i++){if(events[i].id===id){e=events[i];break}}
if(e&&e.sub)for(var j=0;j<e.sub.length;j++){var sd=getMD(id,j);for(var sk in sd){if(sd.hasOwnProperty(sk))total+=(sd[sk].files||[]).length}}
return total;
}
function uploadFiles(eid,mk,ri,si){
mk=months[mk];var prefix=si>=0?'_s'+si:'',fi=document.getElementById('fi'+prefix);
if(!fi||!fi.files.length)return;
var desc=(document.getElementById('fd'+prefix)||{}).value;desc=(desc||'').trim();
var btn=document.getElementById('ub'+prefix);btn.textContent="Загружается...";btn.disabled=true;
var MAX=3072*1024,ad=getMD(eid,ri,si);if(!ad[mk])ad[mk]={report:"",files:[]};
var arr=ad[mk].files,pr=0,sk=0;
function fin(){
try{setMD(eid,ad,ri,si)}catch(e){
var bak=JSON.parse(JSON.stringify(ad));bak[mk].files=bak[mk].files.slice(0,-(pr-sk)||0);try{setMD(eid,bak,ri,si)}catch(e2){}
alert("⚠️ Хранилище заполнено. Очистите файлы на дашборде.");
}
addLog("загрузил файлы",eid,(pr-sk)+" файл(ов) за "+M(mk)+" ("+regions[ri]+")");
if(sk)alert(sk+" файл(ов) > 3 МБ пропущены");closeEM();openEdit(eid,curMonth,ri,si>=0?si:undefined)
}
// ===== FILE OPS =====
function upF(eid,mk,ri,si){
mk=months[mk];var pfx=si>=0?"_s"+si:"",fi=document.getElementById("fi"+pfx);
if(!fi||!fi.files.length)return;var desc=(document.getElementById("fd"+pfx)||{}).value;desc=(desc||"").trim();
var btn=document.getElementById("ub"+pfx);btn.textContent="...";btn.disabled=true;
var ad=getMD(eid,ri,si);if(!ad[mk])ad[mk]={report:"",files:[]};var arr=ad[mk].files,pr=0,sk=0,MAX=3072*1024;
function fin(){try{setMD(eid,ad,ri,si)}catch(e){alert("Хранилище заполнено")}if(sk)alert(sk+" файлов >3 МБ пропущено");addLog("файлы",eid,regions[ri]);closeEM();openEdit(eid,curMonth,ri,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}
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])}
@ -555,24 +451,13 @@ function uploadFiles(eid,mk,ri,si){
function dlF(eid,mk,idx,ri,si){si=si||-1;mk=months[mk];var ad=getMD(eid,ri,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,ri,si){si=si||-1;mk=months[mk];var ad=getMD(eid,ri,si);if(!ad[mk]||!ad[mk].files)return;ad[mk].files.splice(idx,1);setMD(eid,ad,ri,si);closeEM();openEdit(eid,curMonth,ri,si>=0?si:undefined)}
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)})});
// ===== INIT =====
function renderAll(){notifsUpdate();switchTab(document.querySelector(".tab-btn.active").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")});
function renderAll(){notifsUpdate();switchTab(document.querySelector(".tab-btn.active").dataset.tab)}
loadEvents();
var su=localStorage.getItem("samruk_u");if(su){try{curUser=JSON.parse(su);showApp()}catch(e){}}
</script>
</body>