Files

195 lines
10 KiB
JavaScript
Raw Permalink Normal View History

2026-10-30 12:28:48 +01:00
// Generated, do not edit. sha256:064fe66c277773dbc29669a7a0cdf7abab018774c71a0f8e1d2a5cceea3a4d22
2026-10-29 11:57:29 +01:00
import { listSelectableAccounts } from '../data/accounts.js';
import { dayNl, localTime, toUtcIso } from '../../shared/time.js';
import { listAssets, listBookableAssets } from '../data/assets.js';
import { tableViewCfg } from '../data/preferences.js';
import { getPriceAt } from '../data/price-history.js';
import { listValuations, valuationDiffs, updateValuation, deleteValuation, createValuation,
acceptValuationGap } from '../data/valuations.js';
import { t } from '../i18n/index.js';
import { assetLabelHtml } from '../ui/asset-label.js';
import * as dialog from '../ui/dialog.js';
import { esc } from '../ui/escape.js';
import { renderTable } from '../ui/table.js';
import { takeRowFocus } from '../ui/row-focus.js';
import { fmtBase, fmtBasePrice, fmtUnit, isBaseAsset, val, toSubunit, fromSubunit } from '../ui/format.js';
import { baseCurrency } from '../app-settings.js';
import { ensurePriceHistoryFor, historicalPointForVia, primarySourceKey } from '../price/providers.js';
import { bookDifference, acceptDifference } from '../actions/valuation-widget.js';
import { TARGETS, targetHash } from '../../shared/config.js';
import { emptyStateHtml } from '../ui/empty-state.js';
import { setPageHint } from '../ui/page-hint.js';
let tableApi = null;
let onlyAccount = null;
let onlyAsset = null;
let onlyKinds = null;
const assetAllowed = a => (onlyAsset == null || a.id === onlyAsset) && (onlyKinds == null || onlyKinds.includes(a.kind));
export async function render(el, { accountId = null, assetId = null, assetKinds = null } = {}) {
onlyAccount = accountId;
onlyAsset = assetId;
onlyKinds = assetKinds;
if ((await listAssets()).length === 0) {
el.innerHTML = `
${emptyStateHtml(t('valuations.none'), {
why: t('valuations.needAsset'), href: targetHash(TARGETS.ASSETS), linkText: t('common.manageAssets'),
})}`;
return;
}
el.innerHTML = `<div id="tbl"></div>`;
if (onlyAccount == null) setPageHint(el, { title: t('nav.valuations'), rows: [t('valuations.rateNote')] });
buildTable(el);
}
async function assetOpts() { return (await listBookableAssets()).filter(assetAllowed).map(a => ({ value: a.id, label: `${a.symbol}: ${a.name}` })); }
async function accountOpts(huidigeId) {
const accounts = (await listSelectableAccounts({ ookId: Number(huidigeId) || null }))
.filter(a => onlyAccount == null || a.id === onlyAccount);
return [...(onlyAccount == null ? [{ value: '', label: '-' }] : []), ...accounts.map(a => ({ value: a.id, label: a.name }))];
}
const meta = r => ({ kind: r.asset_kind, decimals: r.asset_decimals, unit: r.asset_unit, subunit: r.asset_subunit, displayUnit: r.asset_display_unit });
const isRekenmunt = r => isBaseAsset({ symbol: r.asset_symbol });
async function valuationPayload(v, existing) {
const asset = (await listAssets()).find(a => a.id === Number(v.assetId));
const qty = parseFloat(v.balance);
if (!v.valuedOn || !asset || !Number.isFinite(qty) || qty < 0) {
throw new Error(t('valuations.invalid'));
}
const balance = toSubunit(qty, asset);
const rateAt = toUtcIso(`${v.valuedOn}T${v._time || '00:00'}:00`);
let fiatRate = v.fiatRate === '' || v.fiatRate == null ? null : parseFloat(v.fiatRate);
if (fiatRate == null) {
try { await ensurePriceHistoryFor(asset); } catch { }
const readSrc = (await primarySourceKey(asset, 'daily_history'))
|| (await primarySourceKey(asset, 'history_point'));
const at = await getPriceAt(asset.id, Math.floor(Date.parse(rateAt) / 1000), readSrc);
fiatRate = at ? at.fiatPrice : null;
if (fiatRate == null) {
const p = await historicalPointForVia(asset, v.valuedOn).catch(() => null);
fiatRate = p ? p.price : null;
}
if (fiatRate == null) throw new Error(t('valuations.noRate'));
}
const accountId = v.accountId ?? onlyAccount;
return {
assetId: Number(v.assetId), valuedOn: v.valuedOn,
accountId: accountId !== '' && accountId != null ? Number(accountId) : null,
balance, fiatRate, rateAt,
source: existing ? existing.source : 'manual',
};
}
export async function recordBalance(v) {
return createValuation(await valuationPayload({ fiatRate: '', ...v }, null));
}
async function buildTable(el) {
const alleAssets = await listAssets();
const verschillen = new Map();
const rows = async () => {
const rijen = (await listValuations()).filter(r => (onlyAccount == null || r.accountId === onlyAccount)
&& (onlyAsset == null || r.assetId === onlyAsset));
verschillen.clear();
for (const s of await valuationDiffs()) verschillen.set(s.id, s);
return rijen;
};
const today = new Date().toISOString().slice(0, 10);
const columns = [
{ key: 'valuedOn', label: t('valuations.colDate'), sortable: true,
render: r => dayNl(r.valuedOn),
group: { value: r => r.valuedOn.slice(0, 4), label: v => v, order: 'desc' },
edit: { type: 'date', sticky: true, value: r => r ? r.valuedOn : today } },
{ key: '_time', label: t('valuations.colTime'),
render: r => localTime(r.rateAt),
edit: { type: 'time', value: r => r && r.rateAt ? localTime(r.rateAt) : '00:00' } },
{ key: 'accountId', label: t('valuations.colAccount'), sortable: true, sortValue: r => r.account_name || '',
render: r => r.account_name || '-', group: { value: r => r.account_name || '-', label: v => v },
edit: { type: 'select', sticky: true, options: vals => accountOpts(vals.accountId), value: r => r ? r.accountId ?? '' : '' } },
{ key: 'assetId', label: t('valuations.colAsset'), sortable: true, sortValue: r => r.asset_symbol,
render: r => assetLabelHtml(r), group: { value: r => r.asset_symbol, label: v => v },
edit: { type: 'select', sticky: true, reactive: true, options: assetOpts } },
{ key: 'balance', label: t('valuations.colBalance'), align: 'num', sortable: true, sortValue: r => r.balance,
render: r => {
const fiat = (r.fiatRate && !isRekenmunt(r)) ? fmtBase(r.balance / Math.pow(10, r.asset_decimals) * r.fiatRate) : '';
return `${val(fmtUnit(r.balance, meta(r)))}${fiat ? `<div class="t-muted-cell">${val(fiat)}</div>` : ''}`;
},
edit: { type: 'number', attrs: { min: 0, step: 'any' }, value: r => r ? fromSubunit(r.balance, meta(r)) : '', convert: vals => alleAssets.find(a => a.id === Number(vals.assetId)) } },
{ key: 'fiatRate', label: t('valuations.colRate', { munt: baseCurrency() }), align: 'num', sortable: true, sortValue: r => r.fiatRate,
render: r => (r.fiatRate && !isRekenmunt(r)) ? val(fmtBasePrice(r.fiatRate)) : '-',
edit: { type: 'number', attrs: { min: 0, step: 'any' }, placeholder: t('valuations.ratePlaceholder'),
visible: vals => !isBaseAsset(alleAssets.find(a => a.id === Number(vals.assetId))),
value: r => r && r.fiatRate ? r.fiatRate : '' } },
{ key: 'source', label: t('valuations.colSource'),
render: r => r.source ? `<span class="badge badge-neutral">${t('enums.valuation_source.' + r.source)}</span>` : '-' },
{ key: '_diff', label: t('valuations.colDiff'), align: 'num',
render: r => {
const s = verschillen.get(r.id);
const d = s ? s.gap : null;
if (d == null) return '<span class="t-muted-cell">-</span>';
if (s.future) return `<span class="t-muted-cell" title="${t('valuations.futureHint')}">⏳</span>`;
if (!s.reconcilable) return `<span class="t-muted-cell" title="${t('valuations.anchorHint')}">⚓</span>`;
if (d === 0) return `<span class="t-pos">✓</span>`;
if (s.accepted) {
return `<button class="btn btn-ghost btn-xs" data-unaccept="${r.id}"
title="${esc(`${t('valuations.acceptedHint')} ${fmtUnit(d, meta(r))}`)}">${t('valuations.acceptedMark')}</button>`;
}
return `<span class="${d < 0 ? 't-neg' : 't-pos'}">${val(fmtUnit(d, meta(r)))}</span>
<button class="btn btn-secondary btn-xs" data-book="${r.id}">${t('valuations.book')}</button>
<button class="btn btn-ghost btn-xs" data-accept="${r.id}"
title="${esc(t('valuations.acceptHint'))}">${t('valuations.accept')}</button>`;
} },
].filter(c => onlyAccount == null || c.key !== 'accountId');
const toPayload = valuationPayload;
const rijen = await rows();
2026-10-30 12:28:48 +01:00
const reload = async () => tableApi.refresh(await rows());
2026-10-29 11:57:29 +01:00
tableApi = renderTable(el.querySelector('#tbl'), {
columns, rows: rijen,
...(await tableViewCfg('valuations', { key: 'valuedOn', dir: 'desc' }, onlyAccount != null ? 'assetId' : null)),
emptyText: t('valuations.none'),
noneLabel: t('table.none'),
collapsibleGroups: true,
newLabel: t('table.new'),
convertLabel: t('convert.title'),
newDefaults: { accountId: onlyAccount ?? '', fiatRate: '', ...(onlyAsset != null ? { assetId: onlyAsset } : {}) },
saveLabel: t('trades.save'), cancelLabel: t('trades.cancel'),
onCreate: async v => {
const id = await createValuation(await toPayload(v, null));
2026-10-30 12:28:48 +01:00
tableApi.refresh(await rows(), { locate: id });
2026-10-29 11:57:29 +01:00
},
onSave: async (id, v) => {
const ex = (await listValuations()).find(x => x.id === id);
await updateValuation(id, await toPayload(v, ex)); reload();
},
onDelete: async id => {
if (!(await dialog.confirm({ message: t('valuations.confirmDelete'), danger: true }))) return;
await deleteValuation(id); reload();
},
});
el.querySelector('#tbl').addEventListener('click', async e => {
const b = e.target.closest('[data-book]');
if (b) {
e.preventDefault(); e.stopPropagation();
bookDifference((await rows()).find(x => x.id === Number(b.dataset.book)), reload);
return;
}
const a = e.target.closest('[data-accept]');
if (a) {
e.preventDefault(); e.stopPropagation();
acceptDifference(a.dataset.accept, reload);
return;
}
const u = e.target.closest('[data-unaccept]');
if (u) {
e.preventDefault(); e.stopPropagation();
await acceptValuationGap(Number(u.dataset.unaccept), false);
reload();
}
}, true);
tableApi.focusRow(takeRowFocus('valuations'));
}