Add GBP currency support and update comprehensive documentation
- Add British Pound (GBP) support to all database schemas and constraints - Update API validation to accept EUR, USD, and GBP currencies - Implement centralized currency symbol mapping with getCurrencySymbol() - Replace all hardcoded currency mappings throughout frontend code - Add GBP options to all currency dropdown menus in UI forms - Update README with enhanced feature descriptions and changelog - Document multi-currency support and historical price tracking features - Improve project documentation with comprehensive feature overview 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4119143283
commit
ae6b0ac80e
21
README.md
21
README.md
@ -8,6 +8,10 @@ A web-based application for tracking ETF trades with multi-user authentication,
|
||||
- 💼 **Portfolio Management** - Track your ETF positions, shares, and investments
|
||||
- 📊 **Trade History** - Complete record of all buy/sell transactions
|
||||
- 📈 **Gains/Losses Analysis** - Calculate performance with current market prices
|
||||
- 📉 **Historical Price Tracking** - Store and view price update history with timestamps
|
||||
- 💰 **Multi-Currency Support** - Full support for EUR, USD, and GBP currencies
|
||||
- 🏦 **Cash Account Management** - Track savings accounts and cash transfers
|
||||
- 📊 **Capital Gains Tax (CGT)** - Automatic CGT calculations with long-term rates
|
||||
- 🎯 **Dashboard** - Overview of portfolio metrics and performance
|
||||
- 👥 **Admin Panel** - User management for administrators
|
||||
- 📱 **Responsive Design** - Works on desktop and mobile devices
|
||||
@ -19,7 +23,7 @@ A web-based application for tracking ETF trades with multi-user authentication,
|
||||
Clean overview of your portfolio with key metrics and ETF breakdown.
|
||||
|
||||
### Trade Entry
|
||||
Simple form to add new ETF trades with date/time and currency support (EUR/USD).
|
||||
Simple form to add new ETF trades with date/time and currency support (EUR/USD/GBP).
|
||||
|
||||
### Gains/Losses
|
||||
Update current market prices to see real-time portfolio performance.
|
||||
@ -200,7 +204,7 @@ When you first run the application, a default admin user is created:
|
||||
- `trade_type` - 'buy' or 'sell'
|
||||
- `shares` - Number of shares
|
||||
- `price` - Price per share
|
||||
- `currency` - 'EUR' or 'USD'
|
||||
- `currency` - 'EUR', 'USD', or 'GBP'
|
||||
- `trade_datetime` - Date and time of trade
|
||||
- `fees` - Optional trading fees
|
||||
- `notes` - Optional trade notes
|
||||
@ -269,6 +273,19 @@ For issues or questions:
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.2.0 (Latest)
|
||||
- **Historical Price Tracking**: Store and view price update history for all ETFs
|
||||
- **Price History Modal**: View timeline of price updates with timestamps
|
||||
- **Persistent Pricing**: Automatically load latest prices on app startup
|
||||
- **Multi-Currency Support**: Added British Pound (GBP) alongside EUR/USD
|
||||
- **Enhanced UX**: Improved price update notifications and database persistence
|
||||
|
||||
### v1.1.0
|
||||
- Comprehensive cash savings and transfers system
|
||||
- Capital Gains Tax (CGT) calculations with 8+ year long-term rates
|
||||
- Total holdings tracking across ETFs and cash accounts
|
||||
- Enhanced portfolio analytics and reporting
|
||||
|
||||
### v1.0.0 (Initial Release)
|
||||
- Multi-user authentication system
|
||||
- ETF trade tracking
|
||||
|
||||
@ -222,6 +222,7 @@
|
||||
<option value="">Select currency</option>
|
||||
<option value="EUR">Euro (€)</option>
|
||||
<option value="USD">US Dollar ($)</option>
|
||||
<option value="GBP">British Pound (£)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -448,6 +449,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \<br>
|
||||
<select id="account-currency">
|
||||
<option value="EUR" selected>EUR (€)</option>
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="GBP">GBP (£)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -509,6 +511,7 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \<br>
|
||||
<select id="edit-account-currency">
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="GBP">GBP (£)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
37
script.js
37
script.js
@ -482,7 +482,7 @@ class ETFTradeTracker {
|
||||
const dateTime = new Date(trade.dateTime);
|
||||
const formattedDate = dateTime.toLocaleDateString();
|
||||
const formattedTime = dateTime.toTimeString().split(' ')[0];
|
||||
const currencySymbol = trade.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(trade.currency);
|
||||
|
||||
return `
|
||||
<div class="trade-item ${trade.tradeType}" data-id="${trade.id}">
|
||||
@ -781,7 +781,7 @@ class ETFTradeTracker {
|
||||
}
|
||||
|
||||
const breakdownHTML = etfEntries.map(etf => {
|
||||
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(etf.currency);
|
||||
const avgPrice = etf.shares > 0 ? etf.totalValue / etf.shares : 0;
|
||||
|
||||
return `
|
||||
@ -936,7 +936,7 @@ class ETFTradeTracker {
|
||||
|
||||
accountsList.innerHTML = accounts.slice(0, 5).map(account => {
|
||||
const balance = parseFloat(account.balance);
|
||||
const currencySymbol = account.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(account.currency);
|
||||
const formattedBalance = `${currencySymbol}${balance.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
|
||||
|
||||
return `
|
||||
@ -1023,7 +1023,7 @@ class ETFTradeTracker {
|
||||
<h3>Holdings</h3>
|
||||
<div class="holdings-grid">
|
||||
${activeETFs.map(etf => {
|
||||
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(etf.currency);
|
||||
const allocation = ((etf.totalValue / totalPortfolioValue) * 100).toFixed(1);
|
||||
|
||||
return `
|
||||
@ -1085,7 +1085,7 @@ class ETFTradeTracker {
|
||||
|
||||
const updateHTML = Array.from(etfMap.entries()).map(([symbol, etf]) => {
|
||||
const currentPrice = this.currentPrices.get(symbol) || '';
|
||||
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(etf.currency);
|
||||
|
||||
return `
|
||||
<div class="price-update-item">
|
||||
@ -1280,7 +1280,7 @@ class ETFTradeTracker {
|
||||
if (etf) {
|
||||
const changeAmount = currentPrice - etf.avgPrice;
|
||||
const changePercent = ((changeAmount / etf.avgPrice) * 100);
|
||||
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(etf.currency);
|
||||
|
||||
const changeElement = document.querySelector(`[data-symbol="${symbol}"].price-change`);
|
||||
if (changeElement) {
|
||||
@ -1379,7 +1379,7 @@ class ETFTradeTracker {
|
||||
const currentValue = etf.shares * currentPrice;
|
||||
const gainLoss = currentValue - etf.totalValue;
|
||||
const percentage = etf.totalValue > 0 ? ((gainLoss / etf.totalValue) * 100) : 0;
|
||||
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(etf.currency);
|
||||
|
||||
const performanceClass = gainLoss >= 0 ? 'positive' : 'negative';
|
||||
const hasRealPrice = this.currentPrices.has(symbol);
|
||||
@ -1611,7 +1611,7 @@ class ETFTradeTracker {
|
||||
}
|
||||
|
||||
const breakdownHTML = eligiblePositions.map(position => {
|
||||
const currencySymbol = position.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(position.currency);
|
||||
const gainsClass = position.gains >= 0 ? 'positive' : 'negative';
|
||||
const performanceClass = position.gains >= 0 ? 'positive' : 'negative';
|
||||
const gainsPercentage = position.cost > 0 ? ((position.gains / position.cost) * 100) : 0;
|
||||
@ -1993,7 +1993,7 @@ class ETFTradeTracker {
|
||||
accountsList.innerHTML = accounts.map(account => {
|
||||
const createdDate = new Date(account.created_at).toLocaleDateString();
|
||||
const balance = parseFloat(account.balance);
|
||||
const currencySymbol = account.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(account.currency);
|
||||
const formattedBalance = `${currencySymbol}${balance.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
|
||||
|
||||
const accountTypeMap = {
|
||||
@ -2267,7 +2267,7 @@ class ETFTradeTracker {
|
||||
transfersList.innerHTML = transfers.map(transfer => {
|
||||
const transferDate = new Date(transfer.transfer_date).toLocaleDateString();
|
||||
const amount = parseFloat(transfer.amount);
|
||||
const currencySymbol = transfer.currency === 'USD' ? '$' : '€';
|
||||
const currencySymbol = this.getCurrencySymbol(transfer.currency);
|
||||
|
||||
return `
|
||||
<div class="transfer-item">
|
||||
@ -2305,7 +2305,7 @@ class ETFTradeTracker {
|
||||
// Add active accounts
|
||||
const activeAccounts = accounts.filter(account => account.is_active);
|
||||
activeAccounts.forEach(account => {
|
||||
const currencySymbol = account.currency === 'USD' ? '$' : '€';
|
||||
const currencySymbol = this.getCurrencySymbol(account.currency);
|
||||
const option = document.createElement('option');
|
||||
option.value = account.id;
|
||||
option.textContent = `${account.account_name} (${currencySymbol}${parseFloat(account.balance).toFixed(2)})`;
|
||||
@ -2422,7 +2422,7 @@ class ETFTradeTracker {
|
||||
// Add active accounts
|
||||
const activeAccounts = accounts.filter(account => account.is_active);
|
||||
activeAccounts.forEach(account => {
|
||||
const currencySymbol = account.currency === 'USD' ? '$' : '€';
|
||||
const currencySymbol = this.getCurrencySymbol(account.currency);
|
||||
const option = document.createElement('option');
|
||||
option.value = account.id;
|
||||
option.textContent = `${account.account_name} (${currencySymbol}${parseFloat(account.balance).toFixed(2)})`;
|
||||
@ -2466,7 +2466,7 @@ class ETFTradeTracker {
|
||||
transfersList.innerHTML = recentTransfers.map(transfer => {
|
||||
const transferDate = new Date(transfer.transfer_date).toLocaleDateString();
|
||||
const amount = parseFloat(transfer.amount);
|
||||
const currencySymbol = transfer.currency === 'USD' ? '$' : '€';
|
||||
const currencySymbol = this.getCurrencySymbol(transfer.currency);
|
||||
|
||||
return `
|
||||
<div class="transfer-item">
|
||||
@ -2804,7 +2804,7 @@ class ETFTradeTracker {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
const currencySymbol = history[0]?.currency === 'EUR' ? '€' : '$';
|
||||
const currencySymbol = this.getCurrencySymbol(history[0]?.currency);
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'price-history-modal';
|
||||
@ -2856,6 +2856,15 @@ class ETFTradeTracker {
|
||||
}
|
||||
}
|
||||
|
||||
getCurrencySymbol(currency) {
|
||||
switch(currency) {
|
||||
case 'EUR': return '€';
|
||||
case 'USD': return '$';
|
||||
case 'GBP': return '£';
|
||||
default: return currency;
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
|
||||
12
server.js
12
server.js
@ -65,7 +65,7 @@ function initializeDatabase() {
|
||||
trade_type TEXT NOT NULL CHECK (trade_type IN ('buy', 'sell')),
|
||||
shares REAL NOT NULL CHECK (shares > 0),
|
||||
price REAL NOT NULL CHECK (price > 0),
|
||||
currency TEXT NOT NULL CHECK (currency IN ('EUR', 'USD')),
|
||||
currency TEXT NOT NULL CHECK (currency IN ('EUR', 'USD', 'GBP')),
|
||||
trade_datetime TEXT NOT NULL,
|
||||
fees REAL DEFAULT 0 CHECK (fees >= 0),
|
||||
notes TEXT,
|
||||
@ -180,7 +180,7 @@ function initializeDatabase() {
|
||||
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')),
|
||||
currency TEXT NOT NULL DEFAULT 'EUR' CHECK (currency IN ('EUR', 'USD', 'GBP')),
|
||||
institution_name TEXT,
|
||||
interest_rate REAL DEFAULT 0 CHECK (interest_rate >= 0),
|
||||
notes TEXT,
|
||||
@ -230,7 +230,7 @@ function initializeDatabase() {
|
||||
user_id INTEGER NOT NULL,
|
||||
etf_symbol TEXT NOT NULL,
|
||||
price REAL NOT NULL CHECK (price > 0),
|
||||
currency TEXT NOT NULL CHECK (currency IN ('EUR', 'USD')),
|
||||
currency TEXT NOT NULL CHECK (currency IN ('EUR', 'USD', 'GBP')),
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
@ -990,7 +990,7 @@ app.post('/api/trades', requireAuthOrToken, (req, res) => {
|
||||
return res.status(400).json({ error: 'Invalid trade type' });
|
||||
}
|
||||
|
||||
if (!['EUR', 'USD'].includes(currency)) {
|
||||
if (!['EUR', 'USD', 'GBP'].includes(currency)) {
|
||||
return res.status(400).json({ error: 'Invalid currency' });
|
||||
}
|
||||
|
||||
@ -1232,8 +1232,8 @@ app.post('/api/price-history', requireAuthOrToken, (req, res) => {
|
||||
return res.status(400).json({ error: 'Price must be greater than 0' });
|
||||
}
|
||||
|
||||
if (!['EUR', 'USD'].includes(currency)) {
|
||||
return res.status(400).json({ error: 'Currency must be EUR or USD' });
|
||||
if (!['EUR', 'USD', 'GBP'].includes(currency)) {
|
||||
return res.status(400).json({ error: 'Currency must be EUR, USD, or GBP' });
|
||||
}
|
||||
|
||||
const sql = `
|
||||
|
||||
Loading…
Reference in New Issue
Block a user