// Generated, do not edit. sha256:aff33ec09f0738c89d055fc48911d73d2e952b3818493f0f70824b837ab7c4bf import { listAccounts } from '../data/accounts.js'; import { listAssets } from '../data/assets.js'; import { accountHoldings } from '../data/positions.js'; import { listOpenValuationDiffs, listValuations } from '../data/valuations.js'; import { mountSparkline } from '../ui/sparkline.js'; import { activityItems, heartbeat } from '../ui/activity.js'; import { setRowFocus } from '../ui/row-focus.js'; import { listTrades, listTransfers } from '../data/events.js'; import { listAllSchedules, listInterest } from '../data/interest-schedules.js'; import { scheduleForecast } from '../data/interest-credits.js'; import { latestPricePoint } from '../data/price-history.js'; import { basePriceOf } from '../price/providers.js'; import { loadFiat } from '../ui/fiat.js'; import { t } from '../i18n/index.js'; import { assetLabelHtml, byKindAndLabel } from '../ui/asset-label.js'; import { fmtBase, fmtUnit, fromSubunit, isBaseAsset, isMetal, val } from '../ui/format.js'; import { esc } from '../ui/escape.js'; import { emptyStateHtml } from '../ui/empty-state.js'; import { create as createTabs } from '../ui/tabs.js'; import { create as createFilter, resolveSelection } from '../ui/filter.js'; import { getPref, setPref } from '../data/preferences.js'; import { setPageHint } from '../ui/page-hint.js'; import * as dialog from '../ui/dialog.js'; import * as loading from '../ui/loading.js'; import { localDate, localDateNl, localTime } from '../../shared/time.js'; import { ACCOUNT_ASSET_KINDS, CONFIG, ENUMS, EVENTS_PAGE, TARGETS, balanceCaptureOn, targetHash } from '../../shared/config.js'; import * as valuations from './valuations.js'; import { recordBalance } from './valuations.js'; import * as trades from './trades.js'; import * as transfers from './transfers.js'; import * as interest from './interest.js'; import * as reservations from './reservations.js'; import { listReservations } from '../data/reservations.js'; import { reservationAccount } from '../actions/reservation-book.js'; const TABS = EVENTS_PAGE.TABS; const DETAIL = [ [TABS.VALUATIONS, valuations], [TABS.TRADES, trades], [TABS.TRANSFERS, transfers], [TABS.INTEREST, interest], [TABS.RESERVATIONS, reservations], ]; let lastDetailTab = TABS.VALUATIONS; export async function render(el) { el.innerHTML = '
'; const host = el.querySelector('[data-acc-cards]'); loading.show(host, 'cards', { count: 3 }); setPageHint(el, { title: t('eventAccounts.hintTitle'), rows: [ t('eventAccounts.hintBalance'), t('eventAccounts.hintDetail'), t('eventAccounts.hintInterest'), t('reservations.hint'), t('eventAccounts.hintActivity'), t('eventAccounts.hintCapture'), { heading: t('nav.valuations') }, t('valuations.rateNote'), { heading: t('nav.trades') }, t('trades.fiatNote'), { heading: t('nav.transfers') }, t('transfers.fiatNote'), { heading: t('nav.interest') }, t('interest.importNote'), ] }); try { await renderCards(host); } catch (err) { loading.error(host, err && err.message); } } async function loadModel() { const none = () => []; const [accounts, assets, open, schedules, interestRows, trades, transfers, valuationRows, reservationRows] = await Promise.all([ listAccounts(), listAssets(), listOpenValuationDiffs(), listAllSchedules().catch(none), listInterest().catch(none), listTrades().catch(none), listTransfers().catch(none), listValuations().catch(none), listReservations().catch(none), ]); const startYear = Number(await getPref('app', 'start_year').catch(() => null)) || CONFIG.START_YEAR.AUTO; const fromIso = startYear !== CONFIG.START_YEAR.AUTO ? `${startYear}-01-01T12:00:00Z` : null; const events = { accounts, schedules, interest: interestRows, trades, transfers, valuations: valuationRows, reservations: reservationRows, interestDue: [], fromIso }; const holdings = await accountHoldings(accounts.map(a => a.id)); const assetById = new Map(assets.map(a => [a.id, a])); const held = [...new Set(holdings.map(h => h.assetId))].map(id => assetById.get(id)).filter(Boolean); const spot = await loadFiat(held.filter(a => !isBaseAsset(a))).catch(() => new Map()); const prices = new Map(); for (const a of held) { if (isBaseAsset(a)) { prices.set(a.id, 1); continue; } let price = basePriceOf(spot.get(a.id)); if (price == null) price = (await latestPricePoint(a.id).catch(() => null))?.fiatPrice ?? null; prices.set(a.id, price); } const openPer = new Map(); for (const v of open) if (v.accountId != null) openPer.set(v.accountId, (openPer.get(v.accountId) || 0) + 1); const today = localDate(new Date().toISOString()); const running = schedules.filter(s => !s.endsOn || s.endsOn >= today); const forecasts = new Map(await Promise.all(running.filter(s => s.rateKind === 'fixed') .map(async s => [s.id, await scheduleForecast(s).catch(() => null)]))); events.interestDue = running.flatMap(s => (forecasts.get(s.id)?.upcoming || []) .map(p => ({ accountId: s.accountId, periodEnd: p.periodEnd }))); const interestOf = id => { const own = running.filter(s => s.accountId === id); const booked = interestRows.filter(r => r.toAccountId === id); const next = own.map(s => { const p = forecasts.get(s.id)?.upcoming[0]; return p ? { periodEnd: p.periodEnd, amount: p.projectedSub, meta: payoutMeta(s) } : null; }).filter(Boolean); return { agreements: own.length, booked: booked.length, lastAt: booked[0]?.occurredAt ?? null, next }; }; const cards = accounts.map(account => { const lines = holdings.filter(h => h.accountId === account.id).map(h => { const asset = assetById.get(h.assetId); const price = prices.get(h.assetId); const value = asset && price != null ? (h.saldo / Math.pow(10, asset.decimals)) * price : null; return { ...h, asset, value }; }).filter(l => l.asset).sort((a, b) => byKindAndLabel(a.asset, b.asset)); const total = lines.reduce((s, l) => s + (l.value ?? 0), 0); const own = reservationRows.filter(r => reservationAccount(r).id === account.id); return { account, lines, total, priced: lines.every(l => l.value != null), open: openPer.get(account.id) || 0, interest: interestOf(account.id), reservations: { n: own.length, nextAt: own[0]?.occurredAt ?? null } }; }); const kindRank = k => { const i = ENUMS.ACCOUNT_KIND.indexOf(k); return i < 0 ? ENUMS.ACCOUNT_KIND.length : i; }; return { events, cards: cards .filter(c => c.account.isActive || c.lines.length) .sort((a, b) => kindRank(a.account.kind) - kindRank(b.account.kind) || b.total - a.total || a.account.name.localeCompare(b.account.name)), }; } function dayLabel(day, x, names) { const date = localDateNl(new Date(x).toISOString()); if (!day?.length) return date; const counts = new Map(); for (const it of day) counts.set(it.kind, (counts.get(it.kind) || 0) + 1); const what = [...counts].map(([k, n]) => `${t('eventAccounts.act.' + k)}${n > 1 ? ` ${n}×` : ''}`).join(', '); const who = names ? [...new Set(day.map(it => names.get(it.accountId)))].join(', ') : ''; return `${date}: ${what}${who ? ` (${who})` : ''}`; } const MARK_VARIANT = { balance: 'balance', agreementStart: 'agreement', agreementEnd: 'agreement', interestDue: 'agreement', reservation: 'reservation' }; function mountActivity(host, events, accountIds, { onPick = null, nowText = null } = {}) { const nowIso = new Date().toISOString(); const items = activityItems({ accountIds, ...events }); const beat = heartbeat(items, nowIso, events.fromIso); if (!beat) { host.innerHTML = ''; return false; } const names = accountIds.length > 1 ? new Map(events.accounts.map(a => [a.id, a.name])) : null; const d = localDate(nowIso); const todayX = Date.UTC(Number(d.slice(0, 4)), Number(d.slice(5, 7)) - 1, Number(d.slice(8, 10))); const label = x => { const day = dayLabel(beat.byDay.get(x), x, names); return nowText && x === todayX ? `${day} · ${nowText}` : day; }; const marks = items.filter(it => it.dir === 0) .map(it => ({ x: it.x, variant: MARK_VARIANT[it.kind] || 'account', label: label(it.x) })); if (nowText) marks.push({ x: todayX, variant: 'now', label: label(todayX) }); const peak = Math.max(1, ...beat.points.map(p => Math.abs(p[1]))); mountSparkline(host, beat.points, { yRange: [-peak, peak], markers: marks, snap: items.filter(it => it.dir !== 0).map(it => it.x), formatValue: (_v, x) => label(x), onPick: onPick && (x => { const day = beat.byDay.get(x) || []; const hit = day.find(it => it.table && it.dir !== 0) || day.find(it => it.table); if (hit) onPick(hit); }), }); return true; } const PREF_SCOPE = EVENTS_PAGE.ROUTE; const PREF_KINDS = 'accountKinds'; const PREF_EMPTY = 'accountsEmpty'; async function renderCards(host) { const { cards, events } = await loadModel(); if (!cards.length) { host.innerHTML = emptyStateHtml(t('eventAccounts.none'), { why: t('eventAccounts.noneWhy'), href: targetHash(TARGETS.ACCOUNTS), linkText: t('eventAccounts.manage'), }); return; } const kinds = ENUMS.ACCOUNT_KIND.filter(k => cards.some(c => c.account.kind === k)); const items = kinds.map(k => ({ id: k, label: t('enums.account_kind.' + k) })); let selected = resolveSelection({ saved: await getPref(PREF_SCOPE, PREF_KINDS, null).catch(() => null), items }); let showEmpty = (await getPref(PREF_SCOPE, PREF_EMPTY, true).catch(() => true)) !== false; const hasEmpty = cards.some(c => !c.lines.length); const open = id => openDetail(id).then(() => { if (host.isConnected) renderCards(host); }); const draw = () => { const shown = cards.filter(c => selected.has(c.account.kind) && (showEmpty || c.lines.length)); host.innerHTML = `