prombez-analytics/index.html

1972 lines
64 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ИИ-агент — Аналитика производственной безопасности</title>
<style>
:root {
--ink: #0F1218;
--blue: #2563EB;
--blue-50: #EFF6FF;
--blue-100: #DBEAFE;
--green: #059669;
--green-50: #ECFDF5;
--red: #DC2626;
--red-50: #FEF2F2;
--amber: #D97706;
--amber-50: #FFFBEB;
--white: #FFFFFF;
--gray-50: #F9FAFB;
--gray-100: #F2F4F7;
--gray-200: #E5E7EB;
--gray-500: #5B6573;
--gray-600: #4B5563;
}
* { 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(--ink);
background: var(--white);
}
.container {
max-width: 1140px;
margin: 0 auto;
padding: 0 24px;
}
/* Hero */
.hero {
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
color: var(--white);
padding: 100px 0 80px;
}
.hero-badge {
display: inline-block;
background: rgba(255,255,255,0.15);
color: var(--white);
padding: 6px 16px;
border-radius: 100px;
font-size: 14px;
font-weight: 600;
margin-bottom: 24px;
backdrop-filter: blur(10px);
}
.hero h1 {
font-size: 48px;
font-weight: 800;
line-height: 1.1;
max-width: 700px;
margin-bottom: 20px;
}
.hero p {
font-size: 19px;
color: rgba(255,255,255,0.8);
max-width: 560px;
margin-bottom: 36px;
line-height: 1.6;
}
.hero-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 28px;
border-radius: 8px;
font-weight: 700;
font-size: 15px;
text-decoration: none;
transition: transform 0.15s, box-shadow 0.15s;
cursor: pointer;
border: none;
font-family: inherit;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.btn-primary {
background: var(--white);
color: #1D4ED8;
}
.btn-outline {
background: transparent;
color: var(--white);
border: 2px solid rgba(255,255,255,0.4);
}
.btn-outline:hover {
border-color: var(--white);
}
.btn-blue {
background: var(--blue);
color: var(--white);
}
.btn-blue:hover {
background: #1D4ED8;
}
.btn-sm {
padding: 8px 18px;
font-size: 13px;
font-weight: 600;
}
/* Section */
.section {
padding: 80px 0;
}
.section-label {
display: inline-block;
background: var(--blue-50);
color: var(--blue);
padding: 4px 14px;
border-radius: 100px;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 16px;
}
.section h2 {
font-size: 36px;
font-weight: 700;
margin-bottom: 12px;
line-height: 1.2;
}
.section-subtitle {
font-size: 17px;
color: var(--gray-500);
max-width: 600px;
margin-bottom: 40px;
}
/* Quick Links */
.quick-links {
background: var(--gray-50);
}
.quick-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.quick-card {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: 12px;
padding: 28px;
text-decoration: none;
color: var(--ink);
transition: border-color 0.2s, box-shadow 0.2s;
display: flex;
align-items: flex-start;
gap: 16px;
}
.quick-card:hover {
border-color: var(--blue);
box-shadow: 0 4px 16px rgba(37,99,235,0.08);
}
.quick-card-icon {
font-size: 28px;
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.quick-card-icon.blue { background: var(--blue-50); }
.quick-card-icon.green { background: var(--green-50); }
.quick-card-icon.red { background: var(--red-50); }
.quick-card h3 {
font-size: 17px;
font-weight: 700;
margin-bottom: 4px;
}
.quick-card p {
font-size: 14px;
color: var(--gray-500);
}
/* Blocks */
.blocks-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
}
.block-card {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: 12px;
padding: 32px;
transition: border-color 0.2s;
}
.block-card:hover {
border-color: var(--blue-100);
}
.block-number {
font-size: 13px;
font-weight: 700;
color: var(--blue);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.block-card h3 {
font-size: 20px;
font-weight: 700;
margin-bottom: 12px;
}
.block-card ul {
list-style: none;
}
.block-card li {
font-size: 15px;
color: var(--gray-600);
padding: 6px 0;
padding-left: 20px;
position: relative;
}
.block-card li::before {
content: '';
position: absolute;
left: 0;
top: 13px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--blue);
}
/* Steps */
.steps {
display: grid;
gap: 16px;
counter-reset: step;
}
.step {
display: flex;
gap: 20px;
align-items: flex-start;
}
.step-num {
width: 44px;
height: 44px;
background: var(--blue);
color: var(--white);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 18px;
flex-shrink: 0;
counter-increment: step;
}
.step-num::before {
content: counter(step);
}
.step-text h3 {
font-size: 17px;
font-weight: 700;
margin-bottom: 4px;
}
.step-text p {
font-size: 15px;
color: var(--gray-500);
}
/* Categories */
.cat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.cat-tag {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: 8px;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.cat-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.cat-dot.d1 { background: #2563EB; }
.cat-dot.d2 { background: #7C3AED; }
.cat-dot.d3 { background: #DC2626; }
.cat-dot.d4 { background: #EA580C; }
.cat-dot.d5 { background: #D97706; }
.cat-dot.d6 { background: #059669; }
.cat-dot.d7 { background: #0891B2; }
.cat-dot.d8 { background: #4F46E5; }
.cat-dot.d9 { background: #DB2777; }
/* Dashboard Preview */
.dash-preview {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: 16px;
padding: 40px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 24px;
}
.dash-metric {
text-align: center;
padding: 20px;
background: var(--white);
border-radius: 10px;
}
.dash-metric .value {
font-size: 36px;
font-weight: 800;
color: var(--blue);
line-height: 1;
margin-bottom: 6px;
}
.dash-metric .label {
font-size: 14px;
color: var(--gray-500);
}
.dash-chart {
grid-column: 1 / -1;
background: var(--white);
border-radius: 10px;
padding: 24px;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-bars {
display: flex;
align-items: flex-end;
gap: 16px;
height: 160px;
}
.chart-bar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.chart-bar {
width: 40px;
border-radius: 6px 6px 0 0;
transition: height 0.3s;
}
.chart-bar-label {
font-size: 11px;
color: var(--gray-500);
font-weight: 600;
max-width: 60px;
text-align: center;
line-height: 1.2;
}
/* CTA */
.cta {
background: linear-gradient(135deg, #1D4ED8 0%, #1E40AF 100%);
color: var(--white);
text-align: center;
padding: 80px 0;
}
.cta h2 {
font-size: 32px;
font-weight: 700;
margin-bottom: 12px;
}
.cta p {
color: rgba(255,255,255,0.7);
margin-bottom: 28px;
max-width: 480px;
margin-left: auto;
margin-right: auto;
}
.cta .btn {
background: var(--white);
color: #1D4ED8;
}
/* Footer */
.footer {
background: var(--gray-50);
border-top: 1px solid var(--gray-200);
padding: 32px 0;
text-align: center;
}
.footer p {
font-size: 14px;
color: var(--gray-500);
}
/* ===================================== */
/* ANALYZER */
/* ===================================== */
.analyzer {
background: var(--gray-50);
}
.upload-area {
background: var(--white);
border: 2px dashed var(--gray-200);
border-radius: 16px;
padding: 48px 24px;
text-align: center;
transition: border-color 0.2s, background 0.2s;
cursor: pointer;
margin-bottom: 16px;
}
.upload-area:hover,
.upload-area.drag-over {
border-color: var(--blue);
background: var(--blue-50);
}
.upload-area .upload-icon {
font-size: 40px;
margin-bottom: 12px;
}
.upload-area p {
color: var(--gray-500);
font-size: 15px;
}
.upload-area .upload-hint {
font-size: 13px;
color: var(--gray-500);
margin-top: 4px;
}
.upload-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 8px;
}
.upload-status {
font-size: 14px;
color: var(--gray-500);
margin-left: 12px;
}
/* Results */
#results {
display: none;
}
#results.active {
display: block;
}
.result-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.result-metric {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: 12px;
padding: 24px;
text-align: center;
}
.result-metric .value {
font-size: 32px;
font-weight: 800;
color: var(--blue);
line-height: 1;
margin-bottom: 6px;
}
.result-metric .label {
font-size: 13px;
color: var(--gray-500);
line-height: 1.3;
}
.result-metric.warn .value { color: var(--red); }
.result-metric.good .value { color: var(--green); }
/* Charts */
.result-charts {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 32px;
}
.result-chart-card {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: 12px;
padding: 28px;
}
.result-chart-card h3 {
font-size: 17px;
font-weight: 700;
margin-bottom: 20px;
}
.result-chart-card.full {
grid-column: 1 / -1;
}
/* Bar chart list */
.bar-list-item {
margin-bottom: 14px;
}
.bar-list-header {
display: flex;
justify-content: space-between;
font-size: 14px;
margin-bottom: 4px;
}
.bar-list-header .name {
font-weight: 600;
}
.bar-list-header .count {
color: var(--gray-500);
}
.bar-list-track {
height: 8px;
background: var(--gray-100);
border-radius: 4px;
overflow: hidden;
}
.bar-list-fill {
height: 100%;
border-radius: 4px;
background: var(--blue);
transition: width 0.4s ease;
}
.bar-list-fill.warn { background: var(--red); }
.bar-list-fill.good { background: var(--green); }
/* Table */
.result-table-wrap {
overflow-x: auto;
margin-bottom: 32px;
}
.result-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
background: var(--white);
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--gray-200);
}
.result-table th {
background: var(--gray-50);
padding: 12px 16px;
text-align: left;
font-weight: 700;
font-size: 13px;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.3px;
white-space: nowrap;
}
.result-table td {
padding: 10px 16px;
border-top: 1px solid var(--gray-100);
}
.result-table tbody tr:hover {
background: var(--gray-50);
}
.section-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 32px;
margin-top: 40px;
}
.tab-btn {
padding: 10px 20px;
border: 1px solid var(--gray-200);
border-radius: 8px;
background: var(--white);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
color: var(--ink);
font-family: inherit;
}
.tab-btn:hover {
border-color: var(--blue);
color: var(--blue);
}
.tab-btn.active {
background: var(--blue);
color: var(--white);
border-color: var(--blue);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Conclusions */
.conclusion-box {
background: var(--blue-50);
border: 1px solid var(--blue-100);
border-radius: 12px;
padding: 24px;
margin-top: 32px;
}
.conclusion-box.warn-box {
background: var(--red-50);
border-color: #FECACA;
}
.conclusion-box h4 {
font-size: 16px;
font-weight: 700;
margin-bottom: 12px;
}
.conclusion-box p {
font-size: 15px;
color: var(--gray-600);
line-height: 1.7;
}
/* Form */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.form-grid .full {
grid-column: 1 / -1;
}
.form-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.form-group label {
font-size: 13px;
font-weight: 700;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.3px;
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 10px 14px;
border: 1px solid var(--gray-200);
border-radius: 8px;
font-size: 15px;
font-family: inherit;
color: var(--ink);
background: var(--white);
transition: border-color 0.15s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--blue);
box-shadow: 0 0 0 3px var(--blue-100);
}
.form-group textarea {
min-height: 72px;
resize: vertical;
}
.form-actions {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.form-msg {
font-size: 14px;
font-weight: 600;
padding: 4px 0;
}
.form-msg.ok { color: var(--green); }
.form-msg.err { color: var(--red); }
/* Period filter */
.period-filter {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 24px;
}
.period-filter select,
.period-filter input {
padding: 8px 14px;
border: 1px solid var(--gray-200);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
}
.period-filter label {
font-size: 13px;
font-weight: 700;
color: var(--gray-500);
}
/* Data actions */
.data-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.data-actions .btn-sm {
font-size: 13px;
padding: 8px 16px;
}
.btn-red {
background: var(--red);
color: var(--white);
}
.btn-red:hover { background: #B91C1C; }
.btn-amber {
background: var(--amber);
color: var(--white);
}
.btn-amber:hover { background: #B45309; }
/* Entry list */
.entry-card {
background: var(--white);
border: 1px solid var(--gray-200);
border-radius: 10px;
padding: 14px 18px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
transition: border-color 0.15s;
}
.entry-card:hover { border-color: var(--blue-100); }
.entry-card .entry-date {
font-weight: 700;
color: var(--blue);
white-space: nowrap;
min-width: 72px;
}
.entry-card .entry-cat {
background: var(--gray-100);
padding: 2px 10px;
border-radius: 100px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.entry-card .entry-desc {
flex: 1;
color: var(--gray-600);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.entry-card .entry-status {
font-weight: 700;
font-size: 12px;
padding: 3px 10px;
border-radius: 100px;
white-space: nowrap;
}
.entry-status.fixed { background: var(--green-50); color: var(--green); }
.entry-status.pending { background: var(--red-50); color: var(--red); }
.entry-card .entry-del {
background: none;
border: none;
color: var(--gray-500);
cursor: pointer;
font-size: 16px;
padding: 4px;
line-height: 1;
}
.entry-card .entry-del:hover { color: var(--red); }
.entries-wrap {
max-height: 400px;
overflow-y: auto;
margin-bottom: 16px;
}
.entries-info {
font-size: 13px;
color: var(--gray-500);
margin-bottom: 8px;
}
/* Toggle panels */
.toggle-panel {
display: none;
}
.toggle-panel.active {
display: block;
}
.panel-switch {
display: flex;
gap: 0;
margin-bottom: 32px;
border: 1px solid var(--gray-200);
border-radius: 10px;
overflow: hidden;
width: fit-content;
}
.panel-switch button {
padding: 12px 24px;
border: none;
background: var(--white);
font-size: 14px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
color: var(--ink);
transition: all 0.15s;
}
.panel-switch button:not(:last-child) {
border-right: 1px solid var(--gray-200);
}
.panel-switch button.active {
background: var(--blue);
color: var(--white);
}
.panel-switch button:hover:not(.active) {
background: var(--gray-50);
}
/* Mobile */
@media (max-width: 640px) {
.hero { padding: 64px 0 48px; }
.hero h1 { font-size: 32px; }
.hero p { font-size: 16px; }
.section { padding: 48px 0; }
.section h2 { font-size: 26px; }
.blocks-grid { grid-template-columns: 1fr; }
.dash-preview { padding: 24px; }
.dash-metric .value { font-size: 28px; }
.result-charts { grid-template-columns: 1fr; }
.form-grid { grid-template-columns: 1fr; }
.entry-card { flex-wrap: wrap; }
.entry-card .entry-desc { white-space: normal; }
}
</style>
</head>
<body>
<!-- Hero -->
<header class="hero">
<div class="container">
<span class="hero-badge">AI Analytics</span>
<h1>ИИ-агент аналитики производственной безопасности</h1>
<p>Автоматический сбор, анализ и визуализация результатов внутренних проверок. Контроль выполнения плана и формирование управленческих отчётов.</p>
<div class="hero-actions">
<a href="#analyzer" class="btn btn-primary">Загрузить данные</a>
<a href="#quick" class="btn btn-outline">Инструменты</a>
<a href="#blocks" class="btn btn-outline">Возможности</a>
</div>
</div>
</header>
<!-- Quick Links -->
<section id="quick" class="section quick-links">
<div class="container">
<span class="section-label">Инструменты</span>
<h2>Быстрый доступ</h2>
<p class="section-subtitle">Основные рабочие инструменты команды</p>
<div class="quick-grid">
<a href="#" class="quick-card">
<span class="quick-card-icon blue">📋</span>
<div>
<h3>Google Forms — проверки</h3>
<p>Заполнить форму по результатам проверки</p>
</div>
</a>
<a href="#" class="quick-card">
<span class="quick-card-icon green">📊</span>
<div>
<h3>Google Sheets — данные</h3>
<p>Просмотр и выгрузка всех записей проверок</p>
</div>
</a>
<a href="#" class="quick-card">
<span class="quick-card-icon green">📅</span>
<div>
<h3>План проверок</h3>
<p>Таблица плановых показателей по подразделениям</p>
</div>
</a>
<a href="#analyzer" class="quick-card">
<span class="quick-card-icon blue">🤖</span>
<div>
<h3>Анализатор CSV</h3>
<p>Загрузите выгрузку из Google Sheets — получите аналитику</p>
</div>
</a>
<a href="#" class="quick-card">
<span class="quick-card-icon green">📄</span>
<div>
<h3>Месячный отчёт</h3>
<p>Автоматическая аналитическая справка (PDF)</p>
</div>
</a>
<a href="#" class="quick-card">
<span class="quick-card-icon red">📖</span>
<div>
<h3>Инструкция</h3>
<p>Руководство по работе с системой</p>
</div>
</a>
</div>
</div>
</section>
<!-- Functional Blocks -->
<section id="blocks" class="section">
<div class="container">
<span class="section-label">Функционал</span>
<h2>6 аналитических блоков</h2>
<p class="section-subtitle">Что умеет ИИ-агент и какую информацию выдаёт автоматически</p>
<div class="blocks-grid">
<div class="block-card">
<span class="block-number">Блок №1</span>
<h3>Анализ нарушений</h3>
<ul>
<li>Общее количество выявленных нарушений</li>
<li>Количество по каждому виду нарушений</li>
<li>Доля каждого вида</li>
<li>Распределение по подразделениям и регионам</li>
<li>Рейтинг подразделений (лучшие / худшие)</li>
<li>Рейтинг видов нарушений</li>
</ul>
</div>
<div class="block-card">
<span class="block-number">Блок №2</span>
<h3>Анализ динамики</h3>
<ul>
<li>Сравнение: текущий месяц vs предыдущий</li>
<li>Сравнение с аналогичным периодом прошлого года</li>
<li>Процент роста / снижения</li>
<li>Тренды по каждому виду нарушений</li>
<li>Динамика по подразделениям</li>
</ul>
</div>
<div class="block-card">
<span class="block-number">Блок №3</span>
<h3>Проблемные зоны</h3>
<ul>
<li>Повторяющиеся нарушения</li>
<li>Подразделения с устойчиво высоким уровнем</li>
<li>Ухудшение показателей</li>
<li>Наиболее частые нарушения</li>
<li>Автоматические выводы — «проблемное направление X, оно даёт 27% нарушений»</li>
</ul>
</div>
<div class="block-card">
<span class="block-number">Блок №4</span>
<h3>Контроль плана проверок</h3>
<ul>
<li>Выполнение (%) = Факт / План × 100</li>
<li>Рейтинг подразделений по выполнению</li>
<li>Перечень с низким исполнением</li>
<li>Анализ по регионам</li>
<li>Перевыполнение / невыполнение</li>
</ul>
</div>
<div class="block-card">
<span class="block-number">Блок №5</span>
<h3>Аналитическая справка</h3>
<ul>
<li>Общая информация за месяц</li>
<li>Количество проверок и выполнение плана</li>
<li>ТОП-3 распространённых нарушений</li>
<li>Подразделения-лидеры и аутсайдеры</li>
<li>Тенденции месяца</li>
</ul>
</div>
<div class="block-card">
<span class="block-number">Блок №6</span>
<h3>Рекомендации</h3>
<ul>
<li>Внеплановые проверки по проблемным зонам</li>
<li>Целевые инструктажи</li>
<li>Дополнительное обучение</li>
<li>Выборочный аудит документации</li>
<li>Усиление контроля</li>
</ul>
</div>
</div>
</div>
</section>
<!-- ========================================= -->
<!-- INTERACTIVE TOOL -->
<!-- ========================================= -->
<section id="analyzer" class="section analyzer">
<div class="container">
<span class="section-label">Рабочий инструмент</span>
<h2>Ввод и анализ нарушений</h2>
<p class="section-subtitle">Вносите нарушения, выявленные в ходе проверок. Данные сохраняются в браузере. Всегда доступен дашборд с аналитикой по периодам.</p>
<!-- Panel switch -->
<div class="panel-switch" id="panelSwitch">
<button class="active" data-panel="panel-input">Ввод данных</button>
<button data-panel="panel-dashboard">Дашборд</button>
<button data-panel="panel-list">Все записи</button>
<button data-panel="panel-import">Импорт CSV</button>
</div>
<!-- PANEL 1: Input form -->
<div class="toggle-panel active" id="panel-input">
<div class="form-grid">
<div class="form-group">
<label></label>
<input type="number" id="fNum" placeholder="Авто">
</div>
<div class="form-group">
<label>Дата</label>
<input type="date" id="fDate" required>
</div>
<div class="form-group">
<label>№ указания</label>
<input type="text" id="fDirective" placeholder="Номер указания">
</div>
<div class="form-group">
<label>Филиал</label>
<select id="fFilial">
<option value="">Выберите...</option>
<option>ОДС</option><option>ДРБ</option><option>ДИТ</option>
<option>Сервисная фабрика</option><option>ДКБ</option>
<option>ДУП</option><option>ДТК</option><option>СФ</option>
<option>ЦТО СФ</option><option>ЦЭиК</option>
</select>
</div>
<div class="form-group">
<label>Область</label>
<select id="fRegion">
<option value="">Выберите...</option>
<option>Акмолинская</option><option>Астана</option>
<option>Карагандинская</option><option>Улытауская</option>
<option>Костанайская</option><option>Павлодарская</option>
<option>СКО</option>
</select>
</div>
<div class="form-group">
<label>Город / Район</label>
<input type="text" id="fCity" placeholder="Например: Шахтинск">
</div>
<div class="form-group">
<label>Объект с адресом</label>
<input type="text" id="fAddr" placeholder="Например: ул. Казахстанская 100а">
</div>
<div class="form-group">
<label>Категория нарушения</label>
<select id="fCat">
<option value="">Выберите...</option>
<option>основные док-ты по БиОТ</option>
<option>Пожарная безопасность</option>
<option>Электробезопасность</option>
<option>СИЗ</option>
<option>Транспортная безопасность</option>
<option>Промышленная безопасность</option>
<option>Санитарно-бытовые условия</option>
<option>организационные требования БиОТ</option>
<option>Производственная санитария</option>
<option>Наряды-допуски</option>
<option>Инструкция по проведению 4-х ступенчатого контроля</option>
<option>Не обеспечение СИЗ</option>
<option>Политика в области ТСС</option>
<option>Требовании по организации в управлении транспортных средств</option>
</select>
</div>
<div class="form-group full">
<label>Категория нарушения (описание)</label>
<textarea id="fDesc" placeholder="Подробное описание выявленного нарушения..."></textarea>
</div>
<div class="form-group full">
<label>Ссылка на законодательство</label>
<textarea id="fLaw" rows="2" placeholder="Например: пп.4, п.2 статьи 182 Трудового кодекса РК..."></textarea>
</div>
<div class="form-group">
<label>Срок исполнения</label>
<input type="text" id="fDeadline" placeholder="Например: 14.01.2026г.">
</div>
<div class="form-group">
<label>Исполнено</label>
<select id="fStatus">
<option value="на контроле">На контроле</option>
<option value="исполнено">Исполнено</option>
</select>
</div>
<div class="form-group full">
<label>Примечание</label>
<input type="text" id="fNote" placeholder="Примечание (необязательно)">
</div>
<div class="form-group">
<label>Кто выдал</label>
<select id="fInspector">
<option value="">Выберите...</option>
<option>Инженер по БиОТ Семидоцкий С.А.</option>
<option>Ведущий инженер ОБиОТ Туржанов А.Т.</option>
<option>Инженер ОБиОТ Бачинская Н.В.</option>
<option>Ведущий инженер ОБиОТ Ажакметов М.З.</option>
<option>Ведущий инженер ОБиОТ Баяхметова Ж.Т.</option>
<option>Инженер ОБиОТ Садыков М.Ф.</option>
<option>Ведущий инженер ОБиОТ Тумабаева С.А.</option>
<option>Инженер по БиОТ Джусупова А.Т.</option>
<option>Ведущий инженер ОБиОТ Федоськин А.Н.</option>
</select>
</div>
<div class="form-group">
<label>Кому выдано</label>
<input type="text" id="fIssuedTo" placeholder="Например: Начальник ЛТЦ">
</div>
</div>
<div class="form-actions">
<button class="btn btn-blue" id="btnAdd">Добавить запись</button>
<span class="form-msg" id="formMsg"></span>
</div>
</div>
<!-- PANEL 2: Dashboard -->
<div class="toggle-panel" id="panel-dashboard">
<div class="period-filter">
<label>Период:</label>
<select id="perPreset">
<option value="all">Всё время</option>
<option value="this-month">Текущий месяц</option>
<option value="prev-month">Прошлый месяц</option>
<option value="q1">1 квартал 2026</option>
<option value="q2">2 квартал 2026</option>
<option value="custom">Произвольный...</option>
</select>
<span id="perCustom" style="display:none">
с <input type="date" id="perFrom"> по <input type="date" id="perTo">
<button class="btn btn-blue btn-sm" id="btnPerApply">Применить</button>
</span>
</div>
<div class="result-grid" id="dashMetrics"></div>
<div id="dashCharts"></div>
<div id="dashConclusions"></div>
</div>
<!-- PANEL 3: All entries -->
<div class="toggle-panel" id="panel-list">
<div class="data-actions">
<button class="btn btn-blue btn-sm" id="btnExportCSV">Скачать CSV</button>
<button class="btn btn-red btn-sm" id="btnClear">Очистить все данные</button>
</div>
<div class="entries-info" id="entriesInfo"></div>
<div class="entries-wrap" id="entriesList"></div>
</div>
<!-- PANEL 4: Import CSV -->
<div class="toggle-panel" id="panel-import">
<div class="upload-area" id="dropArea">
<div class="upload-icon">📂</div>
<p><strong>Перетащите CSV-файл сюда</strong> или нажмите для выбора</p>
<p class="upload-hint">Данные добавятся к уже имеющимся записям</p>
<input type="file" id="fileInput" accept=".csv" style="display:none">
</div>
<div class="upload-actions">
<button class="btn btn-blue btn-sm" id="sampleBtn">Загрузить демо-данные</button>
<span class="upload-status" id="uploadStatus"></span>
</div>
</div>
</div>
</section>
<!-- How to work -->
<section id="steps" class="section">
<div class="container">
<span class="section-label">Процесс</span>
<h2>Как работать с системой</h2>
<p class="section-subtitle">4 шага от проверки до управленческого отчёта</p>
<div class="steps">
<div class="step">
<div class="step-num"></div>
<div class="step-text">
<h3>Заполните Google Forms после проверки</h3>
<p>Проверяющий вносит данные: дата, подразделение, регион, вид и количество нарушений, описание. Данные автоматически попадают в Google Sheets.</p>
</div>
</div>
<div class="step">
<div class="step-num"></div>
<div class="step-text">
<h3>Выгрузите CSV и загрузите в анализатор</h3>
<p>В Google Sheets: Файл → Скачать → CSV. Перетащите файл на эту страницу — анализатор сам разберёт данные.</p>
</div>
</div>
<div class="step">
<div class="step-num"></div>
<div class="step-text">
<h3>Смотрите аналитику</h3>
<p>Рейтинги категорий, подразделений и регионов. Графики, проблемные зоны, повторяющиеся нарушения — всё считается автоматически.</p>
</div>
</div>
<div class="step">
<div class="step-num"></div>
<div class="step-text">
<h3>Получайте выводы и рекомендации</h3>
<p>Анализатор формирует текстовые выводы с цифрами и конкретными предложениями по улучшению ситуации.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Categories -->
<section class="section" style="background: var(--gray-50);">
<div class="container">
<span class="section-label">Классификация</span>
<h2>9 категорий нарушений</h2>
<p class="section-subtitle">Анализатор автоматически распределяет нарушения по категориям</p>
<div class="cat-grid">
<span class="cat-tag"><span class="cat-dot d1"></span> Документация по БиОТ</span>
<span class="cat-tag"><span class="cat-dot d2"></span> Наряды-допуски</span>
<span class="cat-tag"><span class="cat-dot d3"></span> Пожарная безопасность</span>
<span class="cat-tag"><span class="cat-dot d4"></span> Транспортная безопасность</span>
<span class="cat-tag"><span class="cat-dot d5"></span> Промышленная безопасность</span>
<span class="cat-tag"><span class="cat-dot d6"></span> Санитарно-бытовые требования</span>
<span class="cat-tag"><span class="cat-dot d7"></span> Средства индивидуальной защиты</span>
<span class="cat-tag"><span class="cat-dot d8"></span> Обучение и проверка знаний</span>
<span class="cat-tag"><span class="cat-dot d9"></span> Электробезопасность</span>
</div>
</div>
</section>
<!-- CTA -->
<section class="cta">
<div class="container">
<h2>Начните использовать анализатор</h2>
<p>Выгрузите данные из Google Sheets в CSV — и получите полную аналитику за минуту. Все вопросы — в рабочий чат команды.</p>
<a href="#analyzer" class="btn">Загрузить данные</a>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p>ИИ-агент аналитики производственной безопасности &middot; Внутренний портал команды &middot; 2026</p>
</div>
</footer>
<!-- ========================================= -->
<!-- JAVASCRIPT: CSV Analyzer -->
<!-- ========================================= -->
<script>
(function() {
'use strict';
var CATEGORIES = [
'Документация по БиОТ', 'Наряды-допуски', 'Пожарная безопасность',
'Транспортная безопасность', 'Промышленная безопасность', 'Санитарно-бытовые требования',
'Средства индивидуальной защиты', 'Обучение и проверка знаний', 'Электробезопасность',
'Организационные требования БиОТ', 'Производственная санитария'
];
var STORAGE_KEY = 'prombez_violations';
var FILIALS = ['ОДС','ДРБ','ДИТ','Сервисная фабрика','ДКБ','ДУП','ДТК','СФ','ЦТО СФ','ЦЭиК'];
// ---- Data layer ----
function loadData() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; }
catch(e) { return []; }
}
function saveData(arr) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); }
catch(e) { showMsg('Нет места в хранилище браузера. Экспортируйте данные.', 'err'); }
}
function addEntry(entry) {
var data = loadData();
entry.id = Date.now() + Math.random();
entry.created = new Date().toISOString();
data.unshift(entry);
saveData(data);
}
function appendEntries(entries) {
var data = loadData();
entries.forEach(function(e) {
e.id = Date.now() + Math.random();
e.created = new Date().toISOString();
data.unshift(e);
});
saveData(data);
}
function deleteEntry(id) {
var data = loadData().filter(function(e) { return e.id !== id; });
saveData(data);
}
function clearAll() {
if (confirm('Удалить ВСЕ записи? Это действие нельзя отменить.')) {
saveData([]);
renderAll();
}
}
// ---- Category normalization ----
function normalizeCategory(raw) {
if (!raw) return 'Без категории';
var s = raw.toLowerCase().trim();
if (s.indexOf('сиз') !== -1) return 'Средства индивидуальной защиты';
if (s.indexOf('пожарн') !== -1 || s.indexOf('пожар') !== -1 || s.indexOf('өрт') !== -1) return 'Пожарная безопасность';
if (s.indexOf('электро') !== -1) return 'Электробезопасность';
if (s.indexOf('транспорт') !== -1 || s.indexOf('тсс') !== -1) return 'Транспортная безопасность';
if (s.indexOf('промышленн') !== -1 || s.indexOf('помышленн') !== -1 || s.indexOf('өнеркәсіп') !== -1) return 'Промышленная безопасность';
if (s.indexOf('наряд') !== -1 || s.indexOf('допуск') !== -1) return 'Наряды-допуски';
if (s.indexOf('санитар') !== -1 || s.indexOf('бытов') !== -1 || s.indexOf('гигиенич') !== -1 || s.indexOf('санитари') !== -1) return 'Санитарно-бытовые требования';
if (s.indexOf('обучен') !== -1 || s.indexOf('знаний') !== -1 || s.indexOf('оқыту') !== -1 || s.indexOf('нұсқау') !== -1) return 'Обучение и проверка знаний';
if (s.indexOf('4-х ступенчат') !== -1 || s.indexOf('четырехступенчат') !== -1 || s.indexOf('организационн') !== -1) return 'Организационные требования БиОТ';
if (s.indexOf('биот') !== -1 || s.indexOf('документаци') !== -1 || s.indexOf('док-ты') !== -1 || s.indexOf('инструктаж') !== -1 || s.indexOf('инструкци') !== -1) return 'Документация по БиОТ';
if (s.length > 3) return raw.trim();
return 'Без категории';
}
function isFixed(val) {
var s = (val || '').toLowerCase().trim();
return s === 'исполнено' || s === 'орындалды' || s === 'устранено' || s.indexOf('исполн') !== -1;
}
// ---- Period filter ----
function filterByPeriod(data) {
var preset = document.getElementById('perPreset').value;
var now = new Date();
var from, to;
if (preset === 'this-month') {
from = new Date(now.getFullYear(), now.getMonth(), 1);
to = new Date(now.getFullYear(), now.getMonth() + 1, 0);
} else if (preset === 'prev-month') {
from = new Date(now.getFullYear(), now.getMonth() - 1, 1);
to = new Date(now.getFullYear(), now.getMonth(), 0);
} else if (preset === 'q1') {
from = new Date(2026, 0, 1); to = new Date(2026, 2, 31);
} else if (preset === 'q2') {
from = new Date(2026, 3, 1); to = new Date(2026, 5, 30);
} else if (preset === 'custom') {
from = document.getElementById('perFrom').value ? new Date(document.getElementById('perFrom').value) : null;
to = document.getElementById('perTo').value ? new Date(document.getElementById('perTo').value) : null;
}
if (preset === 'all' || (!from && !to)) return data;
return data.filter(function(r) {
var d = parseDate(r.date);
if (!d) return true;
if (from && d < from) return false;
if (to) { to.setHours(23,59,59,999); if (d > to) return false; }
return true;
});
}
function parseDate(str) {
if (!str) return null;
str = String(str).trim();
var parts = str.split(/[.\-\/]/);
if (parts.length === 3) {
var d = parseInt(parts[0]), m = parseInt(parts[1]), y = parseInt(parts[2]);
if (y < 100) y += 2000;
if (parts[2].length === 4) { d = parseInt(parts[0]); m = parseInt(parts[1]); }
else if (parts[0].length === 4) { y = parseInt(parts[0]); m = parseInt(parts[1]); d = parseInt(parts[2]); }
return new Date(y, m - 1, d);
}
return null;
}
// ---- Dashboard ----
function renderDashboard() {
var data = loadData();
var filtered = filterByPeriod(data);
var total = filtered.length;
if (total === 0) {
document.getElementById('dashMetrics').innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--gray-500)">Нет данных за выбранный период</div>';
document.getElementById('dashCharts').innerHTML = '';
document.getElementById('dashConclusions').innerHTML = '';
return;
}
var fixed = filtered.filter(function(r) { return isFixed(r.status); }).length;
var pending = total - fixed;
var catMap = {};
var deptMap = {};
var regionMap = {};
var inspMap = {};
var monthlyMap = {};
filtered.forEach(function(r) {
var cat = normalizeCategory(r.category || r.cat || '');
if (!catMap[cat]) catMap[cat] = 0; catMap[cat]++;
var dept = r.filial || r.dept || 'Не указано';
if (!deptMap[dept]) deptMap[dept] = 0; deptMap[dept]++;
var reg = r.region || r.oblast || 'Не указано';
if (!regionMap[reg]) regionMap[reg] = 0; regionMap[reg]++;
var insp = r.inspector || r.issuedBy || 'Не указано';
if (!inspMap[insp]) inspMap[insp] = 0; inspMap[insp]++;
var d = parseDate(r.date);
if (d) {
var key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
if (!monthlyMap[key]) monthlyMap[key] = 0; monthlyMap[key]++;
}
});
var catSorted = Object.entries(catMap).sort(function(a,b) { return b[1] - a[1]; });
var deptSorted = Object.entries(deptMap).sort(function(a,b) { return b[1] - a[1]; });
var regionSorted = Object.entries(regionMap).sort(function(a,b) { return b[1] - a[1]; });
var inspSorted = Object.entries(inspMap).sort(function(a,b) { return b[1] - a[1]; });
var monthlySorted = Object.entries(monthlyMap).sort();
// Metrics
var topCat = catSorted[0];
var htmlM = '';
htmlM += buildMetric(total, 'Всего записей');
htmlM += buildMetric(fixed, 'Устранено', 'good');
htmlM += buildMetric(pending, 'На контроле', pending > fixed ? 'warn' : '');
htmlM += buildMetric(Object.keys(catMap).length, 'Категорий');
if (topCat) {
htmlM += buildMetric(Math.round(topCat[1]/total*100) + '%', 'Доля «'+topCat[0]+'»', topCat[1]/total>0.25?'warn':'');
}
document.getElementById('dashMetrics').innerHTML = htmlM;
// Charts
var htmlC = '<div class="result-charts">';
htmlC += renderBarChart('По категориям', catSorted, total, 'dash-cat');
htmlC += renderBarChart('По филиалам', deptSorted, total, 'dash-dept');
htmlC += renderBarChart('По областям', regionSorted, total, 'dash-reg');
htmlC += renderBarChart('По проверяющим', inspSorted, total, 'dash-insp');
if (monthlySorted.length > 0) {
htmlC += renderBarChart('Динамика по месяцам', monthlySorted, total, 'dash-mon');
}
htmlC += '</div>';
document.getElementById('dashCharts').innerHTML = htmlC;
// Conclusions
var htmlCon = '';
var worstDept = deptSorted[deptSorted.length - 1];
if (topCat && topCat[1] / total > 0.2) {
htmlCon += '<div class="conclusion-box warn-box"><h4>Проблемная зона</h4><p>Наиболее частой категорией является <strong>' +
escHtml(topCat[0]).toLowerCase() + '</strong> — доля <strong>' +
Math.round(topCat[1]/total*100) + '%</strong> (' + topCat[1] + ' из ' + total + ' записей).</p></div>';
}
htmlCon += '<div class="conclusion-box"><h4>Общие выводы</h4><p>Всего <strong>' + total +
'</strong> записей. Устранено: <strong>' + fixed + '</strong> (' + Math.round(fixed/total*100) + '%). ' +
'На контроле: <strong>' + pending + '</strong>. ' +
'ТОП-3 категории: ' + catSorted.slice(0,3).map(function(c,i) {
return (i+1)+') '+escHtml(c[0])+' — '+c[1]+' ('+Math.round(c[1]/total*100)+'%)';
}).join('; ') + '.</p></div>';
if (pending > fixed) {
htmlCon += '<div class="conclusion-box"><h4>Рекомендации</h4><p>На контроле <strong>' + pending +
'</strong> нарушений. Усилить контроль устранения. ';
if (topCat) {
var c = topCat[0];
if (c === 'Средства индивидуальной защиты') htmlCon += 'По СИЗ: внеплановые проверки и целевой инструктаж.';
else if (c === 'Наряды-допуски') htmlCon += 'По нарядам-допускам: доп. обучение и аудит документации.';
else if (c === 'Электробезопасность') htmlCon += 'По электробезопасности: усилить контроль.';
else if (c === 'Пожарная безопасность') htmlCon += 'По пожарной безопасности: проверить инструктажи.';
else htmlCon += 'По «'+escHtml(c)+'»: внеплановая проверка.';
}
htmlCon += '</p></div>';
}
document.getElementById('dashConclusions').innerHTML = htmlCon;
}
function renderBarChart(title, items, total, id) {
var html = '<div class="result-chart-card"><h3>' + title + '</h3>';
var shown = items.slice(0, 12);
var maxVal = shown.length > 0 ? shown[0][1] : 1;
shown.forEach(function(item, idx) {
var pct = total > 0 ? Math.round(item[1]/total*100) : 0;
var barPct = maxVal > 0 ? Math.round(item[1]/maxVal*100) : 0;
html += '<div class="bar-list-item"><div class="bar-list-header"><span class="name">' +
escHtml(item[0]) + '</span><span class="count">' + item[1] + ' (' + pct + '%)</span></div>' +
'<div class="bar-list-track"><div class="bar-list-fill" style="width:' + barPct + '%"></div></div></div>';
});
html += '</div>';
return html;
}
function buildMetric(value, label, cls) {
return '<div class="result-metric' + (cls ? ' ' + cls : '') + '"><div class="value">' +
value + '</div><div class="label">' + label + '</div></div>';
}
// ---- Entry list ----
function renderEntryList() {
var data = loadData();
document.getElementById('entriesInfo').textContent = 'Всего записей: ' + data.length;
var html = '';
data.slice(0, 200).forEach(function(r, i) {
var fixed = isFixed(r.status);
html += '<div class="entry-card">' +
'<span class="entry-date">' + escHtml(r.date || '—') + '</span>' +
'<span class="entry-cat">' + escHtml(normalizeCategory(r.cat || r.category || '')) + '</span>' +
'<span class="entry-desc">' + escHtml((r.desc || r.description || '').slice(0, 80)) + '</span>' +
'<span class="entry-status ' + (fixed ? 'fixed' : 'pending') + '">' + (fixed ? 'Исполнено' : 'На контроле') + '</span>' +
'<button class="entry-del" data-id="' + r.id + '" title="Удалить">&times;</button>' +
'</div>';
});
if (data.length === 0) html = '<p style="color:var(--gray-500);text-align:center;padding:40px">Нет записей. Добавьте через вкладку «Ввод данных».</p>';
if (data.length > 200) html += '<p style="font-size:13px;color:var(--gray-500);text-align:center">Показаны последние 200 из ' + data.length + '</p>';
document.getElementById('entriesList').innerHTML = html;
document.querySelectorAll('.entry-del').forEach(function(btn) {
btn.addEventListener('click', function() {
deleteEntry(parseFloat(btn.getAttribute('data-id')));
renderAll();
});
});
}
// ---- Form handling ----
function submitForm() {
var entry = {
num: document.getElementById('fNum').value,
date: document.getElementById('fDate').value,
directive: document.getElementById('fDirective').value,
filial: document.getElementById('fFilial').value,
region: document.getElementById('fRegion').value,
city: document.getElementById('fCity').value,
addr: document.getElementById('fAddr').value,
cat: document.getElementById('fCat').value,
desc: document.getElementById('fDesc').value,
law: document.getElementById('fLaw').value,
deadline: document.getElementById('fDeadline').value,
status: document.getElementById('fStatus').value,
note: document.getElementById('fNote').value,
inspector: document.getElementById('fInspector').value,
issuedTo: document.getElementById('fIssuedTo').value
};
if (!entry.date || !entry.filial || !entry.cat || !entry.desc) {
showMsg('Заполните обязательные поля: Дата, Филиал, Категория, Описание', 'err');
return;
}
addEntry(entry);
showMsg('Запись добавлена!', 'ok');
// Clear text fields, keep dropdowns
document.getElementById('fNum').value = '';
document.getElementById('fDate').value = '';
document.getElementById('fDirective').value = '';
document.getElementById('fDesc').value = '';
document.getElementById('fLaw').value = '';
document.getElementById('fDeadline').value = '';
document.getElementById('fNote').value = '';
document.getElementById('fIssuedTo').value = '';
renderAll();
setTimeout(function() { showMsg('', ''); }, 2500);
}
function showMsg(text, cls) {
var el = document.getElementById('formMsg');
el.textContent = text;
el.className = 'form-msg ' + cls;
}
// ---- CSV Export ----
function exportCSV() {
var data = loadData();
if (data.length === 0) { alert('Нет данных для экспорта.'); return; }
var header = '№,Дата,№ указания,Филиал,Область,Город/Район,Объект с адресом,Категория нарушения,Категория нарушения,Ссылка на законодательство,Срок исполнения,исполнено,Примечание,кто выдал,кому выдано';
var lines = [header];
data.forEach(function(r) {
lines.push([
r.num || '', r.date || '', r.directive || '', r.filial || '', r.region || '',
r.city || '', r.addr || '', r.cat || r.category || '', r.desc || r.description || '',
r.law || '', r.deadline || '', r.status || '', r.note || '',
r.inspector || r.issuedBy || '', r.issuedTo || ''
].map(function(v) { return '"' + String(v).replace(/"/g, '""') + '"'; }).join(','));
});
var blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/csv;charset=utf-8' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'violations_export_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
}
// ---- CSV Import ----
function parseCSV(text) {
var rows = []; var row = []; var cell = ''; var quoted = false;
for (var i = 0; i < text.length; i++) {
var ch = text[i];
if (quoted) {
if (ch === '"') {
if (i+1 < text.length && text[i+1] === '"') { cell += '"'; i++; }
else quoted = false;
} else cell += ch;
} else {
if (ch === '"') quoted = true;
else if (ch === ',' || ch === ';' || ch === '\t') { row.push(cell.trim()); cell = ''; }
else if (ch === '\n' || ch === '\r') {
if (ch === '\r' && i+1 < text.length && text[i+1] === '\n') i++;
row.push(cell.trim());
if (row.length > 0 && (row.length > 1 || row[0])) rows.push(row);
row = []; cell = '';
} else cell += ch;
}
}
row.push(cell.trim());
if (row.length > 0 && (row.length > 1 || row[0])) rows.push(row);
return rows;
}
function importCSV(rows) {
if (rows.length < 2) return 0;
var headers = rows[0].map(function(h) { return h.toLowerCase().trim(); });
var data = rows.slice(1);
function idx(keys) {
for (var i = 0; i < headers.length; i++)
for (var k = 0; k < keys.length; k++)
if (headers[i].indexOf(keys[k]) !== -1) return i;
return -1;
}
var ciNum = idx(['№']);
var ciDate = idx(['дата']);
var ciDirective = idx(['указания','№ указания']);
var ciFilial = idx(['филиал']);
var ciRegion = idx(['область','регион']);
var ciCity = idx(['город','район','населен']);
var ciAddr = idx(['объект','адрес']);
var ciCat = idx(['категор','нарушен']);
// Second "Категория нарушения" = description
var ciDesc = -1;
for (var hi = 0; hi < headers.length; hi++) {
if (headers[hi].indexOf('категор') !== -1 && hi !== ciCat) { ciDesc = hi; break; }
if (headers[hi].indexOf('описан') !== -1) { ciDesc = hi; break; }
}
if (ciDesc < 0 && ciCat >= 0) ciDesc = ciCat + 1;
var ciLaw = idx(['законодательств','ссылка','основание']);
var ciDeadline = idx(['срок']);
var ciStatus = idx(['исполнен','статус','устран']);
var ciNote = idx(['примечан']);
var ciInspector = idx(['выдал','кто выдал']);
var ciIssued = idx(['кому','выдано']);
var entries = [];
data.forEach(function(r) {
if (r.length < 2) return;
entries.push({
num: ciNum >= 0 ? r[ciNum] || '' : '',
date: ciDate >= 0 ? r[ciDate] || '' : '',
directive: ciDirective >= 0 ? r[ciDirective] || '' : '',
filial: ciFilial >= 0 ? r[ciFilial] || '' : '',
region: ciRegion >= 0 ? r[ciRegion] || '' : '',
city: ciCity >= 0 ? r[ciCity] || '' : '',
addr: ciAddr >= 0 ? r[ciAddr] || '' : '',
cat: ciCat >= 0 ? r[ciCat] || '' : '',
desc: ciDesc >= 0 && ciDesc < r.length ? r[ciDesc] || '' : '',
law: ciLaw >= 0 ? r[ciLaw] || '' : '',
deadline: ciDeadline >= 0 ? r[ciDeadline] || '' : '',
status: ciStatus >= 0 ? r[ciStatus] || '' : '',
note: ciNote >= 0 ? r[ciNote] || '' : '',
inspector: ciInspector >= 0 ? r[ciInspector] || '' : '',
issuedTo: ciIssued >= 0 ? r[ciIssued] || '' : ''
});
});
if (entries.length > 0) {
appendEntries(entries);
}
return entries.length;
}
function processCSVFile(file) {
document.getElementById('uploadStatus').textContent = 'Обработка...';
var reader = new FileReader();
reader.onload = function(e) {
try {
var text = e.target.result;
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
var rows = parseCSV(text);
var count = importCSV(rows);
if (count === 0) {
document.getElementById('uploadStatus').textContent = 'Не удалось распознать данные.';
} else {
document.getElementById('uploadStatus').textContent = 'Импортировано: ' + count + ' записей из «' + file.name + '»';
renderAll();
}
} catch(err) {
document.getElementById('uploadStatus').textContent = 'Ошибка: ' + err.message;
}
};
reader.readAsText(file, 'UTF-8');
}
function generateSampleCSV() {
var header = '№,Дата,№ указания,Филиал,Область,Город/Район,Объект с адресом,Категория нарушения,Категория нарушения,Ссылка на законодательство,Срок исполнения,исполнено,Примечание,кто выдал,кому выдано';
var filials = ['ОДС','ДРБ','ДИТ','Сервисная фабрика','ДКБ','ДУП','ЦТО СФ','ЦЭиК'];
var oblasts = ['Акмолинская','Карагандинская','Астана','Улытауская'];
var cities = ['Шахтинск','Астана','Караганда','Темиртау','Щучинск','Атбасар','Балхаш','Кокшетау'];
var cats = ['основные док-ты по БиОТ','Пожарная безопасность','Электробезопасность','СИЗ','Транспортная безопасность','Промышленная безопасность','Санитарно-бытовые условия','организационные требования БиОТ'];
var descs = [
'Отсутствует журнал инструктажа на рабочем месте',
'Не проведён повторный инструктаж за 1 квартал 2026',
'Работники не ознакомлены с регламентом обеспечения СИЗ',
'Форма журнала наряд-допусков не соответствует правилам',
'Просроченные диэлектрические перчатки',
'Неактуальные инструкции по ТБ по видам работ',
'Отсутствует аптечка для оказания первой помощи',
'Пожарный кран не проверялся на работоспособность',
'Работники не применяли СИЗ (без касок, пояса)',
'Прошёл срок поверки инструмента',
'Аварийный выход заблокирован',
'На территории скопление снега, требуется вывоз'
];
var laws = [
'пп.4, п.2 статьи 182 Трудового кодекса РК',
'Приказ №55 МЧС РК от 21.02.2022',
'Приказ Министра энергетики РК от 19.03.2015 №222',
'Распоряжение №253 от 31.10.2025',
'Распоряжение №271 от 19.11.2025',
'Приказ №262 от 14.11.2018'
];
var inspectors = [
'Инженер по БиОТ Семидоцкий С.А.',
'Ведущий инженер ОБиОТ Туржанов А.Т.',
'Инженер ОБиОТ Бачинская Н.В.',
'Ведущий инженер ОБиОТ Ажакметов М.З.',
'Ведущий инженер ОБиОТ Баяхметова Ж.Т.',
'Инженер ОБиОТ Садыков М.Ф.'
];
var statuses = ['исполнено','исполнено','исполнено','на контроле','на контроле','исполнено','на контроле'];
var months = ['01','02','03','04','05','06'];
var lines = [header];
for (var i = 0; i < 200; i++) {
var d = String(Math.floor(Math.random()*28)+1).padStart(2,'0') + '.' + months[Math.floor(Math.random()*months.length)] + '.2026';
lines.push([
i+1, d, Math.floor(Math.random()*20)+1,
filials[Math.floor(Math.random()*filials.length)],
oblasts[Math.floor(Math.random()*oblasts.length)],
cities[Math.floor(Math.random()*cities.length)],
'ул. Примерная ' + (Math.floor(Math.random()*50)+1),
cats[Math.floor(Math.random()*cats.length)],
descs[Math.floor(Math.random()*descs.length)],
laws[Math.floor(Math.random()*laws.length)],
d, statuses[Math.floor(Math.random()*statuses.length)],
'', inspectors[Math.floor(Math.random()*inspectors.length)], ''
].join(','));
}
return lines.join('\n');
}
// ---- Panel switching ----
function switchPanel(name) {
document.querySelectorAll('.toggle-panel').forEach(function(p) { p.classList.remove('active'); });
document.querySelectorAll('.panel-switch button').forEach(function(b) { b.classList.remove('active'); });
var panel = document.getElementById(name);
if (panel) panel.classList.add('active');
var btn = document.querySelector('[data-panel="' + name + '"]');
if (btn) btn.classList.add('active');
if (name === 'panel-dashboard') renderDashboard();
if (name === 'panel-list') renderEntryList();
}
function renderAll() {
renderDashboard();
renderEntryList();
}
// ---- ESC helper ----
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ---- Init ----
function init() {
// Panel switch clicks
document.querySelectorAll('.panel-switch button').forEach(function(btn) {
btn.addEventListener('click', function() {
switchPanel(btn.getAttribute('data-panel'));
});
});
// Form submit
document.getElementById('btnAdd').addEventListener('click', submitForm);
// Period filter
document.getElementById('perPreset').addEventListener('change', function() {
document.getElementById('perCustom').style.display = this.value === 'custom' ? 'inline' : 'none';
renderDashboard();
});
document.getElementById('btnPerApply').addEventListener('click', renderDashboard);
// Export / Clear
document.getElementById('btnExportCSV').addEventListener('click', exportCSV);
document.getElementById('btnClear').addEventListener('click', clearAll);
// File drop / import
var dropArea = document.getElementById('dropArea');
var fileInput = document.getElementById('fileInput');
dropArea.addEventListener('click', function() { fileInput.click(); });
fileInput.addEventListener('change', function() {
if (fileInput.files.length > 0) processCSVFile(fileInput.files[0]);
});
dropArea.addEventListener('dragover', function(e) { e.preventDefault(); dropArea.classList.add('drag-over'); });
dropArea.addEventListener('dragleave', function() { dropArea.classList.remove('drag-over'); });
dropArea.addEventListener('drop', function(e) {
e.preventDefault(); dropArea.classList.remove('drag-over');
if (e.dataTransfer.files.length > 0) processCSVFile(e.dataTransfer.files[0]);
});
// Sample data
document.getElementById('sampleBtn').addEventListener('click', function() {
document.getElementById('uploadStatus').textContent = 'Загрузка демо-данных...';
var blob = new Blob(['\uFEFF' + generateSampleCSV()], { type: 'text/csv;charset=utf-8' });
processCSVFile(new File([blob], 'demo.csv', { type: 'text/csv' }));
});
// Initial render
renderDashboard();
renderEntryList();
}
init();
})();
</script>
</body>
</html>