safety-audit/index.html

965 lines
46 KiB
HTML
Raw Permalink 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;
--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:center; justify-content:center; min-height:100vh;
background:linear-gradient(135deg, var(--ink) 0%, #1a2332 100%);
}
.login-card{
background:var(--white); border-radius:var(--radius-lg); padding:48px 40px;
width:100%; max-width:440px; box-shadow:0 8px 40px rgba(0,0,0,0.2);
}
.login-card .logo{text-align:center; margin-bottom:32px}
.login-card .logo .icon{font-size:48px; display:block; margin-bottom:8px}
.login-card .logo h1{font-size:22px; font-weight:800; color:var(--ink)}
.login-card .logo p{font-size:14px; color:var(--gray-500); margin-top:4px}
.form-group{margin-bottom:18px}
.form-group label{display:block; font-size:12px; font-weight:700; color:var(--gray-500);
margin-bottom:5px; text-transform:uppercase; letter-spacing:0.5px}
.form-group input,.form-group select,.form-group textarea{
width:100%; padding:10px 12px; border:2px solid var(--gray-200); border-radius:var(--radius);
font-size:14px; 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}
.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:13px; text-align:center; margin-top:12px; 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:1100px; margin:0 auto; padding:28px 24px}
.panel{display:none}
.panel.active{display:block}
/* ===== PAGE HEADER ===== */
.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}
/* ===== AUDIT FORM ===== */
.audit-form{max-width:900px}
/* Header block */
.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 safe/danger toggle */
.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)}
/* Category section */
.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 */
.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}
/* Category footer */
.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}
/* Violation table */
.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:14px 18px; 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)}
.charts-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:16px; margin-bottom:24px}
.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}
/* ===== HISTORY ===== */
.table-filters{display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap}
.table-filters select,.table-filters 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);
}
.table-filters select:focus,.table-filters input:focus{border-color:var(--cyan)}
.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)}
.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 */
.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:12px}
.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}
.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="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 class="btn btn-primary btn-block" onclick="doLogin()">Войти</button>
<div id="loginError" class="login-error">Неверный логин или пароль</div>
<p style="text-align:center;margin-top:14px;font-size:11px;color:var(--gray-500)">
Демо: <b>admin / admin</b> &nbsp;|&nbsp; <b>auditor / auditor</b> &nbsp;|&nbsp; <b>ivanov / 1234</b>
</p>
</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="dashboard" onclick="switchPanel('dashboard',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">
<!-- ============ NEW AUDIT ============ -->
<div id="panelNewAudit" class="panel active">
<div class="page-header">
<h2>📋 Бланк поведенческого аудита безопасности</h2>
<p>Заполните все категории наблюдения</p>
</div>
<div class="audit-form" id="auditForm">
<!-- HEADER -->
<div class="form-header">
<h3>📝 Данные аудита</h3>
<div class="header-grid col4">
<div class="fg"><label>Бланк ПАБ №</label><input id="pabNumber" placeholder="Номер"></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><input type="number" id="pabWorkerCount" min="1" value="1"></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="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>
<!-- CATEGORY SECTIONS generated by JS -->
<div id="categorySections"></div>
<!-- VIOLATIONS TABLE -->
<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>
<!-- SAVE -->
<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>
</div>
</div>
<!-- ============ DASHBOARD ============ -->
<div id="panelDashboard" 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="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>
<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="chartObservers"></canvas></div>
<div class="chart-card"><h3>🔝 Топ-10 нарушений</h3><canvas id="chartTopItems"></canvas></div>
</div>
</div>
<!-- ============ HISTORY ============ -->
<div id="panelHistory" class="panel">
<div class="page-header">
<h2>📁 История аудитов</h2>
<p>Архив всех проведённых ПАБ</p>
</div>
<div class="table-filters">
<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>
<!-- ==================== DATA ==================== -->
<script>
// ========== CATEGORIES DEFINITION (matching Word doc) ==========
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:[
'Ремни безопасности отсутствуют, неисправны или не используются',
'Опасный стиль вождения (резкий разгон/торможение, опасное маневрирование, создание аварийной ситуации)',
'Состояние водителя не соответствует требованиям',
'Использование мобильного средства связи во время движения',
'Несоблюдение правил дорожного движения (Скоростной режим, несоблюдение знаков и дорожной разметки)',
'Состояние транспортного средства не соответствует требованиям безопасности','Другое'
]
}
];
// ========== USERS ==========
const USERS = {
admin:{pass:'admin',name:'Администратор',role:'Руководитель'},
auditor:{pass:'auditor',name:'Петров П.П.',role:'Аудитор'},
ivanov:{pass:'1234',name:'Иванов И.И.',role:'Бригадир'}
};
// ========== STATE ==========
let currentUser=null,currentPanel='newAudit',editId=null,charts={};
// ========== INIT ==========
function init(){
document.getElementById('pabDate').value=new Date().toISOString().split('T')[0];
buildCategorySections();
initVioRows();
if(localStorage.getItem('safetyAuditUser')){
currentUser=JSON.parse(localStorage.getItem('safetyAuditUser'));
document.getElementById('pabObserver').value=currentUser.name;
showApp();
}
document.getElementById('loginUser').addEventListener('keydown',function(e){if(e.key==='Enter')doLogin();});
document.getElementById('loginPass').addEventListener('keydown',function(e){if(e.key==='Enter')doLogin();});
}
init();
// ========== BUILD FORM SECTIONS ==========
function buildCategorySections(){
const container=document.getElementById('categorySections');
let html='';
CATEGORIES.forEach((cat,idx)=>{
const colClass=cat.items.length>7?'col3':(cat.items.length<=4?'col1':'');
html+=`
<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>`;
});
container.innerHTML=html;
}
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);
const toggle=document.getElementById('allSafeToggle-'+catId);
const isAllSafe=!cb.checked;
cb.checked=isAllSafe;
if(isAllSafe){
toggle.classList.add('active');
CATEGORIES.find(c=>c.id===catId).items.forEach((_,i)=>{
const el=document.getElementById('cb-'+catId+'-'+i);
if(el){el.checked=false; updateCheckItemUI(catId,i);}
const other=document.getElementById('other-'+catId);
if(other)other.style.display='none';
});
}else{
toggle.classList.remove('active');
}
updateCatTotal(catId);
}
function onCheckItem(catId,idx){
const cb=document.getElementById('cb-'+catId+'-'+idx);
updateCheckItemUI(catId,idx);
// Uncheck all-safe if any item checked
if(cb.checked){
document.getElementById('allSafeCb-'+catId).checked=false;
document.getElementById('allSafeToggle-'+catId).classList.remove('active');
}
// Show/hide "other" input
const cat=CATEGORIES.find(c=>c.id===catId);
if(cat&&cat.items[idx]==='Другое'){
const other=document.getElementById('other-'+catId);
if(other){other.style.display=cb.checked?'block':'none'; other.classList.toggle('visible',cb.checked);}
}
updateCatTotal(catId);
}
function updateCheckItemUI(catId,idx){
const el=document.getElementById('item-'+catId+'-'+idx);
const 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 totalEl=document.getElementById('total-'+catId);
totalEl.textContent=count;
totalEl.classList.toggle('zero',count===0);
const badge=document.getElementById('badge-'+catId);
if(count===0){badge.textContent='ВСЕ БЕЗОПАСНО'; badge.classList.add('all-safe');}
else{badge.textContent='Нарушений: '+count; badge.classList.remove('all-safe');}
}
function setOverall(type){
document.getElementById('overallSafe').classList.toggle('selected',type==='safe');
document.getElementById('overallDanger').classList.toggle('selected',type==='danger');
}
// ========== VIOLATIONS TABLE ==========
let vioRowCount=6;
function initVioRows(){
const container=document.getElementById('vioRows');
let html='';
for(let i=0;i<vioRowCount;i++){
html+=makeVioRow(i+1);
}
container.innerHTML=html;
}
function makeVioRow(num){
return `<div class="vio-grid" id="vioRow${num}" style="display:grid">
<span class="vio-row-num">${num}</span>
<input placeholder="Описание несоответствия" class="v-nc">
<input placeholder="Исполнитель" class="v-exec">
<select class="v-type"><option>Нарушение</option><option>Замечание</option><option>Риск</option></select>
<input placeholder="Корректирующие меры" class="v-measure">
<input placeholder="Ответственный" class="v-resp">
<input type="date" class="v-date">
<input placeholder="Форма завершения" class="v-done">
<button class="remove-vio-btn" onclick="removeVioRow(${num})" title="Удалить">×</button>
</div>`;
}
function addVioRow(){
vioRowCount++;
const row=makeVioRow(vioRowCount);
document.getElementById('vioRows').insertAdjacentHTML('beforeend',row);
}
function removeVioRow(num){
if(vioRowCount<=1)return;
document.getElementById('vioRow'+num)?.remove();
// Reindex
document.querySelectorAll('#vioRows .vio-grid').forEach((row,i)=>{
row.id='vioRow'+(i+1);
row.querySelector('.vio-row-num').textContent=i+1;
row.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(row=>{
const nc=row.querySelector('.v-nc')?.value?.trim();
if(!nc)return;
rows.push({
nc:nc,
executor:row.querySelector('.v-exec')?.value?.trim()||'',
type:row.querySelector('.v-type')?.value||'Нарушение',
measure:row.querySelector('.v-measure')?.value?.trim()||'',
responsible:row.querySelector('.v-resp')?.value?.trim()||'',
date:row.querySelector('.v-date')?.value||'',
done:row.querySelector('.v-done')?.value?.trim()||''
});
});
return rows;
}
// ========== AUDIT SUBMIT ==========
function submitAudit(){
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 otherVal=item==='Другое'?document.getElementById('other-'+cat.id)?.value?.trim()||'':null;
checked.push({item,other:otherVal});
}
});
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:location,
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(),
overallSafe:overallSafe,
categories:cats,
totalViolations:totalViolations,
violations:getVioRows(),
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();
const s=document.getElementById('formSuccess');
s.style.display='block';
setTimeout(()=>s.style.display='none',3000);
window.scrollTo({top:0,behavior:'smooth'});
}
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('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='';
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 other=document.getElementById('other-'+cat.id);
if(other){other.value='';other.style.display='none';other.classList.remove('visible');}
});
updateCatTotal(cat.id);
});
document.getElementById('vioRows').innerHTML='';
vioRowCount=6;
initVioRows();
document.getElementById('formSuccess').style.display='none';
}
// ========== DATA STORAGE ==========
function getAudits(){try{return JSON.parse(localStorage.getItem('safetyAudits')||'[]')}catch(e){return[]}}
function saveAudits(data){localStorage.setItem('safetyAudits',JSON.stringify(data))}
// ========== LOGIN ==========
function doLogin(){
const u=document.getElementById('loginUser').value.trim().toLowerCase();
const p=document.getElementById('loginPass').value.trim();
const err=document.getElementById('loginError');
if(!USERS[u]||USERS[u].pass!==p){err.style.display='block';return;}
err.style.display='none';
currentUser={login:u,...USERS[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';
document.getElementById('loginUser').value='';document.getElementById('loginPass').value='';
}
function showApp(){
document.getElementById('loginScreen').style.display='none';
document.getElementById('appScreen').style.display='block';
document.getElementById('displayName').textContent=currentUser.login+' ('+currentUser.role+')';
switchPanel('newAudit',document.querySelector('[data-panel="newAudit"]'));
}
// ========== 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();
}
// ========== DASHBOARD ==========
function renderDashboard(){
const audits=getAudits();
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);
document.getElementById('statTotal').textContent=total;
document.getElementById('statAllSafe').textContent=allSafe;
document.getElementById('statWithDanger').textContent=withDanger;
document.getElementById('statViolations').textContent=totalVio;
const sp=total>0?(allSafe/total*100):50;
const 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={};
// By category
const catLabels=CATEGORIES.map(c=>c.title.split('. ')[1]);
const catCounts=CATEGORIES.map(cat=>{
return audits.reduce((s,a)=>{
const c=a.categories&&a.categories[cat.id];
return s+(c?c.items.length:0);
},0);
});
const ctx1=document.getElementById('chartCategories').getContext('2d');
charts.cat=new Chart(ctx1,{
type:'bar',data:{labels:catLabels,datasets:[{label:'Нарушений',data:catCounts,backgroundColor:'#E63946',borderRadius:6}]},
options:{responsive:true,plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,ticks:{stepSize:1}}}}
});
// Timeline
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}}}}
});
// Top observers
const obs={};
audits.forEach(a=>{obs[a.observer]=(obs[a.observer]||0)+1});
const obsS=Object.entries(obs).sort((a,b)=>b[1]-a[1]).slice(0,5);
const ctx3=document.getElementById('chartObservers').getContext('2d');
charts.obs=new Chart(ctx3,{
type:'bar',data:{labels:obsS.map(o=>o[0]),datasets:[{label:'Аудитов',data:obsS.map(o=>o[1]),backgroundColor:['#00B4D8','#48CAE4','#90E0EF','#0077B6','#023E8A'],borderRadius:6}]},
options:{indexAxis:'y',responsive:true,plugins:{legend:{display:false}},scales:{x:{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=>{
const key=it.item;
itemCounts[key]=(itemCounts[key]||0)+1;
});
}
});
}
});
const topItems=Object.entries(itemCounts).sort((a,b)=>b[1]-a[1]).slice(0,10);
const ctx4=document.getElementById('chartTopItems').getContext('2d');
charts.top=new Chart(ctx4,{
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;
const fDate=document.getElementById('filterDate').value;
const 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');
const noData=document.getElementById('noDataRow');
if(audits.length===0){tbody.innerHTML='';noData.style.display='block';return;}
noData.style.display='none';
tbody.innerHTML=audits.map(a=>`
<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>
<a class="view-link" onclick="viewAudit(${a.id})">👁️</a>
<button class="btn btn-danger btn-sm" style="margin-left:6px" onclick="deleteAudit(${a.id})">🗑️</button>
</td>
</tr>
`).join('');
}
function viewAudit(id){
const a=getAudits().find(x=>x.id===id);
if(!a)return;
let text=`БЛАНК ПАБ №${a.number||'—'}\n`;
text+=`Дата: ${a.date} | Время: ${a.timeStart||'—'}${a.timeEnd||'—'}\n`;
text+=`Место: ${a.location} | Тип работы: ${a.workType||'—'}\n`;
text+=`Наблюдатель: ${a.observer} (${a.observerRole||'—'})\n`;
text+=`Руководитель: ${a.supervisor||'—'} (${a.supervisorRole||'—'})\n`;
text+=`Статус: ${a.overallSafe?'ВСЕ БЕЗОПАСНО':'ЕСТЬ ОПАСНО'}\n`;
text+=`Всего нарушений: ${a.totalViolations||0}\n\n`;
text+=`=== КАТЕГОРИИ ===\n`;
CATEGORIES.forEach(cat=>{
const cdata=a.categories&&a.categories[cat.id];
const items=cdata?cdata.items:[];
text+=`\n${cat.title}: ${items.length===0?'ВСЕ БЕЗОПАСНО':items.length+' наруш.'}\n`;
items.forEach(it=>text+=`${it.item}${it.other?' — '+it.other:''}\n`);
});
if(a.violations&&a.violations.length>0){
text+=`\n\n=== НЕСООТВЕТСТВИЯ ===\n`;
a.violations.forEach((v,i)=>text+=`${i+1}. ${v.nc} | Исп: ${v.executor} | Меры: ${v.measure} | Отв: ${v.responsible}\n`);
}
alert(text);
}
function deleteAudit(id){
if(!confirm('Удалить этот аудит?'))return;
saveAudits(getAudits().filter(a=>a.id!==id));
renderHistory();
}
// ========== EXPORT CSV ==========
function exportCSV(){
const audits=getAudits();
if(audits.length===0){alert('Нет данных');return;}
const header='Бланк №;Дата;Время;Место;Тип работы;Наблюдатель;Должность;Руководитель;Должность;Статус;Нарушений всего;Категории с нарушениями';
const rows=audits.map(a=>{
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||''}";"${a.supervisorRole||''}";${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-audit.csv';a.click();
URL.revokeObjectURL(url);
}
</script>
</body>
</html>