/* Dashboard-native portfolio editing. * * Owns: the EDIT button toggle, the add-position form behaviour * (ticker validation on blur, qty/cost inputs, date-mode historical * lookup, Add click), and per-row delete via event delegation. * * Reads/writes the portfolio via window.CassandraPortfolio.loadPie / * savePie / mountAndRender — the same surface portfolio.js exposes * for the CSV-import preview. */ (function () { 'use strict'; const panel = document.getElementById('portfolio-panel'); const editBtn = document.getElementById('pf-edit-btn'); const doneBtn = document.getElementById('pf-done-btn'); const form = document.getElementById('pf-add-form'); if (!panel || !editBtn || !doneBtn || !form) return; function enterEditMode() { panel.classList.add('pf-editing'); form.hidden = false; editBtn.hidden = true; doneBtn.hidden = false; editBtn.setAttribute('aria-pressed', 'true'); document.getElementById('pf-add-ticker').focus(); } function exitEditMode() { panel.classList.remove('pf-editing'); // The form is edit-mode-only — always hide it on exit, including // when the portfolio is empty. The empty state shows guidance text // that nudges the user back to the Edit button. form.hidden = true; editBtn.hidden = false; doneBtn.hidden = true; editBtn.setAttribute('aria-pressed', 'false'); } editBtn.addEventListener('click', enterEditMode); doneBtn.addEventListener('click', exitEditMode); // ---- Ticker validation on blur ------------------------------------- const tickerInput = document.getElementById('pf-add-ticker'); const tickerStatus = document.getElementById('pf-add-ticker-status'); const costCurrencyEl = document.getElementById('pf-add-cost-currency'); const submitBtn = document.getElementById('pf-add-submit'); const warningEl = document.getElementById('pf-add-warning'); let validated = null; // {symbol, price, currency, as_of} or null function setStatus(el, text, kind) { el.textContent = text; el.className = 'pf-add-status' + (kind ? ' pf-add-status--' + kind : ''); } function updateSubmitState() { const qty = parseFloat(document.getElementById('pf-add-qty').value); const cost = parseFloat(document.getElementById('pf-add-cost').value); submitBtn.disabled = !( validated && qty > 0 && cost > 0 && isFinite(qty) && isFinite(cost) ); } function clearDuplicateWarning() { warningEl.hidden = true; warningEl.textContent = ''; } function showDuplicateWarning(existing) { warningEl.hidden = false; warningEl.textContent = `Already in your portfolio (${existing.qty} shares @ ` + `${existing.avg_cost.toFixed(2)}). Adding will create a duplicate row.`; } async function validateTicker() { const raw = tickerInput.value.trim().toUpperCase(); if (!raw) { validated = null; setStatus(tickerStatus, '', ''); costCurrencyEl.textContent = ''; clearDuplicateWarning(); updateSubmitState(); return; } setStatus(tickerStatus, 'checking…', 'pending'); try { const r = await fetch('/api/ticker/validate?symbol=' + encodeURIComponent(raw)); if (!r.ok) throw new Error('HTTP ' + r.status); const j = await r.json(); if (j.ok) { validated = j; setStatus( tickerStatus, '✓ ' + j.price.toFixed(2) + ' ' + (j.currency || ''), 'ok', ); costCurrencyEl.textContent = j.currency || ''; // Duplicate detection. const pie = window.CassandraPortfolio.loadPie(); const existing = pie && (pie.positions || []).find( p => (p.yahoo_ticker || '').toUpperCase() === j.symbol ); if (existing) showDuplicateWarning(existing); else clearDuplicateWarning(); } else { validated = null; setStatus(tickerStatus, '✗ ' + (j.error || 'not recognised'), 'err'); costCurrencyEl.textContent = ''; clearDuplicateWarning(); } } catch (e) { validated = null; setStatus(tickerStatus, '✗ couldn\'t validate — try again', 'err'); costCurrencyEl.textContent = ''; clearDuplicateWarning(); } updateSubmitState(); } tickerInput.addEventListener('blur', validateTicker); document.getElementById('pf-add-qty').addEventListener('input', updateSubmitState); document.getElementById('pf-add-cost').addEventListener('input', updateSubmitState); // ---- Add button → localStorage merge ------------------------------- function resetForm() { tickerInput.value = ''; document.getElementById('pf-add-qty').value = ''; document.getElementById('pf-add-cost').value = ''; document.getElementById('pf-add-date').value = ''; validated = null; setStatus(tickerStatus, '', ''); costCurrencyEl.textContent = ''; clearDuplicateWarning(); updateSubmitState(); tickerInput.focus(); } function addPosition() { if (submitBtn.disabled) return; const qty = parseFloat(document.getElementById('pf-add-qty').value); const cost = parseFloat(document.getElementById('pf-add-cost').value); const sym = validated.symbol; const pie = window.CassandraPortfolio.loadPie() || { pie_name: null, base_currency: 'GBP', positions: [], totals: {invested: 0, value: 0, result: 0}, warnings: [], }; pie.positions = pie.positions || []; pie.positions.push({ yahoo_ticker: sym, t212_slice: sym, // shared shape with CSV path name: validated.name || sym, qty: qty, avg_cost: cost, currency: validated.currency || 'USD', }); window.CassandraPortfolio.savePie(pie); window.CassandraPortfolio.mountAndRender(); resetForm(); } submitBtn.addEventListener('click', addPosition); // Submit on Enter from any input within the form. form.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !submitBtn.disabled) { e.preventDefault(); addPosition(); } }); // ---- Calendar-icon → historical lookup ----------------------------- const dateBtn = document.getElementById('pf-add-date-btn'); const dateInput = document.getElementById('pf-add-date'); const dateStatus = document.getElementById('pf-add-date-status'); const costInput = document.getElementById('pf-add-cost'); dateBtn.addEventListener('click', function () { if (!validated) { setStatus(dateStatus, 'enter a valid ticker first', 'err'); return; } dateInput.hidden = !dateInput.hidden; if (!dateInput.hidden) { dateInput.focus(); if (typeof dateInput.showPicker === 'function') dateInput.showPicker(); } else { setStatus(dateStatus, '', ''); } }); async function fetchHistorical() { if (!validated) { setStatus(dateStatus, 'enter a valid ticker first', 'err'); return; } const d = dateInput.value; if (!d) { setStatus(dateStatus, '', ''); return; } setStatus(dateStatus, 'looking up…', 'pending'); try { const url = '/api/ticker/historical?symbol=' + encodeURIComponent(validated.symbol) + '&date=' + encodeURIComponent(d); const r = await fetch(url); if (r.status === 400) { const j = await r.json().catch(() => ({detail: 'invalid date'})); setStatus(dateStatus, '✗ ' + (j.detail || 'invalid date'), 'err'); updateSubmitState(); return; } const j = await r.json(); if (j.ok) { costInput.value = j.close.toFixed(2); const tag = (j.actual_date && j.actual_date !== d) ? '✓ from ' + j.actual_date : '✓'; setStatus(dateStatus, tag, 'ok'); // Hide the date picker after a successful fill — keeps the row clean. dateInput.hidden = true; } else { setStatus(dateStatus, '✗ ' + (j.error || 'no data'), 'err'); } } catch (e) { setStatus(dateStatus, '✗ couldn\'t fetch — try again', 'err'); } updateSubmitState(); } dateInput.addEventListener('change', fetchHistorical); // ---- Per-row delete (event delegation) ----------------------------- panel.addEventListener('click', function (e) { const btn = e.target.closest('.pf-row-del'); if (!btn) return; const idx = parseInt(btn.dataset.idx, 10); if (!Number.isInteger(idx)) return; const pie = window.CassandraPortfolio.loadPie(); if (!pie || !pie.positions || idx < 0 || idx >= pie.positions.length) return; pie.positions.splice(idx, 1); window.CassandraPortfolio.savePie(pie); window.CassandraPortfolio.mountAndRender(); }); })();