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:
kris 2025-09-01 17:37:41 +00:00
parent 4119143283
commit ae6b0ac80e
4 changed files with 51 additions and 22 deletions

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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 = `