persona-finance-tracker-demo/frontend.html
2025-09-13 17:40:23 +05:30

688 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personal Finance Tracker</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
padding: 2rem 0;
margin-bottom: 2rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.section {
background: white;
margin-bottom: 2rem;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.section h2 {
color: #667eea;
margin-bottom: 1.5rem;
font-size: 1.8rem;
border-bottom: 3px solid #667eea;
padding-bottom: 0.5rem;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #555;
}
input, select, textarea {
width: 100%;
padding: 0.8rem;
border: 2px solid #e1e5e9;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.8rem 2rem;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
button:active {
transform: translateY(0);
}
.btn-danger {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
}
.btn-danger:hover {
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-row-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #e1e5e9;
}
th {
background-color: #f8f9fa;
font-weight: 600;
color: #555;
}
.category-color {
width: 20px;
height: 20px;
border-radius: 50%;
display: inline-block;
margin-right: 0.5rem;
border: 2px solid #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.amount-income {
color: #28a745;
font-weight: 600;
}
.amount-expense {
color: #dc3545;
font-weight: 600;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
border-left: 4px solid #667eea;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.stat-label {
color: #666;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.error {
background-color: #f8d7da;
color: #721c24;
padding: 1rem;
border-radius: 5px;
margin: 1rem 0;
}
.success {
background-color: #d4edda;
color: #155724;
padding: 1rem;
border-radius: 5px;
margin: 1rem 0;
}
@media (max-width: 768px) {
.form-row, .form-row-3 {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
table {
font-size: 0.9rem;
}
th, td {
padding: 0.5rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>💰 Personal Finance Tracker</h1>
<p class="subtitle">Manage your income, expenses, and financial goals</p>
</header>
<!-- Analytics Summary Section -->
<div class="section">
<h2>📊 Financial Summary</h2>
<div id="analytics-loading" class="loading">Loading financial data...</div>
<div id="analytics-content" style="display: none;">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value amount-income" id="total-income">$0.00</div>
<div class="stat-label">Total Income</div>
</div>
<div class="stat-card">
<div class="stat-value amount-expense" id="total-expenses">$0.00</div>
<div class="stat-label">Total Expenses</div>
</div>
<div class="stat-card">
<div class="stat-value" id="net-balance">$0.00</div>
<div class="stat-label">Net Balance</div>
</div>
<div class="stat-card">
<div class="stat-value" id="transaction-count">0</div>
<div class="stat-label">Transactions</div>
</div>
</div>
<button onclick="loadAnalytics()">🔄 Refresh Analytics</button>
</div>
</div>
<!-- Categories Section -->
<div class="section">
<h2>🏷️ Categories</h2>
<!-- Add Category Form -->
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem;">
<h3 style="margin-bottom: 1rem; color: #667eea;">Add New Category</h3>
<form id="category-form">
<div class="form-row-3">
<div class="form-group">
<label for="category-name">Category Name</label>
<input type="text" id="category-name" required placeholder="e.g., Food, Transportation">
</div>
<div class="form-group">
<label for="category-description">Description</label>
<input type="text" id="category-description" placeholder="Optional description">
</div>
<div class="form-group">
<label for="category-color">Color</label>
<input type="color" id="category-color" value="#667eea">
</div>
</div>
<button type="submit"> Add Category</button>
</form>
</div>
<!-- Categories List -->
<div id="categories-loading" class="loading">Loading categories...</div>
<div id="categories-content" style="display: none;">
<table id="categories-table">
<thead>
<tr>
<th>Color</th>
<th>Name</th>
<th>Description</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="categories-tbody">
</tbody>
</table>
</div>
</div>
<!-- Transactions Section -->
<div class="section">
<h2>💳 Transactions</h2>
<!-- Add Transaction Form -->
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem;">
<h3 style="margin-bottom: 1rem; color: #667eea;">Add New Transaction</h3>
<form id="transaction-form">
<div class="form-row">
<div class="form-group">
<label for="transaction-description">Description</label>
<input type="text" id="transaction-description" required placeholder="e.g., Grocery shopping">
</div>
<div class="form-group">
<label for="transaction-amount">Amount</label>
<input type="number" id="transaction-amount" step="0.01" min="0.01" required placeholder="0.00">
</div>
</div>
<div class="form-row-3">
<div class="form-group">
<label for="transaction-type">Type</label>
<select id="transaction-type" required>
<option value="">Select type</option>
<option value="income">💰 Income</option>
<option value="expense">💸 Expense</option>
</select>
</div>
<div class="form-group">
<label for="transaction-category">Category</label>
<select id="transaction-category">
<option value="">No category</option>
</select>
</div>
<div class="form-group">
<label for="transaction-date">Date</label>
<input type="date" id="transaction-date" required>
</div>
</div>
<div class="form-group">
<label for="transaction-notes">Notes (Optional)</label>
<textarea id="transaction-notes" rows="2" placeholder="Additional notes..."></textarea>
</div>
<button type="submit"> Add Transaction</button>
</form>
</div>
<!-- Transactions List -->
<div id="transactions-loading" class="loading">Loading transactions...</div>
<div id="transactions-content" style="display: none;">
<div style="margin-bottom: 1rem;">
<button onclick="loadTransactions()">🔄 Refresh Transactions</button>
</div>
<table id="transactions-table">
<thead>
<tr>
<th>Date</th>
<th>Description</th>
<th>Category</th>
<th>Amount</th>
<th>Type</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="transactions-tbody">
</tbody>
</table>
</div>
</div>
</div>
<script>
const API_BASE = 'http://127.0.0.1:5000/api/v1';
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
// Set today's date as default
document.getElementById('transaction-date').value = new Date().toISOString().split('T')[0];
// Load initial data
loadCategories();
loadTransactions();
loadAnalytics();
// Set up form handlers
setupFormHandlers();
});
function setupFormHandlers() {
// Category form handler
document.getElementById('category-form').addEventListener('submit', async function(e) {
e.preventDefault();
await addCategory();
});
// Transaction form handler
document.getElementById('transaction-form').addEventListener('submit', async function(e) {
e.preventDefault();
await addTransaction();
});
}
async function loadCategories() {
try {
const response = await fetch(`${API_BASE}/categories/`);
const data = await response.json();
if (data.success) {
displayCategories(data.data);
updateCategoryDropdown(data.data);
} else {
showError('Failed to load categories');
}
} catch (error) {
showError('Error loading categories: ' + error.message);
}
}
function displayCategories(categories) {
const tbody = document.getElementById('categories-tbody');
tbody.innerHTML = '';
categories.forEach(category => {
const row = document.createElement('tr');
row.innerHTML = `
<td><span class="category-color" style="background-color: ${category.color || '#667eea'}"></span></td>
<td>${category.name}</td>
<td>${category.description || '-'}</td>
<td>${new Date(category.created_at).toLocaleDateString()}</td>
<td>
<button class="btn-danger" onclick="deleteCategory(${category.id})" style="padding: 0.3rem 0.8rem; font-size: 0.8rem;">Delete</button>
</td>
`;
tbody.appendChild(row);
});
document.getElementById('categories-loading').style.display = 'none';
document.getElementById('categories-content').style.display = 'block';
}
function updateCategoryDropdown(categories) {
const select = document.getElementById('transaction-category');
select.innerHTML = '<option value="">No category</option>';
categories.forEach(category => {
const option = document.createElement('option');
option.value = category.id;
option.textContent = category.name;
select.appendChild(option);
});
}
async function addCategory() {
const name = document.getElementById('category-name').value;
const description = document.getElementById('category-description').value;
const color = document.getElementById('category-color').value;
try {
const response = await fetch(`${API_BASE}/categories/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description || null,
color: color
})
});
const data = await response.json();
if (data.success) {
showSuccess('Category added successfully!');
document.getElementById('category-form').reset();
document.getElementById('category-color').value = '#667eea';
loadCategories();
} else {
showError(data.message || 'Failed to add category');
}
} catch (error) {
showError('Error adding category: ' + error.message);
}
}
async function deleteCategory(id) {
if (!confirm('Are you sure you want to delete this category?')) return;
try {
const response = await fetch(`${API_BASE}/categories/${id}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showSuccess('Category deleted successfully!');
loadCategories();
} else {
showError(data.message || 'Failed to delete category');
}
} catch (error) {
showError('Error deleting category: ' + error.message);
}
}
async function loadTransactions() {
try {
const response = await fetch(`${API_BASE}/transactions/`);
const data = await response.json();
if (data.success) {
displayTransactions(data.data);
} else {
showError('Failed to load transactions');
}
} catch (error) {
showError('Error loading transactions: ' + error.message);
}
}
function displayTransactions(transactions) {
const tbody = document.getElementById('transactions-tbody');
tbody.innerHTML = '';
transactions.forEach(transaction => {
const row = document.createElement('tr');
const amountClass = transaction.type === 'income' ? 'amount-income' : 'amount-expense';
const typeIcon = transaction.type === 'income' ? '💰' : '💸';
row.innerHTML = `
<td>${new Date(transaction.transaction_date).toLocaleDateString()}</td>
<td>${transaction.description}</td>
<td>${transaction.category_name || '-'}</td>
<td class="${amountClass}">$${parseFloat(transaction.amount).toFixed(2)}</td>
<td>${typeIcon} ${transaction.type}</td>
<td>
<button class="btn-danger" onclick="deleteTransaction(${transaction.id})" style="padding: 0.3rem 0.8rem; font-size: 0.8rem;">Delete</button>
</td>
`;
tbody.appendChild(row);
});
document.getElementById('transactions-loading').style.display = 'none';
document.getElementById('transactions-content').style.display = 'block';
}
async function addTransaction() {
const description = document.getElementById('transaction-description').value;
const amount = parseFloat(document.getElementById('transaction-amount').value);
const type = document.getElementById('transaction-type').value;
const categoryId = document.getElementById('transaction-category').value;
const date = document.getElementById('transaction-date').value;
const notes = document.getElementById('transaction-notes').value;
try {
const payload = {
description: description,
amount: amount,
type: type,
transaction_date: date,
notes: notes || null
};
if (categoryId) {
payload.category_id = parseInt(categoryId);
}
const response = await fetch(`${API_BASE}/transactions/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.success) {
showSuccess('Transaction added successfully!');
document.getElementById('transaction-form').reset();
document.getElementById('transaction-date').value = new Date().toISOString().split('T')[0];
loadTransactions();
loadAnalytics();
} else {
showError(data.message || 'Failed to add transaction');
}
} catch (error) {
showError('Error adding transaction: ' + error.message);
}
}
async function deleteTransaction(id) {
if (!confirm('Are you sure you want to delete this transaction?')) return;
try {
const response = await fetch(`${API_BASE}/transactions/${id}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showSuccess('Transaction deleted successfully!');
loadTransactions();
loadAnalytics();
} else {
showError(data.message || 'Failed to delete transaction');
}
} catch (error) {
showError('Error deleting transaction: ' + error.message);
}
}
async function loadAnalytics() {
try {
const response = await fetch(`${API_BASE}/analytics/summary`);
const data = await response.json();
if (data.success) {
displayAnalytics(data.data);
} else {
showError('Failed to load analytics');
}
} catch (error) {
showError('Error loading analytics: ' + error.message);
}
}
function displayAnalytics(analytics) {
document.getElementById('total-income').textContent = `$${parseFloat(analytics.total_income).toFixed(2)}`;
document.getElementById('total-expenses').textContent = `$${parseFloat(analytics.total_expenses).toFixed(2)}`;
document.getElementById('transaction-count').textContent = analytics.transaction_count;
const netBalance = parseFloat(analytics.net_balance);
const balanceElement = document.getElementById('net-balance');
balanceElement.textContent = `$${Math.abs(netBalance).toFixed(2)}`;
if (netBalance >= 0) {
balanceElement.className = 'stat-value amount-income';
} else {
balanceElement.className = 'stat-value amount-expense';
balanceElement.textContent = `-$${Math.abs(netBalance).toFixed(2)}`;
}
document.getElementById('analytics-loading').style.display = 'none';
document.getElementById('analytics-content').style.display = 'block';
}
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = message;
document.body.insertBefore(errorDiv, document.body.firstChild);
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
function showSuccess(message) {
const successDiv = document.createElement('div');
successDiv.className = 'success';
successDiv.textContent = message;
document.body.insertBefore(successDiv, document.body.firstChild);
setTimeout(() => {
successDiv.remove();
}, 3000);
}
</script>
</body>
</html>