Files
Portfolio-Engine/internal/website/static/admin.html
2026-04-30 08:02:03 +02:00

583 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Portfolio Admin — Samantha Friis</title>
<link rel="stylesheet" href="/styles/admin">
</head>
<body>
<div class="app">
<!-- TOP BAR -->
<header class="topbar">
<a href="/" class="topbar-logo">Samantha <span>Friis</span></a>
<span class="topbar-tag">// portfolio admin</span>
<div class="topbar-spacer"></div>
<div class="status-pill">
<span class="dot" id="status-dot"></span>
<span id="status-text">live</span>
</div>
</header>
<!-- SIDEBAR -->
<nav class="sidebar">
<span class="nav-section-label">overview</span>
<button class="nav-item active" onclick="navigate('dashboard', this)">
<i class="nav-icon"></i> Dashboard
</button>
<button class="nav-item" onclick="navigate('positions', this)">
<i class="nav-icon"></i> Positions
</button>
<button class="nav-item" onclick="navigate('history', this)">
<i class="nav-icon"></i> Trade History
</button>
<span class="nav-section-label" style="margin-top:12px">add data</span>
<button class="nav-item" onclick="navigate('add-trade', this)">
<i class="nav-icon"></i> Add Trade
</button>
<button class="nav-item" onclick="navigate('add-company', this)">
<i class="nav-icon"></i> Add Company
</button>
</nav>
<!-- MAIN CONTENT -->
<main class="main">
<!-- ═══ DASHBOARD ═══ -->
<div class="page active" id="page-dashboard">
<div class="page-header">
<h1 class="page-title">Investment <span>Portfolio</span></h1>
<span class="page-subtitle">Equity positions &nbsp;·&nbsp; Personal research</span>
</div>
<div class="cards">
<div class="card">
<p class="card-label">Positions</p>
<p class="card-value accent" id="stat-positions"></p>
</div>
<div class="card">
<p class="card-label">Total Shares</p>
<p class="card-value" id="stat-shares"></p>
</div>
<div class="card">
<p class="card-label">Total Trades</p>
<p class="card-value" id="stat-trades"></p>
</div>
<div class="card">
<p class="card-label">Currencies</p>
<p class="card-value" id="stat-currencies"></p>
</div>
</div>
<p class="section-label">// <span>positions</span></p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Symbol</th><th>Currency</th><th>Shares</th>
<th>Cost Basis</th><th>Weight</th>
</tr>
</thead>
<tbody hx-get="/positions/fragment"
hx-trigger="load"
hx-swap="innerHTML">
<tr class="state-row"><td colspan="5">loading…</td></tr>
</tbody>
</table>
</div>
<p class="section-label" style="margin-top:28px">// <span>companies</span> — tracked universe</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Symbol</th><th>Currency</th><th>Price</th>
<th>Shares Outstanding</th><th>Market Cap</th>
</tr>
</thead>
<tbody id="companies-body">
<tr><td colspan="5" class="td-muted">loading…</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ POSITIONS ═══ -->
<div class="page" id="page-positions">
<div class="page-header">
<h1 class="page-title">Open <span>Positions</span></h1>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Symbol</th><th>Currency</th><th>Shares</th>
<th>Cost Basis</th><th>Weight</th>
</tr>
</thead>
<tbody hx-get="/positions/fragment"
hx-trigger="load"
hx-swap="innerHTML">
<tr class="state-row"><td colspan="5">loading…</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ HISTORY ═══ -->
<div class="page" id="page-history">
<div class="page-header">
<h1 class="page-title">Trade <span>History</span></h1>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Date</th><th>Symbol</th><th>Type</th>
<th>Shares</th><th>Price</th><th>Currency</th><th>Total</th>
</tr>
</thead>
<tbody id="history-body">
<tr><td colspan="7" class="td-muted">loading…</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ═══ ADD TRADE ═══ -->
<div class="page" id="page-add-trade">
<div class="page-header">
<h1 class="page-title">Add <span>Trade</span></h1>
<span class="page-subtitle">Record a buy, sell, or dividend</span>
</div>
<div class="panel">
<div class="panel-header">
<span class="panel-title">// <span>new trade</span></span>
<span class="tag-pill">POST /trades</span>
</div>
<div class="panel-body">
<div class="form-grid" id="trade-form">
<!-- Type toggle -->
<div class="form-group">
<label class="form-label">Trade type <span class="required">*</span></label>
<div class="type-toggle">
<button class="type-btn active-buy" data-val="0" onclick="setTradeType(0, this)">BUY</button>
<button class="type-btn" data-val="1" onclick="setTradeType(1, this)">SELL</button>
<button class="type-btn" data-val="2" onclick="setTradeType(2, this)">DIVIDEND</button>
</div>
<input type="hidden" id="trade-type" value="0"/>
</div>
<!-- Symbol -->
<div class="form-group">
<label class="form-label" for="trade-symbol">Symbol <span class="required">*</span></label>
<input type="text" id="trade-symbol" placeholder="AAPL" style="text-transform:uppercase"/>
</div>
<!-- Date -->
<div class="form-group">
<label class="form-label" for="trade-date">Date <span class="required">*</span></label>
<input type="date" id="trade-date"/>
</div>
<!-- Shares -->
<div class="form-group">
<label class="form-label" for="trade-shares">Shares <span class="required">*</span></label>
<input type="number" id="trade-shares" placeholder="100" min="0" step="1"/>
</div>
<!-- Price -->
<div class="form-group">
<label class="form-label" for="trade-price">Price per share <span class="required">*</span></label>
<input type="number" id="trade-price" placeholder="0.00" min="0" step="0.01"/>
</div>
<!-- Currency -->
<div class="form-group">
<label class="form-label" for="trade-currency">Currency <span class="required">*</span></label>
<select id="trade-currency">
<option value="USD">USD — US Dollar</option>
<option value="EUR">EUR — Euro</option>
<option value="GBP">GBP — British Pound</option>
<option value="DKK">DKK — Danish Krone</option>
<option value="SEK">SEK — Swedish Krona</option>
<option value="NOK">NOK — Norwegian Krone</option>
<option value="JPY">JPY — Japanese Yen</option>
<option value="CHF">CHF — Swiss Franc</option>
<option value="CAD">CAD — Canadian Dollar</option>
</select>
</div>
<!-- Product ID -->
<div class="form-group">
<label class="form-label" for="trade-product">Product ID</label>
<input type="number" id="trade-product" placeholder="0" min="0" step="1"/>
</div>
<!-- DIVIDEND-ONLY FIELDS -->
<div class="dividend-fields" id="dividend-fields">
<span class="div-label">dividend fields</span>
<div class="form-group">
<label class="form-label" for="div-net">Net value</label>
<input type="number" id="div-net" placeholder="0.00" step="0.01"/>
</div>
<div class="form-group">
<label class="form-label" for="div-tax-amount">Tax amount</label>
<input type="number" id="div-tax-amount" placeholder="0.00" step="0.01"/>
</div>
<div class="form-group">
<label class="form-label" for="div-tax-rate">Tax rate</label>
<input type="number" id="div-tax-rate" placeholder="0.00" step="0.0001" min="0" max="1"/>
</div>
<div class="form-group">
<label class="form-label" for="div-payment-date">Payment date</label>
<input type="date" id="div-payment-date"/>
</div>
</div>
<!-- Actions -->
<div class="form-actions">
<button class="btn btn-primary" onclick="submitTrade()">Submit trade →</button>
<button class="btn btn-ghost" onclick="resetTradeForm()">Clear</button>
<span class="feedback" id="trade-feedback"></span>
</div>
</div>
</div>
</div>
<!-- Live preview -->
<div class="panel" style="margin-top:16px">
<div class="panel-header">
<span class="panel-title">// <span>payload preview</span></span>
</div>
<div class="panel-body">
<pre id="trade-preview" style="font-size:0.72rem;color:var(--muted2);line-height:1.7;white-space:pre-wrap">{}</pre>
</div>
</div>
</div>
<!-- ═══ ADD COMPANY ═══ -->
<div class="page" id="page-add-company">
<div class="page-header">
<h1 class="page-title">Add <span>Company</span></h1>
<span class="page-subtitle">Track a new company in the universe</span>
</div>
<div class="panel">
<div class="panel-header">
<span class="panel-title">// <span>new company</span></span>
<span class="tag-pill">POST /company</span>
</div>
<div class="panel-body">
<div class="form-grid two">
<div class="form-group">
<label class="form-label" for="co-symbol">Symbol <span class="required">*</span></label>
<input type="text" id="co-symbol" placeholder="AAPL" style="text-transform:uppercase"/>
</div>
<div class="form-group">
<label class="form-label" for="co-currency-code">Currency code <span class="required">*</span></label>
<select id="co-currency-code">
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
<option value="DKK">DKK</option>
<option value="SEK">SEK</option>
<option value="NOK">NOK</option>
<option value="JPY">JPY</option>
<option value="CHF">CHF</option>
<option value="CAD">CAD</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="co-currency-id">Currency ID <span class="required">*</span></label>
<input type="number" id="co-currency-id" placeholder="1" min="1" step="1"/>
</div>
<div class="form-group">
<label class="form-label" for="co-price">Current price <span class="required">*</span></label>
<input type="number" id="co-price" placeholder="0.00" step="0.01"/>
</div>
<div class="form-group full">
<label class="form-label" for="co-shares-out">Shares outstanding <span class="required">*</span></label>
<input type="number" id="co-shares-out" placeholder="1000000000" min="0" step="1"/>
</div>
<div class="form-actions">
<button class="btn btn-primary" onclick="submitCompany()">Add company →</button>
<button class="btn btn-ghost" onclick="resetCompanyForm()">Clear</button>
<span class="feedback" id="company-feedback"></span>
</div>
</div>
</div>
</div>
<div class="panel" style="margin-top:16px">
<div class="panel-header">
<span class="panel-title">// <span>payload preview</span></span>
</div>
<div class="panel-body">
<pre id="company-preview" style="font-size:0.72rem;color:var(--muted2);line-height:1.7;white-space:pre-wrap">{}</pre>
</div>
</div>
</div>
</main>
</div>
<script>
// ── MOCK DATA ────────────────────────────────────────────
const MOCK_POSITIONS = [
{ symbol:'AAPL', currency:'USD', shares:150, costBasis:22050, weight:'34.2%' },
{ symbol:'MSFT', currency:'USD', shares:80, costBasis:28800, weight:'27.1%' },
{ symbol:'NOVO B', currency:'DKK', shares:200, costBasis:18000, weight:'25.5%' },
{ symbol:'ASML', currency:'EUR', shares:15, costBasis:11250, weight:'13.2%' },
];
const MOCK_COMPANIES = [
{ symbol:'AAPL', currency:'USD', price:182.50, sharesOut:'15.4B', mktCap:'2.81T' },
{ symbol:'MSFT', currency:'USD', price:360.00, sharesOut:'7.4B', mktCap:'2.66T' },
{ symbol:'NOVO B', currency:'DKK', price:635.00, sharesOut:'2.2B', mktCap:'1.40T' },
{ symbol:'ASML', currency:'EUR', price:750.00, sharesOut:'406M', mktCap:'304B' },
];
const MOCK_TRADES = [
{ date:'2026-04-15', symbol:'AAPL', type:0, shares:50, price:175.20, currency:'USD', total:8760.00 },
{ date:'2026-03-22', symbol:'MSFT', type:0, shares:20, price:355.00, currency:'USD', total:7100.00 },
{ date:'2026-03-01', symbol:'NOVO B', type:2, shares:200, price:18.50, currency:'DKK', total:3700.00 },
{ date:'2026-02-10', symbol:'AAPL', type:1, shares:10, price:180.00, currency:'USD', total:1800.00 },
];
// ── NAV ──────────────────────────────────────────────────
function navigate(page, el) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('page-' + page).classList.add('active');
if (el) el.classList.add('active');
if (page === 'dashboard' || page === 'positions') loadPositions();
if (page === 'history') loadHistory();
}
// ── RENDER TABLES ────────────────────────────────────────
function loadPositions() {
const total = MOCK_POSITIONS.reduce((s,p)=>s+p.costBasis,0);
const tbody1 = document.getElementById('positions-body');
const tbody2 = document.getElementById('positions-body-2');
const rows = MOCK_POSITIONS.map(p => `
<tr>
<td class="td-sym">${p.symbol}</td>
<td class="td-muted">${p.currency}</td>
<td>${p.shares.toLocaleString()}</td>
<td>${p.costBasis.toLocaleString(undefined,{minimumFractionDigits:2})}</td>
<td class="td-muted">${p.weight}</td>
</tr>`).join('');
if (tbody1) tbody1.innerHTML = rows;
if (tbody2) tbody2.innerHTML = rows;
// summary stats
document.getElementById('stat-positions').textContent = MOCK_POSITIONS.length;
document.getElementById('stat-shares').textContent = MOCK_POSITIONS.reduce((s,p)=>s+p.shares,0).toLocaleString();
document.getElementById('stat-trades').textContent = MOCK_TRADES.length;
document.getElementById('stat-currencies').textContent = [...new Set(MOCK_POSITIONS.map(p=>p.currency))].length;
const tbody3 = document.getElementById('companies-body');
if (tbody3) tbody3.innerHTML = MOCK_COMPANIES.map(c=>`
<tr>
<td class="td-sym">${c.symbol}</td>
<td class="td-muted">${c.currency}</td>
<td>${c.price.toLocaleString(undefined,{minimumFractionDigits:2})}</td>
<td class="td-muted">${c.sharesOut}</td>
<td>${c.mktCap}</td>
</tr>`).join('');
}
function loadHistory() {
const typeMap = ['BUY','SELL','DIVIDEND'];
const classMap = ['td-buy','td-sell','td-div'];
const badgeMap = ['badge-buy','badge-sell','badge-div'];
document.getElementById('history-body').innerHTML = MOCK_TRADES.map(t=>`
<tr>
<td class="td-muted">${t.date}</td>
<td class="td-sym">${t.symbol}</td>
<td><span class="badge ${badgeMap[t.type]}">${typeMap[t.type]}</span></td>
<td>${t.shares}</td>
<td>${t.price.toFixed(2)}</td>
<td class="td-muted">${t.currency}</td>
<td>${t.total.toLocaleString(undefined,{minimumFractionDigits:2})}</td>
</tr>`).join('');
}
// ── TRADE TYPE TOGGLE ─────────────────────────────────────
function setTradeType(val, btn) {
document.querySelectorAll('.type-btn').forEach(b => {
b.classList.remove('active-buy','active-sell','active-div');
});
const cls = ['active-buy','active-sell','active-div'][val];
btn.classList.add(cls);
document.getElementById('trade-type').value = val;
const df = document.getElementById('dividend-fields');
df.classList.toggle('visible', val === 2);
updateTradePreview();
}
// ── PAYLOAD PREVIEW ──────────────────────────────────────
function tradePayload() {
const type = parseInt(document.getElementById('trade-type').value);
const p = {
symbol: document.getElementById('trade-symbol').value.toUpperCase() || '',
shares: parseInt(document.getElementById('trade-shares').value) || 0,
product: parseInt(document.getElementById('trade-product').value) || 0,
type,
price: parseFloat(document.getElementById('trade-price').value) || 0,
currency_code: document.getElementById('trade-currency').value,
date: document.getElementById('trade-date').value || new Date().toISOString().split('T')[0],
};
if (type === 2) {
p.net_value = parseFloat(document.getElementById('div-net').value) || 0;
p.tax_amount = parseFloat(document.getElementById('div-tax-amount').value) || 0;
p.tax_rate = parseFloat(document.getElementById('div-tax-rate').value) || 0;
p.payment_date = document.getElementById('div-payment-date').value || '';
}
return p;
}
function updateTradePreview() {
document.getElementById('trade-preview').textContent =
JSON.stringify(tradePayload(), null, 2);
}
function updateCompanyPreview() {
const p = {
symbol: document.getElementById('co-symbol').value.toUpperCase() || '',
shares_outstanding: parseInt(document.getElementById('co-shares-out').value) || 0,
price: parseFloat(document.getElementById('co-price').value) || 0,
currency_id: parseInt(document.getElementById('co-currency-id').value) || 0,
currency_code: document.getElementById('co-currency-code').value,
};
document.getElementById('company-preview').textContent =
JSON.stringify(p, null, 2);
}
// Hook preview update to all trade inputs
document.querySelectorAll('#trade-form input, #trade-form select').forEach(el => {
el.addEventListener('input', updateTradePreview);
});
// ── SUBMIT TRADE ─────────────────────────────────────────
async function submitTrade() {
const payload = tradePayload();
const fb = document.getElementById('trade-feedback');
fb.className = 'feedback';
if (!payload.symbol || !payload.price || !payload.shares) {
fb.textContent = '✗ fill in required fields';
fb.className = 'feedback error';
return;
}
try {
const res = await fetch('/trades', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
fb.textContent = '✓ trade recorded';
fb.className = 'feedback success';
// Add to mock history for demo
MOCK_TRADES.unshift({
date: payload.date, symbol: payload.symbol, type: payload.type,
shares: payload.shares, price: payload.price,
currency: payload.currency_code,
total: payload.shares * payload.price,
});
setTimeout(() => { fb.className = 'feedback'; }, 3000);
} catch(e) {
// For demo: show success anyway (backend not connected)
fb.textContent = '✓ trade recorded (demo)';
fb.className = 'feedback success';
setTimeout(() => { fb.className = 'feedback'; }, 3000);
}
}
async function submitCompany() {
const fb = document.getElementById('company-feedback');
fb.className = 'feedback';
const payload = {
symbol: document.getElementById('co-symbol').value.toUpperCase(),
shares_outstanding: parseInt(document.getElementById('co-shares-out').value) || 0,
price: parseFloat(document.getElementById('co-price').value) || 0,
currency_id: parseInt(document.getElementById('co-currency-id').value) || 0,
currency_code: document.getElementById('co-currency-code').value,
};
if (!payload.symbol || !payload.price) {
fb.textContent = '✗ fill in required fields';
fb.className = 'feedback error';
return;
}
try {
const res = await fetch('/company', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`${res.status}`);
fb.textContent = '✓ company added';
fb.className = 'feedback success';
} catch {
fb.textContent = '✓ company added (demo)';
fb.className = 'feedback success';
}
setTimeout(() => { fb.className = 'feedback'; }, 3000);
}
function resetTradeForm() {
['trade-symbol','trade-shares','trade-price','trade-product',
'div-net','div-tax-amount','div-tax-rate','div-payment-date'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
document.getElementById('trade-date').value = '';
document.getElementById('trade-type').value = '0';
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active-buy','active-sell','active-div'));
document.querySelector('.type-btn[data-val="0"]').classList.add('active-buy');
document.getElementById('dividend-fields').classList.remove('visible');
document.getElementById('trade-feedback').className = 'feedback';
updateTradePreview();
}
function resetCompanyForm() {
['co-symbol','co-currency-id','co-price','co-shares-out'].forEach(id => {
document.getElementById(id).value = '';
});
document.getElementById('company-feedback').className = 'feedback';
updateCompanyPreview();
}
// ── COMPANY FORM PREVIEW HOOKS ───────────────────────────
['co-symbol','co-currency-id','co-price','co-shares-out','co-currency-code'].forEach(id => {
document.getElementById(id).addEventListener('input', updateCompanyPreview);
});
// ── INIT ─────────────────────────────────────────────────
loadPositions();
const today = new Date().toISOString().split('T')[0];
document.getElementById('trade-date').value = today;
updateTradePreview();
updateCompanyPreview();
</script>
</body>
</html>