363 lines
17 KiB
JavaScript
363 lines
17 KiB
JavaScript
// Generated, do not edit. sha256:405d455ffaf6423b7f59a7abc882b3d6e788cead63eb4b179c23c015dd404720
|
||||
|
|
import { listSelectableAccounts } from '../data/accounts.js';
|
|||
|
|
import { dayNl, localDate, localDateTimeNl } from '../../shared/time.js';
|
|||
|
|
import {
|
|||
|
|
listPendingProposals, resolveProposalAsTrade, resolveProposalAsTransfer, resolveProposalAsInternal,
|
|||
|
|
resolveProposalAsInterest, ignoreProposal,
|
|||
|
|
deleteProposal, ignoreAllPendingProposals, deleteAllPendingProposals, revertProposal,
|
|||
|
|
listHandledProposals
|
|||
|
|
} from '../data/proposals.js';
|
|||
|
|
import { syncAddressLinks } from '../data/tx-links.js';
|
|||
|
|
import { listAllWalletSources } from '../data/wallet-sources.js';
|
|||
|
|
import { t } from '../i18n/index.js';
|
|||
|
|
import { assetLabelHtml } from '../ui/asset-label.js';
|
|||
|
|
import * as dialog from '../ui/dialog.js';
|
|||
|
|
import { fmtInt, val } from '../ui/format.js';
|
|||
|
|
import { baseCurrency } from '../app-settings.js';
|
|||
|
|
import { esc } from '../ui/escape.js';
|
|||
|
|
import { taskCardHtml, nothingPendingHtml } from '../ui/task-card.js';
|
|||
|
|
import { isOnchainSource, scanSource } from '../onchain.js';
|
|||
|
|
import { openMatchPopover } from '../ui/match-popover.js';
|
|||
|
|
import { onchainAsset } from '../data/tx-links.js';
|
|||
|
|
import { spotOnDate, spotAtMoment } from '../ui/fiat.js';
|
|||
|
|
import { priceUnitLabel, totalFromPrice, priceFromTotal, priceToBase, priceFromBase } from '../../shared/units.js';
|
|||
|
|
import { expanders } from '../ui/expand.js';
|
|||
|
|
import { bulkPlan, bulkCounts, isForward } from './reconcile-bulk.js';
|
|||
|
|
import { memoLine } from '../lightning/memo-line.js';
|
|||
|
|
|
|||
|
|
let wizIdx = 0;
|
|||
|
|
|
|||
|
|
export async function renderReconcile(host, onChanged) {
|
|||
|
|
const rerender = () => renderReconcile(host, onChanged);
|
|||
|
|
const refresh = () => (onChanged ? onChanged() : rerender());
|
|||
|
|
|
|||
|
|
await syncAddressLinks();
|
|||
|
|
const pending = await listPendingProposals();
|
|||
|
|
const handled = await listHandledProposals();
|
|||
|
|
const expand = await expanders('reconcile', { drawer: false });
|
|||
|
|
|
|||
|
|
if (wizIdx >= pending.length) wizIdx = Math.max(0, pending.length - 1);
|
|||
|
|
const current = pending[wizIdx] || null;
|
|||
|
|
|
|||
|
|
const actions = `<button class="btn btn-primary btn-sm" id="rec-rematch">${t('home.rematch')}</button>`;
|
|||
|
|
|
|||
|
|
const wizard = pending.length === 0
|
|||
|
|
? nothingPendingHtml()
|
|||
|
|
: `<div class="rec-wiz-nav">
|
|||
|
|
<button class="btn btn-ghost btn-xs" id="rec-prev" ${wizIdx === 0 ? 'disabled' : ''}>‹ ${t('home.recPrev')}</button>
|
|||
|
|
<span class="t-body-sm t-muted">${wizIdx + 1} / ${pending.length}</span>
|
|||
|
|
<button class="btn btn-ghost btn-xs" id="rec-next" ${wizIdx >= pending.length - 1 ? 'disabled' : ''}>${t('home.recNext')} ›</button>
|
|||
|
|
</div>
|
|||
|
|
${recRow(current, true, current.id)}`;
|
|||
|
|
|
|||
|
|
host.innerHTML = taskCardHtml({
|
|||
|
|
title: t('home.reconcile'),
|
|||
|
|
actions,
|
|||
|
|
body: `<div id="rec-progress" class="t-body-sm t-muted" style="margin-bottom:0.6rem"></div>
|
|||
|
|
${wizard}
|
|||
|
|
${drawerSection(pending, handled, current, expand)}`,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const prev = host.querySelector('#rec-prev');
|
|||
|
|
if (prev) prev.onclick = () => { wizIdx = Math.max(0, wizIdx - 1); rerender(); };
|
|||
|
|
const next = host.querySelector('#rec-next');
|
|||
|
|
if (next) next.onclick = () => { wizIdx = Math.min(pending.length - 1, wizIdx + 1); rerender(); };
|
|||
|
|
expand.bind(host);
|
|||
|
|
|
|||
|
|
const igAll = host.querySelector('#rec-ignore-all');
|
|||
|
|
if (igAll) igAll.onclick = async () => {
|
|||
|
|
if (!(await dialog.confirm({ message: t('home.recIgnoreAllConfirm') }))) return;
|
|||
|
|
await ignoreAllPendingProposals(); refresh();
|
|||
|
|
};
|
|||
|
|
const bookAll = host.querySelector('#rec-book-all');
|
|||
|
|
if (bookAll) bookAll.onclick = () => bookAllLightning(host, pending, refresh);
|
|||
|
|
const delAll = host.querySelector('#rec-delete-all');
|
|||
|
|
if (delAll) delAll.onclick = async () => {
|
|||
|
|
if (!(await dialog.confirm({ message: t('home.recDeleteAllConfirm'), danger: true }))) return;
|
|||
|
|
await deleteAllPendingProposals(); refresh();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
host.querySelectorAll('[data-prop]').forEach(row => {
|
|||
|
|
const id = Number(row.dataset.prop);
|
|||
|
|
const form = row.querySelector('.prop-form');
|
|||
|
|
row.querySelectorAll('[data-act]').forEach(btn => btn.onclick = async () => {
|
|||
|
|
const act = btn.dataset.act;
|
|||
|
|
if (act === 'ignore') { await ignoreProposal(id); refresh(); return; }
|
|||
|
|
if (act === 'internal' || act === 'interest') {
|
|||
|
|
try { await (act === 'internal' ? resolveProposalAsInternal(id) : resolveProposalAsInterest(id)); refresh(); }
|
|||
|
|
catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); }
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (act === 'link') {
|
|||
|
|
openMatchPopover(btn, { kind: 'tx', id: Number(row.dataset.wtx), assetId: Number(row.dataset.asset), onChange: refresh });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (act === 'trade') { showTradeForm(refresh, pending.find(x => x.id === id), form); return; }
|
|||
|
|
await showTransferForm(refresh, id, form);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
host.querySelectorAll('[data-del-prop]').forEach(btn => btn.onclick = async () => {
|
|||
|
|
if (!(await dialog.confirm({ message: t('home.recDeleteConfirm'), danger: true }))) return;
|
|||
|
|
await deleteProposal(Number(btn.dataset.delProp)); refresh();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
host.querySelectorAll('[data-revert]').forEach(btn => btn.onclick = async () => {
|
|||
|
|
if (!(await dialog.confirm({ message: t('home.recRevertConfirm') }))) return;
|
|||
|
|
try { await revertProposal(Number(btn.dataset.revert)); refresh(); }
|
|||
|
|
catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); }
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const rematch = host.querySelector('#rec-rematch');
|
|||
|
|
if (rematch) rematch.onclick = () => runUpdate(host, refresh);
|
|||
|
|
return pending.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function shortTx(h) { return h ? `${h.slice(0, 8)}…${h.slice(-4)}` : ''; }
|
|||
|
|
|
|||
|
|
function momentCel(p) {
|
|||
|
|
const bevestigd = !!p.tx_block_time;
|
|||
|
|
const wanneer = bevestigd ? p.tx_block_time : p.detectedAt;
|
|||
|
|
const uitleg = bevestigd ? t('home.recTxAt') : t('home.recDetectedAt');
|
|||
|
|
return `<span class="t-muted-cell" title="${esc(uitleg)}">(${esc(localDateTimeNl(wanneer))})</span>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function recRow(p, wiz, currentId) {
|
|||
|
|
const dir = p.direction === 'in' ? t('home.recIn') : t('home.recOut');
|
|||
|
|
const wtxAttrs = p.walletTransactionId ? ` data-wtx="${p.walletTransactionId}" data-asset="${p.assetId}"` : '';
|
|||
|
|
const linkBtn = p.walletTransactionId ? `<button class="btn btn-secondary btn-xs" data-act="link">${t('home.recLink')}</button>` : '';
|
|||
|
|
const cls = wiz ? 'rec-item rec-wiz-item' : `rec-item${p.id === currentId ? ' is-current' : ''}`;
|
|||
|
|
const laag = p.tx_layer === 'lightning'
|
|||
|
|
? ` · ${t('home.recLightning')}${p.via_wallet != null
|
|||
|
|
? ` ${t('home.recVia', { wallet: p.via_wallet || t('home.recViaNwc') })}` : ''}`
|
|||
|
|
: '';
|
|||
|
|
const intern = p.tx_kind ? ` · ${t(`home.recInternal_${p.tx_kind}`)}` : '';
|
|||
|
|
const internLabel = p.tx_node_account
|
|||
|
|
? t('home.recAsChannelTransfer', { account: p.tx_node_account }) : t('home.recAsInternal');
|
|||
|
|
const internBtn = p.tx_kind
|
|||
|
|
? `<button class="btn btn-primary btn-xs" data-act="internal" title="${esc(t('home.recInternalHint'))}">${esc(internLabel)}</button>` : '';
|
|||
|
|
const forward = isForward(p);
|
|||
|
|
const renteBtn = forward
|
|||
|
|
? `<button class="btn btn-primary btn-xs" data-act="interest" title="${esc(t('home.recInterestHint'))}">${t('home.recAsInterest')}</button>` : '';
|
|||
|
|
const eerste = p.tx_kind || forward;
|
|||
|
|
const memo = memoLine(p.tx_memo, p.direction, 120);
|
|||
|
|
const memoHtml = memo ? `<div class="t-body-sm t-muted" title="${esc(memo.title)}">${esc(memo.text)}</div>` : '';
|
|||
|
|
return `<div class="${cls}" data-prop="${p.id}"${wtxAttrs}>
|
|||
|
|
<div class="rec-item-row">
|
|||
|
|
<div class="rec-item-meta"><strong>${val(`${fmtInt(p.amount)} sat`)}</strong> ${assetLabelHtml(p)} ${dir}${esc(laag)}${esc(intern)} · ${esc(p.account_name)}
|
|||
|
|
${momentCel(p)}${p.transactionHash ? ` <span class="t-muted-cell" title="${esc(p.transactionHash)}">· ${esc(shortTx(p.transactionHash))}</span>` : ''}${memoHtml}</div>
|
|||
|
|
<div class="row-actions rec-hover">
|
|||
|
|
${linkBtn}
|
|||
|
|
${internBtn}${renteBtn}
|
|||
|
|
<button class="btn ${eerste ? 'btn-secondary' : 'btn-primary'} btn-xs" data-act="trade">${t('home.recAsTrade')}</button>
|
|||
|
|
<button class="btn btn-secondary btn-xs" data-act="transfer">${t('home.recAsTransfer')}</button>
|
|||
|
|
<button class="btn btn-ghost btn-xs" data-act="ignore">${t('home.recIgnore')}</button>
|
|||
|
|
<button class="btn btn-ghost btn-xs" data-del-prop="${p.id}" title="${t('home.recDelete')}">✕</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="prop-form mt-sm hidden"></div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function drawerSection(pending, handled, current, expand) {
|
|||
|
|
const total = pending.length + handled.length;
|
|||
|
|
const teBoeken = bulkPlan(pending).filter(s => s.action !== 'skip').length;
|
|||
|
|
const bulk = `<div class="rec-bulk">
|
|||
|
|
${teBoeken ? `<button class="btn btn-primary btn-xs" id="rec-book-all">${t('home.recBookAll', { n: teBoeken })}</button>` : ''}
|
|||
|
|
<button class="btn btn-secondary btn-xs" id="rec-ignore-all" ${pending.length ? '' : 'disabled'}>${t('home.recIgnoreAll')}</button>
|
|||
|
|
<button class="btn btn-danger btn-xs" id="rec-delete-all" ${pending.length ? '' : 'disabled'}>${t('home.recDeleteAll')}</button>
|
|||
|
|
</div>`;
|
|||
|
|
const rows = total === 0
|
|||
|
|
? `<div class="t-body-sm t-muted" style="padding:0.5rem 0">${t('home.recHandledNone')}</div>`
|
|||
|
|
: pending.map(p => recRow(p, false, current ? current.id : 0)).join('') + handled.map(handledRow).join('');
|
|||
|
|
return `<details class="mt-lg" ${expand.attr('drawer')}>
|
|||
|
|
<summary class="t-label" style="cursor:pointer">${t('home.recAll')} (${total})</summary>
|
|||
|
|
${bulk}
|
|||
|
|
<div class="mt-sm">${rows}</div>
|
|||
|
|
</details>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const STATUS_BADGE = {
|
|||
|
|
accepted_trade: 'badge-pos',
|
|||
|
|
accepted_transfer: 'badge-pos',
|
|||
|
|
accepted_interest: 'badge-pos',
|
|||
|
|
ignored: 'badge-neutral',
|
|||
|
|
auto_linked: 'badge-outline',
|
|||
|
|
};
|
|||
|
|
const REVERTIBLE = new Set(['accepted_trade', 'accepted_transfer', 'accepted_interest', 'ignored']);
|
|||
|
|
|
|||
|
|
function handledRow(p) {
|
|||
|
|
const dir = p.direction === 'in' ? t('home.recIn') : t('home.recOut');
|
|||
|
|
const badge = `<span class="badge ${STATUS_BADGE[p.status] || 'badge-neutral'}">${t('enums.reconcile_status.' + p.status)}</span>`;
|
|||
|
|
const revert = REVERTIBLE.has(p.status)
|
|||
|
|
? `<button class="btn btn-ghost btn-xs" data-revert="${p.id}">${t('home.recRevert')}</button>` : '';
|
|||
|
|
const del = p.status !== 'auto_linked'
|
|||
|
|
? `<button class="btn btn-ghost btn-xs" data-del-prop="${p.id}" title="${t('home.recDelete')}">✕</button>` : '';
|
|||
|
|
return `<div class="rec-item rec-handled">
|
|||
|
|
<div class="rec-item-row">
|
|||
|
|
<div class="rec-item-meta"><strong>${val(`${fmtInt(p.amount)} sat`)}</strong> ${assetLabelHtml(p)} ${dir} · ${esc(p.account_name)}
|
|||
|
|
${momentCel(p)}${p.transactionHash ? ` <span class="t-muted-cell" title="${esc(p.transactionHash)}">· ${esc(shortTx(p.transactionHash))}</span>` : ''}</div>
|
|||
|
|
<div class="row-actions"><span class="row-actions rec-hover">${revert}${del}</span>${badge}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let updating = false;
|
|||
|
|
async function runUpdate(host, refresh) {
|
|||
|
|
if (updating) return;
|
|||
|
|
updating = true;
|
|||
|
|
const btn = host.querySelector('#rec-rematch');
|
|||
|
|
const prog = host.querySelector('#rec-progress');
|
|||
|
|
if (btn) btn.disabled = true;
|
|||
|
|
try {
|
|||
|
|
for (const s of (await listAllWalletSources()).filter(isOnchainSource)) {
|
|||
|
|
const label = s.name ? `${s.account_name} · ${s.name}` : s.account_name;
|
|||
|
|
if (prog) prog.textContent = `${t('home.updating')} ${label}`;
|
|||
|
|
try { await scanSource(s, txt => { if (prog) prog.textContent = `${label}: ${txt}`; }); }
|
|||
|
|
catch (e) { console.error('[reconcile-update]', e); }
|
|||
|
|
}
|
|||
|
|
await syncAddressLinks();
|
|||
|
|
} finally {
|
|||
|
|
updating = false;
|
|||
|
|
}
|
|||
|
|
refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function bookAllLightning(host, pending, refresh) {
|
|||
|
|
const plan = bulkPlan(pending);
|
|||
|
|
const n = bulkCounts(plan);
|
|||
|
|
const ok = await dialog.confirm({
|
|||
|
|
message: t('home.recBookAllConfirm', { trade: n.trade, internal: n.internal, interest: n.interest, skipped: n.onchain }),
|
|||
|
|
});
|
|||
|
|
if (!ok) return;
|
|||
|
|
const prog = host.querySelector('#rec-progress');
|
|||
|
|
const teller = { booked: 0, noPrice: 0, failed: 0 };
|
|||
|
|
const werk = plan.filter(s => s.action !== 'skip');
|
|||
|
|
for (const [i, s] of werk.entries()) {
|
|||
|
|
if (prog) prog.textContent = t('home.recBookAllProgress', { i: i + 1, n: werk.length });
|
|||
|
|
try {
|
|||
|
|
if (s.action === 'internal') {
|
|||
|
|
await resolveProposalAsInternal(s.id);
|
|||
|
|
} else if (s.action === 'interest') {
|
|||
|
|
await resolveProposalAsInterest(s.id);
|
|||
|
|
} else {
|
|||
|
|
const k = await koersVoor(s.proposal);
|
|||
|
|
if (!k) { teller.noPrice++; continue; }
|
|||
|
|
await resolveProposalAsTrade(s.id, { fiatPrice: k.price, priceOrigin: k.origin });
|
|||
|
|
}
|
|||
|
|
teller.booked++;
|
|||
|
|
} catch (e) {
|
|||
|
|
teller.failed++;
|
|||
|
|
console.error('[reconcile-book-all]', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
await dialog.alert({ title: t('home.recBookAllDoneTitle'), message: t('home.recBookAllDone', teller) });
|
|||
|
|
refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function koersVoor(p) {
|
|||
|
|
const asset = await onchainAsset();
|
|||
|
|
if (!asset || asset.id !== p.assetId) return null;
|
|||
|
|
const dag = localDate(p.tx_block_time || p.detectedAt);
|
|||
|
|
const rond = n => Math.round(n * 100) / 100;
|
|||
|
|
const moment = p.tx_block_time ? await spotAtMoment(asset, p.tx_block_time) : null;
|
|||
|
|
if (moment) return { price: rond(moment.price), origin: 'market_moment', at: moment.at, dag, asset };
|
|||
|
|
const koers = await spotOnDate(asset, dag);
|
|||
|
|
return koers == null ? null : { price: rond(koers), origin: 'market_day', dag, asset };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function vulKoers(p, form) {
|
|||
|
|
const input = form.querySelector('[data-price]');
|
|||
|
|
const hint = form.querySelector('[data-spot]');
|
|||
|
|
hint.textContent = t('home.recPriceLoading');
|
|||
|
|
try {
|
|||
|
|
const k = await koersVoor(p);
|
|||
|
|
if (!k) { hint.textContent = t('home.recPriceUnknown'); return null; }
|
|||
|
|
input.value = String(k.price);
|
|||
|
|
input.dataset.filled = input.value;
|
|||
|
|
input.dataset.origin = k.origin;
|
|||
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|||
|
|
hint.textContent = k.origin === 'market_moment'
|
|||
|
|
? t('home.recPriceAt', { time: localDateTimeNl(new Date(k.at).toISOString()), unit: priceUnitLabel(k.asset) })
|
|||
|
|
: t('home.recPriceFrom', { date: dayNl(k.dag), unit: priceUnitLabel(k.asset) });
|
|||
|
|
return k.price;
|
|||
|
|
} catch {
|
|||
|
|
hint.textContent = t('home.recPriceUnknown');
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function koppelBedragEnPrijs(p, form) {
|
|||
|
|
const prijsEl = form.querySelector('[data-price]');
|
|||
|
|
const totaalEl = form.querySelector('[data-total]');
|
|||
|
|
onchainAsset().then(asset => {
|
|||
|
|
if (!asset || asset.id !== p.assetId) return;
|
|||
|
|
const dec = asset.decimals;
|
|||
|
|
const rond = n => (n == null ? '' : String(Math.round(n * 100) / 100));
|
|||
|
|
const uitPrijs = () => {
|
|||
|
|
const basis = totalFromPrice(p.amount, dec, priceToBase(prijsEl.value, asset));
|
|||
|
|
totaalEl.value = rond(basis);
|
|||
|
|
};
|
|||
|
|
const uitTotaal = () => {
|
|||
|
|
const prijs = priceFromTotal(totaalEl.value, p.amount, dec);
|
|||
|
|
prijsEl.value = prijs === null ? '' : rond(priceFromBase(prijs, asset));
|
|||
|
|
};
|
|||
|
|
prijsEl.addEventListener('input', uitPrijs);
|
|||
|
|
totaalEl.addEventListener('input', uitTotaal);
|
|||
|
|
if (prijsEl.value !== '') uitPrijs();
|
|||
|
|
}).catch(() => { });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showTradeForm(refresh, p, form) {
|
|||
|
|
if (!p) return;
|
|||
|
|
form.classList.remove('hidden');
|
|||
|
|
form.innerHTML = `
|
|||
|
|
<div class="flex gap-md" style="flex-wrap:wrap;align-items:flex-end">
|
|||
|
|
<div class="form-group" style="margin-bottom:0">
|
|||
|
|
<label class="form-label">${t('home.recTotal', { munt: baseCurrency() })}</label>
|
|||
|
|
<input type="number" class="input" data-total min="0" step="any" style="min-width:140px">
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group" style="margin-bottom:0">
|
|||
|
|
<label class="form-label">${t('home.recPrice', { munt: baseCurrency() })}</label>
|
|||
|
|
<input type="number" class="input" data-price min="0" step="any" style="min-width:160px">
|
|||
|
|
<div class="form-hint" data-spot></div>
|
|||
|
|
</div>
|
|||
|
|
<button class="btn btn-primary btn-sm" data-confirm>${t('home.recConfirm')}</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-error" data-err></div>`;
|
|||
|
|
|
|||
|
|
vulKoers(p, form);
|
|||
|
|
koppelBedragEnPrijs(p, form);
|
|||
|
|
|
|||
|
|
form.querySelector('[data-confirm]').onclick = async () => {
|
|||
|
|
const err = form.querySelector('[data-err]'); err.textContent = '';
|
|||
|
|
const prijsEl = form.querySelector('[data-price]');
|
|||
|
|
const veld = prijsEl.value;
|
|||
|
|
const price = veld === '' ? await vulKoers(p, form) : parseFloat(veld);
|
|||
|
|
if (!Number.isFinite(price)) { err.textContent = t('home.recPriceRequired'); return; }
|
|||
|
|
const gevuld = prijsEl.dataset.filled;
|
|||
|
|
const priceOrigin = gevuld !== undefined && Number(gevuld) === price ? (prijsEl.dataset.origin || 'market_day') : 'paid';
|
|||
|
|
try { await resolveProposalAsTrade(p.id, { fiatPrice: price, priceOrigin }); refresh(); }
|
|||
|
|
catch (e) { err.textContent = e.message; }
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function showTransferForm(refresh, id, form) {
|
|||
|
|
form.classList.remove('hidden');
|
|||
|
|
const opts = (await listSelectableAccounts()).map(a => `<option value="${a.id}">${esc(a.name)}</option>`).join('');
|
|||
|
|
form.innerHTML = `
|
|||
|
|
<div class="flex gap-md" style="flex-wrap:wrap;align-items:flex-end">
|
|||
|
|
<div class="form-group" style="margin-bottom:0">
|
|||
|
|
<label class="form-label">${t('home.recCounter')}</label>
|
|||
|
|
<select class="input" data-counter style="min-width:180px">${opts}</select>
|
|||
|
|
</div>
|
|||
|
|
<button class="btn btn-primary btn-sm" data-confirm>${t('home.recConfirm')}</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-error" data-err></div>`;
|
|||
|
|
form.querySelector('[data-confirm]').onclick = async () => {
|
|||
|
|
const err = form.querySelector('[data-err]'); err.textContent = '';
|
|||
|
|
const other = Number(form.querySelector('[data-counter]').value);
|
|||
|
|
try { await resolveProposalAsTransfer(id, { other_account_id: other }); refresh(); }
|
|||
|
|
catch (e) { err.textContent = e.message; }
|
|||
|
|
};
|
|||
|
|
}
|