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

242 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Generated, do not edit. sha256:7498a98f290e59aca597eb4a57338d467525bf6c2933429d69f5e8474930a0ac
import { aiProposalsStatus, acceptAiProposal, acceptAiProposals, rejectAiProposal, rejectAiProposals }
from '../data/ai-proposals.js';
import { CONFIG, PREFERENCES } from '../../shared/config.js';
import { appSetting, timerSetting, loadAppSettings } from '../app-settings.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 { fmtBase, fmtBasePrice, fmtUnit, priceFromBase, val, currencyName } from '../ui/format.js';
import { taskCardHtml, nothingPendingHtml } from '../ui/task-card.js';
import { priceUnitLabel } from '../../shared/units.js';
import { localDateNl } from '../../shared/time.js';
const ONOMKEERBAAR = new Set(CONFIG.AI.DESTRUCTIVE_PROPOSAL_KINDS);
const assetVan = p => ({ asset_name: p.asset_name, asset_symbol: p.asset_symbol });
function detailTekst(detail) {
if (typeof detail !== 'string' || !detail.startsWith('{')) return detail;
try {
const o = JSON.parse(detail);
return o && o.key ? t(o.key, o.params) : detail;
} catch { return detail; }
}
const meta = p => ({
kind: p.asset_kind, decimals: p.asset_decimals, unit: p.asset_unit,
subunit: p.asset_subunit, displayUnit: p.asset_display_unit,
});
const eenheidIsSymbool = p => {
const asset = meta(p);
const getoond = asset.displayUnit === 'subunit' && asset.subunit ? asset.subunit : asset.unit;
return String(getoond || '').toUpperCase() === String(p.asset_symbol || '').toUpperCase();
};
const koers = p => {
const asset = meta(p);
return `${fmtBasePrice(priceFromBase(p.fiatPrice, asset))} ${t('aiProposals.perUnit')} ${priceUnitLabel(asset)}`;
};
const leesbareWaarde = (p, json) => {
try { return toon(p, JSON.parse(json)); }
catch { return String(json); }
};
const toon = (p, v) => {
const spec = PREFERENCES[p.scope]?.[p.key];
if (spec?.type === 'bool') return t(Number(v) ? 'aiProposals.preferenceOn' : 'aiProposals.preferenceOff');
return String(v);
};
const huidigeVoorkeur = (p) => {
try {
const v = p.scope === 'timers' ? timerSetting(p.key) : appSetting(p.key);
return v == null ? null : toon(p, v);
} catch { return null; }
};
const OMSCHRIJVING = {
price_point: p =>
`${t('aiProposals.kindPricePoint')}: ${assetLabelHtml(assetVan(p))} · ${val(koers(p))} `
+ `<span class="t-muted-cell">${esc(localDateNl(p.observedAt))}</span>`,
account: p =>
`${t('aiProposals.kindAccount')}: <strong>${esc(p.name)}</strong> `
+ `<span class="t-muted-cell">${esc(t('enums.account_kind.' + p.accountKind))} · `
+ `${esc(p.isOwnCustody ? t('aiProposals.ownCustody') : t('aiProposals.thirdParty'))}</span>`,
account_delete: p =>
`${t('aiProposals.kindAccountDelete')}: <strong>${esc(p.account_name)}</strong> `
+ `<span class="t-muted-cell">${esc(t('enums.account_kind.' + p.account_kind))}</span>`,
portfolio: p =>
`${t('aiProposals.kindPortfolio')}: <strong>${esc(p.name)}</strong> `
+ `<span class="t-muted-cell">${esc(t('aiProposals.portfolioIn'))} ${esc(currencyName(p.currency))} (${esc(p.currency)})</span><br>`
+ `<span class="t-muted-cell">${esc(t('aiProposals.portfolioNote'))}</span>`,
preference: p => {
const nu = huidigeVoorkeur(p);
return `${t('aiProposals.kindPreference')}: <strong>${esc(p.scope)}.${esc(p.key)}</strong> `
+ `→ ${val(leesbareWaarde(p, p.value))}`
+ (nu === null ? '' : ` <span class="t-muted-cell">(${esc(t('aiProposals.preferenceCurrent'))} ${esc(nu)})</span>`)
+ `<br><span class="t-muted-cell">${esc(t('aiProposals.preferenceNote'))}</span>`;
},
source_choice: p =>
`${t('aiProposals.kindSourceChoice')}: ${assetLabelHtml(assetVan(p))} · `
+ `${esc(t('integrations.sourceCap.' + p.scope))} · <strong>${esc(p.provider_name || p.key)}</strong> `
+ `${esc(t(p.value === 'first' ? 'aiProposals.sourceFirst' : 'aiProposals.sourceRemove'))}<br>`
+ `<span class="t-muted-cell">${esc(t('aiProposals.sourceChoiceNote'))}</span>`,
valuation: p => {
const asset = meta(p);
const grootboek = p.ledger_balance == null ? null : Number(p.ledger_balance);
const verschil = grootboek == null ? null : Number(p.balance) - grootboek;
const naast = grootboek == null ? '' :
` · ${t('aiProposals.ledger')} ${val(fmtUnit(grootboek, asset))}`
+ (verschil === 0 ? '' : ` · ${t('aiProposals.diff')} ${verschil > 0 ? '+' : '−'}${val(fmtUnit(Math.abs(verschil), asset))}`);
return `${t('aiProposals.kindValuation')}: ${val(fmtUnit(p.balance, asset))} `
+ `${assetLabelHtml(assetVan(p), { withoutSymbol: eenheidIsSymbool(p) })} `
+ `· <strong>${esc(p.account_name || '')}</strong>${naast}<br>`
+ `<span class="t-muted-cell">${esc(localDateNl(p.observedAt))}</span>`;
},
event: p => {
const waar = [p.from_name, p.to_name].filter(Boolean).map(esc).join(' → ');
const prijs = p.fiatPrice == null ? '' : ` · ${val(koers(p))}`;
const bedragen = [
p.fiatFee == null ? null : val(fmtBase(p.fiatFee)),
p.unitFee == null ? null : val(fmtUnit(p.unitFee, meta(p))),
].filter(Boolean);
const kosten = bedragen.length ? ` · ${t('aiProposals.fee')} ${bedragen.join(' + ')}` : '';
return `${esc(t('enums.event_type.' + p.eventType))}: ${val(fmtUnit(p.amount, meta(p)))} `
+ `${assetLabelHtml(assetVan(p), { withoutSymbol: eenheidIsSymbool(p) })}${prijs}${kosten}<br>`
+ `<span class="t-muted-cell">${waar} · ${esc(localDateNl(p.observedAt))}</span>`;
},
};
function regel(p) {
const maak = OMSCHRIJVING[p.kind];
const tekst = maak ? maak(p) : esc(t('aiProposals.unknownKind') + ` (${p.kind})`);
const notitie = p.note ? `<div class="t-body-sm t-muted">${esc(p.note)}</div>` : '';
const mislukt = p.detail ? `<div class="t-body-sm t-neg">${esc(t('aiProposals.failed'))}: ${esc(detailTekst(p.detail))}</div>` : '';
const weg = ONOMKEERBAAR.has(p.kind);
const waarschuwing = weg
? `<div class="t-body-sm t-neg">${esc(t('aiProposals.irreversible'))}</div>` : '';
const opening = p.kind === 'valuation' && p.events_before === 0
? `<div class="t-body-sm t-neg">${esc(t('aiProposals.openingBalance'))}</div>` : '';
return `<div class="rec-item" data-aiprop="${p.id}" data-aidanger="${weg ? 1 : 0}" data-aikind="${esc(p.kind)}">
<div class="rec-item-row">
<div class="rec-item-meta">${tekst}${notitie}${waarschuwing}${opening}${mislukt}</div>
<div class="row-actions rec-hover">
<button class="btn ${weg ? 'btn-danger' : 'btn-primary'} btn-xs" data-aiact="accept">${weg ? t('aiProposals.acceptDelete') : t('aiProposals.accept')}</button>
<button class="btn btn-ghost btn-xs" data-aiact="reject">${t('aiProposals.reject')}</button>
</div>
</div>
</div>`;
}
function redenTekst(u) {
if (!u.key) return u.reden;
try { return t(u.key, u.params); } catch { return u.reden; }
}
async function meldMislukt(uitkomsten) {
const stuk = uitkomsten.filter(u => !u.ok);
if (!stuk.length) return;
await dialog.alert({
title: t('aiProposals.partialTitle'),
message: `${t('aiProposals.partialBody')}\n\n${stuk.map(u => `- ${redenTekst(u)}`).join('\n')}`,
});
}
let live = null;
let liveGebonden = false;
function bindLive() {
if (liveGebonden) return;
liveGebonden = true;
document.addEventListener('ai-proposals-changed', async () => {
if (!live || !live.host || !live.host.isConnected) { live = null; return; }
try { await renderAiProposals(live.host, live.refresh); } catch { }
});
}
export async function renderAiProposals(host, refresh) {
live = { host, refresh };
bindLive();
const { enabled, pending } = await aiProposalsStatus();
if (!enabled) { host.innerHTML = ''; return null; }
if (!pending.length) {
host.innerHTML = taskCardHtml({ title: t('aiProposals.title'), body: nothingPendingHtml() });
return 0;
}
const teVerwijderen = pending.filter(p => ONOMKEERBAAR.has(p.kind)).length;
const bulkUitleg = teVerwijderen
? `<div class="t-body-sm t-neg" style="margin-bottom:0.6rem">${esc(t('aiProposals.bulkSkips').replace('{n}', teVerwijderen))}</div>`
: '';
host.innerHTML = taskCardHtml({
title: t('aiProposals.title'),
actions: `<div class="row-actions">
<button class="btn btn-secondary btn-xs" id="aiprop-accept-all">${t('aiProposals.acceptAll')}</button>
<button class="btn btn-ghost btn-xs" id="aiprop-reject-all">${t('aiProposals.rejectAll')}</button>
</div>`,
body: `<div class="t-body-sm t-muted" style="margin-bottom:0.6rem">${t('aiProposals.intro')}</div>
${bulkUitleg}
${pending.map(regel).join('')}`,
});
const ids = pending.filter(p => !ONOMKEERBAAR.has(p.kind)).map(p => p.id);
const alleIds = pending.map(p => p.id);
host.querySelector('#aiprop-accept-all').onclick = async () => {
if (!ids.length) {
await dialog.alert({ title: t('aiProposals.title'), message: t('aiProposals.bulkNothing') });
return;
}
if (!(await dialog.confirm({ message: t('aiProposals.acceptAllConfirm').replace('{n}', ids.length) }))) return;
await meldMislukt(await acceptAiProposals(ids));
if (pending.some(p => ids.includes(p.id) && p.kind === 'preference')) {
await loadAppSettings();
location.reload();
return;
}
refresh();
};
host.querySelector('#aiprop-reject-all').onclick = async () => {
if (!(await dialog.confirm({ message: t('aiProposals.rejectAllConfirm').replace('{n}', alleIds.length), danger: true }))) return;
await meldMislukt(await rejectAiProposals(alleIds, t('aiProposals.rejectedByUser')));
refresh();
};
host.querySelectorAll('[data-aiprop]').forEach(rij => {
const id = Number(rij.dataset.aiprop);
rij.querySelectorAll('[data-aiact]').forEach(knop => knop.onclick = async () => {
try {
if (knop.dataset.aiact === 'accept') {
if (rij.dataset.aidanger === '1'
&& !(await dialog.confirm({ message: t('aiProposals.acceptDeleteConfirm'), danger: true }))) return;
await acceptAiProposal(id);
if (rij.dataset.aikind === 'preference') {
await loadAppSettings();
location.reload();
return;
}
} else await rejectAiProposal(id, t('aiProposals.rejectedByUser'));
} catch (e) {
await dialog.alert({ title: t('dialog.error'), message: e.message });
}
refresh();
});
});
return pending.length;
}