Создан main.js — главный модуль: инициализация, обработчики событий, рендер результатов
This commit is contained in:
@@ -0,0 +1,520 @@
|
|||||||
|
/**
|
||||||
|
* main.js — Главный модуль приложения
|
||||||
|
*
|
||||||
|
* Инициализация, обработчики событий, координация модулей.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readExcel, validateFile, arrayToCsv, downloadFile, downloadExcel, formatMoney } from './utils.js';
|
||||||
|
import {
|
||||||
|
renderFileItem, renderFileError, renderTableBody, enableSorting,
|
||||||
|
initTabs, initTheme, showProgress, resetProgress
|
||||||
|
} from './ui.js';
|
||||||
|
import { detectFileType, parseVedomost, parseTransactions, reconcile } from './parser.js';
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
СОСТОЯНИЕ ПРИЛОЖЕНИЯ
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
files: [], // загруженные File-объекты
|
||||||
|
fileTypes: {}, // { index: 'vedomost'|'transaction' }
|
||||||
|
parsedVedomosti: [], // распарсенные ведомости
|
||||||
|
parsedTransactions: [], // распарсенные транзакции
|
||||||
|
result: null, // результат сверки
|
||||||
|
isProcessing: false // флаг блокировки
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
DOM-ССЫЛКИ
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
const dom = {
|
||||||
|
dropzone: $('dropzone'),
|
||||||
|
fileInput: $('fileInput'),
|
||||||
|
fileList: $('fileList'),
|
||||||
|
btnReconcile: $('btnReconcile'),
|
||||||
|
btnClear: $('btnClear'),
|
||||||
|
btnExportCsv: $('btnExportCsv'),
|
||||||
|
btnDownload: $('btnDownload'),
|
||||||
|
btnToggleTheme: $('btnToggleTheme'),
|
||||||
|
progressBar: $('progressBar'),
|
||||||
|
progressFill: $('progressFill'),
|
||||||
|
emptyState: $('emptyState'),
|
||||||
|
resultActions: $('resultActions'),
|
||||||
|
tabsContainer: $('tabsContainer'),
|
||||||
|
// Таблицы
|
||||||
|
tableDiscrepancies: $('tableDiscrepancies'),
|
||||||
|
tableAll: $('tableAll'),
|
||||||
|
tableCashiers: $('tableCashiers'),
|
||||||
|
tableRoutes: $('tableRoutes'),
|
||||||
|
// Счётчики
|
||||||
|
countDisc: $('countDisc'),
|
||||||
|
countAll: $('countAll'),
|
||||||
|
// Выводы
|
||||||
|
conclusionsContent: $('conclusionsContent'),
|
||||||
|
// Статус
|
||||||
|
statusBar: $('statusBar')
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
ИНИЦИАЛИЗАЦИЯ
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initTheme('btnToggleTheme');
|
||||||
|
initTabs();
|
||||||
|
bindEvents();
|
||||||
|
updateUI();
|
||||||
|
showStatus('Загрузите файлы ведомостей и выгрузку транзакций для начала работы', 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
СОБЫТИЯ
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
// Dropzone
|
||||||
|
dom.dropzone.addEventListener('click', () => dom.fileInput.click());
|
||||||
|
dom.dropzone.addEventListener('dragover', onDragOver);
|
||||||
|
dom.dropzone.addEventListener('dragleave', onDragLeave);
|
||||||
|
dom.dropzone.addEventListener('drop', onDrop);
|
||||||
|
dom.fileInput.addEventListener('change', onFileInputChange);
|
||||||
|
|
||||||
|
// Кнопки
|
||||||
|
dom.btnReconcile.addEventListener('click', onReconcile);
|
||||||
|
dom.btnClear.addEventListener('click', onClear);
|
||||||
|
dom.btnExportCsv.addEventListener('click', onExportCsv);
|
||||||
|
dom.btnDownload.addEventListener('click', onDownloadExcel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
ОБРАБОТЧИКИ ФАЙЛОВ
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function onDragOver(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dom.dropzone.classList.add('dragover');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragLeave() {
|
||||||
|
dom.dropzone.classList.remove('dragover');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dom.dropzone.classList.remove('dragover');
|
||||||
|
handleFiles(e.dataTransfer.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFileInputChange(e) {
|
||||||
|
handleFiles(e.target.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFiles(fileList) {
|
||||||
|
if (state.isProcessing) {
|
||||||
|
showStatus('Дождитесь завершения текущей операции', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let added = 0;
|
||||||
|
for (const file of fileList) {
|
||||||
|
if (!validateFile(file)) continue;
|
||||||
|
// Проверка на дубликат
|
||||||
|
if (state.files.some(f => f.name === file.name && f.size === file.size)) {
|
||||||
|
showStatus(`Файл "${file.name}" уже загружен`, 'warning');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
state.files.push(file);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added === 0) return;
|
||||||
|
|
||||||
|
// Определяем типы файлов асинхронно
|
||||||
|
await detectTypes();
|
||||||
|
|
||||||
|
// Проверяем, что есть хотя бы одна ведомость и одна выгрузка
|
||||||
|
const hasVedomost = Object.values(state.fileTypes).includes('vedomost');
|
||||||
|
const hasTransaction = Object.values(state.fileTypes).includes('transaction');
|
||||||
|
|
||||||
|
renderFileList();
|
||||||
|
updateUI();
|
||||||
|
|
||||||
|
if (hasVedomost && hasTransaction) {
|
||||||
|
showStatus('Файлы загружены. Нажмите «Провести сверку»', 'success');
|
||||||
|
} else if (hasVedomost) {
|
||||||
|
showStatus('Загружена ведомость. Добавьте выгрузку транзакций (TRANSACTIONS_EXT)', 'info');
|
||||||
|
} else if (hasTransaction) {
|
||||||
|
showStatus('Загружена выгрузка транзакций. Добавьте ведомости', 'info');
|
||||||
|
} else {
|
||||||
|
showStatus('Файлы не распознаны. Загрузите ведомости (.xlsx) и/или выгрузку TRANSACTIONS_EXT', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определить типы всех загруженных файлов
|
||||||
|
*/
|
||||||
|
async function detectTypes() {
|
||||||
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
|
if (state.fileTypes[i]) continue; // уже определён
|
||||||
|
try {
|
||||||
|
const sheets = await readExcel(state.files[i]);
|
||||||
|
const firstSheet = sheets[0]?.data || [];
|
||||||
|
const type = detectFileType(state.files[i].name, firstSheet);
|
||||||
|
if (type) {
|
||||||
|
state.fileTypes[i] = type;
|
||||||
|
} else {
|
||||||
|
state.fileTypes[i] = 'unknown';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка чтения файла:', state.files[i].name, err);
|
||||||
|
state.fileTypes[i] = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Рендер списка файлов
|
||||||
|
*/
|
||||||
|
function renderFileList() {
|
||||||
|
dom.fileList.innerHTML = '';
|
||||||
|
state.files.forEach((file, i) => {
|
||||||
|
const type = state.fileTypes[i];
|
||||||
|
if (type === 'error' || type === 'unknown') {
|
||||||
|
const msg = type === 'error'
|
||||||
|
? 'Ошибка чтения файла'
|
||||||
|
: 'Не удалось определить тип';
|
||||||
|
dom.fileList.appendChild(renderFileError(file.name, msg));
|
||||||
|
} else {
|
||||||
|
dom.fileList.appendChild(renderFileItem(file, i, type));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Кнопки удаления
|
||||||
|
dom.fileList.querySelectorAll('.file-remove').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const idx = parseInt(e.currentTarget.dataset.i);
|
||||||
|
state.files.splice(idx, 1);
|
||||||
|
// Перестраиваем индексы
|
||||||
|
const newTypes = {};
|
||||||
|
Object.keys(state.fileTypes).forEach(k => {
|
||||||
|
const ki = parseInt(k);
|
||||||
|
if (ki < idx) newTypes[ki] = state.fileTypes[k];
|
||||||
|
else if (ki > idx) newTypes[ki - 1] = state.fileTypes[k];
|
||||||
|
});
|
||||||
|
state.fileTypes = newTypes;
|
||||||
|
state.parsedVedomosti = [];
|
||||||
|
state.parsedTransactions = [];
|
||||||
|
state.result = null;
|
||||||
|
renderFileList();
|
||||||
|
updateUI();
|
||||||
|
hideResult();
|
||||||
|
showStatus('Файл удалён', 'info');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
СВЕРКА
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
async function onReconcile() {
|
||||||
|
if (state.isProcessing) return;
|
||||||
|
state.isProcessing = true;
|
||||||
|
dom.btnReconcile.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
showProgress(1200);
|
||||||
|
|
||||||
|
// Парсим все файлы
|
||||||
|
state.parsedVedomosti = [];
|
||||||
|
state.parsedTransactions = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
|
const type = state.fileTypes[i];
|
||||||
|
if (type === 'unknown' || type === 'error') continue;
|
||||||
|
|
||||||
|
const sheets = await readExcel(state.files[i]);
|
||||||
|
const data = sheets[0]?.data || [];
|
||||||
|
|
||||||
|
if (type === 'vedomost') {
|
||||||
|
const ved = parseVedomost(data);
|
||||||
|
if (ved && ved.drivers.length > 0) {
|
||||||
|
state.parsedVedomosti.push(ved);
|
||||||
|
} else {
|
||||||
|
showStatus(`Ведомость "${state.files[i].name}" не содержит данных о водителях`, 'warning');
|
||||||
|
}
|
||||||
|
} else if (type === 'transaction') {
|
||||||
|
const txs = parseTransactions(data);
|
||||||
|
if (txs.length > 0) {
|
||||||
|
state.parsedTransactions.push(...txs);
|
||||||
|
} else {
|
||||||
|
showStatus(`Выгрузка "${state.files[i].name}" не содержит транзакций`, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка наличия данных
|
||||||
|
if (state.parsedVedomosti.length === 0) {
|
||||||
|
showStatus('Не найдено ни одной ведомости с данными о водителях', 'error');
|
||||||
|
state.isProcessing = false;
|
||||||
|
dom.btnReconcile.disabled = false;
|
||||||
|
resetProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.parsedTransactions.length === 0) {
|
||||||
|
showStatus('Не найдено транзакций для сверки', 'error');
|
||||||
|
state.isProcessing = false;
|
||||||
|
dom.btnReconcile.disabled = false;
|
||||||
|
resetProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем сверку
|
||||||
|
state.result = reconcile(state.parsedVedomosti, state.parsedTransactions);
|
||||||
|
|
||||||
|
if (state.result.drivers.length === 0) {
|
||||||
|
showStatus('Не удалось сопоставить ни одного водителя. Проверьте формат файлов', 'error');
|
||||||
|
state.isProcessing = false;
|
||||||
|
dom.btnReconcile.disabled = false;
|
||||||
|
resetProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResult();
|
||||||
|
updateUI();
|
||||||
|
showStatus(
|
||||||
|
`✅ Сверка завершена: ${state.result.drivers.length} водителей, ` +
|
||||||
|
`${state.result.conclusions.discrepanciesCount} расхождений`,
|
||||||
|
state.result.conclusions.discrepanciesCount > 0 ? 'warning' : 'success'
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка сверки:', err);
|
||||||
|
showStatus('❌ Ошибка при выполнении сверки: ' + err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
state.isProcessing = false;
|
||||||
|
dom.btnReconcile.disabled = false;
|
||||||
|
resetProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
РЕНДЕР РЕЗУЛЬТАТА
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function renderResult() {
|
||||||
|
if (!state.result) return;
|
||||||
|
|
||||||
|
const r = state.result;
|
||||||
|
|
||||||
|
// Скрываем пустое состояние, показываем результат
|
||||||
|
dom.emptyState.style.display = 'none';
|
||||||
|
dom.resultActions.style.display = '';
|
||||||
|
dom.tabsContainer.style.display = '';
|
||||||
|
|
||||||
|
// --- Расхождения ---
|
||||||
|
const discData = r.drivers.filter(d => d.diff !== 0);
|
||||||
|
renderTableBody(dom.tableDiscrepancies, discData, ['driver', 'car', 'given', 'collected', 'diff', 'status'], (val, key, row) => {
|
||||||
|
if (key === 'status') {
|
||||||
|
const labels = { ok: '✓ Совпадает', warn: `⚠ ${formatMoney(row.diff)} ₽`, err: `✕ ${formatMoney(Math.abs(row.diff))} ₽` };
|
||||||
|
return `<span class="badge badge-${val}">${labels[val] || val}</span>`;
|
||||||
|
}
|
||||||
|
if (key === 'diff') {
|
||||||
|
if (val === 0) return `<span class="diff-zero">0 ₽</span>`;
|
||||||
|
return `<span class="${val > 0 ? 'diff-positive' : 'diff-negative'}">${val > 0 ? '+' : ''}${formatMoney(val)} ₽</span>`;
|
||||||
|
}
|
||||||
|
if (typeof val === 'number') return formatMoney(val);
|
||||||
|
return val || '—';
|
||||||
|
});
|
||||||
|
enableSorting('tableDiscrepancies', discData, (sorted) => {
|
||||||
|
renderTableBody(dom.tableDiscrepancies, sorted, ['driver', 'car', 'given', 'collected', 'diff', 'status']);
|
||||||
|
});
|
||||||
|
dom.countDisc.textContent = discData.length;
|
||||||
|
|
||||||
|
// --- Все водители ---
|
||||||
|
const allData = r.drivers.map(d => ({
|
||||||
|
date: d.date,
|
||||||
|
driver: d.driver,
|
||||||
|
route: d.route,
|
||||||
|
section: d.section,
|
||||||
|
given: d.given,
|
||||||
|
collected: d.collected,
|
||||||
|
txCount: d.txCount
|
||||||
|
}));
|
||||||
|
renderTableBody(dom.tableAll, allData, ['date', 'driver', 'route', 'section', 'given', 'collected', 'txCount'], (val, key) => {
|
||||||
|
if (key === 'section') {
|
||||||
|
return val === 'Город' ? '🏙️ Город' : '🚌 Пригород';
|
||||||
|
}
|
||||||
|
if (typeof val === 'number') return formatMoney(val);
|
||||||
|
return val || '—';
|
||||||
|
});
|
||||||
|
enableSorting('tableAll', allData, (sorted) => {
|
||||||
|
renderTableBody(dom.tableAll, sorted, ['date', 'driver', 'route', 'section', 'given', 'collected', 'txCount']);
|
||||||
|
});
|
||||||
|
dom.countAll.textContent = allData.length;
|
||||||
|
|
||||||
|
// --- По кассирам ---
|
||||||
|
renderTableBody(dom.tableCashiers, r.cashiers, ['cashier', 'count', 'given', 'collected', 'diff'], (val, key) => {
|
||||||
|
if (key === 'diff') {
|
||||||
|
if (val === 0) return `<span class="diff-zero">0 ₽</span>`;
|
||||||
|
return `<span class="${val > 0 ? 'diff-positive' : 'diff-negative'}">${val > 0 ? '+' : ''}${formatMoney(val)} ₽</span>`;
|
||||||
|
}
|
||||||
|
if (typeof val === 'number') return formatMoney(val);
|
||||||
|
return val || '—';
|
||||||
|
});
|
||||||
|
enableSorting('tableCashiers', r.cashiers, (sorted) => {
|
||||||
|
renderTableBody(dom.tableCashiers, sorted, ['cashier', 'count', 'given', 'collected', 'diff']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- По маршрутам ---
|
||||||
|
renderTableBody(dom.tableRoutes, r.routes, ['route', 'type', 'drivers', 'sum'], (val, key) => {
|
||||||
|
if (typeof val === 'number') return formatMoney(val);
|
||||||
|
return val || '—';
|
||||||
|
});
|
||||||
|
enableSorting('tableRoutes', r.routes, (sorted) => {
|
||||||
|
renderTableBody(dom.tableRoutes, sorted, ['route', 'type', 'drivers', 'sum']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Выводы ---
|
||||||
|
const c = r.conclusions;
|
||||||
|
const diffClass = c.totalDiff === 0 ? '' : (c.totalDiff > 0 ? 'err' : 'warn');
|
||||||
|
dom.conclusionsContent.innerHTML = `
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="info-block ${diffClass}">
|
||||||
|
<div class="info-block-icon ${c.totalDiff === 0 ? 'green' : (c.totalDiff > 0 ? 'rose' : 'amber')}">
|
||||||
|
${c.totalDiff === 0 ? '✓' : (c.totalDiff > 0 ? '⚠' : 'ℹ')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="info-block-title" style="color:${c.totalDiff === 0 ? 'var(--success-text)' : 'var(--danger-text)'}">
|
||||||
|
Итого сверено: ${c.totalDrivers} водителей
|
||||||
|
</div>
|
||||||
|
<div class="info-block-text">
|
||||||
|
<strong>Сдано:</strong> ${formatMoney(c.totalGiven)} ₽ ·
|
||||||
|
<strong>Собрано:</strong> ${formatMoney(c.totalCollected)} ₽ ·
|
||||||
|
<strong>Расхождение:</strong>
|
||||||
|
<span style="color:${c.totalDiff === 0 ? 'var(--text-muted)' : (c.totalDiff > 0 ? 'var(--danger)' : 'var(--success)')};font-weight:600">
|
||||||
|
${c.totalDiff > 0 ? '+' : ''}${formatMoney(c.totalDiff)} ₽
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-block-text" style="margin-top:0.5rem;">
|
||||||
|
Совпадений: ${c.totalDrivers - c.discrepanciesCount} из ${c.totalDrivers} (${c.matchedPercent}%)
|
||||||
|
${c.discrepanciesCount > 0 ? `<span style="color:var(--danger)"> · Расхождений: ${c.discrepanciesCount}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${r.unmatchedTx.length > 0 ? `
|
||||||
|
<div class="info-block warn" style="margin-top:1rem;">
|
||||||
|
<div class="info-block-icon amber">⚠</div>
|
||||||
|
<div>
|
||||||
|
<div class="info-block-title" style="color:var(--warning-text)">
|
||||||
|
Не сопоставлено транзакций: ${r.unmatchedTx.length}
|
||||||
|
</div>
|
||||||
|
<div class="info-block-text">
|
||||||
|
Эти водители есть в выгрузке, но не найдены в ведомостях.
|
||||||
|
Возможные причины: опечатки в ФИО, разные гос. номера, или водитель работал без ведомости.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
ЭКСПОРТ
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function onExportCsv() {
|
||||||
|
if (!state.result) return;
|
||||||
|
const data = state.result.drivers.map(r => ({
|
||||||
|
driver: r.driver,
|
||||||
|
car: r.car,
|
||||||
|
route: r.route,
|
||||||
|
cashier: r.cashier,
|
||||||
|
date: r.date,
|
||||||
|
given: r.given,
|
||||||
|
collected: r.collected,
|
||||||
|
diff: r.diff,
|
||||||
|
status: r.status === 'ok' ? 'Совпадает' : (r.status === 'warn' ? 'Мелкое расхождение' : 'Расхождение')
|
||||||
|
}));
|
||||||
|
const csv = arrayToCsv(data, ['driver', 'car', 'route', 'cashier', 'date', 'given', 'collected', 'diff', 'status']);
|
||||||
|
downloadFile(csv, 'sverka_vyruchki.csv');
|
||||||
|
showStatus('CSV-файл скачан', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDownloadExcel() {
|
||||||
|
if (!state.result) return;
|
||||||
|
const data = state.result.drivers.map(r => ({
|
||||||
|
'Водитель': r.driver,
|
||||||
|
'Гос. номер': r.car,
|
||||||
|
'Маршрут': r.route,
|
||||||
|
'Кассир': r.cashier,
|
||||||
|
'Дата': r.date,
|
||||||
|
'Сдано (₽)': r.given,
|
||||||
|
'Собрано (₽)': r.collected,
|
||||||
|
'Расхождение (₽)': r.diff,
|
||||||
|
'Статус': r.status === 'ok' ? 'Совпадает' : (r.status === 'warn' ? 'Мелкое расхождение' : 'Расхождение')
|
||||||
|
}));
|
||||||
|
downloadExcel(data, 'sverka_vyruchki.xlsx');
|
||||||
|
showStatus('Excel-отчёт скачан', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
ОЧИСТКА
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
if (state.isProcessing) return;
|
||||||
|
state.files = [];
|
||||||
|
state.fileTypes = {};
|
||||||
|
state.parsedVedomosti = [];
|
||||||
|
state.parsedTransactions = [];
|
||||||
|
state.result = null;
|
||||||
|
dom.fileInput.value = '';
|
||||||
|
renderFileList();
|
||||||
|
updateUI();
|
||||||
|
hideResult();
|
||||||
|
showStatus('Все файлы удалены', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
ОБНОВЛЕНИЕ UI
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
const hasFiles = state.files.length > 0;
|
||||||
|
const hasResult = state.result !== null;
|
||||||
|
|
||||||
|
dom.btnReconcile.disabled = !hasFiles || state.isProcessing;
|
||||||
|
dom.btnClear.disabled = !hasFiles || state.isProcessing;
|
||||||
|
dom.btnExportCsv.disabled = !hasResult;
|
||||||
|
dom.btnDownload.disabled = !hasResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideResult() {
|
||||||
|
dom.emptyState.style.display = '';
|
||||||
|
dom.resultActions.style.display = 'none';
|
||||||
|
dom.tabsContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
СТАТУС-БАР
|
||||||
|
================================================================ */
|
||||||
|
|
||||||
|
function showStatus(message, type = 'info') {
|
||||||
|
const bar = dom.statusBar;
|
||||||
|
if (!bar) return;
|
||||||
|
bar.textContent = message;
|
||||||
|
bar.className = 'status-bar';
|
||||||
|
bar.classList.add('status-' + type);
|
||||||
|
bar.style.display = '';
|
||||||
|
// Авто-скрытие через 8 секунд для успеха и информации
|
||||||
|
if (type === 'success' || type === 'info') {
|
||||||
|
clearTimeout(bar._hideTimer);
|
||||||
|
bar._hideTimer = setTimeout(() => {
|
||||||
|
bar.style.display = 'none';
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user