diff --git a/src/ui.js b/src/ui.js deleted file mode 100644 index f80d1e5..0000000 --- a/src/ui.js +++ /dev/null @@ -1,217 +0,0 @@ -// ui.js -// Основные функции UI: загрузка файлов, отображение списка, запуск сверки, сортировка, фильтрация, экспорт CSV, переключатель темы. -import {readExcel, validateFile, normalizePlate, normalizeName, similarity} from './utils.js'; - -const state = { - files: [], // загруженные файлы (File objects) - vedomosti: [], // массив объектов {driver, plate, cash} - transactions: [], // массив объектов {driver, date, sum} - results: [] // массив {driver, plate, cash, collected, diff, status} -}; - -// ---------- Элементы UI ---------- -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 progressBar = document.getElementById('progressBar'); -const progressFill = document.getElementById('progressFill'); -const emptyState = document.getElementById('emptyState'); -const resultActions = document.getElementById('resultActions'); -const tabsContainer = document.getElementById('tabsContainer'); -const themeToggle = document.getElementById('themeToggle'); - -// ---------- Загрузка файлов ---------- -function initDropzone() { - 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)) state.files.push(f); - } - renderFileList(); - updateButtons(); -} - -function renderFileList() { - fileList.innerHTML = ''; - state.files.forEach((file, i) => { - const isTrans = /transaction/i.test(file.name); - const el = document.createElement('div'); - el.className = 'file-item'; - el.innerHTML = `
${isTrans ? 'TX' : 'VD'}
-
${file.name}
-
${isTrans ? 'Выгрузка транзакций' : 'Ведомость'} • ${(file.size / 1024).toFixed(0)} КБ
- `; - fileList.appendChild(el); - }); - fileList.querySelectorAll('.file-remove').forEach(b => { - b.addEventListener('click', e => { - const idx = +e.currentTarget.dataset.i; - state.files.splice(idx, 1); - renderFileList(); - updateButtons(); - }); - }); -} - -function updateButtons() { - const enabled = state.files.length > 0; - btnReconcile.disabled = !enabled; - btnClear.disabled = !enabled; -} - -btnClear.addEventListener('click', () => { - state.files = []; - fileInput.value = ''; - renderFileList(); - updateButtons(); - emptyState.style.display = ''; - resultActions.style.display = 'none'; - tabsContainer.style.display = 'none'; -}); - -// ---------- Тема ---------- -themeToggle.addEventListener('click', () => { - const html = document.documentElement; - const dark = html.dataset.theme === 'dark'; - html.dataset.theme = dark ? '' : 'dark'; - themeToggle.textContent = dark ? 'Светлая тема' : 'Тёмная тема'; -}); - -// ---------- Сверка ---------- -btnReconcile.addEventListener('click', async () => { - progressBar.classList.add('active'); - progressFill.style.width = '0%'; - let progress = 0; - const step = inc => { - progress = Math.min(100, progress + inc); - progressFill.style.width = `${progress}%`; - }; - - // 1. Чтение файлов - const sheetsArr = await Promise.all(state.files.map(f => readExcel(f))); - state.vedomosti = []; - state.transactions = []; - sheetsArr.forEach((sheets, idx) => { - const file = state.files[idx]; - const isTrans = /transaction/i.test(file.name); - const sheet = sheets[0]; // берем первый лист - const header = sheet.data[0].map(h => String(h).toUpperCase().trim()); - if (isTrans) { - const iDriver = header.findIndex(h => h.includes('CONDUCTOR')); - const iDate = header.findIndex(h => h.includes('DATE')); - const iSum = header.findIndex(h => h.includes('TARIF_SUM')); - if (iDriver === -1 || iDate === -1 || iSum === -1) return; // пропускаем некорректные - for (let r = 1; r < sheet.data.length; r++) { - const row = sheet.data[r]; - const driver = String(row[iDriver] || '').trim(); - const date = String(row[iDate] || '').trim(); - const sum = Number(row[iSum] || 0) / 100; // копейки -> рубли - if (driver) state.transactions.push({ driver, date, sum }); - } - } else { - const iDriver = header.findIndex(h => h.includes('ВОДИТЕЛЬ') || h.includes('DRIVER')); - const iPlate = header.findIndex(h => h.includes('ГОС. НОМЕР') || h.includes('PLATE')); - const iCash = header.findIndex(h => h.includes('СДАНО') || h.includes('CASH')); - if (iDriver === -1) return; - for (let r = 1; r < sheet.data.length; r++) { - const row = sheet.data[r]; - const driver = String(row[iDriver] || '').trim(); - const plate = iPlate !== -1 ? String(row[iPlate] || '').trim() : ''; - const cash = iCash !== -1 ? Number(row[iCash] || 0) : 0; - state.vedomosti.push({ driver, plate, cash }); - } - } - }); - step(20); - - // 2. Сопоставление и расчёт - const transMap = {}; - state.transactions.forEach(t => { - const key = `${normalizeName(t.driver)}|${normalizePlate(t.driver)}`; - transMap[key] = (transMap[key] || 0) + t.sum; - }); - state.results = state.vedomosti.map(v => { - const key = `${normalizeName(v.driver)}|${normalizePlate(v.plate)}`; - const collected = transMap[key] || 0; - const diff = collected - v.cash; - let status = 'ok'; - if (diff === 0) status = 'ok'; - else if (Math.abs(diff) <= 100) status = 'warn'; - else status = 'err'; - return { driver: v.driver, plate: v.plate, cash: v.cash, collected, diff, status }; - }); - step(40); - - // 3. Отображение результатов - renderResults(); - step(40); - progressBar.classList.remove('active'); -}); - -function renderResults() { - emptyState.style.display = 'none'; - resultActions.style.display = ''; - tabsContainer.style.display = ''; - // Таблица расхождений - const tbody = document.getElementById('tableDiscrepancies'); - tbody.innerHTML = state.results.map(r => { - const diffColor = r.diff === 0 ? '#10b981' : '#ef4444'; - const badgeClass = r.status === 'ok' ? 'badge-ok' : (r.status === 'warn' ? 'badge-warn' : 'badge-err'); - const badgeText = r.status === 'ok' ? '✓ Совпадает' : (r.status === 'warn' ? `⚠ ${r.diff} ₽` : `✕ ${r.diff} ₽`); - const diffSign = r.diff > 0 ? '+' : ''; - return ` - ${r.driver} - ${r.plate} - ${r.cash.toLocaleString('ru-RU')} - ${r.collected.toLocaleString('ru-RU')} - ${diffSign}${r.diff.toLocaleString('ru-RU')} - ${badgeText} - `; - }).join(''); - document.getElementById('countDisc').textContent = state.results.filter(r => r.status !== 'ok').length; - document.getElementById('countAll').textContent = state.results.length; - // Таблица "Все водители" - const allBody = document.getElementById('tableAll'); - allBody.innerHTML = state.results.map(r => `-${r.driver}-${r.cash}${r.collected}${r.diff}`).join(''); - // CSV экспорт - document.getElementById('btnDownload').onclick = exportCSV; -} - -function exportCSV() { - const header = ['Водитель', 'Гос. номер', 'Сдано', 'Собрано', 'Расхождение', 'Статус']; - const rows = state.results.map(r => [r.driver, r.plate, r.cash, r.collected, r.diff, r.status]); - const csvContent = [header, ...rows] - .map(row => row.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')) - .join('\n'); - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'reconciliation.csv'; - a.click(); - URL.revokeObjectURL(url); -} - -// ---------- Таб переключения ---------- -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'); - }); -}); - -// ---------- Инициализация ---------- -initDropzone();