Удаление файла
This commit is contained in:
@@ -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();
|
|
||||||
Reference in New Issue
Block a user