Фильтр по датам, отслеживание нарушений, управление данными админа
This commit is contained in:
parent
c9ac12a3a4
commit
7f0c0c322e
153
index.html
153
index.html
@ -238,6 +238,7 @@ body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
|
|||||||
<a href="#" data-panel="newAudit" class="active" onclick="switchPanel('newAudit',this)">Новый аудит</a>
|
<a href="#" data-panel="newAudit" class="active" onclick="switchPanel('newAudit',this)">Новый аудит</a>
|
||||||
<a href="#" data-panel="mySchedule" onclick="switchPanel('mySchedule',this)">Мой график</a>
|
<a href="#" data-panel="mySchedule" onclick="switchPanel('mySchedule',this)">Мой график</a>
|
||||||
<a href="#" data-panel="dashboard" onclick="switchPanel('dashboard',this)">Дашборд</a>
|
<a href="#" data-panel="dashboard" onclick="switchPanel('dashboard',this)">Дашборд</a>
|
||||||
|
<a href="#" data-panel="violations" onclick="switchPanel('violations',this)">Нарушения</a>
|
||||||
<a href="#" data-panel="history" onclick="switchPanel('history',this)">История</a>
|
<a href="#" data-panel="history" onclick="switchPanel('history',this)">История</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="user-area"><span class="role" id="displayName"></span><button class="btn btn-outline btn-sm" style="color:#9aa3b2;border-color:#3a4452" onclick="doLogout()">Выход</button></div>
|
<div class="user-area"><span class="role" id="displayName"></span><button class="btn btn-outline btn-sm" style="color:#9aa3b2;border-color:#3a4452" onclick="doLogout()">Выход</button></div>
|
||||||
@ -324,6 +325,10 @@ body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
|
|||||||
<button class="btn btn-primary btn-sm" onclick="downloadFullCSV()">📥 Скачать все данные (CSV)</button>
|
<button class="btn btn-primary btn-sm" onclick="downloadFullCSV()">📥 Скачать все данные (CSV)</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="downloadWorkerReport()">👥 Отчёт по работникам (CSV)</button>
|
<button class="btn btn-outline btn-sm" onclick="downloadWorkerReport()">👥 Отчёт по работникам (CSV)</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="downloadSummaryHTML()">📊 Сводный отчёт (HTML)</button>
|
<button class="btn btn-outline btn-sm" onclick="downloadSummaryHTML()">📊 Сводный отчёт (HTML)</button>
|
||||||
|
<span style="color:var(--gray-200);margin:0 4px">|</span>
|
||||||
|
<button class="btn btn-danger btn-sm" onclick="clearAllAudits()">🗑️ Очистить все аудиты</button>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="clearAuditsByDate()">📅 Очистить за период</button>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="showAllUsers()">👥 Все пользователи</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-bar" id="dashboardFilters">
|
<div class="filter-bar" id="dashboardFilters">
|
||||||
@ -332,6 +337,10 @@ body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
|
|||||||
<select id="dfBranch" onchange="renderDashboard()"><option value="all">Все филиалы</option></select>
|
<select id="dfBranch" onchange="renderDashboard()"><option value="all">Все филиалы</option></select>
|
||||||
<select id="dfDept" onchange="renderDashboard()"><option value="all">Все подразделения</option></select>
|
<select id="dfDept" onchange="renderDashboard()"><option value="all">Все подразделения</option></select>
|
||||||
<select id="dfCity" onchange="renderDashboard()"><option value="all">Все города</option></select>
|
<select id="dfCity" onchange="renderDashboard()"><option value="all">Все города</option></select>
|
||||||
|
<span style="font-size:12px;color:var(--gray-500)">с</span>
|
||||||
|
<input type="date" id="dfDateFrom" onchange="renderDashboard()" style="width:140px">
|
||||||
|
<span style="font-size:12px;color:var(--gray-500)">по</span>
|
||||||
|
<input type="date" id="dfDateTo" onchange="renderDashboard()" style="width:140px">
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card"><div class="stat-label">Всего аудитов</div><div class="stat-value" id="statTotal">0</div></div>
|
<div class="stat-card"><div class="stat-label">Всего аудитов</div><div class="stat-value" id="statTotal">0</div></div>
|
||||||
@ -356,6 +365,28 @@ body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== VIOLATIONS TRACKING ========== -->
|
||||||
|
<div id="panelViolations" class="panel">
|
||||||
|
<div class="page-header"><h2>⚠️ Отслеживание несоответствий</h2><p>Контроль исполнения корректирующих мер по всем аудитам</p></div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card"><div class="stat-label">Всего несоответствий</div><div class="stat-value" id="vTotal">0</div></div>
|
||||||
|
<div class="stat-card green"><div class="stat-label">Устранено</div><div class="stat-value" id="vDone">0</div></div>
|
||||||
|
<div class="stat-card red"><div class="stat-label">Просрочено</div><div class="stat-value" id="vOverdue">0</div></div>
|
||||||
|
<div class="stat-card orange"><div class="stat-label">В работе</div><div class="stat-value" id="vPending">0</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<select id="vfStatus" onchange="renderViolations()"><option value="all">Все</option><option value="done">Устранено</option><option value="overdue">Просрочено</option><option value="pending">В работе</option></select>
|
||||||
|
<select id="vfType" onchange="renderViolations()"><option value="all">Все виды</option><option>Нарушение</option><option>Замечание</option><option>Риск</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th>№</th><th>Несоответствие</th><th>Аудит (дата)</th><th>Исполнитель</th><th>Вид</th><th>Меры</th><th>Ответственный</th><th>Срок</th><th>Завершение</th><th>Статус</th></tr></thead>
|
||||||
|
<tbody id="violationsBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="no-data" id="vioNoData" style="display:none"><span class="icon">✅</span><p>Несоответствий не найдено</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ========== HISTORY ========== -->
|
<!-- ========== HISTORY ========== -->
|
||||||
<div id="panelHistory" class="panel">
|
<div id="panelHistory" class="panel">
|
||||||
<div class="page-header"><h2>📁 История аудитов</h2><p>Архив всех проведённых ПАБ</p></div>
|
<div class="page-header"><h2>📁 История аудитов</h2><p>Архив всех проведённых ПАБ</p></div>
|
||||||
@ -701,6 +732,7 @@ function switchPanel(name,el){
|
|||||||
if(name==='dashboard')renderDashboard();
|
if(name==='dashboard')renderDashboard();
|
||||||
if(name==='history')renderHistory();
|
if(name==='history')renderHistory();
|
||||||
if(name==='mySchedule')renderMySchedule();
|
if(name==='mySchedule')renderMySchedule();
|
||||||
|
if(name==='violations')renderViolations();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== DASHBOARD ==========
|
// ========== DASHBOARD ==========
|
||||||
@ -742,6 +774,12 @@ function renderDashboard(){
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Date range filter
|
||||||
|
const df=document.getElementById('dfDateFrom')?.value;
|
||||||
|
const dt=document.getElementById('dfDateTo')?.value;
|
||||||
|
if(df)audits=audits.filter(a=>a.date>=df);
|
||||||
|
if(dt)audits=audits.filter(a=>a.date<=dt);
|
||||||
|
|
||||||
const total=audits.length;
|
const total=audits.length;
|
||||||
const allSafe=audits.filter(a=>a.overallSafe).length;
|
const allSafe=audits.filter(a=>a.overallSafe).length;
|
||||||
const withDanger=audits.filter(a=>!a.overallSafe).length;
|
const withDanger=audits.filter(a=>!a.overallSafe).length;
|
||||||
@ -857,7 +895,8 @@ function exportCSV(){
|
|||||||
|
|
||||||
// ========== ADMIN DOWNLOADS ==========
|
// ========== ADMIN DOWNLOADS ==========
|
||||||
function downloadFullCSV(){
|
function downloadFullCSV(){
|
||||||
const audits=getAudits();if(audits.length===0){alert('Нет данных');return}
|
const {audits,all}=getDashboardFilters();
|
||||||
|
if(audits.length===0){alert('Нет данных');return}
|
||||||
const all=getAllUsers();
|
const all=getAllUsers();
|
||||||
const header='Бланк №;Дата;Время;Место;Тип работы;Наблюдатель;Должность;Руководитель;Филиал;Подразделение;Регион;Область;Город;Статус;Нарушений;Категории с нарушениями';
|
const header='Бланк №;Дата;Время;Место;Тип работы;Наблюдатель;Должность;Руководитель;Филиал;Подразделение;Регион;Область;Город;Статус;Нарушений;Категории с нарушениями';
|
||||||
const rows=audits.map(a=>{
|
const rows=audits.map(a=>{
|
||||||
@ -869,7 +908,8 @@ function downloadFullCSV(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function downloadWorkerReport(){
|
function downloadWorkerReport(){
|
||||||
const all=getAllUsers();const audits=getAudits();const userList=Object.entries(all).filter(([l])=>l!=='admin');
|
const {audits}=getDashboardFilters();
|
||||||
|
const all=getAllUsers();const userList=Object.entries(all).filter(([l])=>l!=='admin');
|
||||||
if(userList.length===0){alert('Нет зарегистрированных работников');return}
|
if(userList.length===0){alert('Нет зарегистрированных работников');return}
|
||||||
const header='Логин;ФИО;Должность;Филиал;Подразделение;Регион;Область;Город;Email;График;Период;Выполнено;Нужно;Статус';
|
const header='Логин;ФИО;Должность;Филиал;Подразделение;Регион;Область;Город;Email;График;Период;Выполнено;Нужно;Статус';
|
||||||
const rows=userList.map(([login,u])=>{
|
const rows=userList.map(([login,u])=>{
|
||||||
@ -883,7 +923,8 @@ function downloadWorkerReport(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function downloadSummaryHTML(){
|
function downloadSummaryHTML(){
|
||||||
const audits=getAudits();const all=getAllUsers();const userList=Object.entries(all).filter(([l])=>l!=='admin');
|
const {audits}=getDashboardFilters();
|
||||||
|
const all=getAllUsers();const userList=Object.entries(all).filter(([l])=>l!=='admin');
|
||||||
const total=audits.length;const allSafe=audits.filter(a=>a.overallSafe).length;
|
const total=audits.length;const allSafe=audits.filter(a=>a.overallSafe).length;
|
||||||
const withDanger=audits.filter(a=>!a.overallSafe).length;const totalVio=audits.reduce((s,a)=>s+(a.totalViolations||0),0);
|
const withDanger=audits.filter(a=>!a.overallSafe).length;const totalVio=audits.reduce((s,a)=>s+(a.totalViolations||0),0);
|
||||||
|
|
||||||
@ -944,6 +985,112 @@ tr:hover td{background:#F2F4F7}
|
|||||||
</body></html>`;
|
</body></html>`;
|
||||||
const w=window.open('','_blank','width=1000,height=800');w.document.write(html);w.document.close();
|
const w=window.open('','_blank','width=1000,height=800');w.document.write(html);w.document.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== VIOLATIONS TRACKING ==========
|
||||||
|
function renderViolations(){
|
||||||
|
const all=getAllUsers();let audits=getAudits();
|
||||||
|
const df=document.getElementById('dfDateFrom')?.value,dt=document.getElementById('dfDateTo')?.value;
|
||||||
|
if(df)audits=audits.filter(a=>a.date>=df);
|
||||||
|
if(dt)audits=audits.filter(a=>a.date<=dt);
|
||||||
|
const fStatus=document.getElementById('vfStatus')?.value||'all';
|
||||||
|
const fType=document.getElementById('vfType')?.value||'all';
|
||||||
|
|
||||||
|
// Collect all violations from all audits
|
||||||
|
const today=new Date().toISOString().split('T')[0];
|
||||||
|
let allVios=[];
|
||||||
|
audits.forEach(a=>{
|
||||||
|
if(!a.violations)return;
|
||||||
|
const u=all[a.createdBy]||{};
|
||||||
|
a.violations.forEach((v,i)=>{
|
||||||
|
const dueDate=v.date||'';
|
||||||
|
const done=v.done&&v.done.trim();
|
||||||
|
let status='pending';
|
||||||
|
if(done)status='done';
|
||||||
|
else if(dueDate&&dueDate<today)status='overdue';
|
||||||
|
allVios.push({...v,status,auditDate:a.date,auditNumber:a.number||'—',auditLocation:a.location,auditId:a.id,auditObserver:a.observer,observerBranch:u.branch||''});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const total=allVios.length;
|
||||||
|
const doneCount=allVios.filter(v=>v.status==='done').length;
|
||||||
|
const overdueCount=allVios.filter(v=>v.status==='overdue').length;
|
||||||
|
const pendingCount=allVios.filter(v=>v.status==='pending').length;
|
||||||
|
document.getElementById('vTotal').textContent=total;
|
||||||
|
document.getElementById('vDone').textContent=doneCount;
|
||||||
|
document.getElementById('vOverdue').textContent=overdueCount;
|
||||||
|
document.getElementById('vPending').textContent=pendingCount;
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
if(fStatus!=='all')allVios=allVios.filter(v=>v.status===fStatus);
|
||||||
|
if(fType!=='all')allVios=allVios.filter(v=>v.type===fType);
|
||||||
|
|
||||||
|
const tbody=document.getElementById('violationsBody'),nd=document.getElementById('vioNoData');
|
||||||
|
if(allVios.length===0){tbody.innerHTML='';nd.style.display='block';return}
|
||||||
|
nd.style.display='none';
|
||||||
|
tbody.innerHTML=allVios.map((v,i)=>{
|
||||||
|
const sc=v.status==='done'?'badge-safe':v.status==='overdue'?'badge-danger':'badge-warn';
|
||||||
|
const sl=v.status==='done'?'Устранено':v.status==='overdue'?'Просрочено':'В работе';
|
||||||
|
return `<tr>
|
||||||
|
<td>${i+1}</td><td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escAttr(v.nc)}">${v.nc}</td>
|
||||||
|
<td>${v.auditDate} (№${v.auditNumber})</td><td>${v.executor}</td><td>${v.type}</td>
|
||||||
|
<td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escAttr(v.measure)}">${v.measure||'—'}</td>
|
||||||
|
<td>${v.responsible}</td><td>${v.date||'—'}</td><td>${v.done||'—'}</td>
|
||||||
|
<td><span class="badge ${sc}">${sl}</span></td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== ADMIN DATA MANAGEMENT ==========
|
||||||
|
function clearAllAudits(){
|
||||||
|
if(!isAdmin())return;
|
||||||
|
if(!confirm('ВНИМАНИЕ! Это удалит ВСЕ аудиты безвозвратно. Продолжить?'))return;
|
||||||
|
if(!confirm('Точно удалить все данные?'))return;
|
||||||
|
localStorage.removeItem('safetyAudits');
|
||||||
|
alert('Все аудиты удалены.');
|
||||||
|
renderDashboard();renderHistory();renderViolations();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAuditsByDate(){
|
||||||
|
if(!isAdmin())return;
|
||||||
|
const from=prompt('Удалить аудиты С даты (ГГГГ-ММ-ДД):','');
|
||||||
|
if(!from)return;
|
||||||
|
const to=prompt('Удалить аудиты ПО дату (ГГГГ-ММ-ДД, или оставьте пустым):','');
|
||||||
|
let audits=getAudits();
|
||||||
|
const before=audits.length;
|
||||||
|
audits=audits.filter(a=>{if(a.date<from)return true;if(to&&a.date>to)return true;return false});
|
||||||
|
if(before===audits.length){alert('Нет аудитов за указанный период.');return}
|
||||||
|
if(!confirm('Удалить '+ (before-audits.length) +' аудитов за период '+from+(to?' — '+to:'')+'?'))return;
|
||||||
|
saveAudits(audits);
|
||||||
|
alert('Удалено '+(before-audits.length)+' аудитов.');
|
||||||
|
renderDashboard();renderHistory();renderViolations();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAllUsers(){
|
||||||
|
if(!isAdmin())return;
|
||||||
|
const all=getAllUsers();
|
||||||
|
const userList=Object.entries(all);
|
||||||
|
let html=`<h2>👥 Зарегистрированные пользователи (${userList.length})</h2><table style="width:100%;border-collapse:collapse;font-size:13px"><tr style="background:#0F1218;color:#fff"><th>Логин</th><th>ФИО</th><th>Должность</th><th>Филиал</th><th>Подразделение</th><th>Регион</th><th>Область</th><th>Город</th><th>Email</th><th>График</th></tr>`;
|
||||||
|
userList.forEach(([login,u])=>{
|
||||||
|
const q=getQuota(u.role);
|
||||||
|
html+=`<tr style="border-bottom:1px solid #E2E6EB"><td>${login}${login==='admin'?' ⭐':''}</td><td>${u.name}</td><td>${u.role}</td><td>${u.branch||'—'}</td><td>${u.dept||'—'}</td><td>${u.region||'—'}</td><td>${u.oblast||'—'}</td><td>${u.city||'—'}</td><td>${u.email||'—'}</td><td>${q.label}</td></tr>`;
|
||||||
|
});
|
||||||
|
html+='</table>';
|
||||||
|
const w=window.open('','_blank','width=1000,height=600');
|
||||||
|
w.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Пользователи ПАБ</title><style>body{font:14px/1.5 Arial;max-width:1100px;margin:30px auto;padding:20px}table{width:100%;border-collapse:collapse}td{padding:8px 10px}@media print{body{margin:0;padding:10px}}</style></head><body><button onclick="window.print()" style="padding:8px 20px;font-size:14px;margin-bottom:16px">🖨️ Печать</button>${html}<p style="margin-top:20px;font-size:11px;color:#5B6573">⭐ — предустановленный администратор</p></body></html>`);
|
||||||
|
w.document.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== DOWNLOADS WITH DATE RANGE ==========
|
||||||
|
function getDashboardFilters(){
|
||||||
|
const all=getAllUsers();
|
||||||
|
let audits=getAudits();
|
||||||
|
const fr=document.getElementById('dfRegion')?.value||'all',fo=document.getElementById('dfOblast')?.value||'all',fb=document.getElementById('dfBranch')?.value||'all',fd=document.getElementById('dfDept')?.value||'all',fc=document.getElementById('dfCity')?.value||'all';
|
||||||
|
const df=document.getElementById('dfDateFrom')?.value,dt=document.getElementById('dfDateTo')?.value;
|
||||||
|
audits=audits.filter(a=>{const u=all[a.createdBy];if(!u)return true;if(fr!=='all'&&u.region!==fr)return false;if(fo!=='all'&&u.oblast!==fo)return false;if(fb!=='all'&&u.branch!==fb)return false;if(fd!=='all'&&u.dept!==fd)return false;if(fc!=='all'&&u.city!==fc)return false;return true});
|
||||||
|
if(df)audits=audits.filter(a=>a.date>=df);if(dt)audits=audits.filter(a=>a.date<=dt);
|
||||||
|
return {audits,all};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user