diff --git a/script.js b/script.js
index 9f4dab9..0964b71 100644
--- a/script.js
+++ b/script.js
@@ -19,6 +19,7 @@ class ETFTradeTracker {
if (isAuthenticated) {
await this.loadTrades();
await this.loadCGTSettings();
+ await this.loadLatestPrices();
this.renderTrades();
this.updateDashboard();
this.showPage('dashboard');
@@ -1102,6 +1103,7 @@ class ETFTradeTracker {
class="current-price-input"
data-symbol="${symbol}">
+
Avg Cost: ${currencySymbol}${etf.avgPrice.toFixed(2)}
@@ -1128,6 +1130,14 @@ class ETFTradeTracker {
});
});
+ // Bind history view events
+ document.querySelectorAll('.view-history-btn').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ const symbol = e.target.dataset.symbol;
+ await this.showPriceHistory(symbol);
+ });
+ });
+
// Update on Enter key
document.querySelectorAll('.current-price-input').forEach(input => {
input.addEventListener('keypress', (e) => {
@@ -1178,12 +1188,89 @@ class ETFTradeTracker {
return activePositions;
}
- updateETFPrice(symbol, currentPrice) {
- this.currentPrices.set(symbol, currentPrice);
- this.calculateAndDisplayPerformance();
- this.updatePriceChangeIndicator(symbol, currentPrice);
- this.updateDashboard(); // Update dashboard when prices change
- this.showNotification(`Updated ${symbol} price to ${currentPrice}`, 'success');
+ async updateETFPrice(symbol, currentPrice) {
+ // Get currency for this ETF from trades
+ const etfMap = this.getActiveETFPositions();
+ const etf = etfMap.get(symbol);
+ const currency = etf ? etf.currency : 'USD';
+
+ try {
+ // Save to database
+ await this.savePriceToHistory(symbol, currentPrice, currency);
+
+ // Update in-memory price
+ this.currentPrices.set(symbol, currentPrice);
+ this.calculateAndDisplayPerformance();
+ this.updatePriceChangeIndicator(symbol, currentPrice);
+ this.updateDashboard();
+ this.showNotification(`Updated ${symbol} price to ${currentPrice} and saved to history`, 'success');
+ } catch (error) {
+ console.error('Error updating price:', error);
+ this.showNotification('Error updating price: ' + error.message, 'error');
+ }
+ }
+
+ async loadLatestPrices() {
+ try {
+ const response = await fetch(`${this.apiUrl}/latest-prices`, {
+ method: 'GET',
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const prices = await response.json();
+
+ // Load into memory
+ for (const [symbol, priceData] of Object.entries(prices)) {
+ this.currentPrices.set(symbol, priceData.price);
+ }
+
+ } catch (error) {
+ console.error('Error loading latest prices:', error);
+ }
+ }
+
+ async savePriceToHistory(etf_symbol, price, currency) {
+ const response = await fetch(`${this.apiUrl}/price-history`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ credentials: 'include',
+ body: JSON.stringify({
+ etf_symbol,
+ price: parseFloat(price),
+ currency
+ })
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to save price history');
+ }
+
+ return await response.json();
+ }
+
+ async loadPriceHistory(symbol, limit = 50) {
+ try {
+ const response = await fetch(`${this.apiUrl}/price-history/${symbol}?limit=${limit}`, {
+ method: 'GET',
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error loading price history:', error);
+ throw error;
+ }
}
updatePriceChangeIndicator(symbol, currentPrice) {
@@ -2692,6 +2779,83 @@ class ETFTradeTracker {
}
}
+ async showPriceHistory(symbol) {
+ try {
+ const history = await this.loadPriceHistory(symbol);
+
+ if (history.length === 0) {
+ this.showNotification(`No price history found for ${symbol}`, 'info');
+ return;
+ }
+
+ // Create modal for price history
+ this.createPriceHistoryModal(symbol, history);
+
+ } catch (error) {
+ console.error('Error showing price history:', error);
+ this.showNotification('Failed to load price history', 'error');
+ }
+ }
+
+ createPriceHistoryModal(symbol, history) {
+ // Remove existing modal if any
+ const existingModal = document.getElementById('price-history-modal');
+ if (existingModal) {
+ existingModal.remove();
+ }
+
+ const currencySymbol = history[0]?.currency === 'EUR' ? '€' : '$';
+
+ const modal = document.createElement('div');
+ modal.id = 'price-history-modal';
+ modal.className = 'modal';
+ modal.innerHTML = `
+
+
+
+
+ ${history.map(entry => `
+
+ ${currencySymbol}${parseFloat(entry.price).toFixed(2)}
+ ${new Date(entry.updatedAt).toLocaleString()}
+
+ `).join('')}
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+ modal.style.display = 'block';
+
+ // Close modal events
+ const closeBtn = modal.querySelector('.close-modal');
+ closeBtn.addEventListener('click', () => this.closePriceHistoryModal());
+
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ this.closePriceHistoryModal();
+ }
+ });
+
+ // Close on Escape key
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ this.closePriceHistoryModal();
+ }
+ });
+ }
+
+ closePriceHistoryModal() {
+ const modal = document.getElementById('price-history-modal');
+ if (modal) {
+ modal.remove();
+ }
+ }
+
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
diff --git a/server.js b/server.js
index 81abf1d..7f41229 100644
--- a/server.js
+++ b/server.js
@@ -222,6 +222,27 @@ function initializeDatabase() {
console.log('Cash transfers table ready');
}
});
+
+ // Create price history table
+ const createPriceHistoryTableSQL = `
+ CREATE TABLE IF NOT EXISTS price_history (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ 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')),
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+ )
+ `;
+
+ db.run(createPriceHistoryTableSQL, (err) => {
+ if (err) {
+ console.error('Error creating price history table:', err.message);
+ } else {
+ console.log('Price history table ready');
+ }
+ });
}
async function createDefaultAdmin() {
@@ -1199,6 +1220,107 @@ app.get('/api/portfolio-summary', requireAuthOrToken, (req, res) => {
});
});
+// Save price update to history
+app.post('/api/price-history', requireAuthOrToken, (req, res) => {
+ const { etf_symbol, price, currency } = req.body;
+
+ if (!etf_symbol || !price || !currency) {
+ return res.status(400).json({ error: 'Missing required fields: etf_symbol, price, currency' });
+ }
+
+ if (price <= 0) {
+ 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' });
+ }
+
+ const sql = `
+ INSERT INTO price_history (user_id, etf_symbol, price, currency)
+ VALUES (?, ?, ?, ?)
+ `;
+
+ db.run(sql, [req.session.userId, etf_symbol, price, currency], function(err) {
+ if (err) {
+ console.error('Error saving price history:', err.message);
+ res.status(500).json({ error: 'Failed to save price history' });
+ return;
+ }
+
+ res.json({
+ id: this.lastID,
+ etf_symbol,
+ price,
+ currency,
+ updated_at: new Date().toISOString()
+ });
+ });
+});
+
+// Get price history for a specific ETF
+app.get('/api/price-history/:symbol', requireAuthOrToken, (req, res) => {
+ const { symbol } = req.params;
+ const { limit = 50 } = req.query;
+
+ const sql = `
+ SELECT etf_symbol, price, currency, updated_at
+ FROM price_history
+ WHERE user_id = ? AND etf_symbol = ?
+ ORDER BY updated_at DESC
+ LIMIT ?
+ `;
+
+ db.all(sql, [req.session.userId, symbol, parseInt(limit)], (err, rows) => {
+ if (err) {
+ console.error('Error fetching price history:', err.message);
+ res.status(500).json({ error: 'Failed to fetch price history' });
+ return;
+ }
+
+ const history = rows.map(row => ({
+ symbol: row.etf_symbol,
+ price: row.price,
+ currency: row.currency,
+ updatedAt: row.updated_at
+ }));
+
+ res.json(history);
+ });
+});
+
+// Get latest prices for all ETFs
+app.get('/api/latest-prices', requireAuthOrToken, (req, res) => {
+ const sql = `
+ SELECT DISTINCT etf_symbol,
+ FIRST_VALUE(price) OVER (PARTITION BY etf_symbol ORDER BY updated_at DESC) as latest_price,
+ FIRST_VALUE(currency) OVER (PARTITION BY etf_symbol ORDER BY updated_at DESC) as currency,
+ FIRST_VALUE(updated_at) OVER (PARTITION BY etf_symbol ORDER BY updated_at DESC) as updated_at
+ FROM price_history
+ WHERE user_id = ?
+ ORDER BY etf_symbol
+ `;
+
+ db.all(sql, [req.session.userId], (err, rows) => {
+ if (err) {
+ console.error('Error fetching latest prices:', err.message);
+ res.status(500).json({ error: 'Failed to fetch latest prices' });
+ return;
+ }
+
+ const prices = {};
+ rows.forEach(row => {
+ prices[row.etf_symbol] = {
+ price: row.latest_price,
+ currency: row.currency,
+ updatedAt: row.updated_at
+ };
+ });
+
+ res.json(prices);
+ });
+});
+
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
diff --git a/styles.css b/styles.css
index 939ea05..9a45d09 100644
--- a/styles.css
+++ b/styles.css
@@ -2777,4 +2777,107 @@ body {
align-items: flex-start;
gap: 8px;
}
+}
+
+/* Price History Styles */
+.view-history-btn {
+ background: #007bff;
+ color: white;
+ border: none;
+ padding: 8px 12px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.85em;
+ margin-left: 8px;
+ transition: background-color 0.2s;
+}
+
+.view-history-btn:hover {
+ background: #0056b3;
+}
+
+.price-history-list {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ padding: 16px;
+}
+
+.price-history-entry {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px;
+ border-bottom: 1px solid #eee;
+ transition: background-color 0.2s;
+}
+
+.price-history-entry:last-child {
+ border-bottom: none;
+}
+
+.price-history-entry:hover {
+ background-color: #f8f9fa;
+}
+
+.history-price {
+ font-weight: 600;
+ font-size: 1.1em;
+ color: #007bff;
+}
+
+.history-date {
+ color: #666;
+ font-size: 0.9em;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 16px;
+}
+
+.modal-header h3 {
+ margin: 0;
+ color: #333;
+ font-size: 1.4em;
+}
+
+.close-modal {
+ font-size: 28px;
+ font-weight: bold;
+ color: #aaa;
+ cursor: pointer;
+ line-height: 1;
+}
+
+.close-modal:hover {
+ color: #333;
+}
+
+@media (max-width: 768px) {
+ .price-update-controls {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .view-history-btn {
+ margin-left: 0;
+ width: 100%;
+ }
+
+ .modal-content {
+ padding: 20px;
+ width: 95%;
+ }
+
+ .price-history-entry {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ }
}
\ No newline at end of file