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 @@
+
+
+
+
Transaction Details
+
+
+