Split frontend into tabs, add a tab for showing totals
This commit is contained in:
@@ -243,3 +243,170 @@ function addExpenseToList(expense) {
|
||||
const card = createExpenseCard(expense);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,10 +23,17 @@
|
||||
|
||||
<div id="app-section" style="display: none;">
|
||||
<div class="header-bar">
|
||||
<h2>Add Expense</h2>
|
||||
<h2>Minimal Finance Tracker</h2>
|
||||
<button id="logout-btn">Logout</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-button active" data-tab="expenses">Expenses</button>
|
||||
<button class="tab-button" data-tab="statistics">Statistics</button>
|
||||
</div>
|
||||
|
||||
<div id="expenses-tab" class="tab-content active">
|
||||
<h2>Add Expense</h2>
|
||||
<form id="expense-form">
|
||||
<div class="form-group">
|
||||
<label for="category">Category</label>
|
||||
@@ -50,6 +57,18 @@
|
||||
<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>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
|
||||
@@ -146,3 +146,143 @@ select:focus {
|
||||
font-style: italic;
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user