diff --git a/index.html b/index.html index 07a29f1..cb5b355 100644 --- a/index.html +++ b/index.html @@ -4,41 +4,148 @@ Сверка выручки водителей - - + +
-
🔒 Работает офлайн — файлы не отправляются на сервер
+
[LOCK] Работает офлайн — файлы не отправляются на сервер

Сверка выручки водителей

Загрузите ведомости приёма наличных и выгрузку транзакций — получите сводную таблицу, расхождения и готовый Excel-отчёт

- - - -
-
-
1
-

Файлы

-
+
1

Файлы

-
- - - - - -
+
Перетащите файлы сюда или нажмите для выбора
-
- Любое число ведомостей (.xlsx) + выгрузка TRANSACTIONS_EXT. - Тип определяется автоматически. -
+
Любое число ведомостей (.xlsx) + одна выгрузка TRANSACTIONS_EXT. Тип определяется автоматически.
@@ -46,190 +153,196 @@ - -
-
-
+
+
-
-
-
2
-

Результат

-
+
2

Результат

-
-
📋
-

Загрузите файлы и нажмите «Провести сверку»

-
- +
📋

Загрузите файлы и нажмите «Провести сверку»

+
-
-
- 💡 -

Как это работает

-
+
💡

Как это работает

-
-
📄
-
-
Определение типа файла
-
- Ведомость распознаётся по заголовку «ВЕДОМОСТЬ ПРИЕМА НАЛИЧНЫХ СРЕДСТВ»; - из неё берутся номер документа, дата, ФИО кассира, разделы «Город» / «Пригород» - и строки водителей. Выгрузка распознаётся по столбцам CONDUCTOR, DATE и TARIF_SUM. -
-
-
-
-
📅
-
-
Период
-
- Из транзакций берутся только даты, указанные в загруженных ведомостях. - Остальные даты в сверку не входят. -
-
-
-
-
🔍
-
-
Ключ сопоставления
-
- ФИО водителя + гос. номер ТС. Номера нормализуются (латинские A, B, C, E, H, K, M, O, P, T, X, Y - заменяются на кириллические), ФИО — верхний регистр, Ё→Е. - Опечатки в ФИО сводятся нечётким сравнением при схожести ≥ 88%. -
-
-
-
-
💰
-
-
Суммы
-
- «Сдано» — графа «Итого на сумму» ведомости. - «Собрано» — TARIF_SUM ÷ 100 (копейки → рубли) по всем транзакциям водителя за выбранные даты. -
-
-
-
-
🔒
-
-
Приватность
-
- Файлы никуда не отправляются — весь расчёт выполняется в браузере на вашем компьютере. - Приложение работает полностью офлайн. -
-
-
+
📄
Определение типа файла
Ведомость распознаётся по заголовку «ВЕДОМОСТЬ ПРИЕМА НАЛИЧНЫХ СРЕДСТВ»; из неё берутся номер документа, дата, ФИО кассира, разделы «Город» / «Пригород» и строки водителей. Выгрузка распознаётся по столбцам CONDUCTOR, DATE и TARIF_SUM.
+
📅
Период
Из транзакций берутся только даты, указанные в загруженных ведомостях. Остальные даты показываются справочно и в сверку не входят.
+
🔍
Ключ сопоставления
ФИО водителя + гос. номер ТС. Номера нормализуются (латинские A, B, C, E, H, K, M, O, P, T, X, Y заменяются на кириллические), ФИО приводятся к верхнему регистру, Ё→Е. Опечатки в ФИО сводятся нечётким сравнением при схожести ≥ 88%.
+
💰
Суммы
«Сдано» — графа «Итого на сумму» ведомости. «Собрано» — TARIF_SUM ÷ 100 (копейки → рубли) по всем транзакциям водителя за выбранные даты.
+
🔒
Приватность
Файлы никуда не отправляются — весь расчёт выполняется в браузере на вашем компьютере. Приложение работает без интернета.
- - + + +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=>`${r.driver}${r.car}${r.given.toLocaleString('ru-RU')}${r.collected.toLocaleString('ru-RU')}${r.diff>0?'+':''}${r.diff.toLocaleString('ru-RU')}${r.status==='ok'?'✓ Совпадает':r.status==='warn'?'⚠ -100 ₽':'✕ +400 ₽'}`).join(''); + document.getElementById('countDisc').textContent=result.drivers.filter(d=>d.diff!==0).length; + const tbodyAll=document.getElementById('tableAll'); + tbodyAll.innerHTML=result.all.map(r=>`${r.date}${r.driver}${r.route}${r.given.toLocaleString('ru-RU')}${r.collected.toLocaleString('ru-RU')}${r.transactions.toLocaleString('ru-RU')}`).join(''); + document.getElementById('countAll').textContent=result.all.length; + const tbodyCash=document.getElementById('tableCashiers'); + tbodyCash.innerHTML=result.cashiers.map(r=>`${r.cashier}${r.count}${r.given.toLocaleString('ru-RU')}${r.collected.toLocaleString('ru-RU')}${r.diff.toLocaleString('ru-RU')}`).join(''); + const tbodyRoutes=document.getElementById('tableRoutes'); + tbodyRoutes.innerHTML=result.routes.map(r=>`${r.route}${r.type}${r.drivers}${r.sum.toLocaleString('ru-RU')}`).join(''); + const conc=result.conclusions; + document.getElementById('conclusionsContent').innerHTML=`
Итого сверено: ${conc.totalDrivers} водителей
Сдано: ${conc.totalGiven.toLocaleString('ru-RU')} ₽ · Собрано: ${conc.totalCollected.toLocaleString('ru-RU')} ₽ · Расхождение: ${conc.totalDiff>0?'+':''}${conc.totalDiff.toLocaleString('ru-RU')} ₽
`; + // сортировка + 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'); })); + \ No newline at end of file