// 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();