v3: interactive checklist page - 6 directions, 42 check items, AI auto-fill, violation form, document preview

This commit is contained in:
sayat_aydarbaev 2026-06-03 12:41:35 +00:00
parent c53ff4fbb8
commit 8366052d0c
2 changed files with 861 additions and 1 deletions

860
checklist.html Normal file
View File

@ -0,0 +1,860 @@
<!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>

View File

@ -509,7 +509,7 @@ body {
</div>
<div class="cta-row">
<a href="#waitlist" class="btn">Оставить заявку</a>
<a href="checklist.html" class="btn">Открыть чек-лист</a>
<a href="#process" class="btn-outline">Как работает</a>
</div>