Удаление файла

This commit is contained in:
2026-07-30 17:19:09 +03:00
parent 91d14ec67a
commit 65a4ddd6c6
-217
View File
@@ -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 = `<div class="file-icon ${isTrans ? 'transaction' : 'vedomost'}">${isTrans ? 'TX' : 'VD'}</div>
<div class="file-info"><div class="file-name">${file.name}</div>
<div class="file-type">${isTrans ? 'Выгрузка транзакций' : 'Ведомость'}${(file.size / 1024).toFixed(0)} КБ</div></div>
<button class="file-remove" data-i="${i}" title="Удалить">✕</button>`;
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 `<tr>
<td>${r.driver}</td>
<td><code>${r.plate}</code></td>
<td>${r.cash.toLocaleString('ru-RU')}</td>
<td>${r.collected.toLocaleString('ru-RU')}</td>
<td style="color:${diffColor};font-weight:600;">${diffSign}${r.diff.toLocaleString('ru-RU')}</td>
<td><span class="badge ${badgeClass}">${badgeText}</span></td>
</tr>`;
}).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 => `<tr><td>-</td><td>${r.driver}</td><td>-</td><td>${r.cash}</td><td>${r.collected}</td><td>${r.diff}</td></tr>`).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();