Split frontend into tabs, add a tab for showing totals

This commit is contained in:
2026-01-09 12:04:43 +01:00
parent 870928e20d
commit ec9b3b56fb
3 changed files with 347 additions and 21 deletions

View File

@@ -243,3 +243,170 @@ function addExpenseToList(expense) {
const card = createExpenseCard(expense); const card = createExpenseCard(expense);
listContainer.insertAdjacentElement('afterbegin', card); listContainer.insertAdjacentElement('afterbegin', card);
} }
// Tab switching
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.dataset.tab;
// Update button states
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
button.classList.add('active');
// Update content visibility
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${targetTab}-tab`).classList.add('active');
// Load statistics when switching to statistics tab
if (targetTab === 'statistics') {
loadWeeklyTotals();
}
});
});
// Weekly totals functionality
async function loadWeeklyTotals() {
const token = localStorage.getItem(TOKEN_KEY);
const container = document.getElementById('weekly-totals');
try {
const response = await fetch('/api/statistics/totals?granularity=weekly', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to load weekly totals');
}
const weeklyData = await response.json();
renderWeeklyTotals(weeklyData);
} catch (error) {
console.error('Error loading weekly totals:', error);
container.innerHTML = '<p class="empty-state">Error loading weekly totals</p>';
}
}
function formatDateRange(fromDate, toDate) {
const from = new Date(fromDate);
const to = new Date(toDate);
const options = { month: 'short', day: 'numeric' };
const fromStr = from.toLocaleDateString('en-US', options);
const toStr = to.toLocaleDateString('en-US', options);
return `${fromStr} - ${toStr}`;
}
function renderWeeklyTotals(weeklyData) {
const container = document.getElementById('weekly-totals');
if (weeklyData.length === 0) {
container.innerHTML = '<p class="empty-state">No expense data available</p>';
return;
}
const table = document.createElement('table');
table.className = 'totals-table';
const tbody = document.createElement('tbody');
// Reverse to show newest weeks first
weeklyData.slice().reverse().forEach(week => {
// Main week row
const weekRow = document.createElement('tr');
weekRow.className = 'week-row';
weekRow.dataset.week = week.week;
const dateCell = document.createElement('td');
dateCell.className = 'week-date';
dateCell.innerHTML = `<span class="expand-icon">▶</span>${formatDateRange(week.from_date, week.to_date)}`;
const totalCell = document.createElement('td');
totalCell.className = 'week-total';
totalCell.textContent = `${(week.total / 100).toFixed(2)}`;
weekRow.appendChild(dateCell);
weekRow.appendChild(totalCell);
// Categories row (hidden by default)
const categoriesRow = document.createElement('tr');
categoriesRow.className = 'categories-row';
categoriesRow.dataset.week = week.week;
const categoriesCell = document.createElement('td');
categoriesCell.colSpan = 2;
const categoriesContent = document.createElement('div');
categoriesContent.className = 'categories-content';
if (week.by_category.length === 0) {
categoriesContent.innerHTML = '<p class="empty-state">No expenses this week</p>';
} else {
week.by_category.forEach(category => {
const categoryItem = document.createElement('div');
categoryItem.className = 'category-item';
const nameSpan = document.createElement('span');
nameSpan.className = 'category-name';
nameSpan.textContent = category.name;
const amountSpan = document.createElement('span');
amountSpan.className = 'category-amount';
amountSpan.textContent = `${(category.total / 100).toFixed(2)}`;
categoryItem.appendChild(nameSpan);
categoryItem.appendChild(amountSpan);
categoriesContent.appendChild(categoryItem);
});
}
categoriesCell.appendChild(categoriesContent);
categoriesRow.appendChild(categoriesCell);
// Add click handler for expansion
weekRow.addEventListener('click', () => {
toggleWeekExpansion(week.week);
});
tbody.appendChild(weekRow);
tbody.appendChild(categoriesRow);
});
table.appendChild(tbody);
container.innerHTML = '';
container.appendChild(table);
}
function toggleWeekExpansion(weekId) {
const weekRow = document.querySelector(`.week-row[data-week="${weekId}"]`);
const categoriesRow = document.querySelector(`.categories-row[data-week="${weekId}"]`);
weekRow.classList.toggle('expanded');
categoriesRow.classList.toggle('visible');
}
// Expand/collapse all buttons
document.getElementById('expand-all-btn').addEventListener('click', () => {
document.querySelectorAll('.week-row').forEach(row => {
row.classList.add('expanded');
});
document.querySelectorAll('.categories-row').forEach(row => {
row.classList.add('visible');
});
});
document.getElementById('collapse-all-btn').addEventListener('click', () => {
document.querySelectorAll('.week-row').forEach(row => {
row.classList.remove('expanded');
});
document.querySelectorAll('.categories-row').forEach(row => {
row.classList.remove('visible');
});
});

View File

@@ -23,31 +23,50 @@
<div id="app-section" style="display: none;"> <div id="app-section" style="display: none;">
<div class="header-bar"> <div class="header-bar">
<h2>Add Expense</h2> <h2>Minimal Finance Tracker</h2>
<button id="logout-btn">Logout</button> <button id="logout-btn">Logout</button>
</div> </div>
<form id="expense-form"> <div class="tabs">
<div class="form-group"> <button class="tab-button active" data-tab="expenses">Expenses</button>
<label for="category">Category</label> <button class="tab-button" data-tab="statistics">Statistics</button>
<select id="category" name="category" required> </div>
<option value="">Loading...</option>
</select>
</div>
<div class="form-group">
<label for="value">Amount</label>
<input type="number" id="value" name="value" step="0.01" min="0" required>
</div>
<div class="form-group">
<label for="note">Note</label>
<input type="text" id="note" name="note">
</div>
<button type="submit">Add Expense</button>
</form>
<div id="expense-list-section"> <div id="expenses-tab" class="tab-content active">
<h2>Recent Expenses</h2> <h2>Add Expense</h2>
<div id="expense-list"></div> <form id="expense-form">
<div class="form-group">
<label for="category">Category</label>
<select id="category" name="category" required>
<option value="">Loading...</option>
</select>
</div>
<div class="form-group">
<label for="value">Amount</label>
<input type="number" id="value" name="value" step="0.01" min="0" required>
</div>
<div class="form-group">
<label for="note">Note</label>
<input type="text" id="note" name="note">
</div>
<button type="submit">Add Expense</button>
</form>
<div id="expense-list-section">
<h2>Recent Expenses</h2>
<div id="expense-list"></div>
</div>
</div>
<div id="statistics-tab" class="tab-content">
<div class="stats-header">
<h2>Weekly Totals</h2>
<div class="stats-controls">
<button id="expand-all-btn" class="secondary-btn">Expand All</button>
<button id="collapse-all-btn" class="secondary-btn">Collapse All</button>
</div>
</div>
<div id="weekly-totals"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -146,3 +146,143 @@ select:focus {
font-style: italic; font-style: italic;
padding: 20px; padding: 20px;
} }
.tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 2px solid #e0e0e0;
}
.tab-button {
background: transparent;
color: #666;
border: none;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
}
.tab-button:hover {
background: transparent;
color: #3498db;
transform: none;
}
.tab-button.active {
color: #3498db;
border-bottom-color: #3498db;
font-weight: 600;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.stats-controls {
display: flex;
gap: 10px;
}
.secondary-btn {
background: #95a5a6;
padding: 8px 16px;
font-size: 14px;
}
.secondary-btn:hover {
background: #7f8c8d;
}
.totals-table {
width: 100%;
border-collapse: collapse;
}
.week-row {
cursor: pointer;
user-select: none;
}
.week-row td {
padding: 15px;
border-bottom: 1px solid #e0e0e0;
}
.week-row:hover {
background: #f8f9fa;
}
.week-date {
font-weight: 500;
color: #2c3e50;
}
.week-total {
text-align: right;
font-size: 1.1em;
font-weight: 600;
color: #e74c3c;
}
.expand-icon {
display: inline-block;
margin-right: 8px;
transition: transform 0.2s;
font-size: 0.8em;
}
.week-row.expanded .expand-icon {
transform: rotate(90deg);
}
.categories-row {
display: none;
background: #f8f9fa;
}
.categories-row.visible {
display: table-row;
}
.categories-row td {
padding: 0;
}
.categories-content {
padding: 10px 15px 15px 40px;
}
.category-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e8e8e8;
}
.category-item:last-child {
border-bottom: none;
}
.category-name {
color: #555;
}
.category-amount {
font-weight: 500;
color: #666;
}