1158 lines
96 KiB
HTML
1158 lines
96 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;
|
||
--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">Войти</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&¤tUser.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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>')}
|
||
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(){
|
||
const u=document.getElementById('loginUser').value.trim().toLowerCase();
|
||
const p=document.getElementById('loginPass').value.trim();const err=document.getElementById('loginError');
|
||
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">Отчёт системы ПАБ © ${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>
|