201 lines
10 KiB
JavaScript
201 lines
10 KiB
JavaScript
// Generated, do not edit. sha256:84e73291cc02df3176d4ba8acbc3d3f1b844080d320978a75a2af8c8bde70e30
|
|
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 { capacityCfg } from '../ui/lock.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();
|
|
let used = rijen.length;
|
|
const reload = async () => { const r = await rows(); used = r.length; tableApi.refresh(r); };
|
|
tableApi = renderTable(el.querySelector('#tbl'), {
|
|
columns, rows: rijen,
|
|
...capacityCfg('valuations', () => used),
|
|
...(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));
|
|
const r = await rows();
|
|
used = r.length;
|
|
tableApi.refresh(r, { locate: id });
|
|
},
|
|
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'));
|
|
}
|