Add dashboard top 5 accounts display and form cancel/add functionality

Features added:
- Dashboard now shows top 5 holdings, accounts, and subscriptions under each segment
- Quick action cards positioned underneath data cards for easy access
- Cancel/Add buttons added to all forms (trade, cash account, subscription)
- Auto-focus functionality when navigating from quick actions
- Responsive design optimized for mobile and desktop
- Consistent styling following existing design patterns

UI improvements:
- Card groups structure for better organization
- Top accounts sections with hover effects and proper formatting
- Form actions container with proper button alignment
- Mobile-responsive quick action cards and forms

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kris 2025-09-19 12:11:20 +00:00
parent 8a3631d5f5
commit 8f4899cf8b
3 changed files with 510 additions and 40 deletions

View File

@ -103,43 +103,81 @@
<!-- Dashboard Page --> <!-- Dashboard Page -->
<div id="dashboard-page" class="page"> <div id="dashboard-page" class="page">
<div class="dashboard-summary"> <div class="dashboard-summary">
<div class="summary-card portfolio-summary" onclick="app.navigateToPage('portfolio')"> <div class="card-group">
<div class="summary-header"> <div class="summary-card portfolio-summary">
<h3>Portfolio</h3> <div class="summary-header">
<h3>Portfolio</h3>
</div>
<div class="summary-content">
<div class="primary-value" id="dashboard-current-value">€0.00</div>
<div class="secondary-value" id="dashboard-total-gains">€0.00</div>
</div>
<div class="top-accounts-section">
<div class="top-accounts-title">Top Holdings</div>
<div id="dashboard-top-portfolio" class="top-accounts-list">
<div class="no-data-small">No positions yet</div>
</div>
</div>
</div> </div>
<div class="summary-content"> <div class="quick-action-card portfolio-action" onclick="app.navigateToPage('add-trade')">
<div class="primary-value" id="dashboard-current-value">€0.00</div> <div class="quick-action-icon"></div>
<div class="secondary-value" id="dashboard-total-gains">€0.00</div> <div class="quick-action-text">Add Trade</div>
</div> </div>
</div> </div>
<div class="summary-card cash-summary" onclick="app.navigateToPage('cash-accounts')"> <div class="card-group">
<div class="summary-header"> <div class="summary-card cash-summary">
<h3>Cash</h3> <div class="summary-header">
<h3>Cash</h3>
</div>
<div class="summary-content">
<div class="primary-value" id="dashboard-cash-total">€0.00</div>
<div class="secondary-value" id="dashboard-account-count">0 accounts</div>
</div>
<div class="top-accounts-section">
<div class="top-accounts-title">Top Accounts</div>
<div id="dashboard-top-cash" class="top-accounts-list">
<div class="no-data-small">No accounts yet</div>
</div>
</div>
</div> </div>
<div class="summary-content"> <div class="quick-action-card cash-action" onclick="app.navigateToPage('cash-accounts')">
<div class="primary-value" id="dashboard-cash-total">€0.00</div> <div class="quick-action-icon">💰</div>
<div class="secondary-value" id="dashboard-account-count">0 accounts</div> <div class="quick-action-text">Add Account</div>
</div> </div>
</div> </div>
<div class="summary-card subscriptions-summary" onclick="app.navigateToPage('subscriptions')"> <div class="card-group">
<div class="summary-header"> <div class="summary-card subscriptions-summary">
<h3>Subscriptions</h3> <div class="summary-header">
<h3>Subscriptions</h3>
</div>
<div class="summary-content">
<div class="primary-value" id="dashboard-subscription-monthly">€0.00/mo</div>
<div class="secondary-value" id="dashboard-subscription-count">0 services</div>
</div>
<div class="top-accounts-section">
<div class="top-accounts-title">Top Services</div>
<div id="dashboard-top-subscriptions" class="top-accounts-list">
<div class="no-data-small">No subscriptions yet</div>
</div>
</div>
</div> </div>
<div class="summary-content"> <div class="quick-action-card subscription-action" onclick="app.navigateToPage('add-subscription')">
<div class="primary-value" id="dashboard-subscription-monthly">€0.00/mo</div> <div class="quick-action-icon">📱</div>
<div class="secondary-value" id="dashboard-subscription-count">0 services</div> <div class="quick-action-text">Add Service</div>
</div> </div>
</div> </div>
<div class="summary-card total-summary"> <div class="card-group">
<div class="summary-header"> <div class="summary-card total-summary">
<h3>Total Value</h3> <div class="summary-header">
</div> <h3>Total Value</h3>
<div class="summary-content"> </div>
<div class="primary-value total" id="dashboard-total-combined">€0.00</div> <div class="summary-content">
<div class="secondary-value" id="dashboard-performance">0.0%</div> <div class="primary-value total" id="dashboard-total-combined">€0.00</div>
<div class="secondary-value" id="dashboard-performance">0.0%</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -203,8 +241,11 @@
<label for="notes">Notes (optional)</label> <label for="notes">Notes (optional)</label>
<textarea id="notes" rows="3" placeholder="Additional notes about this trade..."></textarea> <textarea id="notes" rows="3" placeholder="Additional notes about this trade..."></textarea>
</div> </div>
<button type="submit" class="submit-btn">Add Trade</button> <div class="form-actions">
<button type="button" class="cancel-btn" onclick="app.navigateToPage('dashboard')">Cancel</button>
<button type="submit" class="submit-btn">Add Trade</button>
</div>
</form> </form>
</div> </div>
</div> </div>
@ -427,7 +468,10 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \<br>
<textarea id="account-notes" placeholder="Additional notes about this account" rows="2"></textarea> <textarea id="account-notes" placeholder="Additional notes about this account" rows="2"></textarea>
</div> </div>
</div> </div>
<button type="submit" class="create-account-btn">Add Account</button> <div class="form-actions">
<button type="button" class="cancel-btn" onclick="app.navigateToPage('dashboard')">Cancel</button>
<button type="submit" class="create-account-btn">Add Account</button>
</div>
</form> </form>
</div> </div>
@ -1044,8 +1088,8 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \<br>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn-primary">Add Subscription</button> <button type="button" class="cancel-btn" onclick="app.navigateToPage('dashboard')">Cancel</button>
<button type="button" class="btn-secondary" onclick="app.showPage('subscriptions')">Cancel</button> <button type="submit" class="submit-btn">Add Subscription</button>
</div> </div>
</form> </form>
</div> </div>

172
script.js
View File

@ -137,12 +137,34 @@ class ETFTradeTracker {
this.renderTokensPage(); this.renderTokensPage();
} else if (pageId === 'cash-accounts') { } else if (pageId === 'cash-accounts') {
this.renderCashAccountsPage(); this.renderCashAccountsPage();
// Auto-scroll to add account form if coming from quick action
if (document.referrer === '' || window.location.hash === '#add-account') {
setTimeout(() => {
const addAccountForm = document.getElementById('create-account-form');
if (addAccountForm) {
addAccountForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
const firstInput = addAccountForm.querySelector('input[type="text"]');
if (firstInput) firstInput.focus();
}
}, 100);
}
} else if (pageId === 'add-transfer') { } else if (pageId === 'add-transfer') {
this.renderAddTransferPage(); this.renderAddTransferPage();
} else if (pageId === 'subscriptions') { } else if (pageId === 'subscriptions') {
this.renderSubscriptionsPage(); this.renderSubscriptionsPage();
} else if (pageId === 'add-subscription') { } else if (pageId === 'add-subscription') {
this.renderAddSubscriptionPage(); this.renderAddSubscriptionPage();
// Auto-focus first input when coming from quick action
setTimeout(() => {
const firstInput = document.getElementById('subscription-service-name');
if (firstInput) firstInput.focus();
}, 100);
} else if (pageId === 'add-trade') {
// Auto-focus first input when coming from quick action
setTimeout(() => {
const firstInput = document.getElementById('etf-symbol');
if (firstInput) firstInput.focus();
}, 100);
} else if (pageId === 'admin') { } else if (pageId === 'admin') {
this.renderAdminPage(); this.renderAdminPage();
} }
@ -768,6 +790,9 @@ class ETFTradeTracker {
document.getElementById('dashboard-total-combined').textContent = this.formatCurrency(totalValue, 'EUR'); document.getElementById('dashboard-total-combined').textContent = this.formatCurrency(totalValue, 'EUR');
document.getElementById('dashboard-performance').textContent = document.getElementById('dashboard-performance').textContent =
`${portfolioData.gainsPercentage >= 0 ? '+' : ''}${portfolioData.gainsPercentage.toFixed(1)}%`; `${portfolioData.gainsPercentage >= 0 ? '+' : ''}${portfolioData.gainsPercentage.toFixed(1)}%`;
// Load and display top accounts for each segment
this.loadDashboardTopAccounts();
} }
updateDashboardColors(totalGains) { updateDashboardColors(totalGains) {
@ -802,6 +827,153 @@ class ETFTradeTracker {
} }
} }
async loadDashboardTopAccounts() {
try {
// Load data for all segments in parallel
const [portfolioData, cashData, subscriptionData] = await Promise.all([
this.getTopPortfolioHoldings(),
this.getTopCashAccounts(),
this.getTopSubscriptions()
]);
// Render each segment
this.renderTopPortfolio(portfolioData);
this.renderTopCash(cashData);
this.renderTopSubscriptions(subscriptionData);
} catch (error) {
console.error('Error loading dashboard top accounts:', error);
}
}
getTopPortfolioHoldings() {
const etfMap = this.getActiveETFPositions();
const etfArray = Array.from(etfMap.values())
.filter(etf => etf.shares > 0)
.map(etf => {
const currentPrice = this.currentPrices.get(etf.symbol);
const currentValue = currentPrice ? etf.shares * currentPrice : etf.totalValue;
return {
name: etf.symbol,
value: currentValue,
details: `${etf.shares.toFixed(3)} shares`,
currency: etf.currency
};
})
.sort((a, b) => b.value - a.value)
.slice(0, 5);
return etfArray;
}
async getTopCashAccounts() {
try {
const response = await fetch(`${this.apiUrl}/cash-accounts`, {
credentials: 'include'
});
if (response.ok) {
const accounts = await response.json();
return accounts
.sort((a, b) => b.balance - a.balance)
.slice(0, 5)
.map(account => ({
name: account.name,
value: account.balance,
details: account.account_type,
currency: account.currency
}));
}
} catch (error) {
console.error('Error fetching top cash accounts:', error);
}
return [];
}
async getTopSubscriptions() {
try {
const response = await fetch(`${this.apiUrl}/subscriptions`, {
credentials: 'include'
});
if (response.ok) {
const subscriptions = await response.json();
return subscriptions
.sort((a, b) => {
const aCost = a.billingCycle === 'monthly' ? a.monthlyPrice : (a.annualPrice / 12);
const bCost = b.billingCycle === 'monthly' ? b.monthlyPrice : (b.annualPrice / 12);
return bCost - aCost;
})
.slice(0, 5)
.map(sub => ({
name: sub.serviceName,
value: sub.billingCycle === 'monthly' ? sub.monthlyPrice : (sub.annualPrice / 12),
details: sub.category,
currency: sub.currency
}));
}
} catch (error) {
console.error('Error fetching top subscriptions:', error);
}
return [];
}
renderTopPortfolio(holdings) {
const container = document.getElementById('dashboard-top-portfolio');
if (!holdings || holdings.length === 0) {
container.innerHTML = '<div class="no-data-small">No positions yet</div>';
return;
}
const html = holdings.map(holding => `
<div class="top-account-item">
<div class="top-account-name">${holding.name}</div>
<div class="top-account-details">${holding.details}</div>
<div class="top-account-value">${this.formatCurrency(holding.value, holding.currency)}</div>
</div>
`).join('');
container.innerHTML = html;
}
renderTopCash(accounts) {
const container = document.getElementById('dashboard-top-cash');
if (!accounts || accounts.length === 0) {
container.innerHTML = '<div class="no-data-small">No accounts yet</div>';
return;
}
const html = accounts.map(account => `
<div class="top-account-item">
<div class="top-account-name">${account.name}</div>
<div class="top-account-details">${account.details}</div>
<div class="top-account-value">${this.formatCurrency(account.value, account.currency)}</div>
</div>
`).join('');
container.innerHTML = html;
}
renderTopSubscriptions(subscriptions) {
const container = document.getElementById('dashboard-top-subscriptions');
if (!subscriptions || subscriptions.length === 0) {
container.innerHTML = '<div class="no-data-small">No subscriptions yet</div>';
return;
}
const html = subscriptions.map(sub => `
<div class="top-account-item">
<div class="top-account-name">${sub.name}</div>
<div class="top-account-details">${sub.details}</div>
<div class="top-account-value">${this.formatCurrency(sub.value, sub.currency)}/mo</div>
</div>
`).join('');
container.innerHTML = html;
}
calculateDashboardMetrics() { calculateDashboardMetrics() {
const now = new Date(); const now = new Date();
const currentMonth = now.getMonth(); const currentMonth = now.getMonth();

View File

@ -603,6 +603,13 @@ body {
margin: 0 auto; margin: 0 auto;
} }
/* Card Groups - Each group contains a data card and its quick action card */
.card-group {
display: flex;
flex-direction: column;
gap: 10px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.dashboard-summary { .dashboard-summary {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -625,19 +632,9 @@ body {
box-shadow: var(--shadow-light); box-shadow: var(--shadow-light);
border-left: 4px solid var(--accent-primary); border-left: 4px solid var(--accent-primary);
transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
cursor: pointer;
user-select: none; user-select: none;
} }
.summary-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-heavy);
background: var(--bg-tertiary);
}
.summary-card:active {
transform: translateY(-1px);
}
.summary-header { .summary-header {
margin-bottom: 15px; margin-bottom: 15px;
@ -702,6 +699,241 @@ body {
} }
} }
/* Top Accounts Section Styles */
.top-accounts-section {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid var(--border-light);
}
.top-accounts-title {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.top-accounts-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.top-account-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
font-size: 0.85rem;
border-bottom: 1px solid var(--border-light);
transition: background-color 0.2s ease;
}
.top-account-item:last-child {
border-bottom: none;
}
.top-account-item:hover {
background-color: var(--bg-tertiary);
margin: 0 -10px;
padding-left: 10px;
padding-right: 10px;
border-radius: 4px;
}
.top-account-name {
color: var(--text-primary);
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-account-details {
color: var(--text-muted);
font-size: 0.75rem;
margin-left: 8px;
}
.top-account-value {
color: var(--text-secondary);
font-weight: 600;
text-align: right;
min-width: 60px;
}
.no-data-small {
color: var(--text-muted);
font-size: 0.8rem;
text-align: center;
padding: 10px 0;
font-style: italic;
}
/* Responsive adjustments for top accounts */
@media (max-width: 768px) {
.top-accounts-section {
margin-top: 15px;
padding-top: 12px;
}
.top-account-item {
font-size: 0.8rem;
padding: 6px 0;
}
.top-account-details {
display: none;
}
.top-account-value {
min-width: 50px;
font-size: 0.8rem;
}
}
@media (max-width: 480px) {
.top-accounts-title {
font-size: 0.8rem;
margin-bottom: 8px;
}
.top-account-name {
font-size: 0.75rem;
}
.top-account-value {
font-size: 0.75rem;
min-width: 45px;
}
}
/* Quick Action Cards */
.quick-action-card {
background: var(--bg-secondary);
border-radius: 6px;
padding: 8px 12px;
border: 1px dashed var(--border-medium);
box-shadow: var(--shadow-light);
transition: all 0.2s ease;
cursor: pointer;
user-select: none;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
text-align: center;
position: relative;
overflow: hidden;
min-height: 35px;
gap: 6px;
}
.quick-action-card:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-light);
border-color: var(--accent-primary);
background: var(--bg-tertiary);
}
.quick-action-card:active {
transform: translateY(0px);
}
.quick-action-icon {
font-size: 0.9rem;
opacity: 0.8;
transition: all 0.2s ease;
}
.quick-action-text {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-secondary);
transition: color 0.2s ease;
}
.quick-action-card:hover .quick-action-icon {
opacity: 1;
}
.quick-action-card:hover .quick-action-text {
color: var(--text-primary);
}
/* Specific action card styling */
.portfolio-action {
border-color: var(--success);
}
.portfolio-action:hover {
border-color: var(--success);
}
.portfolio-action:hover .quick-action-text {
color: var(--success);
}
.cash-action {
border-color: var(--info);
}
.cash-action:hover {
border-color: var(--info);
}
.cash-action:hover .quick-action-text {
color: var(--info);
}
.subscription-action {
border-color: var(--accent-purple);
}
.subscription-action:hover {
border-color: var(--accent-purple);
}
.subscription-action:hover .quick-action-text {
color: var(--accent-purple);
}
/* Responsive adjustments for quick action cards */
@media (max-width: 768px) {
.quick-action-card {
padding: 6px 10px;
min-height: 30px;
gap: 5px;
}
.quick-action-icon {
font-size: 0.8rem;
}
.quick-action-text {
font-size: 0.7rem;
}
}
@media (max-width: 480px) {
.quick-action-card {
padding: 5px 8px;
min-height: 28px;
gap: 4px;
}
.quick-action-icon {
font-size: 0.75rem;
}
.quick-action-text {
font-size: 0.65rem;
}
}
.dashboard-card.total-value { .dashboard-card.total-value {
border-left-color: var(--success); border-left-color: var(--success);
} }
@ -1439,6 +1671,28 @@ body {
transform: translateY(0); transform: translateY(0);
} }
/* Form Actions Container */
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
align-items: center;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid var(--border-light);
}
@media (max-width: 480px) {
.form-actions {
flex-direction: column-reverse;
gap: 10px;
}
.form-actions button {
width: 100%;
}
}
.trades-controls { .trades-controls {
display: flex; display: flex;
gap: 15px; gap: 15px;