// ─── Seed Data ────────────────────────────────────────────────────────────── const ALL_EVENTS = [ { id: "evt-1", title: "Проведение вводного инструктажа по пожарной безопасности", responsible: "Ахметов Т.К.", section: "Обучение и культура безопасности", deadline: "2026-06-15", status: "completed", branch: "Дирекция ПБ", region: "Астана", description: "Инструктаж проведён для 45 сотрудников", quantity: 45, subItems: [ { id: "1.1", title: "Подготовка материалов инструктажа", status: "completed" }, { id: "1.2", title: "Проведение практического занятия", status: "completed" } ], history: [ { timestamp: "2026-06-01T10:00:00", action: "created" }, { timestamp: "2026-06-10T14:30:00", action: "updated", changes: { status: "completed" } } ], created_at: "2026-06-01" }, { id: "evt-2", title: "Проверка систем автоматического пожаротушения на объектах", responsible: "Сериков Д.А.", section: "Техническая безопасность", deadline: "2026-09-20", status: "in_progress", branch: "Дивизион «Сеть»", region: "Алматы", description: "Проверено 12 из 18 объектов", quantity: 12, subItems: [ { id: "2.1", title: "Инвентаризация систем пожаротушения", status: "completed" }, { id: "2.2", title: "Техническое тестирование датчиков", status: "in_progress" }, { id: "2.3", title: "Замена неисправных элементов", status: "pending" } ], history: [ { timestamp: "2026-05-15T09:00:00", action: "created" }, { timestamp: "2026-06-05T11:00:00", action: "updated", changes: { status: "in_progress", quantity: 12 } } ], created_at: "2026-05-15" }, { id: "evt-3", title: "Разработка плана эвакуации для нового офисного здания", responsible: "Куанышева А.М.", section: "Готовность к ЧС", deadline: "2026-08-01", status: "pending", branch: "Корпоративный бизнес", region: "Шымкент", description: "Требуется разработка плана с учётом новых нормативов", quantity: 0, subItems: [], history: [ { timestamp: "2026-06-01T08:00:00", action: "created" } ], created_at: "2026-06-01" }, { id: "evt-4", title: "Замена огнетушителей с истекшим сроком годности", responsible: "Нурланов Е.С.", section: "Техническая безопасность", deadline: "2026-04-15", status: "overdue", branch: "Розничный бизнес", region: "Караганда", description: "Срок истёк 15 апреля, срочно требуется замена 67 единиц", quantity: 67, subItems: [ { id: "4.1", title: "Инвентаризация огнетушителей", status: "completed" }, { id: "4.2", title: "Закупка новых огнетушителей", status: "overdue" }, { id: "4.3", title: "Установка и утилизация старых", status: "pending" } ], history: [ { timestamp: "2026-03-01T10:00:00", action: "created" }, { timestamp: "2026-05-20T09:00:00", action: "updated", changes: { status: "overdue" } } ], created_at: "2026-03-01" }, { id: "evt-5", title: "Обучение персонала оказанию первой помощи", responsible: "Мусаева Г.Р.", section: "Обучение и культура безопасности", deadline: "2026-06-20", status: "in_progress", branch: "Корп. университет", region: "Астана", description: "Проведено 3 из 8 запланированных тренингов", quantity: 3, subItems: [ { id: "5.1", title: "Программа обучения", status: "completed" }, { id: "5.2", title: "Тренинг для руководителей", status: "completed" }, { id: "5.3", title: "Тренинг для линейного персонала", status: "in_progress" }, { id: "5.4", title: "Итоговая аттестация", status: "pending" } ], history: [ { timestamp: "2026-04-01T08:00:00", action: "created" }, { timestamp: "2026-05-15T14:00:00", action: "updated", changes: { quantity: 3 } } ], created_at: "2026-04-01" }, { id: "evt-6", title: "Внедрение цифровой системы учёта происшествий", responsible: "Тулегенов Б.К.", section: "Цифровизация", deadline: "2026-05-30", status: "completed", branch: "Цифровой бизнес", region: "Алматы", description: "Система запущена в промышленную эксплуатацию, обучено 120 пользователей", quantity: 120, subItems: [ { id: "6.1", title: "Разработка ТЗ", status: "completed" }, { id: "6.2", title: "Пилотное внедрение", status: "completed" }, { id: "6.3", title: "Обучение пользователей", status: "completed" } ], history: [ { timestamp: "2026-02-01T09:00:00", action: "created" }, { timestamp: "2026-05-30T16:00:00", action: "updated", changes: { status: "completed", quantity: 120 } } ], created_at: "2026-02-01" }, { id: "evt-7", title: "Проведение учений по ликвидации аварийного разлива нефтепродуктов", responsible: "Рахимов Ж.Н.", section: "Готовность к ЧС", deadline: "2026-07-25", status: "pending", branch: "Сервисная фабрика", region: "Атырау", description: "Согласование сценария учений с МЧС", quantity: 0, subItems: [], history: [ { timestamp: "2026-06-05T11:00:00", action: "created" } ], created_at: "2026-06-05" }, { id: "evt-8", title: "Аудит системы управления охраной труда в филиалах", responsible: "Садыкова Л.А.", section: "Коммуникации", deadline: "2026-03-01", status: "overdue", branch: "Управление проектами", region: "Актобе", description: "Аудит не проведён, требуется согласование графика", quantity: 0, subItems: [ { id: "8.1", title: "Формирование комиссии", status: "completed" }, { id: "8.2", title: "Проверка документации", status: "overdue" }, { id: "8.3", title: "Выезд на объекты", status: "pending" } ], history: [ { timestamp: "2026-01-10T10:00:00", action: "created" }, { timestamp: "2026-04-01T08:00:00", action: "updated", changes: { status: "overdue" } } ], created_at: "2026-01-10" }, { id: "evt-9", title: "Разработка корпоративного стандарта по работе на высоте", responsible: "Касымов А.Д.", section: "Техническая безопасность", deadline: "2026-10-15", status: "pending", branch: "Телеком Комплект", region: "Астана", description: "Стандарт находится на стадии согласования", quantity: 0, subItems: [], history: [ { timestamp: "2026-06-01T09:00:00", action: "created" } ], created_at: "2026-06-01" }, { id: "evt-10", title: "Обновление информационных стендов по технике безопасности", responsible: "Ибраева Д.С.", section: "Коммуникации", deadline: "2026-07-01", status: "in_progress", branch: "Розничный бизнес", region: "Павлодар", description: "Изготовлено 15 из 24 стендов", quantity: 15, subItems: [], history: [ { timestamp: "2026-05-01T10:00:00", action: "created" }, { timestamp: "2026-06-08T15:00:00", action: "updated", changes: { status: "in_progress", quantity: 15 } } ], created_at: "2026-05-01" } ]; const BRANCHES = [ "Дирекция ПБ", "Дивизион «Сеть»", "Корпоративный бизнес", "Розничный бизнес", "Сервисная фабрика", "Телеком Комплект", "Корп. университет", "Управление проектами", "Цифровой бизнес" ]; // ─── Hardcoded Users ────────────────────────────────────────────────────── const USERS = { "curator@sk.kz": { name: "Куратор ПБ", password: "1234" }, "dpp@sk.kz": { name: "Директор ДПБ", password: "1234" } }; // ─── State ──────────────────────────────────────────────────────────────── let user = null; let eventsData = []; let editingId = null; // ─── Utils ──────────────────────────────────────────────────────────────── function daysUntil(d) { if (!d) return 999; const t = new Date(d + "T23:59:59"); return Math.ceil((t - new Date()) / 86400000); } function esc(s) { const d = document.createElement("div"); d.textContent = String(s); return d.innerHTML; } // ─── Auth ───────────────────────────────────────────────────────────────── function login() { const email = document.getElementById("loginEmail").value.trim(); const pass = document.getElementById("loginPass").value.trim(); const errEl = document.getElementById("loginError"); if (!pass) { errEl.textContent = "Введите пароль"; return; } const u = USERS[email]; if (u && u.password === pass) { user = { email: email, name: u.name }; localStorage.setItem("sh_token", "ok"); localStorage.setItem("sh_user", JSON.stringify(user)); document.getElementById("loginScreen").style.display = "none"; document.getElementById("app").style.display = "block"; document.getElementById("userName").textContent = user.name; document.getElementById("userEmail").textContent = user.email; init(); } else { errEl.textContent = "Неверный email или пароль"; } } function logout() { user = null; localStorage.removeItem("sh_token"); localStorage.removeItem("sh_user"); document.getElementById("app").style.display = "none"; document.getElementById("loginScreen").style.display = "flex"; document.getElementById("loginEmail").value = ""; document.getElementById("loginPass").value = ""; document.getElementById("loginError").textContent = ""; } // ─── Events persistence ─────────────────────────────────────────────────── function loadEvents() { let saved = {}; try { saved = JSON.parse(localStorage.getItem("sh_events") || "{}"); } catch (e) { /* ignore */ } eventsData = ALL_EVENTS.map(function (e) { if (saved[e.id]) { // Merge: override saved fields onto the original, but preserve // subItems and history from saved if they were explicitly set const over = saved[e.id]; const merged = {}; for (var k in e) { merged[k] = e[k]; } for (var k in over) { merged[k] = over[k]; } return merged; } // shallow-clone to avoid mutating seed const clone = {}; for (var k in e) { clone[k] = e[k]; } clone.subItems = e.subItems ? e.subItems.map(function (s) { var c = {}; for (var ks in s) c[ks] = s[ks]; return c; }) : []; clone.history = e.history ? e.history.map(function (h) { var c = {}; for (var kh in h) c[kh] = h[kh]; return c; }) : []; return clone; }); renderTable(); renderStats(); renderNotif(); } function saveEvents() { var changes = {}; eventsData.forEach(function (ev) { var orig = null; for (var i = 0; i < ALL_EVENTS.length; i++) { if (ALL_EVENTS[i].id === ev.id) { orig = ALL_EVENTS[i]; break; } } if (!orig) return; var diff = {}; var fields = ["status", "region", "description", "quantity", "subItems", "history"]; var hasDiff = false; for (var fi = 0; fi < fields.length; fi++) { var f = fields[fi]; if (JSON.stringify(ev[f]) !== JSON.stringify(orig[f])) { diff[f] = JSON.parse(JSON.stringify(ev[f])); hasDiff = true; } } if (hasDiff) { changes[ev.id] = diff; } }); localStorage.setItem("sh_events", JSON.stringify(changes)); } // ─── Filtering ──────────────────────────────────────────────────────────── function getFilteredEvents() { var list = eventsData.slice(); var q = document.getElementById("filterSearch").value.toLowerCase(); var st = document.getElementById("filterStatus").value; var br = document.getElementById("filterBranch").value; if (q) list = list.filter(function (e) { return e.title.toLowerCase().indexOf(q) !== -1; }); if (st) list = list.filter(function (e) { return e.status === st; }); if (br) list = list.filter(function (e) { return e.branch === br; }); var sort = document.getElementById("filterSort").value; list.sort(function (a, b) { if (sort === "deadline") return (a.deadline || "").localeCompare(b.deadline || ""); if (sort === "title") return a.title.localeCompare(b.title); if (sort === "status") return (a.status || "").localeCompare(b.status || ""); return 0; }); return list; } // ─── Render table ───────────────────────────────────────────────────────── function renderTable() { var list = getFilteredEvents(); // Update branch filter dropdown var sel = document.getElementById("filterBranch"); var curVal = sel.value; var branches = []; var seen = {}; eventsData.forEach(function (e) { if (e.branch && !seen[e.branch]) { seen[e.branch] = true; branches.push(e.branch); } }); sel.innerHTML = '' + branches.map(function (b) { return ''; }).join(""); sel.value = curVal; var tbody = document.getElementById("eventsBody"); tbody.innerHTML = list.map(function (ev, i) { var dd = daysUntil(ev.deadline); var cls = ""; if (ev.status === "overdue" || (dd <= 0 && ev.status !== "completed")) cls = "overdue"; else if (dd <= 14 && ev.status !== "completed") cls = "warning"; else if (ev.status === "completed") cls = "completed"; var hasSub = ev.subItems && ev.subItems.length > 0; var statusClass = "status-" + (ev.status || "pending"); var subRows = ""; if (hasSub) { subRows = ev.subItems.map(function (s) { return '' + '' + esc(s.id) + '. ' + esc(s.title) + ' ' + esc(s.status || "pending") + ''; }).join(""); } var ddStr = ""; if (dd < 999 && dd > 0) { ddStr = ' (' + dd + ' дн.)'; } else if (dd <= 0 && ev.status !== "completed") { ddStr = ' (просрочено)'; } return '' + '' + (hasSub ? '' : "") + (i + 1) + '' + '' + esc(ev.title) + '' + '' + esc(ev.responsible || "") + '' + '' + esc(ev.section || "") + '' + '' + esc(ev.deadline || "") + ddStr + '' + '' + esc(ev.status || "pending") + '' + '' + '' + subRows; }).join(""); if (list.length === 0) { tbody.innerHTML = 'Нет данных'; } } function toggleSub(id) { var rows = document.querySelectorAll('.sub-body[data-parent="' + id + '"]'); rows.forEach(function (r) { r.style.display = r.style.display === "none" ? "" : "none"; }); // Toggle arrow direction var toggle = document.querySelector('span.sub-toggle[data-for="' + id + '"]'); } // ─── Stats ──────────────────────────────────────────────────────────────── function renderStats() { var total = eventsData.length; var completed = eventsData.filter(function (e) { return e.status === "completed"; }).length; var in_progress = eventsData.filter(function (e) { return e.status === "in_progress"; }).length; var overdue = eventsData.filter(function (e) { return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); }).length; document.getElementById("statsRow").innerHTML = '
' + total + '
Всего мероприятий
' + '
' + completed + '
Выполнено
' + '
' + in_progress + '
В работе
' + '
' + overdue + '
Просрочено
'; } // ─── Edit modal ─────────────────────────────────────────────────────────── function openEdit(id) { editingId = id; var ev = null; for (var i = 0; i < eventsData.length; i++) { if (eventsData[i].id === id) { ev = eventsData[i]; break; } } if (!ev) return; document.getElementById("editStatus").value = ev.status || "pending"; document.getElementById("editRegion").value = ev.region || ""; document.getElementById("editDesc").value = ev.description || ""; document.getElementById("editQty").value = ev.quantity || ""; document.getElementById("editFile").value = ""; renderEditFiles(id); document.getElementById("editModal").classList.add("open"); } function closeModal() { document.getElementById("editModal").classList.remove("open"); editingId = null; } function renderEditFiles(eventId) { var div = document.getElementById("editFileList"); var allFiles = {}; try { allFiles = JSON.parse(localStorage.getItem("sh_files") || "{}"); } catch (e) { /* ignore */ } var files = allFiles[eventId] || []; var html = ""; if (files.length > 0) { html += '
Файлы:
'; files.forEach(function (f, idx) { html += '
📄 ' + esc(f.name) + ' (' + Math.round(f.size / 1024) + ' KB)' + ' Удалить
'; }); } div.innerHTML = html || '
Нет загруженных файлов
'; } function deleteFile(eventId, idx) { var allFiles = {}; try { allFiles = JSON.parse(localStorage.getItem("sh_files") || "{}"); } catch (e) { /* ignore */ } var files = allFiles[eventId] || []; files.splice(idx, 1); allFiles[eventId] = files; localStorage.setItem("sh_files", JSON.stringify(allFiles)); renderEditFiles(eventId); } function saveEdit() { var ev = null; for (var i = 0; i < eventsData.length; i++) { if (eventsData[i].id === editingId) { ev = eventsData[i]; break; } } if (!ev) return; // Update event fields ev.status = document.getElementById("editStatus").value; ev.region = document.getElementById("editRegion").value; ev.description = document.getElementById("editDesc").value; ev.quantity = parseInt(document.getElementById("editQty").value) || 0; // Add history entry if (!ev.history) ev.history = []; ev.history.push({ timestamp: new Date().toISOString(), action: "updated", changes: { status: ev.status, region: ev.region, description: ev.description, quantity: ev.quantity } }); // Handle file upload var fileInput = document.getElementById("editFile"); if (fileInput.files.length > 0) { var file = fileInput.files[0]; if (file.size > 3 * 1024 * 1024) { alert("Файл превышает 3 МБ. Выберите файл меньшего размера."); return; } var reader = new FileReader(); reader.onload = function () { var allFiles = {}; try { allFiles = JSON.parse(localStorage.getItem("sh_files") || "{}"); } catch (e) { /* ignore */ } if (!allFiles[editingId]) allFiles[editingId] = []; allFiles[editingId].push({ name: file.name, size: file.size, type: file.type, data: reader.result, uploadedAt: new Date().toISOString() }); localStorage.setItem("sh_files", JSON.stringify(allFiles)); saveEvents(); renderTable(); renderStats(); renderEditFiles(editingId); document.getElementById("editFile").value = ""; }; reader.readAsDataURL(file); } else { saveEvents(); renderTable(); renderStats(); closeModal(); } } // ─── Notifications ──────────────────────────────────────────────────────── function renderNotif() { var list = []; eventsData.forEach(function (ev) { if (ev.status === "completed") return; var dd = daysUntil(ev.deadline); if (dd <= 0) list.push({ text: "Просрочено: " + ev.title, date: ev.deadline, type: "danger" }); else if (dd <= 1) list.push({ text: "Остался 1 день: " + ev.title, date: ev.deadline, type: "danger" }); else if (dd <= 7) list.push({ text: "Осталось " + dd + " дн.: " + ev.title, date: ev.deadline, type: "warning" }); else if (dd <= 14) list.push({ text: "Осталось " + dd + " дн.: " + ev.title, date: ev.deadline, type: "warning" }); else if (dd <= 30) list.push({ text: "Осталось " + dd + " дн.: " + ev.title, date: ev.deadline, type: "info" }); }); list.sort(function (a, b) { return a.date.localeCompare(b.date); }); var count = list.length; document.getElementById("notifCount").textContent = count > 99 ? "99+" : count; document.getElementById("notifList").innerHTML = list.slice(0, 50).map(function (n) { var color = n.type === "danger" ? "var(--danger)" : n.type === "warning" ? "var(--warning)" : "var(--accent)"; return '
' + esc(n.text) + '
' + esc(n.date) + '
'; }).join("") || '
Нет уведомлений
'; } function toggleNotif() { document.getElementById("notifPanel").classList.toggle("open"); } // ─── Analytics ──────────────────────────────────────────────────────────── function renderAnalytics() { var total = eventsData.length; var completed = eventsData.filter(function (e) { return e.status === "completed"; }).length; var in_progress = eventsData.filter(function (e) { return e.status === "in_progress"; }).length; var overdue = eventsData.filter(function (e) { return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); }).length; var pct = total ? Math.round(completed / total * 100) : 0; document.getElementById("analyticsStats").innerHTML = '
' + pct + '%
Выполнение
' + '
' + completed + '
Выполнено
' + '
' + in_progress + '
В работе
' + '
' + overdue + '
Просрочено
'; // Branches chart var branches = {}; eventsData.forEach(function (e) { var b = e.branch || "Неизвестно"; branches[b] = (branches[b] || 0) + 1; }); var maxB = Math.max.apply(null, Object.values(branches).concat([1])); var branchKeys = Object.keys(branches).sort(function (a, b) { return branches[b] - branches[a]; }); document.getElementById("branchChart").innerHTML = branchKeys.map(function (k) { return '
' + esc(k) + '
' + branches[k] + '
'; }).join(""); // Status chart var statuses = { completed: 0, in_progress: 0, pending: 0, overdue: 0 }; eventsData.forEach(function (e) { var s = e.status || "pending"; statuses[s] = (statuses[s] || 0) + 1; }); // Include overdue in the overdue count from the computed value var od = eventsData.filter(function (e) { return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); }).length; statuses.overdue = od; var maxS = Math.max.apply(null, Object.values(statuses).concat([1])); var labels = { completed: "Выполнено", in_progress: "В работе", pending: "Ожидает", overdue: "Просрочено" }; var colors = { completed: "var(--success)", in_progress: "var(--accent-bright)", pending: "var(--warning)", overdue: "var(--danger)" }; document.getElementById("statusChart").innerHTML = Object.keys(statuses).map(function (k) { return '
' + labels[k] + '
' + statuses[k] + '
'; }).join(""); } // ─── Reports ────────────────────────────────────────────────────────────── function generateReportHTML() { var now = new Date().toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" }); var total = eventsData.length; var completed = eventsData.filter(function (e) { return e.status === "completed"; }).length; var in_progress = eventsData.filter(function (e) { return e.status === "in_progress"; }).length; var overdue = eventsData.filter(function (e) { return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); }).length; var pending = eventsData.filter(function (e) { return e.status === "pending"; }).length; var pct = total ? Math.round(completed / total * 100) : 0; var rows = eventsData.map(function (e, i) { var stLabel = { completed: "Выполнено", in_progress: "В работе", pending: "Ожидает", overdue: "Просрочено" }; return '' + (i + 1) + '' + esc(e.title) + '' + esc(e.responsible || "") + '' + esc(e.section || "") + '' + esc(e.branch || "") + '' + esc(e.deadline || "") + '' + (stLabel[e.status] || e.status) + ''; }).join(""); return 'Отчёт по ПБ — АО «Самрук-Казына»' + '

Сводный отчёт по производственной безопасности

' + '

АО «Самрук-Казына»

' + '
Сформирован: ' + now + '
' + '
' + '
' + total + '
Всего
' + '
' + completed + '
Выполнено
' + '
' + in_progress + '
В работе
' + '
' + pending + '
Ожидает
' + '
' + overdue + '
Просрочено
' + '
' + pct + '%
Выполнение
' + '
' + '' + rows + '
МероприятиеОтветственныйРазделФилиалСрокСтатус
' + ''; } function downloadWord() { var html = generateReportHTML(); var blob = new Blob(["\ufeff" + html], { type: "application/msword" }); var url = URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.download = "Otchyot_PB.doc"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function downloadPdf() { var html = generateReportHTML(); var w = window.open("", "_blank"); w.document.write(html); w.document.close(); w.focus(); w.print(); } // ─── HSE Integration ────────────────────────────────────────────────────── function sendHseReport() { var result = document.getElementById("hseResult"); result.innerHTML = 'ⓘ Это статическая демо-версия. Интеграция с HSE.sk.kz недоступна в офлайн-режиме. Отчёт за месяц можно скачать на вкладке «Отчёты».'; } // ─── AI Chat ────────────────────────────────────────────────────────────── function sendChat() { var input = document.getElementById("chatInput"); var q = input.value.trim(); if (!q) return; input.value = ""; var msgs = document.getElementById("chatMsgs"); msgs.innerHTML += '
' + esc(q) + '
'; var answer = getAIAnswer(q); msgs.innerHTML += '
🤖 ИИ-агент
' + answer + '
'; msgs.scrollTop = msgs.scrollHeight; } function getAIAnswer(q) { var ev = eventsData; var total = ev.length; var completed = ev.filter(function (e) { return e.status === "completed"; }).length; var in_progress = ev.filter(function (e) { return e.status === "in_progress"; }).length; var pending = ev.filter(function (e) { return e.status === "pending"; }).length; var overdue = ev.filter(function (e) { return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); }).length; var ql = q.toLowerCase(); if (ql.indexOf("просрочен") !== -1 || ql.indexOf("overdue") !== -1) { var overdueList = ev.filter(function (e) { return e.status === "overdue" || (e.status !== "completed" && daysUntil(e.deadline) <= 0); }); if (overdueList.length === 0) return "Просроченных мероприятий нет."; return "Просроченные мероприятия (" + overdueList.length + "):
" + overdueList.map(function (e) { return "- " + esc(e.title) + " (срок: " + esc(e.deadline) + ", филиал: " + esc(e.branch || "—") + ")"; }).join("
"); } if (ql.indexOf("риск") !== -1) { var risky = ev.filter(function (e) { return e.status !== "completed" && daysUntil(e.deadline) <= 14 && daysUntil(e.deadline) > 0; }); if (risky.length === 0) return "Мероприятий с высоким риском срыва нет."; return "Высокий риск срыва у " + risky.length + " мероприятий:
" + risky.map(function (e) { return "- " + esc(e.title) + " (осталось " + daysUntil(e.deadline) + " дн., ответственный: " + esc(e.responsible || "—") + ")"; }).join("
"); } if (ql.indexOf("сводк") !== -1 || ql.indexOf("итог") !== -1) { return "Сводка по ПБ: всего " + total + " мероприятий. Выполнено: " + completed + ", в работе: " + in_progress + ", ожидает: " + pending + ", просрочено: " + overdue + ". Процент выполнения: " + (total ? Math.round(completed / total * 100) : 0) + "%."; } if (ql.indexOf("филиал") !== -1 || ql.indexOf("рейтинг") !== -1 || ql.indexOf("branch") !== -1) { var br = {}; ev.forEach(function (e) { var b = e.branch || "Неизвестно"; if (!br[b]) br[b] = { total: 0, done: 0 }; br[b].total++; if (e.status === "completed") br[b].done++; }); var sorted = Object.keys(br).sort(function (a, b) { return (br[b].done / br[b].total) - (br[a].done / br[a].total); }); return "Рейтинг филиалов по выполнению:
" + sorted.map(function (k, i) { return (i + 1) + ". " + esc(k) + ": " + br[k].done + "/" + br[k].total + " (" + Math.round(br[k].done / br[k].total * 100) + "%)"; }).join("
"); } if ((ql.indexOf("статус") !== -1 || ql.indexOf("номер") !== -1) && ql.match(/\d+/)) { var num = parseInt(ql.match(/\d+/)[0], 10); var found = ev[num - 1]; if (found) { return "№" + num + ": " + esc(found.title) + "
" + "Статус: " + esc(found.status) + " | Срок: " + esc(found.deadline) + " | Ответственный: " + esc(found.responsible) + " | Филиал: " + esc(found.branch) + " | Описание: " + esc(found.description || "—"); } return "Мероприятие №" + num + " не найдено."; } if (ql.indexOf("советник") !== -1 || ql.indexOf("рекомендац") !== -1) { var rlist = ev.filter(function (e) { return e.status !== "completed" && daysUntil(e.deadline) <= 14 && daysUntil(e.deadline) > 0; }); var olist = ev.filter(function (e) { return e.status === "overdue"; }); var adv = "Рекомендации:
"; if (olist.length) adv += "- Срочно принять меры по " + olist.length + " просроченным мероприятиям.
"; if (rlist.length) adv += "- Усилить контроль за " + rlist.length + " мероприятиями с приближающимся сроком.
"; if (pending > 5) adv += "- " + pending + " мероприятий ещё не начаты — требуется активизация.
"; if (adv === "Рекомендации:
") adv += "- Всё в порядке, продолжайте в том же духе."; return adv; } if (ql.indexOf("аудит") !== -1 || ql.indexOf("360") !== -1) { return "Аудит 360°:
- Всего: " + total + "
- Выполнено: " + completed + "
- В работе: " + in_progress + "
- Просрочено: " + overdue + "
- Ожидает: " + pending + "
- Выполнение: " + (total ? Math.round(completed / total * 100) : 0) + "%"; } if (ql.indexOf("прогноз") !== -1) { var willComplete = ev.filter(function (e) { return e.status === "in_progress" && daysUntil(e.deadline) <= 30 && daysUntil(e.deadline) > 0; }).length; var atRisk = ev.filter(function (e) { return (e.status === "pending" && daysUntil(e.deadline) <= 30) || (e.status !== "completed" && daysUntil(e.deadline) <= 7 && daysUntil(e.deadline) >= 0); }).length; return "Прогноз на 30 дней:
" + "- Ожидается завершение ~" + willComplete + " мероприятий в работе.
" + "- " + atRisk + " мероприятий под угрозой срыва.
" + "- Текущий процент выполнения: " + (total ? Math.round(completed / total * 100) : 0) + "%.
" + "- При сохранении темпа к концу месяца выполнение достигнет ~" + Math.min(100, Math.round((completed + willComplete) / total * 100)) + "%."; } return "Я анализирую данные по мероприятиям ПБ. Можете спросить:
" + "- «просроченные» — список просрочек
" + "- «риски» — мероприятия с высоким риском
" + "- «сводка» — общая статистика
" + "- «рейтинг филиалов» — кто в лидерах
" + "- «статус N» — информация по конкретному мероприятию
" + "- «советник» — рекомендации
" + "- «аудит 360» — полный срез
" + "- «прогноз» — что будет через месяц"; } // ─── Init ───────────────────────────────────────────────────────────────── function init() { loadEvents(); // Sidebar navigation document.querySelectorAll(".sidebar a[data-page]").forEach(function (a) { a.addEventListener("click", function (e) { e.preventDefault(); document.querySelectorAll(".sidebar a").forEach(function (x) { x.classList.remove("active"); }); this.classList.add("active"); document.querySelectorAll(".page").forEach(function (p) { p.classList.remove("active"); }); document.getElementById("page-" + this.dataset.page).classList.add("active"); if (this.dataset.page === "analytics") renderAnalytics(); }); }); // Event listeners document.getElementById("btnRefresh").addEventListener("click", loadEvents); document.getElementById("filterSearch").addEventListener("input", renderTable); document.getElementById("filterStatus").addEventListener("change", renderTable); document.getElementById("filterBranch").addEventListener("change", renderTable); document.getElementById("filterSort").addEventListener("change", renderTable); document.getElementById("dlWord").addEventListener("click", downloadWord); document.getElementById("dlPdf").addEventListener("click", downloadPdf); document.getElementById("hseSendBtn").addEventListener("click", sendHseReport); document.getElementById("chatSend").addEventListener("click", sendChat); document.getElementById("chatInput").addEventListener("keydown", function (e) { if (e.key === "Enter") sendChat(); }); document.getElementById("modalCancel").addEventListener("click", closeModal); document.getElementById("modalSave").addEventListener("click", saveEdit); document.getElementById("logoutBtn").addEventListener("click", logout); document.getElementById("notifBell").addEventListener("click", toggleNotif); document.getElementById("notifClose").addEventListener("click", function () { document.getElementById("notifPanel").classList.remove("open"); }); document.getElementById("editModal").addEventListener("click", function (e) { if (e.target === this) closeModal(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeModal(); }); // Set current month for HSE document.getElementById("hseMonth").value = new Date().toISOString().slice(0, 7); } // ─── Auto-login check ───────────────────────────────────────────────────── (function () { var isLoggedIn = localStorage.getItem("sh_token"); var savedUser = {}; try { savedUser = JSON.parse(localStorage.getItem("sh_user") || "{}"); } catch (e) { /* ignore */ } // Login button always needs listener var loginBtn = document.getElementById("loginBtn"); if (loginBtn) { loginBtn.addEventListener("click", login); } var loginPass = document.getElementById("loginPass"); if (loginPass) { loginPass.addEventListener("keydown", function (e) { if (e.key === "Enter") login(); }); } if (isLoggedIn && savedUser.email) { user = savedUser; document.getElementById("loginScreen").style.display = "none"; document.getElementById("app").style.display = "block"; document.getElementById("userName").textContent = user.name || ""; document.getElementById("userEmail").textContent = user.email || ""; init(); } })();