Compare commits
No commits in common. "4fb0d35daff7864ee832cc1613422bae2d36813c" and "711826010fad1cdf17f93396b69080a3bd5b64d5" have entirely different histories.
4fb0d35daf
...
711826010f
442
index.html
442
index.html
@ -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>Personal Finance Tracker</title>
|
<title>ETF Trade 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>Finance Tracker</h2>
|
<h2>ETF 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,14 +41,6 @@
|
|||||||
<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>
|
||||||
@ -80,7 +72,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 Personal Finance Tracker</h2>
|
<h2>Login to ETF 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>
|
||||||
@ -102,138 +94,95 @@
|
|||||||
|
|
||||||
<!-- Dashboard Page -->
|
<!-- Dashboard Page -->
|
||||||
<div id="dashboard-page" class="page">
|
<div id="dashboard-page" class="page">
|
||||||
<div class="dashboard-summary">
|
<div class="dashboard-grid">
|
||||||
<div class="card-group">
|
<div class="dashboard-card total-value">
|
||||||
<div class="summary-card portfolio-summary">
|
<h3>Current Portfolio Value</h3>
|
||||||
<div class="summary-header">
|
<div class="metric-value" id="dashboard-current-value">€0.00</div>
|
||||||
<h3>Portfolio</h3>
|
<div class="metric-detail" id="dashboard-last-updated">Using cost basis</div>
|
||||||
</div>
|
|
||||||
<div class="summary-content">
|
|
||||||
<div class="primary-value" id="dashboard-current-value">€0.00</div>
|
|
||||||
<div class="secondary-value" id="dashboard-total-gains">€0.00</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-accounts-section">
|
|
||||||
<div class="top-accounts-title">Top Holdings</div>
|
|
||||||
<div id="dashboard-top-portfolio" class="top-accounts-list">
|
|
||||||
<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>
|
||||||
|
|
||||||
<div class="card-group">
|
<div class="dashboard-card total-gains">
|
||||||
<div class="summary-card cash-summary">
|
<h3>Total Gains/Losses</h3>
|
||||||
<div class="summary-header">
|
<div class="metric-value" id="dashboard-total-gains">€0.00</div>
|
||||||
<h3>Cash</h3>
|
<div class="metric-change" id="dashboard-gains-percentage">0.0%</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="card-group">
|
<div class="dashboard-card monthly-investment">
|
||||||
<div class="summary-card subscriptions-summary">
|
<h3>Monthly Investment</h3>
|
||||||
<div class="summary-header">
|
<div class="metric-value" id="monthly-investment">€0.00</div>
|
||||||
<h3>Subscriptions</h3>
|
<div class="metric-detail" id="monthly-trades">0 trades</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="card-group">
|
<div class="dashboard-card yearly-investment">
|
||||||
<div class="summary-card total-summary">
|
<h3>Yearly Investment</h3>
|
||||||
<div class="summary-header">
|
<div class="metric-value" id="yearly-investment">€0.00</div>
|
||||||
<h3>Total Value</h3>
|
<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="summary-content">
|
<div class="holdings-item">
|
||||||
<div class="primary-value total" id="dashboard-total-combined">€0.00</div>
|
<span class="holdings-label">Cash Savings:</span>
|
||||||
<div class="secondary-value" id="dashboard-performance">0.0%</div>
|
<span class="holdings-value" id="total-holdings-cash">€0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="holdings-divider"></div>
|
||||||
|
<div class="holdings-total">
|
||||||
|
<span class="holdings-label">Total Value:</span>
|
||||||
|
<span class="holdings-value total" id="total-holdings-combined">€0.00</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Growth Graphs Section -->
|
<div class="cash-breakdown" id="cash-breakdown" style="display: none;">
|
||||||
<div class="growth-graphs-section">
|
<h3>Cash Savings Summary</h3>
|
||||||
<div class="graphs-header">
|
<div class="cash-summary-overview">
|
||||||
<h2>Growth & Trends</h2>
|
<div class="cash-total-card">
|
||||||
<div class="time-period-selector">
|
<div class="cash-amount-line" id="dashboard-cash-eur-line">
|
||||||
<button class="period-btn active" data-period="7d">7D</button>
|
<span class="currency-label">EUR:</span>
|
||||||
<button class="period-btn" data-period="1m">1M</button>
|
<span class="amount" id="dashboard-cash-eur">€0.00</span>
|
||||||
<button class="period-btn" data-period="3m">3M</button>
|
</div>
|
||||||
<button class="period-btn" data-period="6m">6M</button>
|
<div class="cash-amount-line" id="dashboard-cash-usd-line" style="display: none;">
|
||||||
<button class="period-btn" data-period="1y">1Y</button>
|
<span class="currency-label">USD:</span>
|
||||||
|
<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 class="graphs-grid">
|
<div id="dashboard-cash-accounts-list">
|
||||||
<div class="graph-card portfolio-graph">
|
<p class="no-data">No cash accounts yet</p>
|
||||||
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="graph-card cash-graph">
|
<div class="etf-breakdown">
|
||||||
<div class="graph-header">
|
<h3>ETF Breakdown</h3>
|
||||||
<h3>Cash Holdings</h3>
|
<div id="etf-breakdown-list" class="breakdown-list">
|
||||||
<div class="graph-stats">
|
<p class="no-data">No ETF positions yet</p>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
@ -297,10 +246,7 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<div class="form-actions">
|
<button type="submit" class="submit-btn">Add Trade</button>
|
||||||
<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>
|
||||||
@ -523,10 +469,7 @@ 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>
|
||||||
<div class="form-actions">
|
<button type="submit" class="create-account-btn">Add Account</button>
|
||||||
<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>
|
||||||
|
|
||||||
@ -920,235 +863,6 @@ 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>
|
||||||
|
|||||||
313
server.js
313
server.js
@ -243,47 +243,6 @@ 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() {
|
||||||
@ -1362,278 +1321,6 @@ 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'));
|
||||||
});
|
});
|
||||||
|
|||||||
2136
styles.css
2136
styles.css
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user