safety-audit/index.html

1160 lines
96 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Поведенческий аудит безопасности (ПАБ)</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<style>
:root{
--ink:#0F1218; --cyan:#00B4D8; --cyan-light:#48CAE4; --cyan-bg:#E0F7FA;
--white:#FFFFFF; --gray-500:#5B6573; --gray-100:#F2F4F7; --gray-200:#E2E6EB;
--red:#E63946; --red-bg:#FFEBED; --green:#2D6A4F; --green-bg:#EDF7F0;
--orange:#E76F51; --orange-bg:#FFF3EF; --yellow:#F4A261;
--radius:8px; --radius-lg:14px; --shadow:0 2px 12px rgba(0,0,0,0.06);
}
*{box-sizing:border-box;margin:0;padding:0}
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--gray-100);min-height:100vh}
/* ===== LOGIN ===== */
.login-screen{display:flex;align-items:flex-start;justify-content:center;min-height:100vh;background:linear-gradient(135deg,var(--ink)0%,#1a2332 100%);padding:40px 20px}
.login-card{background:var(--white);border-radius:var(--radius-lg);padding:40px 36px;width:100%;max-width:520px;box-shadow:0 8px 40px rgba(0,0,0,0.2)}
.login-card .logo{text-align:center;margin-bottom:24px}
.login-card .logo .icon{font-size:40px;display:block;margin-bottom:6px}
.login-card .logo h1{font-size:20px;font-weight:800;color:var(--ink)}
.login-card .logo p{font-size:13px;color:var(--gray-500);margin-top:2px}
.form-group{margin-bottom:14px}
.form-group label{display:block;font-size:11px;font-weight:700;color:var(--gray-500);margin-bottom:4px;text-transform:uppercase;letter-spacing:0.5px}
.form-group input,.form-group select,.form-group textarea{width:100%;padding:9px 10px;border:2px solid var(--gray-200);border-radius:var(--radius);font-size:13px;font-family:inherit;color:var(--ink);background:var(--white);transition:border-color .2s;outline:none}
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--cyan)}
.form-group textarea{resize:vertical;min-height:60px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.form-row.col3{grid-template-columns:1fr 1fr 1fr}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 20px;border-radius:var(--radius);font-size:14px;font-weight:700;border:none;cursor:pointer;text-decoration:none;font-family:inherit;transition:all .2s;white-space:nowrap}
.btn-primary{background:var(--cyan);color:var(--white)}
.btn-primary:hover{background:var(--cyan-light)}
.btn-danger{background:var(--red);color:var(--white)}
.btn-danger:hover{background:#c1121f}
.btn-outline{background:transparent;color:var(--ink);border:2px solid var(--gray-200)}
.btn-outline:hover{border-color:var(--cyan);color:var(--cyan)}
.btn-sm{padding:6px 14px;font-size:12px}
.btn-block{width:100%}
.login-error{color:var(--red);font-size:12px;text-align:center;margin-top:10px;display:none}
.login-tabs{display:flex;gap:0;margin-bottom:20px;border-radius:var(--radius);overflow:hidden;border:2px solid var(--gray-200)}
.login-tab{flex:1;padding:8px;text-align:center;font-size:13px;font-weight:700;cursor:pointer;background:var(--white);color:var(--gray-500);transition:all .2s;border:none;font-family:inherit}
.login-tab.active{background:var(--cyan);color:var(--white)}
.login-tab:first-child{border-right:1px solid var(--gray-200)}
.login-form{display:none}
.login-form.active{display:block}
.register-success{background:var(--green-bg);border:1px solid var(--green);border-radius:var(--radius);padding:10px 14px;color:var(--green);font-weight:600;text-align:center;margin-top:10px;display:none}
/* ===== APP ===== */
.app-screen{display:none}
.app-header{background:var(--ink);color:var(--white);padding:0 24px;display:flex;align-items:center;justify-content:space-between;height:56px;position:sticky;top:0;z-index:100;box-shadow:0 2px 8px rgba(0,0,0,0.15)}
.app-header .logo-area{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px}
.app-header .logo-area .icon{font-size:22px}
.app-header nav{display:flex;gap:4px}
.app-header nav a{color:#9aa3b2;text-decoration:none;padding:7px 14px;border-radius:var(--radius);font-size:13px;font-weight:600;transition:all .2s}
.app-header nav a:hover,.app-header nav a.active{color:var(--white);background:rgba(255,255,255,0.08)}
.app-header .user-area{display:flex;align-items:center;gap:10px;font-size:13px}
.app-header .user-area .role{color:var(--cyan-light);font-weight:600}
.app-content{max-width:1140px;margin:0 auto;padding:24px 24px}
.panel{display:none}
.panel.active{display:block}
.page-header{margin-bottom:24px}
.page-header h2{font-size:26px;font-weight:800;margin-bottom:6px}
.page-header p{color:var(--gray-500);font-size:15px}
/* ===== SCHEDULE ALERT ===== */
.schedule-alert{background:var(--orange-bg);border:1px solid var(--orange);border-radius:var(--radius-lg);padding:16px 20px;margin-bottom:20px;display:none;box-shadow:var(--shadow)}
.schedule-alert.show{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
.schedule-alert .alert-text{font-size:14px;font-weight:600;color:var(--orange)}
.schedule-alert .alert-btn{white-space:nowrap}
.schedule-alert.danger{background:var(--red-bg);border-color:var(--red)}
.schedule-alert.danger .alert-text{color:var(--red)}
/* ===== PROGRESS CARD ===== */
.progress-card{background:var(--white);border-radius:var(--radius-lg);padding:20px;box-shadow:var(--shadow);margin-bottom:16px}
.progress-card h3{font-size:15px;font-weight:700;margin-bottom:4px}
.progress-card .quota-info{font-size:12px;color:var(--gray-500);margin-bottom:10px}
.progress-bar{height:16px;border-radius:8px;background:var(--gray-200);overflow:hidden;margin-bottom:6px}
.progress-fill{height:100%;border-radius:8px;transition:width .5s}
.progress-fill.good{background:var(--green)}
.progress-fill.warn{background:var(--orange)}
.progress-fill.bad{background:var(--red)}
.progress-card .progress-stats{font-size:12px;font-weight:600}
/* ===== AUDIT FORM ===== */
.audit-form{max-width:900px}
.form-header{background:var(--white);border-radius:var(--radius-lg);padding:24px 28px;box-shadow:var(--shadow);margin-bottom:16px}
.form-header h3{font-size:17px;font-weight:700;margin-bottom:16px;color:var(--ink)}
.header-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px}
.header-grid.col2{grid-template-columns:1fr 1fr}
.header-grid.col4{grid-template-columns:1fr 1fr 1fr 1fr}
.header-grid .fg label{font-size:11px;font-weight:700;color:var(--gray-500);display:block;margin-bottom:3px;text-transform:uppercase}
.header-grid .fg input,.header-grid .fg select{width:100%;padding:8px 10px;border:2px solid var(--gray-200);border-radius:var(--radius);font-size:13px;font-family:inherit;outline:none;background:var(--white)}
.header-grid .fg input:focus,.header-grid .fg select:focus{border-color:var(--cyan)}
.overall-toggle{display:flex;gap:12px;margin-top:12px}
.toggle-btn{flex:1;padding:10px;border:2px solid var(--gray-200);border-radius:var(--radius);text-align:center;cursor:pointer;font-size:13px;font-weight:700;background:var(--white);transition:all .2s}
.toggle-btn.safe.selected{border-color:var(--green);background:var(--green-bg);color:var(--green)}
.toggle-btn.danger.selected{border-color:var(--red);background:var(--red-bg);color:var(--red)}
.cat-section{background:var(--white);border-radius:var(--radius-lg);box-shadow:var(--shadow);margin-bottom:12px;overflow:hidden}
.cat-header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;background:var(--gray-100);cursor:pointer;user-select:none;border-bottom:1px solid var(--gray-200);transition:background .2s}
.cat-header:hover{background:var(--gray-200)}
.cat-header .cat-title{font-size:15px;font-weight:700;display:flex;align-items:center;gap:8px}
.cat-header .cat-badge{font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;background:var(--red-bg);color:var(--red)}
.cat-header .cat-badge.all-safe{background:var(--green-bg);color:var(--green)}
.cat-header .cat-arrow{font-size:12px;transition:transform .3s;color:var(--gray-500)}
.cat-header.open .cat-arrow{transform:rotate(180deg)}
.cat-body{display:none;padding:16px 20px}
.cat-body.open{display:block}
.checklist{display:grid;grid-template-columns:1fr 1fr;gap:6px 24px}
.checklist.col3{grid-template-columns:1fr 1fr 1fr}
.checklist.col1{grid-template-columns:1fr}
.check-item{display:flex;align-items:flex-start;gap:8px;padding:6px 0;font-size:13px;cursor:pointer}
.check-item input[type=checkbox]{margin-top:2px;width:16px;height:16px;accent-color:var(--red);cursor:pointer;flex-shrink:0}
.check-item.checked label{color:var(--red);font-weight:600}
.check-item label{cursor:pointer;flex:1}
.check-item .other-input{width:100%;margin-top:4px;padding:6px 8px;border:1px solid var(--gray-200);border-radius:4px;font-size:12px;display:none}
.check-item.checked .other-input.visible{display:block}
.cat-footer{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;background:var(--gray-100);border-top:1px solid var(--gray-200);font-size:12px;color:var(--gray-500);font-weight:600}
.cat-footer .total-count{color:var(--red);font-weight:700}
.cat-footer .total-count.zero{color:var(--green)}
.all-safe-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;font-weight:700;padding:4px 12px;border-radius:20px;transition:all .2s}
.all-safe-toggle.active{background:var(--green-bg);color:var(--green)}
.all-safe-toggle input{display:none}
.violations-block{background:var(--white);border-radius:var(--radius-lg);padding:20px 24px;box-shadow:var(--shadow);margin-top:16px}
.violations-block h3{font-size:15px;font-weight:700;margin-bottom:14px}
.vio-grid{display:grid;grid-template-columns:40px 1.3fr 1fr 0.8fr 1fr 1fr 1fr 0.8fr 30px;gap:6px;margin-bottom:6px;align-items:end}
.vio-grid.header-row{font-size:11px;font-weight:700;color:var(--gray-500);text-transform:uppercase;margin-bottom:4px}
.vio-grid input,.vio-grid select{padding:7px 8px;border:1px solid var(--gray-200);border-radius:var(--radius);font-size:12px;font-family:inherit;outline:none;width:100%}
.vio-grid input:focus,.vio-grid select:focus{border-color:var(--cyan)}
.vio-row-num{font-size:12px;font-weight:700;color:var(--gray-500);text-align:center;padding-top:8px}
.remove-vio-btn{background:none;border:none;color:var(--red);cursor:pointer;font-size:18px;padding:4px}
.form-actions{display:flex;gap:10px;margin-top:20px}
.form-success{background:var(--green-bg);border:1px solid var(--green);border-radius:var(--radius);padding:16px 20px;color:var(--green);font-weight:600;margin-top:14px;display:none}
/* ===== DASHBOARD ===== */
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}
.stat-card{background:var(--white);border-radius:var(--radius-lg);padding:20px;box-shadow:var(--shadow)}
.stat-card .stat-label{font-size:12px;font-weight:700;color:var(--gray-500);text-transform:uppercase;margin-bottom:4px}
.stat-card .stat-value{font-size:32px;font-weight:800;line-height:1}
.stat-card.green .stat-value{color:var(--green)}.stat-card.red .stat-value{color:var(--red)}.stat-card.blue .stat-value{color:var(--cyan)}.stat-card.orange .stat-value{color:var(--orange)}
.charts-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;margin-bottom:16px}
.chart-card{background:var(--white);border-radius:var(--radius-lg);padding:20px;box-shadow:var(--shadow)}
.chart-card h3{font-size:15px;font-weight:700;margin-bottom:14px}
.chart-card canvas{max-height:260px}
.chart-card.wide{grid-column:1/-1}
.chart-card.wide canvas{max-height:200px}
/* ===== FILTERS ===== */
.filter-bar{display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap;align-items:center}
.filter-bar select,.filter-bar input{padding:8px 12px;border:2px solid var(--gray-200);border-radius:var(--radius);font-size:13px;font-family:inherit;outline:none;background:var(--white)}
.filter-bar select:focus,.filter-bar input:focus{border-color:var(--cyan)}
/* ===== HISTORY ===== */
.table-wrap{overflow-x:auto}
.data-table{width:100%;border-collapse:collapse;background:var(--white);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow);font-size:13px}
.data-table th{background:var(--ink);color:var(--white);padding:11px 14px;text-align:left;font-size:12px;font-weight:700;text-transform:uppercase}
.data-table td{padding:10px 14px;border-bottom:1px solid var(--gray-100)}
.data-table tr:hover td{background:var(--gray-100)}
.badge{display:inline-block;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700}
.badge-safe{background:var(--green-bg);color:var(--green)}.badge-danger{background:var(--red-bg);color:var(--red)}.badge-warn{background:var(--orange-bg);color:var(--orange)}
.no-data{text-align:center;padding:40px 20px;color:var(--gray-500)}
.no-data .icon{font-size:40px;display:block;margin-bottom:10px}
.risk-bar{display:flex;height:20px;border-radius:10px;overflow:hidden;margin-top:6px}
.risk-safe{background:var(--green);transition:width .5s}
.risk-unsafe{background:var(--red);transition:width .5s}
.risk-labels{display:flex;justify-content:space-between;font-size:11px;color:var(--gray-500);margin-top:3px}
.view-link{color:var(--cyan);cursor:pointer;font-weight:600;text-decoration:none}
.view-link:hover{text-decoration:underline}
@media (max-width:768px){
.login-card{padding:28px 20px;margin:0;max-width:100%}
.app-header{padding:0 12px;height:auto;flex-wrap:wrap;gap:6px;padding-top:8px;padding-bottom:8px}
.app-header nav{order:3;width:100%;overflow-x:auto}
.app-content{padding:16px 12px}
.header-grid{grid-template-columns:1fr 1fr}
.header-grid.col4{grid-template-columns:1fr 1fr}
.checklist{grid-template-columns:1fr}
.checklist.col3{grid-template-columns:1fr 1fr}
.charts-grid{grid-template-columns:1fr}
.stats-grid{grid-template-columns:1fr 1fr}
.form-row{grid-template-columns:1fr}
.form-row.col3{grid-template-columns:1fr 1fr}
.vio-grid{grid-template-columns:30px 1fr 1fr;row-gap:4px}
.vio-grid.header-row{display:none}
}
</style>
</head>
<body>
<!-- ========== LOGIN ========== -->
<div id="loginScreen" class="login-screen">
<div class="login-card">
<div class="logo"><span class="icon">🛡️</span><h1>Поведенческий аудит безопасности</h1><p>Система учёта и аналитики ПАБ</p></div>
<div class="login-tabs">
<button class="login-tab active" onclick="switchLoginTab('login')">Вход</button>
<button class="login-tab" onclick="switchLoginTab('register')">Регистрация</button>
</div>
<div id="formLogin" class="login-form active">
<div class="form-group"><label>Логин</label><input type="text" id="loginUser" placeholder="Введите логин" autocomplete="username"></div>
<div class="form-group"><label>Пароль</label><input type="password" id="loginPass" placeholder="Введите пароль" autocomplete="current-password"></div>
<button id="loginBtn" class="btn btn-primary btn-block" onclick="doLogin()">Войти</button>
<div id="loginError" class="login-error">Неверный логин или пароль</div>
</div>
<div id="formRegister" class="login-form">
<div class="form-row"><div class="form-group"><label>Придумайте логин</label><input type="text" id="regLogin" placeholder="ivanov" autocomplete="off"></div><div class="form-group"><label>Придумайте пароль</label><input type="password" id="regPass" placeholder="Минимум 3 символа"></div></div>
<div class="form-row"><div class="form-group"><label>ФИО</label><input type="text" id="regName" placeholder="Фамилия И.О."></div><div class="form-group"><label>Email</label><input type="email" id="regEmail" placeholder="email@telecom.kz"></div></div>
<div class="form-group"><label>Должность (влияет на график ПАБ)</label><select id="regRole" onchange="updateRoleHint()">
<option value="">-- Выберите должность --</option>
<option value="Директор департамента ЦА">Директор департамента ЦА</option>
<option value="Директор департамента филиала">Директор департамента филиала</option>
<option value="Региональный директор филиала">Региональный директор филиала</option>
<option value="Директор ДЭСД">Директор ДЭСД</option>
<option value="Начальник ТУСМ">Начальник ТУСМ</option>
<option value="Руководитель структурного подразделения">Руководитель СП (без произв. участков)</option>
<option value="Начальник центра/службы/цеха">Начальник центра / службы / цеха</option>
<option value="Начальник участка">Начальник участка</option>
<option value="Инженер БиОТ">Инженер БиОТ</option>
<option value="Работник отдела БиОТ">Работник отдела БиОТ региона</option>
<option value="Сотрудник">Сотрудник (без графика)</option>
</select><div id="roleHint" style="font-size:11px;color:var(--gray-500);margin-top:4px"></div></div>
<div class="form-row"><div class="form-group"><label>Филиал</label><input type="text" id="regBranch" placeholder="Напр: АО «Казахтелеком»"></div><div class="form-group"><label>Структурное подразделение</label><input type="text" id="regDept" placeholder="Напр: ЦТО МС"></div></div>
<div class="form-row"><div class="form-group"><label>Регион</label><input type="text" id="regRegion" placeholder="Напр: Алматинский"></div><div class="form-group"><label>Область</label><input type="text" id="regOblast" placeholder="Напр: Алматинская обл."></div></div>
<div class="form-group"><label>Город / село</label><input type="text" id="regCity" placeholder="Напр: г. Алматы"></div>
<button id="regBtn" class="btn btn-primary btn-block">Зарегистрироваться</button>
<div id="regError" class="login-error">Ошибка регистрации</div>
<div id="regSuccess" class="register-success">Регистрация успешна! Сейчас войдите.</div>
</div>
</div>
</div>
<!-- ========== APP ========== -->
<div id="appScreen" class="app-screen">
<header class="app-header">
<div class="logo-area"><span class="icon">🛡️</span> ПАБ Система</div>
<nav>
<a href="#" data-panel="newAudit" class="active" onclick="switchPanel('newAudit',this)">Новый аудит</a>
<a href="#" data-panel="mySchedule" onclick="switchPanel('mySchedule',this)">Мой график</a>
<a href="#" data-panel="dashboard" onclick="switchPanel('dashboard',this)">Дашборд</a>
<a href="#" data-panel="violations" onclick="switchPanel('violations',this)">Нарушения</a>
<a href="#" data-panel="history" onclick="switchPanel('history',this)">История</a>
</nav>
<div class="user-area"><span class="role" id="displayName"></span><button class="btn btn-outline btn-sm" style="color:#9aa3b2;border-color:#3a4452" onclick="doLogout()">Выход</button></div>
</header>
<div class="app-content">
<!-- ========== SCHEDULE ALERT ========== -->
<div class="schedule-alert" id="scheduleAlert">
<span class="alert-text" id="scheduleAlertText"></span>
<button class="btn btn-primary btn-sm alert-btn" onclick="sendScheduleReminder()">✉️ Напомнить себе</button>
</div>
<!-- ========== NEW AUDIT ========== -->
<div id="panelNewAudit" class="panel active">
<div class="page-header"><h2>📋 Бланк поведенческого аудита безопасности</h2><p>Заполните все категории наблюдения</p></div>
<div class="audit-form" id="auditForm">
<div class="form-header">
<h3>📝 Данные аудита</h3>
<div class="header-grid col4">
<div class="fg"><label>Бланк ПАБ №</label><input id="pabNumber" placeholder="Номер" type="number" min="1"></div>
<div class="fg"><label>Дата проведения</label><input type="date" id="pabDate"></div>
<div class="fg"><label>Начало</label><input type="time" id="pabTimeStart"></div>
<div class="fg"><label>Конец</label><input type="time" id="pabTimeEnd"></div>
</div>
<div class="header-grid" style="margin-top:12px">
<div class="fg"><label>Место проведения</label><input id="pabLocation" placeholder="Цех, участок"></div>
<div class="fg"><label>Тип работы</label><input id="pabWorkType" placeholder="Напр: ремонт линий связи"></div>
<div class="fg"><label>Регион</label><select id="pabRegion"><option value="">-- Выберите --</option><option>Центральный</option><option>Алматинский</option><option>Восточный</option><option>Западный</option><option>Северный</option><option>Южный</option><option>Другое</option></select></div>
</div>
<div class="header-grid" style="margin-top:12px">
<div class="fg"><label>Кол-во наблюдаемых</label><input type="number" id="pabWorkerCount" min="1" value="1"></div>
<div class="fg"></div><div class="fg"></div>
</div>
<div class="header-grid col2" style="margin-top:12px">
<div class="fg"><label>ФИО наблюдателя</label><input id="pabObserver" placeholder="ФИО"></div>
<div class="fg"><label>Должность наблюдателя</label><input id="pabObserverRole" placeholder="Должность"></div>
</div>
<div class="header-grid col2" style="margin-top:12px">
<div class="fg"><label>ФИО руководителя работ</label><input id="pabSupervisor" placeholder="ФИО"></div>
<div class="fg"><label>Должность руководителя</label><input id="pabSupervisorRole" placeholder="Должность"></div>
</div>
<div class="header-grid" style="margin-top:12px">
<div class="fg"><label>Email для уведомления</label><input type="email" id="pabEmail" placeholder="email@telecom.kz"></div>
<div class="fg"></div><div class="fg"></div>
</div>
<div class="form-group" style="margin-top:14px;margin-bottom:0">
<label>Отметка для передачи в отдел БиОТ ДПБ</label>
<div class="overall-toggle">
<div class="toggle-btn safe selected" id="overallSafe" onclick="setOverall('safe')">ВСЕ БЕЗОПАСНО</div>
<div class="toggle-btn danger" id="overallDanger" onclick="setOverall('danger')">ЕСТЬ ОПАСНО</div>
</div>
</div>
</div>
<div id="categorySections"></div>
<div class="violations-block">
<h3>📄 Несоответствия и корректирующие меры</h3>
<div class="vio-grid header-row" style="display:grid"><span></span><span>Несоответствие</span><span>Исполнитель</span><span>Вид нарушения</span><span>Меры</span><span>Ответственное лицо</span><span>Дата</span><span>Форма завершения</span><span></span></div>
<div id="vioRows"></div>
<button class="btn btn-outline btn-sm" onclick="addVioRow()" style="margin-top:8px">+ Добавить строку</button>
</div>
<!-- Диалог безопасности -->
<div class="violations-block">
<h3>💬 Итог диалога безопасности с работником</h3>
<p style="font-size:12px;color:var(--gray-500);margin-bottom:14px">Зафиксируйте конкретные результаты диалога</p>
<div class="checklist col1" id="dialogueChecks">
<div class="check-item" id="ditem-0"><input type="checkbox" id="dcb-0" onchange="onCheckDialogue()"><div><label for="dcb-0">Работник привёл примеры безопасных действий</label></div></div>
<div class="check-item" id="ditem-1"><input type="checkbox" id="dcb-1" onchange="onCheckDialogue()"><div><label for="dcb-1">Были обсуждены риски / проблемы</label></div></div>
<div class="check-item" id="ditem-2"><input type="checkbox" id="dcb-2" onchange="onCheckDialogue()"><div><label for="dcb-2">Определены корректирующие меры</label></div></div>
<div class="check-item" id="ditem-3"><input type="checkbox" id="dcb-3" onchange="onCheckDialogue()"><div><label for="dcb-3">Предложения работника зафиксированы</label></div></div>
<div class="check-item" id="ditem-4"><input type="checkbox" id="dcb-4" onchange="onCheckDialogue()"><div><label for="dcb-4">Другое</label><input class="other-input" id="dialogueOther" placeholder="Укажите..." onchange="onCheckDialogue()"></div></div>
</div>
<div style="margin-top:14px;padding:12px;background:var(--gray-100);border-radius:var(--radius);font-size:12px;color:var(--gray-500)">
📎 <b>Фотографии, копии документов и другие свидетельства</b> — при наличии приложите к бумажному бланку. В электронной версии укажите в описании.
</div>
</div>
<div class="form-actions">
<button class="btn btn-primary" onclick="submitAudit()">💾 Сохранить аудит</button>
<button class="btn btn-outline" onclick="resetAuditForm()">🗑️ Очистить форму</button>
</div>
<div id="formSuccess" class="form-success">
<div style="font-size:18px;margin-bottom:8px">✅ Аудит сохранён!</div>
<div id="successDetail" style="font-size:13px;color:var(--ink);margin-bottom:14px"></div>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<button class="btn btn-primary btn-sm" onclick="sendEmailConfirm()">✉️ Отправить подтверждение на email</button>
<button class="btn btn-outline btn-sm" onclick="printConfirm()">🖨️ Распечатать</button>
</div>
</div>
</div>
</div>
<!-- ========== MY SCHEDULE ========== -->
<div id="panelMySchedule" class="panel">
<div class="page-header"><h2>📅 Мой график ПАБ</h2><p id="schedulePageDesc">Выполнение норматива по должности</p></div>
<div id="myScheduleContent"></div>
</div>
<!-- ========== DASHBOARD ========== -->
<div id="panelDashboard" class="panel">
<div class="page-header"><h2>📊 Дашборд статистики</h2><p>Аналитика по всем аудитам</p></div>
<div class="filter-bar">
<div id="adminBar" style="display:none;margin-right:auto;display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-primary btn-sm" onclick="downloadFullCSV()">📥 Скачать все данные (CSV)</button>
<button class="btn btn-outline btn-sm" onclick="downloadWorkerReport()">👥 Отчёт по работникам (CSV)</button>
<button class="btn btn-outline btn-sm" onclick="downloadSummaryHTML()">📊 Сводный отчёт (HTML)</button>
<span style="color:var(--gray-200);margin:0 4px">|</span>
<button class="btn btn-danger btn-sm" onclick="clearAllAudits()">🗑️ Очистить все аудиты</button>
<button class="btn btn-outline btn-sm" onclick="clearAuditsByDate()">📅 Очистить за период</button>
<button class="btn btn-outline btn-sm" onclick="showAllUsers()">👥 Все пользователи</button>
</div>
</div>
<div class="filter-bar" id="dashboardFilters">
<select id="dfRegion" onchange="renderDashboard()"><option value="all">Все регионы</option></select>
<select id="dfOblast" onchange="renderDashboard()"><option value="all">Все области</option></select>
<select id="dfBranch" onchange="renderDashboard()"><option value="all">Все филиалы</option></select>
<select id="dfDept" onchange="renderDashboard()"><option value="all">Все подразделения</option></select>
<select id="dfCity" onchange="renderDashboard()"><option value="all">Все города</option></select>
<span style="font-size:12px;color:var(--gray-500)">с</span>
<input type="date" id="dfDateFrom" onchange="renderDashboard()" style="width:140px">
<span style="font-size:12px;color:var(--gray-500)">по</span>
<input type="date" id="dfDateTo" onchange="renderDashboard()" style="width:140px">
</div>
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">Всего аудитов</div><div class="stat-value" id="statTotal">0</div></div>
<div class="stat-card green"><div class="stat-label">Всего безопасно</div><div class="stat-value" id="statAllSafe">0</div></div>
<div class="stat-card red"><div class="stat-label">С нарушениями</div><div class="stat-value" id="statWithDanger">0</div></div>
<div class="stat-card blue"><div class="stat-label">Выявлено нарушений</div><div class="stat-value" id="statViolations">0</div></div>
<div class="stat-card orange"><div class="stat-label">Выполняют график</div><div class="stat-value" id="statOnTrack">0</div></div>
<div class="stat-card red"><div class="stat-label">Отстают от графика</div><div class="stat-value" id="statBehind">0</div></div>
</div>
<div class="chart-card" style="margin-bottom:16px">
<h3>🟢🔴 Соотношение аудитов: безопасные / с нарушениями</h3>
<div class="risk-bar"><div class="risk-safe" id="riskSafeBar" style="width:50%"></div><div class="risk-unsafe" id="riskUnsafeBar" style="width:50%"></div></div>
<div class="risk-labels"><span>Безопасные: <span id="riskSafeLabel">0</span></span><span>С нарушениями: <span id="riskUnsafeLabel">0</span></span></div>
</div>
<div class="charts-grid">
<div class="chart-card"><h3>📂 Нарушения по категориям</h3><canvas id="chartCategories"></canvas></div>
<div class="chart-card"><h3>📅 Динамика по датам</h3><canvas id="chartTimeline"></canvas></div>
<div class="chart-card"><h3>🏢 По регионам</h3><canvas id="chartRegions"></canvas></div>
<div class="chart-card"><h3>🏭 По филиалам</h3><canvas id="chartBranches"></canvas></div>
<div class="chart-card"><h3>👤 Выполнение графика по должностям</h3><canvas id="chartRoles"></canvas></div>
<div class="chart-card"><h3>🔝 Топ-10 нарушений</h3><canvas id="chartTopItems"></canvas></div>
</div>
</div>
<!-- ========== VIOLATIONS TRACKING ========== -->
<div id="panelViolations" class="panel">
<div class="page-header"><h2>⚠️ Отслеживание несоответствий</h2><p>Контроль исполнения корректирующих мер по всем аудитам</p></div>
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">Всего несоответствий</div><div class="stat-value" id="vTotal">0</div></div>
<div class="stat-card green"><div class="stat-label">Устранено</div><div class="stat-value" id="vDone">0</div></div>
<div class="stat-card red"><div class="stat-label">Просрочено</div><div class="stat-value" id="vOverdue">0</div></div>
<div class="stat-card orange"><div class="stat-label">В работе</div><div class="stat-value" id="vPending">0</div></div>
</div>
<div class="filter-bar">
<select id="vfStatus" onchange="renderViolations()"><option value="all">Все</option><option value="done">Устранено</option><option value="overdue">Просрочено</option><option value="pending">В работе</option></select>
<select id="vfType" onchange="renderViolations()"><option value="all">Все виды</option><option>Нарушение</option><option>Замечание</option><option>Риск</option></select>
</div>
<div class="table-wrap">
<table class="data-table">
<thead><tr><th></th><th>Несоответствие</th><th>Аудит (дата)</th><th>Исполнитель</th><th>Вид</th><th>Меры</th><th>Ответственный</th><th>Срок</th><th>Завершение</th><th>Статус</th></tr></thead>
<tbody id="violationsBody"></tbody>
</table>
</div>
<div class="no-data" id="vioNoData" style="display:none"><span class="icon"></span><p>Несоответствий не найдено</p></div>
</div>
<!-- ========== HISTORY ========== -->
<div id="panelHistory" class="panel">
<div class="page-header"><h2>📁 История аудитов</h2><p>Архив всех проведённых ПАБ</p></div>
<div class="filter-bar">
<select id="filterOverall" onchange="renderHistory()"><option value="all">Все аудиты</option><option value="safe">«ВСЕ БЕЗОПАСНО»</option><option value="danger">С нарушениями</option></select>
<input type="date" id="filterDate" onchange="renderHistory()">
<input type="text" id="filterLocation" onchange="renderHistory()" placeholder="Поиск по месту...">
<button class="btn btn-outline btn-sm" onclick="exportCSV()">📥 Экспорт CSV</button>
</div>
<div class="table-wrap">
<table class="data-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="historyBody"></tbody>
</table>
</div>
<div class="no-data" id="noDataRow" style="display:none"><span class="icon">📭</span><p>Нет записей. Создайте первый аудит!</p></div>
</div>
</div>
</div>
<script>
// ========== PAB QUOTA RULES (пп.18-24) ==========
const PAB_QUOTA = {
'Директор департамента ЦА': {count:1, period:'halfyear', label:'1 раз в полгода'},
'Директор департамента филиала': {count:1, period:'halfyear', label:'1 раз в полгода'},
'Региональный директор филиала': {count:1, period:'quarter', label:'1 раз в квартал'},
'Директор ДЭСД': {count:1, period:'quarter', label:'1 раз в квартал'},
'Начальник ТУСМ': {count:1, period:'quarter', label:'1 раз в квартал'},
'Руководитель структурного подразделения': {count:1, period:'quarter', label:'1 раз в квартал'},
'Начальник центра/службы/цеха': {count:1, period:'month', label:'1 раз в месяц'},
'Начальник участка': {count:2, period:'month', label:'2 раза в месяц'},
'Инженер БиОТ': {count:1, period:'month', label:'1 раз в месяц'},
'Работник отдела БиОТ': {count:1, period:'month', label:'1 раз в месяц'},
'Аудитор': {count:1, period:'month', label:'1 раз в месяц'},
'Бригадир': {count:1, period:'month', label:'1 раз в месяц'},
'Руководитель': {count:1, period:'quarter', label:'1 раз в квартал'},
'Работник': {count:1, period:'quarter', label:'1 раз в квартал'},
'Сотрудник': {count:0, period:null, label:'Без графика'}
};
function getQuota(role){return PAB_QUOTA[role]||PAB_QUOTA['Сотрудник']}
function getCurrentPeriod(period){
const now=new Date();
if(period==='month') return {start:new Date(now.getFullYear(),now.getMonth(),1), label:now.toLocaleString('ru',{month:'long',year:'numeric'})};
if(period==='quarter'){
const q=Math.floor(now.getMonth()/3);
return {start:new Date(now.getFullYear(),q*3,1), label:(q+1)+'-й квартал '+now.getFullYear()};
}
if(period==='halfyear'){
const h=now.getMonth()<6?0:1;
return {start:new Date(now.getFullYear(),h*6,1), label:(h+1)+'-е полугодие '+now.getFullYear()};
}
return {start:new Date(2020,0,1), label:'весь период'};
}
function getEndOfPeriod(period,start){
const d=new Date(start);
if(period==='month'){d.setMonth(d.getMonth()+1);}
else if(period==='quarter'){d.setMonth(d.getMonth()+3);}
else if(period==='halfyear'){d.setMonth(d.getMonth()+6);}
else{d.setFullYear(2100);}
return d;
}
// ========== USERS ==========
const PREDEFINED_USERS = {
admin:{pass:'admin',name:'Администратор',role:'Руководитель',email:'admin@telecom.kz',branch:'АО «Казахтелеком»',dept:'ЦА',region:'Все',oblast:'—',city:'г. Астана'}
};
function getRegisteredUsers(){try{return JSON.parse(localStorage.getItem('safetyAuditRegisteredUsers')||'{}')}catch(e){return{}}}
function saveRegisteredUsers(d){localStorage.setItem('safetyAuditRegisteredUsers',JSON.stringify(d))}
function getAllUsers(){return{...getRegisteredUsers(),...PREDEFINED_USERS}}
// ========== CATEGORIES ==========
const CATEGORIES = [
{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:['Ремни безопасности отсутствуют, неисправны или не используются','Опасный стиль вождения','Состояние водителя не соответствует требованиям','Использование мобильного средства связи во время движения','Несоблюдение правил дорожного движения','Состояние транспортного средства не соответствует требованиям безопасности','Другое']}
];
// ========== STATE ==========
let currentUser=null,currentPanel='newAudit',editId=null,charts={},lastSubmitted=null;
function isAdmin(){return currentUser&&currentUser.login==='admin'}
// ========== INIT ==========
function init(){
var lu=document.getElementById('loginUser');
var lp=document.getElementById('loginPass');
var lb=document.getElementById('loginBtn');
var rb=document.getElementById('regBtn');
if(lu)lu.addEventListener('keydown',function(e){if(e.key==='Enter')doLogin();});
if(lp)lp.addEventListener('keydown',function(e){if(e.key==='Enter')doLogin();});
if(lb)lb.addEventListener('click',function(e){e.preventDefault();doLogin();});
if(rb)rb.addEventListener('click',function(e){e.preventDefault();doRegister();});
['regLogin','regPass','regName','regEmail','regBranch','regDept','regRegion','regOblast','regCity'].forEach(function(id){
var el=document.getElementById(id);if(el)el.addEventListener('keydown',function(e){if(e.key==='Enter')doRegister();});
});
var pd=document.getElementById('pabDate');if(pd)pd.value=new Date().toISOString().split('T')[0];
try{buildCategorySections()}catch(e){console.error(e)}
try{initVioRows()}catch(e){console.error(e)}
var saved=localStorage.getItem('safetyAuditUser');
if(saved){
try{
currentUser=JSON.parse(saved);
var po=document.getElementById('pabObserver');if(po)po.value=currentUser.name||'';
showApp();
}catch(e){console.error(e);localStorage.removeItem('safetyAuditUser');}
}
}
init();
// ========== CATEGORY HTML ==========
function buildCategorySections(){
const c=document.getElementById('categorySections');
c.innerHTML=CATEGORIES.map(cat=>{
const colClass=cat.items.length>7?'col3':(cat.items.length<=4?'col1':'');
return `<div class="cat-section" id="cat-${cat.id}">
<div class="cat-header" onclick="toggleCat('${cat.id}')"><span class="cat-title">${cat.title}</span><span class="cat-badge all-safe" id="badge-${cat.id}">ВСЕ БЕЗОПАСНО</span><span class="cat-arrow">▼</span></div>
<div class="cat-body open" id="body-${cat.id}">
<div class="all-safe-toggle active" id="allSafeToggle-${cat.id}" onclick="toggleAllSafe('${cat.id}')"><input type="checkbox" checked id="allSafeCb-${cat.id}"> ВСЕ БЕЗОПАСНО</div>
<div class="checklist ${colClass}">${cat.items.map((item,i)=>`<div class="check-item" id="item-${cat.id}-${i}"><input type="checkbox" id="cb-${cat.id}-${i}" onchange="onCheckItem('${cat.id}',${i})"><div><label for="cb-${cat.id}-${i}">${item}</label>${item==='Другое'?`<input class="other-input" id="other-${cat.id}" placeholder="Укажите..." onchange="onCheckItem('${cat.id}',${i})">`:''}</div></div>`).join('')}</div>
</div>
<div class="cat-footer"><span>Итого количество: <span class="total-count zero" id="total-${cat.id}">0</span></span></div>
</div>`}).join('');
}
// ========== CATEGORY INTERACTIONS ==========
function toggleCat(id){document.getElementById('body-'+id).classList.toggle('open');document.querySelector('#cat-'+id+' .cat-header').classList.toggle('open')}
function toggleAllSafe(catId){
const cb=document.getElementById('allSafeCb-'+catId),tog=document.getElementById('allSafeToggle-'+catId);
const isAllSafe=!cb.checked;cb.checked=isAllSafe;
if(isAllSafe){tog.classList.add('active');CATEGORIES.find(c=>c.id===catId).items.forEach((_,i)=>{const e=document.getElementById('cb-'+catId+'-'+i);if(e){e.checked=false;updateCheckItemUI(catId,i)};const o=document.getElementById('other-'+catId);if(o){o.style.display='none'}})}
else{tog.classList.remove('active')}
updateCatTotal(catId)
}
function onCheckItem(catId,idx){
const cb=document.getElementById('cb-'+catId+'-'+idx);updateCheckItemUI(catId,idx);
if(cb.checked){document.getElementById('allSafeCb-'+catId).checked=false;document.getElementById('allSafeToggle-'+catId).classList.remove('active')}
const cat=CATEGORIES.find(c=>c.id===catId);
if(cat&&cat.items[idx]==='Другое'){const o=document.getElementById('other-'+catId);if(o){o.style.display=cb.checked?'block':'none';o.classList.toggle('visible',cb.checked)}}
updateCatTotal(catId)
}
function updateCheckItemUI(catId,idx){const el=document.getElementById('item-'+catId+'-'+idx),cb=document.getElementById('cb-'+catId+'-'+idx);if(cb.checked)el.classList.add('checked');else el.classList.remove('checked')}
function updateCatTotal(catId){
const cat=CATEGORIES.find(c=>c.id===catId);let count=0;
cat.items.forEach((_,i)=>{if(document.getElementById('cb-'+catId+'-'+i)?.checked)count++});
const te=document.getElementById('total-'+catId);te.textContent=count;te.classList.toggle('zero',count===0);
const b=document.getElementById('badge-'+catId);
if(count===0){b.textContent='ВСЕ БЕЗОПАСНО';b.classList.add('all-safe')}else{b.textContent='Нарушений: '+count;b.classList.remove('all-safe')}
}
function setOverall(t){document.getElementById('overallSafe').classList.toggle('selected',t==='safe');document.getElementById('overallDanger').classList.toggle('selected',t==='danger')}
// ========== VIOLATION ROWS ==========
let vioRowCount=6;
function initVioRows(){document.getElementById('vioRows').innerHTML=Array.from({length:vioRowCount},(_,i)=>makeVioRow(i+1)).join('')}
function makeVioRow(num,data){const d=data||{};const sel=v=>v===d.type?' selected':'';return `<div class="vio-grid" id="vioRow${num}" style="display:grid"><span class="vio-row-num">${num}</span><input placeholder="Описание несоответствия" class="v-nc" value="${escAttr(d.nc||'')}"><input placeholder="Исполнитель" class="v-exec" value="${escAttr(d.executor||'')}"><select class="v-type"><option${sel('Нарушение')}>Нарушение</option><option${sel('Замечание')}>Замечание</option><option${sel('Риск')}>Риск</option></select><input placeholder="Корректирующие меры" class="v-measure" value="${escAttr(d.measure||'')}"><input placeholder="Ответственный" class="v-resp" value="${escAttr(d.responsible||'')}"><input type="date" class="v-date" value="${escAttr(d.date||'')}"><input placeholder="Форма завершения" class="v-done" value="${escAttr(d.done||'')}"><button class="remove-vio-btn" onclick="removeVioRow(${num})" title="Удалить">×</button></div>`}
function escAttr(s){return(s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
function addVioRow(){vioRowCount++;document.getElementById('vioRows').insertAdjacentHTML('beforeend',makeVioRow(vioRowCount))}
function removeVioRow(num){if(vioRowCount<=1)return;document.getElementById('vioRow'+num)?.remove();document.querySelectorAll('#vioRows .vio-grid').forEach((r,i)=>{r.id='vioRow'+(i+1);r.querySelector('.vio-row-num').textContent=i+1;r.querySelector('.remove-vio-btn').setAttribute('onclick','removeVioRow('+(i+1)+')')});vioRowCount=document.querySelectorAll('#vioRows .vio-grid').length}
function getVioRows(){const rows=[];document.querySelectorAll('#vioRows .vio-grid').forEach(r=>{const nc=r.querySelector('.v-nc')?.value?.trim();if(!nc)return;rows.push({nc,executor:r.querySelector('.v-exec')?.value?.trim()||'',type:r.querySelector('.v-type')?.value||'Нарушение',measure:r.querySelector('.v-measure')?.value?.trim()||'',responsible:r.querySelector('.v-resp')?.value?.trim()||'',date:r.querySelector('.v-date')?.value||'',done:r.querySelector('.v-done')?.value?.trim()||''})});return rows}
function onCheckDialogue(){
const cb4=document.getElementById('dcb-4'),other=document.getElementById('dialogueOther');
if(cb4&&other){other.style.display=cb4.checked?'block':'none';other.classList.toggle('visible',cb4.checked)}
}
function getDialogue(){
const items=[];
const labels=['Работник привёл примеры безопасных действий','Были обсуждены риски / проблемы','Определены корректирующие меры','Предложения работника зафиксированы','Другое'];
labels.forEach((label,i)=>{
if(document.getElementById('dcb-'+i)?.checked){
items.push({item:label,other:label==='Другое'?document.getElementById('dialogueOther')?.value?.trim()||'':null});
}
});
return items;
}
function setDialogue(items){
document.querySelectorAll('#dialogueChecks input[type=checkbox]').forEach(cb=>cb.checked=false);
document.getElementById('dialogueOther').value='';document.getElementById('dialogueOther').style.display='none';
items.forEach(di=>{
const idx=['Работник привёл примеры безопасных действий','Были обсуждены риски / проблемы','Определены корректирующие меры','Предложения работника зафиксированы','Другое'].indexOf(di.item);
if(idx>=0){const cb=document.getElementById('dcb-'+idx);if(cb)cb.checked=true;}
if(di.item==='Другое'&&di.other){const o=document.getElementById('dialogueOther');o.value=di.other;o.style.display='block';o.classList.add('visible')}
});
}
// ========== AUDIT SUBMIT ==========
function submitAudit(){
if(editId&&!isAdmin()){alert('Только администратор может редактировать аудиты');return}
const location=document.getElementById('pabLocation').value.trim();
if(!location){alert('Укажите место проведения ПАБ');return}
const cats={};let totalViolations=0;
CATEGORIES.forEach(cat=>{const checked=[];cat.items.forEach((item,i)=>{const cb=document.getElementById('cb-'+cat.id+'-'+i);if(cb&&cb.checked){const ov=item==='Другое'?document.getElementById('other-'+cat.id)?.value?.trim()||'':null;checked.push({item,other:ov})}});cats[cat.id]={items:checked,allSafe:checked.length===0};totalViolations+=checked.length});
const overallSafe=document.getElementById('overallSafe').classList.contains('selected');
const entry={id:editId||Date.now(),number:document.getElementById('pabNumber').value.trim(),date:document.getElementById('pabDate').value,timeStart:document.getElementById('pabTimeStart').value,timeEnd:document.getElementById('pabTimeEnd').value,location,region:document.getElementById('pabRegion').value,workType:document.getElementById('pabWorkType').value.trim(),workerCount:parseInt(document.getElementById('pabWorkerCount').value)||1,observer:document.getElementById('pabObserver').value.trim()||currentUser.name,observerRole:document.getElementById('pabObserverRole').value.trim(),supervisor:document.getElementById('pabSupervisor').value.trim(),supervisorRole:document.getElementById('pabSupervisorRole').value.trim(),email:document.getElementById('pabEmail').value.trim()||currentUser.email||'',overallSafe,categories:cats,totalViolations,violations:getVioRows(),dialogue:getDialogue(),createdBy:currentUser.login,createdAt:new Date().toISOString()};
let audits=getAudits();
if(editId){audits=audits.map(a=>a.id===editId?entry:a);editId=null}else{audits.unshift(entry)}
saveAudits(audits);resetAuditForm();lastSubmitted=entry;showSuccess(entry);
if(!isAdmin())checkScheduleAlert();
}
function resetAuditForm(){
document.getElementById('pabNumber').value='';document.getElementById('pabDate').value=new Date().toISOString().split('T')[0];
document.getElementById('pabTimeStart').value='';document.getElementById('pabTimeEnd').value='';
document.getElementById('pabLocation').value='';document.getElementById('pabRegion').value='';document.getElementById('pabWorkType').value='';document.getElementById('pabWorkerCount').value='1';
document.getElementById('pabObserver').value=currentUser?currentUser.name:'';document.getElementById('pabObserverRole').value='';
document.getElementById('pabSupervisor').value='';document.getElementById('pabSupervisorRole').value='';
document.getElementById('pabEmail').value=currentUser?currentUser.email||'':'';document.getElementById('pabRegion').value=currentUser?currentUser.region||'':'';setOverall('safe');editId=null;
CATEGORIES.forEach(cat=>{document.getElementById('allSafeCb-'+cat.id).checked=true;document.getElementById('allSafeToggle-'+cat.id).classList.add('active');cat.items.forEach((_,i)=>{const cb=document.getElementById('cb-'+cat.id+'-'+i);if(cb){cb.checked=false;updateCheckItemUI(cat.id,i)};const o=document.getElementById('other-'+cat.id);if(o){o.value='';o.style.display='none';o.classList.remove('visible')}});updateCatTotal(cat.id)});
document.getElementById('vioRows').innerHTML='';vioRowCount=6;initVioRows();document.getElementById('formSuccess').style.display='none';
// Clear dialogue
document.querySelectorAll('#dialogueChecks input[type=checkbox]').forEach(cb=>cb.checked=false);
const dOther=document.getElementById('dialogueOther');if(dOther){dOther.value='';dOther.style.display='none';dOther.classList.remove('visible');}
}
// ========== SUCCESS / EMAIL ==========
function showSuccess(entry){
const catsWithVio=CATEGORIES.filter(cat=>{const c=entry.categories&&entry.categories[cat.id];return c&&c.items.length>0}).map(c=>c.title).join(', ')||'все категории безопасны';
document.getElementById('successDetail').innerHTML=`<b>Бланк №${entry.number||'—'}</b> | ${entry.date} | ${entry.timeStart||'—'}${entry.timeEnd||'—'}<br>Место: ${entry.location} | Тип: ${entry.workType||'—'}<br>Наблюдатель: ${entry.observer} | Статус: <b>${entry.overallSafe?'ВСЕ БЕЗОПАСНО':'ВЫЯВЛЕНО '+entry.totalViolations+' НАРУШЕНИЙ'}</b><br>Категории с нарушениями: ${catsWithVio}${entry.email?'<br>Копия отправляется на: <b>'+entry.email+'</b>':''}`;
const s=document.getElementById('formSuccess');s.style.display='block';window.scrollTo({top:s.offsetTop-80,behavior:'smooth'});
setTimeout(()=>{s.style.display='none'},30000);
}
function sendEmailConfirm(){
if(!lastSubmitted)return;const e=lastSubmitted;
const to=e.email||currentUser.email||'';
const subject='ПАБ №'+(e.number||'—')+' от '+e.date;
const body=`ПОДТВЕРЖДЕНИЕ ПАБ\nБланк №: ${e.number||'—'}\nДата: ${e.date} | Время: ${e.timeStart||'—'}${e.timeEnd||'—'}\nМесто: ${e.location}\nНаблюдатель: ${e.observer}\nСтатус: ${e.overallSafe?'БЕЗОПАСНО':'НАРУШЕНИЙ: '+e.totalViolations}\n\nКатегории:\n${CATEGORIES.map(cat=>{const cd=e.categories&&e.categories[cat.id];const it=cd?cd.items:[];return cat.title+': '+(it.length===0?'БЕЗОПАСНО':it.map(i=>'☒ '+i.item).join('; '))}).join('\n')}\n\nПроведено в системе ПАБ.`;
if(to&&to.includes('@')){window.location.href='mailto:'+encodeURIComponent(to)+'?subject='+encodeURIComponent(subject)+'&body='+encodeURIComponent(body)}
else{alert('Не указан email. Укажите в форме аудита или при регистрации.')}
}
function sendScheduleReminder(){
if(!currentUser)return;
const q=getQuota(currentUser.role);const p=getCurrentPeriod(q.period);const audits=getAudits().filter(a=>a.createdBy===currentUser.login&&new Date(a.date)>=p.start);
const done=audits.length;const need=q.count-done;
const to=currentUser.email||'';
const subject='Напоминание: график ПАБ — '+p.label;
const body=`Уважаемый(ая) ${currentUser.name}!\n\nСогласно графику ПАБ, Вам необходимо провести ${need>0?need:0} наблюдений до конца периода: ${p.label}.\nНорматив: ${q.label}.\nВыполнено: ${done} из ${q.count}.\n${need>0?'Вы отстаёте от графика на '+need+' ПАБ.':'График выполнен!'}\n\nЗайдите в систему: https://pages.git.vibe42.kz/Yershat_Baitayev/safety-audit/\n\nС уважением, Система ПАБ.`;
if(to&&to.includes('@')){window.location.href='mailto:'+encodeURIComponent(to)+'?subject='+encodeURIComponent(subject)+'&body='+encodeURIComponent(body)}
else{alert('Не указан email.')}
}
function printConfirm(){
if(!lastSubmitted)return;const e=lastSubmitted;
const w=window.open('','_blank','width=700,height=800');
w.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>ПАБ №${e.number||'—'}</title><style>body{font:14px/1.5 Arial;max-width:700px;margin:40px auto;padding:20px}h1{font-size:20px}h2{font-size:16px;border-bottom:1px solid #ccc;padding-bottom:6px}table{width:100%;border-collapse:collapse;margin:12px 0}td{padding:6px 8px;border:1px solid #ddd}.mark{width:16px;text-align:center}@media print{body{margin:0;padding:10px}button{display:none}}</style></head><body><button onclick="window.print()" style="padding:8px 20px;font-size:16px;margin-bottom:20px">🖨️ Печать</button><h1>БЛАНК ПАБ №${e.number||'—'} от ${e.date}</h1><p><b>Время:</b> ${e.timeStart||'—'}${e.timeEnd||'—'} | <b>Место:</b> ${e.location}</p><p><b>Наблюдатель:</b> ${e.observer} | <b>Статус:</b> ${e.overallSafe?'БЕЗОПАСНО':'НАРУШЕНИЙ: '+e.totalViolations}</p>${CATEGORIES.map(cat=>{const cd=e.categories&&e.categories[cat.id];const it=cd?cd.items:[];return'<h2>'+cat.title+' — '+(it.length===0?'БЕЗОПАСНО':it.length+' наруш.')+'</h2><table>'+it.map(i=>'<tr><td class="mark">☒</td><td>'+i.item+(i.other?' — '+i.other:'')+'</td></tr>').join('')+'</table>'}).join('')}</body></html>`);
w.document.close();
}
// ========== DATA ==========
function getAudits(){try{return JSON.parse(localStorage.getItem('safetyAudits')||'[]')}catch(e){return[]}}
function saveAudits(d){localStorage.setItem('safetyAudits',JSON.stringify(d))}
// ========== LOGIN / REGISTER ==========
function switchLoginTab(tab){
document.querySelectorAll('.login-tab').forEach(t=>t.classList.toggle('active',(tab==='login'&&t.textContent==='Вход')||(tab==='register'&&t.textContent==='Регистрация')));
document.getElementById('formLogin').classList.toggle('active',tab==='login');
document.getElementById('formRegister').classList.toggle('active',tab==='register');
document.getElementById('loginError').style.display='none';document.getElementById('regError').style.display='none';document.getElementById('regSuccess').style.display='none';
}
function updateRoleHint(){
const role=document.getElementById('regRole').value;const q=getQuota(role);
document.getElementById('roleHint').textContent=q.count>0?'📅 График: '+q.label:'📅 Без обязательного графика';
}
function doRegister(){
const login=document.getElementById('regLogin').value.trim().toLowerCase();
const pass=document.getElementById('regPass').value.trim();
const name=document.getElementById('regName').value.trim();
const email=document.getElementById('regEmail').value.trim();
const role=document.getElementById('regRole').value;
const branch=document.getElementById('regBranch').value.trim();
const dept=document.getElementById('regDept').value.trim();
const region=document.getElementById('regRegion').value.trim();
const oblast=document.getElementById('regOblast').value.trim();
const city=document.getElementById('regCity').value.trim();
const err=document.getElementById('regError'),ok=document.getElementById('regSuccess');ok.style.display='none';
if(!login||login.length<2){err.textContent='Логин минимум 2 символа';err.style.display='block';return}
if(!pass||pass.length<3){err.textContent='Пароль минимум 3 символа';err.style.display='block';return}
if(!name){err.textContent='Укажите ФИО';err.style.display='block';return}
if(!role){err.textContent='Выберите должность';err.style.display='block';return}
if(!email||!email.includes('@')){err.textContent='Укажите корректный Email';err.style.display='block';return}
if(!branch){err.textContent='Укажите филиал';err.style.display='block';return}
if(!region){err.textContent='Укажите регион';err.style.display='block';return}
if(!city){err.textContent='Укажите город/село';err.style.display='block';return}
if(!dept){err.textContent='Укажите подразделение';err.style.display='block';return}
const all=getAllUsers();if(all[login]){err.textContent='Логин занят';err.style.display='block';return}
err.style.display='none';
const users=getRegisteredUsers();users[login]={pass,name,email,role,branch,dept,region,oblast,city};
saveRegisteredUsers(users);ok.style.display='block';
['regLogin','regPass','regName','regEmail','regBranch','regDept','regRegion','regOblast','regCity'].forEach(id=>document.getElementById(id).value='');
document.getElementById('loginUser').value=login;
setTimeout(()=>{switchLoginTab('login');ok.style.display='none'},2000);
}
function doLogin(){
var u=document.getElementById('loginUser').value.trim().toLowerCase();
var p=document.getElementById('loginPass').value.trim();
var err=document.getElementById('loginError');
console.log('doLogin called', u, p);
const all=getAllUsers();if(!all[u]||all[u].pass!==p){err.style.display='block';return}
err.style.display='none';currentUser={login:u,...all[u]};
localStorage.setItem('safetyAuditUser',JSON.stringify(currentUser));
document.getElementById('pabObserver').value=currentUser.name;
showApp();
}
function doLogout(){
localStorage.removeItem('safetyAuditUser');currentUser=null;
document.getElementById('loginScreen').style.display='flex';document.getElementById('appScreen').style.display='none';
['loginUser','loginPass','regLogin','regPass','regName','regEmail','regBranch','regDept','regRegion','regOblast','regCity'].forEach(id=>{const el=document.getElementById(id);if(el)el.value=''});
document.getElementById('loginError').style.display='none';document.getElementById('regError').style.display='none';document.getElementById('regSuccess').style.display='none';
document.getElementById('scheduleAlert').classList.remove('show','danger');
}
function showApp(){
document.getElementById('loginScreen').style.display='none';document.getElementById('appScreen').style.display='block';
document.getElementById('displayName').textContent=currentUser.login+' ('+currentUser.role+')';
document.getElementById('pabObserver').value=currentUser.name;
document.getElementById('pabEmail').value=currentUser.email||'';
document.getElementById('pabObserverRole').value=currentUser.role||'';
document.getElementById('pabRegion').value=currentUser.region||'';
switchPanel('newAudit',document.querySelector('[data-panel="newAudit"]'));
checkScheduleAlert();
}
// ========== SCHEDULE ==========
function checkScheduleAlert(){
if(!currentUser||isAdmin()){document.getElementById('scheduleAlert').classList.remove('show','danger');return}
if(currentPanel==='mySchedule')return;
const q=getQuota(currentUser.role);if(!q.period)return;
const p=getCurrentPeriod(q.period);
const audits=getAudits().filter(a=>a.createdBy===currentUser.login&&new Date(a.date)>=p.start);
const done=audits.length;const need=Math.max(0,q.count-done);
const alert=document.getElementById('scheduleAlert'),text=document.getElementById('scheduleAlertText');
alert.classList.remove('show','danger');
if(need>0){
text.textContent='⚠️ Внимание! Вы отстаёте от графика ПАБ ('+p.label+'): выполнено '+done+' из '+q.count+' ('+q.label+'). Осталось: '+need+'.';
alert.classList.add('show');if(need>=q.count)alert.classList.add('danger');
}
}
function renderMySchedule(){
if(!currentUser)return;
document.getElementById('scheduleAlert').classList.remove('show','danger');
const q=getQuota(currentUser.role);
const container=document.getElementById('myScheduleContent');
if(q.count===0){container.innerHTML='<div class="progress-card"><h3>📅 Без обязательного графика</h3><p style="color:var(--gray-500);font-size:13px">Для вашей должности («'+currentUser.role+'») не установлен обязательный график ПАБ.</p></div>';return}
const p=getCurrentPeriod(q.period);
const audits=getAudits().filter(a=>a.createdBy===currentUser.login&&new Date(a.date)>=p.start);
const done=audits.length;const pct=Math.min(100,Math.round(done/q.count*100));
let cls='good';if(pct<50)cls='bad';else if(pct<100)cls='warn';
const need=Math.max(0,q.count-done);
let recentHTML='';
if(audits.length>0){
recentHTML='<div style="margin-top:16px"><h4 style="font-size:13px;margin-bottom:8px">📋 Проведённые в этом периоде:</h4>'+audits.slice(0,5).map(a=>`<div style="background:var(--gray-100);padding:8px 12px;border-radius:var(--radius);margin-bottom:6px;font-size:13px">${a.date}${a.location} — <span class="badge ${a.overallSafe?'badge-safe':'badge-danger'}">${a.overallSafe?'Безопасно':'Нарушений: '+a.totalViolations}</span></div>`).join('')+'</div>';
}
container.innerHTML=`
<div class="progress-card">
<h3>📅 ${p.label}</h3>
<div class="quota-info">Норматив: <b>${q.label}</b> | Должность: ${currentUser.role}</div>
<div class="progress-bar"><div class="progress-fill ${cls}" style="width:${pct}%"></div></div>
<div class="progress-stats">Выполнено: <b>${done}</b> из <b>${q.count}</b> (${pct}%) ${need>0?'— осталось: <span style="color:var(--red)">'+need+'</span>':'— ✅ график выполнен!'}</div>
${need>0?`<div style="margin-top:10px"><button class="btn btn-primary btn-sm" onclick="sendScheduleReminder()">✉️ Напомнить себе на email</button></div>`:''}
</div>
<div class="progress-card">
<h3>👤 Мои данные</h3>
<div style="font-size:13px;display:grid;grid-template-columns:1fr 1fr;gap:6px">
<div><b>ФИО:</b> ${currentUser.name}</div><div><b>Должность:</b> ${currentUser.role}</div>
<div><b>Филиал:</b> ${currentUser.branch||'—'}</div><div><b>Подразделение:</b> ${currentUser.dept||'—'}</div>
<div><b>Регион:</b> ${currentUser.region||'—'}</div><div><b>Область:</b> ${currentUser.oblast||'—'}</div>
<div><b>Город:</b> ${currentUser.city||'—'}</div><div><b>Email:</b> ${currentUser.email||'—'}</div>
</div>
</div>
${recentHTML}
`;
}
// ========== PANELS ==========
function switchPanel(name,el){
currentPanel=name;
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
document.getElementById('panel'+name.charAt(0).toUpperCase()+name.slice(1)).classList.add('active');
document.querySelectorAll('nav a').forEach(a=>a.classList.remove('active'));
if(el)el.classList.add('active');
if(name==='dashboard')renderDashboard();
if(name==='history')renderHistory();
if(name==='mySchedule')renderMySchedule();
if(name==='violations')renderViolations();
}
// ========== DASHBOARD ==========
function renderDashboard(){
document.getElementById('adminBar').style.display=isAdmin()?'flex':'none';
const allUsers=getAllUsers();
const userList=Object.entries(allUsers).map(([login,data])=>({login,...data}));
// Collect filter values
const regions=[...new Set(userList.map(u=>u.region).filter(Boolean))];
const oblasts=[...new Set(userList.map(u=>u.oblast).filter(Boolean))];
const branches=[...new Set(userList.map(u=>u.branch).filter(Boolean))];
const depts=[...new Set(userList.map(u=>u.dept).filter(Boolean))];
const cities=[...new Set(userList.map(u=>u.city).filter(Boolean))];
function fillSelect(id,values){
const s=document.getElementById(id);if(!s)return;
const v=s.value;s.innerHTML='<option value="all">Все</option>'+values.sort().map(v=>'<option>'+v+'</option>').join('');
s.value=v||'all';
}
fillSelect('dfRegion',regions);fillSelect('dfOblast',oblasts);fillSelect('dfBranch',branches);fillSelect('dfDept',depts);fillSelect('dfCity',cities);
const fr=document.getElementById('dfRegion')?.value||'all';
const fo=document.getElementById('dfOblast')?.value||'all';
const fb=document.getElementById('dfBranch')?.value||'all';
const fd=document.getElementById('dfDept')?.value||'all';
const fc=document.getElementById('dfCity')?.value||'all';
let audits=getAudits();
// Filter audits by user's region/etc
audits=audits.filter(a=>{
const uData=allUsers[a.createdBy];
if(!uData)return true;
if(fr!=='all'&&uData.region!==fr)return false;
if(fo!=='all'&&uData.oblast!==fo)return false;
if(fb!=='all'&&uData.branch!==fb)return false;
if(fd!=='all'&&uData.dept!==fd)return false;
if(fc!=='all'&&uData.city!==fc)return false;
return true;
});
// Date range filter
const df=document.getElementById('dfDateFrom')?.value;
const dt=document.getElementById('dfDateTo')?.value;
if(df)audits=audits.filter(a=>a.date>=df);
if(dt)audits=audits.filter(a=>a.date<=dt);
const total=audits.length;
const allSafe=audits.filter(a=>a.overallSafe).length;
const withDanger=audits.filter(a=>!a.overallSafe).length;
const totalVio=audits.reduce((s,a)=>s+(a.totalViolations||0),0);
// Schedule tracking
let onTrack=0,behind=0;
userList.forEach(u=>{
if(u.login==='admin')return;
const q=getQuota(u.role);if(!q.period)return;
const p=getCurrentPeriod(q.period);
const ua=getAudits().filter(a=>a.createdBy===u.login&&new Date(a.date)>=p.start);
if(ua.length>=q.count)onTrack++;else behind++;
});
document.getElementById('statTotal').textContent=total;
document.getElementById('statAllSafe').textContent=allSafe;
document.getElementById('statWithDanger').textContent=withDanger;
document.getElementById('statViolations').textContent=totalVio;
document.getElementById('statOnTrack').textContent=onTrack;
document.getElementById('statBehind').textContent=behind;
const sp=total>0?(allSafe/total*100):50,dp=total>0?(withDanger/total*100):50;
document.getElementById('riskSafeBar').style.width=sp+'%';document.getElementById('riskUnsafeBar').style.width=dp+'%';
document.getElementById('riskSafeLabel').textContent=allSafe;document.getElementById('riskUnsafeLabel').textContent=withDanger;
Object.values(charts).forEach(c=>{try{c.destroy()}catch(e){}});charts={};
const ctx1=document.getElementById('chartCategories').getContext('2d');
charts.cat=new Chart(ctx1,{type:'bar',data:{labels:CATEGORIES.map(c=>c.title.split('. ')[1]),datasets:[{label:'Нарушений',data:CATEGORIES.map(cat=>audits.reduce((s,a)=>{const c=a.categories&&a.categories[cat.id];return s+(c?c.items.length:0)},0)),backgroundColor:'#E63946',borderRadius:6}]},options:{responsive:true,plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,ticks:{stepSize:1}}}}});
const dates={};audits.forEach(a=>{if(!dates[a.date])dates[a.date]=0;dates[a.date]+=(a.totalViolations||0)});
const sd=Object.keys(dates).sort();
const ctx2=document.getElementById('chartTimeline').getContext('2d');
charts.tl=new Chart(ctx2,{type:'line',data:{labels:sd,datasets:[{label:'Нарушений',data:sd.map(d=>dates[d]),borderColor:'#E63946',backgroundColor:'rgba(230,57,70,0.08)',fill:true,tension:0.3,pointRadius:5}]},options:{responsive:true,plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,ticks:{stepSize:1}}}}});
// By region
const regCounts={};audits.forEach(a=>{const u=allUsers[a.createdBy];const r=u?u.region||'Не указан':'Не указан';regCounts[r]=(regCounts[r]||0)+1});
const regSorted=Object.entries(regCounts).sort((a,b)=>b[1]-a[1]);
const ctx3=document.getElementById('chartRegions').getContext('2d');
charts.reg=new Chart(ctx3,{type:'bar',data:{labels:regSorted.map(r=>r[0]),datasets:[{label:'Аудитов',data:regSorted.map(r=>r[1]),backgroundColor:regSorted.map((_,i)=>['#00B4D8','#48CAE4','#90E0EF','#0077B6','#023E8A','#E63946','#E76F51','#2D6A4F'][i%8]||'#00B4D8'),borderRadius:6}]},options:{responsive:true,plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,ticks:{stepSize:1}}}}});
// By branch
const brCounts={};audits.forEach(a=>{const u=allUsers[a.createdBy];const b=u?u.branch||'Не указан':'Не указан';brCounts[b]=(brCounts[b]||0)+1});
const brSorted=Object.entries(brCounts).sort((a,b)=>b[1]-a[1]).slice(0,10);
const ctx4=document.getElementById('chartBranches').getContext('2d');
charts.br=new Chart(ctx4,{type:'bar',data:{labels:brSorted.map(r=>r[0].length>25?r[0].slice(0,25)+'...':r[0]),datasets:[{label:'Аудитов',data:brSorted.map(r=>r[1]),backgroundColor:'#00B4D8',borderRadius:6}]},options:{indexAxis:'y',responsive:true,plugins:{legend:{display:false}},scales:{x:{beginAtZero:true,ticks:{stepSize:1}}}}});
// By role (schedule compliance)
const roleStats={};
userList.forEach(u=>{
if(u.login==='admin')return;
const q=getQuota(u.role);if(!q.period)return;
const p=getCurrentPeriod(q.period);
const ua=getAudits().filter(a=>a.createdBy===u.login&&new Date(a.date)>=p.start);
if(!roleStats[u.role])roleStats[u.role]={total:0,done:0};
roleStats[u.role].total++;if(ua.length>=q.count)roleStats[u.role].done++;
});
const roleLabels=Object.keys(roleStats);
const ctx5=document.getElementById('chartRoles').getContext('2d');
charts.roles=new Chart(ctx5,{type:'bar',data:{labels:roleLabels,datasets:[{label:'Выполняют',data:roleLabels.map(r=>roleStats[r].done),backgroundColor:'#2D6A4F',borderRadius:6},{label:'Отстают',data:roleLabels.map(r=>roleStats[r].total-roleStats[r].done),backgroundColor:'#E63946',borderRadius:6}]},options:{responsive:true,plugins:{legend:{position:'bottom'}},scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true,ticks:{stepSize:1}}}}});
// Top violation items
const itemCounts={};audits.forEach(a=>{if(a.categories){Object.values(a.categories).forEach(cat=>{if(cat.items){cat.items.forEach(it=>{itemCounts[it.item]=(itemCounts[it.item]||0)+1})})}});
const topItems=Object.entries(itemCounts).sort((a,b)=>b[1]-a[1]).slice(0,10);
const ctx6=document.getElementById('chartTopItems').getContext('2d');
charts.top=new Chart(ctx6,{type:'bar',data:{labels:topItems.map(i=>i[0].length>30?i[0].slice(0,30)+'...':i[0]),datasets:[{label:'Раз',data:topItems.map(i=>i[1]),backgroundColor:topItems.map((_,i)=>['#E63946','#E76F51','#F4A261','#E9C46A','#2A9D8F','#264653','#00B4D8','#0077B6','#023E8A','#6C757D'][i]||'#E63946'),borderRadius:4}]},options:{indexAxis:'y',responsive:true,plugins:{legend:{display:false}},scales:{x:{beginAtZero:true,ticks:{stepSize:1}}}}});
}
// ========== HISTORY ==========
function renderHistory(){
let audits=getAudits();
const fOverall=document.getElementById('filterOverall').value,fDate=document.getElementById('filterDate').value,fLoc=document.getElementById('filterLocation').value.toLowerCase();
if(fOverall==='safe')audits=audits.filter(a=>a.overallSafe);
if(fOverall==='danger')audits=audits.filter(a=>!a.overallSafe);
if(fDate)audits=audits.filter(a=>a.date===fDate);
if(fLoc)audits=audits.filter(a=>a.location.toLowerCase().includes(fLoc));
const tbody=document.getElementById('historyBody'),noData=document.getElementById('noDataRow');
if(audits.length===0){tbody.innerHTML='';noData.style.display='block';return}
noData.style.display='none';
tbody.innerHTML=audits.map(a=>{
const adminBtns=isAdmin()?`<a class="view-link" onclick="editAudit(${a.id})" title="Редактировать">✏️</a><button class="btn btn-danger btn-sm" style="margin-left:6px" onclick="deleteAudit(${a.id})">🗑️</button>`:`<span style="color:var(--gray-500);font-size:11px">только чтение</span>`;
return `<tr><td>${a.number||'—'}</td><td>${a.date}</td><td>${a.timeStart||'—'}${a.timeEnd||'—'}</td><td>${a.location}</td><td>${a.observer}</td><td>${a.workType||'—'}</td><td><span class="badge ${a.overallSafe?'badge-safe':'badge-danger'}">${a.overallSafe?'Безопасно':'Нарушения'}</span></td><td>${a.totalViolations||0}</td><td>${adminBtns}</td></tr>`;
}).join('');
}
function viewAudit(id){const a=getAudits().find(x=>x.id===id);if(!a)return;let text=`БЛАНК ПАБ №${a.number||'—'}\nДата: ${a.date} | Время: ${a.timeStart||'—'}${a.timeEnd||'—'}\nМесто: ${a.location} | Тип: ${a.workType||'—'}\nНаблюдатель: ${a.observer}\nСтатус: ${a.overallSafe?'БЕЗОПАСНО':'НАРУШЕНИЙ: '+a.totalViolations}\n\n`;CATEGORIES.forEach(cat=>{const c=a.categories&&a.categories[cat.id];const it=c?c.items:[];text+=cat.title+': '+(it.length===0?'БЕЗОПАСНО':it.length+' наруш.\n')+it.map(i=>' ☒ '+i.item+(i.other?' — '+i.other:'')).join('\n')+'\n'});alert(text)}
function editAudit(id){
if(!isAdmin()){alert('Только администратор может редактировать');return}
const a=getAudits().find(x=>x.id===id);if(!a)return;editId=id;
document.getElementById('pabNumber').value=a.number||'';document.getElementById('pabDate').value=a.date||'';document.getElementById('pabTimeStart').value=a.timeStart||'';document.getElementById('pabTimeEnd').value=a.timeEnd||'';
document.getElementById('pabLocation').value=a.location||'';document.getElementById('pabRegion').value=a.region||'';document.getElementById('pabWorkType').value=a.workType||'';document.getElementById('pabWorkerCount').value=a.workerCount||1;
document.getElementById('pabObserver').value=a.observer||'';document.getElementById('pabObserverRole').value=a.observerRole||'';
document.getElementById('pabSupervisor').value=a.supervisor||'';document.getElementById('pabSupervisorRole').value=a.supervisorRole||'';
document.getElementById('pabEmail').value=a.email||'';setOverall(a.overallSafe?'safe':'danger');
CATEGORIES.forEach(cat=>{const cd=a.categories&&a.categories[cat.id];const it=cd?cd.items:[];it.forEach(i=>{const idx=cat.items.indexOf(i.item);if(idx>=0){const cb=document.getElementById('cb-'+cat.id+'-'+idx);if(cb){cb.checked=true;updateCheckItemUI(cat.id,idx)};if(i.other){const o=document.getElementById('other-'+cat.id);if(o){o.value=i.other;o.style.display='block';o.classList.add('visible')}}}});const as=it.length===0;document.getElementById('allSafeCb-'+cat.id).checked=as;document.getElementById('allSafeToggle-'+cat.id).classList.toggle('active',as);updateCatTotal(cat.id)});
document.getElementById('vioRows').innerHTML='';
if(a.violations&&a.violations.length>0){vioRowCount=a.violations.length;document.getElementById('vioRows').innerHTML=a.violations.map((v,i)=>makeVioRow(i+1,v)).join('')}else{vioRowCount=1;document.getElementById('vioRows').innerHTML=makeVioRow(1)}
setDialogue(a.dialogue||[]);
switchPanel('newAudit',document.querySelector('[data-panel="newAudit"]'));window.scrollTo({top:0,behavior:'smooth'});
}
function deleteAudit(id){if(!isAdmin()){alert('Только администратор может удалять');return}if(!confirm('Удалить?'))return;saveAudits(getAudits().filter(a=>a.id!==id));renderHistory()}
// ========== CSV ==========
function exportCSV(){
const audits=getAudits();if(audits.length===0){alert('Нет данных');return}
const all=getAllUsers();
const header='Бланк №;Дата;Время;Место;Тип работы;Наблюдатель;Должность;Филиал;Регион;Область;Город;Статус;Нарушений';
const rows=audits.map(a=>{const u=all[a.createdBy]||{};return `${a.number||''};${a.date};${a.timeStart||''}-${a.timeEnd||''};"${a.location}";"${a.workType||''}";"${a.observer}";"${a.observerRole||''}";"${u.branch||''}";"${u.region||''}";"${u.oblast||''}";"${u.city||''}";${a.overallSafe?'Безопасно':'Нарушения'};${a.totalViolations||0}`});
const csv='\uFEFF'+header+'\n'+rows.join('\n');const blob=new Blob([csv],{type:'text/csv;charset=utf-8'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download='pab-audit.csv';a.click();URL.revokeObjectURL(url);
}
// ========== ADMIN DOWNLOADS ==========
function downloadFullCSV(){
const {audits,all}=getDashboardFilters();
if(audits.length===0){alert('Нет данных');return}
const all=getAllUsers();
const header='Бланк №;Дата;Время;Место;Тип работы;Наблюдатель;Должность;Руководитель;Филиал;Подразделение;Регион;Область;Город;Статус;Нарушений;Категории с нарушениями';
const rows=audits.map(a=>{
const u=all[a.createdBy]||{};
const catsWithVio=CATEGORIES.filter(cat=>{const c=a.categories&&a.categories[cat.id];return c&&c.items.length>0}).map(c=>c.title.split('. ')[1]).join(' | ');
return `${a.number||''};${a.date};${a.timeStart||''}-${a.timeEnd||''};"${a.location}";"${a.workType||''}";"${a.observer}";"${a.observerRole||''}";"${a.supervisor||''}";"${u.branch||''}";"${u.dept||''}";"${u.region||''}";"${u.oblast||''}";"${u.city||''}";${a.overallSafe?'Безопасно':'Нарушения'};${a.totalViolations||0};"${catsWithVio}"`;
});
const csv='\uFEFF'+header+'\n'+rows.join('\n');const blob=new Blob([csv],{type:'text/csv;charset=utf-8'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download='pab-full-report.csv';a.click();URL.revokeObjectURL(url);
}
function downloadWorkerReport(){
const {audits}=getDashboardFilters();
const all=getAllUsers();const userList=Object.entries(all).filter(([l])=>l!=='admin');
if(userList.length===0){alert('Нет зарегистрированных работников');return}
const header='Логин;ФИО;Должность;Филиал;Подразделение;Регион;Область;Город;Email;График;Период;Выполнено;Нужно;Статус';
const rows=userList.map(([login,u])=>{
const q=getQuota(u.role);
if(!q.period)return `${login};"${u.name}";"${u.role}";"${u.branch||''}";"${u.dept||''}";"${u.region||''}";"${u.oblast||''}";"${u.city||''}";"${u.email||''}";Без графика;;;0;—`;
const p=getCurrentPeriod(q.period);const done=audits.filter(a=>a.createdBy===login&&new Date(a.date)>=p.start).length;
const status=done>=q.count?'Выполнен':'Отстаёт ('+(q.count-done)+')';
return `${login};"${u.name}";"${u.role}";"${u.branch||''}";"${u.dept||''}";"${u.region||''}";"${u.oblast||''}";"${u.city||''}";"${u.email||''}";"${q.label}";"${p.label}";${done};${q.count};"${status}"`;
});
const csv='\uFEFF'+header+'\n'+rows.join('\n');const blob=new Blob([csv],{type:'text/csv;charset=utf-8'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download='pab-worker-report.csv';a.click();URL.revokeObjectURL(url);
}
function downloadSummaryHTML(){
const {audits}=getDashboardFilters();
const all=getAllUsers();const userList=Object.entries(all).filter(([l])=>l!=='admin');
const total=audits.length;const allSafe=audits.filter(a=>a.overallSafe).length;
const withDanger=audits.filter(a=>!a.overallSafe).length;const totalVio=audits.reduce((s,a)=>s+(a.totalViolations||0),0);
// Schedule stats
let onTrack=0,behind=0,noQuota=0;
userList.forEach(([login,u])=>{const q=getQuota(u.role);if(!q.period){noQuota++;return}const p=getCurrentPeriod(q.period);const done=audits.filter(a=>a.createdBy===login&&new Date(a.date)>=p.start).length;if(done>=q.count)onTrack++;else behind++});
// Category stats
const catStats=CATEGORIES.map(cat=>({name:cat.title,count:audits.reduce((s,a)=>{const c=a.categories&&a.categories[cat.id];return s+(c?c.items.length:0)},0)})).sort((a,b)=>b.count-a.count);
// Region stats
const regStats={};audits.forEach(a=>{const u=all[a.createdBy];const r=u?u.region||'Не указан':'Не указан';if(!regStats[r])regStats[r]={total:0,safe:0,danger:0};regStats[r].total++;if(a.overallSafe)regStats[r].safe++;else regStats[r].danger++});
const regRows=Object.entries(regStats).sort((a,b)=>b[1].total-a[1].total).map(([r,s])=>`<tr><td>${r}</td><td>${s.total}</td><td>${s.safe}</td><td>${s.danger}</td><td>${Math.round(s.safe/s.total*100)}%</td></tr>`).join('');
// Branch stats
const brStats={};audits.forEach(a=>{const u=all[a.createdBy];const b=u?u.branch||'Не указан':'Не указан';brStats[b]=(brStats[b]||0)+1});
const brRows=Object.entries(brStats).sort((a,b)=>b[1]-a[1]).map(([b,c])=>`<tr><td>${b}</td><td>${c}</td></tr>`).join('');
// Top violations
const itemCounts={};audits.forEach(a=>{if(a.categories){Object.values(a.categories).forEach(cat=>{if(cat.items){cat.items.forEach(it=>{itemCounts[it.item]=(itemCounts[it.item]||0)+1})})}});
const topItems=Object.entries(itemCounts).sort((a,b)=>b[1]-a[1]).slice(0,15).map(([i,c])=>`<tr><td>${i}</td><td>${c}</td></tr>`).join('');
// Worker schedule
const workerRows=userList.map(([login,u])=>{
const q=getQuota(u.role);if(!q.period)return`<tr><td>${u.name}</td><td>${u.role}</td><td>${u.branch||'—'}</td><td>${u.dept||'—'}</td><td>${u.region||'—'}</td><td>${u.city||'—'}</td><td>Без графика</td><td>—</td></tr>`;
const p=getCurrentPeriod(q.period);const done=audits.filter(a=>a.createdBy===login&&new Date(a.date)>=p.start).length;
const cls=done>=q.count?'green':'red';return`<tr><td>${u.name}</td><td>${u.role}</td><td>${u.branch||'—'}</td><td>${u.dept||'—'}</td><td>${u.region||'—'}</td><td>${u.city||'—'}</td><td style="color:${cls}">${done}/${q.count} (${q.label})</td><td>${p.label}</td></tr>`;
}).join('');
const html=`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Сводный отчёт ПАБ</title>
<style>body{font:14px/1.5 Arial,sans-serif;max-width:1000px;margin:30px auto;padding:20px;color:#0F1218}
h1{font-size:24px;border-bottom:3px solid #00B4D8;padding-bottom:10px}h2{font-size:18px;margin-top:30px;color:#00B4D8}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:16px 0}
.card{background:#F2F4F7;padding:16px;border-radius:10px;text-align:center}
.card .val{font-size:28px;font-weight:800}.green{color:#2D6A4F}.red{color:#E63946}
table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13px}
th{background:#0F1218;color:#fff;padding:10px 12px;text-align:left}
td{padding:8px 12px;border-bottom:1px solid #E2E6EB}
tr:hover td{background:#F2F4F7}
.footer{margin-top:30px;font-size:12px;color:#5B6573;text-align:center}
@media print{body{margin:0;padding:10px}button{display:none}}</style></head><body>
<button onclick="window.print()" style="padding:10px 24px;font-size:15px;margin-bottom:16px">🖨️ Печать</button>
<h1>📊 Сводный отчёт ПАБ</h1><p>Сформирован: ${new Date().toLocaleString('ru')}</p>
<div class="cards">
<div class="card"><div>Всего аудитов</div><div class="val">${total}</div></div>
<div class="card"><div>Безопасно</div><div class="val green">${allSafe}</div></div>
<div class="card"><div>С нарушениями</div><div class="val red">${withDanger}</div></div>
<div class="card"><div>Нарушений</div><div class="val red">${totalVio}</div></div>
<div class="card"><div>Выполняют график</div><div class="val green">${onTrack}</div></div>
<div class="card"><div>Отстают от графика</div><div class="val red">${behind}</div></div>
</div>
<h2>📂 Нарушения по категориям</h2><table><tr><th>Категория</th><th>Кол-во нарушений</th></tr>${catStats.map(c=>`<tr><td>${c.name}</td><td>${c.count}</td></tr>`).join('')}</table>
<h2>🏢 По регионам</h2><table><tr><th>Регион</th><th>Всего</th><th>Безопасно</th><th>С наруш.</th><th>% Безоп.</th></tr>${regRows}</table>
<h2>🏭 По филиалам</h2><table><tr><th>Филиал</th><th>Аудитов</th></tr>${brRows}</table>
<h2>👥 Выполнение графика по работникам</h2><table><tr><th>ФИО</th><th>Должность</th><th>Филиал</th><th>Подразделение</th><th>Регион</th><th>Город</th><th>Прогресс</th><th>Период</th></tr>${workerRows}</table>
<h2>🔝 Топ-15 нарушений</h2><table><tr><th>Нарушение</th><th>Кол-во</th></tr>${topItems}</table>
<div class="footer">Отчёт системы ПАБ &copy; ${new Date().getFullYear()}</div>
</body></html>`;
const w=window.open('','_blank','width=1000,height=800');w.document.write(html);w.document.close();
}
// ========== VIOLATIONS TRACKING ==========
function renderViolations(){
const all=getAllUsers();let audits=getAudits();
const df=document.getElementById('dfDateFrom')?.value,dt=document.getElementById('dfDateTo')?.value;
if(df)audits=audits.filter(a=>a.date>=df);
if(dt)audits=audits.filter(a=>a.date<=dt);
const fStatus=document.getElementById('vfStatus')?.value||'all';
const fType=document.getElementById('vfType')?.value||'all';
// Collect all violations from all audits
const today=new Date().toISOString().split('T')[0];
let allVios=[];
audits.forEach(a=>{
if(!a.violations)return;
const u=all[a.createdBy]||{};
a.violations.forEach((v,i)=>{
const dueDate=v.date||'';
const done=v.done&&v.done.trim();
let status='pending';
if(done)status='done';
else if(dueDate&&dueDate<today)status='overdue';
allVios.push({...v,status,auditDate:a.date,auditNumber:a.number||'—',auditLocation:a.location,auditId:a.id,auditObserver:a.observer,observerBranch:u.branch||''});
});
});
// Stats
const total=allVios.length;
const doneCount=allVios.filter(v=>v.status==='done').length;
const overdueCount=allVios.filter(v=>v.status==='overdue').length;
const pendingCount=allVios.filter(v=>v.status==='pending').length;
document.getElementById('vTotal').textContent=total;
document.getElementById('vDone').textContent=doneCount;
document.getElementById('vOverdue').textContent=overdueCount;
document.getElementById('vPending').textContent=pendingCount;
// Filter
if(fStatus!=='all')allVios=allVios.filter(v=>v.status===fStatus);
if(fType!=='all')allVios=allVios.filter(v=>v.type===fType);
const tbody=document.getElementById('violationsBody'),nd=document.getElementById('vioNoData');
if(allVios.length===0){tbody.innerHTML='';nd.style.display='block';return}
nd.style.display='none';
tbody.innerHTML=allVios.map((v,i)=>{
const sc=v.status==='done'?'badge-safe':v.status==='overdue'?'badge-danger':'badge-warn';
const sl=v.status==='done'?'Устранено':v.status==='overdue'?'Просрочено':'В работе';
return `<tr>
<td>${i+1}</td><td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escAttr(v.nc)}">${v.nc}</td>
<td>${v.auditDate} (№${v.auditNumber})</td><td>${v.executor}</td><td>${v.type}</td>
<td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escAttr(v.measure)}">${v.measure||'—'}</td>
<td>${v.responsible}</td><td>${v.date||'—'}</td><td>${v.done||'—'}</td>
<td><span class="badge ${sc}">${sl}</span></td>
</tr>`;
}).join('');
}
// ========== ADMIN DATA MANAGEMENT ==========
function clearAllAudits(){
if(!isAdmin())return;
if(!confirm('ВНИМАНИЕ! Это удалит ВСЕ аудиты безвозвратно. Продолжить?'))return;
if(!confirm('Точно удалить все данные?'))return;
localStorage.removeItem('safetyAudits');
alert('Все аудиты удалены.');
renderDashboard();renderHistory();renderViolations();
}
function clearAuditsByDate(){
if(!isAdmin())return;
const from=prompt('Удалить аудиты С даты (ГГГГ-ММ-ДД):','');
if(!from)return;
const to=prompt('Удалить аудиты ПО дату (ГГГГ-ММ-ДД, или оставьте пустым):','');
let audits=getAudits();
const before=audits.length;
audits=audits.filter(a=>{if(a.date<from)return true;if(to&&a.date>to)return true;return false});
if(before===audits.length){alert('Нет аудитов за указанный период.');return}
if(!confirm('Удалить '+ (before-audits.length) +' аудитов за период '+from+(to?' — '+to:'')+'?'))return;
saveAudits(audits);
alert('Удалено '+(before-audits.length)+' аудитов.');
renderDashboard();renderHistory();renderViolations();
}
function showAllUsers(){
if(!isAdmin())return;
const all=getAllUsers();
const userList=Object.entries(all);
let html=`<h2>👥 Зарегистрированные пользователи (${userList.length})</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><th>Область</th><th>Город</th><th>Email</th><th>График</th></tr>`;
userList.forEach(([login,u])=>{
const q=getQuota(u.role);
html+=`<tr style="border-bottom:1px solid #E2E6EB"><td>${login}${login==='admin'?' ⭐':''}</td><td>${u.name}</td><td>${u.role}</td><td>${u.branch||'—'}</td><td>${u.dept||'—'}</td><td>${u.region||'—'}</td><td>${u.oblast||'—'}</td><td>${u.city||'—'}</td><td>${u.email||'—'}</td><td>${q.label}</td></tr>`;
});
html+='</table>';
const w=window.open('','_blank','width=1000,height=600');
w.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Пользователи ПАБ</title><style>body{font:14px/1.5 Arial;max-width:1100px;margin:30px auto;padding:20px}table{width:100%;border-collapse:collapse}td{padding:8px 10px}@media print{body{margin:0;padding:10px}}</style></head><body><button onclick="window.print()" style="padding:8px 20px;font-size:14px;margin-bottom:16px">🖨️ Печать</button>${html}<p style="margin-top:20px;font-size:11px;color:#5B6573">⭐ — предустановленный администратор</p></body></html>`);
w.document.close();
}
// ========== DOWNLOADS WITH DATE RANGE ==========
function getDashboardFilters(){
const all=getAllUsers();
let audits=getAudits();
const fr=document.getElementById('dfRegion')?.value||'all',fo=document.getElementById('dfOblast')?.value||'all',fb=document.getElementById('dfBranch')?.value||'all',fd=document.getElementById('dfDept')?.value||'all',fc=document.getElementById('dfCity')?.value||'all';
const df=document.getElementById('dfDateFrom')?.value,dt=document.getElementById('dfDateTo')?.value;
audits=audits.filter(a=>{const u=all[a.createdBy];if(!u)return true;if(fr!=='all'&&u.region!==fr)return false;if(fo!=='all'&&u.oblast!==fo)return false;if(fb!=='all'&&u.branch!==fb)return false;if(fd!=='all'&&u.dept!==fd)return false;if(fc!=='all'&&u.city!==fc)return false;return true});
if(df)audits=audits.filter(a=>a.date>=df);if(dt)audits=audits.filter(a=>a.date<=dt);
return {audits,all};
}
</script>
</body>
</html>