688 lines
24 KiB
HTML
688 lines
24 KiB
HTML
<!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>
|