965 lines
46 KiB
HTML
965 lines
46 KiB
HTML
<!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> | <b>auditor / auditor</b> | <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>
|