samruk-hse-ai/script.js

790 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─── 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 = '<option value="">Все филиалы</option>' +
branches.map(function (b) { return '<option value="' + esc(b) + '">' + esc(b) + '</option>'; }).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 '<tr class="sub-row sub-body" data-parent="' + ev.id + '" style="display:none"><td></td><td colspan="5">' +
'<span style="color:var(--gray-light)">' + esc(s.id) + '.</span> ' + esc(s.title) +
' <span class="status-badge status-' + (s.status || "pending") + '">' + esc(s.status || "pending") + '</span></td></tr>';
}).join("");
}
var ddStr = "";
if (dd < 999 && dd > 0) {
ddStr = ' <span style="font-size:11px;color:var(--gray-text)">(' + dd + ' дн.)</span>';
} else if (dd <= 0 && ev.status !== "completed") {
ddStr = ' <span style="font-size:11px;color:var(--danger)">(просрочено)</span>';
}
return '<tr class="' + cls + '" data-id="' + ev.id + '">' +
'<td>' + (hasSub ? '<span class="sub-toggle" onclick="toggleSub(\'' + ev.id + '\')">&#9654;</span>' : "") + (i + 1) + '</td>' +
'<td><strong>' + esc(ev.title) + '</strong></td>' +
'<td>' + esc(ev.responsible || "") + '</td>' +
'<td>' + esc(ev.section || "") + '</td>' +
'<td>' + esc(ev.deadline || "") + ddStr + '</td>' +
'<td><span class="status-badge ' + statusClass + '">' + esc(ev.status || "pending") + '</span></td>' +
'<td><button class="btn btn-outline btn-sm" onclick="openEdit(\'' + ev.id + '\')">&#9998;</button></td>' +
'</tr>' + subRows;
}).join("");
if (list.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--gray-text);padding:40px">Нет данных</td></tr>';
}
}
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 =
'<div class="stat-card"><div class="num blue">' + total + '</div><div class="label">Всего мероприятий</div></div>' +
'<div class="stat-card"><div class="num green">' + completed + '</div><div class="label">Выполнено</div></div>' +
'<div class="stat-card"><div class="num yellow">' + in_progress + '</div><div class="label">В работе</div></div>' +
'<div class="stat-card"><div class="num red">' + overdue + '</div><div class="label">Просрочено</div></div>';
}
// ─── 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 += '<div style="font-size:11px;color:var(--gray-text);margin:6px 0">Файлы:</div>';
files.forEach(function (f, idx) {
html += '<div class="file-item">&#128196; ' + esc(f.name) +
' (' + Math.round(f.size / 1024) + ' KB)' +
' <a href="#" onclick="deleteFile(\'' + eventId + '\',' + idx + ');return false" style="color:var(--danger);font-size:11px;margin-left:8px">Удалить</a></div>';
});
}
div.innerHTML = html || '<div style="font-size:11px;color:var(--gray-text)">Нет загруженных файлов</div>';
}
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 '<div class="notif-item" style="border-left:3px solid ' + color + ';padding-left:10px"><div>' + esc(n.text) + '</div><div class="date">' + esc(n.date) + '</div></div>';
}).join("") || '<div style="font-size:13px;color:var(--gray-text)">Нет уведомлений</div>';
}
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 =
'<div class="stat-card"><div class="num blue">' + pct + '%</div><div class="label">Выполнение</div></div>' +
'<div class="stat-card"><div class="num green">' + completed + '</div><div class="label">Выполнено</div></div>' +
'<div class="stat-card"><div class="num yellow">' + in_progress + '</div><div class="label">В работе</div></div>' +
'<div class="stat-card"><div class="num red">' + overdue + '</div><div class="label">Просрочено</div></div>';
// 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 '<div class="chart-bar"><span class="bar-label">' + esc(k) + '</span><div class="bar-fill" style="width:' + (branches[k] / maxB * 200) + 'px"></div><span class="bar-val">' + branches[k] + '</span></div>';
}).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 '<div class="chart-bar"><span class="bar-label">' + labels[k] + '</span><div class="bar-fill" style="width:' + (statuses[k] / maxS * 200) + 'px;background:' + colors[k] + '"></div><span class="bar-val">' + statuses[k] + '</span></div>';
}).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 '<tr><td>' + (i + 1) + '</td><td>' + esc(e.title) + '</td><td>' + esc(e.responsible || "") + '</td><td>' + esc(e.section || "") + '</td><td>' + esc(e.branch || "") + '</td><td>' + esc(e.deadline || "") + '</td><td>' + (stLabel[e.status] || e.status) + '</td></tr>';
}).join("");
return '<html><head><meta charset="utf-8"><title>Отчёт по ПБ — АО «Самрук-Казына»</title><style>' +
'body{font-family:Arial,sans-serif;color:#222;padding:40px;max-width:900px;margin:0 auto}' +
'h1{font-size:22px;margin-bottom:4px}h2{font-size:16px;color:#555;margin-bottom:20px}' +
'.meta{font-size:12px;color:#888;margin-bottom:24px}' +
'.stats{display:flex;gap:16px;margin-bottom:24px;flex-wrap:wrap}' +
'.stat{padding:12px 16px;border:1px solid #ddd;border-radius:8px;min-width:130px}' +
'.stat .n{font-size:24px;font-weight:bold}.stat .l{font-size:12px;color:#666}' +
'table{width:100%;border-collapse:collapse;font-size:13px}' +
'th,td{padding:8px 10px;border:1px solid #ddd;text-align:left}' +
'th{background:#f5f5f5;font-weight:600}' +
'</style></head><body>' +
'<h1>Сводный отчёт по производственной безопасности</h1>' +
'<h2>АО «Самрук-Казына»</h2>' +
'<div class="meta">Сформирован: ' + now + '</div>' +
'<div class="stats">' +
'<div class="stat"><div class="n">' + total + '</div><div class="l">Всего</div></div>' +
'<div class="stat"><div class="n" style="color:green">' + completed + '</div><div class="l">Выполнено</div></div>' +
'<div class="stat"><div class="n" style="color:#00a3ff">' + in_progress + '</div><div class="l">В работе</div></div>' +
'<div class="stat"><div class="n" style="color:#f59e0b">' + pending + '</div><div class="l">Ожидает</div></div>' +
'<div class="stat"><div class="n" style="color:red">' + overdue + '</div><div class="l">Просрочено</div></div>' +
'<div class="stat"><div class="n" style="color:#00a3ff">' + pct + '%</div><div class="l">Выполнение</div></div>' +
'</div>' +
'<table><thead><tr><th>№</th><th>Мероприятие</th><th>Ответственный</th><th>Раздел</th><th>Филиал</th><th>Срок</th><th>Статус</th></tr></thead><tbody>' +
rows + '</tbody></table>' +
'</body></html>';
}
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 = '<span style="color:var(--accent-bright)">&#9432; Это статическая демо-версия. Интеграция с HSE.sk.kz недоступна в офлайн-режиме. Отчёт за месяц можно скачать на вкладке «Отчёты».</span>';
}
// ─── 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 += '<div class="chat-msg user">' + esc(q) + '</div>';
var answer = getAIAnswer(q);
msgs.innerHTML += '<div class="chat-msg ai"><div class="label">&#129302; ИИ-агент</div>' + answer + '</div>';
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 + "):<br>" +
overdueList.map(function (e) {
return "- " + esc(e.title) + " (срок: " + esc(e.deadline) + ", филиал: " + esc(e.branch || "—") + ")";
}).join("<br>");
}
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 + " мероприятий:<br>" +
risky.map(function (e) {
return "- " + esc(e.title) + " (осталось " + daysUntil(e.deadline) + " дн., ответственный: " + esc(e.responsible || "—") + ")";
}).join("<br>");
}
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 "Рейтинг филиалов по выполнению:<br>" +
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("<br>");
}
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 + ": <strong>" + esc(found.title) + "</strong><br>" +
"Статус: " + 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 = "Рекомендации:<br>";
if (olist.length) adv += "- Срочно принять меры по " + olist.length + " просроченным мероприятиям.<br>";
if (rlist.length) adv += "- Усилить контроль за " + rlist.length + " мероприятиями с приближающимся сроком.<br>";
if (pending > 5) adv += "- " + pending + " мероприятий ещё не начаты — требуется активизация.<br>";
if (adv === "Рекомендации:<br>") adv += "- Всё в порядке, продолжайте в том же духе.";
return adv;
}
if (ql.indexOf("аудит") !== -1 || ql.indexOf("360") !== -1) {
return "Аудит 360°:<br>- Всего: " + total +
"<br>- Выполнено: " + completed +
"<br>- В работе: " + in_progress +
"<br>- Просрочено: " + overdue +
"<br>- Ожидает: " + pending +
"<br>- Выполнение: " + (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 дней:<br>" +
"- Ожидается завершение ~" + willComplete + " мероприятий в работе.<br>" +
"- " + atRisk + " мероприятий под угрозой срыва.<br>" +
"- Текущий процент выполнения: " + (total ? Math.round(completed / total * 100) : 0) + "%.<br>" +
"- При сохранении темпа к концу месяца выполнение достигнет ~" + Math.min(100, Math.round((completed + willComplete) / total * 100)) + "%.";
}
return "Я анализирую данные по мероприятиям ПБ. Можете спросить:<br>" +
"- «просроченные» — список просрочек<br>" +
"- «риски» — мероприятия с высоким риском<br>" +
"- «сводка» — общая статистика<br>" +
"- «рейтинг филиалов» — кто в лидерах<br>" +
"- «статус N» — информация по конкретному мероприятию<br>" +
"- «советник» — рекомендации<br>" +
"- «аудит 360» — полный срез<br>" +
"- «прогноз» — что будет через месяц";
}
// ─── 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();
}
})();