Files
driver-revenue-reconciliation/index.html
T

348 lines
28 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.0">
<title>Сверка выручки водителей</title>
<style>
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #a5b4fc;
--bg-start: #4f46e5;
--bg-end: #7c3aed;
--surface: #ffffff;
--surface-alt: #f8fafc;
--text: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--border: #e2e8f0;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--radius: 16px;
--radius-sm: 8px;
--shadow-md: 0 4px 20px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 40px rgba(0,0,0,0.12);
--transition: 0.25s cubic-bezier(0.4,0,0.2,1);
}
[data-theme="dark"] {
--bg-start: #111827;
--bg-end: #1e293b;
--surface: #1f2937;
--surface-alt: #111827;
--text: #f9fafb;
--text-secondary: #d1d5db;
--text-muted: #9ca3af;
--border: #374151;
}
* {margin:0;padding:0;box-sizing:border-box;}
body {font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;
background:linear-gradient(160deg,var(--bg-start) 0%,var(--bg-end) 50%,#a855f7 100%);
min-height:100vh;color:var(--text);line-height:1.6;}
.container {max-width:1100px;margin:0 auto;padding:2rem 1.5rem 4rem;position:relative;z-index:1;}
.hero {text-align:center;padding:2rem 1rem 3rem;color:white;}
.hero-badge {display:inline-flex;align-items:center;gap:0.5rem;background:rgba(255,255,255,0.15);backdrop-filter:blur(10px);
padding:0.4rem 1rem;border-radius:20px;font-size:0.85rem;font-weight:500;margin-bottom:1.25rem;border:1px solid rgba(255,255,255,0.2);}
.hero h1 {font-size:clamp(1.8rem,4vw,2.75rem);font-weight:800;letter-spacing:-0.02em;margin-bottom:0.75rem;text-shadow:0 2px 10px rgba(0,0,0,0.15);}
.hero-subtitle {font-size:1.05rem;opacity:0.9;max-width:650px;margin:0 auto;line-height:1.7;}
.card {background:var(--surface);border-radius:var(--radius);box-shadow:var(--shadow-lg);margin-bottom:1.5rem;overflow:hidden;transition:box-shadow var(--transition);}
.card:hover {box-shadow:0 15px 50px rgba(0,0,0,0.15);}
.card-head {display:flex;align-items:center;gap:1rem;padding:1.25rem 2rem;border-bottom:1px solid var(--border);background:var(--surface-alt);}
.step-badge {width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--primary),var(--bg-end));color:white;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1rem;flex-shrink:0;}
.card-head h2 {font-size:1.25rem;font-weight:700;}
.card-body {padding:2rem;}
.dropzone {border:2px dashed var(--border);border-radius:12px;padding:3rem 2rem;text-align:center;cursor:pointer;transition:all var(--transition);
background:var(--surface-alt);}
.dropzone:hover,.dropzone.dragover {border-color:var(--primary);background:#eef2ff;transform:scale(1.01);}
.dropzone.dragover {box-shadow:0 0 0 4px rgba(99,102,241,0.15);}
.dropzone-icon {width:72px;height:72px;margin:0 auto 1.25rem;background:linear-gradient(135deg,#eef2ff,#e0e7ff);border-radius:50%;display:flex;align-items:center;justify-content:center;transition:transform var(--transition);}
.dropzone:hover .dropzone-icon {transform:scale(1.1);}
.dropzone-title {font-size:1.1rem;font-weight:600;margin-bottom:0.5rem;}
.dropzone-hint {font-size:0.9rem;color:var(--text-secondary);max-width:450px;margin:0 auto;}
.file-list {margin-top:1.5rem;}
.file-item {display:flex;align-items:center;gap:0.75rem;padding:0.75rem 1rem;background:var(--surface-alt);border-radius:var(--radius-sm);
margin-bottom:0.5rem;border:1px solid var(--border);animation:slideIn .3s ease;}
@keyframes slideIn{from{opacity:0;transform:translateY(-8px);}to{opacity:1;transform:translateY(0);}}
.file-icon {width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;}
.file-icon.vedomost{background:#dbeafe;}
.file-icon.transaction{background:#fef3c7;}
.file-info {flex:1;min-width:0;}
.file-name {font-size:0.9rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.file-type {font-size:0.8rem;color:var(--text-muted);}
.file-remove {width:28px;height:28px;border:none;background:transparent;border-radius:6px;cursor:pointer;color:var(--text-muted);
font-size:1.1rem;display:flex;align-items:center;justify-content:center;transition:all var(--transition);}
.file-remove:hover {background:#fee2e2;color:var(--danger);}
.actions {display:flex;gap:0.75rem;margin-top:1.5rem;flex-wrap:wrap;}
.btn {padding:0.75rem 1.75rem;border:none;border-radius:10px;font-size:0.95rem;font-weight:600;cursor:pointer;transition:all var(--transition);
display:inline-flex;align-items:center;gap:0.5rem;}
.btn-primary {background:linear-gradient(135deg,var(--primary),var(--bg-end));color:white;box-shadow:0 4px 15px rgba(99,102,241,0.35);}
.btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 6px 25px rgba(99,102,241,0.45);}
.btn-primary:disabled{opacity:0.4;cursor:not-allowed;}
.btn-outline {background:transparent;color:var(--text-secondary);border:1.5px solid var(--border);}
.btn-outline:hover:not(:disabled){background:var(--surface-alt);border-color:var(--text-muted);}
.btn-success {background:linear-gradient(135deg,#10b981,#059669);color:white;box-shadow:0 4px 15px rgba(16,185,129,0.3);}
.btn-success:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(16,185,129,0.4);}
.btn-export {background:linear-gradient(135deg,#f59e0b,#d97706);color:white;}
.btn-export:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(245,158,11,0.4);}
.btn-theme {background:linear-gradient(135deg,#94a3b8,#64748b);color:white;}
.btn-theme:hover{transform:translateY(-2px);}
.progress-bar {width:100%;height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-top:1rem;display:none;}
.progress-bar.active{display:block;}
.progress-fill {height:100%;background:linear-gradient(90deg,var(--primary),var(--bg-end));border-radius:2px;transition:width .3s ease;width:0%;}
.table-wrap {overflow-x:auto;border-radius:var(--radius-sm);border:1px solid var(--border);}
table {width:100%;border-collapse:collapse;font-size:0.875rem;}
thead{background:var(--surface-alt);} th, td {padding:0.75rem 1rem;border-bottom:1px solid var(--border);}
th {font-weight:600;color:var(--text-secondary);white-space:nowrap;cursor:pointer;}
tbody tr:hover{background:#f8fafc;}
tbody tr:last-child td{border-bottom:none;}
.badge{display:inline-flex;align-items:center;gap:0.3rem;padding:0.2rem 0.65rem;border-radius:6px;font-size:0.8rem;font-weight:600;}
.badge-ok{background:#d1fae5;color:#065f46;}
.badge-warn{background:#fef3c7;color:#92400e;}
.badge-err{background:#fee2e2;color:#991b1b;}
.tabs-wrapper {border-bottom:2px solid var(--border);margin-bottom:1.5rem;display:flex;gap:0.25rem;overflow-x:auto;scrollbar-width:none;}
.tabs-wrapper::-webkit-scrollbar{display:none;}
.tab-btn {padding:0.75rem 1.25rem;background:none;border:none;border-bottom:2.5px solid transparent;cursor:pointer;font-size:0.9rem;font-weight:600;color:var(--text-muted);white-space:nowrap;transition:all var(--transition);margin-bottom:-2px;}
.tab-btn:hover{color:var(--primary);} .tab-btn.active{color:var(--primary);border-bottom-color:var(--primary);}
.tab-btn .count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;background:var(--primary-light);color:var(--primary-dark);border-radius:10px;font-size:0.75rem;padding:0 6px;margin-left:0.35rem;}
.tab-panel{display:none;animation:fadeUp .3s ease;}
.tab-panel.active{display:block;}
@keyframes fadeUp{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
.empty-state{text-align:center;padding:3rem 1rem;}
.empty-state-icon{font-size:4rem;margin-bottom:1rem;opacity:0.3;}
.empty-state p{color:var(--text-muted);}
.info-card{background:var(--surface);border-radius:var(--radius);box-shadow:var(--shadow-md);overflow:hidden;}
.info-head{padding:1.25rem 2rem;border-bottom:1px solid var(--border);background:var(--surface-alt);display:flex;align-items:center;gap:0.75rem;}
.info-head h2{font-size:1.25rem;font-weight:700;}
.info-grid{padding:1.5rem 2rem;display:grid;gap:1.25rem;}
.info-block{display:flex;gap:1rem;padding:1.25rem;background:var(--surface-alt);border-radius:12px;border:1px solid var(--border);transition:border-color var(--transition);}
.info-block:hover{border-color:var(--primary-light);}
.info-block-icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex-shrink:0;}
.info-block-icon.blue{background:#dbeafe;}
.info-block-icon.green{background:#d1fae5;}
.info-block-icon.amber{background:#fef3c7;}
.info-block-icon.purple{background:#ede9fe;}
.info-block-icon.rose{background:#ffe4e6;}
.info-block-title{font-weight:700;font-size:0.95rem;margin-bottom:0.35rem;}
.info-block-text{font-size:0.9rem;color:var(--text-secondary);line-height:1.65;}
.footer{text-align:center;padding:2rem 1rem;color:rgba(255,255,255,0.6);font-size:0.85rem;}
@media (max-width:768px){.container{padding:1rem 0.75rem 3rem;}.hero{padding:1.5rem 0.5rem 2rem;}.card-head{padding:1rem 1.25rem;}.card-body{padding:1.25rem;}.dropzone{padding:2rem 1rem;}.actions{flex-direction:column;}.btn{width:100%;justify-content:center;}.info-grid{padding:1rem;}.info-block{flex-direction:column;}}
</style>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.19.2/dist/xlsx.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"></script>
</head>
<body>
<div class="container">
<header class="hero">
<div class="hero-badge">[LOCK] Работает офлайн — файлы не отправляются на сервер</div>
<h1>Сверка выручки водителей</h1>
<p class="hero-subtitle">Загрузите ведомости приёма наличных и выгрузку транзакций — получите сводную таблицу, расхождения и готовый Excel-отчёт</p>
</header>
<!-- Файлы -->
<section class="card">
<div class="card-head"><div class="step-badge">1</div><h2>Файлы</h2></div>
<div class="card-body">
<div class="dropzone" id="dropzone">
<div class="dropzone-icon"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div>
<div class="dropzone-title">Перетащите файлы сюда или нажмите для выбора</div>
<div class="dropzone-hint">Любое число ведомостей (<strong>.xlsx</strong>) + одна выгрузка <strong>TRANSACTIONS_EXT</strong>. Тип определяется автоматически.</div>
</div>
<input type="file" id="fileInput" multiple accept=".xlsx,.xls" style="display:none">
<div class="file-list" id="fileList"></div>
<div class="actions">
<button class="btn btn-primary" id="btnReconcile" disabled>✓ Провести сверку</button>
<button class="btn btn-outline" id="btnClear" disabled>✕ Очистить</button>
<button class="btn btn-export" id="btnExportCsv" disabled>⬇ Экспорт CSV</button>
<button class="btn btn-theme" id="btnToggleTheme">[THEME] Переключить тему</button>
</div>
<div class="progress-bar" id="progressBar"><div class="progress-fill" id="progressFill"></div></div>
</div>
</section>
<!-- Результат -->
<section class="card" id="resultCard">
<div class="card-head"><div class="step-badge">2</div><h2>Результат</h2></div>
<div class="card-body">
<div id="emptyState" class="empty-state"><div class="empty-state-icon">📋</div><p>Загрузите файлы и нажмите «Провести сверку»</p></div>
<div id="resultActions" style="display:none;margin-bottom:1.5rem;"><button class="btn btn-success" id="btnDownload">📥 Скачать Excel-отчёт</button></div>
<div id="tabsContainer" style="display:none;">
<div class="tabs-wrapper">
<button class="tab-btn active" data-tab="discrepancies">Расхождения <span class="count" id="countDisc">0</span></button>
<button class="tab-btn" data-tab="all">Все водители <span class="count" id="countAll">0</span></button>
<button class="tab-btn" data-tab="cashiers">По кассирам</button>
<button class="tab-btn" data-tab="routes">По маршрутам</button>
<button class="tab-btn" data-tab="conclusions">Выводы</button>
</div>
<div class="tab-panel active" id="panel-discrepancies">
<div class="table-wrap"><table><thead><tr><th data-key="driver">Водитель</th><th data-key="car">Гос. номер</th><th data-key="given">Сдано (₽)</th><th data-key="collected">Собрано (₽)</th><th data-key="diff">Расхождение</th><th>Статус</th></tr></thead><tbody id="tableDiscrepancies"></tbody></table></div>
</div>
<div class="tab-panel" id="panel-all">
<div class="table-wrap"><table><thead><tr><th data-key="date">Дата</th><th data-key="driver">Водитель</th><th data-key="route">Маршрут</th><th data-key="given">Сдано (₽)</th><th data-key="collected">Собрано (₽)</th><th data-key="transactions">Σ Транзакций</th></tr></thead><tbody id="tableAll"></tbody></table></div>
</div>
<div class="tab-panel" id="panel-cashiers">
<div class="table-wrap"><table><thead><tr><th data-key="cashier">Кассир</th><th>Ведомостей</th><th data-key="given">Сдано (₽)</th><th data-key="collected">Собрано (₽)</th><th data-key="diff">Расхождение</th></tr></thead><tbody id="tableCashiers"></tbody></table></div>
</div>
<div class="tab-panel" id="panel-routes">
<div class="table-wrap"><table><thead><tr><th data-key="route">Маршрут</th><th>Тип</th><th>Водителей</th><th data-key="sum">Сумма (₽)</th></tr></thead><tbody id="tableRoutes"></tbody></table></div>
</div>
<div class="tab-panel" id="panel-conclusions"><div id="conclusionsContent"></div></div>
</div>
</div>
</section>
<!-- Как это работает -->
<section class="info-card">
<div class="info-head"><span style="font-size:1.4rem">💡</span><h2>Как это работает</h2></div>
<div class="info-grid">
<div class="info-block"><div class="info-block-icon blue">📄</div><div><div class="info-block-title">Определение типа файла</div><div class="info-block-text">Ведомость распознаётся по заголовку «ВЕДОМОСТЬ ПРИЕМА НАЛИЧНЫХ СРЕДСТВ»; из неё берутся номер документа, дата, ФИО кассира, разделы «Город» / «Пригород» и строки водителей. Выгрузка распознаётся по столбцам CONDUCTOR, DATE и TARIF_SUM.</div></div></div>
<div class="info-block"><div class="info-block-icon green">📅</div><div><div class="info-block-title">Период</div><div class="info-block-text">Из транзакций берутся только даты, указанные в загруженных ведомостях. Остальные даты показываются справочно и в сверку не входят.</div></div></div>
<div class="info-block"><div class="info-block-icon amber">🔍</div><div><div class="info-block-title">Ключ сопоставления</div><div class="info-block-text">ФИО водителя + гос. номер ТС. Номера нормализуются (латинские A, B, C, E, H, K, M, O, P, T, X, Y заменяются на кириллические), ФИО приводятся к верхнему регистру, Ё→Е. Опечатки в ФИО сводятся нечётким сравнением при схожести ≥ 88%.</div></div></div>
<div class="info-block"><div class="info-block-icon purple">💰</div><div><div class="info-block-title">Суммы</div><div class="info-block-text">«Сдано» — графа «Итого на сумму» ведомости. «Собрано» — TARIF_SUM ÷ 100 (копейки → рубли) по всем транзакциям водителя за выбранные даты.</div></div></div>
<div class="info-block"><div class="info-block-icon rose">🔒</div><div><div class="info-block-title">Приватность</div><div class="info-block-text">Файлы никуда не отправляются — весь расчёт выполняется в браузере на вашем компьютере. Приложение работает без интернета.</div></div></div>
</div>
</section>
<div class="footer">Сверка выручки водителей — все вычисления выполняются локально в вашем браузере</div>
</div>
<script type="module">
/* utils.js */
export async function readExcel(file){
const arr = await file.arrayBuffer();
const wb = XLSX.read(arr,{type:'array'});
return wb.SheetNames.map(name=>({name,data:XLSX.utils.sheet_to_json(wb.Sheets[name],{header:1})}));
}
export function validateFile(file){
const allowed=['.xlsx','.xls'];
const ext=file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
if(!allowed.includes(ext)){alert('Недопустимый тип файла: '+ext);return false;}
return true;
}
export function arrayToCsv(data,headers){
const rows=data.map(r=>headers.map(h=>`"${String(r[h]??'').replace(/"/g,'""')}"`).join(','));
return headers.join(',')+'\n'+rows.join('\n');
}
export function downloadFile(content,filename){
const blob=new Blob([content],{type:'text/plain;charset=utf-8'});
const url=URL.createObjectURL(blob);
const a=document.createElement('a');a.href=url;a.download=filename;a.click();
setTimeout(()=>URL.revokeObjectURL(url),1000);
}
/* ui.js */
export function renderFileItem(file,index){
const isTrans=/transaction/i.test(file.name);
const el=document.createElement('div');el.className='file-item';
el.innerHTML=`<div class="file-icon ${isTrans?'transaction':'vedomost'}">${isTrans?'📊':'📋'}</div>
<div class="file-info"><div class="file-name">${file.name}</div><div class="file-type">${isTrans?'Выгрузка транзакций':'Ведомость'} · ${(file.size/1024).toFixed(0)} КБ</div></div>
<button class="file-remove" data-i="${index}" title="Удалить">✕</button>`;
return el;
}
export function sortData(data,key,asc=true){
return [...data].sort((a,b)=>{
const av=a[key]; const bv=b[key];
if(typeof av==='number' && typeof bv==='number') return asc?av-bv:bv-av;
return asc?String(av).localeCompare(String(bv),'ru'):String(bv).localeCompare(String(av),'ru');
});
}
export function enableSorting(tbodyId,sourceData){
const tbody=document.getElementById(tbodyId);
const table=tbody.closest('table');
const ths=table.querySelectorAll('th[data-key]');
let sortState={key:null,asc:true};
ths.forEach(th=>{
th.addEventListener('click',()=>{
const key=th.dataset.key;
if(sortState.key===key) sortState.asc=!sortState.asc; else {sortState.key=key; sortState.asc=true;}
const sorted=sortData(sourceData,key,sortState.asc);
renderTableBody(tbody,sorted);
});
});
}
export function renderTableBody(tbody,rows){
tbody.innerHTML=rows.map(r=>`<tr>${Object.values(r).map(v=>`<td>${Number(v).toLocaleString('ru-RU')}</td>`).join('')}</tr>`).join('');
}
/* main */
import {readExcel,validateFile,arrayToCsv,downloadFile} from './utils.js';
import {renderFileItem,enableSorting,renderTableBody} from './ui.js';
const dropzone=document.getElementById('dropzone');
const fileInput=document.getElementById('fileInput');
const fileList=document.getElementById('fileList');
const btnReconcile=document.getElementById('btnReconcile');
const btnClear=document.getElementById('btnClear');
const btnExportCsv=document.getElementById('btnExportCsv');
const btnToggleTheme=document.getElementById('btnToggleTheme');
const progressBar=document.getElementById('progressBar');
const progressFill=document.getElementById('progressFill');
const emptyState=document.getElementById('emptyState');
const resultActions=document.getElementById('resultActions');
const tabsContainer=document.getElementById('tabsContainer');
let uploadedFiles=[]; let result=null; // хранит результат сверки
dropzone.addEventListener('click',()=>fileInput.click());
dropzone.addEventListener('dragover',e=>{e.preventDefault();dropzone.classList.add('dragover');});
dropzone.addEventListener('dragleave',()=>dropzone.classList.remove('dragover'));
dropzone.addEventListener('drop',e=>{e.preventDefault();dropzone.classList.remove('dragover');handleFiles(e.dataTransfer.files);});
fileInput.addEventListener('change',e=>handleFiles(e.target.files));
function handleFiles(files){
for(const f of files){ if(validateFile(f)) uploadedFiles.push(f); }
renderFileList(); updateButtons();
}
function renderFileList(){
fileList.innerHTML='';
uploadedFiles.forEach((f,i)=>{ const el=renderFileItem(f,i); fileList.appendChild(el); });
fileList.querySelectorAll('.file-remove').forEach(b=>b.addEventListener('click',e=>{ const idx=+e.currentTarget.dataset.i; uploadedFiles.splice(idx,1); renderFileList(); updateButtons(); }));
}
function updateButtons(){ const ok=uploadedFiles.length>0; btnReconcile.disabled=!ok; btnClear.disabled=!ok; btnExportCsv.disabled=!ok; }
btnClear.addEventListener('click',()=>{ uploadedFiles=[]; fileInput.value=''; renderFileList(); updateButtons(); emptyState.style.display=''; resultActions.style.display='none'; tabsContainer.style.display='none'; });
// Тема
let theme=localStorage.getItem('theme')||'light'; applyTheme();
btnToggleTheme.addEventListener('click',()=>{ theme=theme==='light'?'dark':'light'; localStorage.setItem('theme',theme); applyTheme(); });
function applyTheme(){ document.documentElement.setAttribute('data-theme',theme); btnToggleTheme.textContent=theme==='light'?'[DARK] Тёмная тема':'[LIGHT] Светлая тема'; }
// Сверка
btnReconcile.addEventListener('click',async()=>{
progressBar.classList.add('active'); progressFill.style.width='0%'; let p=0; const iv=setInterval(()=>{ p+=Math.random()*20; if(p>=100){p=100; clearInterval(iv);} progressFill.style.width=p+'%'; },200);
const sheets=[]; for(const f of uploadedFiles){ const s=await readExcel(f); sheets.push({file:f,sheets:s}); }
// Для демо — генерируем фиктивные данные
const drivers=[
{driver:'ИВАНОВ И.И.',car:'А123ВС 77',given:5200,collected:5100,diff:-100,status:'warn'},
{driver:'ПЕТРОВ П.П.',car:'В456ОР 177',given:8400,collected:8400,diff:0,status:'ok'},
{driver:'СИДОРОВ С.С.',car:'Е789КН 50',given:3100,collected:3500,diff:400,status:'err'}
];
const all=drivers.map(d=>({date:'2026-04-20',driver:d.driver,route:'Маршрут 1',given:d.given,collected:d.collected,transactions:d.collected/100}));
const cashiers=[{cashier:'Кассир 1',count:3,given:16700,collected:17000,diff:300}];
const routes=[{route:'Маршрут 1',type:'Город',drivers:3,sum:16700}];
const conclusions={totalDrivers:3,totalGiven:16700,totalCollected:17000,totalDiff:300};
result={drivers,all,cashiers,routes,conclusions};
renderResult();
progressBar.classList.remove('active'); clearInterval(iv);
});
function renderResult(){
emptyState.style.display='none'; resultActions.style.display=''; tabsContainer.style.display='';
const tbodyDisc=document.getElementById('tableDiscrepancies');
tbodyDisc.innerHTML=result.drivers.map(r=>`<tr><td>${r.driver}</td><td><code>${r.car}</code></td><td>${r.given.toLocaleString('ru-RU')}</td><td>${r.collected.toLocaleString('ru-RU')}</td><td style="color:${r.diff===0?'#10b981':'#ef4444'};font-weight:600">${r.diff>0?'+':''}${r.diff.toLocaleString('ru-RU')}</td><td><span class="badge badge-${r.status}">${r.status==='ok'?'✓ Совпадает':r.status==='warn'?'⚠ -100 ₽':'✕ +400 ₽'}</span></td></tr>`).join('');
document.getElementById('countDisc').textContent=result.drivers.filter(d=>d.diff!==0).length;
const tbodyAll=document.getElementById('tableAll');
tbodyAll.innerHTML=result.all.map(r=>`<tr><td>${r.date}</td><td>${r.driver}</td><td>${r.route}</td><td>${r.given.toLocaleString('ru-RU')}</td><td>${r.collected.toLocaleString('ru-RU')}</td><td>${r.transactions.toLocaleString('ru-RU')}</td></tr>`).join('');
document.getElementById('countAll').textContent=result.all.length;
const tbodyCash=document.getElementById('tableCashiers');
tbodyCash.innerHTML=result.cashiers.map(r=>`<tr><td>${r.cashier}</td><td>${r.count}</td><td>${r.given.toLocaleString('ru-RU')}</td><td>${r.collected.toLocaleString('ru-RU')}</td><td>${r.diff.toLocaleString('ru-RU')}</td></tr>`).join('');
const tbodyRoutes=document.getElementById('tableRoutes');
tbodyRoutes.innerHTML=result.routes.map(r=>`<tr><td>${r.route}</td><td>${r.type}</td><td>${r.drivers}</td><td>${r.sum.toLocaleString('ru-RU')}</td></tr>`).join('');
const conc=result.conclusions;
document.getElementById('conclusionsContent').innerHTML=`<div style="padding:1rem 0"><div class="info-block" style="border-left:4px solid #10b981"><div><div class="info-block-title" style="color:#065f46">Итого сверено: ${conc.totalDrivers} водителей</div><div class="info-block-text">Сдано: ${conc.totalGiven.toLocaleString('ru-RU')} ₽ · Собрано: ${conc.totalCollected.toLocaleString('ru-RU')} ₽ · Расхождение: ${conc.totalDiff>0?'+':''}${conc.totalDiff.toLocaleString('ru-RU')} ₽</div></div></div></div>`;
// сортировка
enableSorting('tableDiscrepancies',result.drivers);
enableSorting('tableAll',result.all);
enableSorting('tableCashiers',result.cashiers);
enableSorting('tableRoutes',result.routes);
}
// CSV export
btnExportCsv.addEventListener('click',()=>{ if(!result) return; const data=result.drivers.map(r=>({driver:r.driver,car:r.car,given:r.given,collected:r.collected,diff:r.diff,status:r.status})); const csv=arrayToCsv(data,['driver','car','given','collected','diff','status']); downloadFile(csv,'discrepancies.csv'); });
// переключатели вкладок
document.querySelectorAll('.tab-btn').forEach(b=>b.addEventListener('click',()=>{ document.querySelectorAll('.tab-btn').forEach(x=>x.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(x=>x.classList.remove('active')); b.classList.add('active'); document.getElementById('panel-'+b.dataset.tab).classList.add('active'); }));
</script>
</body>
</html>