diff --git a/src/ui.js b/src/ui.js
new file mode 100644
index 0000000..f80d1e5
--- /dev/null
+++ b/src/ui.js
@@ -0,0 +1,217 @@
+// 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();