Add basic expense list rendering to frontend
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
const TOKEN_KEY = 'mft_token';
|
const TOKEN_KEY = 'mft_token';
|
||||||
|
|
||||||
|
// Store categories for mapping cid to name
|
||||||
|
let categories = [];
|
||||||
|
|
||||||
// Validate token with the API
|
// Validate token with the API
|
||||||
async function validateToken(token) {
|
async function validateToken(token) {
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +98,8 @@ document.getElementById('expense-form').addEventListener('submit', async (e) =>
|
|||||||
// Reset form
|
// Reset form
|
||||||
document.getElementById('expense-form').reset();
|
document.getElementById('expense-form').reset();
|
||||||
|
|
||||||
// TODO: Update local expense view with the new expense
|
// Add to expense list
|
||||||
|
addExpenseToList(expense);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding expense:', error);
|
console.error('Error adding expense:', error);
|
||||||
alert(`Failed to add expense: ${error.message}`);
|
alert(`Failed to add expense: ${error.message}`);
|
||||||
@@ -111,6 +115,7 @@ async function showApp() {
|
|||||||
document.getElementById('login-section').style.display = 'none';
|
document.getElementById('login-section').style.display = 'none';
|
||||||
document.getElementById('app-section').style.display = 'block';
|
document.getElementById('app-section').style.display = 'block';
|
||||||
await loadCategories();
|
await loadCategories();
|
||||||
|
await loadExpenses();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
@@ -129,7 +134,7 @@ async function loadCategories() {
|
|||||||
throw new Error('Failed to load categories');
|
throw new Error('Failed to load categories');
|
||||||
}
|
}
|
||||||
|
|
||||||
const categories = await response.json();
|
categories = await response.json();
|
||||||
|
|
||||||
// Clear loading option
|
// Clear loading option
|
||||||
categorySelect.innerHTML = '';
|
categorySelect.innerHTML = '';
|
||||||
@@ -152,3 +157,89 @@ async function loadCategories() {
|
|||||||
categorySelect.innerHTML = '<option value="">Error loading categories</option>';
|
categorySelect.innerHTML = '<option value="">Error loading categories</option>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = '<p>Error loading expenses</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="expense-header">
|
||||||
|
<span class="expense-category"></span>
|
||||||
|
<span class="expense-amount"></span>
|
||||||
|
</div>
|
||||||
|
<div class="expense-details">
|
||||||
|
<span class="expense-time"></span>
|
||||||
|
<span class="expense-note"></span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = '<p class="empty-state">No expenses recorded</p>';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="submit">Add Expense</button>
|
<button type="submit">Add Expense</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div id="expense-list-section">
|
||||||
|
<h2>Recent Expenses</h2>
|
||||||
|
<div id="expense-list"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -87,3 +87,62 @@ select:focus {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: #3498db;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user