biot-inspector/checklist.html

861 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Чек-лист проверки | Цифровой инспектор БиОТ</title>
<style>
:root {
--ink: #0F1218;
--cyan: #00E5FF;
--white: #fff;
--gray-500: #5B6573;
--gray-100: #F2F4F7;
--gray-800: #1A1D25;
--gray-700: #252833;
--gray-600: #3A3E4A;
--red: #FF4D4D;
--green: #00C853;
--amber: #FFB300;
--red-bg: rgba(255,77,77,0.08);
--green-bg: rgba(0,200,83,0.08);
--amber-bg: rgba(255,179,0,0.08);
}
*{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: #f0f2f5;
-webkit-font-smoothing: antialiased;
}
/* ===== TOP BAR ===== */
.topbar {
background: var(--ink);
color: var(--white);
padding: 12px 20px;
display: flex;
align-items: center;
gap: 12px;
position: sticky;
top: 0;
z-index: 100;
}
.topbar .back {
color: var(--cyan);
text-decoration: none;
font-size: 20px;
line-height: 1;
}
.topbar .title { font-weight: 700; font-size: 16px; flex: 1 }
.topbar .save {
color: var(--cyan);
font-size: 14px;
font-weight: 600;
text-decoration: none;
}
/* ===== HEADER CARD ===== */
.header-card {
background: var(--white);
margin: 16px 16px 0;
border-radius: 14px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.header-card .field {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid #f0f2f5;
}
.header-card .field:last-child { border-bottom: none }
.header-card .field .label {
font-size: 13px;
color: var(--gray-500);
width: 110px;
flex-shrink: 0;
}
.header-card .field .value {
font-size: 15px;
font-weight: 600;
flex: 1;
}
.header-card .field select, .header-card .field input {
flex: 1;
border: 1px solid #e0e3e8;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
font-family: inherit;
background: #fafbfc;
color: var(--ink);
}
.progress-bar {
margin: 16px 16px 0;
background: var(--white);
border-radius: 14px;
padding: 14px 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
display: flex;
align-items: center;
gap: 12px;
}
.progress-bar .track {
flex: 1;
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.progress-bar .fill {
height: 100%;
background: var(--cyan);
border-radius: 3px;
transition: width 0.3s;
}
.progress-bar .pct {
font-size: 13px;
font-weight: 700;
color: var(--cyan);
min-width: 45px;
text-align: right;
}
/* ===== TABS ===== */
.tabs {
display: flex;
margin: 16px 16px 0;
background: var(--white);
border-radius: 14px;
padding: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
overflow-x: auto;
gap: 2px;
}
.tab {
flex: 1;
min-width: 90px;
padding: 10px 8px;
border: none;
border-radius: 11px;
background: transparent;
font-size: 12px;
font-weight: 600;
color: var(--gray-500);
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
font-family: inherit;
text-align: center;
}
.tab.active {
background: var(--ink);
color: var(--white);
}
.tab .tab-emoji { display: block; font-size: 18px; margin-bottom: 2px }
/* ===== CHECKLIST ===== */
.checklist {
margin: 12px 16px 80px;
display: none;
}
.checklist.active { display: block }
.check-item {
background: var(--white);
border-radius: 12px;
padding: 16px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
transition: box-shadow 0.15s;
}
.check-item.selected { box-shadow: 0 0 0 2px var(--cyan); }
.check-item.has-violation { box-shadow: 0 0 0 1px rgba(255,77,77,0.3); }
.check-item .item-header {
display: flex;
align-items: flex-start;
gap: 10px;
}
.check-item .item-num {
width: 26px;
height: 26px;
background: #f0f2f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
color: var(--gray-500);
flex-shrink: 0;
margin-top: 1px;
}
.check-item .item-text { flex: 1; font-size: 14px; font-weight: 500 }
.check-item .item-note {
margin-top: 4px;
font-size: 12px;
color: var(--gray-500);
}
/* Status buttons */
.status-row {
display: flex;
gap: 8px;
margin-top: 12px;
}
.status-btn {
flex: 1;
padding: 10px 8px;
border: 2px solid #e5e7eb;
border-radius: 10px;
background: var(--white);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
font-family: inherit;
text-align: center;
}
.status-btn:hover { border-color: #d1d5db }
.status-btn.pass.active {
border-color: var(--green);
background: var(--green-bg);
color: var(--green);
}
.status-btn.fail.active {
border-color: var(--red);
background: var(--red-bg);
color: var(--red);
}
.status-btn.na.active {
border-color: var(--gray-500);
background: #f0f2f5;
color: var(--gray-500);
}
/* Violation form */
.violation-form {
display: none;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f0f2f5;
}
.violation-form.show { display: block }
.violation-form .form-row {
margin-bottom: 10px;
}
.violation-form label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--gray-500);
margin-bottom: 4px;
}
.violation-form textarea, .violation-form input, .violation-form select {
width: 100%;
border: 1px solid #e0e3e8;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
font-family: inherit;
background: #fafbfc;
color: var(--ink);
resize: vertical;
}
.violation-form textarea:focus, .violation-form input:focus, .violation-form select:focus {
outline: none;
border-color: var(--cyan);
}
.violation-form .photo-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: 1px dashed #d1d5db;
border-radius: 8px;
background: #fafbfc;
font-size: 13px;
color: var(--gray-500);
cursor: pointer;
font-family: inherit;
}
.violation-form .auto-fill-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: 1px solid rgba(0,229,255,0.3);
border-radius: 8px;
background: rgba(0,229,255,0.06);
font-size: 13px;
font-weight: 600;
color: var(--cyan);
cursor: pointer;
font-family: inherit;
margin-top: 8px;
}
.risk-options { display: flex; gap: 8px }
.risk-opt {
flex: 1;
padding: 8px;
border: 2px solid #e5e7eb;
border-radius: 8px;
text-align: center;
cursor: pointer;
font-size: 12px;
font-weight: 600;
font-family: inherit;
background: var(--white);
transition: all 0.15s;
}
.risk-opt.low.active { border-color: var(--green); background: var(--green-bg); color: var(--green) }
.risk-opt.mid.active { border-color: var(--amber); background: var(--amber-bg); color: var(--amber) }
.risk-opt.high.active { border-color: var(--red); background: var(--red-bg); color: var(--red) }
/* ===== BOTTOM BAR ===== */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--white);
border-top: 1px solid #e5e7eb;
padding: 12px 20px;
display: flex;
gap: 10px;
z-index: 100;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
.bottom-bar .btn {
flex: 1;
padding: 14px;
border: none;
border-radius: 10px;
font-weight: 700;
font-size: 15px;
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
}
.bottom-bar .btn-preview {
background: var(--white);
border: 2px solid var(--ink);
color: var(--ink);
}
.bottom-bar .btn-generate {
background: var(--ink);
color: var(--white);
}
.bottom-bar .btn-generate:hover { background: #2a2d35 }
.bottom-bar .btn-preview:hover { background: #f0f2f5 }
/* ===== MODAL ===== */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
z-index: 200;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-overlay.show { display: flex }
.modal {
background: var(--white);
border-radius: 16px;
padding: 28px;
max-width: 700px;
width: 100%;
max-height: 85vh;
overflow-y: auto;
}
.modal h3 { font-size: 20px; font-weight: 700; margin-bottom: 6px }
.modal .modal-meta { font-size: 13px; color: var(--gray-500); margin-bottom: 20px }
.modal table { width: 100%; border-collapse: collapse; font-size: 13px }
.modal th {
background: #f3f4f6;
padding: 8px 10px;
text-align: left;
font-weight: 600;
font-size: 11px;
color: var(--gray-500);
text-transform: uppercase;
}
.modal td {
padding: 8px 10px;
border-bottom: 1px solid #f3f4f6;
}
.modal .close-btn {
margin-top: 20px;
width: 100%;
padding: 12px;
background: #f0f2f5;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 15px;
cursor: pointer;
font-family: inherit;
}
.risk-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; vertical-align: middle }
.risk-dot.red { background: var(--red) }
.risk-dot.amber { background: var(--amber) }
.modal .empty-msg { text-align: center; color: var(--gray-500); padding: 40px 0; font-size: 15px }
@media (min-width: 768px) {
.checklist-container { max-width: 768px; margin: 0 auto }
}
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: var(--ink);
color: var(--white);
padding: 12px 24px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
z-index: 300;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show { opacity: 1 }
</style>
</head>
<body>
<div class="topbar">
<a href="index.html" class="back">+</a>
<span class="title">Новая проверка</span>
<a href="#" class="save" onclick="saveChecklist()">Сохранить</a>
</div>
<div style="max-width:768px; margin:0 auto">
<!-- Header -->
<div class="header-card">
<div class="field">
<span class="label">Объект</span>
<select><option>Цех №3</option><option>Складской комплекс</option><option>Административный корпус</option><option>Строительная площадка</option></select>
</div>
<div class="field">
<span class="label">Подразделение</span>
<select><option>Производственный участок</option><option>Ремонтная служба</option><option>Энергоцех</option><option>Транспортный цех</option></select>
</div>
<div class="field">
<span class="label">Ответственный</span>
<input type="text" placeholder="ФИО ответственного лица">
</div>
<div class="field">
<span class="label">Направление</span>
<select id="mainDirection"><option>Комплексная проверка</option><option>Охрана труда и ТБ</option><option>Пожарная безопасность</option><option>Электробезопасность</option><option>Транспортная безопасность</option><option>Охрана здоровья</option><option>Выездная проверка</option></select>
</div>
<div class="field">
<span class="label">Дата проверки</span>
<input type="date" value="2026-06-03">
</div>
</div>
<!-- Progress -->
<div class="progress-bar">
<span style="font-size:12px;color:var(--gray-500);white-space:nowrap">Пройдено:</span>
<div class="track"><div class="fill" id="progressFill" style="width:0%"></div></div>
<span class="pct" id="progressPct">0%</span>
</div>
<!-- Tabs -->
<div class="tabs" id="tabBar">
<button class="tab active" data-tab="1"><span class="tab-emoji">&#9888;</span>ОТ и ТБ</button>
<button class="tab" data-tab="2"><span class="tab-emoji">&#128293;</span>Пожарная</button>
<button class="tab" data-tab="3"><span class="tab-emoji">&#9889;</span>Электро</button>
<button class="tab" data-tab="4"><span class="tab-emoji">&#128739;</span>Транспорт</button>
<button class="tab" data-tab="5"><span class="tab-emoji">&#10084;</span>Здоровье</button>
<button class="tab" data-tab="6"><span class="tab-emoji">&#128506;</span>Выездные</button>
</div>
<!-- Checklist sections -->
<div id="checklistContainer"></div>
</div>
<!-- Bottom bar -->
<div class="bottom-bar">
<button class="btn btn-preview" onclick="showPreview()">Предпросмотр</button>
<button class="btn btn-generate" onclick="generateOrder()">Сформировать указание</button>
</div>
<!-- Modal -->
<div class="modal-overlay" id="modalOverlay" onclick="if(event.target===this)closeModal()">
<div class="modal" id="modalContent"></div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
// ===== CHECKLIST DATA =====
const checklistSections = {
1: {
title: 'Охрана труда и ТБ',
items: [
{id:'1.1', text:'Наличие защитных ограждений на вращающихся и движущихся механизмах', note:'Пункт 1.2 Правил ОТ'},
{id:'1.2', text:'Состояние проходов, проездов и эвакуационных путей', note:'Свободны, не загромождены'},
{id:'1.3', text:'Наличие знаков безопасности и сигнальной разметки', note:'ГОСТ 12.4.026'},
{id:'1.4', text:'Применение средств индивидуальной защиты работниками', note:'Каски, очки, перчатки, спецобувь'},
{id:'1.5', text:'Наличие ограждений при работе на высоте (выше 1.3м)', note:'Страховочные системы, перила'},
{id:'1.6', text:'Состояние лестниц, подмостей и средств подмащивания', note:'Исправность, бирки, даты испытаний'},
{id:'1.7', text:'Исправность и своевременное освидетельствование грузоподъемных механизмов', note:'Краны, тельферы, стропы'},
{id:'1.8', text:'Наличие нарядов-допусков на огневые, газоопасные и высотные работы', note:'Оформлены, подписаны, сроки'},
{id:'1.9', text:'Проведение инструктажей (вводный, первичный, повторный, внеплановый)', note:'Записи в журналах, подписи'},
{id:'1.10', text:'Наличие и ведение журналов по охране труда', note:'Журнал инструктажа, журнал выдачи СИЗ, журнал НС'}
]
},
2: {
title: 'Пожарная безопасность',
items: [
{id:'2.1', text:'Наличие и состояние первичных средств пожаротушения (огнетушители)', note:'Не просрочены, опломбированы, доступны'},
{id:'2.2', text:'Доступность пожарных гидрантов и рукавов', note:'Не загромождены, укомплектованы'},
{id:'2.3', text:'Наличие планов эвакуации и указателей выходов', note:'Актуальные, освещённые, на видных местах'},
{id:'2.4', text:'Состояние эвакуационных выходов и путей', note:'Не заперты, свободны, освещены'},
{id:'2.5', text:'Исправность автоматической пожарной сигнализации и оповещения', note:'Датчики дыма, сирены, проверки'},
{id:'2.6', text:'Проведение противопожарных инструктажей', note:'Записи в журнале, подписи, периодичность'},
{id:'2.7', text:'Состояние электропроводки и электрооборудования', note:'Отсутствие скруток, повреждений изоляции'},
{id:'2.8', text:'Наличие мест для курения, оборудованных по нормам', note:'Урны, знаки, удалённость от строений'}
]
},
3: {
title: 'Электробезопасность',
items: [
{id:'3.1', text:'Наличие защитного заземления и зануления оборудования', note:'Визуальная целостность, протоколы замеров'},
{id:'3.2', text:'Состояние распределительных щитов и шкафов', note:'Закрыты, промаркированы, чистота'},
{id:'3.3', text:'Наличие предупреждающих знаков и плакатов на электроустановках', note:'«Осторожно! Электрическое напряжение»'},
{id:'3.4', text:'Наличие и сроки испытания диэлектрических средств защиты', note:'Перчатки, боты, коврики, штампы'},
{id:'3.5', text:'Наличие у персонала группы допуска по электробезопасности', note:'Удостоверения, сроки, соответствие работам'},
{id:'3.6', text:'Состояние изоляции кабелей и проводов', note:'Отсутствие повреждений, провисаний, скруток'},
{id:'3.7', text:'Наличие однолинейных схем электроснабжения', note:'Актуальные, на рабочих местах'}
]
},
4: {
title: 'Транспортная безопасность',
items: [
{id:'4.1', text:'Проведение предрейсовых медицинских осмотров водителей', note:'Журнал, подписи, штампы'},
{id:'4.2', text:'Техническое состояние транспортных средств перед выездом', note:'Тормоза, рулевое, фары, шины'},
{id:'4.3', text:'Наличие и оформление путевых листов', note:'Заполнены, отметки механика и медика'},
{id:'4.4', text:'Наличие допусков на управление спецтехникой', note:'Удостоверения, категории, сроки'},
{id:'4.5', text:'Состояние грузозахватных приспособлений на транспорте', note:'Стропы, цепи, крюки — бирки, испытания'},
{id:'4.6', text:'Проведение инструктажей водителей и механизаторов', note:'Записи, подписи, периодичность'}
]
},
5: {
title: 'Охрана здоровья',
items: [
{id:'5.1', text:'Проведение периодических медицинских осмотров работников', note:'График, заключения, допуски'},
{id:'5.2', text:'Наличие и укомплектованность аптечек первой помощи', note:'Сроки годности, опись, доступность'},
{id:'5.3', text:'Санитарное состояние производственных и бытовых помещений', note:'Чистота, уборка, дезинфекция'},
{id:'5.4', text:'Наличие питьевой воды и условия для приёма пищи', note:'Куллеры, столовая/комната приёма пищи'},
{id:'5.5', text:'Состояние систем вентиляции и кондиционирования', note:'Работают, чистые, проверки'},
{id:'5.6', text:'Освещённость рабочих мест', note:'Нормы, исправность светильников, замеры'},
{id:'5.7', text:'Параметры микроклимата на рабочих местах', note:'Температура, влажность, сквозняки'}
]
},
6: {
title: 'Выездные проверки',
items: [
{id:'6.1', text:'Состояние мест производства работ на выездном объекте', note:'Ограждения, порядок, безопасность'},
{id:'6.2', text:'Наличие ограждений опасных зон на выездных объектах', note:'Котлованы, проёмы, высотные участки'},
{id:'6.3', text:'Безопасность складирования материалов и конструкций', note:'Устойчивость, проходы, высота штабелей'},
{id:'6.4', text:'Наличие первичных средств пожаротушения на выездном объекте', note:'Огнетушители, ящики с песком'},
{id:'6.5', text:'Соблюдение технологии и проекта производства работ', note:'ППР на месте, соответствие выполняемых работ'},
{id:'6.6', text:'Фиксация GPS-координат места проверки', note:'Широта/долгота, фото объекта'}
]
}
};
// ===== STATE =====
const state = {};
Object.keys(checklistSections).forEach(sec => {
checklistSections[sec].items.forEach(item => {
state[item.id] = { status: null, description:'', photo:'', risk:'', deadline:'', requirement:'', measure:'' };
});
});
let currentTab = 1;
// ===== RENDER =====
function renderChecklist(sectionId) {
const sec = checklistSections[sectionId];
let html = '';
sec.items.forEach((item, idx) => {
const st = state[item.id];
const hasViolation = st.status === 'fail';
const selected = st.status !== null;
html += `
<div class="check-item ${selected ? 'selected' : ''} ${hasViolation ? 'has-violation' : ''}" id="item-${item.id}">
<div class="item-header">
<div class="item-num">${idx + 1}</div>
<div>
<div class="item-text">${item.text}</div>
${item.note ? `<div class="item-note">${item.note}</div>` : ''}
</div>
</div>
<div class="status-row">
<button class="status-btn pass ${st.status==='pass'?'active':''}" onclick="setStatus('${item.id}','pass')">Соответствует</button>
<button class="status-btn fail ${st.status==='fail'?'active':''}" onclick="setStatus('${item.id}','fail')">Не соответствует</button>
<button class="status-btn na ${st.status==='na'?'active':''}" onclick="setStatus('${item.id}','na')">Не применяется</button>
</div>
<div class="violation-form ${hasViolation ? 'show' : ''}" id="form-${item.id}">
<div class="form-row">
<label>Описание нарушения</label>
<textarea rows="2" placeholder="Опишите нарушение..." onchange="updateField('${item.id}','description',this.value)">${st.description}</textarea>
</div>
<div class="form-row">
<label>Фото нарушения</label>
<button class="photo-btn" onclick="alert('В боевой версии: камера / галерея')">+ Прикрепить фото</button>
</div>
<div class="form-row">
<label>Категория риска</label>
<div class="risk-options">
<div class="risk-opt low ${st.risk==='low'?'active':''}" onclick="setRisk('${item.id}','low',this)">Низкий</div>
<div class="risk-opt mid ${st.risk==='mid'?'active':''}" onclick="setRisk('${item.id}','mid',this)">Средний</div>
<div class="risk-opt high ${st.risk==='high'?'active':''}" onclick="setRisk('${item.id}','high',this)">Высокий</div>
</div>
</div>
<div class="form-row">
<label>Срок устранения</label>
<input type="date" value="${st.deadline}" onchange="updateField('${item.id}','deadline',this.value)">
</div>
<div class="form-row">
<label>Ответственное лицо</label>
<input type="text" placeholder="Должность и ФИО" value="${st.measure}" onchange="updateField('${item.id}','measure',this.value)">
</div>
<button class="auto-fill-btn" onclick="autoFillViolation('${item.id}','${item.text}')">+ ИИ: автозаполнение</button>
</div>
</div>`;
});
document.getElementById('checklistContainer').innerHTML = html;
}
function setStatus(id, status) {
if (state[id].status === status) {
state[id].status = null;
} else {
state[id].status = status;
}
if (status !== 'fail') {
state[id].description = '';
state[id].risk = '';
state[id].deadline = '';
state[id].measure = '';
}
renderChecklist(currentTab);
updateProgress();
}
function setRisk(id, risk, el) {
state[id].risk = risk;
renderChecklist(currentTab);
}
function updateField(id, field, val) {
state[id][field] = val;
}
function autoFillViolation(id, itemText) {
const autoData = getAutoViolation(itemText);
state[id].description = autoData.description;
state[id].requirement = autoData.requirement;
state[id].measure = autoData.measure;
state[id].risk = autoData.risk;
state[id].deadline = autoData.deadline;
renderChecklist(currentTab);
showToast('+ ИИ: запись сформирована');
}
function getAutoViolation(text) {
// Simulated AI auto-fill based on checklist item
const map = {
'ограждений': { description:'Отсутствует защитное ограждение вращающихся/движущихся механизмов', requirement:'Установить защитное ограждение согласно требованиям правил ОТ', measure:'Начальник участка', risk:'high', deadline:'2026-06-15' },
'СИЗ': { description:'Работники не применяют средства индивидуальной защиты', requirement:'Обеспечить применение СИЗ согласно нормам выдачи', measure:'Мастер участка', risk:'mid', deadline:'2026-06-10' },
'высоте': { description:'Работы на высоте выполняются без страховочной системы', requirement:'Выполнять работы на высоте только с применением страховочной привязи', measure:'Начальник участка', risk:'high', deadline:'2026-06-12' },
'огнетушител': { description:'Огнетушитель не прошёл своевременную перезарядку', requirement:'Выполнить перезарядку огнетушителя', measure:'Руководитель объекта', risk:'mid', deadline:'2026-06-10' },
'эвакуац': { description:'Эвакуационные выходы загромождены / заперты', requirement:'Обеспечить свободный доступ к эвакуационным выходам', measure:'Руководитель объекта', risk:'high', deadline:'2026-06-08' },
'заземл': { description:'Отсутствует или нарушено защитное заземление оборудования', requirement:'Восстановить защитное заземление согласно ПУЭ', measure:'Главный энергетик', risk:'high', deadline:'2026-06-14' },
'проводк': { description:'Выявлены повреждения изоляции электропроводки', requirement:'Заменить повреждённые участки электропроводки', measure:'Электрик участка', risk:'high', deadline:'2026-06-11' },
'медосмотр': { description:'У работников отсутствуют отметки о прохождении медосмотра', requirement:'Организовать прохождение медицинского осмотра', measure:'Специалист по ОТ', risk:'mid', deadline:'2026-06-20' },
'путев': { description:'Путевые листы оформлены с нарушениями / отсутствуют', requirement:'Обеспечить правильное оформление путевых листов', measure:'Механик гаража', risk:'low', deadline:'2026-06-09' },
'инструктаж': { description:'Пропущен срок проведения инструктажа', requirement:'Провести внеплановый инструктаж с записью в журнале', measure:'Мастер участка', risk:'mid', deadline:'2026-06-07' },
'знаков': { description:'Отсутствуют знаки безопасности в установленных местах', requirement:'Установить знаки безопасности согласно ГОСТ', measure:'Начальник участка', risk:'mid', deadline:'2026-06-17' },
'аптеч': { description:'Аптечка не укомплектована / просрочены медикаменты', requirement:'Укомплектовать аптечку согласно нормам', measure:'Специалист по ОТ', risk:'low', deadline:'2026-06-10' },
'вентиляц': { description:'Система вентиляции не обеспечивает нормативный воздухообмен', requirement:'Провести ремонт/очистку системы вентиляции', measure:'Главный механик', risk:'mid', deadline:'2026-06-21' },
'GPS': { description:'Не зафиксированы GPS-координаты места проверки', requirement:'Включить геолокацию и зафиксировать координаты', measure:'Инспектор', risk:'low', deadline:'2026-06-04' }
};
for (const [key, val] of Object.entries(map)) {
if (text.toLowerCase().includes(key.toLowerCase())) return val;
}
return { description:'Нарушение требований безопасности', requirement:'Устранить нарушение в соответствии с нормативными требованиями', measure:'Ответственный руководитель', risk:'mid', deadline:'2026-06-17' };
}
// ===== TAB SWITCHING =====
document.getElementById('tabBar').addEventListener('click', function(e) {
const tab = e.target.closest('.tab');
if (!tab) return;
currentTab = parseInt(tab.dataset.tab);
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
renderChecklist(currentTab);
});
// ===== PROGRESS =====
function updateProgress() {
let total = 0;
let checked = 0;
Object.keys(checklistSections).forEach(sec => {
checklistSections[sec].items.forEach(item => {
total++;
if (state[item.id].status !== null) checked++;
});
});
const pct = total > 0 ? Math.round(checked / total * 100) : 0;
document.getElementById('progressFill').style.width = pct + '%';
document.getElementById('progressPct').textContent = pct + '%';
}
// ===== GET VIOLATIONS =====
function getViolations() {
const violations = [];
Object.keys(checklistSections).forEach(sec => {
checklistSections[sec].items.forEach(item => {
const st = state[item.id];
if (st.status === 'fail') {
const riskLabel = {low:'Низкий', mid:'Средний', high:'Высокий'};
violations.push({
id: item.id,
num: violations.length + 1,
section: checklistSections[sec].title,
text: item.text,
description: st.description || item.text,
risk: st.risk || 'mid',
riskLabel: riskLabel[st.risk] || 'Средний',
deadline: st.deadline || '—',
responsible: st.measure || '—',
measure: (st.description || item.text).length > 80 ? (st.description || item.text) : 'Устранить нарушение согласно нормативным требованиям'
});
}
});
});
return violations;
}
// ===== PREVIEW =====
function showPreview() {
const violations = getViolations();
const objName = document.querySelector('.header-card select').value;
const dept = document.querySelectorAll('.header-card select')[1].value;
const date = document.querySelector('.header-card input[type="date"]').value;
let html = `<h3>Предпросмотр нарушений</h3>`;
html += `<div class="modal-meta">Объект: ${objName} &middot; Подразделение: ${dept} &middot; Дата: ${date}</div>`;
if (violations.length === 0) {
html += `<div class="empty-msg">Нарушений не выявлено</div>`;
} else {
html += `<table>
<tr><th>№</th><th>Нарушение</th><th>Риск</th><th>Срок</th><th>Ответственный</th></tr>`;
violations.forEach(v => {
html += `<tr>
<td>${v.num}</td>
<td>${v.description.length > 80 ? v.description.substring(0,80)+'...' : v.description}</td>
<td><span class="risk-dot ${v.risk==='high'?'red':v.risk==='mid'?'amber':''}"></span>${v.riskLabel}</td>
<td>${v.deadline}</td>
<td>${v.responsible}</td>
</tr>`;
});
html += `</table>`;
}
html += `<button class="close-btn" onclick="closeModal()">Закрыть</button>`;
document.getElementById('modalContent').innerHTML = html;
document.getElementById('modalOverlay').classList.add('show');
}
function generateOrder() {
const violations = getViolations();
if (violations.length === 0) {
showToast('+ Нет нарушений для формирования указания');
return;
}
const objName = document.querySelector('.header-card select').value;
const dept = document.querySelectorAll('.header-card select')[1].value;
const date = document.querySelector('.header-card input[type="date"]').value;
const responsible = document.querySelector('.header-card input[type="text"]').value || '—';
let html = `<h3>Указание по безопасности и охране труда</h3>`;
html += `<div class="modal-meta">
Номер: ${new Date().getTime().toString().slice(-6)} &middot;
Объект: ${objName} &middot;
Подразделение: ${dept} &middot;
Дата: ${date} &middot;
Проверяющий: ${responsible}
</div>`;
html += `<table>
<tr><th>№</th><th>Выявленное нарушение</th><th>Корректирующее мероприятие</th><th>Ответственный</th><th>Срок</th></tr>`;
violations.forEach(v => {
html += `<tr>
<td>${v.num}</td>
<td>${v.description}</td>
<td>${v.measure}</td>
<td>${v.responsible}</td>
<td>${v.deadline}</td>
</tr>`;
});
html += `</table>
<div style="display:flex;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid #e5e7eb;font-size:13px;color:var(--gray-500)">
<span>Подпись проверяющего: ____________</span>
<span>Подпись руководителя: ____________</span>
</div>
<div style="text-align:right;margin-top:12px;font-size:11px;color:var(--gray-500)">
<span style="background:var(--ink);color:var(--white);padding:4px 8px;border-radius:4px;font-family:monospace">QR-код</span>
&nbsp;Проверка подлинности
</div>`;
html += `<button class="close-btn" onclick="closeModal()">Закрыть</button>`;
html += `<button class="btn" style="width:100%;margin-top:8px;background:var(--cyan);color:var(--ink);border:none;padding:12px;border-radius:10px;font-weight:700;cursor:pointer;font-family:inherit" onclick="alert('В боевой версии: экспорт в PDF / Word')">Скачать PDF</button>`;
document.getElementById('modalContent').innerHTML = html;
document.getElementById('modalOverlay').classList.add('show');
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('show');
}
function saveChecklist() {
showToast('+ Чек-лист сохранён');
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2000);
}
// ===== INIT =====
renderChecklist(1);
updateProgress();
</script>
</body>
</html>