diff --git a/app/static/js/portfolio_edit.js b/app/static/js/portfolio_edit.js index c165cc5..627f9d2 100644 --- a/app/static/js/portfolio_edit.js +++ b/app/static/js/portfolio_edit.js @@ -40,4 +40,88 @@ 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); })();