764 lines
64 KiB
HTML
764 lines
64 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;--gray-700:#374151;--green:#10B981;--red:#EF4444;--amber:#F59E0B;--blue:#3B82F6;--sidebar-w:260px}
|
||
*{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)}
|
||
.btn-red{background:var(--red);color:#fff}.btn-red:hover{background:#dc2626}
|
||
.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}
|
||
|
||
/* LOGIN SCREEN */
|
||
#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}
|
||
|
||
/* MAIN APP (hidden) */
|
||
#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 DROPDOWN */
|
||
.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;cursor:default}
|
||
.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);font-size:14px}
|
||
|
||
/* TABS */
|
||
.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}
|
||
|
||
/* LAYOUT */
|
||
.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 */
|
||
.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:700px;width:92vw;max-height:88vh;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:16px}
|
||
.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:80px;resize:vertical}
|
||
.modal .meta-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}
|
||
.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 .docs{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0}
|
||
.modal .docs .doc-chip{background:var(--gray-100);padding:6px 12px;border-radius:6px;font-size:13px;display:flex;align-items:center;gap:6px;border:1px solid transparent;transition:.15s}
|
||
.modal .docs .doc-chip button{background:none;border:none;cursor:pointer;color:var(--red);font-size:14px;line-height:1}
|
||
.doc-chip:hover{background:var(--cyan-50);color:var(--ink);border-color:var(--cyan)}
|
||
.file-upload{border:2px dashed var(--gray-200);border-radius:8px;padding:20px;text-align:center;color:var(--gray-500);cursor:pointer;margin-bottom:12px;font-size:14px;transition:.15s}
|
||
.file-upload:hover{border-color:var(--cyan);color:var(--cyan)}
|
||
.file-upload .icon{font-size:28px;display:block;margin-bottom:4px}
|
||
.ai-block{background:var(--cyan-50);border-radius:8px;padding:16px;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}
|
||
|
||
@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)}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- LOGIN SCREEN -->
|
||
<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, dpt@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>
|
||
|
||
<!-- MAIN APP -->
|
||
<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()" title="Уведомления">🔔<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>
|
||
|
||
<!-- EDIT MODAL -->
|
||
<div class="modal-overlay" id="editModalOverlay">
|
||
<div class="modal" id="editModalContent"></div>
|
||
</div>
|
||
|
||
<script>
|
||
// ===== DATA =====
|
||
const sections = [
|
||
'I. Люди. Повышение культуры безопасности',
|
||
'II. Безопасность при эксплуатации оборудования',
|
||
'III. Предупреждение и готовность к ликвидации аварий и ЧС',
|
||
'IV. Информационно-разъяснительная работа',
|
||
'V. Внедрение ИИ и цифровизации'
|
||
]
|
||
const branches = [
|
||
'Дирекция производственной безопасности',
|
||
'Объединение «Дивизион «Сеть»»',
|
||
'Дивизион по корпоративному бизнесу',
|
||
'Дивизион по розничному бизнесу',
|
||
'Сервисная фабрика',
|
||
'Дирекция «Телеком Комплект»',
|
||
'Корпоративный университет',
|
||
'Дирекция управления проектами',
|
||
'Дивизион цифрового бизнеса'
|
||
]
|
||
const statusMap = {done:'Исполнено',warn:'На контроле',late:'Просрочено',wait:'Не начато'}
|
||
|
||
// 35 events — exact titles from approved plan
|
||
const baseEvents = [
|
||
{id:1,sec:0,b:6,s:'warn',p:45,due:'31.12.2026',done:'—',
|
||
r:'Генеральный директор КУ, Генеральные директора филиалов и ДАО',
|
||
t:'Продолжить проведение обучения и повышения квалификации руководителей и работников компании в соответствии с лучшими международными практиками, ориентированными на специфику условий труда, работы повышенной опасности и требований промышленной безопасности, а также развитие культуры безопасности, включая обучение производственного персонала по курсу «Культура безопасного труда», в том числе с применением VR, AR – технологий и цифровых симуляторов аварийных ситуаций по различным направлениям производственной безопасности (с правом выдачи сертификатов).',
|
||
docs:[],dname:'Протоколы обучения / Электронная ведомость',
|
||
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',
|
||
r:'Директор ДПБ, Генеральный директор ДИТ, Генеральные директора филиалов и ДАО',
|
||
t:'Провести анализ, в том числе с использованием аналитических платформ (Microsoft Teams, Power BI, Tableau, Qlik и др.), и в случае необходимости, осуществить пересмотр внутренних нормативных документов филиалов/ДАО Общества в соответствии со «Стратегией развития производственной безопасности АО «Самрук-Қазына» на 2024-2028 гг.», включая установку значений ключевых показателей производственной безопасности.',
|
||
docs:[],dname:'Отчёт о проведённом анализе / Утверждённый ВНД',
|
||
ai:'Анализ завершён в срок. ВНД пересмотрены. Ключевые показатели ПБ установлены. Замечаний нет.',
|
||
h:['10.01 — Мероприятие создано','15.02 — Проведён анализ ВНД','28.03 — Отчёт утверждён']},
|
||
{id:3,sec:0,b:0,s:'warn',p:50,due:'31.12.2026',done:'—',
|
||
r:'Главный административный директор, Директор ДПБ / Генеральные директора филиалов и ДАО',
|
||
t:'Организовывать тематические совещания по вопросам производственной безопасности: a) руководство Общества с филиалами/ДАО, не менее 1 раза в квартал; b) руководство филиалов/ДАО со структурными подразделениями, не менее 1 раза в месяц; c) руководство региональных подразделений с подрядными организациями, не менее 1 раза в квартал.',
|
||
docs:[],dname:'Протоколы совещаний (a, b, c)',
|
||
ai:'Проведено 2 квартальных совещания. Ежемесячные совещания филиалов — выполняется. Совещания с подрядчиками — график соблюдается.',
|
||
h:['10.01 — Мероприятие создано','15.02 — Совещание Q1','15.05 — Совещание Q2']},
|
||
{id:4,sec:0,b:6,s:'warn',p:55,due:'31.12.2026',done:'—',
|
||
r:'Генеральные директора филиалов и ДАО',
|
||
t:'Продолжить практику проверки знаний в формате тестирования после проведения инструктажей по охране труда в филиалах/ДАО Общества.',
|
||
docs:[],dname:'Отчёты о проделанной работе / Тесты',
|
||
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',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность нематериального поощрения филиалов и ДАО Общества, демонстрирующих устойчивое снижение количества несчастных случаев, пожаров и аварий по итогам нескольких и более лет.',
|
||
docs:[],dname:'Информация о нематериальном поощрении',
|
||
ai:'Положение утверждено. Определены 3 филиала-лидера за 2023-2025 гг. Поощрение доведено до коллективов.',
|
||
h:['15.01 — Проект положения','01.03 — Согласование','25.03 — Утверждено']},
|
||
{id:6,sec:0,b:6,s:'warn',p:60,due:'30.06.2026',done:'—',
|
||
r:'Генеральный директор КУ, Управляющий директор по персоналу / Генеральные директора филиалов и ДАО',
|
||
t:'Разработать/внести изменения в случае необходимости и утвердить внутренний нормативный документ, регламентирующий процедуру работы внутренних тренеров, в том числе по производственной безопасности, включая порядок их отбора, подготовки и привлечения, а также установление условий доплаты к основной заработной плате за выполнение тренерских функций.',
|
||
docs:[],dname:'ВНД по внутренним тренерам / Перечень тренеров',
|
||
ai:'Проект ВНД на финальном согласовании. Перечень тренеров сформирован (12 чел). Риск: доплаты требуют бюджета.',
|
||
h:['01.03 — Мероприятие создано','15.04 — Проект ВНД','01.06 — Перечень тренеров']},
|
||
{id:7,sec:0,b:1,s:'warn',p:40,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Проводить мероприятия по обмену опытом в области производственной безопасности: a) продолжить практику обмена передовым опытом на площадке Комитета HSE, в том числе путем выездов на производственные объекты ПК; b) рассмотреть возможность организации обмена опытом с иностранными и казахстанскими компаниями, в том числе путем проведения онлайн-семинаров.',
|
||
docs:[],dname:'Материалы обмена опытом',
|
||
ai:'Проведён 1 выезд на площадку KEGOC. Онлайн-семинар с иностранной компанией запланирован на июль.',
|
||
h:['15.02 — Мероприятие создано','01.04 — Выезд на KEGOC','15.05 — План семинаров']},
|
||
{id:8,sec:0,b:4,s:'wait',p:15,due:'30.09.2026',done:'—',
|
||
r:'a) Директор ДПБ / b) Генеральный директор КУ, Управляющий директор по персоналу / c,d) Генеральные директора филиалов и ДАО',
|
||
t:'Провести анализ эффективности реализуемых мероприятий по охране здоровья: a) организовать и обеспечить 100% прохождение обязательных периодических медицинских осмотров; b) организовать ежегодную «Неделю благополучия» (Well-being Week); c) создать условия для прохождения медицинского скрининга; d) внедрить алгоритм учета и расследования микротравм.',
|
||
docs:[],dname:'Заключительный Акт / Программа Well-being / Отчёт о скрининге / Отчёт о микротравмах',
|
||
ai:'Медосмотры по графику — Q3. Well-being Week запланирован на сентябрь. Ранний этап.',
|
||
h:['01.04 — Мероприятие создано','01.06 — Проект алгоритма микротравм']},
|
||
{id:9,sec:0,b:6,s:'wait',p:20,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность участия ДПБ/филиалов/ДАО Общества в международных/национальных конкурсах и отраслевых соревнованиях профессионального мастерства в области производственной безопасности.',
|
||
docs:[],dname:'Результаты конкурсов / Пакет материалов',
|
||
ai:'Определены 2 конкурса для участия. Заявки готовятся. Срок — до конца года.',
|
||
h:['01.05 — Мероприятие создано','01.06 — Отобраны конкурсы']},
|
||
{id:10,sec:1,b:1,s:'warn',p:55,due:'31.12.2026',done:'—',
|
||
r:'Генеральный директор ОДС, Генеральный директор СФ, Генеральный директор ДУП, Генеральный директор ДИТ',
|
||
t:'Проводить работы по техническому перевооружению морально и физически изношенного оборудования, зданий и сооружений, эксплуатация которых из-за их технического состояния сопровождается повышенными рисками возникновения аварий и несчастных случаев с тяжёлыми и летальными исходами, в соответствии с ранее утвержденными Планами на 2024-2027 годы.',
|
||
docs:[],dname:'Аналитическая справка по филиалам/ДАО',
|
||
ai:'По плану 2024-2027. Заменено 55% единиц. Отдельные филиалы отстают на 12%. Рекомендуется усилить контроль.',
|
||
h:['01.01 — Переходящее с 2024','01.04 — Промежуточный отчёт','01.06 — 55% исполнения']},
|
||
{id:11,sec:1,b:1,s:'warn',p:70,due:'30.06.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Пересмотреть и актуализировать внутренний порядок выдачи нарядно-допускной системы, усилив законодательные требования РК путем внедрения в пилотном режиме практики применения сертификатов безопасности для одного из видов работ повышенной опасности.',
|
||
docs:[],dname:'Переутверждённая процедура / Фотоотчёт / Протоколы обучения',
|
||
ai:'Процедура пересмотрена. Пилот сертификатов запущен. Обучение — 85% персонала. Успеваем в Q2.',
|
||
h:['01.02 — Мероприятие создано','01.04 — Проект процедуры','15.05 — Пилот запущен']},
|
||
{id:12,sec:1,b:8,s:'wait',p:8,due:'30.09.2026',done:'—',
|
||
r:'Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность внедрения системы цифровой маркировки опасных технических устройств, предусматривающей присвоение каждому устройству QR-кода для быстрого доступа к паспорту, инструкции по эксплуатации и информации о проведенных технических освидетельствованиях.',
|
||
docs:[],dname:'Справка о внедрении / Фотоотчёт',
|
||
ai:'Проект на стадии ТЭО. QR-коды не заказаны. Начало работ — июль. Риск срыва низкий.',
|
||
h:['01.05 — Мероприятие создано','01.06 — ТЭО в разработке']},
|
||
{id:13,sec:1,b:0,s:'warn',p:50,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ',
|
||
t:'Филиалам/ДАО Общества не реже 1 раза в квартал проводить проверку согласно адаптированным проверочным листам в области БиОТ, промышленной и пожарной безопасности в соответствии с требованиями законодательства Республики Казахстан.',
|
||
docs:[],dname:'Акты проверок / График проверок',
|
||
ai:'Q1 проверки завершены. Q2 — идёт по графику. Выявлено 23 нарушения, устранено 18.',
|
||
h:['01.01 — Мероприятие создано','31.03 — Q1 проверки','01.06 — Q2 начаты']},
|
||
{id:14,sec:1,b:0,s:'warn',p:40,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ',
|
||
t:'Продолжить практику участия в перекрёстных аудитах ПК, в том числе в соответствии с Планом-графиком проведения аудитов.',
|
||
docs:[],dname:'Письмо о предоставлении кандидата',
|
||
ai:'Назначены 4 аудитора. Участвовали в 2 аудитах. График соблюдается.',
|
||
h:['15.01 — Назначены аудиторы','01.03 — Аудит KEGOC','15.04 — Аудит КазТрансОйл']},
|
||
{id:15,sec:1,b:0,s:'warn',p:48,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Усилить контроль за применением проактивных инструментов: мониторинг поведенческих аудитов/наблюдений безопасности, регистрация и расследование опасных условий, опасных действий и потенциально опасных происшествий Near Miss; право приостановки работы.',
|
||
docs:[],dname:'Аналитическая справка / Журнал опережающих индикаторов',
|
||
ai:'Зарегистрировано 147 Near Miss (+12% к 2025). Поведенческие аудиты — 320 шт.',
|
||
h:['01.01 — Мероприятие создано','01.04 — Q1: 68 Near Miss','01.06 — Q2: 79 Near Miss']},
|
||
{id:16,sec:1,b:1,s:'done',p:85,due:'31.12.2026',done:'—',
|
||
r:'Генеральные директора филиалов и ДАО',
|
||
t:'Провести работу по повышению эффективности управления подрядными организациями: a) аудит подрядчиков внутри филиалов/ДАО согласно типового чек-листа Фонда; b) проведение стартовых/установочных совещаний с подрядными организациями перед допуском на территорию объекта.',
|
||
docs:[],dname:'План-график аудитов / Акты / Протоколы совещаний',
|
||
ai:'Q1 — 12 подрядчиков проверено. Стартовые совещания — 100% охват.',
|
||
h:['15.01 — План-график','01.03 — Q1 проверки','01.06 — Q2 проверки']},
|
||
{id:17,sec:1,b:0,s:'warn',p:35,due:'31.12.2026',done:'—',
|
||
r:'a) Главный административный директор, Директор ДПБ / b) Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить контроль за состоянием производственной безопасности на производственных объектах: a) руководителям Общества уровня СЕО-1 не реже 1 раза в квартал лично проверять одно из филиалов/подрядных организаций; b) первым руководителям филиалов лично принимать участие во внутреннем производственном контроле с посещением площадок не реже 1 раза в квартал.',
|
||
docs:[],dname:'Отчёты / График проверок / Фотоотчёт',
|
||
ai:'CEO-1 проверил 2 филиала. Первые руководители — 6 выездов. Активность ниже плана.',
|
||
h:['01.02 — Мероприятие создано','15.03 — Проверка 1','01.05 — Проверка 2']},
|
||
{id:18,sec:1,b:1,s:'done',p:90,due:'31.12.2026',done:'—',
|
||
r:'Генеральные директора филиалов и ДАО, Директор ДПБ',
|
||
t:'Обеспечить контроль за состоянием транспортной безопасности, в том числе путем ежемесячного мониторинга нарушений требований транспортной безопасности со стороны штатных водителей и водителей подрядных организаций, с последующим применением предусмотренных договорами мер воздействия.',
|
||
docs:[],dname:'Ежемесячный сводный отчёт',
|
||
ai:'Январь-май — 34 нарушения. Штрафы применены к 12 водителям. Тренд на снижение (-15% к 2025).',
|
||
h:['01.01 — Мероприятие создано','01.02 — Отчёт январь','01.06 — Отчёт май']},
|
||
{id:19,sec:2,b:1,s:'warn',p:30,due:'31.12.2026',done:'—',
|
||
r:'a) Управляющий директор по безопасности / b) Генеральный директор СФ / c) Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить проведение: a) не менее одной учебной тревоги и/или противоаварийной тренировки по ликвидации крупной аварии, ЧС на опасном производственном объекте; b) не менее двух тренировок по тушению пожара в административных зданиях; c) не менее одного практического занятия по оказанию первой помощи.',
|
||
docs:[],dname:'Акты тренировок / Пресс-релизы',
|
||
ai:'Проведено 1 учение. Пожарные тренировки: 1 из 2. Занятия по первой помощи — Q3.',
|
||
h:['01.02 — Мероприятие создано','15.03 — Учение','15.05 — Пожарная тренировка №1']},
|
||
{id:20,sec:2,b:0,s:'warn',p:65,due:'30.06.2026',done:'—',
|
||
r:'Управляющий директор по безопасности, Генеральные директора филиалов и ДАО',
|
||
t:'Усилить работу по реагированию на ЧС: a) внедрить процедуру «Crisis Management System»; b) провести обучение ответственных работников по действиям в условиях ЧС; c) провести не менее двух заседаний штабов.',
|
||
docs:[],dname:'Приказ CMS / Материалы обучения / Акты штабов',
|
||
ai:'Приказ CMS подписан. Обучение — 60%. Одно заседание штаба проведено.',
|
||
h:['01.03 — Мероприятие создано','01.04 — Приказ CMS','15.05 — Заседание штаба №1']},
|
||
{id:21,sec:3,b:0,s:'done',p:100,due:'31.12.2026',done:'15.02.2026',
|
||
r:'Директор ДПБ, Пресс-секретарь ЦА',
|
||
t:'Обеспечить выпуск обращения от Председателя Правления ПК о важности соблюдения требований по производственной безопасности.',
|
||
docs:[],dname:'Публикация на информационных порталах',
|
||
ai:'Обращение опубликовано. Охват — 100% персонала. Задача выполнена досрочно.',
|
||
h:['15.01 — Проект обращения','01.02 — Подписание','15.02 — Публикация']},
|
||
{id:22,sec:3,b:0,s:'wait',p:15,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО / Департамент по коммуникациям',
|
||
t:'Проведение мероприятий по производственной безопасности: a) стратегических сессий/Форумов для первых руководителей филиалов/ДАО; b) семинаров для подрядных организаций ПК на отдельных площадках филиалов/ДАО.',
|
||
docs:[],dname:'Протоколы форумов / Протоколы семинаров',
|
||
ai:'Форум запланирован на октябрь. Семинары — 2 площадки определены.',
|
||
h:['01.05 — Мероприятие создано','01.06 — Определены площадки']},
|
||
{id:23,sec:3,b:6,s:'wait',p:10,due:'30.09.2026',done:'—',
|
||
r:'Директор ДПБ',
|
||
t:'Проведение Олимпиады по производственной безопасности среди специалистов производственной безопасности Общества и подрядных организаций на уровне Общества.',
|
||
docs:[],dname:'Протокол итогов Олимпиады',
|
||
ai:'Положение об Олимпиаде на согласовании. Рекомендуется начать подготовку.',
|
||
h:['01.05 — Мероприятие создано','01.06 — Проект положения']},
|
||
{id:24,sec:3,b:0,s:'done',p:92,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить ознакомление всех работников филиалов/ДАО Общества с обстоятельствами несчастных случаев с тяжелым и летальным исходами, произошедших в ПК Фонда, посредством направления информационных бюллетеней, в том числе с использованием цифровых решений либо в рамках внеплановых инструктажей.',
|
||
docs:[],dname:'Бюллетени / Листы ознакомления',
|
||
ai:'Разослано 3 бюллетеня. Ознакомление — 92% персонала. Рекомендация: 100% к Q3.',
|
||
h:['01.01 — Мероприятие создано','15.02 — Бюллетень №1','01.05 — Бюллетень №3']},
|
||
{id:25,sec:3,b:6,s:'warn',p:40,due:'31.12.2026',done:'—',
|
||
r:'a) Управляющий директор по персоналу, Департамент по коммуникациям / b,c,d) Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Проведение молодежных проектных инициатив в рамках работы Центра молодых работников по производственной безопасности: a) публикация историй в журнале «SK News»; b) посещение рабочих мест с НС за 2022-2025 гг.; c) привлечение молодых специалистов в перекрёстные аудиты; d) онлайн-семинары/прямые эфиры.',
|
||
docs:[],dname:'Публикации SK News / Материалы мероприятий',
|
||
ai:'Опубликовано 2 истории. Проведён 1 выезд. 3 молодых специалиста в аудитах.',
|
||
h:['01.02 — Мероприятие создано','15.03 — Публикация №1','01.05 — Выезд']},
|
||
{id:26,sec:3,b:2,s:'warn',p:50,due:'31.12.2026',done:'—',
|
||
r:'a) Генеральные директора филиалов и ДАО / b,c) Директор ДПБ, Департамент по коммуникациям / d) Директор ДПБ',
|
||
t:'Усилить наглядную агитацию по производственной безопасности: a) разработка видеороликов/презентаций; b) серии видеороликов «Безопасность будущего»; c) выпуск подкаста с участием трудовых династий; d) разработка постеров, брошюр, информационных рассылок.',
|
||
docs:[],dname:'Видеоролики / Постеры / Брошюры',
|
||
ai:'Снято 2 видеоролика. Постеры распространены. Подкаст в разработке.',
|
||
h:['01.02 — Мероприятие создано','01.04 — Видеоролик №1','01.06 — Видеоролик №2']},
|
||
{id:27,sec:3,b:3,s:'warn',p:30,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотрение возможности организации встреч коллектива с получившими производственные травмы работниками (с их согласия) с целью предупреждения аналогичных случаев травматизма.',
|
||
docs:[],dname:'Фотофиксация',
|
||
ai:'Проведена 1 встреча. Получены согласия от 3 работников. Рекомендуется расширить.',
|
||
h:['01.03 — Мероприятие создано','01.05 — Встреча проведена']},
|
||
{id:28,sec:3,b:0,s:'warn',p:25,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО / Департамент по коммуникациям',
|
||
t:'Проведение мероприятий, направленных на пропаганду безопасного выполнения работ через семейные ценности: a) направление информационного письма членам семьи отличившегося работника; b) проведение Семейных дней охраны труда; c) проведение конкурса рисунков на тему «Спецодежда будущего!».',
|
||
docs:[],dname:'Информационное письмо / Пресс-релизы',
|
||
ai:'Направлено 5 писем семьям. Семейный день — август. Конкурс анонсирован.',
|
||
h:['01.04 — Мероприятие создано','15.05 — Письма семьям']},
|
||
{id:29,sec:3,b:6,s:'late',p:40,due:'30.06.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Разработать корпоративный сборник лучших практик по производственной безопасности в формате методического пособия или интерактивного PDF документа, отражающий меры по снижению производственного травматизма и управлению критическими рисками.',
|
||
docs:[],dname:'Корпоративный сборник лучших практик',
|
||
ai:'Сборник в разработке. Опрошено 4 из 8 филиалов. Риск срыва срока Q2 — 26 дней.',
|
||
h:['01.03 — Мероприятие создано','01.05 — Опрошены 4 филиала','01.06 — Эскалация']},
|
||
{id:30,sec:3,b:7,s:'warn',p:60,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Сбор предложений по совершенствованию системы управления производственной безопасности посредством применения цифровых решений с консолидацией в ДПБ.',
|
||
docs:[],dname:'Предложения / План реализации',
|
||
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:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Разработка видеообзора кейсов происшествий в ПК с учетом специфики деятельности (из доступных на открытых медиа источниках) для наглядной демонстрации и разъяснения работникам о необходимости и важности соблюдения требований безопасности.',
|
||
docs:[],dname:'Видеообзор',
|
||
ai:'Видеообзор смонтирован на 75%. Озвучка — 15 июня. Укладываемся в Q2.',
|
||
h:['01.03 — Мероприятие создано','01.05 — Сценарий','01.06 — Монтаж 75%']},
|
||
{id:32,sec:4,b:8,s:'warn',p:70,due:'30.06.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить применение в филиалах/ДАО Общества чат-бот ИИ ассистент по производственной безопасности с целью упрощения доступа к нормативно-правовым актам Республики Казахстан и ВНД группы Фонда.',
|
||
docs:[],dname:'Справка / Скриншоты чат-бота',
|
||
ai:'Чат-бот разработан, тестируется. База НПА загружена. Пилот — 15 июня.',
|
||
h:['01.02 — Мероприятие создано','01.04 — Разработка','01.06 — Тестирование']},
|
||
{id:33,sec:4,b:8,s:'wait',p:15,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Обеспечить применение в филиалах/ДАО Общества интегрированную систему анализа и предупреждения несчастных случаев и критических происшествий, а также платформу по идентификации и оценке рисков перед началом проведения работ на опасных производственных объектах.',
|
||
docs:[],dname:'Справка / Скриншоты системы',
|
||
ai:'ТЗ согласовывается. Интеграция с платформой Фонда прорабатывается.',
|
||
h:['01.04 — Мероприятие создано','01.06 — ТЗ на согласовании']},
|
||
{id:34,sec:4,b:8,s:'wait',p:10,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность запуска в филиалах/ДАО Общества электронного HSE паспорта на каждого работника с последующей интеграцией в корпоративную цифровую систему.',
|
||
docs:[],dname:'Справка / Скриншоты HSE паспорта',
|
||
ai:'Концепция утверждена. Выбран подрядчик. Интеграция — Q3.',
|
||
h:['01.05 — Мероприятие создано','01.06 — Концепция утверждена']},
|
||
{id:35,sec:4,b:5,s:'wait',p:8,due:'31.12.2026',done:'—',
|
||
r:'Директор ДПБ, Генеральные директора филиалов и ДАО',
|
||
t:'Рассмотреть возможность внедрения системы оформления нарядов-допусков на проведение работ повышенной опасности в электронном виде в филиалах/ДАО Общества.',
|
||
docs:[],dname:'Справка / Скриншоты системы',
|
||
ai:'Предпроектная стадия. Анализ рынка проведён. Выбор платформы — Q3.',
|
||
h:['01.05 — Мероприятие создано','01.06 — Анализ рынка']}
|
||
]
|
||
|
||
// ===== USER SYSTEM =====
|
||
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
|
||
let events = JSON.parse(JSON.stringify(baseEvents))
|
||
|
||
function loadState(){
|
||
const saved = localStorage.getItem('samruk_events')
|
||
if(saved) events = JSON.parse(saved)
|
||
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()
|
||
}
|
||
|
||
// ===== NOTIFICATIONS =====
|
||
function getNotifs(){
|
||
const myEvs = currentUser.role === 'admin' || currentUser.role === 'director'
|
||
? events : events.filter(e => e.b === currentUser.branch)
|
||
const notifs = []
|
||
myEvs.forEach(e => {
|
||
if(e.s === 'late') notifs.push({type:'danger', msg:`Просрочено: ${e.t.slice(0,60)}...`, time: e.due})
|
||
if(e.s === 'warn' && e.p < 30) notifs.push({type:'warn', msg:`Низкий прогресс (${e.p}%): ${e.t.slice(0,50)}...`, time: 'Сейчас'})
|
||
})
|
||
return notifs
|
||
}
|
||
|
||
function renderNotifs(){
|
||
const notifs = getNotifs()
|
||
const el = document.getElementById('notifDrop')
|
||
const cnt = document.getElementById('notifCount')
|
||
cnt.textContent = notifs.length
|
||
cnt.style.display = notifs.length ? 'inline-block' : 'none'
|
||
if(!notifs.length){ el.innerHTML = '<div class="empty">Новых уведомлений нет</div>'; return }
|
||
el.innerHTML = notifs.map(n => `<div class="item">
|
||
<div class="title">${n.type==='danger'?'🔴':'🟡'} ${n.msg}</div>
|
||
<div class="time">${n.time}</div></div>`).join('')
|
||
}
|
||
|
||
function toggleNotif(){
|
||
renderNotifs()
|
||
document.getElementById('notifDrop').classList.toggle('open')
|
||
}
|
||
|
||
// ===== EVENTS HELPER =====
|
||
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>`}
|
||
|
||
// ===== MY EVENTS =====
|
||
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 statusF = document.getElementById('myStatusFilter').value
|
||
let list = getMyEvents()
|
||
if(statusF) list = list.filter(e => e.s === statusF)
|
||
|
||
const done = list.filter(e=>e.s==='done').length
|
||
const late = list.filter(e=>e.s==='late').length
|
||
const warn = list.filter(e=>e.s==='warn').length
|
||
const 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('')}`
|
||
}
|
||
|
||
// ===== ALL EVENTS =====
|
||
function renderAllEvents(){
|
||
const search = document.getElementById('allSearch').value.toLowerCase()
|
||
const statusF = document.getElementById('allStatusFilter').value
|
||
const 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(statusF) list = list.filter(e => e.s === statusF)
|
||
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('')}`
|
||
}
|
||
|
||
// ===== ANALYTICS =====
|
||
function renderAnalytics(){
|
||
let done = events.filter(e=>e.s==='done').length
|
||
let late = events.filter(e=>e.s==='late').length
|
||
let 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">${late}</div></div>`
|
||
|
||
document.getElementById('analyticsContent').innerHTML = `<h3>Рейтинг дивизионов</h3>
|
||
${branches.map((b,i) => {
|
||
let items = events.filter(e => e.b === i)
|
||
let d = items.filter(e=>e.s==='done').length
|
||
let 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('')}`
|
||
}
|
||
|
||
// ===== EDIT MODAL =====
|
||
function openEdit(id){
|
||
const e = events.find(x => x.id === id)
|
||
if(!e) return
|
||
// Load saved edits from this user
|
||
const savedEdits = JSON.parse(localStorage.getItem('samruk_edits_'+e.id) || '{}')
|
||
|
||
// Generate file list from saved file metadata
|
||
const savedFiles = JSON.parse(localStorage.getItem('samruk_files_'+e.id) || '[]')
|
||
const fileList = savedFiles.map((f,i) => `
|
||
<span class="doc-chip" onclick="downloadFile(${e.id},${i})" title="Скачать ${f.name}" style="cursor:pointer">
|
||
📄 ${f.name} (${(f.size/1024).toFixed(0)} КБ)
|
||
<button onclick="event.stopPropagation();removeFile(${e.id},${i})" title="Удалить">×</button></span>`).join('')
|
||
|
||
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>
|
||
|
||
<div style="font-weight:600;margin-bottom:4px;font-size:14px">Подтверждающие материалы</div>
|
||
${fileList ? `<div class="docs">${fileList}</div>` : '<p style="font-size:13px;color:var(--gray-500);margin-bottom:8px">Файлы не загружены</p>'}
|
||
<div class="field">
|
||
<label>Загрузить файл</label>
|
||
<input type="file" id="editFileInput" multiple style="width:100%;padding:8px;border:1px solid var(--gray-200);border-radius:8px;font-size:14px">
|
||
<p style="font-size:11px;color:var(--gray-500);margin-top:4px">Файлы сохраняются локально в браузере для демонстрации. Формы завершения: ${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 autoProgress(){
|
||
const s = document.getElementById('editStatus').value
|
||
const p = document.getElementById('editProgress')
|
||
if(s === 'done') p.value = 100
|
||
else if(s === 'wait') p.value = 0
|
||
document.getElementById('pVal').textContent = p.value + '%'
|
||
}
|
||
|
||
function saveEdit(id){
|
||
const e = events.find(x => x.id === id)
|
||
if(!e) return
|
||
const newStatus = document.getElementById('editStatus').value
|
||
const newProgress = parseInt(document.getElementById('editProgress').value)
|
||
const comment = document.getElementById('editComment').value.trim()
|
||
|
||
// Save files as data URLs
|
||
const fileInput = document.getElementById('editFileInput')
|
||
const MAX_SIZE = 4 * 1024 * 1024 // 4 MB limit per file for localStorage
|
||
|
||
function finalizeSave(){
|
||
const now = new Date().toLocaleDateString()
|
||
e.h.push(`${now} — ${currentUser.name}: статус ${statusMap[newStatus]}, прогресс ${newProgress}%${comment?' — комм.: '+comment:''}`)
|
||
e.s = newStatus
|
||
e.p = newProgress
|
||
if(newStatus === 'done' && e.done === '—') e.done = now
|
||
localStorage.setItem('samruk_edits_'+id, JSON.stringify({comment, editedBy: currentUser.name, editedAt: now}))
|
||
saveState()
|
||
closeEditModal()
|
||
renderAll()
|
||
renderNotifs()
|
||
}
|
||
|
||
if(fileInput.files.length > 0){
|
||
const existing = JSON.parse(localStorage.getItem('samruk_files_'+id) || '[]')
|
||
let processed = 0
|
||
let skipped = 0
|
||
for(const f of fileInput.files){
|
||
if(f.size > MAX_SIZE){
|
||
skipped++
|
||
if(++processed === fileInput.files.length){
|
||
if(skipped) alert(`⚠️ ${skipped} файл(ов) > 4 МБ пропущены — слишком велики для локального хранения.`)
|
||
finalizeSave()
|
||
}
|
||
continue
|
||
}
|
||
const reader = new FileReader()
|
||
reader.onload = function(ev){
|
||
existing.push({name: f.name, size: f.size, type: f.type, date: new Date().toLocaleDateString(), data: ev.target.result})
|
||
processed++
|
||
if(processed === fileInput.files.length){
|
||
try {
|
||
localStorage.setItem('samruk_files_'+id, JSON.stringify(existing))
|
||
} catch(e) {
|
||
alert('⚠️ Хранилище браузера переполнено. Удалите старые файлы.')
|
||
}
|
||
if(skipped) alert(`⚠️ ${skipped} файл(ов) > 4 МБ пропущены.`)
|
||
finalizeSave()
|
||
}
|
||
}
|
||
reader.readAsDataURL(f)
|
||
}
|
||
} else {
|
||
finalizeSave()
|
||
}
|
||
}
|
||
|
||
function downloadFile(eventId, fileIdx){
|
||
const files = JSON.parse(localStorage.getItem('samruk_files_'+eventId) || '[]')
|
||
const f = files[fileIdx]
|
||
if(!f || !f.data) return
|
||
const 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, fileIdx){
|
||
const files = JSON.parse(localStorage.getItem('samruk_files_'+eventId) || '[]')
|
||
files.splice(fileIdx, 1)
|
||
localStorage.setItem('samruk_files_'+eventId, JSON.stringify(files))
|
||
closeEditModal()
|
||
openEdit(eventId)
|
||
}
|
||
|
||
function closeEditModal(){ document.getElementById('editModalOverlay').classList.remove('open') }
|
||
|
||
// ===== TABS =====
|
||
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()
|
||
})
|
||
})
|
||
|
||
// ===== CLOSE MODAL =====
|
||
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') })
|
||
|
||
// ===== INIT =====
|
||
function renderAll(){
|
||
renderMyEvents()
|
||
renderAllEvents()
|
||
renderNotifs()
|
||
}
|
||
loadState()
|
||
if(currentUser) showApp()
|
||
</script>
|
||
</body>
|
||
</html> |