239 lines
14 KiB
JavaScript
239 lines
14 KiB
JavaScript
// Generated, do not edit. sha256:691f92a0fa91d8a9f654cd1214fd7139438481faeed67130424bba664831b55c
|
||
import { listSelectableAccounts } from '../data/accounts.js';
|
||
import { localDate, localDateNl, localTime, toUtcIso } from '../../shared/time.js';
|
||
import { listAssets, listBookableAssets } from '../data/assets.js';
|
||
import { listTransfers, createEvent, updateEvent, deleteEvent } from '../data/events.js';
|
||
import { listMetalProducts, listMetalProductsForAsset } from '../data/metals.js';
|
||
import { tableViewCfg } from '../data/preferences.js';
|
||
import { eventLinkMap, onchainAsset } from '../data/tx-links.js';
|
||
import { t } from '../i18n/index.js';
|
||
import { assetLabelHtml } from '../ui/asset-label.js';
|
||
import * as dialog from '../ui/dialog.js';
|
||
import { renderTable } from '../ui/table.js';
|
||
import { takeRowFocus } from '../ui/row-focus.js';
|
||
import { fmtUnit, val, subline, toSubunit, fromSubunit, isMetal } from '../ui/format.js';
|
||
import { matchDot, wireMatchTriggers } from '../ui/match-popover.js';
|
||
import { loadHistorical, fiatHistorical, fiatHistoricalOrigin } from '../ui/fiat.js';
|
||
import { originHtml } from '../ui/price-origin.js';
|
||
import { copyButton } from '../ui/copy-button.js';
|
||
import { TARGETS, targetHash } from '../../shared/config.js';
|
||
import { esc } from '../ui/escape.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));
|
||
const shortAddr = a => (a && a.length > 16) ? `${a.slice(0, 8)}…${a.slice(-6)}` : (a || '');
|
||
|
||
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('transfers.none'), {
|
||
why: t('transfers.needAsset'), href: targetHash(TARGETS.ASSETS), linkText: t('common.manageAssets'),
|
||
})}`;
|
||
return;
|
||
}
|
||
el.innerHTML = `<div id="tbl"></div>`;
|
||
if (onlyAccount == null) setPageHint(el, { title: t('nav.transfers'), rows: [t('transfers.fiatNote')] });
|
||
buildTable(el);
|
||
}
|
||
|
||
async function assetOptsOfKind(kind) {
|
||
return (await listBookableAssets()).filter(a => (!kind || a.kind === kind) && assetAllowed(a))
|
||
.map(a => ({ value: a.id, label: `${a.symbol}: ${a.name}` }));
|
||
}
|
||
async function accountOpts(huidigeId) {
|
||
return [{ value: '', label: '-' },
|
||
...(await listSelectableAccounts({ ookId: Number(huidigeId) || null })).map(a => ({ value: a.id, label: a.name }))];
|
||
}
|
||
async function productOpts(assetId) {
|
||
const id = Number(assetId);
|
||
const opts = [{ value: '', label: t('transfers.looseWeight') }];
|
||
if (Number.isInteger(id)) for (const p of await listMetalProductsForAsset(id)) opts.push({ value: p.id, label: p.name });
|
||
return opts;
|
||
}
|
||
|
||
async function buildTable(el) {
|
||
const rows = async () => (await listTransfers())
|
||
.filter(r => (onlyAccount == null || r.fromAccountId === onlyAccount || r.toAccountId === onlyAccount)
|
||
&& (onlyAsset == null || r.assetId === onlyAsset));
|
||
const meta = r => ({ id: r.assetId, kind: r.asset_kind, decimals: r.asset_decimals, unit: r.asset_unit, subunit: r.asset_subunit, displayUnit: r.asset_display_unit });
|
||
const herkomst = r => originHtml(fiatHistoricalOrigin(meta(r), localDate(r.occurredAt)), { ...meta(r), symbol: r.asset_symbol });
|
||
const btc = await onchainAsset();
|
||
let linkMap = await eventLinkMap();
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
const alleAssets = await listAssets();
|
||
const assetById = id => alleAssets.find(a => a.id === Number(id));
|
||
const firstOf = kind => { const a = alleAssets.find(x => x.kind === kind && assetAllowed(x)); return a ? a.id : ''; };
|
||
const convertAsset = vals => (vals.productId !== '' && vals.productId != null) ? null : (assetById(vals.assetId) || null);
|
||
|
||
const dirOf = r => (r.toAccountId === onlyAccount && r.fromAccountId !== onlyAccount ? 'in' : 'out');
|
||
const otherOf = r => (dirOf(r) === 'in'
|
||
? { id: r.fromAccountId, name: r.from_name }
|
||
: { id: r.toAccountId, name: r.to_name });
|
||
|
||
function productCell(r) {
|
||
if (r.productId && r.product_name) return `${esc(r.product_name)} <span class="t-muted-cell">×${r.productCount}</span>`;
|
||
if (r.asset_kind === 'metal') return `<span class="t-muted-cell">${t('transfers.looseWeight')}</span>`;
|
||
return '';
|
||
}
|
||
|
||
const columns = [
|
||
{ key: 'occurredAt', label: t('transfers.colDate'), sortable: true,
|
||
render: r => `${btc && r.assetId === btc.id ? matchDot({ matched: linkMap.has(r.id), method: linkMap.get(r.id), kind: 'event', id: r.id }) : ''}${localDateNl(r.occurredAt)}`,
|
||
group: { value: r => localDate(r.occurredAt).slice(0, 4), label: v => v, order: 'desc' },
|
||
edit: { type: 'date', sticky: true, attrs: { max: today }, value: r => r ? localDate(r.occurredAt) : today } },
|
||
{ key: '_time', label: t('transfers.time'),
|
||
render: r => localTime(r.occurredAt),
|
||
edit: { type: 'time', value: r => r ? localTime(r.occurredAt) : '12:00' } },
|
||
{ key: 'assetId', label: t('transfers.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: vals => assetOptsOfKind(vals.asset_kind) } },
|
||
{ key: 'asset_kind', label: t('transfers.colKind'), sortable: true,
|
||
render: r => t('enums.asset_kind.' + r.asset_kind),
|
||
group: { value: r => r.asset_kind, label: v => t('enums.asset_kind.' + v) },
|
||
edit: { value: r => r ? r.asset_kind : '', visible: () => false } },
|
||
{ key: 'productId', label: t('transfers.colProduct'), sortable: true, sortValue: r => r.product_name || '',
|
||
render: productCell,
|
||
edit: { type: 'select', sticky: true, reactive: true, visible: vals => vals.asset_kind === 'metal',
|
||
options: vals => productOpts(vals.assetId),
|
||
value: r => r ? (r.productId ?? '') : '' } },
|
||
{ key: 'amount', label: t('transfers.colAmount'), align: 'num', sortable: true, sortValue: r => r.amount,
|
||
render: r => { const fiat = fiatHistorical(r.amount, meta(r), localDate(r.occurredAt)); return `${val(fmtUnit(r.amount, meta(r)))}${subline(fiat ? val(fiat) + herkomst(r) : '')}`; },
|
||
edit: { type: 'number', attrs: { min: 0, step: 'any' },
|
||
value: r => r ? (r.productCount != null ? r.productCount : fromSubunit(r.amount, meta(r))) : '',
|
||
convert: convertAsset } },
|
||
...(onlyAccount == null ? [
|
||
{ key: 'fromAccountId', label: t('transfers.colFrom'), sortable: true, sortValue: r => r.from_name || '',
|
||
render: r => r.from_name || '-', group: { value: r => r.from_name || '-', label: v => v },
|
||
edit: { type: 'select', sticky: true, options: vals => accountOpts(vals.fromAccountId) } },
|
||
{ key: 'toAccountId', label: t('transfers.colTo'), sortable: true, sortValue: r => r.to_name || '',
|
||
render: r => r.to_name || '-', group: { value: r => r.to_name || '-', label: v => v },
|
||
edit: { type: 'select', sticky: true, options: vals => accountOpts(vals.toAccountId) } },
|
||
] : [
|
||
{ key: '_dir', label: t('transfers.colDirection'), sortable: true, sortValue: r => dirOf(r),
|
||
render: r => t(dirOf(r) === 'in' ? 'transfers.dirIn' : 'transfers.dirOut'),
|
||
group: { value: r => dirOf(r), label: v => t(v === 'in' ? 'transfers.dirIn' : 'transfers.dirOut') },
|
||
edit: { type: 'select', sticky: true, value: r => (r ? dirOf(r) : 'in'),
|
||
options: () => [{ value: 'in', label: t('transfers.dirIn') }, { value: 'out', label: t('transfers.dirOut') }] } },
|
||
{ key: '_other', label: t('transfers.colOther'), sortable: true, sortValue: r => otherOf(r).name || '',
|
||
render: r => esc(otherOf(r).name || '-'),
|
||
group: { value: r => otherOf(r).name || '-', label: v => v },
|
||
edit: { type: 'select', sticky: true, options: vals => accountOpts(vals._other),
|
||
value: r => (r ? otherOf(r).id ?? '' : '') } },
|
||
]),
|
||
{ key: 'unitFee', label: t('transfers.colFee'), align: 'num', sortable: true, sortValue: r => r.unitFee ?? -1,
|
||
render: r => { if (r.unitFee == null) return `-${subline('')}`; const fiat = fiatHistorical(r.unitFee, meta(r), localDate(r.occurredAt)); return `${val(fmtUnit(r.unitFee, meta(r)))}${subline(fiat ? val(fiat) + herkomst(r) : '')}`; },
|
||
edit: { type: 'number', attrs: { min: 0, step: 'any' }, value: r => r && r.unitFee != null ? fromSubunit(r.unitFee, meta(r)) : '', convert: convertAsset } },
|
||
{ key: 'priceOrigin', label: t('transfers.colOrigin'),
|
||
render: r => (r.priceOrigin === 'none'
|
||
? `<span class="badge badge-neutral">${t('transfers.originNone')}</span>`
|
||
: `<span class="t-muted-cell">-</span>`),
|
||
group: { value: r => (r.priceOrigin === 'none' ? t('transfers.originNone') : '-'), label: v => v },
|
||
edit: { type: 'select', value: r => (r && r.priceOrigin === 'none' ? 'none' : ''),
|
||
options: () => ([
|
||
{ value: '', label: t('transfers.originPick') },
|
||
{ value: 'none', label: t('transfers.originNonePick') },
|
||
]) } },
|
||
{ key: 'note', label: t('transfers.colNote'), text: true,
|
||
render: r => `<span class="t-muted-cell">${esc(r.note || '')}</span>`,
|
||
edit: { type: 'textarea', expand: true } },
|
||
{ key: 'externalAddress', label: t('transfers.colAddress'),
|
||
render: r => r.externalAddress ? `<span class="t-muted-cell" title="${esc(r.externalAddress)}">${esc(shortAddr(r.externalAddress))}</span>${copyButton(r.externalAddress)}` : '',
|
||
edit: { type: 'text', expand: true, placeholder: t('transfers.addressPlaceholder') } },
|
||
{ key: 'source', label: t('transfers.colSource'), sortable: true,
|
||
render: r => `<span class="t-muted-cell">${r.source ? t('enums.event_source.' + r.source) : '-'}</span>`,
|
||
group: { value: r => r.source || 'manual', label: v => t('enums.event_source.' + v) } },
|
||
];
|
||
|
||
async function toPayload(v, existing) {
|
||
const asset = alleAssets.find(a => a.id === Number(v.assetId));
|
||
if (!v.occurredAt || !asset) throw new Error(t('transfers.fillDateAsset'));
|
||
|
||
let productId = null, productCount = null, amount;
|
||
const wantProduct = isMetal(asset) && v.productId !== '' && v.productId != null;
|
||
if (wantProduct) {
|
||
const product = (await listMetalProducts()).find(p => p.id === Number(v.productId) && p.assetId === asset.id);
|
||
if (!product) throw new Error(t('transfers.pickValidProduct'));
|
||
const cnt = parseInt(v.amount, 10);
|
||
if (!Number.isInteger(cnt) || cnt <= 0) throw new Error(t('transfers.fillCount'));
|
||
productId = product.id; productCount = cnt;
|
||
amount = product.fineGrams * cnt;
|
||
} else {
|
||
const qty = parseFloat(v.amount);
|
||
if (!Number.isFinite(qty) || qty <= 0) throw new Error(t('transfers.fillDateAssetQuantity'));
|
||
amount = toSubunit(qty, asset);
|
||
}
|
||
const idOf = x => (x !== '' && x != null ? Number(x) : null);
|
||
let from = idOf(v.fromAccountId), to = idOf(v.toAccountId);
|
||
if (onlyAccount != null) {
|
||
const other = idOf(v._other);
|
||
[from, to] = v._dir === 'in' ? [other, onlyAccount] : [onlyAccount, other];
|
||
}
|
||
if (!from && !to) throw new Error(t('transfers.fillOneAccount'));
|
||
if (onlyAccount != null && from !== onlyAccount && to !== onlyAccount) throw new Error(t('transfers.fillThisAccount'));
|
||
return {
|
||
assetId: asset.id, type: 'transfer',
|
||
occurredAt: toUtcIso(`${v.occurredAt}T${v._time || '00:00'}:00`),
|
||
amount, fiatPrice: null,
|
||
fiatFee: existing ? existing.fiatFee ?? null : null,
|
||
unitFee: v.unitFee === '' || v.unitFee == null ? null : toSubunit(parseFloat(v.unitFee), asset),
|
||
productId, productCount,
|
||
fromAccountId: from, toAccountId: to,
|
||
source: existing ? existing.source : 'manual',
|
||
note: (v.note || '').trim() || null,
|
||
externalAddress: (v.externalAddress || '').trim() || null,
|
||
priceOrigin: v.priceOrigin === 'none' ? 'none' : null,
|
||
};
|
||
}
|
||
const reload = async () => tableApi.refresh(await rows());
|
||
const historieRijen = await rows();
|
||
loadHistorical(historieRijen.map(r => ({ assetId: r.assetId, dateKey: localDate(r.occurredAt) })))
|
||
.then(async () => { if (tableApi) tableApi.refresh(await rows()); }).catch(() => {});
|
||
|
||
tableApi = renderTable(el.querySelector('#tbl'), {
|
||
columns, rows: await rows(),
|
||
...(await tableViewCfg('transfers', { key: 'occurredAt', dir: 'desc' }, onlyAccount != null ? 'assetId' : null)),
|
||
emptyText: t('transfers.none'),
|
||
noneLabel: t('table.none'),
|
||
collapsibleGroups: true,
|
||
groupSummary: items => {
|
||
const first = items[0]; if (!first) return '';
|
||
if (!items.every(r => r.assetId === first.assetId)) return '';
|
||
const tot = items.reduce((s, r) => s + (r.amount || 0), 0);
|
||
return val(fmtUnit(tot, meta(first)));
|
||
},
|
||
newLabel: t('table.new'),
|
||
convertLabel: t('convert.title'),
|
||
newDefaults: { fromAccountId: '', toAccountId: '', unitFee: '' },
|
||
newOptions: [
|
||
{ key: 'crypto', label: t('transfers.addCrypto'), defaults: { asset_kind: 'crypto', assetId: firstOf('crypto') } },
|
||
{ key: 'metal', label: t('transfers.addMetal'), defaults: { asset_kind: 'metal', assetId: firstOf('metal') } },
|
||
{ key: 'fiat', label: t('transfers.addFiat'), defaults: { asset_kind: 'fiat', assetId: firstOf('fiat') } },
|
||
].filter(o => o.defaults.assetId !== ''),
|
||
saveLabel: t('transfers.save'), cancelLabel: t('transfers.cancel'),
|
||
onCreate: async v => { const id = await createEvent(await toPayload(v, null)); tableApi.refresh(await rows(), { locate: id }); },
|
||
onSave: async (id, v) => {
|
||
const ex = (await listTransfers()).find(x => x.id === id);
|
||
await updateEvent(id, await toPayload(v, ex)); reload();
|
||
},
|
||
onDelete: async id => {
|
||
if (!(await dialog.confirm({ message: t('transfers.confirmDelete'), danger: true }))) return;
|
||
await deleteEvent(id); reload();
|
||
},
|
||
});
|
||
|
||
wireMatchTriggers(el.querySelector('#tbl'), (kind, id) => ({
|
||
kind, id, assetId: btc ? btc.id : null,
|
||
onChange: async () => { linkMap = await eventLinkMap(); tableApi.refresh(await rows()); },
|
||
}));
|
||
|
||
tableApi.focusRow(takeRowFocus('transfers'));
|
||
}
|