Compare commits

...

3 Commits

Author SHA1 Message Date
kris
4fb0d35daf Rebrand from ETF Trade Tracker to Personal Finance Tracker with growth charts
Update application branding to reflect broader financial tracking capabilities beyond just ETF trades. Add comprehensive growth visualization features with time period selectors and three distinct chart views for portfolio, cash, and total net worth tracking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-23 13:58:43 +00:00
kris
8f4899cf8b Add dashboard top 5 accounts display and form cancel/add functionality
Features added:
- Dashboard now shows top 5 holdings, accounts, and subscriptions under each segment
- Quick action cards positioned underneath data cards for easy access
- Cancel/Add buttons added to all forms (trade, cash account, subscription)
- Auto-focus functionality when navigating from quick actions
- Responsive design optimized for mobile and desktop
- Consistent styling following existing design patterns

UI improvements:
- Card groups structure for better organization
- Top accounts sections with hover effects and proper formatting
- Form actions container with proper button alignment
- Mobile-responsive quick action cards and forms

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 12:11:20 +00:00
kris
8a3631d5f5 Add subscription management system with website URL functionality
- Complete subscription tracking with monthly/annual billing cycles
- Dashboard integration with simplified card-based layout
- Website URL fields with clickable links for easy service access
- Comprehensive form validation and error handling
- Database schema with proper website_url column support
- Responsive design with mobile-friendly interface
- Export functionality for subscription data management
- Real-time dashboard summaries showing total costs and service counts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 09:29:34 +00:00
4 changed files with 3387 additions and 559 deletions

View File

@ -3,14 +3,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ETF Trade Tracker</title> <title>Personal Finance Tracker</title>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
<nav class="sidebar"> <nav class="sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<h2>ETF Tracker</h2> <h2>Finance Tracker</h2>
</div> </div>
<ul class="sidebar-menu"> <ul class="sidebar-menu">
<li class="menu-item active" data-page="dashboard"> <li class="menu-item active" data-page="dashboard">
@ -41,6 +41,14 @@
<span class="menu-icon">📋</span> <span class="menu-icon">📋</span>
<span class="menu-text">Trade History</span> <span class="menu-text">Trade History</span>
</li> </li>
<li class="menu-item" data-page="subscriptions">
<span class="menu-icon">📱</span>
<span class="menu-text">Subscriptions</span>
</li>
<li class="menu-item" data-page="add-subscription">
<span class="menu-icon"></span>
<span class="menu-text">Add Subscription</span>
</li>
<li class="menu-item menu-separator" data-page="cgt-settings"> <li class="menu-item menu-separator" data-page="cgt-settings">
<span class="menu-icon">🧮</span> <span class="menu-icon">🧮</span>
<span class="menu-text">CGT Settings</span> <span class="menu-text">CGT Settings</span>
@ -72,7 +80,7 @@
<!-- Login Page --> <!-- Login Page -->
<div id="login-page" class="page active"> <div id="login-page" class="page active">
<div class="login-container"> <div class="login-container">
<h2>Login to ETF Tracker</h2> <h2>Login to Personal Finance Tracker</h2>
<form id="login-form" class="login-form"> <form id="login-form" class="login-form">
<div class="form-group"> <div class="form-group">
<label for="login-username">Username</label> <label for="login-username">Username</label>
@ -94,99 +102,142 @@
<!-- Dashboard Page --> <!-- Dashboard Page -->
<div id="dashboard-page" class="page"> <div id="dashboard-page" class="page">
<div class="dashboard-grid"> <div class="dashboard-summary">
<div class="dashboard-card total-value"> <div class="card-group">
<h3>Current Portfolio Value</h3> <div class="summary-card portfolio-summary">
<div class="metric-value" id="dashboard-current-value">€0.00</div> <div class="summary-header">
<div class="metric-detail" id="dashboard-last-updated">Using cost basis</div> <h3>Portfolio</h3>
</div>
<div class="dashboard-card total-gains">
<h3>Total Gains/Losses</h3>
<div class="metric-value" id="dashboard-total-gains">€0.00</div>
<div class="metric-change" id="dashboard-gains-percentage">0.0%</div>
</div>
<div class="dashboard-card monthly-investment">
<h3>Monthly Investment</h3>
<div class="metric-value" id="monthly-investment">€0.00</div>
<div class="metric-detail" id="monthly-trades">0 trades</div>
</div>
<div class="dashboard-card yearly-investment">
<h3>Yearly Investment</h3>
<div class="metric-value" id="yearly-investment">€0.00</div>
<div class="metric-detail" id="yearly-trades">0 trades</div>
</div>
<div class="dashboard-card total-shares">
<h3>Total Shares</h3>
<div class="metric-value" id="total-shares">0</div>
<div class="metric-detail" id="unique-etfs">0 ETFs</div>
</div>
<div class="dashboard-card cost-basis">
<h3>Total Investment</h3>
<div class="metric-value" id="dashboard-cost-basis">€0.00</div>
<div class="metric-detail" id="dashboard-avg-return">Avg return: 0.0%</div>
</div>
</div>
<!-- Total Holdings Card -->
<div class="total-holdings-section">
<div class="total-holdings-card">
<h3>Total Holdings</h3>
<div class="holdings-breakdown">
<div class="holdings-item">
<span class="holdings-label">Portfolio Value:</span>
<span class="holdings-value" id="total-holdings-portfolio">€0.00</span>
</div> </div>
<div class="holdings-item"> <div class="summary-content">
<span class="holdings-label">Cash Savings:</span> <div class="primary-value" id="dashboard-current-value">€0.00</div>
<span class="holdings-value" id="total-holdings-cash">€0.00</span> <div class="secondary-value" id="dashboard-total-gains">€0.00</div>
</div> </div>
<div class="holdings-divider"></div> <div class="top-accounts-section">
<div class="holdings-total"> <div class="top-accounts-title">Top Holdings</div>
<span class="holdings-label">Total Value:</span> <div id="dashboard-top-portfolio" class="top-accounts-list">
<span class="holdings-value total" id="total-holdings-combined">€0.00</span> <div class="no-data-small">No positions yet</div>
</div>
</div>
</div>
<div class="quick-action-card portfolio-action" onclick="app.navigateToPage('add-trade')">
<div class="quick-action-icon"></div>
<div class="quick-action-text">Add Trade</div>
</div>
</div>
<div class="card-group">
<div class="summary-card cash-summary">
<div class="summary-header">
<h3>Cash</h3>
</div>
<div class="summary-content">
<div class="primary-value" id="dashboard-cash-total">€0.00</div>
<div class="secondary-value" id="dashboard-account-count">0 accounts</div>
</div>
<div class="top-accounts-section">
<div class="top-accounts-title">Top Accounts</div>
<div id="dashboard-top-cash" class="top-accounts-list">
<div class="no-data-small">No accounts yet</div>
</div>
</div>
</div>
<div class="quick-action-card cash-action" onclick="app.navigateToPage('cash-accounts')">
<div class="quick-action-icon">💰</div>
<div class="quick-action-text">Add Account</div>
</div>
</div>
<div class="card-group">
<div class="summary-card subscriptions-summary">
<div class="summary-header">
<h3>Subscriptions</h3>
</div>
<div class="summary-content">
<div class="primary-value" id="dashboard-subscription-monthly">€0.00/mo</div>
<div class="secondary-value" id="dashboard-subscription-count">0 services</div>
</div>
<div class="top-accounts-section">
<div class="top-accounts-title">Top Services</div>
<div id="dashboard-top-subscriptions" class="top-accounts-list">
<div class="no-data-small">No subscriptions yet</div>
</div>
</div>
</div>
<div class="quick-action-card subscription-action" onclick="app.navigateToPage('add-subscription')">
<div class="quick-action-icon">📱</div>
<div class="quick-action-text">Add Service</div>
</div>
</div>
<div class="card-group">
<div class="summary-card total-summary">
<div class="summary-header">
<h3>Total Value</h3>
</div>
<div class="summary-content">
<div class="primary-value total" id="dashboard-total-combined">€0.00</div>
<div class="secondary-value" id="dashboard-performance">0.0%</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="cash-breakdown" id="cash-breakdown" style="display: none;"> <!-- Growth Graphs Section -->
<h3>Cash Savings Summary</h3> <div class="growth-graphs-section">
<div class="cash-summary-overview"> <div class="graphs-header">
<div class="cash-total-card"> <h2>Growth & Trends</h2>
<div class="cash-amount-line" id="dashboard-cash-eur-line"> <div class="time-period-selector">
<span class="currency-label">EUR:</span> <button class="period-btn active" data-period="7d">7D</button>
<span class="amount" id="dashboard-cash-eur">€0.00</span> <button class="period-btn" data-period="1m">1M</button>
</div> <button class="period-btn" data-period="3m">3M</button>
<div class="cash-amount-line" id="dashboard-cash-usd-line" style="display: none;"> <button class="period-btn" data-period="6m">6M</button>
<span class="currency-label">USD:</span> <button class="period-btn" data-period="1y">1Y</button>
<span class="amount" id="dashboard-cash-usd">$0.00</span>
</div>
</div>
<div class="cash-stats-card">
<span class="stat-detail" id="dashboard-account-count">0 accounts</span>
<span class="stat-detail" id="dashboard-avg-interest-display" style="display: none;">Avg: 0.0%</span>
</div> </div>
</div> </div>
<div id="cash-accounts-breakdown" class="breakdown-list">
<div id="dashboard-cash-accounts-list"> <div class="graphs-grid">
<p class="no-data">No cash accounts yet</p> <div class="graph-card portfolio-graph">
<div class="graph-header">
<h3>Portfolio Value</h3>
<div class="graph-stats">
<span class="current-value" id="portfolio-graph-value">€0.00</span>
<span class="change-indicator" id="portfolio-graph-change">+0.0%</span>
</div>
</div>
<div class="graph-container">
<canvas id="portfolio-chart" class="chart-canvas"></canvas>
</div>
</div>
<div class="graph-card cash-graph">
<div class="graph-header">
<h3>Cash Holdings</h3>
<div class="graph-stats">
<span class="current-value" id="cash-graph-value">€0.00</span>
<span class="change-indicator" id="cash-graph-change">+0.0%</span>
</div>
</div>
<div class="graph-container">
<canvas id="cash-chart" class="chart-canvas"></canvas>
</div>
</div>
<div class="graph-card combined-graph">
<div class="graph-header">
<h3>Total Net Worth</h3>
<div class="graph-stats">
<span class="current-value" id="total-graph-value">€0.00</span>
<span class="change-indicator" id="total-graph-change">+0.0%</span>
</div>
</div>
<div class="graph-container">
<canvas id="total-chart" class="chart-canvas"></canvas>
</div>
</div> </div>
</div>
</div>
<div class="etf-breakdown">
<h3>ETF Breakdown</h3>
<div id="etf-breakdown-list" class="breakdown-list">
<p class="no-data">No ETF positions yet</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Add Trade Page --> <!-- Add Trade Page -->
<div id="add-trade-page" class="page"> <div id="add-trade-page" class="page">
<div class="trade-form-container"> <div class="trade-form-container">
@ -245,8 +296,11 @@
<label for="notes">Notes (optional)</label> <label for="notes">Notes (optional)</label>
<textarea id="notes" rows="3" placeholder="Additional notes about this trade..."></textarea> <textarea id="notes" rows="3" placeholder="Additional notes about this trade..."></textarea>
</div> </div>
<button type="submit" class="submit-btn">Add Trade</button> <div class="form-actions">
<button type="button" class="cancel-btn" onclick="app.navigateToPage('dashboard')">Cancel</button>
<button type="submit" class="submit-btn">Add Trade</button>
</div>
</form> </form>
</div> </div>
</div> </div>
@ -469,7 +523,10 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \<br>
<textarea id="account-notes" placeholder="Additional notes about this account" rows="2"></textarea> <textarea id="account-notes" placeholder="Additional notes about this account" rows="2"></textarea>
</div> </div>
</div> </div>
<button type="submit" class="create-account-btn">Add Account</button> <div class="form-actions">
<button type="button" class="cancel-btn" onclick="app.navigateToPage('dashboard')">Cancel</button>
<button type="submit" class="create-account-btn">Add Account</button>
</div>
</form> </form>
</div> </div>
@ -863,6 +920,235 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \<br>
</div> </div>
</div> </div>
</div> </div>
<!-- Subscriptions Page -->
<div id="subscriptions-page" class="page">
<div class="subscriptions-container">
<h2>Subscription Management</h2>
<div class="subscription-intro">
<p>Track and manage your recurring subscriptions. Monitor monthly and annual spending across different services.</p>
</div>
<div class="subscription-summary-cards">
<div class="subscription-summary-card total-monthly">
<h3>Monthly Total</h3>
<div class="subscription-amounts">
<div class="amount-line">
<span class="amount" id="monthly-total">€0.00</span>
</div>
</div>
</div>
<div class="subscription-summary-card total-annual">
<h3>Annual Total</h3>
<div class="subscription-amounts">
<div class="amount-line">
<span class="amount" id="annual-total">€0.00</span>
</div>
</div>
</div>
<div class="subscription-summary-card active-count">
<h3>Active Subscriptions</h3>
<div class="subscription-amounts">
<div class="amount-line">
<span class="amount" id="total-subscriptions">0</span>
</div>
</div>
</div>
</div>
<div class="subscriptions-controls">
<button onclick="app.showPage('add-subscription')" class="add-subscription-btn">Add New Subscription</button>
<button onclick="app.exportSubscriptions()" class="export-btn">Export Subscriptions</button>
</div>
<div class="subscriptions-list-section">
<h3>Your Subscriptions</h3>
<div id="subscriptions-list" class="subscriptions-list">
<p class="no-data">Loading subscriptions...</p>
</div>
</div>
</div>
<!-- Edit Subscription Modal -->
<div id="edit-subscription-modal" class="modal" style="display: none;">
<div class="modal-content">
<h3>Edit Subscription</h3>
<form id="edit-subscription-form">
<div class="form-row">
<div class="form-group">
<label for="edit-subscription-service-name">Service Name *</label>
<input type="text" id="edit-subscription-service-name" name="serviceName" required>
</div>
<div class="form-group">
<label for="edit-subscription-category">Category</label>
<select id="edit-subscription-category" name="category">
<option value="streaming">Streaming</option>
<option value="software">Software</option>
<option value="news">News & Media</option>
<option value="gaming">Gaming</option>
<option value="fitness">Fitness</option>
<option value="productivity">Productivity</option>
<option value="storage">Cloud Storage</option>
<option value="other">Other</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="edit-subscription-billing-cycle">Billing Cycle *</label>
<select id="edit-subscription-billing-cycle" name="billingCycle" required>
<option value="monthly">Monthly</option>
<option value="annual">Annual</option>
</select>
</div>
<div class="form-group">
<label for="edit-subscription-currency">Currency</label>
<select id="edit-subscription-currency" name="currency">
<option value="EUR">EUR (€)</option>
<option value="USD">USD ($)</option>
<option value="GBP">GBP (£)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="edit-subscription-monthly-price">Monthly Price</label>
<input type="number" id="edit-subscription-monthly-price" name="monthlyPrice" step="0.01" min="0">
</div>
<div class="form-group">
<label for="edit-subscription-annual-price">Annual Price</label>
<input type="number" id="edit-subscription-annual-price" name="annualPrice" step="0.01" min="0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="edit-subscription-start-date">Start Date *</label>
<input type="date" id="edit-subscription-start-date" name="startDate" required>
</div>
<div class="form-group">
<label for="edit-subscription-end-date">End Date (optional)</label>
<input type="date" id="edit-subscription-end-date" name="endDate">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="edit-subscription-free-trial">Free Trial Days</label>
<input type="number" id="edit-subscription-free-trial" name="freeTrialDays" min="0" value="0">
</div>
</div>
<div class="form-group">
<label for="edit-subscription-website-url">Website URL (optional)</label>
<input type="url" id="edit-subscription-website-url" name="websiteUrl" placeholder="https://example.com">
</div>
<div class="form-group">
<label for="edit-subscription-notes">Notes (optional)</label>
<textarea id="edit-subscription-notes" name="notes" rows="3"></textarea>
</div>
<div class="modal-actions">
<button type="submit" class="save-btn">Save Changes</button>
<button type="button" class="cancel-btn" onclick="app.closeEditSubscriptionModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<!-- Add Subscription Page -->
<div id="add-subscription-page" class="page">
<div class="add-subscription-container">
<h2>Add New Subscription</h2>
<form id="subscription-form" class="subscription-form">
<div class="form-row">
<div class="form-group">
<label for="subscription-service-name">Service Name *</label>
<input type="text" id="subscription-service-name" name="serviceName" required>
</div>
<div class="form-group">
<label for="subscription-category">Category</label>
<select id="subscription-category" name="category">
<option value="streaming">Streaming</option>
<option value="software">Software</option>
<option value="news">News & Media</option>
<option value="gaming">Gaming</option>
<option value="fitness">Fitness</option>
<option value="productivity">Productivity</option>
<option value="storage">Cloud Storage</option>
<option value="other">Other</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="subscription-billing-cycle">Billing Cycle *</label>
<select id="subscription-billing-cycle" name="billingCycle" required>
<option value="monthly">Monthly</option>
<option value="annual">Annual</option>
</select>
</div>
<div class="form-group">
<label for="subscription-currency">Currency</label>
<select id="subscription-currency" name="currency">
<option value="EUR">EUR (€)</option>
<option value="USD">USD ($)</option>
<option value="GBP">GBP (£)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="subscription-monthly-price">Monthly Price</label>
<input type="number" id="subscription-monthly-price" name="monthlyPrice" step="0.01" min="0">
</div>
<div class="form-group">
<label for="subscription-annual-price">Annual Price</label>
<input type="number" id="subscription-annual-price" name="annualPrice" step="0.01" min="0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="subscription-start-date">Start Date *</label>
<input type="date" id="subscription-start-date" name="startDate" required>
</div>
<div class="form-group">
<label for="subscription-end-date">End Date (optional)</label>
<input type="date" id="subscription-end-date" name="endDate">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="subscription-free-trial">Free Trial Days</label>
<input type="number" id="subscription-free-trial" name="freeTrialDays" min="0" value="0">
</div>
</div>
<div class="form-group">
<label for="subscription-website-url">Website URL (optional)</label>
<input type="url" id="subscription-website-url" name="websiteUrl" placeholder="https://example.com">
</div>
<div class="form-group">
<label for="subscription-notes">Notes (optional)</label>
<textarea id="subscription-notes" name="notes" rows="3" placeholder="Additional notes about this subscription..."></textarea>
</div>
<div class="form-actions">
<button type="button" class="cancel-btn" onclick="app.navigateToPage('dashboard')">Cancel</button>
<button type="submit" class="submit-btn">Add Subscription</button>
</div>
</form>
</div>
</div>
</main> </main>
</div> </div>
</div> </div>

1009
script.js

File diff suppressed because it is too large Load Diff

315
server.js
View File

@ -235,7 +235,7 @@ function initializeDatabase() {
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) )
`; `;
db.run(createPriceHistoryTableSQL, (err) => { db.run(createPriceHistoryTableSQL, (err) => {
if (err) { if (err) {
console.error('Error creating price history table:', err.message); console.error('Error creating price history table:', err.message);
@ -243,6 +243,47 @@ function initializeDatabase() {
console.log('Price history table ready'); console.log('Price history table ready');
} }
}); });
// Create subscriptions table
const createSubscriptionsTableSQL = `
CREATE TABLE IF NOT EXISTS subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
service_name TEXT NOT NULL,
monthly_price REAL CHECK (monthly_price > 0),
annual_price REAL CHECK (annual_price > 0),
billing_cycle TEXT NOT NULL CHECK (billing_cycle IN ('monthly', 'annual')),
currency TEXT NOT NULL DEFAULT 'EUR' CHECK (currency IN ('EUR', 'USD', 'GBP')),
start_date DATE NOT NULL,
end_date DATE,
free_trial_days INTEGER DEFAULT 0 CHECK (free_trial_days >= 0),
category TEXT DEFAULT 'other',
notes TEXT,
website_url TEXT,
is_active BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
CHECK (monthly_price IS NOT NULL OR annual_price IS NOT NULL)
)
`;
db.run(createSubscriptionsTableSQL, (err) => {
if (err) {
console.error('Error creating subscriptions table:', err.message);
} else {
console.log('Subscriptions table ready');
// Add website_url column if it doesn't exist (for existing databases)
db.run(`ALTER TABLE subscriptions ADD COLUMN website_url TEXT`, (alterErr) => {
if (alterErr && !alterErr.message.includes('duplicate column name')) {
console.error('Error adding website_url column:', alterErr.message);
} else if (!alterErr) {
console.log('Added website_url column to subscriptions table');
}
});
}
});
} }
async function createDefaultAdmin() { async function createDefaultAdmin() {
@ -1321,6 +1362,278 @@ app.get('/api/latest-prices', requireAuthOrToken, (req, res) => {
}); });
}); });
// Subscriptions Management endpoints
app.get('/api/subscriptions', requireAuthOrToken, (req, res) => {
const sql = `
SELECT id, service_name, monthly_price, annual_price, billing_cycle, currency,
start_date, end_date, free_trial_days, category, notes, website_url, is_active,
created_at, updated_at
FROM subscriptions
WHERE user_id = ? AND is_active = 1
ORDER BY service_name ASC
`;
db.all(sql, [req.session.userId], (err, rows) => {
if (err) {
console.error('Error fetching subscriptions:', err.message);
res.status(500).json({ error: 'Failed to fetch subscriptions' });
return;
}
const subscriptions = rows.map(row => ({
id: row.id,
serviceName: row.service_name,
monthlyPrice: row.monthly_price,
annualPrice: row.annual_price,
billingCycle: row.billing_cycle,
currency: row.currency,
startDate: row.start_date,
endDate: row.end_date,
freeTrialDays: row.free_trial_days,
category: row.category,
notes: row.notes,
website_url: row.website_url,
isActive: row.is_active === 1,
createdAt: row.created_at,
updatedAt: row.updated_at
}));
res.json(subscriptions);
});
});
app.post('/api/subscriptions', requireAuthOrToken, (req, res) => {
const {
serviceName,
monthlyPrice,
annualPrice,
billingCycle,
currency = 'EUR',
startDate,
endDate,
freeTrialDays = 0,
category = 'other',
notes = '',
websiteUrl = ''
} = req.body;
if (!serviceName || !billingCycle || !startDate) {
return res.status(400).json({ error: 'Service name, billing cycle, and start date are required' });
}
if (!['monthly', 'annual'].includes(billingCycle)) {
return res.status(400).json({ error: 'Billing cycle must be monthly or annual' });
}
if (!['EUR', 'USD', 'GBP'].includes(currency)) {
return res.status(400).json({ error: 'Currency must be EUR, USD, or GBP' });
}
if (!monthlyPrice && !annualPrice) {
return res.status(400).json({ error: 'Either monthly price or annual price must be provided' });
}
if (monthlyPrice && monthlyPrice <= 0) {
return res.status(400).json({ error: 'Monthly price must be greater than 0' });
}
if (annualPrice && annualPrice <= 0) {
return res.status(400).json({ error: 'Annual price must be greater than 0' });
}
if (freeTrialDays < 0) {
return res.status(400).json({ error: 'Free trial days cannot be negative' });
}
const sql = `
INSERT INTO subscriptions (
user_id, service_name, monthly_price, annual_price, billing_cycle, currency,
start_date, end_date, free_trial_days, category, notes, website_url
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
db.run(sql, [
req.session.userId,
serviceName.trim(),
monthlyPrice || null,
annualPrice || null,
billingCycle,
currency,
startDate,
endDate || null,
freeTrialDays,
category.trim(),
notes.trim(),
websiteUrl ? websiteUrl.trim() : null
], function(err) {
if (err) {
console.error('Error creating subscription:', err.message);
res.status(500).json({ error: 'Failed to create subscription' });
return;
}
res.status(201).json({
id: this.lastID,
serviceName: serviceName.trim(),
monthlyPrice,
annualPrice,
billingCycle,
currency,
startDate,
endDate,
freeTrialDays,
category: category.trim(),
notes: notes.trim(),
message: 'Subscription created successfully'
});
});
});
app.put('/api/subscriptions/:id', requireAuthOrToken, (req, res) => {
const subscriptionId = req.params.id;
const {
serviceName,
monthlyPrice,
annualPrice,
billingCycle,
currency,
startDate,
endDate,
freeTrialDays = 0,
category = 'other',
notes = '',
websiteUrl = ''
} = req.body;
if (!subscriptionId || isNaN(subscriptionId)) {
return res.status(400).json({ error: 'Invalid subscription ID' });
}
if (!serviceName || !billingCycle || !startDate) {
return res.status(400).json({ error: 'Service name, billing cycle, and start date are required' });
}
if (!['monthly', 'annual'].includes(billingCycle)) {
return res.status(400).json({ error: 'Billing cycle must be monthly or annual' });
}
if (!['EUR', 'USD', 'GBP'].includes(currency)) {
return res.status(400).json({ error: 'Currency must be EUR, USD, or GBP' });
}
if (!monthlyPrice && !annualPrice) {
return res.status(400).json({ error: 'Either monthly price or annual price must be provided' });
}
const sql = `
UPDATE subscriptions
SET service_name = ?, monthly_price = ?, annual_price = ?, billing_cycle = ?,
currency = ?, start_date = ?, end_date = ?, free_trial_days = ?,
category = ?, notes = ?, website_url = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?
`;
const websiteUrlValue = websiteUrl ? websiteUrl.trim() : null;
db.run(sql, [
serviceName.trim(),
monthlyPrice || null,
annualPrice || null,
billingCycle,
currency,
startDate,
endDate || null,
freeTrialDays,
category.trim(),
notes.trim(),
websiteUrlValue,
subscriptionId,
req.session.userId
], function(err) {
if (err) {
console.error('Error updating subscription:', err.message);
res.status(500).json({ error: 'Failed to update subscription' });
return;
}
if (this.changes === 0) {
res.status(404).json({ error: 'Subscription not found' });
return;
}
res.json({ message: 'Subscription updated successfully' });
});
});
app.delete('/api/subscriptions/:id', requireAuthOrToken, (req, res) => {
const subscriptionId = req.params.id;
if (!subscriptionId || isNaN(subscriptionId)) {
return res.status(400).json({ error: 'Invalid subscription ID' });
}
const sql = 'UPDATE subscriptions SET is_active = 0 WHERE id = ? AND user_id = ?';
db.run(sql, [subscriptionId, req.session.userId], function(err) {
if (err) {
console.error('Error deleting subscription:', err.message);
res.status(500).json({ error: 'Failed to delete subscription' });
return;
}
if (this.changes === 0) {
res.status(404).json({ error: 'Subscription not found' });
return;
}
res.json({ message: 'Subscription deleted successfully' });
});
});
app.get('/api/subscriptions-summary', requireAuthOrToken, (req, res) => {
const sql = `
SELECT
COUNT(*) as total_subscriptions,
SUM(CASE
WHEN billing_cycle = 'monthly' AND monthly_price IS NOT NULL THEN monthly_price
WHEN billing_cycle = 'annual' AND annual_price IS NOT NULL THEN annual_price / 12
ELSE 0
END) as total_monthly_cost,
SUM(CASE
WHEN billing_cycle = 'annual' AND annual_price IS NOT NULL THEN annual_price
WHEN billing_cycle = 'monthly' AND monthly_price IS NOT NULL THEN monthly_price * 12
ELSE 0
END) as total_annual_cost,
currency,
COUNT(CASE WHEN billing_cycle = 'monthly' THEN 1 END) as monthly_subscriptions,
COUNT(CASE WHEN billing_cycle = 'annual' THEN 1 END) as annual_subscriptions
FROM subscriptions
WHERE user_id = ? AND is_active = 1
GROUP BY currency
`;
db.all(sql, [req.session.userId], (err, rows) => {
if (err) {
console.error('Error fetching subscriptions summary:', err.message);
res.status(500).json({ error: 'Failed to fetch subscriptions summary' });
return;
}
const summary = rows.length ? rows : [{
total_subscriptions: 0,
total_monthly_cost: 0,
total_annual_cost: 0,
currency: 'EUR',
monthly_subscriptions: 0,
annual_subscriptions: 0
}];
res.json(summary);
});
});
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html')); res.sendFile(path.join(__dirname, 'index.html'));
}); });

2162
styles.css

File diff suppressed because it is too large Load Diff