Show which legs a transfer cannot pair, and what would pair them
The transfers screen counted unpaired legs per definition but never said
which transactions they were; finding them meant retyping the definition
into the builder. A ⚠ row now opens onto its legs, newest first, each
naming the missing side and what that side would have had to be:
revolut → ? arriving leg +250.00 in other matching *FROM REVOLUT*,
dated 2026-02-01 to 2026-02-11
That description comes from transfers.Engine.Want, next to
bestCounterpart, so it is stated in the terms the pairing actually uses:
the other half of the definition, the window, and the amount -- exact,
widened only by the definition's own tolerance, and "any amount" across
currencies, where amounts are not compared. When the other account has
nothing imported at all, the line says so, since that is the usual cause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -889,6 +889,9 @@ function mountTransfers(main) {
|
||||
h('button', { type: 'button', onclick: () => refresh().then(() => setStatus('pairing refreshed')) }, 'Refresh pairing')),
|
||||
body);
|
||||
let defs = [];
|
||||
let unpaired = 0;
|
||||
// Which ⚠ rows are open, by position, so a refresh does not fold them away.
|
||||
const open = new Set();
|
||||
|
||||
async function del(positions, question) {
|
||||
if (!confirm(question)) return;
|
||||
@@ -897,6 +900,7 @@ function mountTransfers(main) {
|
||||
positions, expect: positions.map((p) => transferExpect(defs[p])),
|
||||
});
|
||||
setStatus(res.status);
|
||||
open.clear(); // positions shift under a delete
|
||||
await refreshAll();
|
||||
} catch (e) { setError(e); }
|
||||
}
|
||||
@@ -907,8 +911,13 @@ function mountTransfers(main) {
|
||||
res = await api('GET', '/api/transfers');
|
||||
} catch (e) { setError(e); return; }
|
||||
defs = res.transfers;
|
||||
title.replaceChildren('Transfers ', h('span', { class: 'sub' }, res.unpaired
|
||||
? `· ${defs.length} definitions · ${res.unpaired} leg(s) unpaired`
|
||||
unpaired = res.unpaired;
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
title.replaceChildren('Transfers ', h('span', { class: 'sub' }, unpaired
|
||||
? `· ${defs.length} definitions · ${unpaired} leg(s) unpaired — open a ⚠ row to see them`
|
||||
: `· ${defs.length} definitions · every leg paired`));
|
||||
// Only definitions catching nothing at all are pruned. One with unpaired
|
||||
// legs is doing something, and deleting it would hide the problem.
|
||||
@@ -928,9 +937,17 @@ function mountTransfers(main) {
|
||||
h('th', {}, 'Note'), h('th', {}, ''))),
|
||||
h('tbody', {}, defs.map((d) => {
|
||||
let mark = '', cls = 'mark', tip = null;
|
||||
if (d.orphaned > 0) [mark, cls, tip] = ['⚠', 'mark warn', 'Catches legs it cannot pair'];
|
||||
if (d.orphaned > 0) [mark, cls, tip] = [open.has(d.pos) ? '▾' : '⚠', 'mark warn', 'Show the legs it cannot pair'];
|
||||
else if (d.paired === 0) [mark, cls, tip] = ['✗', 'mark neg', 'Matches nothing at all'];
|
||||
return h('tr', {},
|
||||
const toggle = () => {
|
||||
if (open.has(d.pos)) open.delete(d.pos); else open.add(d.pos);
|
||||
draw();
|
||||
};
|
||||
return [h('tr', {
|
||||
class: d.orphaned > 0 ? 'clickable' : null,
|
||||
title: d.orphaned > 0 ? 'Show the legs it cannot pair' : null,
|
||||
onclick: d.orphaned > 0 ? (e) => { if (!e.target.closest('button')) toggle(); } : null,
|
||||
},
|
||||
h('td', { class: 'num muted' }, d.pos + 1),
|
||||
h('td', { class: cls, title: tip }, mark),
|
||||
h('td', {}, d.fromAccount, ' ', h('span', { style: 'font-family: var(--mono)' }, d.fromDesc)),
|
||||
@@ -942,9 +959,22 @@ function mountTransfers(main) {
|
||||
h('td', { class: 'actions' }, h('button', {
|
||||
type: 'button', class: 'danger',
|
||||
onclick: () => del([d.pos], `Delete transfer ${d.pos + 1} (${d.fromAccount} → ${d.toAccount}), ${d.paired} matched?`),
|
||||
}, 'Delete')));
|
||||
}, 'Delete'))),
|
||||
open.has(d.pos) && d.orphaned > 0 ? h('tr', { class: 'legs' }, h('td', { colspan: 9 }, legsTable(d))) : null];
|
||||
})))));
|
||||
}
|
||||
|
||||
// legsTable lists a definition's unpaired legs: which side each is missing,
|
||||
// and what that side would have had to look like to pair.
|
||||
function legsTable(d) {
|
||||
return h('table', { class: 'sub' },
|
||||
h('thead', {}, h('tr', {}, h('th', {}, 'Date'), h('th', { class: 'num' }, 'Amount'), h('th', {}, 'Movement'),
|
||||
h('th', {}, 'Description'), h('th', {}, 'Missing'), h('th', {}, 'Would pair with'))),
|
||||
h('tbody', {}, d.legs.map((l) => h('tr', {},
|
||||
h('td', {}, l.date), h('td', { class: 'num' }, l.amount), h('td', {}, l.movement),
|
||||
h('td', { class: 'desc' }, l.description), h('td', { class: 'warn' }, l.missing + ' leg'),
|
||||
h('td', { class: 'desc muted' }, l.want)))));
|
||||
}
|
||||
refresh();
|
||||
return { refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user