658 lines
68 KiB
HTML
658 lines
68 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<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;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)}
|
||
.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)}
|
||
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}
|
||
|
||
#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}
|
||
.topbar .brand{font-weight:800;font-size:16px}.topbar .brand span{color:var(--cyan)}
|
||
.topbar .right{display:flex;align-items:center;gap:16px}
|
||
.topbar .user-info{font-size:13px;color:var(--gray-500)}.topbar .user-info strong{color:var(--ink)}
|
||
.notif-btn{position:relative;background:none;border:none;font-size:22px;cursor:pointer;padding:4px}
|
||
.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)}
|
||
|
||
.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}
|
||
.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 .sub{font-size:12px;color:var(--gray-500)}
|
||
.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}
|
||
|
||
.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}
|
||
.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 */
|
||
.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 SELECTOR */
|
||
.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)}
|
||
|
||
/* FILE ROW */
|
||
.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}
|
||
.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}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="loginScreen">
|
||
<form class="login-box" onsubmit="doLogin(event)">
|
||
<h1><span>ИИ-Агент</span> ПБ</h1>
|
||
<p class="sub">АО «Казахтелеком» — мониторинг производственной безопасности</p>
|
||
<label>Корпоративная почта</label>
|
||
<input type="email" id="loginEmail" placeholder="surname@telecom.kz" required>
|
||
<p class="hint">Например: ahmetov@telecom.kz, serikov@telecom.kz, admin@telecom.kz</p>
|
||
<label>Пароль</label>
|
||
<input type="password" id="loginPass" placeholder="••••••••" required>
|
||
<p class="err" id="loginErr">Неверная почта или пароль</p>
|
||
<button type="submit" class="btn" style="width:100%;margin-top:8px">Войти</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div id="app">
|
||
<div class="topbar">
|
||
<div class="brand"><span>ИИ-Агент</span> ПБ</div>
|
||
<div class="right">
|
||
<span class="user-info" id="userLabel"></span>
|
||
<div style="position:relative">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
<div class="main">
|
||
<div class="tabs">
|
||
<button class="tab-btn active" data-tab="myevents">Мои мероприятия</button>
|
||
<button class="tab-btn" data-tab="allevents">Весь реестр</button>
|
||
<button class="tab-btn" data-tab="analytics">Аналитика</button>
|
||
</div>
|
||
<div class="tab-content active" id="tab-myevents">
|
||
<div class="panel" style="border-radius:0 0 12px 12px">
|
||
<div class="stats-row" id="myStats"></div>
|
||
<div class="filters">
|
||
<select id="myStatusFilter" onchange="renderMyEvents()"><option value="">Все статусы</option><option value="warn">На контроле</option><option value="late">Просрочено</option><option value="done">Исполнено</option><option value="wait">В процессе</option></select>
|
||
<span class="user-info" id="myCount"></span>
|
||
</div>
|
||
<table id="myEventsTable"></table>
|
||
</div>
|
||
</div>
|
||
<div class="tab-content" id="tab-allevents">
|
||
<div class="panel" style="border-radius:0 0 12px 12px">
|
||
<div class="filters">
|
||
<input id="allSearch" placeholder="Поиск..." oninput="renderAllEvents()">
|
||
<select id="allStatusFilter" onchange="renderAllEvents()"><option value="">Все статусы</option><option value="warn">На контроле</option><option value="late">Просрочено</option><option value="done">Исполнено</option><option value="wait">В процессе</option></select>
|
||
<select id="allSecFilter" onchange="renderAllEvents()"><option value="">Все разделы</option><option value="0">I. Люди</option><option value="1">II. Оборудование</option><option value="2">III. Аварии и ЧС</option><option value="3">IV. Информ. работа</option><option value="4">V. ИИ и цифровизация</option></select>
|
||
</div>
|
||
<table id="allEventsTable"></table>
|
||
</div>
|
||
</div>
|
||
<div class="tab-content" id="tab-analytics">
|
||
<div class="stats-row" id="globalStats"></div>
|
||
<div class="panel" id="analyticsContent"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="editModalOverlay"><div class="modal" id="editModalContent"></div></div>
|
||
|
||
<script>
|
||
const sections=['I. Люди. Повышение культуры безопасности','II. Безопасность при эксплуатации оборудования','III. Предупреждение и готовность к ликвидации аварий и ЧС','IV. Информационно-разъяснительная работа','V. Внедрение ИИ и цифровизации']
|
||
const branches=['Дирекция производственной безопасности','Объединение «Дивизион «Сеть»»','Дивизион по корпоративному бизнесу','Дивизион по розничному бизнесу','Сервисная фабрика','Дирекция «Телеком Комплект»','Корпоративный университет','Дирекция управления проектами','Дивизион цифрового бизнеса']
|
||
const statusMap={done:'Исполнено',warn:'На контроле',late:'Просрочено',wait:'В процессе'}
|
||
const 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']
|
||
const monthNames=['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек']
|
||
|
||
function M(idx){ const [y,m]=months[idx].split('-'); return monthNames[parseInt(m)-1]+' '+y }
|
||
|
||
const baseEvents = [
|
||
{id:1,sec:0,b:6,s:'warn',p:45,due:'31.12.2026',done:'—',dname:'Протоколы обучения / Электронная ведомость',
|
||
r:'Генеральный директор КУ, Генеральные директора филиалов и ДАО',
|
||
t:'Продолжить проведение обучения и повышения квалификации руководителей и работников компании в соответствии с лучшими международными практиками, ориентированными на специфику условий труда, работы повышенной опасности и требований промышленной безопасности, а также развитие культуры безопасности, включая обучение производственного персонала по курсу «Культура безопасного труда», в том числе с применением VR, AR – технологий и цифровых симуляторов аварийных ситуаций по различным направлениям производственной безопасности (с правом выдачи сертификатов).',
|
||
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 и др.), и в случае необходимости, осуществить пересмотр внутренних нормативных документов филиалов/ДАО Общества в соответствии со «Стратегией развития производственной безопасности АО «Самрук-Қазына» на 2024-2028 гг.», включая установку значений ключевых показателей производственной безопасности.',
|
||
ai:'Анализ завершён в срок. ВНД пересмотрены. Ключевые показатели ПБ установлены.',
|
||
h:['10.01 — Мероприятие создано','15.02 — Проведён анализ','28.03 — Отчёт утверждён']},
|
||
{id:3,sec:0,b:0,s:'warn',p:50,due:'31.12.2026',done:'—',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:'—',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 — Утверждено']},
|
||
{id:6,sec:0,b:6,s:'warn',p:60,due:'30.06.2026',done:'—',dname:'ВНД по внутренним тренерам / Перечень тренеров',
|
||
r:'Генеральный директор КУ, Управляющий директор по персоналу / Генеральные директора филиалов и ДАО',
|
||
t:'Разработать/внести изменения в случае необходимости и утвердить внутренний нормативный документ, регламентирующий процедуру работы внутренних тренеров, в том числе по производственной безопасности, включая порядок их отбора, подготовки и привлечения, а также установление условий доплаты к основной заработной плате за выполнение тренерских функций.',
|
||
ai:'Проект ВНД на финальном согласовании. Перечень — 12 чел.',h:['01.03 — Создано','15.04 — Проект ВНД','01.06 — Перечень']},
|
||
{id:7,sec:0,b:1,s:'warn',p:40,due:'31.12.2026',done:'—',dname:'Материалы обмена опытом',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Проводить мероприятия по обмену опытом в области производственной безопасности.',
|
||
ai:'Проведён 1 выезд на площадку KEGOC. Онлайн-семинар — июль.',h:['15.02 — Создано','01.04 — Выезд','15.05 — План'],
|
||
sub:[{l:'a',t:'Продолжить практику обмена передовым опытом на площадке Комитета HSE, в том числе путем выездов на производственные объекты ПК'},
|
||
{l:'b',t:'Рассмотреть возможность организации обмена опытом с иностранными и казахстанскими компаниями, в том числе путем проведения онлайн-семинаров'}]},
|
||
{id:8,sec:0,b:4,s:'wait',p:15,due:'30.09.2026',done:'—',dname:'Заключительный Акт / Программа Well-being / Отчёт / Отчёт о микротравмах',
|
||
r:'a) Директор ДПБ / b) Ген. директор КУ, Упр. директор по персоналу / c,d) Ген. директора филиалов и ДАО',
|
||
t:'Провести анализ эффективности реализуемых мероприятий по охране здоровья.',
|
||
ai:'Медосмотры — Q3. Well-being Week — сентябрь. Ранний этап.',h:['01.04 — Создано','01.06 — Проект алгоритма'],
|
||
sub:[{l:'a',t:'Организовать и обеспечить 100% прохождение обязательных периодических медицинских осмотров работниками, включая офисных работников'},
|
||
{l:'b',t:'Организовать ежегодную «Неделю благополучия» (Well-being Week) для работников всех уровней'},
|
||
{l:'c',t:'Создать условия и обеспечить контроль за прохождением медицинского скрининга работников'},
|
||
{l:'d',t:'Внедрить алгоритм учета и расследования микротравм'}]},
|
||
{id:9,sec:0,b:6,s:'wait',p:20,due:'31.12.2026',done:'—',dname:'Результаты конкурсов / Пакет материалов',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность участия ДПБ/филиалов/ДАО Общества в международных/национальных конкурсах и отраслевых соревнованиях профессионального мастерства в области производственной безопасности.',
|
||
ai:'Определены 2 конкурса. Заявки готовятся.',h:['01.05 — Создано','01.06 — Отобраны конкурсы']},
|
||
{id:10,sec:1,b:1,s:'warn',p:55,due:'31.12.2026',done:'—',dname:'Аналитическая справка по филиалам/ДАО',
|
||
r:'Генеральный директор ОДС, Генеральный директор СФ, Генеральный директор ДУП, Генеральный директор ДИТ',
|
||
t:'Проводить работы по техническому перевооружению морально и физически изношенного оборудования, зданий и сооружений, эксплуатация которых из-за их технического состояния сопровождается повышенными рисками возникновения аварий и несчастных случаев с тяжёлыми и летальными исходами, в соответствии с ранее утвержденными Планами на 2024-2027 годы.',
|
||
ai:'Заменено 55% единиц. Отдельные филиалы отстают на 12%.',h:['01.01 — Переходящее','01.04 — Отчёт','01.06 — 55%']},
|
||
{id:11,sec:1,b:1,s:'warn',p:70,due:'30.06.2026',done:'—',dname:'Переутверждённая процедура / Фотоотчёт / Протоколы обучения',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Пересмотреть и актуализировать внутренний порядок выдачи нарядно-допускной системы, усилив законодательные требования РК путем внедрения в пилотном режиме практики применения сертификатов безопасности для одного из видов работ повышенной опасности.',
|
||
ai:'Процедура пересмотрена. Пилот запущен. Обучение — 85%.',h:['01.02 — Создано','01.04 — Проект','15.05 — Пилот']},
|
||
{id:12,sec:1,b:8,s:'wait',p:8,due:'30.09.2026',done:'—',dname:'Справка о внедрении / Фотоотчёт',
|
||
r:'Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность внедрения системы цифровой маркировки опасных технических устройств, предусматривающей присвоение каждому устройству QR-кода для быстрого доступа к паспорту, инструкции по эксплуатации и информации о проведенных технических освидетельствованиях.',
|
||
ai:'Проект на стадии ТЭО. Начало — июль.',h:['01.05 — Создано','01.06 — ТЭО']},
|
||
{id:13,sec:1,b:0,s:'warn',p:50,due:'31.12.2026',done:'—',dname:'Акты проверок / График',
|
||
r:'Директор ДПБ',
|
||
t:'Филиалам/ДАО Общества не реже 1 раза в квартал проводить проверку согласно адаптированным проверочным листам в области БиОТ, промышленной и пожарной безопасности в соответствии с требованиями законодательства Республики Казахстан.',
|
||
ai:'Q1 завершены. Q2 — по графику. Выявлено 23 нарушения.',h:['01.01 — Создано','31.03 — Q1','01.06 — Q2']},
|
||
{id:14,sec:1,b:0,s:'warn',p:40,due:'31.12.2026',done:'—',dname:'Письмо о предоставлении кандидата',
|
||
r:'Директор ДПБ',
|
||
t:'Продолжить практику участия в перекрёстных аудитах ПК, в том числе в соответствии с Планом-графиком проведения аудитов.',
|
||
ai:'Назначены 4 аудитора. Участвовали в 2 аудитах.',h:['15.01 — Назначены','01.03 — Аудит 1','15.04 — Аудит 2']},
|
||
{id:15,sec:1,b:0,s:'warn',p:48,due:'31.12.2026',done:'—',dname:'Аналитическая справка / Журнал опережающих индикаторов',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Усилить контроль за применением проактивных инструментов: мониторинг поведенческих аудитов/наблюдений безопасности, регистрация и расследование опасных условий, опасных действий и потенциально опасных происшествий Near Miss; право приостановки работы.',
|
||
ai:'147 Near Miss (+12% к 2025). Аудиты — 320 шт.',h:['01.01 — Создано','01.04 — Q1','01.06 — Q2']},
|
||
{id:16,sec:1,b:1,s:'done',p:85,due:'31.12.2026',done:'—',dname:'План-график аудитов / Акты / Протоколы совещаний',
|
||
r:'Генеральные директора филиалов и ДАО',
|
||
t:'Провести работу по повышению эффективности управления подрядными организациями.',
|
||
ai:'Q1 — 12 подрядчиков проверено. Стартовые совещания — 100%.',h:['15.01 — План','01.03 — Q1','01.06 — Q2'],
|
||
sub:[{l:'a',t:'Обеспечить проведение аудита подрядчиков внутри филиалов/ДАО согласно типового чек-листа Фонда'},
|
||
{l:'b',t:'Продолжить практику проведения стартовых/установочных совещаний с подрядными организациями перед допуском на объект'}]},
|
||
{id:17,sec:1,b:0,s:'warn',p:35,due:'31.12.2026',done:'—',dname:'Отчёты / График проверок / Фотоотчёт',
|
||
r:'a) Главный административный директор, Директор ДПБ / b) Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить контроль за состоянием производственной безопасности на производственных объектах.',
|
||
ai:'CEO-1 проверил 2 филиала. 6 выездов. Активность ниже плана.',h:['01.02 — Создано','15.03 — Проверка 1','01.05 — Проверка 2'],
|
||
sub:[{l:'a',t:'Руководителям Общества уровня СЕО-1 не реже 1 раза в квартал лично проверять одно из филиалов/подрядных организаций'},
|
||
{l:'b',t:'Первым руководителям филиалов лично принимать участие во внутреннем производственном контроле с посещением площадок не реже 1 раза в квартал'}]},
|
||
{id:18,sec:1,b:1,s:'done',p:90,due:'31.12.2026',done:'—',dname:'Ежемесячный сводный отчёт',
|
||
r:'Генеральные директора филиалов и ДАО, Директор ДПБ',
|
||
t:'Обеспечить контроль за состоянием транспортной безопасности, в том числе путем ежемесячного мониторинга нарушений требований транспортной безопасности со стороны штатных водителей и водителей подрядных организаций, с последующим применением предусмотренных договорами мер воздействия.',
|
||
ai:'34 нарушения. Штрафы — 12 водителей. Тренд — снижение.',h:['01.01 — Создано','01.02 — Отчёт янв','01.06 — Отчёт май']},
|
||
{id:19,sec:2,b:1,s:'warn',p:30,due:'31.12.2026',done:'—',dname:'Акты тренировок / Пресс-релизы',
|
||
r:'a) Управляющий директор по безопасности / b) Генеральный директор СФ / c) Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить проведение учебных тревог и тренировок.',
|
||
ai:'1 учение. Пожарные: 1 из 2. Первая помощь — Q3.',h:['01.02 — Создано','15.03 — Учение','15.05 — Пожарная №1'],
|
||
sub:[{l:'a',t:'Не менее одной учебной тревоги и/или противоаварийной тренировки по ликвидации крупной аварии, ЧС на опасном производственном объекте'},
|
||
{l:'b',t:'Не менее двух тренировок по тушению пожара в административных зданиях (офисах)'},
|
||
{l:'c',t:'Не менее одного практического занятия по оказанию первой помощи с применением симуляционного оборудования'}]},
|
||
{id:20,sec:2,b:0,s:'warn',p:65,due:'30.06.2026',done:'—',dname:'Приказ CMS / Материалы обучения / Акты штабов',
|
||
r:'Управляющий директор по безопасности, Генеральные директора филиалов и ДАО',
|
||
t:'Усилить работу по реагированию на ЧС.',
|
||
ai:'CMS подписана. Обучение — 60%. 1 заседание штаба.',h:['01.03 — Создано','01.04 — Приказ','15.05 — Штаб №1'],
|
||
sub:[{l:'a',t:'Внедрить процедуру «Crisis Management System» для своевременной реакции на кризисные события'},
|
||
{l:'b',t:'Рассмотреть возможность проведения обучения для ответственных работников по действиям в условиях ЧС'},
|
||
{l:'c',t:'Провести не менее двух заседаний штабов с целью отработки действий на практике'}]},
|
||
{id:21,sec:3,b:0,s:'done',p:100,due:'31.12.2026',done:'15.02.2026',dname:'Публикация на информационных порталах',
|
||
r:'Директор ДПБ, Пресс-секретарь ЦА',
|
||
t:'Обеспечить выпуск обращения от Председателя Правления ПК о важности соблюдения требований по производственной безопасности.',
|
||
ai:'Опубликовано. Охват — 100%.',h:['15.01 — Проект','01.02 — Подписание','15.02 — Публикация']},
|
||
{id:22,sec:3,b:0,s:'wait',p:15,due:'31.12.2026',done:'—',dname:'Протоколы форумов / Протоколы семинаров',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО / Департамент по коммуникациям',
|
||
t:'Проведение мероприятий по производственной безопасности.',
|
||
ai:'Форум — октябрь. Семинары — 2 площадки.',h:['01.05 — Создано','01.06 — Площадки'],
|
||
sub:[{l:'a',t:'Стратегические сессии/Форумы для первых руководителей филиалов/ДАО'},
|
||
{l:'b',t:'Семинары для подрядных организаций ПК на отдельных площадках филиалов/ДАО'}]},
|
||
{id:23,sec:3,b:6,s:'wait',p:10,due:'30.09.2026',done:'—',dname:'Протокол итогов Олимпиады',
|
||
r:'Директор ДПБ',
|
||
t:'Проведение Олимпиады по производственной безопасности среди специалистов производственной безопасности Общества и подрядных организаций на уровне Общества.',
|
||
ai:'Положение на согласовании.',h:['01.05 — Создано','01.06 — Проект']},
|
||
{id:24,sec:3,b:0,s:'done',p:92,due:'31.12.2026',done:'—',dname:'Бюллетени / Листы ознакомления',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить ознакомление всех работников филиалов/ДАО Общества с обстоятельствами несчастных случаев с тяжелым и летальным исходами, произошедших в ПК Фонда, посредством направления информационных бюллетеней, в том числе с использованием цифровых решений либо в рамках внеплановых инструктажей.',
|
||
ai:'3 бюллетеня. Ознакомление — 92%.',h:['01.01 — Создано','15.02 — №1','01.05 — №3']},
|
||
{id:25,sec:3,b:6,s:'warn',p:40,due:'31.12.2026',done:'—',dname:'Публикации SK News / Материалы мероприятий',
|
||
r:'a) Управляющий директор по персоналу, Департамент по коммуникациям / b,c,d) Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Проведение молодежных проектных инициатив в рамках работы Центра молодых работников по производственной безопасности.',
|
||
ai:'2 истории. 1 выезд. 3 специалиста в аудитах.',h:['01.02 — Создано','15.03 — Публикация','01.05 — Выезд'],
|
||
sub:[{l:'a',t:'Публикация реальных историй из трудовой жизни работников в корпоративном журнале «SK News»'},
|
||
{l:'b',t:'Посещение рабочих мест, где в 2022-2025 гг. произошли несчастные случаи с летальным или тяжелым исходом'},
|
||
{l:'c',t:'Привлечение молодых специалистов по ПБ в перекрёстные аудиты ДЗО ПК'},
|
||
{l:'d',t:'Онлайн-семинары/прямые эфиры на различные темы по вопросам производственной безопасности'}]},
|
||
{id:26,sec:3,b:2,s:'warn',p:50,due:'31.12.2026',done:'—',dname:'Видеоролики / Постеры / Брошюры',
|
||
r:'a) Генеральные директора филиалов и ДАО / b,c) Директор ДПБ, Департамент по коммуникациям / d) Директор ДПБ',
|
||
t:'Усилить наглядную агитацию по производственной безопасности.',
|
||
ai:'2 видеоролика. Постеры распространены.',h:['01.02 — Создано','01.04 — Ролик №1','01.06 — Ролик №2'],
|
||
sub:[{l:'a',t:'Разработка и использование видеороликов/презентаций по ПБ, в т.ч. с участием получивших травму работников'},
|
||
{l:'b',t:'Разработка серий видеороликов «Безопасность будущего» и по профилактике травматизма'},
|
||
{l:'c',t:'Рассмотреть возможность выпуска подкаста с участием трудовых династий'},
|
||
{l:'d',t:'Разработка и распространение постеров, брошюр, информационных рассылок'}]},
|
||
{id:27,sec:3,b:3,s:'warn',p:30,due:'31.12.2026',done:'—',dname:'Фотофиксация',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотрение возможности организации встреч коллектива с получившими производственные травмы работниками (с их согласия) с целью предупреждения аналогичных случаев травматизма.',
|
||
ai:'1 встреча. Согласия от 3 работников.',h:['01.03 — Создано','01.05 — Встреча']},
|
||
{id:28,sec:3,b:0,s:'warn',p:25,due:'31.12.2026',done:'—',dname:'Информационное письмо / Пресс-релизы',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО / Департамент по коммуникациям',
|
||
t:'Проведение мероприятий, направленных на пропаганду безопасного выполнения работ через семейные ценности.',
|
||
ai:'5 писем. Семейный день — август.',h:['01.04 — Создано','15.05 — Письма'],
|
||
sub:[{l:'a',t:'Направление информационного письма членам семьи работника, положительно отличившегося в вопросах ПБ'},
|
||
{l:'b',t:'Проведение Семейных дней охраны труда и дней открытых дверей для семей работников'},
|
||
{l:'c',t:'Проведение конкурса рисунков среди работников и их детей на тему «Спецодежда будущего!»'}]},
|
||
{id:29,sec:3,b:6,s:'late',p:40,due:'30.06.2026',done:'—',dname:'Корпоративный сборник лучших практик',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Разработать корпоративный сборник лучших практик по производственной безопасности в формате методического пособия или интерактивного PDF документа, отражающий меры по снижению производственного травматизма и управлению критическими рисками.',
|
||
ai:'Сборник в разработке. Риск срыва Q2.',h:['01.03 — Создано','01.05 — 4 филиала','01.06 — Эскалация']},
|
||
{id:30,sec:3,b:7,s:'warn',p:60,due:'31.12.2026',done:'—',dname:'Предложения / План реализации',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Сбор предложений по совершенствованию системы управления производственной безопасности посредством применения цифровых решений с консолидацией в ДПБ.',
|
||
ai:'18 предложений. 5 в реализацию.',h:['01.01 — Создано','01.04 — 10','01.06 — 18']},
|
||
{id:31,sec:3,b:0,s:'warn',p:75,due:'30.06.2026',done:'—',dname:'Видеообзор',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Разработка видеообзора кейсов происшествий в ПК с учетом специфики деятельности (из доступных на открытых медиа источниках) для наглядной демонстрации и разъяснения работникам о необходимости и важности соблюдения требований безопасности.',
|
||
ai:'Монтаж 75%. Озвучка — 15 июня.',h:['01.03 — Создано','01.05 — Сценарий','01.06 — 75%']},
|
||
{id:32,sec:4,b:8,s:'warn',p:70,due:'30.06.2026',done:'—',dname:'Справка / Скриншоты чат-бота',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить применение в филиалах/ДАО Общества чат-бот ИИ ассистент по производственной безопасности с целью упрощения доступа к нормативно-правовым актам Республики Казахстан и ВНД группы Фонда.',
|
||
ai:'Чат-бот тестируется. Пилот — 15 июня.',h:['01.02 — Создано','01.04 — Разработка','01.06 — Тест']},
|
||
{id:33,sec:4,b:8,s:'wait',p:15,due:'31.12.2026',done:'—',dname:'Справка / Скриншоты системы',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить применение в филиалах/ДАО Общества интегрированную систему анализа и предупреждения несчастных случаев и критических происшествий, а также платформу по идентификации и оценке рисков перед началом проведения работ на опасных производственных объектах.',
|
||
ai:'ТЗ согласовывается. Интеграция прорабатывается.',h:['01.04 — Создано','01.06 — ТЗ']},
|
||
{id:34,sec:4,b:8,s:'wait',p:10,due:'31.12.2026',done:'—',dname:'Справка / Скриншоты HSE паспорта',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность запуска в филиалах/ДАО Общества электронного HSE паспорта на каждого работника с последующей интеграцией в корпоративную цифровую систему.',
|
||
ai:'Концепция утверждена. Подрядчик выбран.',h:['01.05 — Создано','01.06 — Концепция']},
|
||
{id:35,sec:4,b:5,s:'wait',p:8,due:'31.12.2026',done:'—',dname:'Справка / Скриншоты системы',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность внедрения системы оформления нарядов-допусков на проведение работ повышенной опасности в электронном виде в филиалах/ДАО Общества.',
|
||
ai:'Предпроект. Анализ рынка — Q3.',h:['01.05 — Создано','01.06 — Анализ']}
|
||
]
|
||
|
||
const 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'}
|
||
}
|
||
|
||
let currentUser=null, events=JSON.parse(JSON.stringify(baseEvents))
|
||
function loadState(){
|
||
const s=localStorage.getItem('samruk_events');if(s) events=JSON.parse(s)
|
||
const u=localStorage.getItem('samruk_user');if(u){currentUser=JSON.parse(u);showApp()}
|
||
}
|
||
function saveState(){localStorage.setItem('samruk_events',JSON.stringify(events))}
|
||
function saveUser(){localStorage.setItem('samruk_user',JSON.stringify(currentUser))}
|
||
function doLogin(e){
|
||
e.preventDefault()
|
||
const email=document.getElementById('loginEmail').value.trim().toLowerCase()
|
||
const pass=document.getElementById('loginPass').value
|
||
if(users[email]&&pass.length>=1){currentUser={email,...users[email]};saveUser();showApp()}
|
||
else document.getElementById('loginErr').style.display='block'
|
||
}
|
||
function doLogout(){localStorage.removeItem('samruk_user');currentUser=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>${currentUser.name}</strong> · ${branches[currentUser.branch]}`;renderAll()}
|
||
|
||
function getNotifs(){
|
||
const my=currentUser.role==='admin'||currentUser.role==='director'?events:events.filter(e=>e.b===currentUser.branch)
|
||
const n=[]
|
||
my.forEach(e=>{if(e.s==='late')n.push({type:'danger',msg:`Просрочено: ${e.t.slice(0,60)}...`,time:e.due});if(e.s==='warn'&&e.p<30)n.push({type:'warn',msg:`Низкий прогресс (${e.p}%): ${e.t.slice(0,50)}...`,time:'Сейчас'})})
|
||
return n
|
||
}
|
||
function renderNotifs(){
|
||
const n=getNotifs(),el=document.getElementById('notifDrop'),cnt=document.getElementById('notifCount')
|
||
cnt.textContent=n.length;cnt.style.display=n.length?'inline-block':'none'
|
||
el.innerHTML=n.length?n.map(n=>`<div class="item"><div class="title">${n.type==='danger'?'🔴':'🟡'} ${n.msg}</div><div class="time">${n.time}</div></div>`).join(''):'<div class="empty">Новых уведомлений нет</div>'
|
||
}
|
||
function toggleNotif(){renderNotifs();document.getElementById('notifDrop').classList.toggle('open')}
|
||
|
||
function sBadge(s){const m={done:'green',warn:'amber',late:'red',wait:'gray'};return`<span class="badge ${m[s]}">${statusMap[s]}</span>`}
|
||
function pctHtml(p){return`<div class="pct-bar"><div class="track"><div class="fill" style="width:${p}%;background:${p>=80?'var(--green)':p>=40?'var(--amber)':'var(--red)'}"></div></div>${p}%</div>`}
|
||
|
||
function getMyEvents(){if(!currentUser)return[];if(currentUser.role==='admin'||currentUser.role==='director')return events;return events.filter(e=>e.b===currentUser.branch)}
|
||
|
||
function renderMyEvents(){
|
||
const sf=document.getElementById('myStatusFilter').value
|
||
let list=getMyEvents();if(sf)list=list.filter(e=>e.s===sf)
|
||
const done=list.filter(e=>e.s==='done').length,late=list.filter(e=>e.s==='late').length,warn=list.filter(e=>e.s==='warn').length,wait=list.filter(e=>e.s==='wait').length
|
||
document.getElementById('myStats').innerHTML=`<div class="stat-card"><div class="lbl">Мои мероприятия</div><div class="num">${list.length}</div></div><div class="stat-card green"><div class="lbl">Исполнено</div><div class="num">${done}</div></div><div class="stat-card amber"><div class="lbl">На контроле</div><div class="num">${warn}</div></div><div class="stat-card red"><div class="lbl">Просрочено</div><div class="num">${late}</div></div><div class="stat-card blue"><div class="lbl">В процессе</div><div class="num">${wait}</div></div>`
|
||
document.getElementById('myCount').textContent=`Найдено: ${list.length}`
|
||
document.getElementById('myEventsTable').innerHTML=`<tr><th>№</th><th>Мероприятие</th><th>Раздел</th><th>Срок</th><th>Прогресс</th><th>Статус</th><th></th></tr>${list.map(e=>`<tr><td>${e.id}</td><td style="font-size:12px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${e.t}">${e.t}</td><td><span class="badge blue">${['I','II','III','IV','V'][e.sec]}</span></td><td style="font-size:12px">${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>`).join('')}`
|
||
}
|
||
function renderAllEvents(){
|
||
const search=document.getElementById('allSearch').value.toLowerCase(),sf=document.getElementById('allStatusFilter').value,secF=document.getElementById('allSecFilter').value
|
||
let list=events;if(search)list=list.filter(e=>e.t.toLowerCase().includes(search)||branches[e.b].toLowerCase().includes(search));if(sf)list=list.filter(e=>e.s===sf);if(secF!=='')list=list.filter(e=>e.sec===parseInt(secF))
|
||
document.getElementById('allEventsTable').innerHTML=`<tr><th>№</th><th>Мероприятие</th><th>Дивизион</th><th>Раздел</th><th>Срок</th><th>Прогресс</th><th>Статус</th></tr>${list.map(e=>`<tr><td>${e.id}</td><td style="font-size:12px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${e.t}">${e.t}</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 style="font-size:12px">${e.due}</td><td>${pctHtml(e.p)}</td><td>${sBadge(e.s)}</td></tr>`).join('')}`
|
||
}
|
||
function renderAnalytics(){
|
||
let done=events.filter(e=>e.s==='done').length,total=events.length
|
||
document.getElementById('globalStats').innerHTML=`<div class="stat-card"><div class="lbl">Всего</div><div class="num">${total}</div></div><div class="stat-card green"><div class="lbl">Исполнено</div><div class="num">${done}</div><div class="sub">${Math.round(done/total*100)}%</div></div><div class="stat-card amber"><div class="lbl">На контроле</div><div class="num">${events.filter(e=>e.s==='warn').length}</div></div><div class="stat-card red"><div class="lbl">Просрочено</div><div class="num">${events.filter(e=>e.s==='late').length}</div></div>`
|
||
document.getElementById('analyticsContent').innerHTML=`<h3>Рейтинг дивизионов</h3>${branches.map((b,i)=>{let items=events.filter(e=>e.b===i),d=items.filter(e=>e.s==='done').length,pct=Math.round(d/Math.max(1,items.length)*100);return`<div class="pct-bar" style="margin-bottom:8px"><span style="width:220px;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;width:60px;text-align:right">${pct}%</span></div>`}).join('')}`
|
||
}
|
||
|
||
// ===== FILE STORAGE: month-keyed with reports =====
|
||
// { "2026-01": { report: "текст отчёта", files: [{name,size,type,desc,date,data},...] }, ... }
|
||
function getMonthData(eventId){ return JSON.parse(localStorage.getItem('samruk_files_'+eventId)||'{}') }
|
||
function setMonthData(eventId,obj){ localStorage.setItem('samruk_files_'+eventId,JSON.stringify(obj)) }
|
||
function getSubChecks(eventId){ return JSON.parse(localStorage.getItem('samruk_sub_'+eventId)||'[]') }
|
||
function setSubChecks(eventId,arr){ localStorage.setItem('samruk_sub_'+eventId,JSON.stringify(arr)) }
|
||
|
||
// ===== EDIT MODAL =====
|
||
let editMonthIdx=5 // default June
|
||
|
||
function openEdit(id,monthIdx){
|
||
if(typeof monthIdx==='number') editMonthIdx=monthIdx
|
||
const e=events.find(x=>x.id===id);if(!e)return
|
||
const savedEdits=JSON.parse(localStorage.getItem('samruk_edits_'+e.id)||'{}')
|
||
const allData=getMonthData(e.id)
|
||
const subChecks=getSubChecks(e.id)
|
||
const curMonth=months[editMonthIdx]
|
||
const curData=allData[curMonth]||{report:'',files:[]}
|
||
const curFiles=curData.files||[]
|
||
let totalFiles=0;Object.values(allData).forEach(d=>{totalFiles+=(d.files||[]).length})
|
||
|
||
let 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.map((s,i)=>{let ch=subChecks.includes(i);return`<div class="sub-item"><input type="checkbox" id="subchk_${i}" ${ch?'checked':''}><span class="sub-label">${s.l})</span><span class="sub-text">${s.t}</span></div>`}).join('')}</div>`
|
||
}
|
||
|
||
let filesHtml=''
|
||
if(curFiles.length){
|
||
filesHtml=`<div style="font-weight:600;margin:12px 0 6px;font-size:13px">Файлы за ${M(editMonthIdx)} (${curFiles.length} шт.)</div>`
|
||
curFiles.forEach((f,i)=>{filesHtml+=`<div class="file-row"><span class="file-info"><span class="file-name" onclick="downloadFile(${e.id},'${curMonth}',${i})">📄 ${f.name}</span>${f.desc?`<span class="file-desc">${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>`})}
|
||
}
|
||
|
||
document.getElementById('editModalContent').innerHTML=`
|
||
<button class="close" onclick="closeEditModal()">×</button>
|
||
<span class="badge blue">Раздел ${['I','II','III','IV','V'][e.sec]}</span>
|
||
<h3 style="margin:8px 0">${e.t}</h3>
|
||
<div class="meta-row">
|
||
<div class="fld">Дивизион<strong>${branches[e.b]}</strong></div>
|
||
<div class="fld">Ответственный<strong>${e.r}</strong></div>
|
||
<div class="fld">Срок<strong>${e.due}</strong></div>
|
||
<div class="fld">Факт<strong>${e.done}</strong></div>
|
||
</div>
|
||
|
||
<div class="field"><label>Статус</label><select id="editStatus" onchange="autoProgress()"><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>
|
||
<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>
|
||
<div class="field"><label>Комментарий</label><textarea id="editComment" placeholder="Комментарий к статусу...">${savedEdits.comment||''}</textarea></div>
|
||
|
||
${subHtml}
|
||
|
||
<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:12px">
|
||
<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>
|
||
|
||
<div class="month-tabs">${months.map((m,i)=>`<span class="month-tab${i===editMonthIdx?' active':''}" onclick="openEdit(${e.id},${i})">${M(i)}</span>`).join('')}</div>
|
||
|
||
<div class="field" style="margin-top:12px">
|
||
<label>Текст отчёта за ${M(editMonthIdx)}</label>
|
||
<textarea id="monthReport" placeholder="Опишите ход исполнения, результаты, проблемы... Можно без прикрепления файлов." style="min-height:80px">${curData.report||''}</textarea>
|
||
</div>
|
||
|
||
${filesHtml}
|
||
|
||
<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>
|
||
<p style="font-size:11px;color:var(--gray-500);margin-top:6px">Формы завершения: ${e.dname}</p>
|
||
</div>
|
||
|
||
<div class="ai-block"><h4>🤖 Вывод ИИ-агента</h4>${e.ai}</div>
|
||
<div style="font-weight:600;margin:8px 0 4px;font-size:14px">История:</div>
|
||
<div>${e.h.map(h=>`<div class="history-item"><div class="dot"></div>${h}</div>`).join('')}</div>
|
||
|
||
<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('editModalOverlay').classList.add('open')
|
||
}
|
||
|
||
// File list for current month
|
||
let filesHtml=''
|
||
if(curFiles.length){
|
||
filesHtml=`<div style="font-weight:600;margin:12px 0 6px;font-size:13px">Загруженные файлы за ${M(editMonthIdx)} (${curFiles.length} шт.)</div>`
|
||
curFiles.forEach((f,i)=>{filesHtml+=`<div class="file-row"><span class="file-info"><span class="file-name" onclick="downloadFile(${e.id},'${curMonth}',${i})">📄 ${f.name}</span><span class="file-desc">${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>`})
|
||
}else{filesHtml=`<p style="font-size:13px;color:var(--gray-500);margin:12px 0 6px">Нет файлов за ${M(editMonthIdx)}</p>`}
|
||
|
||
document.getElementById('editModalContent').innerHTML=`
|
||
<button class="close" onclick="closeEditModal()">×</button>
|
||
<span class="badge blue">Раздел ${['I','II','III','IV','V'][e.sec]}</span>
|
||
<h3 style="margin:8px 0">${e.t}</h3>
|
||
<div class="meta-row">
|
||
<div class="fld">Дивизион<strong>${branches[e.b]}</strong></div>
|
||
<div class="fld">Ответственный<strong>${e.r}</strong></div>
|
||
<div class="fld">Срок<strong>${e.due}</strong></div>
|
||
<div class="fld">Факт<strong>${e.done}</strong></div>
|
||
</div>
|
||
|
||
<div class="field"><label>Статус</label><select id="editStatus" onchange="autoProgress()"><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>
|
||
<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>
|
||
<div class="field"><label>Комментарий</label><textarea id="editComment" placeholder="Комментарий к статусу...">${savedEdits.comment||''}</textarea></div>
|
||
|
||
${subHtml}
|
||
|
||
<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:12px">
|
||
<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>
|
||
|
||
<div class="month-tabs">${months.map((m,i)=>`<span class="month-tab${i===editMonthIdx?' active':''}" onclick="openEdit(${e.id},${i})">${M(i)}</span>`).join('')}</div>
|
||
|
||
${filesHtml}
|
||
|
||
<div class="upload-row">
|
||
<input type="text" id="fileDesc" placeholder="Описание (акт, протокол, фото...)" style="flex:2">
|
||
<input type="file" id="editFileInput" multiple>
|
||
<button class="btn btn-sm" onclick="uploadFiles(${e.id},'${curMonth}')">Загрузить</button>
|
||
</div>
|
||
<p style="font-size:11px;color:var(--gray-500);margin-top:6px">Формы завершения: ${e.dname}</p>
|
||
</div>
|
||
|
||
<div class="ai-block"><h4>🤖 Вывод ИИ-агента</h4>${e.ai}</div>
|
||
<div style="font-weight:600;margin:8px 0 4px;font-size:14px">История:</div>
|
||
<div>${e.h.map(h=>`<div class="history-item"><div class="dot"></div>${h}</div>`).join('')}</div>
|
||
|
||
<div style="margin-top:20px;display:flex;gap:12px">
|
||
<button class="btn" onclick="saveEdit(${e.id})">Сохранить</button>
|
||
<button class="btn btn-outline" onclick="closeEditModal()">Отмена</button>
|
||
</div>`
|
||
document.getElementById('editModalOverlay').classList.add('open')
|
||
}
|
||
|
||
function uploadFiles(eventId, monthKey){
|
||
const fi=document.getElementById('editFileInput')
|
||
const desc=document.getElementById('fileDesc').value.trim()
|
||
const btn=document.getElementById('uploadBtn')
|
||
if(!fi||!fi.files||!fi.files.length) return
|
||
btn.textContent='Загружается...';btn.disabled=true
|
||
const MAX=4*1024*1024,allData=getMonthData(eventId)
|
||
if(!allData[monthKey]) allData[monthKey]={report:'',files:[]}
|
||
const arr=allData[monthKey].files
|
||
let processed=0,skipped=0
|
||
function finish(){
|
||
try{setMonthData(eventId,allData)}catch(e){alert('⚠️ Хранилище переполнено')}
|
||
if(skipped) alert(`⚠️ ${skipped} файл(ов) > 4 МБ пропущены`)
|
||
closeEditModal();openEdit(eventId)
|
||
}
|
||
if(!fi.files.length){finish();return}
|
||
for(const f of fi.files){
|
||
if(f.size>MAX){skipped++;processed++;if(processed===fi.files.length)finish();continue}
|
||
const r=new FileReader()
|
||
r.onload=function(ev){
|
||
arr.push({name:f.name,size:f.size,type:f.type,desc,date:new Date().toLocaleDateString(),data:ev.target.result})
|
||
processed++
|
||
if(processed===fi.files.length) finish()
|
||
}
|
||
r.onerror=function(){processed++;if(processed===fi.files.length)finish()}
|
||
r.readAsDataURL(f)
|
||
}
|
||
}
|
||
|
||
function downloadFile(eventId, monthKey, idx){
|
||
const allData=getMonthData(eventId),arr=allData[monthKey]?.files
|
||
if(!arr||!arr[idx]||!arr[idx].data) return
|
||
const 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 removeFile(eventId, monthKey, idx){
|
||
const allData=getMonthData(eventId)
|
||
if(!allData[monthKey]||!allData[monthKey].files) return
|
||
allData[monthKey].files.splice(idx,1);setMonthData(eventId,allData)
|
||
closeEditModal();openEdit(eventId)
|
||
}
|
||
|
||
function saveEdit(id, monthKey){
|
||
const e=events.find(x=>x.id===id);if(!e)return
|
||
e.s=document.getElementById('editStatus').value
|
||
e.p=parseInt(document.getElementById('editProgress').value)
|
||
const comment=document.getElementById('editComment').value.trim()
|
||
const monthReport=document.getElementById('monthReport')?.value||''
|
||
|
||
// Save monthly report text
|
||
if(monthKey){
|
||
const allData=getMonthData(id)
|
||
if(!allData[monthKey]) allData[monthKey]={report:'',files:[]}
|
||
allData[monthKey].report=monthReport
|
||
setMonthData(id,allData)
|
||
}
|
||
|
||
if(e.sub&&e.sub.length){
|
||
const checks=[]
|
||
e.sub.forEach((_,i)=>{if(document.getElementById('subchk_'+i)?.checked)checks.push(i)})
|
||
setSubChecks(id,checks)
|
||
}
|
||
|
||
const now=new Date().toLocaleDateString()
|
||
e.h.push(`${now} — ${currentUser.name}: статус ${statusMap[e.s]}, прогресс ${e.p}%${comment?' — комм.: '+comment:''}`)
|
||
if(e.s==='done'&&e.done==='—')e.done=now
|
||
localStorage.setItem('samruk_edits_'+id,JSON.stringify({comment,editedBy:currentUser.name,editedAt:now}))
|
||
saveState();closeEditModal();renderAll();renderNotifs()
|
||
}
|
||
|
||
function closeEditModal(){document.getElementById('editModalOverlay').classList.remove('open')}
|
||
|
||
function autoProgress(){
|
||
const s=document.getElementById('editStatus'),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+'%'
|
||
}
|
||
|
||
document.querySelectorAll('.tab-btn').forEach(btn=>{btn.addEventListener('click',function(){document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));document.querySelectorAll('.tab-content').forEach(c=>c.classList.remove('active'));this.classList.add('active');document.getElementById('tab-'+this.dataset.tab).classList.add('active');if(this.dataset.tab==='analytics')renderAnalytics()})})
|
||
document.getElementById('editModalOverlay').addEventListener('click',function(e){if(e.target===this)closeEditModal()})
|
||
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeEditModal();document.getElementById('notifDrop').classList.remove('open')}})
|
||
document.addEventListener('click',e=>{if(!e.target.closest('.notif-btn')&&!e.target.closest('.notif-drop'))document.getElementById('notifDrop').classList.remove('open')})
|
||
|
||
function renderAll(){renderMyEvents();renderAllEvents();renderNotifs()}
|
||
loadState();if(currentUser)showApp()
|
||
</script>
|
||
</body>
|
||
</html> |