Add comprehensive cash savings and transfers system with total holdings tracking
- Add cash accounts management with CRUD operations and account types - Implement date-stamped transfer system with deposits/withdrawals - Add total holdings card combining portfolio and cash values - Create dedicated "Add Transfer" page in navigation - Include automatic balance calculations and transaction history - Add database migrations for cash_accounts and cash_transfers tables - Integrate cash data with dashboard and real-time updates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
503addf705
commit
f10332d9f5
417
index.html
417
index.html
@ -21,22 +21,34 @@
|
|||||||
<span class="menu-icon">💼</span>
|
<span class="menu-icon">💼</span>
|
||||||
<span class="menu-text">Portfolio</span>
|
<span class="menu-text">Portfolio</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item" data-page="trade-history">
|
<li class="menu-item" data-page="add-trade">
|
||||||
<span class="menu-icon">📋</span>
|
<span class="menu-icon">➕</span>
|
||||||
<span class="menu-text">Trade History</span>
|
<span class="menu-text">Add Trade</span>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item" data-page="cash-accounts">
|
||||||
|
<span class="menu-icon">💰</span>
|
||||||
|
<span class="menu-text">Cash Accounts</span>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item" data-page="add-transfer">
|
||||||
|
<span class="menu-icon">💸</span>
|
||||||
|
<span class="menu-text">Add Transfer</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item" data-page="gains-losses">
|
<li class="menu-item" data-page="gains-losses">
|
||||||
<span class="menu-icon">📈</span>
|
<span class="menu-icon">📈</span>
|
||||||
<span class="menu-text">Gains/Losses</span>
|
<span class="menu-text">Gains/Losses</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item menu-separator" data-page="add-trade">
|
<li class="menu-item" data-page="trade-history">
|
||||||
<span class="menu-icon">➕</span>
|
<span class="menu-icon">📋</span>
|
||||||
<span class="menu-text">Add Trade</span>
|
<span class="menu-text">Trade History</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="menu-item" 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>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="menu-item" data-page="tokens">
|
||||||
|
<span class="menu-icon">🔑</span>
|
||||||
|
<span class="menu-text">API Tokens</span>
|
||||||
|
</li>
|
||||||
<li class="menu-item admin-only" data-page="admin" style="display: none;">
|
<li class="menu-item admin-only" data-page="admin" style="display: none;">
|
||||||
<span class="menu-icon">👥</span>
|
<span class="menu-icon">👥</span>
|
||||||
<span class="menu-text">Admin Panel</span>
|
<span class="menu-text">Admin Panel</span>
|
||||||
@ -120,6 +132,53 @@
|
|||||||
</div>
|
</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 class="holdings-item">
|
||||||
|
<span class="holdings-label">Cash Savings:</span>
|
||||||
|
<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 class="cash-breakdown" id="cash-breakdown" style="display: none;">
|
||||||
|
<h3>Cash Savings Summary</h3>
|
||||||
|
<div class="cash-summary-overview">
|
||||||
|
<div class="cash-total-card">
|
||||||
|
<div class="cash-amount-line" id="dashboard-cash-eur-line">
|
||||||
|
<span class="currency-label">EUR:</span>
|
||||||
|
<span class="amount" id="dashboard-cash-eur">€0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="cash-amount-line" id="dashboard-cash-usd-line" style="display: none;">
|
||||||
|
<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 id="cash-accounts-breakdown" class="breakdown-list">
|
||||||
|
<div id="dashboard-cash-accounts-list">
|
||||||
|
<p class="no-data">No cash accounts yet</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="etf-breakdown">
|
<div class="etf-breakdown">
|
||||||
<h3>ETF Breakdown</h3>
|
<h3>ETF Breakdown</h3>
|
||||||
<div id="etf-breakdown-list" class="breakdown-list">
|
<div id="etf-breakdown-list" class="breakdown-list">
|
||||||
@ -257,6 +316,337 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API Tokens Page -->
|
||||||
|
<div id="tokens-page" class="page">
|
||||||
|
<div class="tokens-container">
|
||||||
|
<h2>API Access Tokens</h2>
|
||||||
|
<div class="tokens-intro">
|
||||||
|
<p>Create access tokens to authenticate API requests for third-party integrations like n8n workflows. Keep your tokens secure and never share them publicly.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="token-actions">
|
||||||
|
<h3>Create New Token</h3>
|
||||||
|
<form id="create-token-form" class="create-token-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="token-name">Token Name</label>
|
||||||
|
<input type="text" id="token-name" required placeholder="e.g., N8N Workflow Token" maxlength="50">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="token-expires">Expires (optional)</label>
|
||||||
|
<select id="token-expires">
|
||||||
|
<option value="">Never</option>
|
||||||
|
<option value="30">30 days</option>
|
||||||
|
<option value="90">90 days</option>
|
||||||
|
<option value="365">1 year</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="token-scopes">Permissions</label>
|
||||||
|
<select id="token-scopes">
|
||||||
|
<option value="read">Read Only</option>
|
||||||
|
<option value="read,write" selected>Read & Write</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="create-token-btn">Create Token</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tokens-list-section">
|
||||||
|
<h3>Your Access Tokens</h3>
|
||||||
|
<div id="tokens-list" class="tokens-list">
|
||||||
|
<p class="no-data">Loading tokens...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token Creation Modal -->
|
||||||
|
<div id="token-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Token Created Successfully</h3>
|
||||||
|
<p><strong>Important:</strong> Copy your token now. You won't be able to see it again!</p>
|
||||||
|
<div class="token-display">
|
||||||
|
<input type="text" id="new-token-value" readonly>
|
||||||
|
<button id="copy-token-btn" class="copy-btn">Copy</button>
|
||||||
|
</div>
|
||||||
|
<div class="token-usage">
|
||||||
|
<h4>Usage Example:</h4>
|
||||||
|
<code class="usage-example">
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" \<br>
|
||||||
|
http://localhost:3000/api/trades
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<button id="close-token-modal" class="close-modal-btn">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cash Accounts Page -->
|
||||||
|
<div id="cash-accounts-page" class="page">
|
||||||
|
<div class="cash-accounts-container">
|
||||||
|
<h2>Cash Savings Accounts</h2>
|
||||||
|
<div class="cash-intro">
|
||||||
|
<p>Track your cash savings accounts alongside your ETF investments. Manage different account types and monitor your total cash position.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cash-summary-cards">
|
||||||
|
<div class="cash-summary-card total-cash">
|
||||||
|
<h3>Total Cash</h3>
|
||||||
|
<div class="cash-amounts">
|
||||||
|
<div class="amount-line">
|
||||||
|
<span class="currency">EUR:</span>
|
||||||
|
<span class="amount" id="total-cash-eur">€0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="amount-line">
|
||||||
|
<span class="currency">USD:</span>
|
||||||
|
<span class="amount" id="total-cash-usd">$0.00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cash-summary-card account-stats">
|
||||||
|
<h3>Account Stats</h3>
|
||||||
|
<div class="stat-line">
|
||||||
|
<span>Accounts:</span>
|
||||||
|
<span id="account-count">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-line">
|
||||||
|
<span>Avg Interest:</span>
|
||||||
|
<span id="avg-interest">0.0%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cash-actions">
|
||||||
|
<h3>Add New Account</h3>
|
||||||
|
<form id="create-account-form" class="create-account-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="account-name">Account Name</label>
|
||||||
|
<input type="text" id="account-name" required placeholder="e.g., Emergency Savings" maxlength="100">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="account-type">Account Type</label>
|
||||||
|
<select id="account-type">
|
||||||
|
<option value="savings">Savings Account</option>
|
||||||
|
<option value="checking">Checking Account</option>
|
||||||
|
<option value="money_market">Money Market</option>
|
||||||
|
<option value="cd">Certificate of Deposit</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="account-balance">Balance</label>
|
||||||
|
<input type="number" id="account-balance" step="0.01" min="0" required placeholder="0.00">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="account-currency">Currency</label>
|
||||||
|
<select id="account-currency">
|
||||||
|
<option value="EUR" selected>EUR (€)</option>
|
||||||
|
<option value="USD">USD ($)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="institution-name">Institution (optional)</label>
|
||||||
|
<input type="text" id="institution-name" placeholder="e.g., Bank of Ireland">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="interest-rate">Interest Rate % (optional)</label>
|
||||||
|
<input type="number" id="interest-rate" step="0.01" min="0" placeholder="0.00">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="account-notes">Notes (optional)</label>
|
||||||
|
<textarea id="account-notes" placeholder="Additional notes about this account" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="create-account-btn">Add Account</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accounts-list-section">
|
||||||
|
<h3>Your Cash Accounts</h3>
|
||||||
|
<div id="accounts-list" class="accounts-list">
|
||||||
|
<p class="no-data">Loading accounts...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Account Modal -->
|
||||||
|
<div id="edit-account-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Edit Cash Account</h3>
|
||||||
|
<form id="edit-account-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-account-name">Account Name</label>
|
||||||
|
<input type="text" id="edit-account-name" required maxlength="100">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-account-type">Account Type</label>
|
||||||
|
<select id="edit-account-type">
|
||||||
|
<option value="savings">Savings Account</option>
|
||||||
|
<option value="checking">Checking Account</option>
|
||||||
|
<option value="money_market">Money Market</option>
|
||||||
|
<option value="cd">Certificate of Deposit</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-account-balance">Balance</label>
|
||||||
|
<input type="number" id="edit-account-balance" step="0.01" min="0" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-account-currency">Currency</label>
|
||||||
|
<select id="edit-account-currency">
|
||||||
|
<option value="EUR">EUR (€)</option>
|
||||||
|
<option value="USD">USD ($)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-institution-name">Institution</label>
|
||||||
|
<input type="text" id="edit-institution-name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-interest-rate">Interest Rate %</label>
|
||||||
|
<input type="number" id="edit-interest-rate" step="0.01" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="edit-account-notes">Notes</label>
|
||||||
|
<textarea id="edit-account-notes" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button type="button" id="cancel-edit-account" class="cancel-btn">Cancel</button>
|
||||||
|
<button type="submit" class="save-btn">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transfer Modal -->
|
||||||
|
<div id="transfer-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Add Transfer</h3>
|
||||||
|
<form id="transfer-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="transfer-account">Account</label>
|
||||||
|
<select id="transfer-account" required>
|
||||||
|
<option value="">Select Account</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="transfer-type">Transfer Type</label>
|
||||||
|
<select id="transfer-type" required>
|
||||||
|
<option value="">Select Type</option>
|
||||||
|
<option value="deposit">Deposit</option>
|
||||||
|
<option value="withdrawal">Withdrawal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="transfer-amount">Amount</label>
|
||||||
|
<input type="number" id="transfer-amount" step="0.01" min="0.01" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="transfer-date">Transfer Date</label>
|
||||||
|
<input type="date" id="transfer-date" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="transfer-description">Description</label>
|
||||||
|
<textarea id="transfer-description" rows="2" placeholder="Optional description"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button type="button" id="cancel-transfer" class="cancel-btn">Cancel</button>
|
||||||
|
<button type="submit" class="save-btn">Add Transfer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transfers Section -->
|
||||||
|
<div class="transfers-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>Recent Transfers</h3>
|
||||||
|
<button id="add-transfer-btn" class="primary-btn">Add Transfer</button>
|
||||||
|
</div>
|
||||||
|
<div id="transfers-list" class="transfers-list">
|
||||||
|
<p class="no-data">No transfers found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Transfer Page -->
|
||||||
|
<div id="add-transfer-page" class="page">
|
||||||
|
<div class="transfer-form-container">
|
||||||
|
<h2>Add Cash Transfer</h2>
|
||||||
|
<div class="transfer-intro">
|
||||||
|
<p>Record deposits and withdrawals for your cash accounts. Transfers will automatically update account balances.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="standalone-transfer-form" class="transfer-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="standalone-transfer-account">Account</label>
|
||||||
|
<select id="standalone-transfer-account" required>
|
||||||
|
<option value="">Select Account</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="standalone-transfer-type">Transfer Type</label>
|
||||||
|
<select id="standalone-transfer-type" required>
|
||||||
|
<option value="">Select Type</option>
|
||||||
|
<option value="deposit">Deposit</option>
|
||||||
|
<option value="withdrawal">Withdrawal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="standalone-transfer-amount">Amount</label>
|
||||||
|
<input type="number" id="standalone-transfer-amount" step="0.01" min="0.01" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="standalone-transfer-date">Transfer Date</label>
|
||||||
|
<input type="date" id="standalone-transfer-date" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="standalone-transfer-description">Description</label>
|
||||||
|
<textarea id="standalone-transfer-description" rows="3" placeholder="Optional description"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="submit-btn">Add Transfer</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="recent-transfers">
|
||||||
|
<h3>Recent Transfers</h3>
|
||||||
|
<div id="standalone-transfers-list" class="transfers-list">
|
||||||
|
<p class="no-data">No transfers found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Gains/Losses Page -->
|
<!-- Gains/Losses Page -->
|
||||||
<div id="gains-losses-page" class="page">
|
<div id="gains-losses-page" class="page">
|
||||||
<div class="gains-losses-container">
|
<div class="gains-losses-container">
|
||||||
@ -407,9 +797,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cgt-rate-item">
|
<div class="cgt-rate-item">
|
||||||
<label for="cgt-longterm">2+ Years (%)</label>
|
<label for="cgt-longterm">2-8 Years (%)</label>
|
||||||
<input type="number" id="cgt-longterm" min="0" max="100" step="0.1" value="10" class="cgt-rate-input">
|
<input type="number" id="cgt-longterm" min="0" max="100" step="0.1" value="10" class="cgt-rate-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="cgt-rate-item">
|
||||||
|
<label for="cgt-8years">8+ Years (%)</label>
|
||||||
|
<input type="number" id="cgt-8years" min="0" max="100" step="0.1" value="33" class="cgt-rate-input">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cgt-options">
|
<div class="cgt-options">
|
||||||
@ -454,9 +849,13 @@
|
|||||||
<div class="rate-value">20%</div>
|
<div class="rate-value">20%</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rate-bar" data-period="longterm">
|
<div class="rate-bar" data-period="longterm">
|
||||||
<span class="period-label">2Y+</span>
|
<span class="period-label">2-8Y</span>
|
||||||
<div class="rate-value">10%</div>
|
<div class="rate-value">10%</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="rate-bar" data-period="8years">
|
||||||
|
<span class="period-label">8Y+</span>
|
||||||
|
<div class="rate-value">33%</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
642
server.js
642
server.js
@ -4,6 +4,7 @@ const cors = require('cors');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
|
const crypto = require('crypto');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -88,6 +89,35 @@ function initializeDatabase() {
|
|||||||
console.error('Error creating trades table:', err.message);
|
console.error('Error creating trades table:', err.message);
|
||||||
} else {
|
} else {
|
||||||
console.log('Trades table ready');
|
console.log('Trades table ready');
|
||||||
|
|
||||||
|
// Check if user_id column exists, if not add it (for existing databases)
|
||||||
|
db.all("PRAGMA table_info(trades)", (err, columns) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error checking trades table structure:', err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUserId = columns.some(col => col.name === 'user_id');
|
||||||
|
if (!hasUserId) {
|
||||||
|
console.log('Adding user_id column to existing trades table...');
|
||||||
|
db.run("ALTER TABLE trades ADD COLUMN user_id INTEGER DEFAULT 1", (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error adding user_id column:', err.message);
|
||||||
|
} else {
|
||||||
|
console.log('user_id column added to trades table');
|
||||||
|
|
||||||
|
// Update all existing trades to belong to user 1 (admin)
|
||||||
|
db.run("UPDATE trades SET user_id = 1 WHERE user_id IS NULL", (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error updating existing trades:', err.message);
|
||||||
|
} else {
|
||||||
|
console.log('Existing trades updated with user_id');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,6 +131,7 @@ function initializeDatabase() {
|
|||||||
rate_1year REAL DEFAULT 30.0,
|
rate_1year REAL DEFAULT 30.0,
|
||||||
rate_2years REAL DEFAULT 20.0,
|
rate_2years REAL DEFAULT 20.0,
|
||||||
rate_longterm REAL DEFAULT 10.0,
|
rate_longterm REAL DEFAULT 10.0,
|
||||||
|
rate_8years REAL DEFAULT 33.0,
|
||||||
annual_exemption REAL DEFAULT 1270.0,
|
annual_exemption REAL DEFAULT 1270.0,
|
||||||
enabled BOOLEAN DEFAULT 1,
|
enabled BOOLEAN DEFAULT 1,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
@ -116,6 +147,81 @@ function initializeDatabase() {
|
|||||||
console.log('CGT settings table ready');
|
console.log('CGT settings table ready');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create access tokens table
|
||||||
|
const createAccessTokensTableSQL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS access_tokens (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token_name TEXT NOT NULL,
|
||||||
|
token_hash TEXT NOT NULL,
|
||||||
|
token_prefix TEXT NOT NULL,
|
||||||
|
scopes TEXT DEFAULT 'read,write',
|
||||||
|
last_used_at DATETIME,
|
||||||
|
expires_at DATETIME,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(createAccessTokensTableSQL, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error creating access tokens table:', err.message);
|
||||||
|
} else {
|
||||||
|
console.log('Access tokens table ready');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create cash accounts table
|
||||||
|
const createCashAccountsTableSQL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS cash_accounts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
account_name TEXT NOT NULL,
|
||||||
|
account_type TEXT DEFAULT 'savings' CHECK (account_type IN ('savings', 'checking', 'money_market', 'cd', 'other')),
|
||||||
|
balance REAL NOT NULL DEFAULT 0 CHECK (balance >= 0),
|
||||||
|
currency TEXT NOT NULL DEFAULT 'EUR' CHECK (currency IN ('EUR', 'USD')),
|
||||||
|
institution_name TEXT,
|
||||||
|
interest_rate REAL DEFAULT 0 CHECK (interest_rate >= 0),
|
||||||
|
notes 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
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create cash transfers table
|
||||||
|
const createCashTransfersTableSQL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS cash_transfers (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
transfer_type TEXT NOT NULL CHECK (transfer_type IN ('deposit', 'withdrawal')),
|
||||||
|
amount REAL NOT NULL CHECK (amount > 0),
|
||||||
|
description TEXT,
|
||||||
|
transfer_date DATE NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (account_id) REFERENCES cash_accounts (id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(createCashAccountsTableSQL, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error creating cash accounts table:', err.message);
|
||||||
|
} else {
|
||||||
|
console.log('Cash accounts table ready');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.run(createCashTransfersTableSQL, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error creating cash transfers table:', err.message);
|
||||||
|
} else {
|
||||||
|
console.log('Cash transfers table ready');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDefaultAdmin() {
|
async function createDefaultAdmin() {
|
||||||
@ -167,6 +273,72 @@ function requireAdmin(req, res, next) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Middleware for token-based authentication
|
||||||
|
async function authenticateToken(req, res, next) {
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Access token required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token prefix (first 8 characters)
|
||||||
|
const tokenPrefix = token.substring(0, 8);
|
||||||
|
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT at.*, u.username, u.is_admin
|
||||||
|
FROM access_tokens at
|
||||||
|
JOIN users u ON at.user_id = u.id
|
||||||
|
WHERE at.token_prefix = ? AND at.token_hash = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.get(sql, [tokenPrefix, tokenHash], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Token authentication error:', err);
|
||||||
|
return res.status(500).json({ error: 'Authentication error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return res.status(401).json({ error: 'Invalid access token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is expired
|
||||||
|
if (row.expires_at && new Date() > new Date(row.expires_at)) {
|
||||||
|
return res.status(401).json({ error: 'Access token expired' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last used timestamp
|
||||||
|
db.run('UPDATE access_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?', [row.id]);
|
||||||
|
|
||||||
|
// Set user info for downstream middleware
|
||||||
|
req.session = req.session || {};
|
||||||
|
req.session.userId = row.user_id;
|
||||||
|
req.session.username = row.username;
|
||||||
|
req.session.isAdmin = row.is_admin === 1;
|
||||||
|
req.tokenAuth = true;
|
||||||
|
req.tokenScopes = row.scopes ? row.scopes.split(',') : ['read', 'write'];
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token authentication error:', error);
|
||||||
|
res.status(500).json({ error: 'Authentication error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combined authentication middleware (session or token)
|
||||||
|
function requireAuthOrToken(req, res, next) {
|
||||||
|
// Check for token authentication first
|
||||||
|
if (req.headers['authorization']) {
|
||||||
|
return authenticateToken(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to session authentication
|
||||||
|
return requireAuth(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication endpoints
|
// Authentication endpoints
|
||||||
app.post('/api/login', async (req, res) => {
|
app.post('/api/login', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
@ -220,7 +392,7 @@ app.post('/api/logout', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/me', requireAuth, (req, res) => {
|
app.get('/api/me', requireAuthOrToken, (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
id: req.session.userId,
|
id: req.session.userId,
|
||||||
username: req.session.username,
|
username: req.session.username,
|
||||||
@ -306,7 +478,450 @@ app.delete('/api/admin/users/:id', requireAdmin, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/trades', requireAuth, (req, res) => {
|
// Access Token Management endpoints
|
||||||
|
app.get('/api/tokens', requireAuthOrToken, (req, res) => {
|
||||||
|
const sql = `
|
||||||
|
SELECT id, token_name, token_prefix, scopes, last_used_at, expires_at, created_at
|
||||||
|
FROM access_tokens
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(sql, [req.session.userId], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error fetching tokens:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch access tokens' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/tokens', requireAuthOrToken, (req, res) => {
|
||||||
|
const { name, expires_in_days = null, scopes = 'read,write' } = req.body;
|
||||||
|
|
||||||
|
if (!name || name.trim().length === 0) {
|
||||||
|
return res.status(400).json({ error: 'Token name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.length > 50) {
|
||||||
|
return res.status(400).json({ error: 'Token name must be 50 characters or less' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a secure token (32 bytes = 64 hex characters)
|
||||||
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
|
const tokenPrefix = token.substring(0, 8);
|
||||||
|
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
|
||||||
|
|
||||||
|
// Calculate expiration date
|
||||||
|
const expiresAt = expires_in_days ? new Date(Date.now() + expires_in_days * 24 * 60 * 60 * 1000) : null;
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO access_tokens (user_id, token_name, token_hash, token_prefix, scopes, expires_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(sql, [req.session.userId, name.trim(), tokenHash, tokenPrefix, scopes, expiresAt], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error creating access token:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to create access token' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Access token created successfully',
|
||||||
|
token_id: this.lastID,
|
||||||
|
token: token, // Only returned once
|
||||||
|
token_name: name.trim(),
|
||||||
|
scopes: scopes,
|
||||||
|
expires_at: expiresAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/tokens/:id', requireAuthOrToken, (req, res) => {
|
||||||
|
const tokenId = req.params.id;
|
||||||
|
|
||||||
|
if (!tokenId || isNaN(tokenId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid token ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = 'DELETE FROM access_tokens WHERE id = ? AND user_id = ?';
|
||||||
|
|
||||||
|
db.run(sql, [tokenId, req.session.userId], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error deleting access token:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to delete access token' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
res.status(404).json({ error: 'Access token not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Access token deleted successfully' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch('/api/tokens/:id', requireAuthOrToken, (req, res) => {
|
||||||
|
const tokenId = req.params.id;
|
||||||
|
const { name } = req.body;
|
||||||
|
|
||||||
|
if (!tokenId || isNaN(tokenId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid token ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name || name.trim().length === 0) {
|
||||||
|
return res.status(400).json({ error: 'Token name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.length > 50) {
|
||||||
|
return res.status(400).json({ error: 'Token name must be 50 characters or less' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = 'UPDATE access_tokens SET token_name = ? WHERE id = ? AND user_id = ?';
|
||||||
|
|
||||||
|
db.run(sql, [name.trim(), tokenId, req.session.userId], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error updating access token:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to update access token' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
res.status(404).json({ error: 'Access token not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Access token updated successfully' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cash Accounts Management endpoints
|
||||||
|
app.get('/api/cash-accounts', requireAuthOrToken, (req, res) => {
|
||||||
|
const sql = `
|
||||||
|
SELECT id, account_name, account_type, balance, currency, institution_name,
|
||||||
|
interest_rate, notes, is_active, created_at, updated_at
|
||||||
|
FROM cash_accounts
|
||||||
|
WHERE user_id = ? AND is_active = 1
|
||||||
|
ORDER BY account_name ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(sql, [req.session.userId], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error fetching cash accounts:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch cash accounts' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/cash-accounts', requireAuthOrToken, (req, res) => {
|
||||||
|
const {
|
||||||
|
account_name,
|
||||||
|
account_type = 'savings',
|
||||||
|
balance = 0,
|
||||||
|
currency = 'EUR',
|
||||||
|
institution_name = '',
|
||||||
|
interest_rate = 0,
|
||||||
|
notes = ''
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!account_name || account_name.trim().length === 0) {
|
||||||
|
return res.status(400).json({ error: 'Account name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account_name.length > 100) {
|
||||||
|
return res.status(400).json({ error: 'Account name must be 100 characters or less' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (balance < 0) {
|
||||||
|
return res.status(400).json({ error: 'Balance cannot be negative' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interest_rate < 0) {
|
||||||
|
return res.status(400).json({ error: 'Interest rate cannot be negative' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO cash_accounts (
|
||||||
|
user_id, account_name, account_type, balance, currency,
|
||||||
|
institution_name, interest_rate, notes
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(sql, [
|
||||||
|
req.session.userId,
|
||||||
|
account_name.trim(),
|
||||||
|
account_type,
|
||||||
|
parseFloat(balance),
|
||||||
|
currency,
|
||||||
|
institution_name.trim(),
|
||||||
|
parseFloat(interest_rate),
|
||||||
|
notes.trim()
|
||||||
|
], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error creating cash account:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to create cash account' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Cash account created successfully',
|
||||||
|
account_id: this.lastID,
|
||||||
|
account_name: account_name.trim(),
|
||||||
|
account_type,
|
||||||
|
balance: parseFloat(balance),
|
||||||
|
currency
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/cash-accounts/:id', requireAuthOrToken, (req, res) => {
|
||||||
|
const accountId = req.params.id;
|
||||||
|
const {
|
||||||
|
account_name,
|
||||||
|
account_type,
|
||||||
|
balance,
|
||||||
|
currency,
|
||||||
|
institution_name = '',
|
||||||
|
interest_rate = 0,
|
||||||
|
notes = ''
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!accountId || isNaN(accountId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid account ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!account_name || account_name.trim().length === 0) {
|
||||||
|
return res.status(400).json({ error: 'Account name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account_name.length > 100) {
|
||||||
|
return res.status(400).json({ error: 'Account name must be 100 characters or less' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (balance < 0) {
|
||||||
|
return res.status(400).json({ error: 'Balance cannot be negative' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interest_rate < 0) {
|
||||||
|
return res.status(400).json({ error: 'Interest rate cannot be negative' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
UPDATE cash_accounts
|
||||||
|
SET account_name = ?, account_type = ?, balance = ?, currency = ?,
|
||||||
|
institution_name = ?, interest_rate = ?, notes = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ? AND user_id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(sql, [
|
||||||
|
account_name.trim(),
|
||||||
|
account_type,
|
||||||
|
parseFloat(balance),
|
||||||
|
currency,
|
||||||
|
institution_name.trim(),
|
||||||
|
parseFloat(interest_rate),
|
||||||
|
notes.trim(),
|
||||||
|
accountId,
|
||||||
|
req.session.userId
|
||||||
|
], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error updating cash account:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to update cash account' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
res.status(404).json({ error: 'Cash account not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Cash account updated successfully' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/cash-accounts/:id', requireAuthOrToken, (req, res) => {
|
||||||
|
const accountId = req.params.id;
|
||||||
|
|
||||||
|
if (!accountId || isNaN(accountId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid account ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = 'UPDATE cash_accounts SET is_active = 0 WHERE id = ? AND user_id = ?';
|
||||||
|
|
||||||
|
db.run(sql, [accountId, req.session.userId], function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error deleting cash account:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to delete cash account' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changes === 0) {
|
||||||
|
res.status(404).json({ error: 'Cash account not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Cash account deleted successfully' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/cash-summary', requireAuthOrToken, (req, res) => {
|
||||||
|
const sql = `
|
||||||
|
SELECT
|
||||||
|
SUM(CASE WHEN currency = 'EUR' THEN balance ELSE 0 END) as total_eur,
|
||||||
|
SUM(CASE WHEN currency = 'USD' THEN balance ELSE 0 END) as total_usd,
|
||||||
|
COUNT(*) as account_count,
|
||||||
|
AVG(CASE WHEN interest_rate > 0 THEN interest_rate ELSE NULL END) as avg_interest_rate
|
||||||
|
FROM cash_accounts
|
||||||
|
WHERE user_id = ? AND is_active = 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.get(sql, [req.session.userId], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error fetching cash summary:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch cash summary' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
total_eur: row.total_eur || 0,
|
||||||
|
total_usd: row.total_usd || 0,
|
||||||
|
account_count: row.account_count || 0,
|
||||||
|
avg_interest_rate: row.avg_interest_rate || 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cash Transfers API endpoints
|
||||||
|
app.post('/api/cash-transfers', requireAuthOrToken, (req, res) => {
|
||||||
|
const {
|
||||||
|
account_id,
|
||||||
|
transfer_type,
|
||||||
|
amount,
|
||||||
|
description = '',
|
||||||
|
transfer_date
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!account_id || !transfer_type || !amount || !transfer_date) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['deposit', 'withdrawal'].includes(transfer_type)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid transfer type' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount <= 0) {
|
||||||
|
return res.status(400).json({ error: 'Amount must be greater than 0' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if account belongs to user
|
||||||
|
db.get('SELECT * FROM cash_accounts WHERE id = ? AND user_id = ? AND is_active = 1',
|
||||||
|
[account_id, req.session.userId],
|
||||||
|
(err, account) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error checking account:', err.message);
|
||||||
|
return res.status(500).json({ error: 'Failed to verify account' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return res.status(404).json({ error: 'Account not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// For withdrawals, check if sufficient balance
|
||||||
|
if (transfer_type === 'withdrawal' && account.balance < amount) {
|
||||||
|
return res.status(400).json({ error: 'Insufficient balance' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin transaction
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
|
// Insert transfer record
|
||||||
|
const insertTransferSQL = `
|
||||||
|
INSERT INTO cash_transfers (user_id, account_id, transfer_type, amount, description, transfer_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(insertTransferSQL, [req.session.userId, account_id, transfer_type, amount, description, transfer_date],
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error inserting transfer:', err.message);
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
return res.status(500).json({ error: 'Failed to create transfer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const transferId = this.lastID;
|
||||||
|
|
||||||
|
// Update account balance
|
||||||
|
const balanceChange = transfer_type === 'deposit' ? amount : -amount;
|
||||||
|
const updateBalanceSQL = `
|
||||||
|
UPDATE cash_accounts
|
||||||
|
SET balance = balance + ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ? AND user_id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.run(updateBalanceSQL, [balanceChange, account_id, req.session.userId],
|
||||||
|
function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error updating balance:', err.message);
|
||||||
|
db.run('ROLLBACK');
|
||||||
|
return res.status(500).json({ error: 'Failed to update balance' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run('COMMIT');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: transferId,
|
||||||
|
account_id,
|
||||||
|
transfer_type,
|
||||||
|
amount,
|
||||||
|
description,
|
||||||
|
transfer_date,
|
||||||
|
message: 'Transfer created successfully'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/cash-transfers', requireAuthOrToken, (req, res) => {
|
||||||
|
const { account_id } = req.query;
|
||||||
|
|
||||||
|
let sql = `
|
||||||
|
SELECT ct.*, ca.account_name, ca.currency
|
||||||
|
FROM cash_transfers ct
|
||||||
|
JOIN cash_accounts ca ON ct.account_id = ca.id
|
||||||
|
WHERE ct.user_id = ?
|
||||||
|
`;
|
||||||
|
const params = [req.session.userId];
|
||||||
|
|
||||||
|
if (account_id) {
|
||||||
|
sql += ' AND ct.account_id = ?';
|
||||||
|
params.push(account_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ' ORDER BY ct.transfer_date DESC, ct.created_at DESC';
|
||||||
|
|
||||||
|
db.all(sql, params, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error fetching transfers:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch transfers' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/trades', requireAuthOrToken, (req, res) => {
|
||||||
const sql = 'SELECT * FROM trades WHERE user_id = ? ORDER BY trade_datetime DESC, created_at DESC';
|
const sql = 'SELECT * FROM trades WHERE user_id = ? ORDER BY trade_datetime DESC, created_at DESC';
|
||||||
|
|
||||||
db.all(sql, [req.session.userId], (err, rows) => {
|
db.all(sql, [req.session.userId], (err, rows) => {
|
||||||
@ -334,7 +949,7 @@ app.get('/api/trades', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/trades', requireAuth, (req, res) => {
|
app.post('/api/trades', requireAuthOrToken, (req, res) => {
|
||||||
const {
|
const {
|
||||||
etfSymbol,
|
etfSymbol,
|
||||||
tradeType,
|
tradeType,
|
||||||
@ -407,7 +1022,7 @@ app.post('/api/trades', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/trades/:id', requireAuth, (req, res) => {
|
app.delete('/api/trades/:id', requireAuthOrToken, (req, res) => {
|
||||||
const tradeId = req.params.id;
|
const tradeId = req.params.id;
|
||||||
|
|
||||||
if (!tradeId || isNaN(tradeId)) {
|
if (!tradeId || isNaN(tradeId)) {
|
||||||
@ -432,7 +1047,7 @@ app.delete('/api/trades/:id', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/trades', requireAuth, (req, res) => {
|
app.delete('/api/trades', requireAuthOrToken, (req, res) => {
|
||||||
const sql = 'DELETE FROM trades WHERE user_id = ?';
|
const sql = 'DELETE FROM trades WHERE user_id = ?';
|
||||||
|
|
||||||
db.run(sql, [req.session.userId], function(err) {
|
db.run(sql, [req.session.userId], function(err) {
|
||||||
@ -450,7 +1065,7 @@ app.delete('/api/trades', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// CGT Settings endpoints
|
// CGT Settings endpoints
|
||||||
app.get('/api/cgt-settings', requireAuth, (req, res) => {
|
app.get('/api/cgt-settings', requireAuthOrToken, (req, res) => {
|
||||||
const sql = 'SELECT * FROM cgt_settings WHERE user_id = ?';
|
const sql = 'SELECT * FROM cgt_settings WHERE user_id = ?';
|
||||||
|
|
||||||
db.get(sql, [req.session.userId], (err, row) => {
|
db.get(sql, [req.session.userId], (err, row) => {
|
||||||
@ -468,6 +1083,7 @@ app.get('/api/cgt-settings', requireAuth, (req, res) => {
|
|||||||
rate_1year: 30.0,
|
rate_1year: 30.0,
|
||||||
rate_2years: 20.0,
|
rate_2years: 20.0,
|
||||||
rate_longterm: 10.0,
|
rate_longterm: 10.0,
|
||||||
|
rate_8years: 33.0,
|
||||||
annual_exemption: 1270.0,
|
annual_exemption: 1270.0,
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
@ -480,25 +1096,27 @@ app.get('/api/cgt-settings', requireAuth, (req, res) => {
|
|||||||
rate_1year: row.rate_1year,
|
rate_1year: row.rate_1year,
|
||||||
rate_2years: row.rate_2years,
|
rate_2years: row.rate_2years,
|
||||||
rate_longterm: row.rate_longterm,
|
rate_longterm: row.rate_longterm,
|
||||||
|
rate_8years: row.rate_8years,
|
||||||
annual_exemption: row.annual_exemption,
|
annual_exemption: row.annual_exemption,
|
||||||
enabled: row.enabled === 1
|
enabled: row.enabled === 1
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/cgt-settings', requireAuth, (req, res) => {
|
app.post('/api/cgt-settings', requireAuthOrToken, (req, res) => {
|
||||||
const {
|
const {
|
||||||
rate_1month = 40.0,
|
rate_1month = 40.0,
|
||||||
rate_6months = 35.0,
|
rate_6months = 35.0,
|
||||||
rate_1year = 30.0,
|
rate_1year = 30.0,
|
||||||
rate_2years = 20.0,
|
rate_2years = 20.0,
|
||||||
rate_longterm = 10.0,
|
rate_longterm = 10.0,
|
||||||
|
rate_8years = 33.0,
|
||||||
annual_exemption = 1270.0,
|
annual_exemption = 1270.0,
|
||||||
enabled = true
|
enabled = true
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Validate rates are between 0 and 100
|
// Validate rates are between 0 and 100
|
||||||
const rates = [rate_1month, rate_6months, rate_1year, rate_2years, rate_longterm];
|
const rates = [rate_1month, rate_6months, rate_1year, rate_2years, rate_longterm, rate_8years];
|
||||||
if (rates.some(rate => rate < 0 || rate > 100)) {
|
if (rates.some(rate => rate < 0 || rate > 100)) {
|
||||||
return res.status(400).json({ error: 'CGT rates must be between 0 and 100' });
|
return res.status(400).json({ error: 'CGT rates must be between 0 and 100' });
|
||||||
}
|
}
|
||||||
@ -509,8 +1127,8 @@ app.post('/api/cgt-settings', requireAuth, (req, res) => {
|
|||||||
|
|
||||||
const sql = `
|
const sql = `
|
||||||
INSERT OR REPLACE INTO cgt_settings
|
INSERT OR REPLACE INTO cgt_settings
|
||||||
(user_id, rate_1month, rate_6months, rate_1year, rate_2years, rate_longterm, annual_exemption, enabled, updated_at)
|
(user_id, rate_1month, rate_6months, rate_1year, rate_2years, rate_longterm, rate_8years, annual_exemption, enabled, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const params = [
|
const params = [
|
||||||
@ -520,6 +1138,7 @@ app.post('/api/cgt-settings', requireAuth, (req, res) => {
|
|||||||
rate_1year,
|
rate_1year,
|
||||||
rate_2years,
|
rate_2years,
|
||||||
rate_longterm,
|
rate_longterm,
|
||||||
|
rate_8years,
|
||||||
annual_exemption,
|
annual_exemption,
|
||||||
enabled ? 1 : 0
|
enabled ? 1 : 0
|
||||||
];
|
];
|
||||||
@ -539,6 +1158,7 @@ app.post('/api/cgt-settings', requireAuth, (req, res) => {
|
|||||||
rate_1year,
|
rate_1year,
|
||||||
rate_2years,
|
rate_2years,
|
||||||
rate_longterm,
|
rate_longterm,
|
||||||
|
rate_8years,
|
||||||
annual_exemption,
|
annual_exemption,
|
||||||
enabled
|
enabled
|
||||||
}
|
}
|
||||||
@ -546,7 +1166,7 @@ app.post('/api/cgt-settings', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/portfolio-summary', requireAuth, (req, res) => {
|
app.get('/api/portfolio-summary', requireAuthOrToken, (req, res) => {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
etf_symbol,
|
etf_symbol,
|
||||||
|
|||||||
979
styles.css
979
styles.css
@ -535,6 +535,94 @@ body {
|
|||||||
border-left-color: #dc3545;
|
border-left-color: #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Total Holdings Card Styles */
|
||||||
|
.total-holdings-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-holdings-card {
|
||||||
|
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 15px;
|
||||||
|
border-left: 4px solid #f39c12;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-holdings-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-holdings-card h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #e67e22;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 2px solid #f39c12;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holdings-breakdown {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holdings-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holdings-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holdings-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holdings-divider {
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, #f39c12 0%, #e67e22 100%);
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holdings-total {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
background: rgba(243, 156, 18, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holdings-total .holdings-label {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #d35400;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holdings-total .holdings-value.total {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
.etf-breakdown {
|
.etf-breakdown {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
@ -1798,4 +1886,895 @@ body {
|
|||||||
.long-term-stat span:last-child {
|
.long-term-stat span:last-child {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
API Tokens Styles
|
||||||
|
=========================================== */
|
||||||
|
.tokens-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens-intro {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens-intro p {
|
||||||
|
margin: 0;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.95em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-actions {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-token-form .form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-token-form .form-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-token-form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-token-form input,
|
||||||
|
.create-token-form select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-token-form input:focus,
|
||||||
|
.create-token-form select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-token-btn {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-token-btn:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens-list-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-details {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-detail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-prefix {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-status {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-status.active {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-status.expired {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-actions-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-edit-btn,
|
||||||
|
.token-delete-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-edit-btn {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-edit-btn:hover {
|
||||||
|
background: #e0a800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-delete-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-delete-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-display {
|
||||||
|
display: flex;
|
||||||
|
margin: 20px 0;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-display input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #28a745;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn.copied {
|
||||||
|
background: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-example {
|
||||||
|
display: block;
|
||||||
|
background: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 10px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal-btn {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal-btn:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.create-token-form .form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-item {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-actions-buttons {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
Cash Accounts Styles
|
||||||
|
=========================================== */
|
||||||
|
.cash-accounts-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-intro {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-intro p {
|
||||||
|
margin: 0;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.95em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-summary-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-summary-card {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-summary-card h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-amounts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-line, .stat-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-line .currency {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-line .amount {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-line span:first-child {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-line span:last-child {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-actions {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-account-form .form-group.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-account-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-account-form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-account-btn {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-account-btn:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accounts-list-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accounts-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-type-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: capitalize;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-detail-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-balance {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-actions-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-edit-btn,
|
||||||
|
.account-delete-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-edit-btn {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-edit-btn:hover {
|
||||||
|
background: #e0a800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-delete-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-delete-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal buttons */
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transfers Section Styles */
|
||||||
|
.transfers-section {
|
||||||
|
margin-top: 40px;
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfers-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-item:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-account {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-type {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-type.deposit {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-type.withdrawal {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-amount {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-amount.deposit {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-amount.withdrawal {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-amount.deposit::before {
|
||||||
|
content: '+';
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-amount.withdrawal::before {
|
||||||
|
content: '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standalone Transfer Form Page Styles */
|
||||||
|
.transfer-form-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-intro {
|
||||||
|
background: #e8f4fd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-intro p {
|
||||||
|
margin: 0;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.95em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-form {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-form h2 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-transfers {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-transfers h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design for Cash Accounts */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cash-summary-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-item {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-details {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-actions-buttons {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
Dashboard Cash Cards Styles
|
||||||
|
=========================================== */
|
||||||
|
.cash-breakdown {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-breakdown h3 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-summary-overview {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-total-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-amount-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-label {
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-amount-line .amount {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-stats-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-detail {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.cash-account-breakdown-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-account-breakdown-item:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-account-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-account-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-account-type {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-account-balance {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design for Dashboard Cash */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cash-summary-overview {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-stats-card {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cash-account-breakdown-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user