From 2477e26c3022d637cbe6cad1dbc57fd58afe0215 Mon Sep 17 00:00:00 2001 From: Dauren777 Date: Fri, 5 Jun 2026 08:10:19 +0000 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BD=D1=82=D0=B5=D1=80=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D0=B9=20=D0=B8=D0=BD=D1=81=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=BC=D0=B5=D0=BD=D1=82:=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B0=20=D0=B2=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=80=D1=83=D1=88=D0=B5=D0=BD=D0=B8=D0=B9=20+=20=D0=B4=D0=B0?= =?UTF-8?q?=D1=88=D0=B1=D0=BE=D1=80=D0=B4=20=D1=81=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D0=BE=D0=B4=D0=B0=D0=BC=D0=B8=20+=20localStorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 1403 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 864 insertions(+), 539 deletions(-) diff --git a/index.html b/index.html index 9d29f87..524cd5b 100644 --- a/index.html +++ b/index.html @@ -724,6 +724,244 @@ body { 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; } @@ -735,6 +973,9 @@ body { .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; } } @@ -886,108 +1127,169 @@ body { - +
- -

Загрузите данные из Google Sheets

-

Экспортируйте таблицу в CSV (Файл → Скачать → CSV) и загрузите сюда. Анализатор сам определит категории, построит рейтинги и покажет выводы.

+ +

Ввод и анализ нарушений

+

Вносите нарушения, выявленные в ходе проверок. Данные сохраняются в браузере. Всегда доступен дашборд с аналитикой по периодам.

-
-
📂
-

Перетащите CSV-файл сюда или нажмите для выбора

-

Поддерживаются файлы .csv из Google Sheets (кодировка UTF-8)

- + +
+ + + +
-
- - -
- - -
-
- - -
- - -
- - - - - - -
- -
-
-
-
-
-
- - -
-
-
-
- - -
-
- -

Визуализация данных

-

Пример того, как выглядит аналитическая панель ИИ-агента

-
-
-
247
-
Нарушений за месяц
-
-
-
87%
-
Выполнение плана
-
-
-
142
-
Проведено проверок
-
-
-
32
-
Устранено за месяц
-
-
-
-
-
- Янв -
-
-
- Фев -
-
-
- Мар -
-
-
- Апр -
-
-
- Май -
-
-
- Июн -
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ + +
+
+ + + +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
📂
+

Перетащите CSV-файл сюда или нажмите для выбора

+

Данные добавятся к уже имеющимся записям

+ +
+
+ + +
+
+
@@ -1074,66 +1376,351 @@ body { 'use strict'; var CATEGORIES = [ - 'Документация по БиОТ', - 'Наряды-допуски', - 'Пожарная безопасность', - 'Транспортная безопасность', - 'Промышленная безопасность', - 'Санитарно-бытовые требования', - 'Средства индивидуальной защиты', - 'Обучение и проверка знаний', - 'Электробезопасность', - 'Организационные требования БиОТ', - 'Производственная санитария' + 'Документация по БиОТ', 'Наряды-допуски', 'Пожарная безопасность', + 'Транспортная безопасность', 'Промышленная безопасность', 'Санитарно-бытовые требования', + 'Средства индивидуальной защиты', 'Обучение и проверка знаний', 'Электробезопасность', + 'Организационные требования БиОТ', 'Производственная санитария' ]; - var CAT_SHORT = { - 'Документация по БиОТ': 'Документация БиОТ', - 'Наряды-допуски': 'Наряды-допуски', - 'Пожарная безопасность': 'Пожарная безопасность', - 'Транспортная безопасность': 'Транспортная безопасность', - 'Промышленная безопасность': 'Промышленная безопасность', - 'Санитарно-бытовые требования': 'Санитарно-бытовые', - 'Средства индивидуальной защиты': 'СИЗ', - 'Обучение и проверка знаний': 'Обучение', - 'Электробезопасность': 'Электробезопасность', - 'Организационные требования БиОТ': 'Орг. требования', - 'Производственная санитария': 'Произв. санитария', - 'Без категории': 'Без категории' - }; + var STORAGE_KEY = 'prombez_violations'; + var FILIALS = ['ОДС','ДРБ','ДИТ','Сервисная фабрика','ДКБ','ДУП','ДТК','СФ','ЦТО СФ','ЦЭиК']; - 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; + // ---- 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 = '
Нет данных за выбранный период
'; + 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 = '
'; + 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 += '
'; + document.getElementById('dashCharts').innerHTML = htmlC; + + // Conclusions + var htmlCon = ''; + var worstDept = deptSorted[deptSorted.length - 1]; + if (topCat && topCat[1] / total > 0.2) { + htmlCon += '

Проблемная зона

Наиболее частой категорией является ' + + escHtml(topCat[0]).toLowerCase() + ' — доля ' + + Math.round(topCat[1]/total*100) + '% (' + topCat[1] + ' из ' + total + ' записей).

'; + } + htmlCon += '

Общие выводы

Всего ' + total + + ' записей. Устранено: ' + fixed + ' (' + Math.round(fixed/total*100) + '%). ' + + 'На контроле: ' + pending + '. ' + + 'ТОП-3 категории: ' + catSorted.slice(0,3).map(function(c,i) { + return (i+1)+') '+escHtml(c[0])+' — '+c[1]+' ('+Math.round(c[1]/total*100)+'%)'; + }).join('; ') + '.

'; + if (pending > fixed) { + htmlCon += '

Рекомендации

На контроле ' + pending + + ' нарушений. Усилить контроль устранения. '; + 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 += '

'; + } + document.getElementById('dashConclusions').innerHTML = htmlCon; + } + + function renderBarChart(title, items, total, id) { + var html = '

' + title + '

'; + 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 += '
' + + escHtml(item[0]) + '' + item[1] + ' (' + pct + '%)
' + + '
'; + }); + html += '
'; + return html; + } + + function buildMetric(value, label, cls) { + return '
' + + value + '
' + label + '
'; + } + + // ---- 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 += '
' + + '' + + '' + escHtml(normalizeCategory(r.category || r.cat || '')) + '' + + '' + escHtml((r.desc || r.description || '').slice(0, 80)) + '' + + '' + (fixed ? 'Исполнено' : 'На контроле') + '' + + '' + + '
'; + }); + if (data.length === 0) html = '

Нет записей. Добавьте через форму «Ввод данных».

'; + if (data.length > 200) html += '

Показаны последние 200 из ' + data.length + '

'; + 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 = { + date: document.getElementById('fDate').value, + filial: document.getElementById('fFilial').value, + region: document.getElementById('fRegion').value, + city: document.getElementById('fCity').value, + addr: document.getElementById('fAddr').value, + issuedTo: document.getElementById('fIssuedTo').value, + cat: document.getElementById('fCat').value, + status: document.getElementById('fStatus').value, + desc: document.getElementById('fDesc').value, + deadline: document.getElementById('fDeadline').value, + inspector: document.getElementById('fInspector').value + }; + + if (!entry.date || !entry.filial || !entry.cat || !entry.desc) { + showMsg('Заполните обязательные поля: дата, филиал, категория, описание', 'err'); + return; + } + + addEntry(entry); + showMsg('Запись добавлена!', 'ok'); + document.getElementById('fDesc').value = ''; + document.getElementById('fDate').value = ''; + document.getElementById('fIssuedTo').value = ''; + document.getElementById('fAddr').value = ''; + document.getElementById('fCity').value = ''; + document.getElementById('fDeadline').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.date || '', r.filial || '', r.region || '', r.city || '', + r.addr || '', r.cat || r.category || '', r.desc || r.description || '', + r.status || '', r.deadline || '', 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; + 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++; } + if (i+1 < text.length && text[i+1] === '"') { cell += '"'; i++; } else quoted = false; - } else { cell += ch; } + } 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++; + 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; } + } else cell += ch; } } row.push(cell.trim()); @@ -1141,448 +1728,186 @@ body { 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]; + 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); - 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; + 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; + } - // Second category column detection: find second match + var ciDate = idx(['дата']); + var ciFilial = idx(['филиал']); + var ciRegion = idx(['область','регион']); + var ciCity = idx(['город','район']); + var ciAddr = idx(['объект','адрес']); + var ciCat = idx(['категор','нарушен']); + var ciDesc = -1; 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; } + if (headers[hi].indexOf('категор') !== -1 && hi !== ciCat) { ciDesc = hi; break; } + if (headers[hi].indexOf('описан') !== -1 || headers[hi].indexOf('суть') !== -1) { ciDesc = hi; break; } } - var colDesc = colDescCat2 >= 0 ? colDescCat2 : (colCategory >= 0 ? colCategory + 1 : -1); + if (ciDesc < 0) ciDesc = ciCat >= 0 ? ciCat + 1 : -1; + var ciStatus = idx(['исполнен','статус','устран']); + var ciDeadline = idx(['срок']); + var ciInspector = idx(['выдал','кто выдал','проверяющ']); + var ciIssued = idx(['кому','выдано']); - 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 + var entries = []; + data.forEach(function(r) { + if (r.length < 2) return; + entries.push({ + date: ciDate >= 0 ? r[ciDate] || '' : '', + 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] || '' : '', + status: ciStatus >= 0 ? r[ciStatus] || '' : '', + deadline: ciDeadline >= 0 ? r[ciDeadline] || '' : '', + inspector: ciInspector >= 0 ? r[ciInspector] || '' : '', + issuedTo: ciIssued >= 0 ? r[ciIssued] || '' : '' }); - } - - 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, '&').replace(//g, '>').replace(/"/g, '"'); - } - - function buildMetric(value, label, cls) { - return '
' + - '
' + value + '
' + label + '
'; - } - - 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' : '' - ); + if (entries.length > 0) { + appendEntries(entries); } - 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' }); + return entries.length; } - 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 = '

' + (titles[containerId] || '') + '

'; - 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 += '
' + - escHtml(item[0]) + '' + item[1] + ' (' + pct + '%)
' + - '
'; - }); - if (items.length > maxBars) { - html += '

Показаны первые ' + maxBars + ' из ' + items.length + '

'; - } - html += '
'; - 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 = '

Повторяющиеся нарушения

Повторяющихся нарушений не найдено.

'; - } else { - html = '

Повторяющиеся нарушения (' + items.length + ')

'; - items.slice(0, 30).forEach(function(item) { - html += '
' + - escHtml(item[1].desc || item[0]) + '' + item[1].count + ' раз
' + - '
'; - }); - html += '
'; - } - 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 = '
'; - cols.forEach(function(c) { html += ''; }); - html += ''; - r.slice(0, 500).forEach(function(rec) { - html += ''; - 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 += ''; - }); - html += ''; - }); - if (r.length > 500) { - html += ''; - } - html += '
' + labels[c] + '
' + escHtml(String(val)) + '
Показаны первые 500 из ' + r.length + ' записей
'; - 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 += '

Проблемная зона

Наиболее частой категорией является ' + - escHtml(topCat[0]).toLowerCase() + ' — доля ' + - Math.round(topCat[1] / data.totalRecords * 100) + '% (' + topCat[1] + ' из ' + data.totalRecords + ' записей). ' + - 'Основная концентрация — филиал ' + escHtml(topDept[0]) + ' (' + topDept[1] + ' нарушений).

'; - } - - html += '

Общие выводы

Всего ' + data.totalRecords + - ' записей о нарушениях. Устранено: ' + data.totalFixed + - ' (' + Math.round(data.totalFixed / data.totalRecords * 100) + '%). На контроле: ' + data.totalPending + '. ' + - 'ТОП-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('; ') + '.

'; - - if (data.totalPending > data.totalFixed) { - html += '

Рекомендации

На контроле ' + data.totalPending + - ' нарушений — требуется усилить контроль устранения. '; - 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 += 'Рекомендуется еженедельный мониторинг.

'; - } - - document.getElementById('conclusions').innerHTML = html; - } - - function processFile(file) { + function processCSVFile(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 count = importCSV(rows); + if (count === 0) { + document.getElementById('uploadStatus').textContent = 'Не удалось распознать данные.'; + } else { + document.getElementById('uploadStatus').textContent = 'Импортировано: ' + count + ' записей из «' + file.name + '»'; + renderAll(); } - 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) { + } 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 filials = ['ОДС','ДРБ','ДИТ','Сервисная фабрика','ДКБ','ДУП','ЦТО СФ','ЦЭиК']; + var oblasts = ['Акмолинская','Карагандинская','Астана','Астана','Улытауская','Карагандинская','Акмолинская']; var cities = ['Шахтинск','Астана','Караганда','Темиртау','Щучинск','Атбасар','Балхаш','Кокшетау']; var inspectors = [ - 'Инженер по БиОТ Семидоцкий С.А.', - 'Ведущий инженер ОБиОТ Туржанов А.Т.', - 'Инженер ОБиОТ Бачинская Н.В.', - 'Ведущий инженер ОБиОТ Ажакметов М.З.', - 'Ведущий инженер ОБиОТ Баяхметова Ж.Т.', - 'Инженер ОБиОТ Садыков М.Ф.', - 'Ведущий инженер ОБиОТ Тумабаева С.А.', - 'Инженер по БиОТ Джусупова А.Т.', - 'Инженер по БиОТ Кришталь В.П.' - ]; - var realCats = [ - 'основные док-ты по БиОТ','основные док-ты по БиОТ','основные документы по БиОТ', - 'Пожарная безопасность','Пожарная безопасность','пожарная безопасность', - 'Электробезопасность', - 'СИЗ','СИЗ','Не обеспечение СИЗ', - 'Транспортная безопасность', - 'Промышленная безопасность','промышленная безопасность', - 'Санитарно-бытовые условия', - 'организационные требования БиОТ','Инструкция по проведению 4-х ступенчатого контроля', - 'Производственная санитария' + 'Инженер по БиОТ Семидоцкий С.А.','Ведущий инженер ОБиОТ Туржанов А.Т.', + 'Инженер ОБиОТ Бачинская Н.В.','Ведущий инженер ОБиОТ Ажакметов М.З.', + 'Ведущий инженер ОБиОТ Баяхметова Ж.Т.','Инженер ОБиОТ Садыков М.Ф.' ]; + var realCats = ['основные док-ты по БиОТ','Пожарная безопасность','Электробезопасность','СИЗ','Транспортная безопасность','Промышленная безопасность','Санитарно-бытовые условия','организационные требования БиОТ']; var descs = [ - 'Отсутствует журнал инструктажа на рабочем месте', - 'Не проведён повторный инструктаж за 1 квартал', - 'Работники не ознакомлены с регламентом обеспечения СИЗ', - 'Форма журнала наряд-допусков не соответствует правилам', - 'Просроченные диэлектрические перчатки', - 'Лестница не имеет даты проверки лабораторных испытаний', - 'Неактуальные инструкции по ТБ по видам работ', - 'Отсутствует аптечка для оказания первой помощи', - 'Пожарный кран не проверялся на работоспособность', - 'Работники не применяли СИЗ (без касок, пояса)', - 'Прошёл срок поверки инструмента с изолированными ручками', - 'Не корректно ведётся журнал 4-х ступенчатого контроля', - 'На территории скопление снега, требуется вывоз', - 'Отсутствует комната для сушки спецодежды', - 'Аварийный выход заблокирован', - 'В журнале учёта такелажных средств нет подписи председателя', - 'Нет таблички об ответственности по пожарной безопасности', - 'Работники не обеспечены спецодеждой и обувью' + 'Отсутствует журнал инструктажа','Не проведён повторный инструктаж','Работники не ознакомлены с регламентом СИЗ', + 'Просроченные диэлектрические перчатки','Неактуальные инструкции по ТБ','Отсутствует аптечка', + 'Пожарный кран не проверялся','Работники без касок','Прошёл срок поверки инструмента', + 'Аварийный выход заблокирован','На территории скопление снега' ]; var statuses = ['исполнено','исполнено','исполнено','на контроле','на контроле','исполнено','на контроле']; - var months = ['01','01','02','02','03','03','04','04','05','05','06']; + var months = ['01','02','03','04','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(',')); + var d = String(Math.floor(Math.random()*28)+1).padStart(2,'0') + '.' + months[Math.floor(Math.random()*months.length)] + '.2026'; + lines.push([d, 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), + realCats[Math.floor(Math.random()*realCats.length)], descs[Math.floor(Math.random()*descs.length)], + '', 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,'&').replace(//g,'>').replace(/"/g,'"'); + } + + // ---- 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(); })();