refactor: remove backend, extract CSS/JS, apply design.md palette
This commit is contained in:
parent
91c630c9b8
commit
29b1aafb15
694
index.html
694
index.html
@ -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">☰</span> <span>Дашборд</span></a>
|
||||
<a data-page="analytics"><span class="icon">📈</span> <span>Аналитика</span></a>
|
||||
<a data-page="reports"><span class="icon">📄</span> <span>Отчёты</span></a>
|
||||
<a data-page="hse"><span class="icon">🔗</span> <span>HSE.sk.kz</span></a>
|
||||
<a data-page="ai"><span class="icon">🤖</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">📊</div>
|
||||
<h3>Централизованный сбор данных</h3>
|
||||
<p>Сбор отчётности от филиалов и ДО через веб-интерфейс. Единая база данных исполнения Плана.</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="icon">📋</div>
|
||||
<h3>Управление мероприятиями</h3>
|
||||
<p>Электронный реестр, назначение ответственных, контрольные сроки и мониторинг статусов.</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="icon">📎</div>
|
||||
<h3>Подтверждение исполнения</h3>
|
||||
<p>Загрузка фото, актов, протоколов, приказов, презентаций и видео. Электронный архив материалов.</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="icon">📃</div>
|
||||
<h3>Цифровой паспорт мероприятия</h3>
|
||||
<p>Карточка с описанием, сроками, исполнителями, статусом, историей изменений и выводами ИИ.</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="icon">🤖</div>
|
||||
<h3>Искусственный интеллект</h3>
|
||||
<p>Анализ отчётов, проверка полноты, выявление рисков, формирование выводов и рекомендаций.</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="icon">⏰</div>
|
||||
<h3>Контроль дисциплины</h3>
|
||||
<p>Мониторинг сроков, автонапоминания, эскалация просрочек, рейтинг организаций.</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="icon">📈</div>
|
||||
<h3>Аналитика и дашборды</h3>
|
||||
<p>Процент исполнения, рейтинг качества, анализ просрочек, карта рисков, динамика по периодам.</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="icon">📄</div>
|
||||
<h3>Формирование отчётности</h3>
|
||||
<p>Автоматические ежемесячные, квартальные и годовые отчёты, справки и презентации.</p>
|
||||
</div>
|
||||
<div class="module-card">
|
||||
<div class="icon">👥</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">🤖</div>
|
||||
<h3>ИИ-агент анализирует и рекомендует</h3>
|
||||
<p>Система не просто собирает данные — она помогает принимать решения.</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="ai-features">
|
||||
<li><span class="check">✓</span> Анализ представленных отчётов и подтверждающих материалов</li>
|
||||
<li><span class="check">✓</span> Проверка полноты и достаточности подтверждения исполнения</li>
|
||||
<li><span class="check">✓</span> Выявление отсутствующих документов</li>
|
||||
<li><span class="check">✓</span> Оценка рисков нарушения сроков</li>
|
||||
<li><span class="check">✓</span> Автоматическое формирование выводов и рекомендаций</li>
|
||||
<li><span class="check">✓</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">⚠ 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">🔔<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">↻ Обновить</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">📤 Отправить отчёт</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">🤖 ИИ-агент</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">
|
||||
АО «Самрук-Казына» © 2026 · Цифровая платформа производственной безопасности
|
||||
</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">×</span>
|
||||
<h3>Уведомления</h3>
|
||||
<div id="notifList"></div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
789
script.js
Normal file
789
script.js
Normal 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 + '\')">▶</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 + '\')">✎</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">📄 ' + 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)">ⓘ Это статическая демо-версия. Интеграция с 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">🤖 ИИ-агент</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();
|
||||
}
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user