From 7175a2e3c2cfaf1a5f18e89b9db6fcb8cac832ef Mon Sep 17 00:00:00 2001 From: host Date: Thu, 30 Jul 2026 17:14:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui.js | 248 ---------------------------------------------------------- 1 file changed, 248 deletions(-) delete mode 100644 ui.js diff --git a/ui.js b/ui.js deleted file mode 100644 index e4faf80..0000000 --- a/ui.js +++ /dev/null @@ -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 = ` -
- ${isTrans ? '📊' : '📋'} -
-
-
${escapeHtml(file.name)}
-
${isTrans ? 'Выгрузка транзакций' : 'Ведомость'} · ${(file.size / 1024).toFixed(0)} КБ
-
- `; - 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 = ` -
⚠️
-
-
${escapeHtml(fileName)}
-
${escapeHtml(message)}
-
`; - 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 = `Нет данных`; - 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 `${formatted}`; - }).join(''); - return `${cells}`; - }).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'; } -} \ No newline at end of file