Анализатор обновлён под реальные данные Google Sheets

This commit is contained in:
Dauren777 2026-06-03 12:30:42 +00:00
parent a0c5504471
commit d7a34a46ed

View File

@ -89,6 +89,9 @@ body {
font-size: 15px;
text-decoration: none;
transition: transform 0.15s, box-shadow 0.15s;
cursor: pointer;
border: none;
font-family: inherit;
}
.btn:hover {
@ -111,6 +114,21 @@ body {
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;
@ -445,6 +463,267 @@ body {
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;
}
/* Mobile */
@media (max-width: 640px) {
.hero { padding: 64px 0 48px; }
@ -455,6 +734,7 @@ body {
.blocks-grid { grid-template-columns: 1fr; }
.dash-preview { padding: 24px; }
.dash-metric .value { font-size: 28px; }
.result-charts { grid-template-columns: 1fr; }
}
</style>
</head>
@ -467,9 +747,9 @@ body {
<h1>ИИ-агент аналитики производственной безопасности</h1>
<p>Автоматический сбор, анализ и визуализация результатов внутренних проверок. Контроль выполнения плана и формирование управленческих отчётов.</p>
<div class="hero-actions">
<a href="#quick" class="btn btn-primary">Инструменты</a>
<a href="#analyzer" class="btn btn-primary">Загрузить данные</a>
<a href="#quick" class="btn btn-outline">Инструменты</a>
<a href="#blocks" class="btn btn-outline">Возможности</a>
<a href="#steps" class="btn btn-outline">Как работать</a>
</div>
</div>
</header>
@ -502,11 +782,11 @@ body {
<p>Таблица плановых показателей по подразделениям</p>
</div>
</a>
<a href="#" class="quick-card">
<a href="#analyzer" class="quick-card">
<span class="quick-card-icon blue">🤖</span>
<div>
<h3>Дашборд ИИ-агента</h3>
<p>Интерактивная аналитика и визуализация</p>
<h3>Анализатор CSV</h3>
<p>Загрузите выгрузку из Google Sheets — получите аналитику</p>
</div>
</a>
<a href="#" class="quick-card">
@ -605,8 +885,59 @@ body {
</div>
</section>
<!-- ========================================= -->
<!-- ANALYZER -->
<!-- ========================================= -->
<section id="analyzer" class="section analyzer">
<div class="container">
<span class="section-label">Анализатор</span>
<h2>Загрузите данные из Google Sheets</h2>
<p class="section-subtitle">Экспортируйте таблицу в CSV (Файл → Скачать → CSV) и загрузите сюда. Анализатор сам определит категории, построит рейтинги и покажет выводы.</p>
<div class="upload-area" id="dropArea">
<div class="upload-icon">📂</div>
<p><strong>Перетащите CSV-файл сюда</strong> или нажмите для выбора</p>
<p class="upload-hint">Поддерживаются файлы .csv из Google Sheets (кодировка UTF-8)</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>
<!-- Results -->
<div id="results">
<div class="sort-info" style="font-size:14px;color:var(--gray-500);margin-bottom:24px" id="dataInfo"></div>
<!-- Key Metrics -->
<div class="result-grid" id="metricsGrid"></div>
<!-- Tabs -->
<div class="section-tabs">
<button class="tab-btn active" data-tab="tab-categories">По категориям</button>
<button class="tab-btn" data-tab="tab-divisions">По подразделениям</button>
<button class="tab-btn" data-tab="tab-regions">По регионам</button>
<button class="tab-btn" data-tab="tab-inspectors">По проверяющим</button>
<button class="tab-btn" data-tab="tab-repeat">Повторяющиеся</button>
<button class="tab-btn" data-tab="tab-table">Все записи</button>
</div>
<div class="tab-content active" id="tab-categories"></div>
<div class="tab-content" id="tab-divisions"></div>
<div class="tab-content" id="tab-regions"></div>
<div class="tab-content" id="tab-inspectors"></div>
<div class="tab-content" id="tab-repeat"></div>
<div class="tab-content" id="tab-table"></div>
<!-- Auto conclusion -->
<div id="conclusions"></div>
</div>
</div>
</section>
<!-- Dashboard Preview -->
<section class="section" style="background: var(--gray-50);">
<section id="dashboard-preview" class="section" style="background: var(--gray-50);">
<div class="container">
<span class="section-label">Дашборд</span>
<h2>Визуализация данных</h2>
@ -677,22 +1008,22 @@ body {
<div class="step">
<div class="step-num"></div>
<div class="step-text">
<h3>ИИ-агент обрабатывает данные</h3>
<p>Автоматический сбор новых записей, классификация нарушений по 9 категориям, расчёт выполнения плана проверок.</p>
<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>
<h3>Смотрите аналитику</h3>
<p>Рейтинги категорий, подразделений и регионов. Графики, проблемные зоны, повторяющиеся нарушения — всё считается автоматически.</p>
</div>
</div>
<div class="step">
<div class="step-num"></div>
<div class="step-text">
<h3>Получайте готовый отчёт и рекомендации</h3>
<p>1-го числа каждого месяца — автоматическая аналитическая справка с выводами и рекомендациями. Отчёт в PDF, Excel и PowerPoint.</p>
<h3>Получайте выводы и рекомендации</h3>
<p>Анализатор формирует текстовые выводы с цифрами и конкретными предложениями по улучшению ситуации.</p>
</div>
</div>
</div>
@ -704,7 +1035,7 @@ body {
<div class="container">
<span class="section-label">Классификация</span>
<h2>9 категорий нарушений</h2>
<p class="section-subtitle">ИИ-агент автоматически распределяет нарушения по категориям</p>
<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>
@ -722,9 +1053,9 @@ body {
<!-- CTA -->
<section class="cta">
<div class="container">
<h2>Начните использовать ИИ-агента</h2>
<p>Подключите Google Forms к системе и получайте аналитику автоматически. Все вопросы — в рабочий чат команды.</p>
<a href="#quick" class="btn">Перейти к инструментам</a>
<h2>Начните использовать анализатор</h2>
<p>Выгрузите данные из Google Sheets в CSV — и получите полную аналитику за минуту. Все вопросы — в рабочий чат команды.</p>
<a href="#analyzer" class="btn">Загрузить данные</a>
</div>
</section>
@ -735,5 +1066,525 @@ body {
</div>
</footer>
<!-- ========================================= -->
<!-- JAVASCRIPT: CSV Analyzer -->
<!-- ========================================= -->
<script>
(function() {
'use strict';
var CATEGORIES = [
'Документация по БиОТ',
'Наряды-допуски',
'Пожарная безопасность',
'Транспортная безопасность',
'Промышленная безопасность',
'Санитарно-бытовые требования',
'Средства индивидуальной защиты',
'Обучение и проверка знаний',
'Электробезопасность',
'Организационные требования БиОТ',
'Производственная санитария'
];
var CAT_SHORT = {
'Документация по БиОТ': 'Документация БиОТ',
'Наряды-допуски': 'Наряды-допуски',
'Пожарная безопасность': 'Пожарная безопасность',
'Транспортная безопасность': 'Транспортная безопасность',
'Промышленная безопасность': 'Промышленная безопасность',
'Санитарно-бытовые требования': 'Санитарно-бытовые',
'Средства индивидуальной защиты': 'СИЗ',
'Обучение и проверка знаний': 'Обучение',
'Электробезопасность': 'Электробезопасность',
'Организационные требования БиОТ': 'Орг. требования',
'Производственная санитария': 'Произв. санитария',
'Без категории': 'Без категории'
};
function findColumn(headers, keywords) {
var h = headers.map(function(hdr) { return hdr.toLowerCase().trim(); });
for (var i = 0; i < h.length; i++) {
for (var k = 0; k < keywords.length; k++) {
if (h[i].indexOf(keywords[k].toLowerCase()) !== -1) return i;
}
}
return -1;
}
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 normalizeCategory(raw) {
if (!raw) return '';
var s = raw.toLowerCase().trim();
if (s === 'сиз' || s === 'не обеспечение сиз' || 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) 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) return 'Документация по БиОТ';
if (s.indexOf('инструктаж') !== -1 || s.indexOf('инструкци') !== -1) return 'Документация по БиОТ';
if (s.length > 3) return raw.trim();
return '';
}
function isFixedStatus(val) {
var s = (val || '').toLowerCase().trim();
if (s === 'исполнено' || s === 'орындалды' || s === 'устранено' || s === 'выполнено') return true;
if (s.indexOf('исполн') !== -1) return true;
return false;
}
function analyzeData(rows) {
if (rows.length < 2) return null;
var headers = rows[0];
var data = rows.slice(1);
var colDate = findColumn(headers, ['дата']);
var colDept = findColumn(headers, ['филиал','подразделение','структур']);
var colRegion = findColumn(headers, ['область','регион']);
var colCity = findColumn(headers, ['город','район','населен']);
var colAddr = findColumn(headers, ['объект','адрес']);
var colCategory = findColumn(headers, ['категория','нарушен']);
var colInspector = findColumn(headers, ['выдал','кто выдал','проверяющий']);
var colStatus = findColumn(headers, ['исполнено','статус','устран']);
var colDeadline = findColumn(headers, ['срок']);
var colDescCat2 = -1;
// Second category column detection: find second match
for (var hi = 0; hi < headers.length; hi++) {
var hdr = headers[hi].toLowerCase().trim();
if (hdr.indexOf('категори') !== -1 && hi !== colCategory) { colDescCat2 = hi; break; }
if (hdr.indexOf('описан') !== -1 || hdr.indexOf('суть') !== -1) { colDescCat2 = hi; break; }
}
var colDesc = colDescCat2 >= 0 ? colDescCat2 : (colCategory >= 0 ? colCategory + 1 : -1);
var records = [];
for (var i = 0; i < data.length; i++) {
var r = data[i];
if (r.length < 2) continue;
var catRaw = colCategory >= 0 ? (r[colCategory] || '') : '';
var catNorm = normalizeCategory(catRaw);
var descRaw = colDesc >= 0 && colDesc < r.length ? (r[colDesc] || '') : '';
if (!catNorm && descRaw) catNorm = normalizeCategory(descRaw);
if (!catNorm && catRaw) catNorm = catRaw;
var statusRaw = colStatus >= 0 ? (r[colStatus] || '') : '';
var fixed = isFixedStatus(statusRaw);
records.push({
date: colDate >= 0 ? (r[colDate] || '') : '',
dept: colDept >= 0 ? (r[colDept] || 'Не указано') : 'Не указано',
region: colRegion >= 0 ? (r[colRegion] || 'Не указано') : 'Не указано',
city: colCity >= 0 ? (r[colCity] || '') : '',
addr: colAddr >= 0 ? (r[colAddr] || '') : '',
inspector: colInspector >= 0 ? (r[colInspector] || 'Не указано') : 'Не указано',
count: 1,
category: catNorm || 'Без категории',
desc: descRaw,
status: statusRaw,
fixed: fixed,
deadline: colDeadline >= 0 ? (r[colDeadline] || '') : '',
raw: r
});
}
if (records.length === 0) return null;
var totalRecords = records.length;
var totalFixed = records.filter(function(r) { return r.fixed; }).length;
var totalPending = totalRecords - totalFixed;
var catStats = {};
records.forEach(function(r) {
if (!catStats[r.category]) catStats[r.category] = 0;
catStats[r.category] += r.count;
});
var catSorted = Object.entries(catStats).sort(function(a, b) { return b[1] - a[1]; });
var deptStats = {};
records.forEach(function(r) {
if (!deptStats[r.dept]) deptStats[r.dept] = 0;
deptStats[r.dept] += r.count;
});
var deptSorted = Object.entries(deptStats).sort(function(a, b) { return b[1] - a[1]; });
var regionStats = {};
records.forEach(function(r) {
if (!regionStats[r.region]) regionStats[r.region] = 0;
regionStats[r.region] += r.count;
});
var regionSorted = Object.entries(regionStats).sort(function(a, b) { return b[1] - a[1]; });
var inspStats = {};
records.forEach(function(r) {
if (!inspStats[r.inspector]) inspStats[r.inspector] = 0;
inspStats[r.inspector] += r.count;
});
var inspSorted = Object.entries(inspStats).sort(function(a, b) { return b[1] - a[1]; });
var descStats = {};
records.forEach(function(r) {
var key = (r.desc || r.category || '').trim().toLowerCase();
if (!key || key.length < 3) key = r.category.toLowerCase() + '_' + (Math.random() + '').slice(2, 6);
if (!descStats[key]) descStats[key] = { count: 0, desc: r.desc || r.category, cat: r.category };
descStats[key].count += r.count;
});
var repeatSorted = Object.entries(descStats)
.filter(function(e) { return e[1].count > 1; })
.sort(function(a, b) { return b[1].count - a[1].count; });
var topCat = catSorted.length > 0 ? catSorted[0] : null;
return {
records: records, totalRecords: totalRecords,
totalFixed: totalFixed, totalPending: totalPending,
catSorted: catSorted, deptSorted: deptSorted,
regionSorted: regionSorted, inspSorted: inspSorted,
repeatSorted: repeatSorted, topCat: topCat,
headers: headers
};
}
function escHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function buildMetric(value, label, cls) {
return '<div class="result-metric' + (cls ? ' ' + cls : '') + '">' +
'<div class="value">' + value + '</div><div class="label">' + label + '</div></div>';
}
function renderResults(data) {
var results = document.getElementById('results');
results.classList.add('active');
document.getElementById('dataInfo').textContent =
'Загружено: ' + data.totalRecords + ' записей. Устранено: ' + data.totalFixed +
' (' + Math.round(data.totalFixed / data.totalRecords * 100) + '%). На контроле: ' + data.totalPending + '.';
var metricsHTML = '';
metricsHTML += buildMetric(data.totalRecords, 'Всего записей');
metricsHTML += buildMetric(data.totalFixed, 'Устранено', 'good');
metricsHTML += buildMetric(data.totalPending, 'На контроле', data.totalPending > data.totalFixed * 2 ? 'warn' : '');
metricsHTML += buildMetric(data.catSorted.length, 'Категорий нарушений');
if (data.topCat) {
metricsHTML += buildMetric(
Math.round(data.topCat[1] / data.totalRecords * 100) + '%',
'Доля «' + (CAT_SHORT[data.topCat[0]] || data.topCat[0]) + '»',
data.topCat[1] / data.totalRecords > 0.25 ? 'warn' : ''
);
}
document.getElementById('metricsGrid').innerHTML = metricsHTML;
renderCategoriesTab(data);
renderDivisionsTab(data);
renderRegionsTab(data);
renderInspectorsTab(data);
renderRepeatTab(data);
renderTableTab(data);
renderConclusions(data);
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
document.querySelector('[data-tab="tab-categories"]').classList.add('active');
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
document.getElementById('tab-categories').classList.add('active');
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function renderBarList(containerId, items, total, maxBars) {
maxBars = maxBars || 15;
var container = document.getElementById(containerId);
var titles = {
'tab-categories': 'Распределение по категориям нарушений',
'tab-divisions': 'Рейтинг филиалов',
'tab-regions': 'Рейтинг областей',
'tab-inspectors': 'По проверяющим',
'tab-repeat': 'Повторяющиеся нарушения'
};
var html = '<div class="result-chart-card full"><h3>' + (titles[containerId] || '') + '</h3>';
var shown = items.slice(0, maxBars);
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;
var isWarn = containerId === 'tab-categories' && idx === 0 && pct > 20;
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' + (isWarn ? ' warn' : '') +
'" style="width:' + barPct + '%"></div></div></div>';
});
if (items.length > maxBars) {
html += '<p style="font-size:13px;color:var(--gray-500);margin-top:12px">Показаны первые ' + maxBars + ' из ' + items.length + '</p>';
}
html += '</div>';
container.innerHTML = html;
}
function renderCategoriesTab(data) { renderBarList('tab-categories', data.catSorted, data.totalRecords, 15); }
function renderDivisionsTab(data) { renderBarList('tab-divisions', data.deptSorted, data.totalRecords); }
function renderRegionsTab(data) { renderBarList('tab-regions', data.regionSorted, data.totalRecords); }
function renderInspectorsTab(data) { renderBarList('tab-inspectors', data.inspSorted, data.totalRecords); }
function renderRepeatTab(data) {
var container = document.getElementById('tab-repeat');
var items = data.repeatSorted;
var html = '';
if (items.length === 0) {
html = '<div class="result-chart-card full"><h3>Повторяющиеся нарушения</h3><p style="color:var(--gray-500)">Повторяющихся нарушений не найдено.</p></div>';
} else {
html = '<div class="result-chart-card full"><h3>Повторяющиеся нарушения (' + items.length + ')</h3>';
items.slice(0, 30).forEach(function(item) {
html += '<div class="bar-list-item"><div class="bar-list-header"><span class="name">' +
escHtml(item[1].desc || item[0]) + '</span><span class="count">' + item[1].count + ' раз</span></div>' +
'<div class="bar-list-track"><div class="bar-list-fill warn" style="width:' + Math.min(100, item[1].count * 15) + '%"></div></div></div>';
});
html += '</div>';
}
container.innerHTML = html;
}
function renderTableTab(data) {
var container = document.getElementById('tab-table');
var r = data.records;
var cols = ['date','dept','region','inspector','category','desc','status'];
var labels = { date: 'Дата', dept: 'Филиал', region: 'Область', inspector: 'Проверяющий', category: 'Категория', desc: 'Описание', status: 'Статус' };
var html = '<div class="result-table-wrap"><table class="result-table"><thead><tr>';
cols.forEach(function(c) { html += '<th>' + labels[c] + '</th>'; });
html += '</tr></thead><tbody>';
r.slice(0, 500).forEach(function(rec) {
html += '<tr>';
cols.forEach(function(c) {
var val = rec[c] || '';
if (c === 'status') val = rec.fixed ? 'Устранено' : 'На контроле';
if (c === 'desc' && val.length > 80) val = val.slice(0, 80) + '…';
html += '<td>' + escHtml(String(val)) + '</td>';
});
html += '</tr>';
});
if (r.length > 500) {
html += '<tr><td colspan="7" style="text-align:center;color:var(--gray-500)">Показаны первые 500 из ' + r.length + ' записей</td></tr>';
}
html += '</tbody></table></div>';
container.innerHTML = html;
}
function renderConclusions(data) {
var html = '';
var topCat = data.catSorted[0];
var topDept = data.deptSorted[0];
var worstDept = data.deptSorted[data.deptSorted.length - 1];
if (topCat && topCat[1] / data.totalRecords > 0.2) {
html += '<div class="conclusion-box warn-box"><h4>Проблемная зона</h4><p>Наиболее частой категорией является <strong>' +
escHtml(topCat[0]).toLowerCase() + '</strong> — доля <strong>' +
Math.round(topCat[1] / data.totalRecords * 100) + '%</strong> (' + topCat[1] + ' из ' + data.totalRecords + ' записей). ' +
'Основная концентрация — филиал <strong>' + escHtml(topDept[0]) + '</strong> (' + topDept[1] + ' нарушений).</p></div>';
}
html += '<div class="conclusion-box"><h4>Общие выводы</h4><p>Всего <strong>' + data.totalRecords +
'</strong> записей о нарушениях. Устранено: <strong>' + data.totalFixed +
'</strong> (' + Math.round(data.totalFixed / data.totalRecords * 100) + '%). На контроле: <strong>' + data.totalPending + '</strong>. ' +
'ТОП-3 категории: ' + data.catSorted.slice(0, 3).map(function(c, i) {
return (i + 1) + ') ' + escHtml(c[0]) + ' — ' + c[1] + ' (' + Math.round(c[1] / data.totalRecords * 100) + '%)';
}).join('; ') + '.</p></div>';
if (data.totalPending > data.totalFixed) {
html += '<div class="conclusion-box"><h4>Рекомендации</h4><p>На контроле <strong>' + data.totalPending +
'</strong> нарушений — требуется усилить контроль устранения. ';
if (topCat) {
var cat = topCat[0];
if (cat === 'Средства индивидуальной защиты') html += 'По СИЗ: провести внеплановые проверки применения средств защиты и целевой инструктаж. ';
else if (cat === 'Наряды-допуски') html += 'По нарядам-допускам: организовать дополнительное обучение и выборочный аудит. ';
else if (cat === 'Электробезопасность') html += 'По электробезопасности: усилить контроль со стороны ответственных лиц. ';
else if (cat === 'Пожарная безопасность') html += 'По пожарной безопасности: проверить сроки и качество инструктажей. ';
else html += 'По «' + escHtml(cat) + '»: провести внеплановую проверку и целевой инструктаж. ';
}
html += 'Рекомендуется еженедельный мониторинг.</p></div>';
}
document.getElementById('conclusions').innerHTML = html;
}
function processFile(file) {
document.getElementById('uploadStatus').textContent = 'Обработка...';
var reader = new FileReader();
reader.onload = function(e) {
try {
var text = e.target.result;
// Remove BOM
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
// Auto-detect delimiter
var commaCount = (text.split('\n')[0] || '').split(',').length;
var semiCount = (text.split('\n')[0] || '').split(';').length;
if (semiCount > commaCount + 2) {
text = text.replace(/;/g, ',');
}
var rows = parseCSV(text);
if (rows.length < 2) {
document.getElementById('uploadStatus').textContent = 'Ошибка: файл пуст.';
return;
}
var data = analyzeData(rows);
if (!data || data.totalRecords === 0) {
document.getElementById('uploadStatus').textContent = 'Ошибка: не удалось распознать данные.';
return;
}
document.getElementById('uploadStatus').textContent = 'Загружен «' + file.name + '» (' + data.totalRecords + ' записей)';
renderResults(data);
} catch (err) {
document.getElementById('uploadStatus').textContent = 'Ошибка: ' + err.message;
}
};
reader.readAsText(file, 'UTF-8');
}
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) processFile(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) processFile(e.dataTransfer.files[0]);
});
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
btn.classList.add('active');
var target = document.getElementById(btn.getAttribute('data-tab'));
if (target) target.classList.add('active');
});
});
document.getElementById('sampleBtn').addEventListener('click', function() {
document.getElementById('uploadStatus').textContent = 'Загрузка демо-данных...';
var sampleCSV = generateSampleCSV();
var blob = new Blob(['\uFEFF' + sampleCSV], { type: 'text/csv;charset=utf-8' });
processFile(new File([blob], 'demo_data.csv', { type: 'text/csv' }));
});
function generateSampleCSV() {
var header = 'Дата,Филиал,Область,Город/Район,Объект с адресом,Категория нарушения,Описание нарушения,Срок исполнения,исполнено,кто выдал,кому выдано';
var filials = ['ОДС','ОДС','ОДС','ДРБ','ДИТ','Сервисная фабрика','Сервисная фабрика','ДКБ','ДУП','СФ','ЦТО СФ','ЦЭиК'];
var oblasts = ['Акмолинская','Карагандинская','Астана','Астана','Улытауская','Карагандинская','Акмолинская','Астана','Карагандинская'];
var cities = ['Шахтинск','Астана','Караганда','Темиртау','Щучинск','Атбасар','Балхаш','Кокшетау'];
var inspectors = [
'Инженер по БиОТ Семидоцкий С.А.',
'Ведущий инженер ОБиОТ Туржанов А.Т.',
'Инженер ОБиОТ Бачинская Н.В.',
'Ведущий инженер ОБиОТ Ажакметов М.З.',
'Ведущий инженер ОБиОТ Баяхметова Ж.Т.',
'Инженер ОБиОТ Садыков М.Ф.',
'Ведущий инженер ОБиОТ Тумабаева С.А.',
'Инженер по БиОТ Джусупова А.Т.',
'Инженер по БиОТ Кришталь В.П.'
];
var realCats = [
'основные док-ты по БиОТ','основные док-ты по БиОТ','основные документы по БиОТ',
'Пожарная безопасность','Пожарная безопасность','пожарная безопасность',
'Электробезопасность',
'СИЗ','СИЗ','Не обеспечение СИЗ',
'Транспортная безопасность',
'Промышленная безопасность','промышленная безопасность',
'Санитарно-бытовые условия',
'организационные требования БиОТ','Инструкция по проведению 4-х ступенчатого контроля',
'Производственная санитария'
];
var descs = [
'Отсутствует журнал инструктажа на рабочем месте',
'Не проведён повторный инструктаж за 1 квартал',
'Работники не ознакомлены с регламентом обеспечения СИЗ',
'Форма журнала наряд-допусков не соответствует правилам',
'Просроченные диэлектрические перчатки',
'Лестница не имеет даты проверки лабораторных испытаний',
'Неактуальные инструкции по ТБ по видам работ',
'Отсутствует аптечка для оказания первой помощи',
'Пожарный кран не проверялся на работоспособность',
'Работники не применяли СИЗ (без касок, пояса)',
'Прошёл срок поверки инструмента с изолированными ручками',
'Не корректно ведётся журнал 4-х ступенчатого контроля',
'На территории скопление снега, требуется вывоз',
'Отсутствует комната для сушки спецодежды',
'Аварийный выход заблокирован',
'В журнале учёта такелажных средств нет подписи председателя',
'Нет таблички об ответственности по пожарной безопасности',
'Работники не обеспечены спецодеждой и обувью'
];
var statuses = ['исполнено','исполнено','исполнено','на контроле','на контроле','исполнено','на контроле'];
var months = ['01','01','02','02','03','03','04','04','05','05','06'];
var lines = [header];
for (var i = 0; i < 200; i++) {
var m = months[Math.floor(Math.random() * months.length)];
var day = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0');
var date = day + '.' + m + '.2026';
var fil = filials[Math.floor(Math.random() * filials.length)];
var obl = oblasts[Math.floor(Math.random() * oblasts.length)];
var city = cities[Math.floor(Math.random() * cities.length)];
var addr = 'ул. Примерная ' + (Math.floor(Math.random() * 50) + 1);
var cat = realCats[Math.floor(Math.random() * realCats.length)];
var desc = descs[Math.floor(Math.random() * descs.length)];
var insp = inspectors[Math.floor(Math.random() * inspectors.length)];
var status = statuses[Math.floor(Math.random() * statuses.length)];
lines.push([date, fil, obl, city, addr, cat, desc, '', status, insp, ''].join(','));
}
return lines.join('\n');
}
})();
</script>
</body>
</html>