Highlight versements not (fully) backed by their entrée

For "versement freelance" rows specifically: an amber outline plus a
badge when the versement either isn't linked to any entrée client at
all ("Non liée"), or is linked but reconciliationByEntree still shows
a residual on that entrée ("Reste à verser" / "Sur-versé", with the
amount in the tooltip). The residual belongs to the entrée as a whole,
not to any one sortie -- when several versements share an entrée, each
shows the same aggregate figure, since there's no way to attribute the
shortfall to one specific payment. No highlight when the linked entrée
isn't in the currently loaded window (can't tell either way -- same
accepted trade-off as reconciliationByEntree itself).

Verified: filtering to "Versement freelance" shows 21 of 25 loaded
rows flagged (17 unlinked, 4 with a reste-à-verser residual), the
remaining 4 fully-reconciled rows correctly unflagged, and the amber
outline resolves correctly in dark mode.
This commit is contained in:
2026-09-05 10:40:01 +02:00
parent ef2d7b801d
commit 1ab38bc2a7
3 changed files with 56 additions and 1 deletions
@@ -383,6 +383,35 @@
const detail = relevant.map((compte) => compte + ' : ' + this.formatEur(ecarts[compte])).join(', ');
return { detail, comptes: relevant.length };
},
// Flags a "versement freelance" row that isn't (fully) backed by
// the entrée client it pays out against: either not linked at all,
// or linked but reconciliationByEntree still shows a residual on
// that entrée. The residual belongs to the entrée as a whole, not
// to any one sortie -- when several versements share an entrée,
// each shows the same aggregate residual, since there's no way to
// say which specific one is "the" shortfall. Null (no highlight)
// when the linked entrée isn't in the currently loaded window --
// same accepted trade-off as reconciliationByEntree itself.
versementStatus(item) {
if (item.type !== 'versement') return null;
if (!item.entreeLieeId) {
return { kind: 'non-liee', detail: 'Aucune entrée client liée.' };
}
const recon = this.reconciliationByEntree.get(item.entreeLieeId);
if (!recon) return null;
if (recon.resteAVerser > 0.01) {
return { kind: 'reste', detail: 'Reste à verser sur lentrée liée : ' + this.formatEur(recon.resteAVerser) };
}
if (recon.surVerse > 0.01) {
return { kind: 'sur-verse', detail: 'Sur-versé sur lentrée liée : ' + this.formatEur(recon.surVerse) };
}
return null;
},
versementStatusLabel(kind) {
if (kind === 'non-liee') return 'Non liée';
if (kind === 'reste') return 'Reste à verser';
return 'Sur-versé';
},
// jj/mm/aa -- shorter than the API's ISO yyyy-mm-dd, saves column
// width in a table already packed with 8 compte columns.
formatDate(iso) {