Files
AtreidesAssetsFree/client/actions/valuation-widget.js
T
2026-10-29 11:57:29 +01:00

145 lines
7.3 KiB
JavaScript

// Generated, do not edit. sha256:d42fa49545ab26fee8488ce3ebd3d684d5394b406ae802e38e6b624c87d49f71
import { listSelectableAccounts } from '../data/accounts.js';
import { createEvent, findEventByRef, deleteEvent, listEvents, updateEvent } from '../data/events.js';
import { valuationGap, listOpenValuationDiffs, acceptValuationGap, listValuations } from '../data/valuations.js';
import { feeCandidates, withFee } from './valuation-fee.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 { fmtUnit, val } from '../ui/format.js';
import { taskCardHtml, nothingPendingHtml } from '../ui/task-card.js';
import { lightningBalanceDiffs, recordLightningBalance } from '../data/positions.js';
import { onchainAsset } from '../data/tx-links.js';
import { spotOnDate } from '../ui/fiat.js';
import { priceToBase } from '../../shared/units.js';
import { dayNl, localDate, localDateNl } from '../../shared/time.js';
const meta = v => ({ decimals: v.asset_decimals, unit: v.asset_unit, subunit: v.asset_subunit, displayUnit: v.asset_display_unit });
const ADJUST_REF_PREFIX = 'val:';
export async function bookDifference(v, refresh) {
if (!v || !v.accountId) return;
const gap = await valuationGap(v);
if (!gap) return;
const refVan = (tegen) => (tegen ? `${ADJUST_REF_PREFIX}${v.id}:${tegen}` : `${ADJUST_REF_PREFIX}${v.id}`);
const netVan = (e) => (e ? (e.toAccountId === v.accountId ? e.amount : -e.amount) : 0);
const anderen = (await listSelectableAccounts({ ookId: null })).filter(a => a.id !== v.accountId);
const kandidaten = [{ tegen: null, naam: null }, ...anderen.map(a => ({ tegen: a.id, naam: a.name }))];
const priors = new Map();
for (const k of kandidaten) {
const e = await findEventByRef(refVan(k.tegen));
if (e) priors.set(k.tegen, e);
}
const bedragVan = (k) => gap + netVan(priors.get(k.tegen));
const keuzes = kandidaten.map(k => {
const bedrag = bedragVan(k);
const uit = bedrag < 0;
const basis = k.tegen === null
? (uit ? t('valuations.bookAsSell') : t('valuations.bookAsBuy'))
: `${uit ? t('valuations.bookAsTransferTo') : t('valuations.bookAsTransferFrom')} ${k.naam}`;
const extra = !priors.has(k.tegen) ? ''
: bedrag === 0 ? ` (${t('valuations.bookRemoves')})`
: ` (${t('valuations.bookReplaces')} ${fmtUnit(Math.abs(bedrag), meta(v))})`;
return { value: k.tegen === null ? 'trade' : `acc:${k.tegen}`, label: basis + extra };
});
const kandidatenFee = gap < 0
? feeCandidates({ events: await listEvents(), valuations: await listValuations(), v }).slice(0, 10)
: [];
for (const e of kandidatenFee) {
keuzes.push({ value: `fee:${e.id}`, label: t('valuations.bookAsFeeOn',
{ soort: t(`enums.event_type.${e.type}`), datum: localDateNl(e.occurredAt) }) });
}
const res = await dialog.form({
title: t('valuations.bookTitle'),
fields: [{ key: 'hoe', type: 'select', value: 'trade', options: keuzes,
label: `${fmtUnit(Math.abs(gap), meta(v))} ${t('valuations.bookAsSuffix')}` }],
confirmLabel: t('valuations.book'),
});
if (!res) return;
if (res.hoe.startsWith('fee:')) {
const e = kandidatenFee.find(x => x.id === Number(res.hoe.slice(4)));
if (e) await updateEvent(e.id, withFee(e, gap, t('valuations.bookFeeNote')));
refresh();
return;
}
const tegen = res.hoe.startsWith('acc:') ? Number(res.hoe.slice(4)) : null;
const prior = priors.get(tegen) || null;
const bedrag = gap + netVan(prior);
const uit = bedrag < 0;
if (prior) await deleteEvent(prior.id);
if (!bedrag) { refresh(); return; }
const gemeen = {
assetId: v.assetId, occurredAt: v.rateAt, amount: Math.abs(bedrag),
fiatFee: null, unitFee: null, source: 'reconcile',
note: t('valuations.bookNote'), importReference: refVan(tegen),
};
await createEvent(tegen
? { ...gemeen, type: 'transfer', fiatPrice: null,
fromAccountId: uit ? v.accountId : tegen, toAccountId: uit ? tegen : v.accountId }
: { ...gemeen, type: uit ? 'sell' : 'buy', fiatPrice: v.fiatRate,
fromAccountId: uit ? v.accountId : null, toAccountId: uit ? null : v.accountId });
refresh();
}
export async function acceptDifference(id, refresh) {
if (!(await dialog.confirm({ message: t('valuations.confirmAccept') }))) return;
await acceptValuationGap(Number(id));
refresh();
}
async function recordMeasured(d, refresh) {
const asset = await onchainAsset();
const dag = localDate(d.scannedAt);
let koers = null;
try { koers = asset ? await spotOnDate(asset, dag) : null; } catch { koers = null; }
const res = await dialog.form({
title: t('valuations.recordTitle'),
fields: [{ key: 'rate', type: 'number', value: koers != null ? String(Math.round(koers * 100) / 100) : '',
label: t('valuations.recordRate', { date: dayNl(dag) }), attrs: { min: 0, step: 'any' } }],
confirmLabel: t('valuations.recordBalance'),
});
if (!res) return;
const rate = parseFloat(String(res.rate).replace(',', '.'));
if (!(rate > 0)) return;
try {
await recordLightningBalance(d.accountId, { valuedOn: dag, fiatRate: priceToBase(rate, asset) });
} catch (err) {
await dialog.alert({ message: err.message });
return;
}
refresh();
}
export async function renderValuationWidget(host, refresh) {
const [rows, gemeten] = await Promise.all([listOpenValuationDiffs(), lightningBalanceDiffs()]);
const btc = gemeten.length ? await onchainAsset() : null;
const btcMeta = btc ? { decimals: btc.decimals, unit: btc.unit, subunit: btc.subunit, displayUnit: btc.displayUnit } : null;
const nodeRijen = gemeten.map((d, i) => `<div class="flex gap-md" style="align-items:center;flex-wrap:wrap;margin-top:0.4rem">
<span class="t-body-sm" title="${esc(t('valuations.lightningHint'))}">${esc(localDateNl(d.scannedAt))} · ${esc(d.account_name)} · ${esc(t('valuations.lightningRow'))}:
<span class="${d.gap < 0 ? 't-neg' : 't-pos'}">${val(fmtUnit(d.gap, btcMeta))}</span></span>
<button class="btn btn-secondary btn-sm" data-vrecord="${i}" title="${esc(t('valuations.recordBalanceHint'))}">${t('valuations.recordBalance')}</button>
</div>`).join('');
const balansRijen = rows.map((v, i) => `<div class="flex gap-md" style="align-items:center;flex-wrap:wrap;margin-top:0.4rem">
<span class="t-body-sm">${esc(v.valuedOn)} · ${esc(v.account_name)} · ${assetLabelHtml(v)}:
<span class="${v.gap < 0 ? 't-neg' : 't-pos'}">${val(fmtUnit(v.gap, meta(v)))}</span></span>
<button class="btn btn-secondary btn-sm" data-vbook="${i}">${t('valuations.book')}</button>
<button class="btn btn-ghost btn-sm" data-vaccept="${i}" title="${esc(t('valuations.acceptHint'))}">${t('valuations.accept')}</button>
</div>`).join('');
const body = rows.length + gemeten.length === 0 ? nothingPendingHtml() : nodeRijen + balansRijen;
host.innerHTML = taskCardHtml({ title: t('actions.valuationTitle'), body });
host.querySelectorAll('[data-vrecord]').forEach(b =>
b.onclick = () => recordMeasured(gemeten[Number(b.dataset.vrecord)], refresh));
host.querySelectorAll('[data-vbook]').forEach(b =>
b.onclick = () => bookDifference(rows[Number(b.dataset.vbook)], refresh));
host.querySelectorAll('[data-vaccept]').forEach(b =>
b.onclick = () => acceptDifference(rows[Number(b.dataset.vaccept)].id, refresh));
return rows.length + gemeten.length;
}