Compare commits

...

76 Commits
main ... pages

Author SHA1 Message Date
8f04d65862 Регистрация показывает ошибку если Supabase недоступен 2026-06-11 09:49:10 +00:00
d288e9da7b Логин: быстрая проверка localStorage, Supabase только если нет локально 2026-06-11 06:48:25 +00:00
9d3e87b63a Удаление аудитов из Supabase, синхронизация заменяет данные 2026-06-11 05:48:43 +00:00
ca53bd4e98 Дашборд: сохранение фильтров, перезапуск графиков при обновлении 2026-06-11 05:44:46 +00:00
7c1c56b292 Авто-синхронизация каждые 45 сек + обновление активной вкладки 2026-06-11 04:58:00 +00:00
657b8a0359 sbSync теперь синхронизирует и пользователей и аудиты 2026-06-11 04:51:55 +00:00
e55485b30a Логин подтягивает всех из Supabase + кнопка Синхронизировать для админа 2026-06-11 04:44:13 +00:00
fa77de1526 CSV: UTF-8 BOM, разделитель ; для Excel, Экспорт теперь CSV вместо JSON 2026-06-11 04:34:49 +00:00
8b7d98beec Баннер отставания, email-напоминание, все 4 улучшения 2026-06-10 10:24:27 +00:00
0b4f7782cc Фильтр по датам в дашборде + удаление пользователей админом 2026-06-10 10:21:58 +00:00
9fefb11b17 Область и Город в Истории, Нарушениях и везде 2026-06-10 10:15:14 +00:00
fdfac12176 Область и Город сразу после Региона 2026-06-10 10:09:57 +00:00
03fc3f67aa Область и Город в форме ПАБ и при регистрации 2026-06-10 10:07:37 +00:00
785b616387 Добавлено: время, руководитель, таблица несоответствий 2026-06-10 10:04:01 +00:00
b5fb1abd6a Дашборд: убрал дубликаты, объединил перевыполнение+выполнение, чёткие названия 2026-06-10 09:59:15 +00:00
44fc6d4937 Нарушения из категорий показываются как В работе 2026-06-10 09:54:52 +00:00
2c1faefee2 Supabase синхронизация без правки showPanel — вкладки работают 2026-06-10 09:50:23 +00:00
55f8906b77 Исправлены вкладки: синхронизация фоном без двойного рендера 2026-06-10 09:46:31 +00:00
ca4fbf2549 Вкладки обновляются при клике: сначала из localStorage, потом из Supabase 2026-06-10 09:44:13 +00:00
42f33f4cbb Синхронизация Supabase, таблица несоответствий, просмотр аудита 2026-06-10 04:37:58 +00:00
ab9554b122 Только загрузка фото в Supabase, без ломки табов 2026-06-10 04:25:20 +00:00
280aaf6113 Откат до рабочей версии без Supabase 2026-06-10 04:22:04 +00:00
cf4292ba9d Загрузка фото в Supabase Storage при сохранении аудита 2026-06-10 04:17:56 +00:00
f026d2e8fc Вход проверяет Supabase если нет в localStorage 2026-06-05 11:24:14 +00:00
1767c480e5 Корректные URL и ключ Supabase (service_role) 2026-06-05 11:17:48 +00:00
b96e968540 Исправлен URL Supabase: bhlf→bhlm 2026-06-05 11:11:19 +00:00
801df72196 Регистрация отправляет в Supabase через fetch напрямую 2026-06-05 11:04:52 +00:00
faa69e8cbd Прямой REST API Supabase вместо JS-клиента 2026-06-05 11:00:42 +00:00
4789eb558e Логирование ошибок Supabase, прямая запись при сохранении 2026-06-05 10:58:33 +00:00
a1f41535ae Sync: localStorage→Supabase→localStorage, без дублирования клиента 2026-06-05 10:53:45 +00:00
2415e0ceab Supabase: регистрация, аудиты и синхронизация между ПК 2026-06-05 10:46:15 +00:00
cd3d1176ed Экспорт/импорт данных через JSON файлы для обмена между ПК 2026-06-05 10:34:18 +00:00
6fa64cb392 Вкладка Нарушения обновляется после сохранения аудита 2026-06-05 08:55:23 +00:00
1238a0b0fb Поле для прикрепления фото в бланке ПАБ 2026-06-04 12:28:28 +00:00
d084929cf9 Карточка профиля в Моём графике 2026-06-04 12:22:25 +00:00
45b182c87b Дашборд с Chart.js: столбчатые и линейные графики 2026-06-04 12:14:19 +00:00
82d0fe1161 Полная переработка без Chart.js — чистая статика 2026-06-04 12:11:59 +00:00
8de0864b6b Поле для ссылок на документы и прикрепления файлов 2026-06-04 10:37:49 +00:00
a127271971 Кнопки меняют и текст и цвет при переключении 2026-06-04 10:08:29 +00:00
769f02f424 Кнопки Безопасно/Опасно — прямая смена цвета через JS 2026-06-04 10:06:28 +00:00
b744974674 Исправлено переключение Безопасно/Опасно 2026-06-04 09:46:43 +00:00
1c6c7e8d17 Крупное подтверждение отправки с анимацией 2026-06-04 09:44:43 +00:00
7dd7928852 Подсказка нажать На email в подтверждении 2026-06-04 09:42:26 +00:00
f1be670631 Сообщение когда нет работников у админа 2026-06-04 07:21:36 +00:00
9d2ce89573 Исправлено: сообщение об успехе не скрывается сбросом формы 2026-06-04 07:19:05 +00:00
af9c833488 Админ видит график всех работников с прогрессом и статусом 2026-06-04 07:01:00 +00:00
2506f757ea Админ без графика — только управление, не проводит ПАБ 2026-06-04 06:59:06 +00:00
f50f53d04b Полностью убран без графика из Мой график, админу 1/мес 2026-06-04 06:56:54 +00:00
2fd6b18f81 Убрал Без графика — у всех обязательный график ПАБ 2026-06-04 06:52:05 +00:00
70dcc3c4e1 Без графика: показывает активность и статистику вместо заглушки 2026-06-04 06:50:52 +00:00
64ce8be89d График = минимум, перевыполнение >100%, дашборд с перевыполнением 2026-06-04 06:48:09 +00:00
231995df76 Поле Периодичность ПАБ при регистрации + getUserQuota 2026-06-04 06:39:05 +00:00
71a40f20d7 Форма в HTML напрямую (не innerHTML), регистрация на входе 2026-06-04 05:06:06 +00:00
1637998145 Полное приложение: ПАБ-форма, график, дашборд, нарушения, история, админ 2026-06-04 04:59:15 +00:00
b8ddf82a38 index.html = точная копия test-login.html которая работает 2026-06-04 04:53:04 +00:00
3740f565ca Два файла: простой вход + приложение с табами 2026-06-04 04:52:01 +00:00
6863760ee0 Точная копия подхода тестовой страницы — .onclick = function 2026-06-04 04:47:17 +00:00
1c5348083a onclick прямо в HTML кнопок — как в рабочей тестовой странице 2026-06-04 04:41:01 +00:00
603def833d Всё в одном файле: простой вход + приложение 2026-06-04 04:37:36 +00:00
977c2dfc04 Исправлены data-p атрибуты навигации 2026-06-04 04:31:40 +00:00
fe5aec4ba6 Разделение: index.html=простой вход, app.html=приложение 2026-06-03 13:09:45 +00:00
e9c00d9406 Мини-скрипт входа в <head> — гарантированно работает 2026-06-03 13:03:37 +00:00
41c324d9fb Кнопка входа через onclick как в рабочем тесте 2026-06-03 13:01:50 +00:00
b68dd5b138 Убрал IIFE — doLogin теперь доступен глобально для onclick 2026-06-03 12:58:25 +00:00
cca3e99623 Полная переработка: надёжные обработчики событий, чистый код 2026-06-03 12:56:51 +00:00
998c88377b Тестовая страница входа 2026-06-03 12:50:55 +00:00
f6e3a22544 Отладка: console.log в doLogin + onclick на кнопке 2026-06-03 12:49:38 +00:00
8e460986ac Программные обработчики клика для кнопок Войти/Регистрация 2026-06-03 12:47:36 +00:00
a05f4d5256 Исправление init: слушатели событий входа всегда добавляются первыми 2026-06-03 12:41:33 +00:00
6575589bdb Из Google Формы: регион в бланке, диалог безопасности, числовой номер ПАБ 2026-06-03 12:37:02 +00:00
7f0c0c322e Фильтр по датам, отслеживание нарушений, управление данными админа 2026-06-03 12:29:47 +00:00
c9ac12a3a4 Админ: скачивание полного CSV, отчёта по работникам, сводного HTML-отчёта 2026-06-03 12:22:48 +00:00
2fbd2f4a10 График ПАБ, квоты по должностям, регионы/филиалы, уведомления об отставании 2026-06-03 12:18:19 +00:00
f10b8c3f75 Email-уведомления: подтверждение с отправкой на почту и печать 2026-06-03 12:04:37 +00:00
2349fdcccb Админские права: только admin может редактировать и удалять 2026-06-03 11:38:44 +00:00
b567fa7d09 Добавлена регистрация работников 2026-06-03 11:34:31 +00:00
3 changed files with 365 additions and 946 deletions

262
app.html Normal file
View File

@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ПАБ — Система</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#0F1218;background:#F2F4F7;min-height:100vh}
.ah{background:#0F1218;color:#fff;padding:0 24px;display:flex;align-items:center;justify-content:space-between;height:56px;position:sticky;top:0;z-index:100}
.ah nav a{color:#9aa3b2;text-decoration:none;padding:7px 14px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer}.ah nav a.ac,.ah nav a:hover{color:#fff;background:rgba(255,255,255,.08)}
.acont{max-width:1140px;margin:0 auto;padding:24px}
.pn{display:none}.pn.ac{display:block}
.ph{margin-bottom:20px}.ph h2{font-size:26px;font-weight:800}
.card{background:#fff;border-radius:14px;padding:20px;box-shadow:0 2px 12px rgba(0,0,0,.06);margin-bottom:14px}
.card h3{font-size:16px;font-weight:700;margin-bottom:10px}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:14px}
.st{background:#fff;border-radius:14px;padding:16px;box-shadow:0 2px 12px rgba(0,0,0,.06);text-align:center}
.st .n{font-size:30px;font-weight:800}.st .l{font-size:11px;color:#5B6573;text-transform:uppercase;margin-top:4px}
.gr .n{color:#2D6A4F}.rd .n{color:#E63946}.bl .n{color:#00B4D8}
.btn{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;border-radius:8px;font-size:13px;font-weight:700;border:none;cursor:pointer;font-family:inherit}
.bp{background:#00B4D8;color:#fff}.bo{background:transparent;border:2px solid #E2E6EB;color:#0F1218}.bd{background:#E63946;color:#fff}
table{width:100%;border-collapse:collapse;font-size:13px;background:#fff;border-radius:14px;overflow:hidden}
th{background:#0F1218;color:#fff;padding:10px 12px;text-align:left;font-size:11px;text-transform:uppercase}
td{padding:8px 12px;border-bottom:1px solid #F2F4F7}tr:hover td{background:#F2F4F7}
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:700}
.bs{background:#EDF7F0;color:#2D6A4F}.bd2{background:#FFEBED;color:#E63946}.bw{background:#FFF3EF;color:#E76F51}
.fg{margin-bottom:12px}.fg label{display:block;font-size:11px;font-weight:700;color:#5B6573;margin-bottom:3px;text-transform:uppercase}
.fg input,.fg select,.fg textarea{width:100%;padding:9px 10px;border:2px solid #E2E6EB;border-radius:8px;font-size:13px;font-family:inherit;outline:none}
.fg input:focus,.fg select:focus,.fg textarea:focus{border-color:#00B4D8}
.fg textarea{resize:vertical;min-height:60px}
.hg{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}.hg.c2{grid-template-columns:1fr 1fr}
.ci{display:flex;align-items:flex-start;gap:8px;padding:4px 0;font-size:13px}
.ci input[type=checkbox]{margin-top:2px;width:15px;height:15px;accent-color:#E63946;cursor:pointer;flex-shrink:0}
.ci.ck label{color:#E63946;font-weight:600}
.ot{display:flex;gap:10px;margin-top:10px}
.tb{flex:1;padding:10px;border:2px solid #E2E6EB;border-radius:8px;text-align:center;cursor:pointer;font-size:13px;font-weight:700}
.tb.sf{background:#EDF7F0;border-color:#2D6A4F;color:#2D6A4F}
.tb.df{background:#FFEBED;border-color:#E63946;color:#E63946}
.fs{background:#EDF7F0;border:2px solid #2D6A4F;border-radius:8px;padding:20px;color:#2D6A4F;font-weight:600;margin-top:14px;display:none;text-align:center}
@media(max-width:768px){.ah{padding:0 10px;flex-wrap:wrap;height:auto;padding-top:8px}.ah nav{width:100%;overflow-x:auto}.acont{padding:12px}.hg{grid-template-columns:1fr 1fr}.stats{grid-template-columns:1fr 1fr}}
</style>
</head>
<body>
<header class="ah"><div style="font-weight:700">🛡️ ПАБ Система</div><nav><a onclick="showPanel('NA')" class="ac">Новый аудит</a><a onclick="showPanel('MS')">Мой график</a><a onclick="showPanel('DB')">Дашборд</a><a onclick="showPanel('VL')">Нарушения</a><a onclick="showPanel('HS')">История</a></nav><span style="font-size:13px"><span id="dn" style="color:#48CAE4;font-weight:600"></span> <button class="btn bo" style="color:#9aa3b2;border-color:#3a4452;font-size:12px;padding:4px 10px" onclick="doLogout()">Выход</button></span></header>
<div class="acont">
<div id="sa" style="background:#FFF3EF;border:1px solid #E76F51;border-radius:14px;padding:16px 20px;margin-bottom:20px;display:none" class=""><span id="sat" style="font-size:14px;font-weight:600;color:#E76F51"></span> <a onclick="sendScheduleReminder()" style="color:#00B4D8;cursor:pointer;font-weight:600;text-decoration:underline;margin-left:8px;white-space:nowrap">✉️ Напомнить</a></div>
<div id="pnNA" class="pn ac">
<div class="ph"><h2>📋 Бланк ПАБ</h2></div>
<div class="card"><h3>📝 Данные аудита</h3>
<div class="hg"><div class="fg"><label>Бланк №</label><input type="number" id="pn"></div><div class="fg"><label>Дата</label><input type="date" id="pd"></div><div class="fg"><label>Регион</label><select id="pr"><option value="">--</option><option>Центральный</option><option>Алматинский</option><option>Восточный</option><option>Западный</option><option>Северный</option><option>Южный</option></select></div></div>
<div class="hg"><div class="fg"><label>Область</label><input id="pob"></div><div class="fg"><label>Город / село</label><input id="pct"></div><div class="fg"></div></div>
<div class="hg"><div class="fg"><label>Время начала</label><input type="time" id="ps"></div><div class="fg"><label>Время конца</label><input type="time" id="pe"></div><div class="fg"></div></div>
<div class="hg"><div class="fg"><label>Место</label><input id="pl"></div><div class="fg"><label>Тип работы</label><input id="pw"></div><div class="fg"><label>Кол-во наблюдаемых</label><input type="number" id="pc" value="1"></div></div>
<div class="hg c2"><div class="fg"><label>ФИО наблюдателя</label><input id="po"></div><div class="fg"><label>Должность наблюдателя</label><input id="por"></div></div>
<div class="hg c2"><div class="fg"><label>ФИО руководителя работ</label><input id="psv"></div><div class="fg"><label>Должность руководителя</label><input id="psr"></div></div>
<div class="fg"><label>Отметка</label><div class="ot"><div class="tb sf" id="os" onclick="setO('safe')">ВСЕ БЕЗОПАСНО</div><div class="tb" id="od" onclick="setO('danger')">⚠️ ЕСТЬ ОПАСНО</div></div></div>
<div class="fg" style="margin-top:10px"><label>📎 Прикрепить фото</label><input type="file" id="pfiles" multiple accept="image/*" onchange="var n=[];for(var i=0;i<this.files.length;i++)n.push(this.files[i].name);document.getElementById('fn').textContent=n.length?'📷 '+n.join(', '):''"><div id="fn" style="font-size:12px;color:#5B6573;margin-top:4px"></div></div></div>
<div class="card"><h3>📄 Категории наблюдения</h3><div id="cats"></div></div>
<div class="card"><h3>💬 Итог диалога</h3>
<div class="ci"><input type="checkbox" id="d0"><label for="d0">Работник привёл примеры безопасных действий</label></div>
<div class="ci"><input type="checkbox" id="d1"><label for="d1">Были обсуждены риски / проблемы</label></div>
<div class="ci"><input type="checkbox" id="d2"><label for="d2">Определены корректирующие меры</label></div>
<div class="ci"><input type="checkbox" id="d3"><label for="d3">Предложения работника зафиксированы</label></div>
</div>
<div class="card"><h3>📄 Несоответствия и корректирующие меры</h3>
<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:#0F1218;color:#fff"><th></th><th>Несоответствие</th><th>Исполнитель</th><th>Вид нарушения</th><th>Меры</th><th>Ответственный</th><th>Срок</th><th>Завершение</th><th></th></tr></thead><tbody id="vioBody"><tr id="vioRow1"><td>1</td><td><input class="v-nc" placeholder="Описание" style="width:100%;padding:3px;border:1px solid #E2E6EB;border-radius:4px;font-size:12px"></td><td><input class="v-ex" placeholder="Исполнитель" style="width:100%;padding:3px;border:1px solid #E2E6EB;border-radius:4px;font-size:12px"></td><td><select class="v-tp" style="width:100%;padding:3px;border:1px solid #E2E6EB;border-radius:4px;font-size:12px"><option>Нарушение</option><option>Замечание</option><option>Риск</option></select></td><td><input class="v-ms" placeholder="Меры" style="width:100%;padding:3px;border:1px solid #E2E6EB;border-radius:4px;font-size:12px"></td><td><input class="v-rs" placeholder="Ответственный" style="width:100%;padding:3px;border:1px solid #E2E6EB;border-radius:4px;font-size:12px"></td><td><input type="date" class="v-dt" style="width:100%;padding:3px;border:1px solid #E2E6EB;border-radius:4px;font-size:12px"></td><td><input class="v-fn" placeholder="Завершение" style="width:100%;padding:3px;border:1px solid #E2E6EB;border-radius:4px;font-size:12px"></td><td><button onclick="removeVioRow(this)" style="background:none;border:none;color:#E63946;cursor:pointer;font-size:16px">×</button></td></tr></tbody></table></div>
<button class="btn bo bs" onclick="addVioRowFn()" style="margin-top:6px">+ Добавить строку</button></div>
<button class="btn bp" onclick="submitAudit()" style="margin-right:10px">💾 Сохранить аудит</button><button class="btn bo" onclick="resetF()">🗑️ Очистить</button>
<div class="fs" id="fs"><div style="font-size:24px"></div><div style="font-size:16px;font-weight:800">Аудит успешно отправлен!</div><div id="sd" style="font-size:13px;color:#0F1218;margin-top:8px"></div></div>
</div>
<div id="pnMS" class="pn">
<div class="ph"><h2>📅 Мой график</h2></div><div id="msc">Загрузка...</div>
</div>
<div id="pnDB" class="pn">
<div class="ph"><h2>📊 Дашборд</h2></div><div id="dbc">Загрузка...</div>
</div>
<div id="pnVL" class="pn">
<div class="ph"><h2>⚠️ Несоответствия</h2></div><div id="vlc">Загрузка...</div>
</div>
<div id="pnHS" class="pn">
<div class="ph"><h2>📁 История</h2></div>
<button class="btn bo" onclick="exportCSV()" style="margin-bottom:10px">📥 CSV</button><button class="btn bo" onclick="exportData()" style="margin-bottom:10px;margin-left:6px">📤 Экспорт</button>
<table><thead><tr><th>Бланк</th><th>Дата</th><th>Место</th><th>Область</th><th>Город</th><th>Наблюдатель</th><th>Статус</th><th>Нарушений</th><th></th></tr></thead><tbody id="hbd"><tr><td colspan="9" style="text-align:center;padding:20px;color:#5B6573">Нет записей</td></tr></tbody></table>
</div>
</div>
<script>
var U,editId,lastSubmitted,vrc=6;
try{U=JSON.parse(sessionStorage.getItem("pab_user"));if(!U)location.href="index.html"}catch(e){location.href="index.html"}
document.getElementById("dn").textContent=U.login;
function isA(){return U&&U.login==="admin"}
function getU(){try{return JSON.parse(localStorage.getItem("pab_users")||"{}")}catch(e){return{}}}
function allU(){var r=getU();r.admin={pass:"admin",name:"Администратор",role:"Руководитель",email:"admin@telecom.kz",branch:"АО «Казахтелеком»",dept:"ЦА",region:"Центральный",oblast:"—",city:"г. Астана"};return r}
function getA(){try{return JSON.parse(localStorage.getItem("pab_audits")||"[]")}catch(e){return[]}}
function saveA(d){localStorage.setItem("pab_audits",JSON.stringify(d))}
function saveU(d){localStorage.setItem("pab_users",JSON.stringify(d))}
// Supabase — фоновая синхронизация, не трогает вкладки
function sbSync(){sbPullUsers();sbPullAudits().then(function(){refreshCurrentTab()})}
function refreshCurrentTab(){var panels=["pnNA","pnMS","pnDB","pnVL","pnHS"];for(var i=0;i<panels.length;i++){if(document.getElementById(panels[i]).classList.contains("ac")){var n=panels[i].replace("pn","");if(n==="MS")rMS();if(n==="DB")rDB();if(n==="VL")rVL();if(n==="HS")rHS();break}}}
function sbPullUsers(){fetch(SBU+"/rest/v1/users?select=*",{headers:{"apikey":SBK,"Authorization":"Bearer "+SBK}}).then(function(r){return r.json()}).then(function(d){var um=getU();d.forEach(function(x){if(!um[x.login])um[x.login]={pass:x.pass,name:x.name,email:x.email,role:x.role,freq:x.freq,branch:x.branch,dept:x.dept,region:x.region,oblast:x.oblast,city:x.city}});saveU(um)}).catch(function(){})}
function sbPullAudits(){fetch(SBU+"/rest/v1/audits?select=*&order=created_at.desc",{headers:{"apikey":SBK,"Authorization":"Bearer "+SBK}}).then(function(r){return r.json()}).then(function(d){var am=[];d.forEach(function(x){am.push({id:x.id,number:x.number,date:x.date,location:x.location,region:x.region,workType:x.work_type,workerCount:x.worker_count,observer:x.observer,observerRole:x.observer_role,overallSafe:x.overall_safe,categories:x.categories,totalViolations:x.total_violations,dialogue:x.dialogue,photos:x.photos,docs:x.docs,createdBy:x.created_by,createdAt:x.created_at})});saveA(am)}).catch(function(){})}
function sbPushAudit(e){fetch(SBU+"/rest/v1/audits",{method:"POST",headers:{"apikey":SBK,"Authorization":"Bearer "+SBK,"Content-Type":"application/json","Prefer":"resolution=merge-duplicates"},body:JSON.stringify({id:e.id,number:e.number,date:e.date,location:e.location,region:e.region,work_type:e.workType,worker_count:e.workerCount,observer:e.observer,observer_role:e.observerRole,overall_safe:e.overallSafe,categories:e.categories,total_violations:e.totalViolations,dialogue:e.dialogue,photos:e.photos,docs:e.docs,created_by:e.createdBy,created_at:e.createdAt})}).catch(function(){})}
// Supabase для фото
var SBU="https://znexbjafkvyjffffbhlf.supabase.co";
var SBK="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpuZXhiamFma3Z5amZmZmZiaGxmIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc4MDY0NDE4MiwiZXhwIjoyMDk2MjIwMTgyfQ.5pOYTkL5eCmpSHBY3EwKof6NVKt7tL4Fn8xUAKM8itE";
function gq(role){var q={c:1,p:"month",l:"1 раз в месяц"};return q}
function getUserQuota(u){if(u.freq){var p=u.freq.split(",");if(p.length===2)return{c:parseInt(p[0]),p:p[1],l:parseInt(p[0])+" раз(а) в "+(p[1]==="month"?"месяц":p[1]==="quarter"?"квартал":"полгода")}}return gq(u.role)}
function gp(p){var n=new Date();if(p==="month")return{s:new Date(n.getFullYear(),n.getMonth(),1),l:n.toLocaleString("ru",{month:"long",year:"numeric"})};if(p==="quarter"){var q=Math.floor(n.getMonth()/3);return{s:new Date(n.getFullYear(),q*3,1),l:(q+1)+"-й квартал "+n.getFullYear()}}return{s:new Date(n.getFullYear(),n.getMonth()<6?0:6,1),l:(n.getMonth()<6?1:2)+"-е полугодие "+n.getFullYear()}}
var CATS=[{id:"reaction",title:"1. Реакция работника",items:["Приводит в порядок СИЗ","Меняет положение","Перестраивает работу","Прекращает работу","Наклоняется, прячется","Меняет инструмент","Подсоединяет защитные устройства","Другое"]},{id:"posture",title:"2. Поза работника",items:["Столкновения и удары","Защемление","Падение","Повторяющиеся движения","Статичные позы","Другое"]},{id:"ppe",title:"3. Отсутствие СИЗ",items:["Голова (каски)","Уши (беруши)","Глаза и лицо (очки)","Органы дыхания","Руки (перчатки)","Тела (спецодежда)","Ноги (обувь)","Другое"]},{id:"tools",title:"4. Инструменты и оборудование",items:["Самодельный инструмент","Ненадлежащее состояние","Не по назначению","Оборудование в плохом состоянии","Лестницы/стремянки","Ограждения","Переносное освещение","Другое"]},{id:"rules",title:"5. Инструкции и правила",items:["Отсутствие наряда","Инструкции не соответствуют","Требования не соблюдаются","Инструктажи не проведены","Тех. мероприятия не выполнены","Подготовка места не выполнена","Наряд не заполнен","Нет удостоверения","Неприменение СИЗ","Другое"]},{id:"conditions",title:"6. Условия труда",items:["Шум","Освещенность","Пыль","Задымленность","Беспорядок","Загромождение проходов","Нерациональное размещение","Температура","Другое"]},{id:"transport",title:"7. Транспорт",items:["Ремни безопасности","Опасное вождение","Состояние водителя","Телефон за рулём","Нарушение ПДД","Состояние ТС","Другое"]}];
// Build categories
var ch="";CATS.forEach(function(c){ch+="<div style=\"border:1px solid #E2E6EB;border-radius:8px;margin-bottom:8px;padding:12px;background:#F2F4F7\"><b>"+c.title+"</b><br>"+c.items.map(function(it,i){return"<div class=\"ci\"><input type=\"checkbox\" id=\"cb-"+c.id+"-"+i+"\" onchange=\"updateCT('"+c.id+"')\"><label for=\"cb-"+c.id+"-"+i+"\">"+it+"</label></div>"}).join("")+"<div style=\"margin-top:6px;font-size:12px;color:#5B6573\">Отмечено: <b id=\"cnt-"+c.id+"\">0</b></div></div>"});document.getElementById("cats").innerHTML=ch;
function updateCT(id){var cnt=0;CATS.find(function(c){return c.id===id}).items.forEach(function(_,i){if(document.getElementById("cb-"+id+"-"+i)&&document.getElementById("cb-"+id+"-"+i).checked)cnt++});document.getElementById("cnt-"+id).textContent=cnt}
function setO(t){var os=document.getElementById("os"),od=document.getElementById("od");os.className="tb"+(t==="safe"?" sf":"");od.className="tb"+(t==="danger"?" df":"")}
// Init form
document.getElementById("pd").value=new Date().toISOString().split("T")[0];
document.getElementById("po").value=U.name;document.getElementById("por").value=U.role||"";
document.getElementById("pr").value=U.region||"";document.getElementById("pob").value=U.oblast||"";document.getElementById("pct").value=U.city||"";
function showPanel(n){
["NA","MS","DB","VL","HS"].forEach(function(id){document.getElementById("pn"+id).classList.remove("ac")});
document.getElementById("pn"+n).classList.add("ac");
document.querySelectorAll("nav a").forEach(function(a){a.classList.toggle("ac",a.getAttribute("onclick").indexOf("'"+n+"'")>=0)});
if(n==="MS")rMS();if(n==="DB")rDB();if(n==="VL")rVL();if(n==="HS")rHS();sbSync();
}
function doLogout(){sessionStorage.removeItem("pab_user");location.href="index.html"}
function checkSA(){if(!U||isA()){document.getElementById("sa").style.display="none";return}var q=getUserQuota(U);if(!q.p)return;var p=gp(q.p);var done=getA().filter(function(a){return a.createdBy===U.login&&new Date(a.date)>=p.s}).length;var need=Math.max(0,q.c-done);var sa=document.getElementById("sa"),at=document.getElementById("sat");if(need>0){at.innerHTML="⚠️ Отставание: "+p.l+" — "+done+" из "+q.c+". Осталось: <b>"+need+"</b>.";sa.style.display="block";sa.style.background=need>=q.c?"#FFEBED":"#FFF3EF";sa.style.borderColor=need>=q.c?"#E63946":"#E76F51"}else{sa.style.display="none"}}
function sendScheduleReminder(){if(!U)return;var q=getUserQuota(U);var p=gp(q.p);var done=getA().filter(function(a){return a.createdBy===U.login&&new Date(a.date)>=p.s}).length;var need=Math.max(0,q.c-done);var to=U.email||"";if(!to||to.indexOf("@")<0){alert("Укажите email в профиле");return}location.href="mailto:"+encodeURIComponent(to)+"?subject="+encodeURIComponent("График ПАБ "+p.l)+"&body="+encodeURIComponent("Уважаемый(ая) "+U.name+"!\n\рафик ПАБ: "+q.l+".\ериод: "+p.l+".\nВыполнено: "+done+" из "+q.c+".\n"+(need>0?"Отставание: "+need+" ПАБ.":"График выполнен!")+"\n\nС уважением, Система ПАБ")}
function rMS(){
var c=document.getElementById("msc");if(!c)return;
if(isA()){
var all=allU();var rows="",wc=0;
for(var k in all){if(k==="admin")continue;wc++;var u=all[k];var q=getUserQuota(u);var p=gp(q.p);var d=getA().filter(function(a){return a.createdBy===k&&new Date(a.date)>=p.s}).length;var pct=Math.round(d/q.c*100);var st=d>q.c?"🔵 +"+(d-q.c):d>=q.c?"🟢 OK":"🔴 -"+(q.c-d);rows+="<tr><td>"+u.name+"</td><td>"+u.role+"</td><td>"+(u.branch||"—")+"</td><td>"+(u.region||"—")+"</td><td>"+q.l+"</td><td>"+d+"/"+q.c+"</td><td>"+st+"</td><td>"+p.l+"</td><td><button class=\"btn bd\" style=\"padding:2px 6px;font-size:10px\" onclick=\"delUser('"+k+"')\">🗑️</button></td></tr>"}
c.innerHTML=wc===0?"<div class=\"card\"><h3>👥 График работников</h3><p style=\"color:#5B6573\">Нет зарегистрированных работников. Зарегистрируйте их на странице входа.</p></div>":"<div class=\"card\"><h3>👥 График всех работников</h3></div><table><thead><tr><th>ФИО</th><th>Должность</th><th>Филиал</th><th>Регион</th><th>Норма</th><th>Прогресс</th><th>Статус</th><th>Период</th><th></th></tr></thead><tbody>"+rows+"</tbody></table>";
return;
}
var q=getUserQuota(U);var p=gp(q.p);var d=getA().filter(function(a){return a.createdBy===U.login&&new Date(a.date)>=p.s}).length;var pct=Math.round(d/q.c*100);var over=d>q.c;var cl=over?"#00B4D8":d>=q.c?"#2D6A4F":d>=q.c/2?"#E76F51":"#E63946";
c.innerHTML="<div class=\"card\"><h3>📅 "+p.l+"</h3><div style=\"font-size:12px;color:#5B6573;margin-bottom:8px\">Минимум: <b>"+q.l+"</b> | "+U.role+"</div><div style=\"height:14px;border-radius:7px;background:#E2E6EB;overflow:hidden;margin-bottom:6px\"><div style=\"height:100%;border-radius:7px;width:"+Math.min(pct,200)+"%;background:"+cl+";transition:width .5s\"></div></div><div style=\"font-size:13px;font-weight:600\">Проведено: <b>"+d+"</b> из <b>"+q.c+"</b>"+(over?" — <span style=\"color:#00B4D8\">✅ +"+(d-q.c)+" сверх плана!</span>":d>=q.c?" — ✅ план выполнен":" — <span style=\"color:#E63946\">осталось "+(q.c-d)+"</span>")+"</div></div><div class=\"card\" style=\"background:#E0F7FA;border:1px solid #00B4D8;margin-top:14px\"><h3>👤 Профиль</h3><div style=\"display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:13px\"><div><b>ФИО:</b> "+U.name+"</div><div><b>Должность:</b> "+U.role+"</div><div><b>Филиал:</b> "+(U.branch||"—")+"</div><div><b>Подразделение:</b> "+(U.dept||"—")+"</div><div><b>Регион:</b> "+(U.region||"—")+"</div><div><b>Область:</b> "+(U.oblast||"—")+"</div><div><b>Город:</b> "+(U.city||"—")+"</div><div><b>Email:</b> "+(U.email||"—")+"</div><div><b>График:</b> "+q.l+"</div><div><b>Проведено всего:</b> "+getA().filter(function(a){return a.createdBy===U.login}).length+"</div></div></div>"+(d>0?"<div class=\"card\"><h3>📋 Последние аудиты</h3>"+getA().filter(function(a){return a.createdBy===U.login}).slice(0,5).map(function(a){return"<div style=\"padding:6px 0;border-bottom:1px solid #F2F4F7;font-size:13px\">"+a.date+" — "+a.location+" — <span class=\"badge "+(a.overallSafe?"bs":"bd2")+"\">"+(a.overallSafe?"Безопасно":"Нарушений: "+a.totalViolations)+"</span></div>"}).join("")+"</div>":"");
}
function rDB(){
var c=document.getElementById("dbc");if(!c)return;
var a=getA(),all=allU();
// Сохраняем значения фильтров до перерисовки
var savedDf=(document.getElementById("df")||{}).value||"",savedDt=(document.getElementById("dt")||{}).value||"";
// Date filter
if(savedDf)a=a.filter(function(x){return x.date>=savedDf});
if(savedDt)a=a.filter(function(x){return x.date<=savedDt});
var t=a.length,sf=a.filter(function(x){return x.overallSafe}).length,wd=a.filter(function(x){return !x.overallSafe}).length,tv=a.reduce(function(s,x){return s+(x.totalViolations||0)},0);
var ot=0,bh=0,ov=0;for(var k in all){if(k==="admin")continue;var u=all[k];var q=getUserQuota(u);if(!q.p)continue;var p=gp(q.p);var d=a.filter(function(x){return x.createdBy===k&&new Date(x.date)>=p.s}).length;if(d>q.c)ov++;else if(d>=q.c)ot++;else bh++}
var adb=isA()?"<div style=\"margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap\"><button class=\"btn bp\" onclick=\"downloadFullCSV()\">📥 CSV данные</button><button class=\"btn bo\" onclick=\"downloadSummaryHTML()\">📊 HTML отчёт</button><button class=\"btn bo\" onclick=\"showAllUsers()\">👥 Пользователи</button><span style=\"color:#E2E6EB;margin:0 4px\">|</span><button class=\"btn bp\" onclick=\"sbSync();alert('Синхронизировано!')\" style=\"background:#2D6A4F\">🔄 Синхронизировать</button><button class=\"btn bo\" onclick=\"importData()\">📥 Импорт</button><button class=\"btn bo\" onclick=\"exportData()\">📤 Экспорт</button></div>":"";
c.innerHTML=adb+"<div class=\"fb\"><span style=\"font-size:12px;color:#5B6573\">с</span><input type=\"date\" id=\"df\" style=\"width:140px\" onchange=\"rDB()\" value=\""+savedDf+"\"><span style=\"font-size:12px;color:#5B6573\">по</span><input type=\"date\" id=\"dt\" style=\"width:140px\" onchange=\"rDB()\" value=\""+savedDt+"\"><button class=\"btn bo bs\" onclick=\"document.getElementById('df').value='';document.getElementById('dt').value='';rDB()\" style=\"margin-left:6px\">✕ Сбросить</button></div>"+
"<div class=\"stats\"><div class=\"st\"><div class=\"n\">"+t+"</div><div class=\"l\">Всего аудитов</div></div><div class=\"st gr\"><div class=\"n\">"+sf+"</div><div class=\"l\">Безопасно</div></div><div class=\"st rd\"><div class=\"n\">"+wd+"</div><div class=\"l\">С нарушениями</div></div><div class=\"st rd\"><div class=\"n\">"+tv+"</div><div class=\"l\">Всего пунктов нарушений</div></div><div class=\"st gr\"><div class=\"n\">"+(ot+ov)+"</div><div class=\"l\">Выполняют график</div></div><div class=\"st rd\"><div class=\"n\">"+bh+"</div><div class=\"l\">Отстают от графика</div></div></div>"+
"<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px\"><div class=\"card\"><h3>📂 Нарушения по категориям</h3><canvas id=\"ch1\"></canvas></div><div class=\"card\"><h3>📅 Динамика по датам</h3><canvas id=\"ch2\"></canvas></div></div>"+
"<div class=\"card\"><h3>🔝 Топ-10 нарушений</h3><canvas id=\"ch3\"></canvas></div>";
// Charts — с проверкой что canvas существует
var initCharts=function(){if(!document.getElementById("ch1")){setTimeout(initCharts,100);return}
if(window._charts){for(var ck in window._charts)try{window._charts[ck].destroy()}catch(e){}}window._charts={};
window._charts.cat=new Chart(document.getElementById("ch1"),{type:"bar",data:{labels:CATS.map(function(c){return c.title.split(". ")[1]}),datasets:[{label:"Нарушений",data:CATS.map(function(cat){return a.reduce(function(s,x){var c2=x.categories&&x.categories[cat.id];return s+(c2?c2.items.length:0)},0)}),backgroundColor:"#E63946",borderRadius:4}]},options:{responsive:true,plugins:{legend:{display:false}}}});
var dates={};a.forEach(function(x){if(!dates[x.date])dates[x.date]=0;dates[x.date]+=(x.totalViolations||0)});var sd=Object.keys(dates).sort();
window._charts.tl=new Chart(document.getElementById("ch2"),{type:"line",data:{labels:sd,datasets:[{label:"Нарушений",data:sd.map(function(d){return dates[d]}),borderColor:"#E63946",tension:0.3,pointRadius:4}]},options:{responsive:true,plugins:{legend:{display:false}}}});
var ic={};a.forEach(function(x){if(x.categories){Object.values(x.categories).forEach(function(cat){if(cat.items)cat.items.forEach(function(it){ic[it.item]=(ic[it.item]||0)+1})})}});var ti=Object.entries(ic).sort(function(a,b){return b[1]-a[1]}).slice(0,10);
window._charts.top=new Chart(document.getElementById("ch3"),{type:"bar",data:{labels:ti.map(function(i){return i[0]}),datasets:[{label:"Раз",data:ti.map(function(i){return i[1]}),backgroundColor:["#E63946","#E76F51","#F4A261","#E9C46A","#2A9D8F","#264653","#00B4D8","#0077B6","#023E8A","#6C757D"],borderRadius:4}]},options:{indexAxis:"y",responsive:true,plugins:{legend:{display:false}}}});
};
setTimeout(initCharts,200);
}
function rVL(){var c=document.getElementById("vlc");if(!c)return;var a=getA(),td=new Date().toISOString().split("T")[0],av=[];
a.forEach(function(x){
if(x.violations&&x.violations.length>0){x.violations.forEach(function(v){var dd=v.date||"",dn=v.done&&v.done.trim();var st="pending";if(dn)st="done";else if(dd&&dd<td)st="overdue";av.push({nc:v.nc,ex:v.executor,ms:v.measure,rs:v.responsible,dt:dd,dn:v.done||"",st:st,ad:x.date,an:x.number||"—",ob:x.oblast||"",ct:x.city||""})})}
else if(x.totalViolations>0&&x.categories){Object.values(x.categories).forEach(function(cat){if(cat.items)cat.items.forEach(function(it){av.push({nc:it.item,ex:x.observer,ms:"",rs:"",dt:"",dn:"",st:"pending",ad:x.date,an:x.number||"—",ob:x.oblast||"",ct:x.city||""})})})}
});c.innerHTML="<div class=\"stats\"><div class=\"st\"><div class=\"n\">"+av.length+"</div><div class=\"l\">Всего</div></div><div class=\"st gr\"><div class=\"n\">"+av.filter(function(v){return v.st==="done"}).length+"</div><div class=\"l\">Устранено</div></div><div class=\"st rd\"><div class=\"n\">"+av.filter(function(v){return v.st==="overdue"}).length+"</div><div class=\"l\">Просрочено</div></div><div class=\"st\"><div class=\"n\" style=\"color:#E76F51\">"+av.filter(function(v){return v.st==="pending"}).length+"</div><div class=\"l\">В работе</div></div></div>"+(av.length>0?"<table><thead><tr><th></th><th>Несоответствие</th><th>Аудит</th><th>Область</th><th>Город</th><th>Исполнитель</th><th>Меры</th><th>Срок</th><th>Статус</th></tr></thead><tbody>"+av.map(function(v,i){var sc=v.st==="done"?"bs":v.st==="overdue"?"bd2":"bw";var sl=v.st==="done"?"Устранено":v.st==="overdue"?"Просрочено":"В работе";return"<tr><td>"+(i+1)+"</td><td>"+v.nc+"</td><td>"+v.ad+"</td><td>"+(v.ob||"—")+"</td><td>"+(v.ct||"—")+"</td><td>"+v.ex+"</td><td>"+(v.ms||"—")+"</td><td>"+(v.dt||"—")+"</td><td><span class=\"badge "+sc+"\">"+sl+"</span></td></tr>"}).join("")+"</tbody></table>":"<p style=\"color:#5B6573;padding:20px\">Несоответствий не найдено</p>")}
function rHS(){var a=getA(),tb=document.getElementById("hbd");if(!tb)return;tb.innerHTML=a.length===0?"<tr><td colspan=\"9\" style=\"text-align:center;padding:20px;color:#5B6573\">Нет записей</td></tr>":a.map(function(x){var ab="<a style=\"color:#00B4D8;cursor:pointer;font-weight:600\" onclick=\"viewA("+x.id+")\">👁️</a>"+(isA()?" <a style=\"color:#00B4D8;cursor:pointer;font-weight:600\" onclick=\"editA("+x.id+")\">✏️</a> <button class=\"btn bd\" style=\"padding:4px 8px;font-size:11px\" onclick=\"delA("+x.id+")\">🗑️</button>":"");return"<tr><td>"+(x.number||"—")+"</td><td>"+x.date+"</td><td>"+x.location+"</td><td>"+(x.oblast||"—")+"</td><td>"+(x.city||"—")+"</td><td>"+x.observer+"</td><td><span class=\"badge "+(x.overallSafe?"bs":"bd2")+"\">"+(x.overallSafe?"Безопасно":"Нарушения")+"</span></td><td>"+(x.totalViolations||0)+"</td><td>"+ab+"</td></tr>"}).join("")}
function uploadPhotos(files,callback){var urls=[];var done=0;function check(){done++;if(done>=files.length)callback(urls)}if(files.length===0){callback([]);return}for(var i=0;i<files.length;i++){(function(f){var fn=Date.now()+"_"+Math.random().toString(36).slice(2)+"_"+f.name.replace(/[^a-zA-Z0-9._-]/g,"_");fetch(SBU+"/storage/v1/object/photos/"+fn,{method:"POST",headers:{"apikey":SBK,"Authorization":"Bearer "+SBK,"Content-Type":f.type},body:f}).then(function(r){if(r.ok){urls.push(SBU+"/storage/v1/object/public/photos/"+fn)}check()}).catch(function(){check()})})(files[i])}}
// Violations table
var vioRC=1;
function addVioRowFn(){vioRC++;var t=document.getElementById("vioRow1").cloneNode(true);t.id="vioRow"+vioRC;t.querySelector("td").textContent=vioRC;t.querySelectorAll("input").forEach(function(i){i.value=""});document.getElementById("vioBody").appendChild(t)}
function removeVioRow(btn){var rows=document.querySelectorAll("#vioBody tr");if(rows.length<=1)return;btn.closest("tr").remove();document.querySelectorAll("#vioBody tr").forEach(function(r,i){r.querySelector("td").textContent=i+1});vioRC=document.querySelectorAll("#vioBody tr").length}
function getVioData(){var r=[];document.querySelectorAll("#vioBody tr").forEach(function(row){var nc=row.querySelector(".v-nc");if(!nc||!nc.value.trim())return;r.push({nc:nc.value.trim(),executor:row.querySelector(".v-ex").value.trim(),type:row.querySelector(".v-tp").value,measure:row.querySelector(".v-ms").value.trim(),responsible:row.querySelector(".v-rs").value.trim(),date:row.querySelector(".v-dt").value,done:row.querySelector(".v-fn").value.trim()})});return r}
function submitAudit(){
if(editId&&!isA()){alert("Только администратор может редактировать");return}
var loc=document.getElementById("pl").value.trim();if(!loc){alert("Укажите место проведения");return}
var pf=document.getElementById("pfiles");var files=pf&&pf.files?Array.from(pf.files):[];
uploadPhotos(files,function(photoUrls){
var cats={},tv=0;CATS.forEach(function(cat){var ch=[];cat.items.forEach(function(item,i){var cb=document.getElementById("cb-"+cat.id+"-"+i);if(cb&&cb.checked)ch.push({item:item})});cats[cat.id]={items:ch,allSafe:ch.length===0};tv+=ch.length});
var dl=[];if(document.getElementById("d0").checked)dl.push("Работник привёл примеры безопасных действий");if(document.getElementById("d1").checked)dl.push("Были обсуждены риски / проблемы");if(document.getElementById("d2").checked)dl.push("Определены корректирующие меры");if(document.getElementById("d3").checked)dl.push("Предложения работника зафиксированы");
var e={id:editId||Date.now(),number:document.getElementById("pn").value.trim(),date:document.getElementById("pd").value,timeStart:document.getElementById("ps").value,timeEnd:document.getElementById("pe").value,location:loc,region:document.getElementById("pr").value,workType:document.getElementById("pw").value.trim(),workerCount:parseInt(document.getElementById("pc").value)||1,observer:document.getElementById("po").value.trim()||U.name,observerRole:document.getElementById("por").value.trim(),supervisor:document.getElementById("psv").value.trim(),supervisorRole:document.getElementById("psr").value.trim(),oblast:document.getElementById("pob").value.trim(),city:document.getElementById("pct").value.trim(),overallSafe:document.getElementById("os").classList.contains("sf"),categories:cats,totalViolations:tv,violations:getVioData(),dialogue:dl,photos:photoUrls,createdBy:U.login,createdAt:new Date().toISOString()};
var audits=getA();if(editId){audits=audits.map(function(a){return a.id===editId?e:a});editId=null}else{audits.unshift(e)}saveA(audits);sbPushAudit(e);lastSubmitted=e;
document.getElementById("sd").innerHTML="<b>Бланк №"+(e.number||"—")+"</b> | "+e.date+" | "+(e.overallSafe?"БЕЗОПАСНО":"НАРУШЕНИЙ: "+e.totalViolations);document.getElementById("fs").style.display="block";setTimeout(function(){document.getElementById("fs").style.display="none"},20000);
resetF();rVL();
});
}
function resetF(){
document.getElementById("pn").value="";document.getElementById("pd").value=new Date().toISOString().split("T")[0];document.getElementById("ps").value="";document.getElementById("pe").value="";document.getElementById("pr").value=U.region||"";document.getElementById("pl").value="";document.getElementById("pw").value="";document.getElementById("pc").value="1";document.getElementById("po").value=U.name;document.getElementById("por").value="";document.getElementById("psv").value="";document.getElementById("psr").value="";document.getElementById("pob").value=U.oblast||"";document.getElementById("pct").value=U.city||"";setO("safe");editId=null;
CATS.forEach(function(cat){cat.items.forEach(function(_,i){var cb=document.getElementById("cb-"+cat.id+"-"+i);if(cb)cb.checked=false});updateCT(cat.id)});
document.getElementById("d0").checked=false;document.getElementById("d1").checked=false;document.getElementById("d2").checked=false;document.getElementById("d3").checked=false;
document.getElementById("pfiles").value="";document.getElementById("fn").textContent="";
}
function exportCSV(){var a=getA();if(a.length===0){alert("Нет данных");return}var all=allU(),h="Бланк №;Дата;Место;Область;Город;Наблюдатель;Должность;Филиал;Регион;Статус;Нарушений",rs=a.map(function(x){var u=all[x.createdBy]||{};return(x.number||"")+";"+x.date+";"+x.location+";"+(x.oblast||"")+";"+(x.city||"")+";"+x.observer+";"+(x.observerRole||"")+";"+(u.branch||"")+";"+(u.region||"")+";"+(x.overallSafe?"Безопасно":"Нарушения")+";"+(x.totalViolations||0)});var bom="\uFEFF",csv=bom+h+"\n"+rs.join("\n"),bl=new Blob([csv],{type:"text/csv;charset=utf-8"}),ur=URL.createObjectURL(bl),dl=document.createElement("a");dl.href=ur;dl.download="pab.csv";dl.click();URL.revokeObjectURL(ur);alert("CSV в кодировке UTF-8. Откройте в Excel: Данные → Из текста → выберите файл → UTF-8")}
function editA(id){if(!isA()){alert("Только админ");return}alert("Редактирование: аудит #"+id)}
function delA(id){if(!isA()){alert("Только админ");return}if(!confirm("Удалить?"))return;saveA(getA().filter(function(a){return a.id!==id}));fetch(SBU+"/rest/v1/audits?id=eq."+id,{method:"DELETE",headers:{"apikey":SBK,"Authorization":"Bearer "+SBK}}).catch(function(){});rHS()}
function delUser(login){if(!isA())return;if(!confirm("Удалить пользователя "+login+" и все его аудиты?"))return;var u=getU();delete u[login];saveU(u);var a=getA().filter(function(x){return x.createdBy!==login});saveA(a);fetch(SBU+"/rest/v1/users?login=eq."+encodeURIComponent(login),{method:"DELETE",headers:{"apikey":SBK,"Authorization":"Bearer "+SBK}}).catch(function(){});rMS();rHS();rDB()}
function downloadFullCSV(){var a=getA();if(a.length===0){alert("Нет данных");return}var all=allU(),h="Бланк №;Дата;Место;Наблюдатель;Филиал;Регион;Статус;Нарушений",rs=a.map(function(x){var u=all[x.createdBy]||{};return(x.number||"")+";"+x.date+";"+x.location+";"+x.observer+";"+(u.branch||"")+";"+(u.region||"")+";"+(x.overallSafe?"Безопасно":"Нарушения")+";"+(x.totalViolations||0)}),csv="\uFEFF"+h+"\n"+rs.join("\n"),bl=new Blob([csv],{type:"text/csv"}),ur=URL.createObjectURL(bl),dl=document.createElement("a");dl.href=ur;dl.download="pab-full.csv";dl.click();URL.revokeObjectURL(ur)}
function showAllUsers(){if(!isA())return;var all=allU(),h="<h2>👥 Пользователи</h2><table style=\"width:100%;border-collapse:collapse;font-size:13px\"><tr style=\"background:#0F1218;color:#fff\"><th>Логин</th><th>ФИО</th><th>Должность</th><th>Филиал</th><th>Регион</th><th>Город</th></tr>";for(var k in all){var u=all[k];h+="<tr><td>"+k+(k==="admin"?" ⭐":"")+"</td><td>"+u.name+"</td><td>"+u.role+"</td><td>"+(u.branch||"—")+"</td><td>"+(u.region||"—")+"</td><td>"+(u.city||"—")+"</td></tr>"}h+="</table>";var w=window.open("","_blank","width=800,height=500");w.document.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Пользователи</title><style>body{font:14px/1.5 Arial;max-width:800px;margin:20px auto;padding:20px}</style></head><body>"+h+"</body></html>");w.document.close()}
function downloadSummaryHTML(){var a=getA(),all=allU(),t=a.length;var w=window.open("","_blank","width=800,height=600");w.document.write("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Отчёт ПАБ</title><style>body{font:14px/1.5 Arial;max-width:900px;margin:20px auto;padding:20px}h1{font-size:22px}h2{font-size:16px;margin-top:20px}table{width:100%;border-collapse:collapse;font-size:12px}th{background:#0F1218;color:#fff;padding:8px}td{padding:6px 8px;border-bottom:1px solid #E2E6EB}@media print{button{display:none}}</style></head><body><button onclick=\"window.print()\" style=\"padding:8px 16px;margin-bottom:16px\">🖨️ Печать</button><h1>📊 Сводный отчёт ПАБ</h1><p>Сформирован: "+new Date().toLocaleString("ru")+" | Всего аудитов: "+t+"</p><h2>📋 Аудиты</h2><table><tr><th></th><th>Дата</th><th>Место</th><th>Наблюдатель</th><th>Статус</th><th>Нарушений</th></tr>"+a.map(function(x){return"<tr><td>"+(x.number||"—")+"</td><td>"+x.date+"</td><td>"+x.location+"</td><td>"+x.observer+"</td><td>"+(x.overallSafe?"Безопасно":"Нарушения")+"</td><td>"+(x.totalViolations||0)+"</td></tr>"}).join("")+"</table></body></html>");w.document.close()}
function exportData(){
var a=getA();if(a.length===0){alert("Нет данных");return}
var all=allU(),h="Бланк №;Дата;Время;Место;Область;Город;Наблюдатель;Должность;Руководитель;Тип работы;Регион;Филиал;Статус;Нарушений";
var rs=a.map(function(x){var u=all[x.createdBy]||{};return(x.number||"")+";"+x.date+";"+(x.timeStart||"")+"-"+(x.timeEnd||"")+";"+x.location+";"+(x.oblast||"")+";"+(x.city||"")+";"+x.observer+";"+(x.observerRole||"")+";"+(x.supervisor||"")+";"+(x.workType||"")+";"+(x.region||"")+";"+(u.branch||"")+";"+(x.overallSafe?"Безопасно":"Нарушения")+";"+(x.totalViolations||0)});
var csv="\uFEFF"+h+"\n"+rs.join("\n"),bl=new Blob([csv],{type:"text/csv;charset=utf-8"}),ur=URL.createObjectURL(bl),dl=document.createElement("a");
dl.href=ur;dl.download="pab-export-"+new Date().toISOString().split("T")[0]+".csv";dl.click();URL.revokeObjectURL(ur);
}
function importData(){
var input=document.createElement("input");input.type="file";input.accept=".json";
input.onchange=function(){
var file=this.files[0];if(!file)return;
var reader=new FileReader();
reader.onload=function(e){
try{
var data=JSON.parse(e.target.result);
if(data.audits&&confirm("Импортировать "+data.audits.length+" аудитов и пользователей? Текущие данные будут объединены.")){
var curA=getA(),curU=getU();
// Merge audits (avoid duplicates by id)
var ids={};curA.forEach(function(a){ids[a.id]=true});
data.audits.forEach(function(a){if(!ids[a.id])curA.push(a)});
saveA(curA);
// Merge users
for(var k in data.users){if(!curU[k])curU[k]=data.users[k]}
localStorage.setItem("pab_users",JSON.stringify(curU));
alert("Импортировано! Аудитов: "+curA.length+", пользователей: "+Object.keys(curU).length);
rDB();rHS();rVL();rMS();
}
}catch(ex){alert("Ошибка чтения файла")}
};
reader.readAsText(file);
};
input.click();
}
rHS();sbSync();checkSA();setInterval(function(){sbSync()},45000);
</script>
</body>
</html>

1010
index.html

File diff suppressed because it is too large Load Diff

39
test-login.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ПАБ — Тест входа</title>
<style>
body{font:16px/1.5 sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#0F1218}
.card{background:#fff;padding:40px;border-radius:12px;width:360px;box-shadow:0 8px 40px rgba(0,0,0,0.3)}
h1{font-size:20px;text-align:center;margin-bottom:24px}
input{width:100%;padding:10px;margin-bottom:12px;border:2px solid #ddd;border-radius:6px;font-size:14px;box-sizing:border-box}
button{width:100%;padding:12px;background:#00B4D8;color:#fff;border:none;border-radius:6px;font-size:15px;font-weight:700;cursor:pointer}
button:hover{background:#48CAE4}
.err{color:#E63946;font-size:13px;text-align:center;display:none;margin-top:8px}
</style>
</head>
<body>
<div class="card">
<h1>🛡️ Вход в ПАБ</h1>
<input type="text" id="user" placeholder="Логин">
<input type="password" id="pass" placeholder="Пароль">
<button id="btn">Войти</button>
<div id="err" class="err">Неверный логин или пароль</div>
</div>
<script>
document.getElementById('btn').onclick = function(){
var u = document.getElementById('user').value.trim().toLowerCase();
var p = document.getElementById('pass').value.trim();
if(u==='admin' && p==='admin'){
alert('Вход успешен! Кнопка работает.');
window.location.href = '/';
} else {
document.getElementById('err').style.display = 'block';
}
};
</script>
</body>
</html>