Files
driver-revenue-reconciliation/index-single.html
T

272 lines
48 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>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.19.2/dist/xlsx.full.min.js"></script>
<style>
/* ===== Переменные и тема ===== */
:root{--primary:#6366f1;--primary-dark:#4f46e5;--primary-light:#a5b4fc;--bg-start:#4f46e5;--bg-end:#7c3aed;--surface:#fff;--surface-alt:#f8fafc;--text:#1e293b;--text-secondary:#64748b;--text-muted:#94a3b8;--border:#e2e8f0;--success:#10b981;--success-bg:#d1fae5;--success-text:#065f46;--warning:#f59e0b;--warning-bg:#fef3c7;--warning-text:#92400e;--danger:#ef4444;--danger-bg:#fee2e2;--danger-text:#991b1b;--radius:16px;--radius-sm:8px;--radius-xs:6px;--shadow-md:0 4px 20px rgba(0,0,0,.1);--shadow-lg:0 10px 40px rgba(0,0,0,.12);--transition:.25s cubic-bezier(.4,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;--success-bg:#064e3b;--success-text:#6ee7b7;--warning-bg:#78350f;--warning-text:#fcd34d;--danger-bg:#7f1d1d;--danger-text:#fca5a5}
*{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:#fff}
.hero-badge{display:inline-flex;align-items:center;gap:.5rem;background:rgba(255,255,255,.15);backdrop-filter:blur(10px);padding:.4rem 1rem;border-radius:20px;font-size:.85rem;font-weight:500;margin-bottom:1.25rem;border:1px solid rgba(255,255,255,.2)}
.hero h1{font-size:clamp(1.8rem,4vw,2.75rem);font-weight:800;letter-spacing:-.02em;margin-bottom:.75rem;text-shadow:0 2px 10px rgba(0,0,0,.15)}
.hero-subtitle{font-size:1.05rem;opacity:.9;max-width:650px;margin:0 auto;line-height:1.7}
/* ===== Статус-бар ===== */
.status-bar{background:var(--surface);border-radius:var(--radius-sm);padding:.75rem 1.25rem;margin-bottom:1rem;font-size:.9rem;font-weight:500;box-shadow:var(--shadow-md);animation:slideIn .3s ease;border-left:4px solid var(--primary)}
.status-info{border-left-color:var(--primary);color:var(--text)}
.status-success{border-left-color:var(--success);color:var(--success-text);background:var(--success-bg)}
.status-warning{border-left-color:var(--warning);color:var(--warning-text);background:var(--warning-bg)}
.status-error{border-left-color:var(--danger);color:var(--danger-text);background:var(--danger-bg)}
/* ===== Карточки ===== */
.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,.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:#fff;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 ===== */
.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)}
[data-theme=dark] .dropzone:hover,[data-theme=dark] .dropzone.dragover{background:#1e1b4b}
.dropzone.dragover{box-shadow:0 0 0 4px rgba(99,102,241,.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)}
[data-theme=dark] .dropzone-icon{background:linear-gradient(135deg,#312e81,#4338ca)}
.dropzone:hover .dropzone-icon{transform:scale(1.1)}
.dropzone-title{font-size:1.1rem;font-weight:600;margin-bottom:.5rem}
.dropzone-hint{font-size:.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:.75rem;padding:.75rem 1rem;background:var(--surface-alt);border-radius:var(--radius-sm);margin-bottom:.5rem;border:1px solid var(--border);animation:slideIn .3s ease}
.file-item.file-error{border-color:var(--danger);background:var(--danger-bg)}
@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}
[data-theme=dark] .file-icon.vedomost{background:#1e3a5f}
.file-icon.transaction{background:#fef3c7}
[data-theme=dark] .file-icon.transaction{background:#5c3d0e}
.file-info{flex:1;min-width:0}
.file-name{font-size:.9rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.file-type{font-size:.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:var(--danger-bg);color:var(--danger)}
/* ===== Кнопки ===== */
.actions{display:flex;gap:.75rem;margin-top:1.5rem;flex-wrap:wrap}
.btn{padding:.75rem 1.75rem;border:none;border-radius:10px;font-size:.95rem;font-weight:600;cursor:pointer;transition:all var(--transition);display:inline-flex;align-items:center;gap:.5rem;white-space:nowrap}
.btn:disabled{opacity:.4;cursor:not-allowed;transform:none!important}
.btn-primary{background:linear-gradient(135deg,var(--primary),var(--bg-end));color:#fff;box-shadow:0 4px 15px rgba(99,102,241,.35)}
.btn-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 6px 25px rgba(99,102,241,.45)}
.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:#fff;box-shadow:0 4px 15px rgba(16,185,129,.3)}
.btn-success:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 6px 20px rgba(16,185,129,.4)}
.btn-export{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff}
.btn-export:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 6px 20px rgba(245,158,11,.4)}
.btn-theme{background:linear-gradient(135deg,#94a3b8,#64748b);color:#fff}
.btn-theme:hover:not(:disabled){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;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:.875rem}
thead{background:var(--surface-alt)}
th,td{padding:.75rem 1rem;border-bottom:1px solid var(--border);text-align:left}
th{font-weight:600;color:var(--text-secondary);white-space:nowrap;user-select:none}
th[data-key]::after{content:' ↕';opacity:.3;font-size:.75rem}
th.sort-asc::after{content:' ↑';opacity:1;color:var(--primary)}
th.sort-desc::after{content:' ↓';opacity:1;color:var(--primary)}
tbody tr:hover{background:var(--surface-alt)}
tbody tr:last-child td{border-bottom:none}
/* ===== Бейджи ===== */
.badge{display:inline-flex;align-items:center;gap:.3rem;padding:.2rem .65rem;border-radius:6px;font-size:.8rem;font-weight:600}
.badge-ok{background:var(--success-bg);color:var(--success-text)}
.badge-warn{background:var(--warning-bg);color:var(--warning-text)}
.badge-err{background:var(--danger-bg);color:var(--danger-text)}
/* ===== Вкладки ===== */
.tabs-wrapper{border-bottom:2px solid var(--border);margin-bottom:1.5rem;display:flex;gap:.25rem;overflow-x:auto;scrollbar-width:none}
.tabs-wrapper::-webkit-scrollbar{display:none}
.tab-btn{padding:.75rem 1.25rem;background:none;border:none;border-bottom:2.5px solid transparent;cursor:pointer;font-size:.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:.75rem;padding:0 6px;margin-left:.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:.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:.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}
[data-theme=dark] .info-block-icon.blue{background:#1e3a5f}
.info-block-icon.green{background:#d1fae5}
[data-theme=dark] .info-block-icon.green{background:#064e3b}
.info-block-icon.amber{background:#fef3c7}
[data-theme=dark] .info-block-icon.amber{background:#78350f}
.info-block-icon.purple{background:#ede9fe}
[data-theme=dark] .info-block-icon.purple{background:#2e1065}
.info-block-icon.rose{background:#ffe4e6}
[data-theme=dark] .info-block-icon.rose{background:#4c0519}
.info-block-title{font-weight:700;font-size:.95rem;margin-bottom:.35rem}
.info-block-text{font-size:.9rem;color:var(--text-secondary);line-height:1.65}
/* ===== Сводка ===== */
.summary-card{padding:1rem 0}
.summary-card .info-block{border-left:4px solid var(--success)}
.summary-card .info-block.warn{border-left-color:var(--warning)}
.summary-card .info-block.err{border-left-color:var(--danger)}
/* ===== Подвал ===== */
.footer{text-align:center;padding:2rem 1rem;color:rgba(255,255,255,.6);font-size:.85rem}
/* ===== Адаптивность ===== */
@media(max-width:768px){.container{padding:1rem .75rem 3rem}.hero{padding:1.5rem .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}th,td{padding:.5rem .75rem}}
/* ===== Утилиты ===== */
code{font-family:'SF Mono','Fira Code','Consolas',monospace;font-size:.85em;background:var(--surface-alt);padding:.1em .3em;border-radius:4px;border:1px solid var(--border)}
.diff-positive{color:var(--danger);font-weight:600}
.diff-negative{color:var(--success);font-weight:600}
.diff-zero{color:var(--text-muted)}
</style>
</head>
<body>
<div class="container">
<header class="hero">
<div class="hero-badge">🔒 Работает офлайн — файлы не отправляются на сервер</div>
<h1>Сверка выручки водителей</h1>
<p class="hero-subtitle">Загрузите ведомости приёма наличных и выгрузку транзакций — получите сводную таблицу, расхождения и готовый Excel-отчёт</p>
</header>
<div class="status-bar" id="statusBar" style="display:none"></div>
<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">🌙 Тёмная тема</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="section">Тип</th><th data-key="given">Сдано (₽)</th><th data-key="collected">Собрано (₽)</th><th data-key="txCount">Транзакций</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">ФИО водителя + гос. номер ТС. Номера нормализуются (латиница→кириллица), ФИО — верхний регистр, Ё→Е. Опечатки в ФИО сводятся нечётким сравнением при схожести ≥ 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>
/* =================================================================
ЕДИНЫЙ СКРИПТ — все модули объединены (single-file версия)
================================================================= */
/* ---------- utils ---------- */
function readExcel(file){return file.arrayBuffer().then(arr=>{const wb=XLSX.read(arr,{type:'array',codepage:1251});return wb.SheetNames.map(name=>({name,data:XLSX.utils.sheet_to_json(wb.Sheets[name],{header:1,defval:''})}))})}
function validateFile(file){const ext=file.name.slice(file.name.lastIndexOf('.')).toLowerCase();if(!['.xlsx','.xls'].includes(ext)){alert('❌ Недопустимый тип: '+ext+'. Разрешены: .xlsx, .xls');return false}return true}
function arrayToCsv(data,headers){return '\uFEFF'+headers.join(',')+'\n'+data.map(r=>headers.map(h=>'"'+String(r[h]??'').replace(/"/g,'""')+'"').join(',')).join('\n')}
function downloadFile(content,filename){const blob=new Blob([content],{type:'application/octet-stream'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=filename;document.body.appendChild(a);a.click();document.body.removeChild(a);setTimeout(()=>URL.revokeObjectURL(url),1000)}
function downloadExcel(data,filename){const wb=XLSX.utils.book_new();const ws=XLSX.utils.json_to_sheet(data);const colWidths=Object.keys(data[0]||{}).map(key=>{const maxLen=data.reduce((m,r)=>Math.max(m,String(r[key]??'').length),key.length);return{wch:Math.min(maxLen+2,40)}});ws['!cols']=colWidths;XLSX.utils.book_append_sheet(wb,ws,'Сверка');downloadFile(XLSX.write(wb,{bookType:'xlsx',type:'array'}),filename)}
function formatMoney(num){if(num===null||num===undefined||isNaN(num))return'—';return num.toLocaleString('ru-RU',{minimumFractionDigits:0,maximumFractionDigits:0})}
/* ---------- ui ---------- */
function escapeHtml(str){const d=document.createElement('div');d.textContent=str;return d.innerHTML}
function renderFileItem(file,index,type){const isTrans=type==='transaction';const el=document.createElement('div');el.className='file-item';el.dataset.index=index;el.innerHTML='<div class="file-icon '+(isTrans?'transaction':'vedomost')+'">'+(isTrans?'📊':'📋')+'</div><div class="file-info"><div class="file-name">'+escapeHtml(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}
function renderFileError(fileName,message){const el=document.createElement('div');el.className='file-item file-error';el.innerHTML='<div class="file-icon" style="background:#fee2e2;">⚠️</div><div class="file-info"><div class="file-name">'+escapeHtml(fileName)+'</div><div class="file-type" style="color:#ef4444;">'+escapeHtml(message)+'</div></div>';return el}
function sortData(data,key,asc){asc=asc!==false;return[...data].sort((a,b)=>{let av=a[key],bv=b[key];if(av==null)av='';if(bv==null)bv='';if(typeof av==='number'&&typeof bv==='number')return asc?av-bv:bv-av;const na=parseFloat(String(av).replace(/\s/g,'').replace(',','.'));const nb=parseFloat(String(bv).replace(/\s/g,'').replace(',','.'));if(!isNaN(na)&&!isNaN(nb))return asc?na-nb:nb-na;return asc?String(av).localeCompare(String(bv),'ru'):String(bv).localeCompare(String(av),'ru')})}
function enableSorting(tbodyId,sourceData,renderFn){const tbody=document.getElementById(tbodyId);if(!tbody)return;const table=tbody.closest('table');if(!table)return;const ths=table.querySelectorAll('th[data-key]');let st={key:null,asc:true};ths.forEach(th=>{th.style.cursor='pointer';th.addEventListener('click',()=>{const key=th.dataset.key;if(st.key===key)st.asc=!st.asc;else{st.key=key;st.asc=true}ths.forEach(t=>t.classList.remove('sort-asc','sort-desc'));th.classList.add(st.asc?'sort-asc':'sort-desc');renderFn(sortData(sourceData,key,st.asc))})})}
function renderTableBody(tbody,rows,keys,formatter){if(!rows||rows.length===0){tbody.innerHTML='<tr><td colspan="'+(keys||Object.keys(rows[0]||{})).length+'" style="text-align:center;padding:2rem;color:var(--text-muted)">Нет данных</td></tr>';return}tbody.innerHTML=rows.map(row=>{const cells=(keys||Object.keys(row)).map(key=>{const val=row[key];if(formatter)return formatter(val,key,row);if(val==null||val==='')return'—';if(typeof val==='number')return formatMoney(val);return escapeHtml(String(val))}).join('');return'<tr>'+cells+'</tr>'}).join('')}
function initTabs(){document.querySelectorAll('.tab-btn').forEach(btn=>{btn.addEventListener('click',()=>{document.querySelectorAll('.tab-btn').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.tab-panel').forEach(x=>x.classList.remove('active'));btn.classList.add('active');const p=document.getElementById('panel-'+btn.dataset.tab);if(p)p.classList.add('active')})})}
let currentTheme=localStorage.getItem('theme')||'light';
function applyTheme(){document.documentElement.setAttribute('data-theme',currentTheme);const btn=document.getElementById('btnToggleTheme');if(btn)btn.textContent=currentTheme==='light'?'🌙 Тёмная тема':'☀️ Светлая тема'}
function initTheme(){applyTheme();document.getElementById('btnToggleTheme')?.addEventListener('click',()=>{currentTheme=currentTheme==='light'?'dark':'light';localStorage.setItem('theme',currentTheme);applyTheme()})}
function showProgress(duration){duration=duration||800;const bar=document.getElementById('progressBar');const fill=document.getElementById('progressFill');if(!bar||!fill)return Promise.resolve();bar.classList.add('active');fill.style.width='0%';return new Promise(resolve=>{requestAnimationFrame(()=>{fill.style.transition='width '+duration+'ms cubic-bezier(0.4,0,0.2,1)';fill.style.width='95%'});setTimeout(()=>{fill.style.width='100%';setTimeout(()=>{bar.classList.remove('active');fill.style.width='0%';fill.style.transition='none';resolve()},300)},duration+200)})}
function resetProgress(){const bar=document.getElementById('progressBar');const fill=document.getElementById('progressFill');if(bar)bar.classList.remove('active');if(fill){fill.style.width='0%';fill.style.transition='none'}}
/* ---------- parser ---------- */
const VEDOMOST_HEADER='ВЕДОМОСТЬ ПРИЕМА НАЛИЧНЫХ СРЕДСТВ';
const LATIN_TO_CYRILLIC={A:'А',B:'В',C:'С',E:'Е',H:'Н',K:'К',M:'М',O:'О',P:'Р',T:'Т',X:'Х',Y:'У'};
const FUZZY_THRESHOLD=0.88;
function detectFileType(filename,firstSheet){if(hasHeader(firstSheet,VEDOMOST_HEADER))return'vedomost';const lower=filename.toLowerCase();if(lower.includes('transaction')||lower.includes('transactions')||lower.includes('выгрузка'))return'transaction';if(hasTransactionColumns(firstSheet))return'transaction';if(hasHeader(firstSheet,'ВЕДОМОСТЬ'))return'vedomost';return null}
function hasHeader(sheetData,header){if(!sheetData||sheetData.length===0)return false;for(let i=0;i<Math.min(sheetData.length,20);i++){const row=sheetData[i];if(!row)continue;const joined=row.filter(c=>c!=null).map(String).join(' ').toUpperCase();if(joined.includes(header.toUpperCase()))return true}return false}
function hasTransactionColumns(sheetData){if(!sheetData||sheetData.length===0)return false;for(let i=0;i<Math.min(sheetData.length,5);i++){const row=sheetData[i];if(!row)continue;const cells=row.map(c=>String(c).toUpperCase().trim());if(cells.some(c=>c.includes('CONDUCTOR'))&&cells.some(c=>c.includes('DATE'))&&cells.some(c=>c.includes('TARIF_SUM')||c.includes('TARIF')))return true}return false}
function normalizePlate(plate){if(!plate)return'';let s=String(plate).toUpperCase().trim();s=s.split('').map(ch=>LATIN_TO_CYRILLIC[ch]||ch).join('');s=s.replace(/[^А-ЯЁA-Z0-9]/gi,'');return s}
function normalizeName(name){if(!name)return'';return String(name).toUpperCase().trim().replace(/Ё/g,'Е').replace(/\s+/g,' ')}
function nameSimilarity(a,b){if(!a||!b)return 0;const s1=normalizeName(a),s2=normalizeName(b);if(s1===s2)return 1;if(s1.length===0||s2.length===0)return 0;const dist=levenshtein(s1,s2);return 1-dist/Math.max(s1.length,s2.length)}
function levenshtein(a,b){const m=a.length,n=b.length;const dp=Array.from({length:m+1},()=>Array(n+1).fill(0));for(let i=0;i<=m;i++)dp[i][0]=i;for(let j=0;j<=n;j++)dp[0][j]=j;for(let i=1;i<=m;i++){for(let j=1;j<=n;j++){dp[i][j]=a[i-1]===b[j-1]?dp[i-1][j-1]:1+Math.min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])}}return dp[m][n]}
function parseVedomost(sheetData){if(!sheetData||sheetData.length===0)return null;const result={docNumber:'',date:'',cashier:'',sections:{city:[],suburb:[]},drivers:[]};let currentSection=null,headerFound=false,tableStarted=false,tableEnded=false;for(let i=0;i<sheetData.length;i++){const row=sheetData[i];if(!row||row.length===0)continue;const cells=row.map(c=>String(c).trim());if(!headerFound){const joined=cells.join(' ').toUpperCase();if(joined.includes(VEDOMOST_HEADER)){headerFound=true}continue}if(!result.docNumber){const numIdx=cells.findIndex(c=>c.toUpperCase().includes('ДОКУМЕНТ'));if(numIdx>=0){result.docNumber=cells[numIdx+1]||'';const dateIdx=cells.findIndex(c=>/^\d{2}[./-]\d{2}[./-]\d{4}$/.test(c));if(dateIdx>=0)result.date=cells[dateIdx]}}if(!result.date){const dateMatch=cells.find(c=>/^\d{2}[./-]\d{2}[./-]\d{4}$/.test(c));if(dateMatch)result.date=dateMatch}if(!result.cashier){const cashIdx=cells.findIndex(c=>c.toUpperCase().includes('КАССИР'));if(cashIdx>=0&&cashIdx+1<cells.length&&cells[cashIdx+1].length>2)result.cashier=cells[cashIdx+1]}const joinedRow=cells.join(' ').toUpperCase();if(joinedRow.includes('ГОРОД')||joinedRow.includes('ГОР.')||(joinedRow.includes('ГОР')&&!joinedRow.includes('ПРИГОР'))){currentSection='city';tableStarted=false;continue}if(joinedRow.includes('ПРИГОРОД')||joinedRow.includes('ПРИГ.')){currentSection='suburb';tableStarted=false;continue}if(cells.every(c=>c==='')){if(tableStarted)tableEnded=true;continue}const headerRow=cells.join(' ').toUpperCase();if(headerRow.includes('ФИО')||headerRow.includes('Ф.И.О')||headerRow.includes('ВОДИТЕЛЬ')||headerRow.includes('ФАМИЛИЯ')||(headerRow.includes('ФИО')&&(headerRow.includes('СУММА')||headerRow.includes('МАРШРУТ')))){tableStarted=true;tableEnded=false;continue}if(tableEnded&&!tableStarted)continue;if(tableStarted&&!tableEnded&&currentSection){const driver=parseDriverRow(cells,currentSection);if(driver){result.drivers.push(driver);result.sections[currentSection].push(driver)}}}return result}
function parseDriverRow(cells,section){if(!cells||cells.length<2)return null;const nonEmpty=cells.filter(c=>c.trim()!=='');if(nonEmpty.length<2)return null;const joined=cells.join(' ').toUpperCase();if(joined.includes('ИТОГО')||joined.includes('ВСЕГО')||joined.includes('ПО РАЗДЕЛУ'))return null;const fio=cells[0]||'';if(fio.length<3||/^\d+$/.test(fio))return null;let plate='',plateIdx=-1;for(let j=0;j<cells.length;j++){const cleaned=cells[j].replace(/[\s-]/g,'').toUpperCase();if(/^[А-ЯA-Z]{1}\d{3}[А-ЯA-Z]{2}\d{2,3}$/.test(cleaned)||/^[А-ЯA-Z]{1}\d{3}[А-ЯA-Z]{2}$/.test(cleaned)){plate=cleaned;plateIdx=j;break}}let route='';for(let j=1;j<cells.length;j++){const c=cells[j].trim();if(/^\d{1,3}$/.test(c)&&c!==plate){route=c;break}}let sum=0;for(let j=cells.length-1;j>=0;j--){const c=cells[j].trim().replace(/\s/g,'').replace(',','.');const num=parseFloat(c);if(!isNaN(num)&&num>0&&j!==plateIdx&&j!==0){sum=num;break}}return{fio:normalizeName(fio),plate:normalizePlate(plate),section,route,sum,cash:0,nonCash:0}}
function parseTransactions(sheetData){if(!sheetData||sheetData.length===0)return[];let headerRow=-1;let colIndexes={};for(let i=0;i<Math.min(sheetData.length,10);i++){const row=sheetData[i];if(!row)continue;const cells=row.map(c=>String(c).toUpperCase().trim());const idxDate=cells.findIndex(c=>c.includes('DATE'));const idxConductor=cells.findIndex(c=>c.includes('CONDUCTOR'));const idxTarif=cells.findIndex(c=>c.includes('TARIF_SUM')||(c.includes('TARIF')&&!c.includes('TARIF_PAY')));const idxCar=cells.findIndex(c=>c.includes('CAR')||c.includes('PLATE')||c.includes('AUTO'));const idxRoute=cells.findIndex(c=>c.includes('ROUTE')||c.includes('RUTE'));if(idxDate>=0&&idxConductor>=0&&idxTarif>=0){headerRow=i;colIndexes={date:idxDate,conductor:idxConductor,tarifSum:idxTarif,car:idxCar>=0?idxCar:-1,route:idxRoute>=0?idxRoute:-1};break}}if(headerRow<0)return[];const transactions=[];for(let i=headerRow+1;i<sheetData.length;i++){const row=sheetData[i];if(!row||row.length===0)continue;const date=String(row[colIndexes.date]||'').trim();const conductor=String(row[colIndexes.conductor]||'').trim();const tarifSumStr=String(row[colIndexes.tarifSum]||'0').trim().replace(',','.');const tarifSum=parseFloat(tarifSumStr);if(!date||!conductor||isNaN(tarifSum))continue;if(conductor.toUpperCase().includes('CONDUCTOR'))continue;const car=colIndexes.car>=0?String(row[colIndexes.car]||'').trim():'';const route=colIndexes.route>=0?String(row[colIndexes.route]||'').trim():'';transactions.push({date,conductor:normalizeName(conductor),plate:normalizePlate(car),route,tarifSum,tarifRub:tarifSum/100})}return transactions}
function reconcile(vedomosti,transactions){const vedomostDates=new Set();vedomosti.forEach(v=>{if(v.date)vedomostDates.add(normalizeDate(v.date))});const filteredTransactions=transactions.filter(t=>vedomostDates.has(normalizeDate(t.date)));const txByDriver=new Map();filteredTransactions.forEach(t=>{const key=t.conductor+'|'+(t.plate||'');if(!txByDriver.has(key))txByDriver.set(key,{conductor:t.conductor,plate:t.plate,totalTarif:0,txCount:0,dates:new Set()});const entry=txByDriver.get(key);entry.totalTarif+=t.tarifRub;entry.txCount++;entry.dates.add(normalizeDate(t.date))});const driverMap=new Map();vedomosti.forEach(v=>{v.drivers.forEach(d=>{const key=d.fio+'|'+d.plate;if(!driverMap.has(key))driverMap.set(key,{fio:d.fio,plate:d.plate,route:d.route,section:d.section,cashier:v.cashier,vedomostDate:v.date,totalGiven:0});driverMap.get(key).totalGiven+=d.sum})});const results=[];const unmatchedTx=[];const usedTxKeys=new Set();driverMap.forEach((vd,key)=>{const txKey=vd.fio+'|'+vd.plate;let txEntry=txByDriver.get(txKey);if(!txEntry){let bestSim=0,bestKey='';txByDriver.forEach((entry,k)=>{const[txFio,txPlate]=k.split('|');const plateMatch=txPlate&&vd.plate&&normalizePlate(txPlate)===normalizePlate(vd.plate);const sim=nameSimilarity(vd.fio,txFio);if(plateMatch&&sim>=FUZZY_THRESHOLD&&sim>bestSim){bestSim=sim;bestKey=k}});if(bestKey){txEntry=txByDriver.get(bestKey);usedTxKeys.add(bestKey)}}else{usedTxKeys.add(txKey)}const totalCollected=txEntry?txEntry.totalTarif:0;const diff=txEntry?Math.round(totalCollected-vd.totalGiven):-vd.totalGiven;results.push({driver:vd.fio,car:vd.plate||'—',route:vd.route||'—',section:vd.section==='city'?'Город':'Пригород',cashier:vd.cashier||'—',date:vd.vedomostDate||'—',given:Math.round(vd.totalGiven),collected:Math.round(totalCollected),diff,txCount:txEntry?txEntry.txCount:0,status:diff===0?'ok':(Math.abs(diff)<=10?'warn':'err')})});txByDriver.forEach((entry,key)=>{if(!usedTxKeys.has(key))unmatchedTx.push({driver:entry.conductor,plate:entry.plate||'—',totalCollected:Math.round(entry.totalTarif),txCount:entry.txCount})});const cashierMap=new Map();results.forEach(r=>{const key=r.cashier;if(!cashierMap.has(key))cashierMap.set(key,{cashier:key,count:0,given:0,collected:0,diff:0});const c=cashierMap.get(key);c.count++;c.given+=r.given;c.collected+=r.collected;c.diff+=r.diff});const routeMap=new Map();results.forEach(r=>{const key=r.route||'Без маршрута';if(!routeMap.has(key))routeMap.set(key,{route:key,type:r.section,drivers:0,sum:0});const rm=routeMap.get(key);rm.drivers++;rm.sum+=r.given});const totalGiven=results.reduce((s,r)=>s+r.given,0);const totalCollected=results.reduce((s,r)=>s+r.collected,0);const totalDiff=results.reduce((s,r)=>s+r.diff,0);const discrepancies=results.filter(r=>r.diff!==0);return{drivers:results,cashiers:Array.from(cashierMap.values()),routes:Array.from(routeMap.values()),unmatchedTx,conclusions:{totalDrivers:results.length,totalGiven,totalCollected,totalDiff,discrepanciesCount:discrepancies.length,matchedPercent:results.length>0?Math.round((results.length-discrepancies.length)/results.length*100):0}}}
function normalizeDate(dateStr){if(!dateStr)return'';let d=String(dateStr).trim();const serial=parseInt(d);if(!isNaN(serial)&&serial>40000&&serial<60000){const date=new Date((serial-25569)*86400*1000);return date.toISOString().split('T')[0]}const parts=d.split(/[./-]/);if(parts.length===3){let day,month,year;if(parts[0].length===4){year=parts[0];month=parts[1];day=parts[2]}else{day=parts[0];month=parts[1];year=parts[2]}if(year.length===2)year='20'+year;return year+'-'+month.padStart(2,'0')+'-'+day.padStart(2,'0')}return d}
/* ---------- main ---------- */
const state={files:[],fileTypes:{},parsedVedomosti:[],parsedTransactions:[],result:null,isProcessing:false};
const $=id=>document.getElementById(id);
const dom={dropzone:$('dropzone'),fileInput:$('fileInput'),fileList:$('fileList'),btnReconcile:$('btnReconcile'),btnClear:$('btnClear'),btnExportCsv:$('btnExportCsv'),btnDownload:$('btnDownload'),btnToggleTheme:$('btnToggleTheme'),progressBar:$('progressBar'),progressFill:$('progressFill'),emptyState:$('emptyState'),resultActions:$('resultActions'),tabsContainer:$('tabsContainer'),tableDiscrepancies:$('tableDiscrepancies'),tableAll:$('tableAll'),tableCashiers:$('tableCashiers'),tableRoutes:$('tableRoutes'),countDisc:$('countDisc'),countAll:$('countAll'),conclusionsContent:$('conclusionsContent'),statusBar:$('statusBar')};
document.addEventListener('DOMContentLoaded',()=>{initTheme();initTabs();bindEvents();updateUI();showStatus('Загрузите файлы ведомостей и выгрузку транзакций для начала работы','info')});
function bindEvents(){dom.dropzone.addEventListener('click',()=>dom.fileInput.click());dom.dropzone.addEventListener('dragover',e=>{e.preventDefault();dom.dropzone.classList.add('dragover')});dom.dropzone.addEventListener('dragleave',()=>dom.dropzone.classList.remove('dragover'));dom.dropzone.addEventListener('drop',e=>{e.preventDefault();dom.dropzone.classList.remove('dragover');handleFiles(e.dataTransfer.files)});dom.fileInput.addEventListener('change',e=>handleFiles(e.target.files));dom.btnReconcile.addEventListener('click',onReconcile);dom.btnClear.addEventListener('click',onClear);dom.btnExportCsv.addEventListener('click',onExportCsv);dom.btnDownload.addEventListener('click',onDownloadExcel)}
async function handleFiles(fileList){if(state.isProcessing){showStatus('Дождитесь завершения текущей операции','warning');return}let added=0;for(const file of fileList){if(!validateFile(file))continue;if(state.files.some(f=>f.name===file.name&&f.size===file.size)){showStatus('Файл "'+file.name+'" уже загружен','warning');continue}state.files.push(file);added++}if(added===0)return;await detectTypes();const hasVed=Object.values(state.fileTypes).includes('vedomost');const hasTx=Object.values(state.fileTypes).includes('transaction');renderFileList();updateUI();if(hasVed&&hasTx)showStatus('Файлы загружены. Нажмите «Провести сверку»','success');else if(hasVed)showStatus('Загружена ведомость. Добавьте выгрузку транзакций','info');else if(hasTx)showStatus('Загружена выгрузка транзакций. Добавьте ведомости','info');else showStatus('Файлы не распознаны. Загрузите ведомости (.xlsx) и/или выгрузку','warning')}
async function detectTypes(){for(let i=0;i<state.files.length;i++){if(state.fileTypes[i])continue;try{const sheets=await readExcel(state.files[i]);const firstSheet=sheets[0]?.data||[];const type=detectFileType(state.files[i].name,firstSheet);state.fileTypes[i]=type||'unknown'}catch(err){console.error('Ошибка чтения файла:',state.files[i].name,err);state.fileTypes[i]='error'}}}
function renderFileList(){dom.fileList.innerHTML='';state.files.forEach((file,i)=>{const type=state.fileTypes[i];if(type==='error'||type==='unknown'){dom.fileList.appendChild(renderFileError(file.name,type==='error'?'Ошибка чтения файла':'Не удалось определить тип'))}else{dom.fileList.appendChild(renderFileItem(file,i,type))}});dom.fileList.querySelectorAll('.file-remove').forEach(btn=>{btn.addEventListener('click',e=>{const idx=parseInt(e.currentTarget.dataset.i);state.files.splice(idx,1);const newTypes={};Object.keys(state.fileTypes).forEach(k=>{const ki=parseInt(k);if(ki<idx)newTypes[ki]=state.fileTypes[k];else if(ki>idx)newTypes[ki-1]=state.fileTypes[k]});state.fileTypes=newTypes;state.parsedVedomosti=[];state.parsedTransactions=[];state.result=null;renderFileList();updateUI();hideResult();showStatus('Файл удалён','info')})})}
async function onReconcile(){if(state.isProcessing)return;state.isProcessing=true;dom.btnReconcile.disabled=true;try{showProgress(1200);state.parsedVedomosti=[];state.parsedTransactions=[];for(let i=0;i<state.files.length;i++){const type=state.fileTypes[i];if(type==='unknown'||type==='error')continue;const sheets=await readExcel(state.files[i]);const data=sheets[0]?.data||[];if(type==='vedomost'){const ved=parseVedomost(data);if(ved&&ved.drivers.length>0)state.parsedVedomosti.push(ved);else showStatus('Ведомость "'+state.files[i].name+'" не содержит данных о водителях','warning')}else if(type==='transaction'){const txs=parseTransactions(data);if(txs.length>0)state.parsedTransactions.push(...txs);else showStatus('Выгрузка "'+state.files[i].name+'" не содержит транзакций','warning')}}if(state.parsedVedomosti.length===0){showStatus('Не найдено ни одной ведомости с данными о водителях','error');state.isProcessing=false;dom.btnReconcile.disabled=false;resetProgress();return}if(state.parsedTransactions.length===0){showStatus('Не найдено транзакций для сверки','error');state.isProcessing=false;dom.btnReconcile.disabled=false;resetProgress();return}state.result=reconcile(state.parsedVedomosti,state.parsedTransactions);if(state.result.drivers.length===0){showStatus('Не удалось сопоставить ни одного водителя. Проверьте формат файлов','error');state.isProcessing=false;dom.btnReconcile.disabled=false;resetProgress();return}renderResult();updateUI();showStatus('✅ Сверка завершена: '+state.result.drivers.length+' водителей, '+state.result.conclusions.discrepanciesCount+' расхождений',state.result.conclusions.discrepanciesCount>0?'warning':'success')}catch(err){console.error('Ошибка сверки:',err);showStatus('❌ Ошибка: '+err.message,'error')}finally{state.isProcessing=false;dom.btnReconcile.disabled=false;resetProgress()}}
function renderResult(){if(!state.result)return;const r=state.result;dom.emptyState.style.display='none';dom.resultActions.style.display='';dom.tabsContainer.style.display='';const discData=r.drivers.filter(d=>d.diff!==0);renderTableBody(dom.tableDiscrepancies,discData,['driver','car','given','collected','diff','status'],(val,key,row)=>{if(key==='status'){const labels={ok:'✓ Совпадает',warn:'⚠ '+formatMoney(row.diff)+' ₽',err:'✕ '+formatMoney(Math.abs(row.diff))+' ₽'};return'<span class="badge badge-'+val+'">'+labels[val]+'</span>'}if(key==='diff'){if(val===0)return'<span class="diff-zero">0 ₽</span>';return'<span class="'+(val>0?'diff-positive':'diff-negative')+'">'+(val>0?'+':'')+formatMoney(val)+' ₽</span>'}if(typeof val==='number')return formatMoney(val);return val||'—'});enableSorting('tableDiscrepancies',discData,sorted=>{renderTableBody(dom.tableDiscrepancies,sorted,['driver','car','given','collected','diff','status'])});dom.countDisc.textContent=discData.length;const allData=r.drivers.map(d=>({date:d.date,driver:d.driver,route:d.route,section:d.section,given:d.given,collected:d.collected,txCount:d.txCount}));renderTableBody(dom.tableAll,allData,['date','driver','route','section','given','collected','txCount'],(val,key)=>{if(key==='section')return val==='Город'?'🏙️ Город':'🚌 Пригород';if(typeof val==='number')return formatMoney(val);return val||'—'});enableSorting('tableAll',allData,sorted=>{renderTableBody(dom.tableAll,sorted,['date','driver','route','section','given','collected','txCount'])});dom.countAll.textContent=allData.length;renderTableBody(dom.tableCashiers,r.cashiers,['cashier','count','given','collected','diff'],(val,key)=>{if(key==='diff'){if(val===0)return'<span class="diff-zero">0 ₽</span>';return'<span class="'+(val>0?'diff-positive':'diff-negative')+'">'+(val>0?'+':'')+formatMoney(val)+' ₽</span>'}if(typeof val==='number')return formatMoney(val);return val||'—'});enableSorting('tableCashiers',r.cashiers,sorted=>{renderTableBody(dom.tableCashiers,sorted,['cashier','count','given','collected','diff'])});renderTableBody(dom.tableRoutes,r.routes,['route','type','drivers','sum'],(val,key)=>{if(typeof val==='number')return formatMoney(val);return val||'—'});enableSorting('tableRoutes',r.routes,sorted=>{renderTableBody(dom.tableRoutes,sorted,['route','type','drivers','sum'])});const c=r.conclusions;const dc=c.totalDiff===0?'':(c.totalDiff>0?'err':'warn');dom.conclusionsContent.innerHTML='<div class="summary-card"><div class="info-block '+dc+'"><div class="info-block-icon '+(c.totalDiff===0?'green':(c.totalDiff>0?'rose':'amber'))+'">'+(c.totalDiff===0?'✓':'⚠')+'</div><div><div class="info-block-title" style="color:'+(c.totalDiff===0?'var(--success-text)':'var(--danger-text)')+'">Итого сверено: '+c.totalDrivers+' водителей</div><div class="info-block-text"><strong>Сдано:</strong> '+formatMoney(c.totalGiven)+' ₽ · <strong>Собрано:</strong> '+formatMoney(c.totalCollected)+' ₽ · <strong>Расхождение:</strong> <span style="color:'+(c.totalDiff===0?'var(--text-muted)':(c.totalDiff>0?'var(--danger)':'var(--success)'))+';font-weight:600">'+(c.totalDiff>0?'+':'')+formatMoney(c.totalDiff)+' ₽</span></div><div class="info-block-text" style="margin-top:.5rem">Совпадений: '+(c.totalDrivers-c.discrepanciesCount)+' из '+c.totalDrivers+' ('+c.matchedPercent+'%)'+(c.discrepanciesCount>0?'<span style="color:var(--danger)"> · Расхождений: '+c.discrepanciesCount+'</span>':'')+'</div></div></div>'+(r.unmatchedTx.length>0?'<div class="info-block warn" style="margin-top:1rem"><div class="info-block-icon amber">⚠</div><div><div class="info-block-title" style="color:var(--warning-text)">Не сопоставлено транзакций: '+r.unmatchedTx.length+'</div><div class="info-block-text">Эти водители есть в выгрузке, но не найдены в ведомостях.</div></div></div>':'')+'</div>'}
function onExportCsv(){if(!state.result)return;const data=state.result.drivers.map(r=>({driver:r.driver,car:r.car,route:r.route,cashier:r.cashier,date:r.date,given:r.given,collected:r.collected,diff:r.diff,status:r.status==='ok'?'Совпадает':(r.status==='warn'?'Мелкое расхождение':'Расхождение')}));downloadFile(arrayToCsv(data,['driver','car','route','cashier','date','given','collected','diff','status']),'sverka_vyruchki.csv');showStatus('CSV-файл скачан','success')}
function onDownloadExcel(){if(!state.result)return;const data=state.result.drivers.map(r=>({'Водитель':r.driver,'Гос. номер':r.car,'Маршрут':r.route,'Кассир':r.cashier,'Дата':r.date,'Сдано (₽)':r.given,'Собрано (₽)':r.collected,'Расхождение (₽)':r.diff,'Статус':r.status==='ok'?'Совпадает':(r.status==='warn'?'Мелкое расхождение':'Расхождение')}));downloadExcel(data,'sverka_vyruchki.xlsx');showStatus('Excel-отчёт скачан','success')}
function onClear(){if(state.isProcessing)return;state.files=[];state.fileTypes={};state.parsedVedomosti=[];state.parsedTransactions=[];state.result=null;dom.fileInput.value='';renderFileList();updateUI();hideResult();showStatus('Все файлы удалены','info')}
function updateUI(){const hf=state.files.length>0,hr=state.result!==null;dom.btnReconcile.disabled=!hf||state.isProcessing;dom.btnClear.disabled=!hf||state.isProcessing;dom.btnExportCsv.disabled=!hr;dom.btnDownload.disabled=!hr}
function hideResult(){dom.emptyState.style.display='';dom.resultActions.style.display='none';dom.tabsContainer.style.display='none'}
function showStatus(msg,type){type=type||'info';const bar=dom.statusBar;if(!bar)return;bar.textContent=msg;bar.className='status-bar status-'+type;bar.style.display='';if(type==='success'||type==='info'){clearTimeout(bar._hideTimer);bar._hideTimer=setTimeout(()=>{bar.style.display='none'},8000)}}
</script>
</body>
</html>