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

This commit is contained in:
2026-07-30 17:14:06 +03:00
parent 8d84056cc1
commit 7175a2e3c2
-248
View File
@@ -1,248 +0,0 @@
/**
* ui.js — Компоненты интерфейса
* Рендер файлов, таблиц, сортировка, вкладки, управление темой
*/
import { formatMoney } from './utils.js';
/* ========== Управление файлами ========== */
/**
* Создать DOM-элемент для отображения загруженного файла
* @param {File} file
* @param {number} index
* @param {string} type - 'vedomost' | 'transaction'
* @returns {HTMLElement}
*/
export function renderFileItem(file, index, type) {
const isTrans = type === 'transaction';
const el = document.createElement('div');
el.className = 'file-item';
el.dataset.index = index;
el.innerHTML = `
<div class="file-icon ${isTrans ? 'transaction' : 'vedomost'}">
${isTrans ? '📊' : '📋'}
</div>
<div class="file-info">
<div class="file-name">${escapeHtml(file.name)}</div>
<div class="file-type">${isTrans ? 'Выгрузка транзакций' : 'Ведомость'} · ${(file.size / 1024).toFixed(0)} КБ</div>
</div>
<button class="file-remove" data-i="${index}" title="Удалить">✕</button>`;
return el;
}
/**
* Создать DOM-элемент для сообщения об ошибке в файле
* @param {string} fileName
* @param {string} message
* @returns {HTMLElement}
*/
export function renderFileError(fileName, message) {
const el = document.createElement('div');
el.className = 'file-item file-error';
el.innerHTML = `
<div class="file-icon" style="background:#fee2e2;">⚠️</div>
<div class="file-info">
<div class="file-name">${escapeHtml(fileName)}</div>
<div class="file-type" style="color:#ef4444;">${escapeHtml(message)}</div>
</div>`;
return el;
}
/* ========== Сортировка таблиц ========== */
/**
* Отсортировать массив данных по ключу
* @param {Object[]} data
* @param {string} key
* @param {boolean} asc
* @returns {Object[]}
*/
export function sortData(data, key, asc = true) {
return [...data].sort((a, b) => {
let av = a[key], bv = b[key];
if (av === null || av === undefined) av = '';
if (bv === null || bv === undefined) bv = '';
if (typeof av === 'number' && typeof bv === 'number') {
return asc ? av - bv : bv - av;
}
// Числовые строки сравниваем как числа
const na = parseFloat(String(av).replace(/\s/g, '').replace(',', '.'));
const nb = parseFloat(String(bv).replace(/\s/g, '').replace(',', '.'));
if (!isNaN(na) && !isNaN(nb)) {
return asc ? na - nb : nb - na;
}
return asc
? String(av).localeCompare(String(bv), 'ru')
: String(bv).localeCompare(String(av), 'ru');
});
}
/**
* Включить сортировку по клику на заголовки таблицы
* @param {string} tbodyId - ID tbody
* @param {Object[]} sourceData - ссылка на исходный массив
* @param {Function} renderFn - функция перерисовки
*/
export function enableSorting(tbodyId, sourceData, renderFn) {
const tbody = document.getElementById(tbodyId);
if (!tbody) return;
const table = tbody.closest('table');
if (!table) return;
const ths = table.querySelectorAll('th[data-key]');
let sortState = { key: null, asc: true };
ths.forEach(th => {
th.style.cursor = 'pointer';
th.addEventListener('click', () => {
const key = th.dataset.key;
if (sortState.key === key) {
sortState.asc = !sortState.asc;
} else {
sortState.key = key;
sortState.asc = true;
}
// Визуальный индикатор
ths.forEach(t => t.classList.remove('sort-asc', 'sort-desc'));
th.classList.add(sortState.asc ? 'sort-asc' : 'sort-desc');
const sorted = sortData(sourceData, key, sortState.asc);
renderFn(sorted);
});
});
}
/* ========== Рендер таблиц ========== */
/**
* Рендер тела таблицы из массива объектов
* @param {HTMLElement} tbody
* @param {Object[]} rows
* @param {string[]} keys - порядок/набор ключей для отображения
* @param {Function} formatter - функция форматирования ячейки (value, key, row) => string
*/
export function renderTableBody(tbody, rows, keys, formatter) {
if (!rows || rows.length === 0) {
tbody.innerHTML = `<tr><td colspan="${(keys || Object.keys(rows[0] || {})).length}" style="text-align:center;padding:2rem;color:var(--text-muted)">Нет данных</td></tr>`;
return;
}
tbody.innerHTML = rows.map(row => {
const cells = (keys || Object.keys(row)).map(key => {
const val = row[key];
const formatted = formatter ? formatter(val, key, row) : formatCell(val);
return `<td>${formatted}</td>`;
}).join('');
return `<tr>${cells}</tr>`;
}).join('');
}
/**
* Форматирование значения ячейки по умолчанию
*/
function formatCell(val) {
if (val === null || val === undefined || val === '') return '—';
if (typeof val === 'number') return formatMoney(val);
return escapeHtml(String(val));
}
/**
* Экранирование HTML
*/
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
/* ========== Управление вкладками ========== */
/**
* Инициализировать переключатели вкладок
*/
export function initTabs() {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(x => x.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(x => x.classList.remove('active'));
btn.classList.add('active');
const panel = document.getElementById('panel-' + btn.dataset.tab);
if (panel) panel.classList.add('active');
});
});
}
/* ========== Тема ========== */
let currentTheme = localStorage.getItem('theme') || 'light';
/**
* Применить тему
*/
export function applyTheme() {
document.documentElement.setAttribute('data-theme', currentTheme);
const btn = document.getElementById('btnToggleTheme');
if (btn) {
btn.textContent = currentTheme === 'light'
? '🌙 Тёмная тема'
: '☀️ Светлая тема';
}
}
/**
* Переключить тему
*/
export function toggleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', currentTheme);
applyTheme();
}
/**
* Инициализировать тему
*/
export function initTheme(toggleBtnId) {
applyTheme();
const btn = document.getElementById(toggleBtnId);
if (btn) btn.addEventListener('click', toggleTheme);
}
/* ========== Прогресс-бар ========== */
/**
* Показать прогресс-бар с анимацией
* @param {number} duration - длительность анимации в мс
* @returns {Promise} - разрешается по завершении
*/
export function showProgress(duration = 800) {
const bar = document.getElementById('progressBar');
const fill = document.getElementById('progressFill');
if (!bar || !fill) return Promise.resolve();
bar.classList.add('active');
fill.style.width = '0%';
return new Promise(resolve => {
// Плавная анимация до 95%
requestAnimationFrame(() => {
fill.style.transition = `width ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
fill.style.width = '95%';
});
// Завершение
setTimeout(() => {
fill.style.width = '100%';
setTimeout(() => {
bar.classList.remove('active');
fill.style.width = '0%';
fill.style.transition = 'none';
resolve();
}, 300);
}, duration + 200);
});
}
/**
* Сбросить прогресс-бар
*/
export function resetProgress() {
const bar = document.getElementById('progressBar');
const fill = document.getElementById('progressFill');
if (bar) bar.classList.remove('active');
if (fill) { fill.style.width = '0%'; fill.style.transition = 'none'; }
}