Add full implementation without surrogate emojis

This commit is contained in:
2026-07-30 16:45:56 +03:00
parent d6b303b038
commit 5b5a0e68ee
+312 -4
View File
@@ -26,15 +26,323 @@
--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; }
/* ... все остальные стили и скрипт из оригинального кода ... */
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">
<!-- Здесь разместите ваш HTML‑контент, скрипты и стили -->
<p>Тестовый файл для репозитория «Сверка выручки водителей». Добавьте сюда улучшенный код.</p>
<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>