Files

213 lines
40 KiB
HTML
Raw Permalink 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:#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;--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}
.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{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}
@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}
.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-primary:disabled{opacity:.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:#fff;box-shadow:0 4px 15px rgba(16,185,129,.3)}
.btn-success:hover{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{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{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:.875rem}
thead{background:var(--surface-alt)}th,td{padding:.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}[data-theme=dark] 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}
.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)}
</style>
</head>
<body>
<div class="container">
<header class="hero">
<div class="hero-badge">🔒 Работает офлайн — файлы не отправляются на сервер</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">🌙 Тёмная тема</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">ФИО + гос. номер. Нормализация, нечёткое сравнение ≥ 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>
/* ================================================================
Встроенный XLSX (минимальный, только чтение .xlsx через zip)
================================================================ */
(function(){if(typeof XLSX!=='undefined')return;var g=typeof window!=='undefined'?window:{};g.XLSX={};var W=g.XLSX;W.utils={};W.read=function(data){var zip;try{zip=new JSZip(data)}catch(e){throw new Error('Не удалось прочитать .xlsx')}var result=[];var ssFile=zip.file('xl/sharedStrings.xml');var ss=[];if(ssFile){var doc=W._parseXml(ssFile.asText());var sis=doc.getElementsByTagName('si');for(var i=0;i<sis.length;i++){var t=sis[i].getElementsByTagName('t');ss.push(t.length>0?t[0].textContent||'':'')}}var wbXml=zip.file('xl/workbook.xml');var sheetCount=1;if(wbXml){var wbDoc=W._parseXml(wbXml.asText());var wbSheets=wbDoc.getElementsByTagName('sheet');sheetCount=wbSheets.length}for(var si=0;si<sheetCount;si++){var sXml=zip.file('xl/worksheets/sheet'+(si+1)+'.xml');if(!sXml&&si===0)sXml=zip.file('xl/worksheets/sheet1.xml');var data2d=[];if(sXml){var sDoc=W._parseXml(sXml.asText());var rows=sDoc.getElementsByTagName('row');for(var ri=0;ri<rows.length;ri++){var row=rows[ri];var cells=row.getElementsByTagName('c');var rowData=[];var maxCol=0;for(var ci=0;ci<cells.length;ci++){var cell=cells[ci];var r=cell.getAttribute('r');var colNum=W._colToNum(r);var v=cell.getElementsByTagName('v');var t=cell.getAttribute('t');var val='';if(v.length>0)val=v[0].textContent||'';if(t==='s'){var idx=parseInt(val);val=ss[idx]||val}rowData[colNum]=val}for(var j=0;j<rowData.length;j++){if(rowData[j]===undefined)rowData[j]=''}rowData.length=rowData.length||0;data2d.push(rowData)}}result.push({name:'Sheet'+(si+1),data:data2d})}return result};W._parseXml=function(text){return new DOMParser().parseFromString(text,'text/xml')};W._colToNum=function(ref){var match=ref.match(/^([A-Z]+)(\d+)$/);if(!match)return 0;var col=0;for(var i=0;i<match[1].length;i++){col=col*26+(match[1].charCodeAt(i)-64)}return col-1};W.utils.sheet_to_json=function(sheet,opts){opts=opts||{};var data=sheet.data||sheet;if(!data||!data.length)return[];var header=data[0]||[];var result=[];for(var i=1;i<data.length;i++){var row=data[i];if(!row)continue;var obj={};for(var j=0;j<header.length;j++){var h=header[j];obj[h]=(row[j]!==undefined?row[j]:'')}if(opts.defval!==undefined){for(var j=0;j<header.length;j++){if(obj[header[j]]==='')obj[header[j]]=opts.defval}}result.push(obj)}return result};W.utils.book_new=function(){return{SheetNames:[],Sheets:{}}};W.utils.json_to_sheet=function(data){var ws={};if(!data||!data.length)return ws;var keys=Object.keys(data[0]);for(var i=0;i<data.length;i++){for(var j=0;j<keys.length;j++){var ref=String.fromCharCode(65+j)+(i+1);ws[ref]={v:data[i][keys[j]]||''}}}return ws};W.utils.book_append_sheet=function(wb,ws,name){wb.SheetNames.push(name);wb.Sheets[name]=ws};W.write=function(wb){var xml='<?xml version="1.0" encoding="UTF-8"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheets>';for(var i=0;i<wb.SheetNames.length;i++){xml+='<sheet name="'+wb.SheetNames[i]+'" sheetId="'+(i+1)+'"/>'}xml+='</sheets></workbook>';var sheetXml='<?xml version="1.0" encoding="UTF-8"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>';var ws=wb.Sheets[wb.SheetNames[0]];if(ws){for(var key in ws){if(key[0]==='!')continue;var match=key.match(/^([A-Z]+)(\d+)$/);if(!match)continue;var val=String(ws[key].v||'');sheetXml+='<row r="'+match[2]+'"><c r="'+key+'" t="inlineStr"><is><t>'+val.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</t></is></c></row>'}}sheetXml+='</sheetData></worksheet>';return new TextEncoder().encode(xml+'\n'+sheetXml)}})();
/* ================================================================
Встроенный JSZip (минимальный, распаковка zip)
================================================================ */
(function(){if(typeof JSZip!=='undefined')return;var g=typeof window!=='undefined'?window:{};var JSZip=function(data){this.files={};this._load(data)};JSZip.prototype={_load:function(data){var arr=data instanceof ArrayBuffer?new Uint8Array(data):data;var i=0;function rs(n){var s='';for(var k=0;k<n;k++)s+=String.fromCharCode(arr[i+k]);i+=n;return s}function r16(){var v=arr[i]|(arr[i+1]<<8);i+=2;return v}function r32(){var v=arr[i]|(arr[i+1]<<8)|(arr[i+2]<<16)|(arr[i+3]<<24);i+=4;return v}while(i<arr.length-30){var sig=r32();if(sig===0x04034b50){r16();r16();var method=r16();r16();r16();r32();var compSize=r32();var uncompSize=r32();var fnameLen=r16();var extraLen=r16();var fname=rs(fnameLen);i+=extraLen;var content;if(method===0){content=arr.subarray(i,i+uncompSize)}else{content=arr.subarray(i,i+compSize)}i+=compSize;var self=this;(function(n,c){self.files[n]={asText:function(){var dec='';for(var k=0;k<c.length;k++)dec+=String.fromCharCode(c[k]);return decodeURIComponent(escape(dec))},data:c}})(fname,content)}else{i-=4;break}}},file:function(name){return this.files[name]||null}};g.JSZip=JSZip})();
/* ================================================================
Основной код приложения
================================================================ */
function readExcel(file){return file.arrayBuffer().then(function(arr){return XLSX.read(arr)})}
function validateFile(file){var ext=file.name.slice(file.name.lastIndexOf('.')).toLowerCase();if(!['.xlsx','.xls'].includes(ext)){alert('❌ Недопустимый тип: '+ext);return false}return true}
function arrayToCsv(data,headers){return '\uFEFF'+headers.join(',')+'\n'+data.map(function(r){return headers.map(function(h){return '"'+String(r[h]||'').replace(/"/g,'""')+'"'}).join(',')}).join('\n')}
function downloadFile(content,filename){var blob=new Blob([content],{type:'application/octet-stream'});var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download=filename;document.body.appendChild(a);a.click();document.body.removeChild(a);setTimeout(function(){URL.revokeObjectURL(url)},1000)}
function formatMoney(n){if(n===null||n===undefined||isNaN(n))return'—';return n.toLocaleString('ru-RU',{minimumFractionDigits:0,maximumFractionDigits:0})}
function detectFileType(name,data){if(hasHeader(data,'ВЕДОМОСТЬ ПРИЕМА НАЛИЧНЫХ СРЕДСТВ'))return'vedomost';var low=name.toLowerCase();if(low.includes('transaction')||low.includes('transactions')||low.includes('выгрузка'))return'transaction';if(hasTxCols(data))return'transaction';if(hasHeader(data,'ВЕДОМОСТЬ'))return'vedomost';return null}
function hasHeader(d,h){if(!d||!d.length)return false;for(var i=0;i<Math.min(d.length,20);i++){var r=d[i];if(!r)continue;if(r.filter(function(c){return c!=null}).map(String).join(' ').toUpperCase().includes(h.toUpperCase()))return true}return false}
function hasTxCols(d){if(!d||!d.length)return false;for(var i=0;i<Math.min(d.length,5);i++){var r=d[i];if(!r)continue;var c=r.map(function(x){return String(x).toUpperCase().trim()});if(c.some(function(x){return x.includes('CONDUCTOR')})&&c.some(function(x){return x.includes('DATE')})&&c.some(function(x){return x.includes('TARIF_SUM')||x.includes('TARIF')}))return true}return false}
var L2C={A:'А',B:'В',C:'С',E:'Е',H:'Н',K:'К',M:'М',O:'О',P:'Р',T:'Т',X:'Х',Y:'У'};
function normPlate(p){if(!p)return'';return String(p).toUpperCase().trim().split('').map(function(c){return L2C[c]||c}).join('').replace(/[^А-ЯЁA-Z0-9]/gi,'')}
function normName(n){if(!n)return'';return String(n).toUpperCase().trim().replace(/Ё/g,'Е').replace(/\s+/g,' ')}
function sim(a,b){if(!a||!b)return 0;var s1=normName(a),s2=normName(b);if(s1===s2)return 1;if(!s1.length||!s2.length)return 0;var d=lev(s1,s2);return 1-d/Math.max(s1.length,s2.length)}
function lev(a,b){var m=a.length,n=b.length;var dp=Array(m+1);for(var i=0;i<=m;i++){dp[i]=Array(n+1).fill(0);dp[i][0]=i}for(var j=0;j<=n;j++)dp[0][j]=j;for(var i=1;i<=m;i++){for(var 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 parseVed(d){if(!d||!d.length)return null;var r={docNumber:'',date:'',cashier:'',sections:{city:[],suburb:[]},drivers:[]},cs=null,hf=false,ts=false,te=false;for(var i=0;i<d.length;i++){var row=d[i];if(!row||!row.length)continue;var c=row.map(function(x){return String(x).trim()});if(!hf){if(c.join(' ').toUpperCase().includes('ВЕДОМОСТЬ ПРИЕМА НАЛИЧНЫХ СРЕДСТВ')){hf=true}continue}if(!r.docNumber){var ni=c.findIndex(function(x){return x.toUpperCase().includes('ДОКУМЕНТ')});if(ni>=0){r.docNumber=c[ni+1]||'';var di=c.findIndex(function(x){return/^\d{2}[./-]\d{2}[./-]\d{4}$/.test(x)});if(di>=0)r.date=c[di]}}if(!r.date){var dm=c.find(function(x){return/^\d{2}[./-]\d{2}[./-]\d{4}$/.test(x)});if(dm)r.date=dm}if(!r.cashier){var ci=c.findIndex(function(x){return x.toUpperCase().includes('КАССИР')});if(ci>=0&&ci+1<c.length&&c[ci+1].length>2)r.cashier=c[ci+1]}var jr=c.join(' ').toUpperCase();if(jr.includes('ГОРОД')||jr.includes('ГОР.')||(jr.includes('ГОР')&&!jr.includes('ПРИГОР'))){cs='city';ts=false;continue}if(jr.includes('ПРИГОРОД')||jr.includes('ПРИГ.')){cs='suburb';ts=false;continue}if(c.every(function(x){return x===''})){if(ts)te=true;continue}if(c.join(' ').toUpperCase().includes('ФИО')||c.join(' ').toUpperCase().includes('ВОДИТЕЛЬ')||c.join(' ').toUpperCase().includes('ФАМИЛИЯ')){ts=true;te=false;continue}if(te&&!ts)continue;if(ts&&!te&&cs){var dr=parseDR(c,cs);if(dr){r.drivers.push(dr);r.sections[cs].push(dr)}}}return r}
function parseDR(c,sec){if(!c||c.length<2)return null;if(c.join(' ').toUpperCase().includes('ИТОГО')||c.join(' ').toUpperCase().includes('ВСЕГО'))return null;var f=c[0]||'';if(f.length<3||/^\d+$/.test(f))return null;var pl='',pi=-1;for(var j=0;j<c.length;j++){var cl=c[j].replace(/[\s-]/g,'').toUpperCase();if(/^[А-ЯA-Z]\d{3}[А-ЯA-Z]{2}\d{2,3}$/.test(cl)||/^[А-ЯA-Z]\d{3}[А-ЯA-Z]{2}$/.test(cl)){pl=cl;pi=j;break}}var rt='';for(var j=1;j<c.length;j++){if(/^\d{1,3}$/.test(c[j].trim())&&c[j]!==pl){rt=c[j];break}}var sm=0;for(var j=c.length-1;j>=0;j--){var n=parseFloat(c[j].trim().replace(/\s/g,'').replace(',','.'));if(!isNaN(n)&&n>0&&j!==pi&&j!==0){sm=n;break}}return{fio:normName(f),plate:normPlate(pl),section:sec,route:rt,sum:sm}}
function parseTx(d){if(!d||!d.length)return[];var hr=-1,ci={};for(var i=0;i<Math.min(d.length,10);i++){var row=d[i];if(!row)continue;var c=row.map(function(x){return String(x).toUpperCase().trim()});var id=c.findIndex(function(x){return x.includes('DATE')}),ic=c.findIndex(function(x){return x.includes('CONDUCTOR')}),it=c.findIndex(function(x){return x.includes('TARIF_SUM')||(x.includes('TARIF')&&!x.includes('TARIF_PAY'))}),icar=c.findIndex(function(x){return x.includes('CAR')||x.includes('PLATE')||x.includes('AUTO')});if(id>=0&&ic>=0&&it>=0){hr=i;ci={date:id,conductor:ic,tarifSum:it,car:icar>=0?icar:-1};break}}if(hr<0)return[];var txs=[];for(var i=hr+1;i<d.length;i++){var row=d[i];if(!row||!row.length)continue;var date=String(row[ci.date]||'').trim(),cond=String(row[ci.conductor]||'').trim(),tsStr=String(row[ci.tarifSum]||'0').trim().replace(',','.'),ts=parseFloat(tsStr);if(!date||!cond||isNaN(ts))continue;if(cond.toUpperCase().includes('CONDUCTOR'))continue;var car=ci.car>=0?String(row[ci.car]||'').trim():'';txs.push({date:date,conductor:normName(cond),plate:normPlate(car),tarifRub:ts/100})}return txs}
function reconcile(veds,txs){var vDates=new Set();veds.forEach(function(v){if(v.date)vDates.add(normDate(v.date))});var ft=txs.filter(function(t){return vDates.has(normDate(t.date))});var txMap=new Map();ft.forEach(function(t){var k=t.conductor+'|'+(t.plate||'');if(!txMap.has(k))txMap.set(k,{conductor:t.conductor,plate:t.plate,total:0,cnt:0});var e=txMap.get(k);e.total+=t.tarifRub;e.cnt++});var dMap=new Map();veds.forEach(function(v){v.drivers.forEach(function(d){var k=d.fio+'|'+d.plate;if(!dMap.has(k))dMap.set(k,{fio:d.fio,plate:d.plate,route:d.route,section:d.section,cashier:v.cashier,date:v.date,given:0});dMap.get(k).given+=d.sum})});var res=[],unmatched=[];var used=new Set();dMap.forEach(function(vd,k){var tk=vd.fio+'|'+vd.plate;var tx=txMap.get(tk);if(!tx){var bs=0,bk='';txMap.forEach(function(e,k){var p=k.split('|'),tf=p[0],tp=p[1]||'';if(tp&&vd.plate&&normPlate(tp)===normPlate(vd.plate)){var s=sim(vd.fio,tf);if(s>=0.88&&s>bs){bs=s;bk=k}}});if(bk){tx=txMap.get(bk);used.add(bk)}}else{used.add(tk)}var col=tx?tx.total:0;var diff=tx?Math.round(col-vd.given):-vd.given;res.push({driver:vd.fio,car:vd.plate||'—',route:vd.route||'—',section:vd.section==='city'?'Город':'Пригород',cashier:vd.cashier||'—',date:vd.date||'—',given:Math.round(vd.given),collected:Math.round(col),diff:diff,txCount:tx?tx.cnt:0,status:diff===0?'ok':(Math.abs(diff)<=10?'warn':'err')})});txMap.forEach(function(e,k){if(!used.has(k)){var p=k.split('|');unmatched.push({driver:p[0],plate:p[1]||'—',total:Math.round(e.total),cnt:e.cnt})}});var cm=new Map();res.forEach(function(r){var k=r.cashier;if(!cm.has(k))cm.set(k,{cashier:k,count:0,given:0,collected:0,diff:0});var e=cm.get(k);e.count++;e.given+=r.given;e.collected+=r.collected;e.diff+=r.diff});var rm=new Map();res.forEach(function(r){var k=r.route||'Без маршрута';if(!rm.has(k))rm.set(k,{route:k,type:r.section,drivers:0,sum:0});var e=rm.get(k);e.drivers++;e.sum+=r.given});var tg=res.reduce(function(s,r){return s+r.given},0),tc=res.reduce(function(s,r){return s+r.collected},0),td=res.reduce(function(s,r){return s+r.diff},0),disc=res.filter(function(r){return r.diff!==0});return{drivers:res,cashiers:Array.from(cm.values()),routes:Array.from(rm.values()),unmatchedTx:unmatched,conclusions:{totalDrivers:res.length,totalGiven:tg,totalCollected:tc,totalDiff:td,discrepanciesCount:disc.length,matchedPercent:res.length>0?Math.round((res.length-disc.length)/res.length*100):0}}}
function normDate(s){if(!s)return'';var d=String(s).trim();var ser=parseInt(d);if(!isNaN(ser)&&ser>40000&&ser<60000){var dt=new Date((ser-25569)*86400*1000);return dt.toISOString().split('T')[0]}var p=d.split(/[./-]/);if(p.length===3){var day,month,year;if(p[0].length===4){year=p[0];month=p[1];day=p[2]}else{day=p[0];month=p[1];year=p[2]}if(year.length===2)year='20'+year;return year+'-'+month.padStart(2,'0')+'-'+day.padStart(2,'0')}return d}
/* ===== main ===== */
var st={files:[],fileTypes:{},parsedVeds:[],parsedTxs:[],result:null,busy:false};
var $=function(id){return document.getElementById(id)};
var dom={dz:$('dropzone'),fi:$('fileInput'),fl:$('fileList'),br:$('btnReconcile'),bc:$('btnClear'),be:$('btnExportCsv'),bd:$('btnDownload'),btt:$('btnToggleTheme'),pb:$('progressBar'),pf:$('progressFill'),es:$('emptyState'),ra:$('resultActions'),tc:$('tabsContainer'),td:$('tableDiscrepancies'),ta:$('tableAll'),tca:$('tableCashiers'),tr:$('tableRoutes'),cd:$('countDisc'),ca:$('countAll'),cc:$('conclusionsContent')};
document.addEventListener('DOMContentLoaded',function(){initTheme();initTabs();bind();updUI()});
function initTheme(){var t=localStorage.getItem('theme')||'light';document.documentElement.setAttribute('data-theme',t);dom.btt.textContent=t==='light'?'🌙 Тёмная тема':'☀️ Светлая тема';dom.btt.onclick=function(){var x=localStorage.getItem('theme')||'light';x=x==='light'?'dark':'light';localStorage.setItem('theme',x);document.documentElement.setAttribute('data-theme',x);this.textContent=x==='light'?'🌙 Тёмная тема':'☀️ Светлая тема'}}
function initTabs(){document.querySelectorAll('.tab-btn').forEach(function(b){b.onclick=function(){document.querySelectorAll('.tab-btn').forEach(function(x){x.classList.remove('active')});document.querySelectorAll('.tab-panel').forEach(function(x){x.classList.remove('active')});b.classList.add('active');var p=document.getElementById('panel-'+b.dataset.tab);if(p)p.classList.add('active')}})}
function bind(){dom.dz.onclick=function(){dom.fi.click()};dom.dz.ondragover=function(e){e.preventDefault();dom.dz.classList.add('dragover')};dom.dz.ondragleave=function(){dom.dz.classList.remove('dragover')};dom.dz.ondrop=function(e){e.preventDefault();dom.dz.classList.remove('dragover');hdl(e.dataTransfer.files)};dom.fi.onchange=function(e){hdl(e.target.files)};dom.br.onclick=onRec;dom.bc.onclick=onClr;dom.be.onclick=onCSV;dom.bd.onclick=onXLS}
function hdl(fl){if(st.busy)return;for(var i=0;i<fl.length;i++){var f=fl[i];if(!validateFile(f))continue;if(st.files.some(function(x){return x.name===f.name&&x.size===f.size}))continue;st.files.push(f)}if(!st.files.length)return;detect().then(function(){rndFL();updUI()})}
function detect(){var ps=[];for(var i=0;i<st.files.length;i++){(function(idx){if(st.fileTypes[idx]){ps.push(Promise.resolve());return}ps.push(readExcel(st.files[idx]).then(function(sh){var d=sh[0]?.data||[];st.fileTypes[idx]=detectFileType(st.files[idx].name,d)||'unknown'}).catch(function(){st.fileTypes[idx]='error'}))})(i)}return Promise.all(ps)}
function rndFL(){dom.fl.innerHTML='';st.files.forEach(function(f,i){var t=st.fileTypes[i],el;if(t==='error'||t==='unknown'){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">'+esc(f.name)+'</div><div class="file-type" style="color:#ef4444;">'+(t==='error'?'Ошибка чтения':'Не определён')+'</div></div>'}else{el=document.createElement('div');el.className='file-item';el.innerHTML='<div class="file-icon '+(t==='transaction'?'transaction':'vedomost')+'">'+(t==='transaction'?'📊':'📋')+'</div><div class="file-info"><div class="file-name">'+esc(f.name)+'</div><div class="file-type">'+(t==='transaction'?'Выгрузка':'Ведомость')+' · '+(f.size/1024).toFixed(0)+' КБ</div></div><button class="file-remove" data-i="'+i+'">✕</button>'}dom.fl.appendChild(el)});dom.fl.querySelectorAll('.file-remove').forEach(function(b){b.onclick=function(){var i=parseInt(this.dataset.i);st.files.splice(i,1);var nt={};Object.keys(st.fileTypes).forEach(function(k){var ki=parseInt(k);if(ki<i)nt[ki]=st.fileTypes[k];else if(ki>i)nt[ki-1]=st.fileTypes[k]});st.fileTypes=nt;st.parsedVeds=[];st.parsedTxs=[];st.result=null;rndFL();updUI();hideR()}})}
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}
function updUI(){dom.br.disabled=!st.files.length||st.busy;dom.bc.disabled=!st.files.length||st.busy;dom.be.disabled=!st.result;dom.bd.disabled=!st.result}
async function onRec(){if(st.busy)return;st.busy=true;dom.br.disabled=true;try{dom.pb&&dom.pb.classList.add('active');dom.pf&&(dom.pf.style.width='0%');st.parsedVeds=[];st.parsedTxs=[];for(var i=0;i<st.files.length;i++){var t=st.fileTypes[i];if(t==='unknown'||t==='error')continue;var sh=await readExcel(st.files[i]),d=sh[0]?.data||[];if(t==='vedomost'){var v=parseVed(d);if(v&&v.drivers.length)st.parsedVeds.push(v)}else if(t==='transaction'){var tx=parseTx(d);if(tx.length)for(var j=0;j<tx.length;j++)st.parsedTxs.push(tx[j])}}if(!st.parsedVeds.length||!st.parsedTxs.length){alert('Нужны: ведомость + выгрузка транзакций');st.busy=false;dom.br.disabled=false;dom.pb&&dom.pb.classList.remove('active');return}st.result=reconcile(st.parsedVeds,st.parsedTxs);if(!st.result.drivers.length){alert('Не удалось сопоставить водителей');st.busy=false;dom.br.disabled=false;dom.pb&&dom.pb.classList.remove('active');return}rndR();updUI()}catch(e){console.error(e);alert('Ошибка: '+e.message)}finally{st.busy=false;dom.br.disabled=false;dom.pb&&dom.pb.classList.remove('active')}}
function rndR(){if(!st.result)return;var r=st.result;dom.es.style.display='none';dom.ra.style.display='';dom.tc.style.display='';var dd=r.drivers.filter(function(d){return d.diff!==0});rndT(dom.td,dd,['driver','car','given','collected','diff','status'],function(v,k,row){if(k==='status'){var lb={ok:'✓ Совпадает',warn:'⚠ '+formatMoney(row.diff)+' ₽',err:'✕ '+formatMoney(Math.abs(row.diff))+' ₽'};return'<span class="badge badge-'+v+'">'+(lb[v]||v)+'</span>'}if(k==='diff'){if(v===0)return'<span class="diff-zero">0 ₽</span>';return'<span class="'+(v>0?'diff-positive':'diff-negative')+'">'+(v>0?'+':'')+formatMoney(v)+' ₽</span>'}if(typeof v==='number')return formatMoney(v);return v||'—'});dom.cd.textContent=dd.length;var ad=r.drivers.map(function(d){return{date:d.date,driver:d.driver,route:d.route,section:d.section,given:d.given,collected:d.collected,txCount:d.txCount}});rndT(dom.ta,ad,['date','driver','route','section','given','collected','txCount'],function(v,k){if(k==='section')return v==='Город'?'🏙️ Город':'🚌 Пригород';if(typeof v==='number')return formatMoney(v);return v||'—'});dom.ca.textContent=ad.length;rndT(dom.tca,r.cashiers,['cashier','count','given','collected','diff'],function(v,k){if(k==='diff'){if(v===0)return'<span class="diff-zero">0 ₽</span>';return'<span class="'+(v>0?'diff-positive':'diff-negative')+'">'+(v>0?'+':'')+formatMoney(v)+' ₽</span>'}if(typeof v==='number')return formatMoney(v);return v||'—'});rndT(dom.tr,r.routes,['route','type','drivers','sum'],function(v,k){if(typeof v==='number')return formatMoney(v);return v||'—'});var c=r.conclusions,dc=c.totalDiff===0?'':(c.totalDiff>0?'err':'warn');dom.cc.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 rndT(tb,rows,keys,fmt){if(!rows||!rows.length){tb.innerHTML='<tr><td colspan="'+(keys||Object.keys(rows[0]||{}).length)+'" style="text-align:center;padding:2rem;color:var(--text-muted)">Нет данных</td></tr>';return}tb.innerHTML=rows.map(function(row){return'<tr>'+(keys||Object.keys(row)).map(function(k){var v=row[k];if(fmt)return fmt(v,k,row);if(v==null||v==='')return'—';if(typeof v==='number')return formatMoney(v);return esc(String(v))}).join('')+'</tr>'}).join('')}
function onCSV(){if(!st.result)return;var d=st.result.drivers.map(function(r){return{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(d,['driver','car','route','cashier','date','given','collected','diff','status']),'sverka_vyruchki.csv')}
function onXLS(){if(!st.result)return;var d=st.result.drivers.map(function(r){return{'Водитель':r.driver,'Гос. номер':r.car,'Маршрут':r.route,'Кассир':r.cashier,'Дата':r.date,'Сдано (₽)':r.given,'Собрано (₽)':r.collected,'Расхождение (₽)':r.diff,'Статус':r.status==='ok'?'Совпадает':(r.status==='warn'?'Мелкое расхождение':'Расхождение')}});var wb=XLSX.utils.book_new(),ws=XLSX.utils.json_to_sheet(d);XLSX.utils.book_append_sheet(wb,ws,'Сверка');downloadFile(XLSX.write(wb),'sverka_vyruchki.xlsx')}
function onClr(){if(st.busy)return;st.files=[];st.fileTypes={};st.parsedVeds=[];st.parsedTxs=[];st.result=null;dom.fi.value='';rndFL();updUI();hideR()}
function hideR(){dom.es.style.display='';dom.ra.style.display='none';dom.tc.style.display='none'}
</script>
</body>
</html>