From be1544d5d5beb75c5bb9ccfc36f732c6c2fcb03a Mon Sep 17 00:00:00 2001 From: omicron Date: Thu, 1 Jan 2026 02:23:50 +0100 Subject: [PATCH] Add basic expense list rendering to frontend --- mft/static/app.js | 95 ++++++++++++++++++++++++++++++++++++++++++- mft/static/index.html | 5 +++ mft/static/style.css | 59 +++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) diff --git a/mft/static/app.js b/mft/static/app.js index 8e950ec..24e2964 100644 --- a/mft/static/app.js +++ b/mft/static/app.js @@ -1,5 +1,8 @@ const TOKEN_KEY = 'mft_token'; +// Store categories for mapping cid to name +let categories = []; + // Validate token with the API async function validateToken(token) { try { @@ -95,7 +98,8 @@ document.getElementById('expense-form').addEventListener('submit', async (e) => // Reset form document.getElementById('expense-form').reset(); - // TODO: Update local expense view with the new expense + // Add to expense list + addExpenseToList(expense); } catch (error) { console.error('Error adding expense:', error); alert(`Failed to add expense: ${error.message}`); @@ -111,6 +115,7 @@ async function showApp() { document.getElementById('login-section').style.display = 'none'; document.getElementById('app-section').style.display = 'block'; await loadCategories(); + await loadExpenses(); } async function loadCategories() { @@ -129,7 +134,7 @@ async function loadCategories() { throw new Error('Failed to load categories'); } - const categories = await response.json(); + categories = await response.json(); // Clear loading option categorySelect.innerHTML = ''; @@ -152,3 +157,89 @@ async function loadCategories() { categorySelect.innerHTML = ''; } } + +async function loadExpenses() { + const token = localStorage.getItem(TOKEN_KEY); + const listContainer = document.getElementById('expense-list'); + + try { + const response = await fetch('/api/expenses', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error('Failed to load expenses'); + } + + const expenses = await response.json(); + renderExpenses(expenses); + } catch (error) { + console.error('Error loading expenses:', error); + listContainer.innerHTML = '

Error loading expenses

'; + } +} + +function createExpenseCard(expense) { + const category = categories.find(c => c.id === expense.cid); + const categoryName = category ? category.name : 'Unknown'; + const amount = (expense.value / 100).toFixed(2); + const timestamp = new Date(expense.ts).toLocaleString(); + + // Create card structure + const item = document.createElement('div'); + item.className = 'expense-item'; + item.innerHTML = ` +
+ + +
+
+ + +
+ `; + + // Populate with textContent + item.querySelector('.expense-category').textContent = categoryName; + item.querySelector('.expense-amount').textContent = `€${amount}`; + item.querySelector('.expense-time').textContent = timestamp; + + if (expense.note) { + item.querySelector('.expense-note').textContent = expense.note; + } else { + item.querySelector('.expense-note').remove(); + } + + return item; +} + +function renderExpenses(expenses) { + const listContainer = document.getElementById('expense-list'); + + if (expenses.length === 0) { + listContainer.innerHTML = '

No expenses recorded

'; + return; + } + + listContainer.innerHTML = ''; + expenses.forEach(expense => { + const card = createExpenseCard(expense); + listContainer.appendChild(card); + }); +} + +function addExpenseToList(expense) { + const listContainer = document.getElementById('expense-list'); + + // Remove empty state if it exists + const emptyState = listContainer.querySelector('.empty-state'); + if (emptyState) { + listContainer.innerHTML = ''; + } + + const card = createExpenseCard(expense); + listContainer.insertAdjacentElement('afterbegin', card); +} diff --git a/mft/static/index.html b/mft/static/index.html index d0fc67b..a985ee2 100644 --- a/mft/static/index.html +++ b/mft/static/index.html @@ -44,6 +44,11 @@ + +
+

Recent Expenses

+
+
diff --git a/mft/static/style.css b/mft/static/style.css index 67e0fe5..8a44590 100644 --- a/mft/static/style.css +++ b/mft/static/style.css @@ -87,3 +87,62 @@ select:focus { outline: none; border-color: #3498db; } + +.header-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +#expense-list-section { + margin-top: 40px; +} + +.expense-item { + padding: 15px; + margin-bottom: 10px; + border: 1px solid #e0e0e0; + border-radius: 4px; + background: #fafafa; +} + +.expense-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.expense-category { + font-weight: 600; + color: #2c3e50; +} + +.expense-amount { + font-size: 1.1em; + font-weight: 600; + color: #e74c3c; +} + +.expense-details { + display: flex; + gap: 15px; + font-size: 0.9em; + color: #666; +} + +.expense-time { + font-style: italic; +} + +.expense-note { + color: #555; +} + +.empty-state { + text-align: center; + color: #999; + font-style: italic; + padding: 20px; +}