refactor: remove backend, extract CSS/JS, apply design.md palette

This commit is contained in:
Dauren777 2026-06-10 11:25:45 +00:00
parent 91c630c9b8
commit 29b1aafb15
3 changed files with 3104 additions and 526 deletions

View File

@ -4,547 +4,189 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ИИ-агент мониторинга ПБ — АО «Самрук-Казына»</title>
<style>
:root {
--bg-deep: #060E1A;
--bg-navy: #0A1628;
--bg-card: #112240;
--accent: #0088CC;
--accent-bright: #00A3FF;
--white: #FFFFFF;
--gray-text: #8892A4;
--gray-light: #A8B2C1;
--border: #1E3250;
--danger: #EF4444;
--warning: #F59E0B;
--success: #10B981;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font: 17px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
color: var(--white);
background: var(--bg-deep);
}
.container { max-width: 1140px; margin: 0 auto; padding: 80px 24px; }
/* Hero */
.hero {
background: linear-gradient(135deg, var(--bg-deep) 0%, var(--bg-navy) 50%, #0C1E3A 100%);
min-height: 100vh;
display: flex; align-items: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: "";
position: absolute;
top: -200px; right: -100px;
width: 600px; height: 600px;
background: radial-gradient(circle, rgba(0,163,255,0.08) 0%, transparent 70%);
border-radius: 50%;
}
.hero::after {
content: "";
position: absolute;
bottom: -150px; left: -100px;
width: 500px; height: 500px;
background: radial-gradient(circle, rgba(0,136,204,0.06) 0%, transparent 70%);
border-radius: 50%;
}
.hero .container { position: relative; z-index: 1; }
.hero .badge {
display: inline-block;
background: rgba(0,163,255,0.12);
color: var(--accent-bright);
padding: 6px 16px;
border-radius: 100px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
margin-bottom: 28px;
border: 1px solid rgba(0,163,255,0.2);
}
.hero h1 {
font-size: 52px; font-weight: 800; line-height: 1.08;
margin-bottom: 20px; max-width: 750px;
}
.hero h1 span { color: var(--accent-bright); }
.hero .subtitle {
font-size: 19px; color: var(--gray-text); max-width: 580px;
margin-bottom: 36px; line-height: 1.5;
}
.btn-primary {
display: inline-block;
background: var(--accent);
color: var(--white);
padding: 15px 32px;
border-radius: 8px;
font-weight: 700;
font-size: 16px;
text-decoration: none;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover { background: var(--accent-bright); }
.btn-outline {
display: inline-block;
background: transparent;
color: var(--white);
padding: 14px 30px;
border-radius: 8px;
font-weight: 600;
font-size: 16px;
text-decoration: none;
border: 2px solid var(--border);
cursor: pointer;
transition: border-color 0.2s;
margin-left: 12px;
}
.btn-outline:hover { border-color: var(--accent-bright); }
/* Section titles */
.section-label {
display: block;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--accent-bright);
margin-bottom: 12px;
}
.section h2 {
font-size: 36px; font-weight: 700;
margin-bottom: 16px; line-height: 1.2;
}
.section .section-desc {
color: var(--gray-text);
max-width: 600px;
margin-bottom: 48px;
}
/* Metrics */
.metrics {
background: var(--bg-navy);
border-bottom: 1px solid var(--border);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
text-align: center;
}
.metric-value {
font-size: 40px; font-weight: 800;
color: var(--accent-bright);
line-height: 1.1;
}
.metric-label {
font-size: 14px; color: var(--gray-text);
margin-top: 6px;
}
/* Modules */
.modules {
background: var(--bg-deep);
}
.modules-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.module-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px;
transition: border-color 0.2s;
}
.module-card:hover { border-color: var(--accent); }
.module-card .icon {
width: 44px; height: 44px;
background: rgba(0,163,255,0.1);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 22px;
margin-bottom: 16px;
}
.module-card h3 {
font-size: 17px; font-weight: 700;
margin-bottom: 8px;
}
.module-card p {
font-size: 14px; color: var(--gray-text);
line-height: 1.5;
}
/* AI section */
.ai-section {
background: var(--bg-navy);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.ai-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
align-items: center;
}
.ai-visual {
background: linear-gradient(135deg, var(--bg-card), #0C1E3A);
border: 1px solid var(--border);
border-radius: 16px;
padding: 40px;
text-align: center;
}
.ai-visual .big-icon {
font-size: 64px; margin-bottom: 16px;
}
.ai-visual h3 {
font-size: 20px; margin-bottom: 8px;
color: var(--accent-bright);
}
.ai-visual p {
font-size: 14px; color: var(--gray-text);
}
.ai-features {
list-style: none;
}
.ai-features li {
display: flex; gap: 14px;
padding: 12px 0;
border-bottom: 1px solid var(--border);
font-size: 15px;
}
.ai-features li:last-child { border: none; }
.ai-features .check {
color: var(--accent-bright);
font-weight: 700;
flex-shrink: 0;
}
/* Effects */
.effects {
background: var(--bg-deep);
}
.effects-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.effect-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px;
}
.effect-card .percent {
font-size: 36px; font-weight: 800;
color: var(--accent-bright);
margin-bottom: 4px;
}
.effect-card p {
font-size: 15px; color: var(--gray-text);
}
/* Integration */
.integration {
background: var(--bg-navy);
border-top: 1px solid var(--border);
}
.integration-tags {
display: flex; flex-wrap: wrap;
gap: 10px;
}
.tag {
background: var(--bg-card);
border: 1px solid var(--border);
padding: 8px 18px;
border-radius: 100px;
font-size: 14px;
color: var(--gray-light);
}
/* CTA */
.cta {
background: linear-gradient(135deg, var(--bg-deep), #0C1E3A);
border-top: 1px solid var(--border);
}
.cta-box {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 48px;
max-width: 640px;
}
.cta-box h2 {
font-size: 28px; margin-bottom: 8px;
}
.cta-box p {
color: var(--gray-text);
margin-bottom: 24px;
}
.form-group { margin-bottom: 16px; }
.form-group label {
display: block;
font-size: 13px; font-weight: 600;
color: var(--gray-light);
margin-bottom: 6px;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px 16px;
background: var(--bg-navy);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--white);
font-size: 15px;
font-family: inherit;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--accent);
}
.form-group textarea { resize: vertical; min-height: 80px; }
.btn-submit {
width: 100%;
padding: 14px;
background: var(--accent);
color: var(--white);
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
font-family: inherit;
transition: background 0.2s;
}
.btn-submit:hover { background: var(--accent-bright); }
/* Footer */
.footer {
background: var(--bg-deep);
border-top: 1px solid var(--border);
padding: 32px 24px;
text-align: center;
color: var(--gray-text);
font-size: 13px;
}
/* Responsive */
@media (max-width: 768px) {
.hero h1 { font-size: 32px; }
.container { padding: 48px 20px; }
.section h2 { font-size: 26px; }
.modules-grid { grid-template-columns: 1fr 1fr; }
.metrics-grid { grid-template-columns: 1fr 1fr; }
.ai-grid { grid-template-columns: 1fr; gap: 32px; }
.effects-grid { grid-template-columns: 1fr; }
.btn-outline { margin-left: 0; margin-top: 10px; }
.cta-box { padding: 28px 20px; }
}
@media (max-width: 480px) {
.modules-grid { grid-template-columns: 1fr; }
.metrics-grid { grid-template-columns: 1fr; }
}
</style>
<link rel="stylesheet" href="style.css">
</head>
<body>
<section class="hero">
<div class="container">
<div class="badge">Цифровая платформа</div>
<h1>ИИ-агент мониторинга <span>производственной безопасности</span></h1>
<p class="subtitle">
Единая цифровая платформа с искусственным интеллектом для автоматизации контроля исполнения мероприятий, анализа эффективности и подготовки управленческих решений в Группе компаний АО «Самрук-Казына».
</p>
<a class="btn-primary" href="#cta">Узнать подробнее</a>
<a class="btn-outline" href="#modules">Возможности системы</a>
<!-- Login -->
<div id="loginScreen">
<div class="login-box">
<h1>Мониторинг ПБ</h1>
<p>АО «Самрук-Казына» — План 2026</p>
<label>Корпоративный email</label>
<input type="email" id="loginEmail" placeholder="user@telecom.kz" autocomplete="email">
<label>Пароль</label>
<input type="password" id="loginPass" placeholder="Любой пароль" autocomplete="current-password">
<button id="loginBtn">Войти</button>
<div class="login-error" id="loginError"></div>
</div>
</section>
</div>
<section class="metrics">
<div class="container">
<div class="metrics-grid">
<div>
<div class="metric-value">-80%</div>
<div class="metric-label">Сокращение ручного сбора отчётности</div>
</div>
<div>
<div class="metric-value">100%</div>
<div class="metric-label">Прозрачность контроля исполнения</div>
</div>
<div>
<div class="metric-value">24/7</div>
<div class="metric-label">Мониторинг в реальном времени</div>
</div>
<div>
<div class="metric-value">x5</div>
<div class="metric-label">Быстрее подготовка отчётов</div>
</div>
<!-- App -->
<div id="app">
<!-- Sidebar -->
<div class="sidebar">
<div class="logo">ПБ <span>Монитор</span></div>
<a class="active" data-page="dashboard"><span class="icon">&#9776;</span> <span>Дашборд</span></a>
<a data-page="analytics"><span class="icon">&#128200;</span> <span>Аналитика</span></a>
<a data-page="reports"><span class="icon">&#128196;</span> <span>Отчёты</span></a>
<a data-page="hse"><span class="icon">&#128279;</span> <span>HSE.sk.kz</span></a>
<a data-page="ai"><span class="icon">&#129302;</span> <span>ИИ-помощник</span></a>
<div class="user-info">
<div class="name" id="userName"></div>
<div id="userEmail" style="font-size:11px"></div>
<div class="logout-btn" id="logoutBtn">Выйти</div>
</div>
</div>
</section>
<section id="modules" class="modules section">
<div class="container">
<span class="section-label">Возможности</span>
<h2>Модули системы</h2>
<p class="section-desc">Платформа охватывает все ключевые процессы управления производственной безопасностью — от сбора данных до отчётности перед руководством.</p>
<div class="modules-grid">
<div class="module-card">
<div class="icon">&#128202;</div>
<h3>Централизованный сбор данных</h3>
<p>Сбор отчётности от филиалов и ДО через веб-интерфейс. Единая база данных исполнения Плана.</p>
</div>
<div class="module-card">
<div class="icon">&#128203;</div>
<h3>Управление мероприятиями</h3>
<p>Электронный реестр, назначение ответственных, контрольные сроки и мониторинг статусов.</p>
</div>
<div class="module-card">
<div class="icon">&#128206;</div>
<h3>Подтверждение исполнения</h3>
<p>Загрузка фото, актов, протоколов, приказов, презентаций и видео. Электронный архив материалов.</p>
</div>
<div class="module-card">
<div class="icon">&#128195;</div>
<h3>Цифровой паспорт мероприятия</h3>
<p>Карточка с описанием, сроками, исполнителями, статусом, историей изменений и выводами ИИ.</p>
</div>
<div class="module-card">
<div class="icon">&#129302;</div>
<h3>Искусственный интеллект</h3>
<p>Анализ отчётов, проверка полноты, выявление рисков, формирование выводов и рекомендаций.</p>
</div>
<div class="module-card">
<div class="icon">&#9200;</div>
<h3>Контроль дисциплины</h3>
<p>Мониторинг сроков, автонапоминания, эскалация просрочек, рейтинг организаций.</p>
</div>
<div class="module-card">
<div class="icon">&#128200;</div>
<h3>Аналитика и дашборды</h3>
<p>Процент исполнения, рейтинг качества, анализ просрочек, карта рисков, динамика по периодам.</p>
</div>
<div class="module-card">
<div class="icon">&#128196;</div>
<h3>Формирование отчётности</h3>
<p>Автоматические ежемесячные, квартальные и годовые отчёты, справки и презентации.</p>
</div>
<div class="module-card">
<div class="icon">&#128101;</div>
<h3>Управленческий помощник</h3>
<p>Подготовка проектов поручений, выявление системных нарушений, прогноз достижения KPI.</p>
</div>
</div>
</div>
</section>
<section class="ai-section section">
<div class="container">
<span class="section-label">Искусственный интеллект</span>
<div class="ai-grid">
<div class="ai-visual">
<div class="big-icon">&#129302;</div>
<h3>ИИ-агент анализирует и рекомендует</h3>
<p>Система не просто собирает данные — она помогает принимать решения.</p>
</div>
<div>
<ul class="ai-features">
<li><span class="check">&#10003;</span> Анализ представленных отчётов и подтверждающих материалов</li>
<li><span class="check">&#10003;</span> Проверка полноты и достаточности подтверждения исполнения</li>
<li><span class="check">&#10003;</span> Выявление отсутствующих документов</li>
<li><span class="check">&#10003;</span> Оценка рисков нарушения сроков</li>
<li><span class="check">&#10003;</span> Автоматическое формирование выводов и рекомендаций</li>
<li><span class="check">&#10003;</span> Подготовка кратких аналитических справок для руководства</li>
</ul>
</div>
</div>
</div>
</section>
<section class="effects section">
<div class="container">
<span class="section-label">Ожидаемый эффект</span>
<h2>Что изменится после внедрения</h2>
<p class="section-desc">Цифровизация процессов производственной безопасности принесёт измеримые результаты.</p>
<div class="effects-grid">
<div class="effect-card">
<div class="percent">-80%</div>
<p>Сокращение ручного сбора отчётности и времени на подготовку сводок</p>
</div>
<div class="effect-card">
<div class="percent">100%</div>
<p>Достоверность и прозрачность контроля исполнения каждого мероприятия</p>
</div>
<div class="effect-card">
<div class="percent">&#9888; 0</div>
<p>Пропущенных сроков — оперативное выявление рисков и нарушений</p>
</div>
<div class="effect-card">
<div class="percent">Real-time</div>
<p>Формирование управленческой отчётности в режиме реального времени</p>
</div>
</div>
</div>
</section>
<section class="integration section">
<div class="container">
<span class="section-label">Интеграции</span>
<h2>Встраивается в существующую ИТ-инфраструктуру</h2>
<p class="section-desc" style="margin-bottom:24px">Платформа интегрируется с корпоративными системами холдинга.</p>
<div class="integration-tags">
<span class="tag">Корпоративная электронная почта</span>
<span class="tag">Системы электронного документооборота</span>
<span class="tag">Корпоративные BI-платформы</span>
<span class="tag">Excel / Word / PDF</span>
</div>
</div>
</section>
<section id="cta" class="cta section">
<div class="container">
<div class="cta-box">
<h2>Узнайте подробнее о системе</h2>
<p>Оставьте контакты — мы свяжемся, чтобы провести демонстрацию платформы и обсудить внедрение.</p>
<form action="https://formspree.io/f/example" method="POST">
<div class="form-group">
<label for="name">Имя</label>
<input type="text" id="name" name="name" placeholder="Иванов Иван Иванович" required>
<div class="main">
<!-- Dashboard page -->
<div class="page active" id="page-dashboard">
<div class="page-header">
<div style="display:flex;align-items:center;gap:12px">
<h2>Мероприятия ПБ</h2>
<div class="notif-bell" id="notifBell">&#128276;<span class="badge" id="notifCount">0</span></div>
</div>
<div class="form-group">
<label for="org">Организация</label>
<input type="text" id="org" name="org" placeholder="Наименование филиала / ДО">
<div class="actions">
<button class="btn btn-outline btn-sm" id="btnRefresh">&#x21bb; Обновить</button>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="email@sk.kz" required>
</div>
<!-- Stats -->
<div class="stats-grid" id="statsRow"></div>
<!-- Filters -->
<div class="filters">
<input id="filterSearch" placeholder="Поиск по названию...">
<select id="filterStatus"><option value="">Все статусы</option>
<option value="completed">Выполнено</option><option value="in_progress">В работе</option>
<option value="pending">Ожидает</option><option value="overdue">Просрочено</option>
</select>
<select id="filterBranch"><option value="">Все филиалы</option></select>
<label style="font-size:12px">Сортировка:</label>
<select id="filterSort"><option value="deadline">По сроку</option><option value="title">По названию</option><option value="status">По статусу</option></select>
</div>
<!-- Table -->
<div class="table-wrap">
<table>
<thead><tr>
<th data-sort="idx" style="width:40px"></th>
<th data-sort="title">Мероприятие</th>
<th data-sort="responsible">Ответственный</th>
<th data-sort="section">Раздел</th>
<th data-sort="deadline">Срок</th>
<th data-sort="status">Статус</th>
<th style="width:60px"></th>
</tr></thead>
<tbody id="eventsBody"></tbody>
</table>
</div>
</div>
<!-- Analytics page -->
<div class="page" id="page-analytics">
<div class="page-header"><h2>Аналитика</h2></div>
<div class="stats-grid" id="analyticsStats"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div class="report-card">
<h3>По филиалам</h3>
<div id="branchChart"></div>
</div>
<div class="form-group">
<label for="msg">Комментарий</label>
<textarea id="msg" name="msg" placeholder="Опишите, что вас интересует..."></textarea>
<div class="report-card">
<h3>По статусам</h3>
<div id="statusChart"></div>
</div>
<button type="submit" class="btn-submit">Отправить заявку</button>
</form>
</div>
</div>
<!-- Reports page -->
<div class="page" id="page-reports">
<div class="page-header"><h2>Скачать отчёты</h2></div>
<div class="report-card">
<h3>Word (.docx)</h3>
<p>Полный сводный отчёт по всем мероприятиям с таблицей и статистикой</p>
<button class="btn btn-primary" id="dlWord">Скачать DOCX</button>
</div>
<div class="report-card">
<h3>PDF</h3>
<p>Сводный отчёт в формате PDF для печати и рассылки</p>
<button class="btn btn-primary" id="dlPdf">Скачать PDF</button>
</div>
</div>
<!-- HSE page -->
<div class="page" id="page-hse">
<div class="page-header"><h2>Интеграция с HSE.sk.kz</h2></div>
<div class="report-card">
<h3>Отправка сводного отчёта</h3>
<p>Сформировать подписанный отчёт за месяц и отправить в HSE.sk.kz</p>
<div class="hse-config">
<label>Месяц отчёта</label>
<input type="month" id="hseMonth">
<label>Формат</label>
<select id="hseFormat"><option value="word">Word</option><option value="pdf">PDF</option></select>
<label>API Key HSE.sk.kz</label>
<input type="password" id="hseApiKey" placeholder="API ключ для аутентификации">
<div class="hint">API ключ выдаётся администратором HSE.sk.kz</div>
<label>Endpoint (опционально)</label>
<input type="url" id="hseEndpoint" placeholder="https://hse.sk.kz/api/v1/documents/upload">
<button class="btn btn-primary" id="hseSendBtn" style="margin-top:14px">&#128228; Отправить отчёт</button>
<div id="hseResult" style="margin-top:12px;font-size:13px"></div>
</div>
</div>
</div>
<!-- AI page -->
<div class="page" id="page-ai">
<div class="page-header"><h2>ИИ-помощник</h2></div>
<div class="chat-box">
<div class="chat-msgs" id="chatMsgs">
<div class="chat-msg ai"><div class="label">&#129302; ИИ-агент</div>Здравствуйте! Я анализирую данные по мероприятиям ПБ. Спросите о статусах, рисках, просрочках или рейтинге филиалов.</div>
</div>
<div class="chat-input-wrap">
<input id="chatInput" placeholder="Напишите вопрос..." />
<button class="btn btn-primary" id="chatSend">Отправить</button>
</div>
</div>
</div>
</div>
</section>
</div>
<footer class="footer">
АО «Самрук-Казына» &copy; 2026 &middot; Цифровая платформа производственной безопасности
</footer>
<!-- Edit modal -->
<div class="modal-overlay" id="editModal">
<div class="modal">
<h3>Редактировать мероприятие</h3>
<label>Статус</label>
<select id="editStatus">
<option value="pending">Ожидает</option>
<option value="in_progress">В работе</option>
<option value="completed">Выполнено</option>
<option value="overdue">Просрочено</option>
</select>
<label>Регион</label>
<input id="editRegion" placeholder="Регион">
<label>Описание выполнения (напишите отчёт)</label>
<textarea id="editDesc" placeholder="Что сделано?"></textarea>
<label>Количественный показатель</label>
<input type="number" id="editQty" placeholder="0" min="0">
<label>Прикрепить файл (PDF, DOC, XLS, JPG/PNG, PPT — до 3 МБ каждый)</label>
<input type="file" id="editFile">
<div id="editFileList" class="file-list"></div>
<div class="btn-group">
<button class="btn btn-outline" id="modalCancel">Отмена</button>
<button class="btn btn-primary" id="modalSave">Сохранить</button>
</div>
</div>
</div>
<!-- Notifications panel -->
<div class="notif-panel" id="notifPanel">
<span class="close" id="notifClose">&times;</span>
<h3>Уведомления</h3>
<div id="notifList"></div>
</div>
<script src="script.js"></script>
</body>
</html>

789
script.js Normal file
View File

@ -0,0 +1,789 @@
// ─── 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();
}
})();

2147
style.css Normal file

File diff suppressed because it is too large Load Diff