diff --git a/app.js b/app.js index 9493440..db22e57 100644 --- a/app.js +++ b/app.js @@ -1770,12 +1770,16 @@ const csvColumnLabels = { // Preferred column order (most important first) const csvColumnOrder = ['transaction_date', 'simple_name', 'amount_rub', 'category', 'subcategory', 'microcategory', 'transaction_description', 'merchant_name', 'account', 'card_info', 'location', 'amount_original', 'currency_original', 'mcc', 'info_source', 'processing_date', 'comment']; +// Modal state +let currentTransactions = []; +let currentColumns = []; +let currentSortColumn = null; +let currentSortDirection = 'desc'; + // Open transaction modal function openTransactionModal(item) { const modal = document.getElementById('transaction-modal'); const modalTitle = document.getElementById('modal-title'); - const modalThead = document.getElementById('modal-thead'); - const modalTbody = document.getElementById('modal-tbody'); // Set title modalTitle.textContent = item.name; @@ -1788,89 +1792,208 @@ function openTransactionModal(item) { transactions = [item]; } - // Clear previous content - modalThead.innerHTML = ''; - modalTbody.innerHTML = ''; - if (transactions.length === 0) { + const modalTbody = document.getElementById('modal-tbody'); + const modalThead = document.getElementById('modal-thead'); + modalThead.innerHTML = ''; modalTbody.innerHTML = 'No transaction data available'; modal.style.display = 'flex'; setupModalListeners(); return; } - // Sort by date descending - transactions.sort((a, b) => { - const dateA = a.originalRow?.transaction_date || a.date || ''; - const dateB = b.originalRow?.transaction_date || b.date || ''; - return dateB.localeCompare(dateA); + // Get columns from first transaction + const sampleRow = transactions[0].originalRow; + currentColumns = csvColumnOrder.filter(col => col in sampleRow); + currentTransactions = transactions; + + // Initial sort by amount descending + currentSortColumn = 'amount_rub'; + currentSortDirection = 'desc'; + + renderTransactionTable(); + modal.style.display = 'flex'; + + // Reset scroll position to top-left + const tableContainer = document.querySelector('.modal-table-container'); + tableContainer.scrollTop = 0; + tableContainer.scrollLeft = 0; + + setupModalListeners(); +} + +// Render the transaction table with current sort +function renderTransactionTable() { + const modalThead = document.getElementById('modal-thead'); + const modalTbody = document.getElementById('modal-tbody'); + + // Sort transactions + const sortedTransactions = [...currentTransactions].sort((a, b) => { + const aVal = a.originalRow?.[currentSortColumn] ?? ''; + const bVal = b.originalRow?.[currentSortColumn] ?? ''; + + // Numeric sort for amount columns + if (currentSortColumn === 'amount_rub' || currentSortColumn === 'amount_original') { + const aNum = parseFloat(aVal) || 0; + const bNum = parseFloat(bVal) || 0; + return currentSortDirection === 'asc' ? aNum - bNum : bNum - aNum; + } + + // String sort for other columns + const aStr = String(aVal); + const bStr = String(bVal); + const cmp = aStr.localeCompare(bStr); + return currentSortDirection === 'asc' ? cmp : -cmp; }); - // Get all columns from the first transaction's originalRow - const sampleRow = transactions[0].originalRow; - const availableColumns = csvColumnOrder.filter(col => col in sampleRow); + // Clear content + modalThead.innerHTML = ''; + modalTbody.innerHTML = ''; - // Build header + // Build header with sort indicators const headerRow = document.createElement('tr'); - availableColumns.forEach(col => { + currentColumns.forEach(col => { const th = document.createElement('th'); th.textContent = csvColumnLabels[col] || col; + th.dataset.column = col; + + // Add sort class + if (col === currentSortColumn) { + th.classList.add(currentSortDirection === 'asc' ? 'sort-asc' : 'sort-desc'); + } + + // Click to sort + th.addEventListener('click', () => { + if (currentSortColumn === col) { + currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc'; + } else { + currentSortColumn = col; + currentSortDirection = 'asc'; + } + renderTransactionTable(); + }); + headerRow.appendChild(th); }); modalThead.appendChild(headerRow); // Build rows - transactions.forEach(transaction => { + sortedTransactions.forEach(transaction => { const row = document.createElement('tr'); - availableColumns.forEach(col => { + + currentColumns.forEach(col => { const td = document.createElement('td'); let value = transaction.originalRow[col]; - // Format amount values - if ((col === 'amount' || col === 'original_amount') && typeof value === 'number') { + if ((col === 'amount_rub' || col === 'amount_original') && typeof value === 'number') { value = value.toLocaleString(); } td.textContent = value !== undefined && value !== null ? value : ''; - td.title = td.textContent; // Show full text on hover + td.title = td.textContent; row.appendChild(td); }); + + // Row click opens detail modal + row.addEventListener('click', () => openRowDetailModal(transaction)); modalTbody.appendChild(row); }); +} + +// Open the row detail modal +function openRowDetailModal(transaction) { + const modal = document.getElementById('row-detail-modal'); + const title = document.getElementById('row-detail-title'); + const body = document.getElementById('row-detail-body'); + + const name = transaction.originalRow?.simple_name || transaction.name || 'Transaction'; + title.textContent = name; + + body.innerHTML = ''; + + currentColumns.forEach(col => { + // Skip simple_name since it's already shown in the title + if (col === 'simple_name') return; + + const value = transaction.originalRow[col]; + + const itemDiv = document.createElement('div'); + itemDiv.className = 'row-detail-item'; + + const labelSpan = document.createElement('span'); + labelSpan.className = 'row-detail-label'; + labelSpan.textContent = csvColumnLabels[col] || col; + + const valueSpan = document.createElement('span'); + valueSpan.className = 'row-detail-value'; + valueSpan.textContent = value; + + itemDiv.appendChild(labelSpan); + itemDiv.appendChild(valueSpan); + body.appendChild(itemDiv); + }); modal.style.display = 'flex'; - setupModalListeners(); + setupRowDetailModalListeners(); +} + +// Close row detail modal +function closeRowDetailModal() { + const modal = document.getElementById('row-detail-modal'); + modal.style.display = 'none'; +} + +// Setup row detail modal listeners +function setupRowDetailModalListeners() { + const modal = document.getElementById('row-detail-modal'); + const closeBtn = document.getElementById('row-detail-close'); + + closeBtn.onclick = (e) => { + e.stopPropagation(); + closeRowDetailModal(); + }; + + modal.onclick = function(e) { + if (e.target === modal) { + closeRowDetailModal(); + } + }; } // Close transaction modal function closeTransactionModal() { const modal = document.getElementById('transaction-modal'); modal.style.display = 'none'; - document.removeEventListener('keydown', handleModalEscape); } -// Handle Escape key to close modal -function handleModalEscape(e) { +// Global escape key handler for all modals +function handleGlobalEscape(e) { if (e.key === 'Escape') { - closeTransactionModal(); + const rowDetailModal = document.getElementById('row-detail-modal'); + const transactionModal = document.getElementById('transaction-modal'); + + // Close row detail first if open, then transaction modal + if (rowDetailModal.style.display !== 'none') { + closeRowDetailModal(); + } else if (transactionModal.style.display !== 'none') { + closeTransactionModal(); + } } } +// Setup global escape listener once +document.addEventListener('keydown', handleGlobalEscape); + // Setup modal close listeners function setupModalListeners() { const modal = document.getElementById('transaction-modal'); const closeBtn = document.getElementById('modal-close'); - // Close on X button click closeBtn.onclick = closeTransactionModal; - // Close on backdrop click modal.onclick = function(e) { if (e.target === modal) { closeTransactionModal(); } }; - - // Close on Escape key - document.addEventListener('keydown', handleModalEscape); } diff --git a/index.html b/index.html index 78e37d1..469047e 100644 --- a/index.html +++ b/index.html @@ -54,6 +54,13 @@ + \ No newline at end of file diff --git a/styles.css b/styles.css index cafedec..4784c39 100644 --- a/styles.css +++ b/styles.css @@ -440,7 +440,7 @@ body { .modal-table-container { flex: 1; - overflow: auto; + overflow: scroll; padding: 0; } @@ -496,3 +496,109 @@ body { } } +/* Row detail modal styles */ +.row-detail-content { + position: relative; + width: 90vw; + max-width: 500px; + max-height: 80vh; + background-color: white; + border-radius: 12px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); + padding: 24px; + overflow-y: auto; +} + +.row-detail-content .modal-close { + position: absolute; + top: 12px; + right: 12px; +} + +.row-detail-content h3 { + margin: 0 0 20px 0; + font-size: 18px; + color: #333; + padding-right: 30px; +} + +#row-detail-body { + display: table; + width: 100%; + border-collapse: collapse; +} + +.row-detail-item { + display: table-row; +} + +.row-detail-item:hover { + background-color: #f8f9fa; +} + +.row-detail-label { + display: table-cell; + padding: 8px 32px 8px 0; + font-size: 13px; + color: #999; + font-weight: 400; + white-space: nowrap; + vertical-align: top; + border-bottom: 1px solid #eee; + min-width: 140px; +} + +.row-detail-value { + display: table-cell; + padding: 8px 0; + font-size: 13px; + color: #222; + font-weight: 600; + word-break: break-word; + border-bottom: 1px solid #eee; +} + +.row-detail-item:last-child .row-detail-label, +.row-detail-item:last-child .row-detail-value { + border-bottom: none; +} + +/* Sortable table header styles */ +.transaction-table th { + cursor: pointer; + user-select: none; + position: relative; + padding-right: 20px !important; +} + +.transaction-table th:hover { + background-color: #f0f0f0; +} + +.transaction-table th::after { + content: ''; + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + border: 4px solid transparent; + opacity: 0.3; +} + +.transaction-table th.sort-asc::after { + border-bottom-color: #333; + border-top: none; + opacity: 1; +} + +.transaction-table th.sort-desc::after { + border-top-color: #333; + border-bottom: none; + opacity: 1; +} + +.transaction-table tbody tr { + cursor: pointer; + transition: background-color 0.15s; +} +