ot-tb-control/index.html

469 lines
22 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:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
color:var(--ink);background:var(--gray-100);min-height:100vh;
}
.header{background:var(--white);border-bottom:1px solid var(--gray-200);position:sticky;top:0;z-index:100}
.header-inner{max-width:1300px;margin:0 auto;padding:0 24px;display:flex;align-items:center;justify-content:space-between;height:58px}
.header h1{font-size:17px;font-weight:700}
.header-badge{font-size:11px;background:var(--cyan-50);color:var(--ink);padding:3px 10px;border-radius:20px;font-weight:600}
.tabs{background:var(--white);border-bottom:1px solid var(--gray-200);position:sticky;top:58px;z-index:99}
.tabs-inner{max-width:1300px;margin:0 auto;padding:0 24px;display:flex;gap:0}
.tab{
padding:13px 22px;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{max-width:1300px;margin:0 auto;padding:28px 24px}
.tab-panel{display:none}
.tab-panel.active{display:block}
/* Cards */
.stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:20px}
.stat-card{background:var(--white);border-radius:var(--radius);padding:18px 22px;box-shadow:var(--shadow)}
.stat-card .stat-label{font-size:12px;color:var(--gray-500);margin-bottom:2px}
.stat-card .stat-value{font-size:28px;font-weight:800;line-height:1}
.stat-card .stat-sub{font-size:12px;color:var(--gray-500);margin-top:2px}
/* Section title */
.section-title{font-size:17px;font-weight:700;margin-bottom:14px}
/* Table */
.table-wrap{background:var(--white);border-radius:var(--radius);box-shadow:var(--shadow);overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:13px;white-space:nowrap}
th{text-align:left;padding:10px 14px;background:var(--gray-100);font-weight:600;font-size:11px;color:var(--gray-500);text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--gray-200)}
td{padding:10px 14px;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:2px 9px;border-radius:10px;font-size:11px;font-weight:600}
.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-info{background:var(--cyan-50);color:var(--ink)}
/* Buttons */
.btn{
display:inline-flex;align-items:center;gap:5px;padding:7px 14px;
border-radius:var(--radius-sm);font-size:13px;font-weight:600;
border:none;cursor:pointer;transition:all .15s;line-height:1.4;
}
.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 9px;font-size:11px}
.btn-danger{background:var(--red-bg);color:var(--red);border:1px solid var(--red-bg)}
.btn-danger:hover{background:var(--red);color:#fff}
.btn-group{display:flex;gap:4px}
/* Toolbar */
.toolbar{display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap;align-items:center}
.toolbar select,.toolbar input{
padding:7px 11px;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)}
/* Form */
.form-card{background:var(--white);border-radius:var(--radius);padding:22px 26px;box-shadow:var(--shadow);max-width:700px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.form-group{margin-bottom:14px}
.form-group label{display:block;font-size:12px;font-weight:600;margin-bottom:3px}
.form-group input,.form-group select,.form-group textarea{
width:100%;padding:9px 11px;border:1.5px solid var(--gray-200);
border-radius:var(--radius-sm);font-size:13px;color:var(--ink);font-family:inherit;
}
.form-group textarea{resize:vertical;min-height:60px}
.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:26px;
max-width:620px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.15);
}
.modal h3{font-size:17px;font-weight:700;margin-bottom:14px}
.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:18px}
.empty{text-align:center;padding:40px 20px;color:var(--gray-500)}
/* Chart */
.chart-card{background:var(--white);border-radius:var(--radius);padding:22px;box-shadow:var(--shadow);margin-bottom:18px}
.chart-card h3{font-size:15px;font-weight:700;margin-bottom:13px}
.bar-row{display:flex;align-items:center;gap:10px;margin-bottom:8px;font-size:12px}
.bar-label{min-width:200px;text-align:right}
.bar-track{flex:1;background:var(--gray-100);border-radius:3px;height:18px;overflow:hidden}
.bar-fill{height:100%;border-radius:3px;transition:width .4s;min-width:2px;background:var(--cyan)}
.bar-num{font-weight:600;min-width:40px;text-align:right}
.exec-cell{font-weight:600}
.exec-good{color:var(--green)}
.exec-bad{color:var(--red)}
.exec-ok{color:var(--amber)}
@media (max-width:768px){
.tabs-inner{overflow-x:auto}
.tab{padding:11px 14px;font-size:12px}
.stats-row{grid-template-columns:1fr 1fr}
.form-row{grid-template-columns:1fr}
table{font-size:11px}
th,td{padding:7px 8px}
.bar-label{min-width:120px;font-size:11px}
}
</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="table">Таблица учёта</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 style="display:grid;grid-template-columns:1fr 1fr;gap:18px" id="dashCharts">
<div class="chart-card" id="chartByUnit"></div>
<div class="chart-card" id="chartByMonth"></div>
</div>
</section>
<!-- TABLE -->
<section class="tab-panel" id="tab-table">
<div class="toolbar">
<select id="fUnit" onchange="renderTable()"><option value="all">Все подразделения</option></select>
<select id="fRegion" onchange="renderTable()"><option value="all">Все регионы</option></select>
<select id="fMonth" onchange="renderTable()">
<option value="all">Все месяцы</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><option value="Декабрь">Декабрь</option>
</select>
<select id="fYear" onchange="renderTable()"><option value="all">Все годы</option></select>
</div>
<div class="table-wrap" id="dataTable"></div>
</section>
<!-- ADD -->
<section class="tab-panel" id="tab-add">
<div class="form-card">
<h3 style="margin-bottom:14px;font-size:17px">Новая запись</h3>
<div class="form-row">
<div class="form-group"><label>Наименование структурного подразделения</label><input id="afUnit" placeholder="Например: Цех №3"></div>
<div class="form-group"><label>Населённый пункт</label><input id="afCity" placeholder="Например: г. Алматы"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Регион</label><input id="afRegion" placeholder="Например: Алматинская обл."></div>
<div class="form-group"><label>Месяц</label><select id="afMonth">
<option value="">— выберите —</option>
<option>Январь</option><option>Февраль</option><option>Март</option><option>Апрель</option>
<option>Май</option><option>Июнь</option><option>Июль</option><option>Август</option>
<option>Сентябрь</option><option>Октябрь</option><option>Ноябрь</option><option>Декабрь</option>
</select></div>
</div>
<div class="form-row">
<div class="form-group"><label>Год</label><input type="number" id="afYear" min="2020" max="2030"></div>
</div>
<div class="form-row">
<div class="form-group"><label>План (кол-во проверок)</label><input type="number" id="afPlan" min="0"></div>
<div class="form-group"><label>Факт (выполнено)</label><input type="number" id="afFact" min="0"></div>
</div>
<div class="form-row">
<div class="form-group"><label>№ Указания</label><input id="afDirNo" placeholder="Например: 45/ОТ"></div>
<div class="form-group"><label>Указание оформил</label><input id="afDirBy" placeholder="ФИО"></div>
</div>
<div class="form-group"><label>Акт приостановки</label><input id="afSusp" placeholder="Например: Акт №12 от 15.03.2026"></div>
<button class="btn btn-primary" onclick="addEntry()" style="margin-top:6px">Сохранить запись</button>
</div>
</section>
</main>
<div class="modal-overlay" id="editModal" style="display:none" onclick="if(event.target===this)closeEditModal()">
<div class="modal" id="editModalContent"></div>
</div>
<script>
const MONTHS = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
function load(k,f) { try{ const v=localStorage.getItem(k); return v?JSON.parse(v):f } catch{ return f } }
function save(k,v) { localStorage.setItem(k,JSON.stringify(v)) }
let entries = load('ot_entries', []);
function genId() { return 'e'+Date.now()+'_'+Math.random().toString(36).slice(2,6) }
/* ========== NAV ========== */
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(n){
if(n==='dashboard') renderDashboard();
else if(n==='table') renderTable();
else if(n==='add') prepareAdd();
}
/* ========== DASHBOARD ========== */
function renderDashboard(){
const total=entries.length;
const totalPlan=entries.reduce((s,e)=>s+(+e.plan||0),0);
const totalFact=entries.reduce((s,e)=>s+(+e.fact||0),0);
const pct=totalPlan?Math.round(totalFact/totalPlan*100):0;
document.getElementById('statsRow').innerHTML=`
<div class="stat-card"><div class="stat-label">Всего записей</div><div class="stat-value">${total}</div></div>
<div class="stat-card"><div class="stat-label">План проверок</div><div class="stat-value">${totalPlan}</div></div>
<div class="stat-card"><div class="stat-label">Факт выполнено</div><div class="stat-value">${totalFact}</div></div>
<div class="stat-card"><div class="stat-label">Выполнение плана</div><div class="stat-value" style="color:${pct>=100?'var(--green)':pct>=70?'var(--amber)':'var(--red)'}">${pct}%</div></div>
<div class="stat-card"><div class="stat-label">Подразделений</div><div class="stat-value">${new Set(entries.map(e=>e.unit)).size}</div></div>
<div class="stat-card"><div class="stat-label">Указаний оформлено</div><div class="stat-value">${entries.filter(e=>e.directiveNo).length}</div></div>
`;
const byUnit={};
entries.forEach(e=>{ const u=e.unit||'—'; byUnit[u]=(byUnit[u]||0)+(+e.fact||0) });
const sorted=Object.entries(byUnit).sort((a,b)=>b[1]-a[1]).slice(0,8);
const maxU=Math.max(1,...sorted.map(s=>s[1]));
let h1='<h3>Выполнено проверок по подразделениям</h3>';
sorted.forEach(([u,c])=>{
h1+=`<div class="bar-row"><span class="bar-label">${esc(u)}</span><div class="bar-track"><div class="bar-fill" style="width:${(c/maxU*100).toFixed(0)}%"></div></div><span class="bar-num">${c}</span></div>`;
});
if(!sorted.length) h1+='<div class="empty">Нет данных</div>';
document.getElementById('chartByUnit').innerHTML=h1;
const byMonth={};
MONTHS.forEach(m=>{byMonth[m]=0});
entries.forEach(e=>{ const m=e.month; if(byMonth[m]!=null) byMonth[m]+=(+e.fact||0) });
const maxM=Math.max(1,...Object.values(byMonth));
let h2='<h3>Выполнено проверок по месяцам</h3>';
MONTHS.forEach(m=>{
const c=byMonth[m];
h2+=`<div class="bar-row"><span class="bar-label">${m}</span><div class="bar-track"><div class="bar-fill" style="width:${(c/maxM*100).toFixed(0)}%"></div></div><span class="bar-num">${c}</span></div>`;
});
document.getElementById('chartByMonth').innerHTML=h2;
}
/* ========== TABLE ========== */
function renderTable(){
const fUnit=document.getElementById('fUnit').value;
const fRegion=document.getElementById('fRegion').value;
const fMonth=document.getElementById('fMonth').value;
const fYear=document.getElementById('fYear').value;
let list=entries.slice();
if(fUnit!=='all') list=list.filter(e=>e.unit===fUnit);
if(fRegion!=='all') list=list.filter(e=>e.region===fRegion);
if(fMonth!=='all') list=list.filter(e=>e.month===fMonth);
if(fYear!=='all') list=list.filter(e=>String(e.year)===fYear);
list.sort((a,b)=>(b.year-a.year)||(MONTHS.indexOf(b.month)-MONTHS.indexOf(a.month)));
function execBadge(p,f){
const r=f?Math.round(f/p*100):0;
if(!p) return '<span class="badge badge-info">—</span>';
if(r>=100) return `<span class="badge badge-green">${r}%</span>`;
if(r>=70) return `<span class="badge badge-amber">${r}%</span>`;
return `<span class="badge badge-red">${r}%</span>`;
}
let html=`<table><thead><tr>
<th>Подразделение</th><th>Нас. пункт</th><th>Регион</th>
<th>Месяц</th><th>Год</th><th>План</th><th>Факт</th>
<th>% исп.</th><th>№ Указания</th><th>Оформил</th>
<th>Акт приостановки</th><th></th>
</tr></thead><tbody>`;
if(!list.length) html+='<tr><td colspan="12" class="empty">Нет записей</td></tr>';
else list.forEach(e=>{
html+=`<tr>
<td><strong>${esc(e.unit)}</strong></td>
<td>${esc(e.city)}</td>
<td>${esc(e.region)}</td>
<td>${esc(e.month)}</td>
<td>${e.year}</td>
<td style="text-align:center">${e.plan||0}</td>
<td style="text-align:center">${e.fact||0}</td>
<td style="text-align:center">${execBadge(e.plan,e.fact)}</td>
<td>${esc(e.directiveNo||'—')}</td>
<td>${esc(e.directiveBy||'—')}</td>
<td>${esc(e.suspensionAct||'—')}</td>
<td><div class="btn-group">
<button class="btn btn-sm btn-outline" onclick="editEntry('${e.id}')">✏️</button>
<button class="btn btn-sm btn-danger" onclick="deleteEntry('${e.id}')">🗑</button>
</div></td>
</tr>`;
});
html+='</tbody></table>';
document.getElementById('dataTable').innerHTML=html;
updateFilters();
}
function updateFilters(){
const units=[...new Set(entries.map(e=>e.unit).filter(Boolean))].sort();
const regions=[...new Set(entries.map(e=>e.region).filter(Boolean))].sort();
const years=[...new Set(entries.map(e=>e.year).filter(Boolean))].sort((a,b)=>b-a);
const cu=document.getElementById('fUnit').value;
const cr=document.getElementById('fRegion').value;
const cy=document.getElementById('fYear').value;
document.getElementById('fUnit').innerHTML='<option value="all">Все подразделения</option>'+units.map(u=>`<option value="${escAttr(u)}" ${cu===u?'selected':''}>${esc(u)}</option>`).join('');
document.getElementById('fRegion').innerHTML='<option value="all">Все регионы</option>'+regions.map(r=>`<option value="${escAttr(r)}" ${cr===r?'selected':''}>${esc(r)}</option>`).join('');
document.getElementById('fYear').innerHTML='<option value="all">Все годы</option>'+years.map(y=>`<option value="${y}" ${cy===String(y)?'selected':''}>${y}</option>`).join('');
}
/* ========== ADD ========== */
function prepareAdd(){
document.getElementById('afUnit').value='';
document.getElementById('afCity').value='';
document.getElementById('afRegion').value='';
document.getElementById('afMonth').value='';
document.getElementById('afYear').value=new Date().getFullYear();
document.getElementById('afPlan').value='';
document.getElementById('afFact').value='';
document.getElementById('afDirNo').value='';
document.getElementById('afDirBy').value='';
document.getElementById('afSusp').value='';
}
function addEntry(){
const unit=document.getElementById('afUnit').value.trim();
const city=document.getElementById('afCity').value.trim();
const region=document.getElementById('afRegion').value.trim();
const month=document.getElementById('afMonth').value;
const year=document.getElementById('afYear').value;
const plan=parseInt(document.getElementById('afPlan').value)||0;
const fact=parseInt(document.getElementById('afFact').value)||0;
const dirNo=document.getElementById('afDirNo').value.trim();
const dirBy=document.getElementById('afDirBy').value.trim();
const susp=document.getElementById('afSusp').value.trim();
if(!unit){ alert('Укажите наименование структурного подразделения'); return; }
if(!month){ alert('Выберите месяц'); return; }
if(!year){ alert('Укажите год'); return; }
entries.push({id:genId(),unit,city,region,month,year:+year,plan,fact,directiveNo:dirNo,directiveBy:dirBy,suspensionAct:susp});
save('ot_entries',entries);
prepareAdd();
renderDashboard();
alert('Запись сохранена!');
}
/* ========== EDIT ========== */
function editEntry(id){
const e=entries.find(x=>x.id===id); if(!e) return;
document.getElementById('editModalContent').innerHTML=`
<h3>Редактировать запись</h3>
<div class="form-row">
<div class="form-group"><label>Подразделение</label><input id="evUnit" value="${escAttr(e.unit)}"></div>
<div class="form-group"><label>Нас. пункт</label><input id="evCity" value="${escAttr(e.city)}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>Регион</label><input id="evRegion" value="${escAttr(e.region)}"></div>
<div class="form-group"><label>Месяц</label><select id="evMonth">${MONTHS.map(m=>`<option value="${m}" ${m===e.month?'selected':''}>${m}</option>`).join('')}</select></div>
</div>
<div class="form-row">
<div class="form-group"><label>Год</label><input type="number" id="evYear" value="${e.year}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>План</label><input type="number" id="evPlan" value="${e.plan}"></div>
<div class="form-group"><label>Факт</label><input type="number" id="evFact" value="${e.fact}"></div>
</div>
<div class="form-row">
<div class="form-group"><label>№ Указания</label><input id="evDirNo" value="${escAttr(e.directiveNo||'')}"></div>
<div class="form-group"><label>Оформил</label><input id="evDirBy" value="${escAttr(e.directiveBy||'')}"></div>
</div>
<div class="form-group"><label>Акт приостановки</label><input id="evSusp" value="${escAttr(e.suspensionAct||'')}"></div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeEditModal()">Отмена</button>
<button class="btn btn-primary" onclick="saveEdit('${id}')">Сохранить</button>
</div>
`;
document.getElementById('editModal').style.display='flex';
}
function closeEditModal(){ document.getElementById('editModal').style.display='none' }
function saveEdit(id){
entries=entries.map(e=>e.id===id?{
...e,
unit:document.getElementById('evUnit').value.trim(),
city:document.getElementById('evCity').value.trim(),
region:document.getElementById('evRegion').value.trim(),
month:document.getElementById('evMonth').value,
year:+document.getElementById('evYear').value,
plan:+document.getElementById('evPlan').value||0,
fact:+document.getElementById('evFact').value||0,
directiveNo:document.getElementById('evDirNo').value.trim(),
directiveBy:document.getElementById('evDirBy').value.trim(),
suspensionAct:document.getElementById('evSusp').value.trim()
}:e);
save('ot_entries',entries);
closeEditModal();
renderTable();
renderDashboard();
}
function deleteEntry(id){
if(!confirm('Удалить запись?')) return;
entries=entries.filter(e=>e.id!==id);
save('ot_entries',entries);
renderTable();
renderDashboard();
}
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;') }
/* INIT */
renderDashboard();
</script>
</body>
</html>