';
}
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();
}
})();