1159 lines
44 KiB
JavaScript
1159 lines
44 KiB
JavaScript
|
|
class ETFTradeTracker {
|
|||
|
|
constructor() {
|
|||
|
|
this.trades = [];
|
|||
|
|
this.currentPrices = new Map(); // Store current market prices
|
|||
|
|
this.currentUser = null;
|
|||
|
|
this.apiUrl = '/api';
|
|||
|
|
this.initializeApp();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async initializeApp() {
|
|||
|
|
this.bindEvents();
|
|||
|
|
this.bindNavigation();
|
|||
|
|
this.bindAuthEvents();
|
|||
|
|
this.setDefaultDateTime();
|
|||
|
|
|
|||
|
|
// Check if user is logged in
|
|||
|
|
const isAuthenticated = await this.checkAuthentication();
|
|||
|
|
if (isAuthenticated) {
|
|||
|
|
await this.loadTrades();
|
|||
|
|
this.renderTrades();
|
|||
|
|
this.updateDashboard();
|
|||
|
|
this.showPage('dashboard');
|
|||
|
|
this.showAppContent();
|
|||
|
|
} else {
|
|||
|
|
this.showLoginPage();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bindEvents() {
|
|||
|
|
const form = document.getElementById('trade-form');
|
|||
|
|
const clearBtn = document.getElementById('clear-trades');
|
|||
|
|
const exportBtn = document.getElementById('export-trades');
|
|||
|
|
|
|||
|
|
form.addEventListener('submit', (e) => this.handleSubmit(e));
|
|||
|
|
clearBtn.addEventListener('click', () => this.clearAllTrades());
|
|||
|
|
exportBtn.addEventListener('click', () => this.exportTrades());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bindNavigation() {
|
|||
|
|
const menuItems = document.querySelectorAll('.menu-item');
|
|||
|
|
const sidebarToggle = document.querySelector('.sidebar-toggle');
|
|||
|
|
const sidebar = document.querySelector('.sidebar');
|
|||
|
|
|
|||
|
|
menuItems.forEach(item => {
|
|||
|
|
item.addEventListener('click', () => {
|
|||
|
|
const page = item.dataset.page;
|
|||
|
|
this.showPage(page);
|
|||
|
|
|
|||
|
|
// Update active menu item
|
|||
|
|
menuItems.forEach(mi => mi.classList.remove('active'));
|
|||
|
|
item.classList.add('active');
|
|||
|
|
|
|||
|
|
// Hide sidebar on mobile after selection
|
|||
|
|
if (window.innerWidth <= 768) {
|
|||
|
|
sidebar.classList.remove('open');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
sidebarToggle.addEventListener('click', () => {
|
|||
|
|
sidebar.classList.toggle('open');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Close sidebar when clicking outside on mobile
|
|||
|
|
document.addEventListener('click', (e) => {
|
|||
|
|
if (window.innerWidth <= 768 &&
|
|||
|
|
!sidebar.contains(e.target) &&
|
|||
|
|
!sidebarToggle.contains(e.target)) {
|
|||
|
|
sidebar.classList.remove('open');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showPage(pageId) {
|
|||
|
|
const pages = document.querySelectorAll('.page');
|
|||
|
|
const pageTitle = document.getElementById('page-title');
|
|||
|
|
|
|||
|
|
pages.forEach(page => {
|
|||
|
|
page.classList.remove('active');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const targetPage = document.getElementById(`${pageId}-page`);
|
|||
|
|
if (targetPage) {
|
|||
|
|
targetPage.classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update page title
|
|||
|
|
const titles = {
|
|||
|
|
'dashboard': 'Dashboard',
|
|||
|
|
'add-trade': 'Add Trade',
|
|||
|
|
'trade-history': 'Trade History',
|
|||
|
|
'portfolio': 'Portfolio',
|
|||
|
|
'gains-losses': 'Gains & Losses',
|
|||
|
|
'admin': 'Admin Panel'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
pageTitle.textContent = titles[pageId] || 'Dashboard';
|
|||
|
|
|
|||
|
|
// Update specific pages when shown
|
|||
|
|
if (pageId === 'portfolio') {
|
|||
|
|
this.renderPortfolioPage();
|
|||
|
|
} else if (pageId === 'gains-losses') {
|
|||
|
|
this.renderGainsLossesPage();
|
|||
|
|
} else if (pageId === 'admin') {
|
|||
|
|
this.renderAdminPage();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bindAuthEvents() {
|
|||
|
|
const loginForm = document.getElementById('login-form');
|
|||
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|||
|
|
const createUserForm = document.getElementById('create-user-form');
|
|||
|
|
|
|||
|
|
if (loginForm) {
|
|||
|
|
loginForm.addEventListener('submit', (e) => this.handleLogin(e));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (logoutBtn) {
|
|||
|
|
logoutBtn.addEventListener('click', () => this.handleLogout());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (createUserForm) {
|
|||
|
|
createUserForm.addEventListener('submit', (e) => this.handleCreateUser(e));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async checkAuthentication() {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/me`, {
|
|||
|
|
method: 'GET',
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.ok) {
|
|||
|
|
this.currentUser = await response.json();
|
|||
|
|
this.updateUserInfo();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.log('Not authenticated');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showLoginPage() {
|
|||
|
|
document.querySelector('.sidebar').style.display = 'none';
|
|||
|
|
document.getElementById('login-page').classList.add('active');
|
|||
|
|
document.getElementById('page-title').textContent = 'Login';
|
|||
|
|
|
|||
|
|
// Hide all other pages
|
|||
|
|
document.querySelectorAll('.page:not(#login-page)').forEach(page => {
|
|||
|
|
page.classList.remove('active');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showAppContent() {
|
|||
|
|
document.querySelector('.sidebar').style.display = 'block';
|
|||
|
|
document.getElementById('login-page').classList.remove('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleLogin(e) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
|
|||
|
|
const username = document.getElementById('login-username').value;
|
|||
|
|
const password = document.getElementById('login-password').value;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/login`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
credentials: 'include',
|
|||
|
|
body: JSON.stringify({ username, password })
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.ok) {
|
|||
|
|
this.currentUser = await response.json();
|
|||
|
|
this.updateUserInfo();
|
|||
|
|
this.showAppContent();
|
|||
|
|
|
|||
|
|
// Load user data
|
|||
|
|
await this.loadTrades();
|
|||
|
|
this.renderTrades();
|
|||
|
|
this.updateDashboard();
|
|||
|
|
this.showPage('dashboard');
|
|||
|
|
|
|||
|
|
this.showNotification('Login successful!', 'success');
|
|||
|
|
} else {
|
|||
|
|
const error = await response.json();
|
|||
|
|
this.showNotification(error.error || 'Login failed', 'error');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Login error:', error);
|
|||
|
|
this.showNotification('Login failed', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleLogout() {
|
|||
|
|
try {
|
|||
|
|
await fetch(`${this.apiUrl}/logout`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.currentUser = null;
|
|||
|
|
this.trades = [];
|
|||
|
|
this.currentPrices.clear();
|
|||
|
|
this.showLoginPage();
|
|||
|
|
this.showNotification('Logged out successfully', 'success');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Logout error:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateUserInfo() {
|
|||
|
|
const userInfo = document.getElementById('user-info');
|
|||
|
|
const currentUserSpan = document.getElementById('current-user');
|
|||
|
|
const adminMenus = document.querySelectorAll('.admin-only');
|
|||
|
|
|
|||
|
|
if (this.currentUser) {
|
|||
|
|
userInfo.style.display = 'block';
|
|||
|
|
currentUserSpan.textContent = `${this.currentUser.username} ${this.currentUser.isAdmin ? '(Admin)' : ''}`;
|
|||
|
|
|
|||
|
|
// Show/hide admin menus
|
|||
|
|
adminMenus.forEach(menu => {
|
|||
|
|
menu.style.display = this.currentUser.isAdmin ? 'block' : 'none';
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
userInfo.style.display = 'none';
|
|||
|
|
adminMenus.forEach(menu => {
|
|||
|
|
menu.style.display = 'none';
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleCreateUser(e) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
|
|||
|
|
const username = document.getElementById('new-username').value;
|
|||
|
|
const password = document.getElementById('new-password').value;
|
|||
|
|
const email = document.getElementById('new-email').value;
|
|||
|
|
const isAdmin = document.getElementById('new-is-admin').checked;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/admin/users`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
credentials: 'include',
|
|||
|
|
body: JSON.stringify({ username, password, email, isAdmin })
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.ok) {
|
|||
|
|
document.getElementById('create-user-form').reset();
|
|||
|
|
this.loadUsers(); // Refresh users list
|
|||
|
|
this.showNotification('User created successfully!', 'success');
|
|||
|
|
} else {
|
|||
|
|
const error = await response.json();
|
|||
|
|
this.showNotification(error.error || 'Failed to create user', 'error');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Create user error:', error);
|
|||
|
|
this.showNotification('Failed to create user', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async renderAdminPage() {
|
|||
|
|
if (!this.currentUser || !this.currentUser.isAdmin) {
|
|||
|
|
this.showNotification('Admin access required', 'error');
|
|||
|
|
this.showPage('dashboard');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.loadUsers();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async loadUsers() {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/admin/users`, {
|
|||
|
|
method: 'GET',
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.ok) {
|
|||
|
|
const users = await response.json();
|
|||
|
|
this.renderUsersList(users);
|
|||
|
|
} else {
|
|||
|
|
this.showNotification('Failed to load users', 'error');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Load users error:', error);
|
|||
|
|
this.showNotification('Failed to load users', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderUsersList(users) {
|
|||
|
|
const usersList = document.getElementById('users-list');
|
|||
|
|
|
|||
|
|
if (users.length === 0) {
|
|||
|
|
usersList.innerHTML = '<p class="no-data">No users found</p>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const usersHTML = users.map(user => {
|
|||
|
|
const createdDate = new Date(user.created_at).toLocaleDateString();
|
|||
|
|
const lastLogin = user.last_login ? new Date(user.last_login).toLocaleDateString() : 'Never';
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="user-item">
|
|||
|
|
<div class="user-header">
|
|||
|
|
<span class="user-username">${user.username}</span>
|
|||
|
|
<div class="user-badges">
|
|||
|
|
${user.is_admin ? '<span class="admin-badge">Admin</span>' : '<span class="user-badge">User</span>'}
|
|||
|
|
${user.id === this.currentUser.id ? '<span class="current-badge">You</span>' : ''}
|
|||
|
|
</div>
|
|||
|
|
${user.id !== this.currentUser.id ? `<button class="delete-user-btn" onclick="app.deleteUser(${user.id})">Delete</button>` : ''}
|
|||
|
|
</div>
|
|||
|
|
<div class="user-details">
|
|||
|
|
<div class="user-info">
|
|||
|
|
<span>Email: ${user.email || 'Not provided'}</span>
|
|||
|
|
<span>Created: ${createdDate}</span>
|
|||
|
|
<span>Last Login: ${lastLogin}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).join('');
|
|||
|
|
|
|||
|
|
usersList.innerHTML = usersHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async deleteUser(userId) {
|
|||
|
|
if (!confirm('Are you sure you want to delete this user? This will also delete all their trades.')) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/admin/users/${userId}`, {
|
|||
|
|
method: 'DELETE',
|
|||
|
|
credentials: 'include'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.ok) {
|
|||
|
|
this.loadUsers(); // Refresh users list
|
|||
|
|
this.showNotification('User deleted successfully', 'success');
|
|||
|
|
} else {
|
|||
|
|
const error = await response.json();
|
|||
|
|
this.showNotification(error.error || 'Failed to delete user', 'error');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Delete user error:', error);
|
|||
|
|
this.showNotification('Failed to delete user', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setDefaultDateTime() {
|
|||
|
|
const now = new Date();
|
|||
|
|
const dateInput = document.getElementById('trade-date');
|
|||
|
|
const timeInput = document.getElementById('trade-time');
|
|||
|
|
|
|||
|
|
dateInput.value = now.toISOString().split('T')[0];
|
|||
|
|
timeInput.value = now.toTimeString().split(' ')[0].substring(0, 5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async handleSubmit(e) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
|
|||
|
|
const formData = new FormData(e.target);
|
|||
|
|
const trade = this.createTradeFromForm(formData);
|
|||
|
|
|
|||
|
|
if (this.validateTrade(trade)) {
|
|||
|
|
await this.addTrade(trade);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createTradeFromForm(formData) {
|
|||
|
|
const etfSymbol = document.getElementById('etf-symbol').value.trim().toUpperCase();
|
|||
|
|
const tradeType = document.getElementById('trade-type').value;
|
|||
|
|
const shares = parseFloat(document.getElementById('shares').value);
|
|||
|
|
const price = parseFloat(document.getElementById('price').value);
|
|||
|
|
const currency = document.getElementById('currency').value;
|
|||
|
|
const tradeDate = document.getElementById('trade-date').value;
|
|||
|
|
const tradeTime = document.getElementById('trade-time').value;
|
|||
|
|
const fees = parseFloat(document.getElementById('fees').value) || 0;
|
|||
|
|
const notes = document.getElementById('notes').value.trim();
|
|||
|
|
|
|||
|
|
const dateTime = new Date(`${tradeDate}T${tradeTime}`);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
etfSymbol,
|
|||
|
|
tradeType,
|
|||
|
|
shares,
|
|||
|
|
price,
|
|||
|
|
currency,
|
|||
|
|
dateTime: dateTime.toISOString(),
|
|||
|
|
fees,
|
|||
|
|
notes
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
validateTrade(trade) {
|
|||
|
|
if (!trade.etfSymbol || !trade.tradeType || !trade.shares || !trade.price || !trade.currency) {
|
|||
|
|
this.showNotification('Please fill in all required fields', 'error');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (trade.shares <= 0 || trade.price <= 0) {
|
|||
|
|
this.showNotification('Shares and price must be greater than 0', 'error');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async addTrade(trade) {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/trades`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify(trade)
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
const error = await response.json();
|
|||
|
|
throw new Error(error.error || 'Failed to add trade');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const newTrade = await response.json();
|
|||
|
|
this.trades.unshift(newTrade);
|
|||
|
|
this.renderTrades();
|
|||
|
|
this.updateDashboard();
|
|||
|
|
this.resetForm();
|
|||
|
|
this.showNotification('Trade added successfully!', 'success');
|
|||
|
|
|
|||
|
|
// Navigate to dashboard after successful trade
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.showPage('dashboard');
|
|||
|
|
const menuItems = document.querySelectorAll('.menu-item');
|
|||
|
|
menuItems.forEach(mi => mi.classList.remove('active'));
|
|||
|
|
document.querySelector('[data-page="dashboard"]').classList.add('active');
|
|||
|
|
}, 1500);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error adding trade:', error);
|
|||
|
|
this.showNotification(error.message || 'Failed to add trade', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderTrades() {
|
|||
|
|
const tradesList = document.getElementById('trades-list');
|
|||
|
|
|
|||
|
|
if (this.trades.length === 0) {
|
|||
|
|
tradesList.innerHTML = '<p class="no-trades">No trades recorded yet. Add your first trade above!</p>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const tradesHTML = this.trades.map(trade => this.createTradeHTML(trade)).join('');
|
|||
|
|
tradesList.innerHTML = tradesHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createTradeHTML(trade) {
|
|||
|
|
const dateTime = new Date(trade.dateTime);
|
|||
|
|
const formattedDate = dateTime.toLocaleDateString();
|
|||
|
|
const formattedTime = dateTime.toTimeString().split(' ')[0];
|
|||
|
|
const currencySymbol = trade.currency === 'EUR' ? '€' : '$';
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="trade-item ${trade.tradeType}" data-id="${trade.id}">
|
|||
|
|
<div class="trade-header">
|
|||
|
|
<span class="etf-symbol">${trade.etfSymbol}</span>
|
|||
|
|
<span class="trade-type ${trade.tradeType}">${trade.tradeType.toUpperCase()}</span>
|
|||
|
|
<button class="delete-btn" onclick="app.deleteTrade('${trade.id}')">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="trade-details">
|
|||
|
|
<div class="trade-info">
|
|||
|
|
<span class="shares">${trade.shares} shares</span>
|
|||
|
|
<span class="price">${currencySymbol}${trade.price.toFixed(2)} per share</span>
|
|||
|
|
<span class="total-value">Total: ${currencySymbol}${trade.totalValue.toFixed(2)}</span>
|
|||
|
|
${trade.fees > 0 ? `<span class="fees">Fees: ${currencySymbol}${trade.fees.toFixed(2)}</span>` : ''}
|
|||
|
|
</div>
|
|||
|
|
<div class="trade-datetime">
|
|||
|
|
<span class="date">${formattedDate}</span>
|
|||
|
|
<span class="time">${formattedTime}</span>
|
|||
|
|
</div>
|
|||
|
|
${trade.notes ? `<div class="trade-notes">${trade.notes}</div>` : ''}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async deleteTrade(tradeId) {
|
|||
|
|
if (confirm('Are you sure you want to delete this trade?')) {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/trades/${tradeId}`, {
|
|||
|
|
method: 'DELETE'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
const error = await response.json();
|
|||
|
|
throw new Error(error.error || 'Failed to delete trade');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.trades = this.trades.filter(trade => trade.id !== tradeId);
|
|||
|
|
this.renderTrades();
|
|||
|
|
this.updateDashboard();
|
|||
|
|
this.showNotification('Trade deleted successfully!', 'success');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error deleting trade:', error);
|
|||
|
|
this.showNotification(error.message || 'Failed to delete trade', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async clearAllTrades() {
|
|||
|
|
if (confirm('Are you sure you want to delete all trades? This action cannot be undone.')) {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/trades`, {
|
|||
|
|
method: 'DELETE'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
const error = await response.json();
|
|||
|
|
throw new Error(error.error || 'Failed to clear trades');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.trades = [];
|
|||
|
|
this.renderTrades();
|
|||
|
|
this.updateDashboard();
|
|||
|
|
this.showNotification('All trades cleared!', 'success');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error clearing trades:', error);
|
|||
|
|
this.showNotification(error.message || 'Failed to clear trades', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
exportTrades() {
|
|||
|
|
if (this.trades.length === 0) {
|
|||
|
|
this.showNotification('No trades to export', 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const dataStr = JSON.stringify(this.trades, null, 2);
|
|||
|
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|||
|
|
|
|||
|
|
const link = document.createElement('a');
|
|||
|
|
link.href = URL.createObjectURL(dataBlob);
|
|||
|
|
link.download = `etf-trades-${new Date().toISOString().split('T')[0]}.json`;
|
|||
|
|
link.click();
|
|||
|
|
|
|||
|
|
this.showNotification('Trades exported successfully!', 'success');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resetForm() {
|
|||
|
|
document.getElementById('trade-form').reset();
|
|||
|
|
this.setDefaultDateTime();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async loadTrades() {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/trades`);
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error('Failed to load trades');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.trades = await response.json();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error loading trades:', error);
|
|||
|
|
this.showNotification('Failed to load trades from server', 'error');
|
|||
|
|
this.trades = [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showNotification(message, type = 'info') {
|
|||
|
|
const notification = document.createElement('div');
|
|||
|
|
notification.className = `notification ${type}`;
|
|||
|
|
notification.textContent = message;
|
|||
|
|
|
|||
|
|
document.body.appendChild(notification);
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
notification.classList.add('show');
|
|||
|
|
}, 100);
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
notification.classList.remove('show');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
document.body.removeChild(notification);
|
|||
|
|
}, 300);
|
|||
|
|
}, 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async getPortfolioSummary() {
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`${this.apiUrl}/portfolio-summary`);
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error('Failed to fetch portfolio summary');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return await response.json();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error fetching portfolio summary:', error);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateDashboard() {
|
|||
|
|
this.calculateDashboardMetrics();
|
|||
|
|
this.renderETFBreakdown();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateDashboardColors(totalGains) {
|
|||
|
|
const gainsCard = document.querySelector('.dashboard-card.total-gains');
|
|||
|
|
const currentValueCard = document.querySelector('.dashboard-card.total-value');
|
|||
|
|
const costBasisCard = document.querySelector('.dashboard-card.cost-basis');
|
|||
|
|
|
|||
|
|
if (gainsCard) {
|
|||
|
|
gainsCard.classList.remove('positive', 'negative', 'neutral');
|
|||
|
|
if (totalGains > 0) {
|
|||
|
|
gainsCard.classList.add('positive');
|
|||
|
|
} else if (totalGains < 0) {
|
|||
|
|
gainsCard.classList.add('negative');
|
|||
|
|
} else {
|
|||
|
|
gainsCard.classList.add('neutral');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Add subtle indicators to other cards
|
|||
|
|
if (currentValueCard) {
|
|||
|
|
currentValueCard.classList.remove('positive', 'negative');
|
|||
|
|
if (totalGains !== 0) {
|
|||
|
|
currentValueCard.classList.add(totalGains > 0 ? 'positive' : 'negative');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (costBasisCard) {
|
|||
|
|
costBasisCard.classList.remove('positive', 'negative');
|
|||
|
|
if (totalGains !== 0) {
|
|||
|
|
costBasisCard.classList.add(totalGains > 0 ? 'positive' : 'negative');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
calculateDashboardMetrics() {
|
|||
|
|
const now = new Date();
|
|||
|
|
const currentMonth = now.getMonth();
|
|||
|
|
const currentYear = now.getFullYear();
|
|||
|
|
|
|||
|
|
let totalCostBasis = 0;
|
|||
|
|
let totalCurrentValue = 0;
|
|||
|
|
let totalShares = 0;
|
|||
|
|
let monthlyInvestment = 0;
|
|||
|
|
let yearlyInvestment = 0;
|
|||
|
|
let monthlyTrades = 0;
|
|||
|
|
let yearlyTrades = 0;
|
|||
|
|
let uniqueETFs = new Set();
|
|||
|
|
let hasCurrentPrices = false;
|
|||
|
|
|
|||
|
|
// Calculate portfolio metrics using ETF positions
|
|||
|
|
const etfMap = this.getActiveETFPositions();
|
|||
|
|
etfMap.forEach((etf, symbol) => {
|
|||
|
|
totalCostBasis += etf.totalValue;
|
|||
|
|
totalShares += etf.shares;
|
|||
|
|
uniqueETFs.add(symbol);
|
|||
|
|
|
|||
|
|
// Calculate current value using updated prices or cost basis
|
|||
|
|
const currentPrice = this.currentPrices.get(symbol);
|
|||
|
|
if (currentPrice) {
|
|||
|
|
totalCurrentValue += etf.shares * currentPrice;
|
|||
|
|
hasCurrentPrices = true;
|
|||
|
|
} else {
|
|||
|
|
totalCurrentValue += etf.totalValue; // Use cost basis if no current price
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Calculate trade-based metrics (monthly/yearly)
|
|||
|
|
this.trades.forEach(trade => {
|
|||
|
|
const tradeDate = new Date(trade.dateTime);
|
|||
|
|
const tradeValue = trade.tradeType === 'buy' ? trade.totalValue : -trade.totalValue;
|
|||
|
|
|
|||
|
|
yearlyInvestment += tradeValue;
|
|||
|
|
yearlyTrades++;
|
|||
|
|
|
|||
|
|
if (tradeDate.getMonth() === currentMonth && tradeDate.getFullYear() === currentYear) {
|
|||
|
|
monthlyInvestment += tradeValue;
|
|||
|
|
monthlyTrades++;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Calculate gains/losses
|
|||
|
|
const totalGains = totalCurrentValue - totalCostBasis;
|
|||
|
|
const gainsPercentage = totalCostBasis > 0 ? ((totalGains / totalCostBasis) * 100) : 0;
|
|||
|
|
const avgReturn = gainsPercentage;
|
|||
|
|
|
|||
|
|
// Update dashboard elements
|
|||
|
|
document.getElementById('dashboard-current-value').textContent = this.formatCurrency(totalCurrentValue);
|
|||
|
|
document.getElementById('dashboard-total-gains').textContent = this.formatCurrency(totalGains);
|
|||
|
|
document.getElementById('dashboard-gains-percentage').textContent = `${totalGains >= 0 ? '+' : ''}${gainsPercentage.toFixed(1)}%`;
|
|||
|
|
document.getElementById('dashboard-cost-basis').textContent = this.formatCurrency(totalCostBasis);
|
|||
|
|
document.getElementById('dashboard-avg-return').textContent = `Avg return: ${avgReturn >= 0 ? '+' : ''}${avgReturn.toFixed(1)}%`;
|
|||
|
|
|
|||
|
|
document.getElementById('total-shares').textContent = totalShares.toFixed(3);
|
|||
|
|
document.getElementById('unique-etfs').textContent = `${uniqueETFs.size} ETFs`;
|
|||
|
|
document.getElementById('monthly-investment').textContent = this.formatCurrency(monthlyInvestment);
|
|||
|
|
document.getElementById('monthly-trades').textContent = `${monthlyTrades} trades`;
|
|||
|
|
document.getElementById('yearly-investment').textContent = this.formatCurrency(yearlyInvestment);
|
|||
|
|
document.getElementById('yearly-trades').textContent = `${yearlyTrades} trades`;
|
|||
|
|
|
|||
|
|
// Update status indicators
|
|||
|
|
const lastUpdatedElement = document.getElementById('dashboard-last-updated');
|
|||
|
|
if (hasCurrentPrices) {
|
|||
|
|
lastUpdatedElement.textContent = `Updated: ${new Date().toLocaleString()}`;
|
|||
|
|
} else {
|
|||
|
|
lastUpdatedElement.textContent = 'Using cost basis - update prices in Gains/Losses';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update dashboard card colors for gains/losses
|
|||
|
|
this.updateDashboardColors(totalGains);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderETFBreakdown() {
|
|||
|
|
const etfMap = new Map();
|
|||
|
|
|
|||
|
|
this.trades.forEach(trade => {
|
|||
|
|
if (!etfMap.has(trade.etfSymbol)) {
|
|||
|
|
etfMap.set(trade.etfSymbol, {
|
|||
|
|
symbol: trade.etfSymbol,
|
|||
|
|
shares: 0,
|
|||
|
|
totalValue: 0,
|
|||
|
|
currency: trade.currency,
|
|||
|
|
trades: 0
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const etf = etfMap.get(trade.etfSymbol);
|
|||
|
|
const multiplier = trade.tradeType === 'buy' ? 1 : -1;
|
|||
|
|
|
|||
|
|
etf.shares += trade.shares * multiplier;
|
|||
|
|
etf.totalValue += trade.totalValue * multiplier;
|
|||
|
|
etf.trades++;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const breakdownList = document.getElementById('etf-breakdown-list');
|
|||
|
|
|
|||
|
|
if (etfMap.size === 0) {
|
|||
|
|
breakdownList.innerHTML = '<p class="no-data">No ETF positions yet</p>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const etfEntries = Array.from(etfMap.values()).filter(etf => etf.shares > 0);
|
|||
|
|
|
|||
|
|
if (etfEntries.length === 0) {
|
|||
|
|
breakdownList.innerHTML = '<p class="no-data">No current ETF positions</p>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const breakdownHTML = etfEntries.map(etf => {
|
|||
|
|
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
|||
|
|
const avgPrice = etf.shares > 0 ? etf.totalValue / etf.shares : 0;
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="etf-item">
|
|||
|
|
<div class="etf-header">
|
|||
|
|
<span class="etf-symbol">${etf.symbol}</span>
|
|||
|
|
<span class="etf-value">${currencySymbol}${etf.totalValue.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="etf-details">
|
|||
|
|
<span class="etf-shares">${etf.shares.toFixed(3)} shares</span>
|
|||
|
|
<span class="etf-avg-price">Avg: ${currencySymbol}${avgPrice.toFixed(2)}</span>
|
|||
|
|
<span class="etf-trades">${etf.trades} trades</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).join('');
|
|||
|
|
|
|||
|
|
breakdownList.innerHTML = breakdownHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderPortfolioPage() {
|
|||
|
|
const portfolioSummary = document.getElementById('portfolio-summary');
|
|||
|
|
const etfMap = new Map();
|
|||
|
|
|
|||
|
|
this.trades.forEach(trade => {
|
|||
|
|
if (!etfMap.has(trade.etfSymbol)) {
|
|||
|
|
etfMap.set(trade.etfSymbol, {
|
|||
|
|
symbol: trade.etfSymbol,
|
|||
|
|
shares: 0,
|
|||
|
|
totalValue: 0,
|
|||
|
|
currency: trade.currency,
|
|||
|
|
trades: 0,
|
|||
|
|
avgPrice: 0
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const etf = etfMap.get(trade.etfSymbol);
|
|||
|
|
const multiplier = trade.tradeType === 'buy' ? 1 : -1;
|
|||
|
|
|
|||
|
|
etf.shares += trade.shares * multiplier;
|
|||
|
|
etf.totalValue += trade.totalValue * multiplier;
|
|||
|
|
etf.trades++;
|
|||
|
|
etf.avgPrice = etf.shares > 0 ? etf.totalValue / etf.shares : 0;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const activeETFs = Array.from(etfMap.values()).filter(etf => etf.shares > 0);
|
|||
|
|
|
|||
|
|
if (activeETFs.length === 0) {
|
|||
|
|
portfolioSummary.innerHTML = '<p class="no-data">No active positions in your portfolio</p>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let totalPortfolioValue = 0;
|
|||
|
|
activeETFs.forEach(etf => {
|
|||
|
|
totalPortfolioValue += etf.totalValue;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const portfolioHTML = `
|
|||
|
|
<div class="portfolio-overview">
|
|||
|
|
<h3>Portfolio Overview</h3>
|
|||
|
|
<div class="overview-stats">
|
|||
|
|
<div class="stat-item">
|
|||
|
|
<span class="stat-label">Total Value</span>
|
|||
|
|
<span class="stat-value">${this.formatCurrency(totalPortfolioValue)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-item">
|
|||
|
|
<span class="stat-label">Active ETFs</span>
|
|||
|
|
<span class="stat-value">${activeETFs.length}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-item">
|
|||
|
|
<span class="stat-label">Total Trades</span>
|
|||
|
|
<span class="stat-value">${this.trades.length}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="portfolio-holdings">
|
|||
|
|
<h3>Holdings</h3>
|
|||
|
|
<div class="holdings-grid">
|
|||
|
|
${activeETFs.map(etf => {
|
|||
|
|
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
|||
|
|
const allocation = ((etf.totalValue / totalPortfolioValue) * 100).toFixed(1);
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="holding-card">
|
|||
|
|
<div class="holding-header">
|
|||
|
|
<span class="holding-symbol">${etf.symbol}</span>
|
|||
|
|
<span class="holding-allocation">${allocation}%</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="holding-details">
|
|||
|
|
<div class="holding-stat">
|
|||
|
|
<span>Shares</span>
|
|||
|
|
<span>${etf.shares.toFixed(3)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="holding-stat">
|
|||
|
|
<span>Value</span>
|
|||
|
|
<span>${currencySymbol}${etf.totalValue.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="holding-stat">
|
|||
|
|
<span>Avg Price</span>
|
|||
|
|
<span>${currencySymbol}${etf.avgPrice.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="holding-stat">
|
|||
|
|
<span>Trades</span>
|
|||
|
|
<span>${etf.trades}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).join('')}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
portfolioSummary.innerHTML = portfolioHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderGainsLossesPage() {
|
|||
|
|
this.renderPriceUpdateList();
|
|||
|
|
this.calculateAndDisplayPerformance();
|
|||
|
|
this.bindGainsLossesEvents();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bindGainsLossesEvents() {
|
|||
|
|
const updateAllBtn = document.getElementById('update-all-prices');
|
|||
|
|
if (updateAllBtn && !updateAllBtn.hasEventListener) {
|
|||
|
|
updateAllBtn.addEventListener('click', () => this.updateAllPrices());
|
|||
|
|
updateAllBtn.hasEventListener = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderPriceUpdateList() {
|
|||
|
|
const priceUpdateList = document.getElementById('price-update-list');
|
|||
|
|
const etfMap = this.getActiveETFPositions();
|
|||
|
|
|
|||
|
|
if (etfMap.size === 0) {
|
|||
|
|
priceUpdateList.innerHTML = '<p class="no-data">No ETF positions to update</p>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateHTML = Array.from(etfMap.entries()).map(([symbol, etf]) => {
|
|||
|
|
const currentPrice = this.currentPrices.get(symbol) || '';
|
|||
|
|
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="price-update-item">
|
|||
|
|
<div class="price-update-header">
|
|||
|
|
<span class="etf-symbol-update">${symbol}</span>
|
|||
|
|
<span class="etf-position">${etf.shares.toFixed(3)} shares</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="price-update-controls">
|
|||
|
|
<label>Current Price (${currencySymbol})</label>
|
|||
|
|
<input type="number"
|
|||
|
|
step="0.01"
|
|||
|
|
min="0"
|
|||
|
|
placeholder="0.00"
|
|||
|
|
value="${currentPrice}"
|
|||
|
|
class="current-price-input"
|
|||
|
|
data-symbol="${symbol}">
|
|||
|
|
<button class="update-price-btn" data-symbol="${symbol}">Update</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="price-info">
|
|||
|
|
<span>Avg Cost: ${currencySymbol}${etf.avgPrice.toFixed(2)}</span>
|
|||
|
|
<span class="price-change" data-symbol="${symbol}">-</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).join('');
|
|||
|
|
|
|||
|
|
priceUpdateList.innerHTML = updateHTML;
|
|||
|
|
|
|||
|
|
// Bind individual update events
|
|||
|
|
document.querySelectorAll('.update-price-btn').forEach(btn => {
|
|||
|
|
btn.addEventListener('click', (e) => {
|
|||
|
|
const symbol = e.target.dataset.symbol;
|
|||
|
|
const input = document.querySelector(`input[data-symbol="${symbol}"]`);
|
|||
|
|
const price = parseFloat(input.value);
|
|||
|
|
|
|||
|
|
if (price > 0) {
|
|||
|
|
this.updateETFPrice(symbol, price);
|
|||
|
|
} else {
|
|||
|
|
this.showNotification('Please enter a valid price', 'error');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Update on Enter key
|
|||
|
|
document.querySelectorAll('.current-price-input').forEach(input => {
|
|||
|
|
input.addEventListener('keypress', (e) => {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
const symbol = e.target.dataset.symbol;
|
|||
|
|
const price = parseFloat(e.target.value);
|
|||
|
|
|
|||
|
|
if (price > 0) {
|
|||
|
|
this.updateETFPrice(symbol, price);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getActiveETFPositions() {
|
|||
|
|
const etfMap = new Map();
|
|||
|
|
|
|||
|
|
this.trades.forEach(trade => {
|
|||
|
|
if (!etfMap.has(trade.etfSymbol)) {
|
|||
|
|
etfMap.set(trade.etfSymbol, {
|
|||
|
|
symbol: trade.etfSymbol,
|
|||
|
|
shares: 0,
|
|||
|
|
totalValue: 0,
|
|||
|
|
currency: trade.currency,
|
|||
|
|
trades: 0,
|
|||
|
|
avgPrice: 0
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const etf = etfMap.get(trade.etfSymbol);
|
|||
|
|
const multiplier = trade.tradeType === 'buy' ? 1 : -1;
|
|||
|
|
|
|||
|
|
etf.shares += trade.shares * multiplier;
|
|||
|
|
etf.totalValue += trade.totalValue * multiplier;
|
|||
|
|
etf.trades++;
|
|||
|
|
etf.avgPrice = etf.shares > 0 ? etf.totalValue / etf.shares : 0;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Filter out positions with 0 or negative shares
|
|||
|
|
const activePositions = new Map();
|
|||
|
|
etfMap.forEach((etf, symbol) => {
|
|||
|
|
if (etf.shares > 0) {
|
|||
|
|
activePositions.set(symbol, etf);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
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');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updatePriceChangeIndicator(symbol, currentPrice) {
|
|||
|
|
const etfMap = this.getActiveETFPositions();
|
|||
|
|
const etf = etfMap.get(symbol);
|
|||
|
|
|
|||
|
|
if (etf) {
|
|||
|
|
const changeAmount = currentPrice - etf.avgPrice;
|
|||
|
|
const changePercent = ((changeAmount / etf.avgPrice) * 100);
|
|||
|
|
const currencySymbol = etf.currency === 'EUR' ? '€' : '$';
|
|||
|
|
|
|||
|
|
const changeElement = document.querySelector(`[data-symbol="${symbol}"].price-change`);
|
|||
|
|
if (changeElement) {
|
|||
|
|
const changeClass = changeAmount >= 0 ? 'positive' : 'negative';
|
|||
|
|
const changeSign = changeAmount >= 0 ? '+' : '';
|
|||
|
|
|
|||
|
|
changeElement.className = `price-change ${changeClass}`;
|
|||
|
|
changeElement.textContent = `${changeSign}${currencySymbol}${changeAmount.toFixed(2)} (${changeSign}${changePercent.toFixed(1)}%)`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateAllPrices() {
|
|||
|
|
const inputs = document.querySelectorAll('.current-price-input');
|
|||
|
|
let updatedCount = 0;
|
|||
|
|
|
|||
|
|
inputs.forEach(input => {
|
|||
|
|
const price = parseFloat(input.value);
|
|||
|
|
if (price > 0) {
|
|||
|
|
this.updateETFPrice(input.dataset.symbol, price);
|
|||
|
|
updatedCount++;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (updatedCount > 0) {
|
|||
|
|
this.showNotification(`Updated ${updatedCount} ETF prices`, 'success');
|
|||
|
|
} else {
|
|||
|
|
this.showNotification('Please enter valid prices first', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
calculateAndDisplayPerformance() {
|
|||
|
|
const etfMap = this.getActiveETFPositions();
|
|||
|
|
let totalCost = 0;
|
|||
|
|
let totalCurrentValue = 0;
|
|||
|
|
let hasCurrentPrices = false;
|
|||
|
|
|
|||
|
|
etfMap.forEach((etf, symbol) => {
|
|||
|
|
totalCost += etf.totalValue;
|
|||
|
|
|
|||
|
|
const currentPrice = this.currentPrices.get(symbol);
|
|||
|
|
if (currentPrice) {
|
|||
|
|
totalCurrentValue += etf.shares * currentPrice;
|
|||
|
|
hasCurrentPrices = true;
|
|||
|
|
} else {
|
|||
|
|
totalCurrentValue += etf.totalValue; // Use cost basis if no current price
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const totalGainLoss = totalCurrentValue - totalCost;
|
|||
|
|
const totalPercentage = totalCost > 0 ? ((totalGainLoss / totalCost) * 100) : 0;
|
|||
|
|
|
|||
|
|
// Update performance summary
|
|||
|
|
document.getElementById('total-performance').textContent = this.formatCurrency(totalGainLoss);
|
|||
|
|
document.getElementById('total-percentage').textContent = `${totalPercentage >= 0 ? '+' : ''}${totalPercentage.toFixed(1)}%`;
|
|||
|
|
document.getElementById('unrealized-performance').textContent = this.formatCurrency(totalGainLoss);
|
|||
|
|
document.getElementById('unrealized-percentage').textContent = `${totalPercentage >= 0 ? '+' : ''}${totalPercentage.toFixed(1)}%`;
|
|||
|
|
document.getElementById('current-portfolio-value').textContent = this.formatCurrency(totalCurrentValue);
|
|||
|
|
|
|||
|
|
// Update performance card colors
|
|||
|
|
const performanceCards = document.querySelectorAll('.performance-card');
|
|||
|
|
performanceCards.forEach(card => {
|
|||
|
|
if (card.classList.contains('total-gains') || card.classList.contains('unrealized-gains')) {
|
|||
|
|
card.classList.remove('positive', 'negative');
|
|||
|
|
card.classList.add(totalGainLoss >= 0 ? 'positive' : 'negative');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Update last updated timestamp
|
|||
|
|
const lastUpdated = document.getElementById('last-updated');
|
|||
|
|
if (hasCurrentPrices) {
|
|||
|
|
lastUpdated.textContent = `Updated: ${new Date().toLocaleString()}`;
|
|||
|
|
} else {
|
|||
|
|
lastUpdated.textContent = 'Using cost basis - update prices';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.renderGainsBreakdown(etfMap);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
renderGainsBreakdown(etfMap) {
|
|||
|
|
const breakdownList = document.getElementById('gains-breakdown-list');
|
|||
|
|
|
|||
|
|
if (etfMap.size === 0) {
|
|||
|
|
breakdownList.innerHTML = '<p class="no-data">No active positions</p>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const breakdownHTML = Array.from(etfMap.entries()).map(([symbol, etf]) => {
|
|||
|
|
const currentPrice = this.currentPrices.get(symbol) || etf.avgPrice;
|
|||
|
|
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 performanceClass = gainLoss >= 0 ? 'positive' : 'negative';
|
|||
|
|
const hasRealPrice = this.currentPrices.has(symbol);
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="gains-breakdown-item ${performanceClass}">
|
|||
|
|
<div class="breakdown-header">
|
|||
|
|
<span class="breakdown-symbol">${symbol}</span>
|
|||
|
|
<span class="breakdown-performance">
|
|||
|
|
${gainLoss >= 0 ? '+' : ''}${currencySymbol}${gainLoss.toFixed(2)}
|
|||
|
|
(${gainLoss >= 0 ? '+' : ''}${percentage.toFixed(1)}%)
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="breakdown-details">
|
|||
|
|
<div class="breakdown-stat">
|
|||
|
|
<span>Shares</span>
|
|||
|
|
<span>${etf.shares.toFixed(3)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="breakdown-stat">
|
|||
|
|
<span>Avg Cost</span>
|
|||
|
|
<span>${currencySymbol}${etf.avgPrice.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="breakdown-stat">
|
|||
|
|
<span>Current Price</span>
|
|||
|
|
<span>${currencySymbol}${currentPrice.toFixed(2)} ${!hasRealPrice ? '(est.)' : ''}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="breakdown-stat">
|
|||
|
|
<span>Current Value</span>
|
|||
|
|
<span>${currencySymbol}${currentValue.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).join('');
|
|||
|
|
|
|||
|
|
breakdownList.innerHTML = breakdownHTML;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
formatCurrency(amount) {
|
|||
|
|
const absAmount = Math.abs(amount);
|
|||
|
|
if (absAmount >= 1000000) {
|
|||
|
|
return (amount >= 0 ? '€' : '-€') + (absAmount / 1000000).toFixed(2) + 'M';
|
|||
|
|
} else if (absAmount >= 1000) {
|
|||
|
|
return (amount >= 0 ? '€' : '-€') + (absAmount / 1000).toFixed(1) + 'K';
|
|||
|
|
} else {
|
|||
|
|
return (amount >= 0 ? '€' : '-€') + absAmount.toFixed(2);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const app = new ETFTradeTracker();
|