ot-tb-control/index.html

648 lines
30 KiB
HTML
Raw 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;--cyan-50:#E8FCFF;
--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--gray-200:#E4E7EC;
--red:#EF4444;--amber:#F59E0B;--green:#10B981;
--red-bg:#FEF2F2;--amber-bg:#FFFBEB;--green-bg:#ECFDF5;
--shadow:0 1px 3px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.06);
--radius:12px;--radius-sm:8px;
}
*{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;
}
/* Header */
.header{background:var(--white);border-bottom:1px solid var(--gray-200);position:sticky;top:0;z-index:100}
.header-inner{max-width:1200px;margin:0 auto;padding:0 24px;display:flex;align-items:center;justify-content:space-between;height:60px}
.header h1{font-size:18px;font-weight:700}
.header-badge{font-size:12px;background:var(--cyan-50);color:var(--ink);padding:4px 10px;border-radius:20px;font-weight:600}
/* Tabs */
.tabs{background:var(--white);border-bottom:1px solid var(--gray-200);position:sticky;top:60px;z-index:99}
.tabs-inner{max-width:1200px;margin:0 auto;padding:0 24px;display:flex;gap:0}
.tab{
padding:14px 20px;font-size:14px;font-weight:500;color:var(--gray-500);
border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;
transition:all .15s;white-space:nowrap;
}
.tab:hover{color:var(--ink)}
.tab.active{color:var(--ink);border-bottom-color:var(--cyan);font-weight:600}
/* Main */
.main{max-width:1200px;margin:0 auto;padding:32px 24px}
/* Tab content */
.tab-panel{display:none}
.tab-panel.active{display:block}
/* Dashboard cards */
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:24px}
.stat-card{background:var(--white);border-radius:var(--radius);padding:20px 24px;box-shadow:var(--shadow)}
.stat-card .stat-label{font-size:13px;color:var(--gray-500);margin-bottom:4px}
.stat-card .stat-value{font-size:32px;font-weight:800;line-height:1}
.stat-card .stat-icon{font-size:28px;margin-bottom:8px}
.stat-red .stat-value{color:var(--red)}
.stat-amber .stat-value{color:var(--amber)}
.stat-green .stat-value{color:var(--green)}
/* Chart bars */
.chart-card{background:var(--white);border-radius:var(--radius);padding:24px;box-shadow:var(--shadow);margin-bottom:24px}
.chart-card h3{font-size:16px;font-weight:700;margin-bottom:16px}
.chart-bar-row{display:flex;align-items:center;gap:12px;margin-bottom:10px}
.chart-bar-label{width:240px;font-size:13px;flex-shrink:0;text-align:right}
.chart-bar-track{flex:1;background:var(--gray-100);border-radius:4px;height:22px;overflow:hidden}
.chart-bar-fill{height:100%;border-radius:4px;transition:width .4s;min-width:2px}
.chart-bar-count{font-size:13px;font-weight:600;width:32px;text-align:right;flex-shrink:0}
/* Section titles */
.section-title{font-size:18px;font-weight:700;margin-bottom:16px;display:flex;align-items:center;gap:8px}
/* Table */
.table-wrap{background:var(--white);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
table{width:100%;border-collapse:collapse;font-size:13px}
th{text-align:left;padding:12px 16px;background:var(--gray-100);font-weight:600;font-size:12px;color:var(--gray-500);text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--gray-200)}
td{padding:12px 16px;border-bottom:1px solid var(--gray-100)}
tr:last-child td{border-bottom:none}
tr:hover td{background:var(--cyan-50)}
/* Badges */
.badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:12px;font-weight:600;line-height:1.5}
.badge-red{background:var(--red-bg);color:var(--red)}
.badge-amber{background:var(--amber-bg);color:var(--amber)}
.badge-green{background:var(--green-bg);color:var(--green)}
.badge-type{background:var(--gray-100);color:var(--ink)}
/* Buttons */
.btn{
display:inline-flex;align-items:center;gap:6px;padding:8px 16px;
border-radius:var(--radius-sm);font-size:13px;font-weight:600;
border:none;cursor:pointer;transition:all .15s;line-height:1.5;
}
.btn-primary{background:var(--cyan);color:var(--ink)}
.btn-primary:hover{background:#1be5ff}
.btn-outline{background:transparent;color:var(--ink);border:1.5px solid var(--gray-200)}
.btn-outline:hover{border-color:var(--ink)}
.btn-sm{padding:4px 10px;font-size:12px}
.btn-danger{background:var(--red-bg);color:var(--red);border:1px solid var(--red-bg)}
.btn-danger:hover{background:var(--red);color:#fff}
.btn-success{background:var(--green-bg);color:var(--green);border:1px solid var(--green-bg)}
.btn-success:hover{background:var(--green);color:#fff}
.btn-group{display:flex;gap:6px;flex-wrap:wrap}
/* Toolbar */
.toolbar{display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center}
.toolbar select,.toolbar input{
padding:8px 12px;border:1.5px solid var(--gray-200);border-radius:var(--radius-sm);
font-size:13px;background:var(--white);color:var(--ink);
}
.toolbar select:focus,.toolbar input:focus{outline:none;border-color:var(--cyan)}
.toolbar input{min-width:200px}
/* Form */
.form-card{background:var(--white);border-radius:var(--radius);padding:24px 28px;box-shadow:var(--shadow);max-width:640px}
.form-group{margin-bottom:16px}
.form-group label{display:block;font-size:13px;font-weight:600;margin-bottom:4px}
.form-group input,.form-group select,.form-group textarea{
width:100%;padding:10px 12px;border:1.5px solid var(--gray-200);
border-radius:var(--radius-sm);font-size:14px;color:var(--ink);font-family:inherit;
}
.form-group textarea{resize:vertical;min-height:80px}
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{
outline:none;border-color:var(--cyan)
}
/* Modal */
.modal-overlay{
position:fixed;inset:0;background:rgba(15,18,24,.5);z-index:200;
display:flex;align-items:center;justify-content:center;padding:24px;
}
.modal{
background:var(--white);border-radius:var(--radius);padding:28px;
max-width:560px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.15);
}
.modal h3{font-size:18px;font-weight:700;margin-bottom:16px}
.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:20px}
/* Empty state */
.empty{text-align:center;padding:40px 20px;color:var(--gray-500);font-size:14px}
/* Pinned recent */
.recent-card{background:var(--white);border-radius:var(--radius);padding:24px;box-shadow:var(--shadow)}
.recent-card h3{font-size:16px;font-weight:700;margin-bottom:12px}
/* Responsive */
@media (max-width:768px){
.header h1{font-size:16px}
.tab{padding:12px 14px;font-size:13px}
.stats-row{grid-template-columns:1fr 1fr}
.chart-bar-label{width:140px;font-size:11px}
.toolbar{flex-direction:column}
.toolbar select,.toolbar input{width:100%}
table{font-size:12px}
th,td{padding:8px 10px}
}
.plan-status-completed{text-decoration:line-through;opacity:.6}
.plan-actions{display:flex;gap:4px}
</style>
</head>
<body>
<header class="header">
<div class="header-inner">
<h1>Охрана труда и ТБ</h1>
<span class="header-badge">Внутренний учёт</span>
</div>
</header>
<nav class="tabs">
<div class="tabs-inner" id="tabNav">
<button class="tab active" data-tab="dashboard">Сводка</button>
<button class="tab" data-tab="plan">План проверок</button>
<button class="tab" data-tab="journal">Журнал нарушений</button>
<button class="tab" data-tab="add">Добавить нарушение</button>
</div>
</nav>
<main class="main">
<!-- DASHBOARD -->
<section class="tab-panel active" id="tab-dashboard">
<div class="stats-row" id="statsRow"></div>
<div class="chart-card" id="chartCard"></div>
<div class="recent-card" id="recentCard"></div>
</section>
<!-- PLAN -->
<section class="tab-panel" id="tab-plan">
<div class="toolbar">
<button class="btn btn-primary" onclick="togglePlanForm()">+ Добавить пункт плана</button>
</div>
<div class="form-card" id="planFormCard" style="display:none;margin-bottom:20px">
<h3 id="planFormTitle">Новый пункт плана</h3>
<div class="form-group"><label>Название</label><input id="planName" placeholder="Например: Еженедельный обход цеха"></div>
<div class="form-group"><label>Периодичность</label>
<select id="planFreq">
<option value="daily">Ежедневно</option>
<option value="weekly">Еженедельно</option>
<option value="monthly">Ежемесячно</option>
<option value="quarterly">Ежеквартально</option>
<option value="once">Разово</option>
</select>
</div>
<div class="form-group"><label>Описание</label><textarea id="planDesc" rows="2"></textarea></div>
<div class="form-group"><label>Ответственный</label><input id="planResp" placeholder="ФИО"></div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="togglePlanForm()">Отмена</button>
<button class="btn btn-primary" id="planSaveBtn" onclick="savePlan()">Сохранить</button>
</div>
</div>
<div class="table-wrap" id="planTable"></div>
</section>
<!-- JOURNAL -->
<section class="tab-panel" id="tab-journal">
<div class="toolbar">
<select id="filterType" onchange="renderJournal()">
<option value="all">Все виды нарушений</option>
</select>
<select id="filterStatus" onchange="renderJournal()">
<option value="all">Все статусы</option>
<option value="detected">Выявлено</option>
<option value="in_progress">В работе</option>
<option value="resolved">Устранено</option>
</select>
<input type="text" id="filterSearch" placeholder="Поиск по описанию..." oninput="renderJournal()">
</div>
<div class="table-wrap" id="journalTable"></div>
</section>
<!-- ADD -->
<section class="tab-panel" id="tab-add">
<div class="form-card">
<h3 style="margin-bottom:16px">Новое нарушение</h3>
<div class="form-group"><label>Дата выявления</label><input type="date" id="vioDate"></div>
<div class="form-group"><label>Место / участок</label><input id="vioLocation" placeholder="Цех №2, склад..."></div>
<div class="form-group"><label>Вид нарушения</label><select id="vioType"></select></div>
<div class="form-group"><label>Описание</label><textarea id="vioDesc" rows="3" placeholder="Опишите нарушение..."></textarea></div>
<div class="form-group"><label>Ответственный за устранение</label><input id="vioResp" placeholder="ФИО"></div>
<div class="form-group"><label>Срок устранения</label><input type="date" id="vioDeadline"></div>
<div class="form-group"><label>Связать с пунктом плана</label><select id="vioPlan"><option value="">— не привязано —</option></select></div>
<button class="btn btn-primary" onclick="addViolation()" style="margin-top:8px">Сохранить нарушение</button>
</div>
</section>
</main>
<!-- MODAL for editing -->
<div class="modal-overlay" id="editModal" style="display:none" onclick="if(event.target===this)closeEditModal()">
<div class="modal" id="editModalContent"></div>
</div>
<script>
/* ========== DATA LAYER ========== */
const VIOLATION_TYPES = [
'Несоблюдение требований безопасности',
'Отсутствие / неисправность СИЗ',
'Нарушение правил эксплуатации оборудования',
'Отсутствие инструктажа / обучения',
'Несоблюдение пожарной безопасности',
'Нарушение электробезопасности',
'Отсутствие ограждений / знаков безопасности',
'Захламлённость проходов и выходов',
'Нарушение правил работы на высоте',
'Другое'
];
const STATUS_LABELS = {
detected: 'Выявлено',
in_progress: 'В работе',
resolved: 'Устранено'
};
const BAR_COLORS = ['#00E5FF','#0EA5E9','#6366F1','#8B5CF6','#EC4899','#F43F5E','#F97316','#EAB308','#22C55E','#14B8A6'];
function load(key, fallback) {
try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; }
catch { return fallback; }
}
function save(key, val) { localStorage.setItem(key, JSON.stringify(val)); }
let violations = load('violations', []);
let planItems = load('planItems', []);
let editingPlanId = null;
let editingViolationId = null;
if (!planItems.length) {
planItems = [
{ id: 'p1', name: 'Ежедневный обход территории', freq: 'daily', desc: 'Осмотр рабочих мест, проходов, средств пожаротушения', resp: 'Начальник смены', completed: false },
{ id: 'p2', name: 'Еженедельная проверка СИЗ', freq: 'weekly', desc: 'Проверка наличия и состояния средств индивидуальной защиты', resp: 'Специалист по ОТ', completed: false },
{ id: 'p3', name: 'Ежемесячный аудит безопасности', freq: 'monthly', desc: 'Полный аудит соблюдения норм охраны труда', resp: 'Инженер по ОТ и ТБ', completed: false },
{ id: 'p4', name: 'Проверка электроустановок', freq: 'quarterly', desc: 'Осмотр электрощитов, заземления, изоляции', resp: 'Главный энергетик', completed: false },
{ id: 'p5', name: 'Инструктаж новых сотрудников', freq: 'once', desc: 'Вводный инструктаж по охране труда для новых сотрудников', resp: 'Специалист по ОТ', completed: false }
];
save('planItems', planItems);
}
function genId() { return 'v' + Date.now() + '_' + Math.random().toString(36).slice(2,7); }
/* ========== NAVIGATION ========== */
document.getElementById('tabNav').addEventListener('click', function(e) {
if (!e.target.classList.contains('tab')) return;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + e.target.dataset.tab).classList.add('active');
switchTab(e.target.dataset.tab);
});
function switchTab(name) {
if (name === 'dashboard') renderDashboard();
else if (name === 'plan') renderPlan();
else if (name === 'journal') renderJournal();
else if (name === 'add') prepareAddForm();
}
/* ========== DASHBOARD ========== */
function renderDashboard() {
const total = violations.length;
const detected = violations.filter(v => v.status === 'detected').length;
const inProgress = violations.filter(v => v.status === 'in_progress').length;
const resolved = violations.filter(v => v.status === 'resolved').length;
document.getElementById('statsRow').innerHTML = `
<div class="stat-card"><div class="stat-icon">📋</div><div class="stat-label">Всего нарушений</div><div class="stat-value">${total}</div></div>
<div class="stat-card stat-red"><div class="stat-icon">🔴</div><div class="stat-label">Выявлено</div><div class="stat-value">${detected}</div></div>
<div class="stat-card stat-amber"><div class="stat-icon">🟡</div><div class="stat-label">В работе</div><div class="stat-value">${inProgress}</div></div>
<div class="stat-card stat-green"><div class="stat-icon">🟢</div><div class="stat-label">Устранено</div><div class="stat-value">${resolved}</div></div>
`;
const typeStats = {};
VIOLATION_TYPES.forEach(t => { typeStats[t] = 0; });
violations.forEach(v => { if (typeStats[v.type] != null) typeStats[v.type]++; });
const maxCount = Math.max(1, ...Object.values(typeStats));
let chartHtml = '<h3>Нарушения по видам</h3>';
VIOLATION_TYPES.forEach((t, i) => {
const c = typeStats[t];
const pct = (c / maxCount * 100).toFixed(0);
chartHtml += `<div class="chart-bar-row">
<span class="chart-bar-label">${t}</span>
<div class="chart-bar-track"><div class="chart-bar-fill" style="width:${pct}%;background:${BAR_COLORS[i]}"></div></div>
<span class="chart-bar-count">${c}</span>
</div>`;
});
document.getElementById('chartCard').innerHTML = chartHtml;
const recent = violations.slice().sort((a,b) => b.date.localeCompare(a.date)).slice(0,5);
let recentHtml = '<h3>Последние нарушения</h3>';
if (!recent.length) recentHtml += '<div class="empty">Пока нет записей</div>';
else {
recentHtml += '<table><thead><tr><th>Дата</th><th>Вид</th><th>Описание</th><th>Статус</th></tr></thead><tbody>';
recent.forEach(v => {
recentHtml += `<tr>
<td>${v.date}</td>
<td><span class="badge badge-type">${v.type}</span></td>
<td>${esc(v.description)}</td>
<td>${statusBadge(v.status)}</td>
</tr>`;
});
recentHtml += '</tbody></table>';
}
document.getElementById('recentCard').innerHTML = recentHtml;
}
/* ========== PLAN ========== */
const FREQ_LABELS = { daily: 'Ежедневно', weekly: 'Еженедельно', monthly: 'Ежемесячно', quarterly: 'Ежеквартально', once: 'Разово' };
function renderPlan() {
let html = '<table><thead><tr><th>Название</th><th>Периодичность</th><th>Описание</th><th>Ответственный</th><th>Статус</th><th></th></tr></thead><tbody>';
if (!planItems.length) html += '<tr><td colspan="6" class="empty">План пуст — добавьте пункты</td></tr>';
else {
planItems.forEach(p => {
const cls = p.completed ? 'plan-status-completed' : '';
html += `<tr class="${cls}">
<td><strong>${esc(p.name)}</strong></td>
<td>${FREQ_LABELS[p.freq] || p.freq}</td>
<td style="color:var(--gray-500)">${esc(p.desc || '')}</td>
<td>${esc(p.resp || '')}</td>
<td>${p.completed ? '<span class="badge badge-green">Выполнено</span>' : '<span class="badge badge-amber">Активно</span>'}</td>
<td>
<div class="plan-actions">
<button class="btn btn-sm btn-outline" onclick="editPlan('${p.id}')">✏️</button>
${p.completed
? `<button class="btn btn-sm btn-outline" onclick="togglePlanComplete('${p.id}')">↩️</button>`
: `<button class="btn btn-sm btn-success" onclick="togglePlanComplete('${p.id}')">✓</button>`
}
<button class="btn btn-sm btn-danger" onclick="deletePlan('${p.id}')">🗑</button>
</div>
</td>
</tr>`;
});
}
html += '</tbody></table>';
document.getElementById('planTable').innerHTML = html;
}
function togglePlanForm() {
const card = document.getElementById('planFormCard');
if (card.style.display === 'none') {
card.style.display = 'block';
document.getElementById('planFormTitle').textContent = 'Новый пункт плана';
document.getElementById('planSaveBtn').textContent = 'Сохранить';
document.getElementById('planName').value = '';
document.getElementById('planFreq').value = 'daily';
document.getElementById('planDesc').value = '';
document.getElementById('planResp').value = '';
editingPlanId = null;
} else {
card.style.display = 'none';
editingPlanId = null;
}
}
function editPlan(id) {
const p = planItems.find(x => x.id === id);
if (!p) return;
editingPlanId = id;
document.getElementById('planFormTitle').textContent = 'Редактировать пункт';
document.getElementById('planSaveBtn').textContent = 'Обновить';
document.getElementById('planName').value = p.name;
document.getElementById('planFreq').value = p.freq;
document.getElementById('planDesc').value = p.desc || '';
document.getElementById('planResp').value = p.resp || '';
document.getElementById('planFormCard').style.display = 'block';
document.getElementById('planFormCard').scrollIntoView({behavior:'smooth'});
}
function savePlan() {
const name = document.getElementById('planName').value.trim();
if (!name) { alert('Введите название'); return; }
const data = {
id: editingPlanId || genId(),
name,
freq: document.getElementById('planFreq').value,
desc: document.getElementById('planDesc').value.trim(),
resp: document.getElementById('planResp').value.trim(),
completed: editingPlanId ? (planItems.find(p => p.id === editingPlanId)?.completed || false) : false
};
if (editingPlanId) {
planItems = planItems.map(p => p.id === editingPlanId ? data : p);
} else {
planItems.push(data);
}
save('planItems', planItems);
togglePlanForm();
renderPlan();
updatePlanSelects();
}
function togglePlanComplete(id) {
planItems = planItems.map(p => p.id === id ? {...p, completed: !p.completed} : p);
save('planItems', planItems);
renderPlan();
}
function deletePlan(id) {
if (!confirm('Удалить пункт плана?')) return;
planItems = planItems.filter(p => p.id !== id);
save('planItems', planItems);
renderPlan();
updatePlanSelects();
}
/* ========== JOURNAL ========== */
function renderJournal() {
const typeFilter = document.getElementById('filterType').value;
const statusFilter = document.getElementById('filterStatus').value;
const search = document.getElementById('filterSearch').value.toLowerCase();
let filtered = violations.slice();
if (typeFilter !== 'all') filtered = filtered.filter(v => v.type === typeFilter);
if (statusFilter !== 'all') filtered = filtered.filter(v => v.status === statusFilter);
if (search) filtered = filtered.filter(v => v.description.toLowerCase().includes(search) || v.location.toLowerCase().includes(search));
filtered.sort((a,b) => b.date.localeCompare(a.date));
let html = '<table><thead><tr><th>Дата</th><th>Место</th><th>Вид нарушения</th><th>Описание</th><th>Статус</th><th>Срок</th><th></th></tr></thead><tbody>';
if (!filtered.length) html += '<tr><td colspan="7" class="empty">Нет записей</td></tr>';
else {
filtered.forEach(v => {
html += `<tr>
<td>${v.date}</td>
<td>${esc(v.location)}</td>
<td><span class="badge badge-type">${esc(v.type)}</span></td>
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(v.description)}">${esc(v.description)}</td>
<td>${statusBadge(v.status)}</td>
<td>${v.deadline || '—'}</td>
<td>
<div class="btn-group">
${v.status !== 'resolved' ? `<button class="btn btn-sm btn-success" onclick="changeStatus('${v.id}','resolved')">✓</button>` : `<button class="btn btn-sm btn-outline" onclick="changeStatus('${v.id}','detected')">↩</button>`}
${v.status === 'detected' ? `<button class="btn btn-sm btn-outline" onclick="changeStatus('${v.id}','in_progress')">▶</button>` : ''}
<button class="btn btn-sm btn-outline" onclick="editViolation('${v.id}')">✏️</button>
<button class="btn btn-sm btn-danger" onclick="deleteViolation('${v.id}')">🗑</button>
</div>
</td>
</tr>`;
});
}
html += '</tbody></table>';
document.getElementById('journalTable').innerHTML = html;
updateFilterOptions();
}
function updateFilterOptions() {
const sel = document.getElementById('filterType');
const cur = sel.value;
sel.innerHTML = '<option value="all">Все виды нарушений</option>';
VIOLATION_TYPES.forEach(t => {
sel.innerHTML += `<option value="${esc(t)}" ${cur === t ? 'selected' : ''}>${t}</option>`;
});
}
function changeStatus(id, newStatus) {
violations = violations.map(v => v.id === id ? {...v, status: newStatus, resolvedDate: newStatus === 'resolved' ? new Date().toISOString().slice(0,10) : v.resolvedDate} : v);
save('violations', violations);
renderJournal();
renderDashboard();
}
function deleteViolation(id) {
if (!confirm('Удалить запись о нарушении?')) return;
violations = violations.filter(v => v.id !== id);
save('violations', violations);
renderJournal();
renderDashboard();
}
/* ========== ADD ========== */
function prepareAddForm() {
document.getElementById('vioDate').value = new Date().toISOString().slice(0,10);
document.getElementById('vioLocation').value = '';
document.getElementById('vioDesc').value = '';
document.getElementById('vioResp').value = '';
document.getElementById('vioDeadline').value = '';
updatePlanSelects();
const typeSel = document.getElementById('vioType');
typeSel.innerHTML = '';
VIOLATION_TYPES.forEach(t => { typeSel.innerHTML += `<option value="${esc(t)}">${t}</option>`; });
editingViolationId = null;
}
function updatePlanSelects() {
const html = '<option value="">— не привязано —</option>' + planItems.map(p => `<option value="${p.id}">${esc(p.name)}</option>`).join('');
document.getElementById('vioPlan').innerHTML = html;
}
function editViolation(id) {
const v = violations.find(x => x.id === id);
if (!v) return;
editingViolationId = id;
document.getElementById('editModalContent').innerHTML = `
<h3>Редактировать нарушение</h3>
<div class="form-group"><label>Дата</label><input type="date" id="evDate" value="${v.date}"></div>
<div class="form-group"><label>Место</label><input id="evLocation" value="${escAttr(v.location)}"></div>
<div class="form-group"><label>Вид нарушения</label><select id="evType">${VIOLATION_TYPES.map(t => `<option value="${escAttr(t)}" ${t===v.type?'selected':''}>${t}</option>`).join('')}</select></div>
<div class="form-group"><label>Описание</label><textarea id="evDesc" rows="3">${esc(v.description)}</textarea></div>
<div class="form-group"><label>Ответственный</label><input id="evResp" value="${escAttr(v.responsible||'')}"></div>
<div class="form-group"><label>Срок</label><input type="date" id="evDeadline" value="${v.deadline||''}"></div>
<div class="form-group"><label>Статус</label>
<select id="evStatus">
<option value="detected" ${v.status==='detected'?'selected':''}>Выявлено</option>
<option value="in_progress" ${v.status==='in_progress'?'selected':''}>В работе</option>
<option value="resolved" ${v.status==='resolved'?'selected':''}>Устранено</option>
</select>
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeEditModal()">Отмена</button>
<button class="btn btn-primary" onclick="saveEditViolation('${v.id}')">Сохранить</button>
</div>
`;
document.getElementById('editModal').style.display = 'flex';
}
function closeEditModal() {
document.getElementById('editModal').style.display = 'none';
editingViolationId = null;
}
function saveEditViolation(id) {
const status = document.getElementById('evStatus').value;
violations = violations.map(v => v.id === id ? {
...v,
date: document.getElementById('evDate').value,
location: document.getElementById('evLocation').value.trim(),
type: document.getElementById('evType').value,
description: document.getElementById('evDesc').value.trim(),
responsible: document.getElementById('evResp').value.trim(),
deadline: document.getElementById('evDeadline').value,
status,
resolvedDate: status === 'resolved' ? new Date().toISOString().slice(0,10) : v.resolvedDate
} : v);
save('violations', violations);
closeEditModal();
renderJournal();
renderDashboard();
}
/* ========== ADD VIOLATION ========== */
function addViolation() {
const date = document.getElementById('vioDate').value;
const location = document.getElementById('vioLocation').value.trim();
const type = document.getElementById('vioType').value;
const description = document.getElementById('vioDesc').value.trim();
const responsible = document.getElementById('vioResp').value.trim();
const deadline = document.getElementById('vioDeadline').value;
const planId = document.getElementById('vioPlan').value;
if (!date) { alert('Выберите дату'); return; }
if (!location) { alert('Укажите место'); return; }
if (!description) { alert('Опишите нарушение'); return; }
const entry = {
id: genId(),
date,
location,
type,
description,
responsible,
deadline,
status: 'detected',
planId: planId || null,
resolvedDate: null
};
violations.push(entry);
save('violations', violations);
prepareAddForm();
renderDashboard();
alert('Нарушение сохранено!');
}
/* ========== HELPERS ========== */
function statusBadge(s) {
const labels = { detected: ['badge-red','Выявлено'], in_progress: ['badge-amber','В работе'], resolved: ['badge-green','Устранено'] };
const [cls, label] = labels[s] || ['',''];
return `<span class="badge ${cls}">${label}</span>`;
}
function esc(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function escAttr(s) { return String(s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
/* ========== INIT ========== */
updateFilterOptions();
renderDashboard();
</script>
</body>
</html>