Show a badge on linked-and-settled versements too

versementStatus() returned null whenever a linked versement's own
compte(s) had no residual against any linked entrée -- meaning a
versement that was linked but fully reconciled got no badge at all,
silently losing the only way to open its drill-down (the badge is
also the click target). Only "not linked" and "has a residual" ever
rendered one.

Now always returns a status for any versement, adding a fourth kind
("ok": linked, no residual on any checked entrée) alongside
non-liee/reste/sur-verse. Renders as a plain "Lié" badge in the
default green (no is-anomalie/is-reste modifier), matching the
convention the entrée side already uses for a fully-settled "N
sorties liées" badge, and stays clickable since item.entreeLieeIds
is still non-empty.

Verified: several previously badge-less linked versements (e.g.
"facture-SC-Bachir-260329B-FIGLI", "F58_260506_FIGLI") now show a
green "Lié" badge, and clicking one opens the same 3-row drill-down
(entrée + both its linked sorties) as before.
This commit is contained in:
2026-09-05 12:53:30 +02:00
parent 0314625593
commit 0f33d57ec0
2 changed files with 39 additions and 23 deletions
@@ -508,22 +508,29 @@
const detail = relevant.map((compte) => compte + ' : ' + this.formatEur(ecarts[compte])).join(', '); const detail = relevant.map((compte) => compte + ' : ' + this.formatEur(ecarts[compte])).join(', ');
return { detail, comptes: relevant.length }; return { detail, comptes: relevant.length };
}, },
// Flags a "versement freelance" row that isn't (fully) backed by // Status badge for a "versement freelance" row -- always present
// the entrée client(s) it pays out against: either not linked at // when the row is a versement, specifically so a linked-but-settled
// all, or linked but its own compte(s) still show a residual // versement still gets a badge to drill down through (it used to
// against at least one of them. Deliberately scoped to just the // return null there, silently losing the only way to open the
// compte(s) this versement's own répartition touches // linked entrée's filtered view for versements with nothing wrong
// (reconciliationByEntree's parCompteResidual), not the entrée's // to report). "kind" distinguishes not-linked-at-all, a residual
// overall resteAVerser/surVerse -- those can be driven entirely by // against at least one linked entrée, or linked-and-settled:
// a *different* compte tied to some other sortie linked to the // - not linked at all
// same entrée, which says nothing about whether this versement's // - linked but its own compte(s) still show a residual against at
// own répartition is settled. When linked to several entrées (see // least one linked entrée -- deliberately scoped to just the
// reconciliationByEntree's equal-split note), each entrée's // compte(s) this versement's own répartition touches
// residual for these compte(s) counts separately -- they're // (reconciliationByEntree's parCompteResidual), not the
// independent invoices, each with its own outstanding amount. // entrée's overall resteAVerser/surVerse, which can be driven
// Entrées outside the currently loaded window are skipped (same // entirely by a *different* compte tied to some other sortie
// accepted trade-off as reconciliationByEntree itself); null only // linked to the same entrée and would say nothing about
// if none of them could be checked at all. // whether this versement's own répartition is settled. When
// linked to several entrées (see reconciliationByEntree's
// equal-split note), each entrée's residual for these compte(s)
// counts separately -- they're independent invoices, each with
// its own outstanding amount.
// - linked and settled (as far as the currently loaded window can
// tell -- a linked entrée outside it is silently skipped, same
// accepted trade-off as reconciliationByEntree itself)
versementStatus(item) { versementStatus(item) {
if (item.type !== 'versement') return null; if (item.type !== 'versement') return null;
if (!item.entreeLieeIds.length) { if (!item.entreeLieeIds.length) {
@@ -531,30 +538,39 @@
} }
let resteAVerser = 0; let resteAVerser = 0;
let surVerse = 0; let surVerse = 0;
let checked = 0;
for (const entreeId of item.entreeLieeIds) { for (const entreeId of item.entreeLieeIds) {
const recon = this.reconciliationByEntree.get(entreeId); const recon = this.reconciliationByEntree.get(entreeId);
if (!recon) continue; if (!recon) continue;
checked++;
for (const c of Object.keys(item.parCompte)) { for (const c of Object.keys(item.parCompte)) {
const residual = recon.parCompteResidual[c] || 0; const residual = recon.parCompteResidual[c] || 0;
if (residual > 0.01) resteAVerser += residual; if (residual > 0.01) resteAVerser += residual;
else if (residual < -0.01) surVerse += -residual; else if (residual < -0.01) surVerse += -residual;
} }
} }
if (!checked) return null;
if (resteAVerser > 0.01) { if (resteAVerser > 0.01) {
return { kind: 'reste', detail: 'Reste à verser (comptes de cette ligne) : ' + this.formatEur(Math.round(resteAVerser * 100) / 100) }; return { kind: 'reste', detail: 'Reste à verser (comptes de cette ligne) : ' + this.formatEur(Math.round(resteAVerser * 100) / 100) };
} }
if (surVerse > 0.01) { if (surVerse > 0.01) {
return { kind: 'sur-verse', detail: 'Sur-versé (comptes de cette ligne) : ' + this.formatEur(Math.round(surVerse * 100) / 100) }; return { kind: 'sur-verse', detail: 'Sur-versé (comptes de cette ligne) : ' + this.formatEur(Math.round(surVerse * 100) / 100) };
} }
return null; const n = item.entreeLieeIds.length;
return { kind: 'ok', detail: 'Lié à ' + n + ' entrée' + (n > 1 ? 's' : '') + ' client' + (n > 1 ? 's' : '') + '.' };
}, },
versementStatusLabel(kind) { versementStatusLabel(kind) {
if (kind === 'non-liee') return 'Non liée'; if (kind === 'non-liee') return 'Non liée';
if (kind === 'reste') return 'Reste à verser'; if (kind === 'reste') return 'Reste à verser';
return 'Sur-versé'; if (kind === 'sur-verse') return 'Sur-versé';
return 'Lié';
},
// Only non-liee/sur-versé read as a hard anomaly (red); reste is
// its own softer amber; ok gets neither, falling back to the
// badge's default green -- same "all clear" green the entrée side
// already uses for a fully-settled "N sorties liées".
versementStatusClasses(kind) {
return {
'is-anomalie': kind === 'non-liee' || kind === 'sur-verse',
'is-reste': kind === 'reste',
};
}, },
// jj/mm/aa -- shorter than the API's ISO yyyy-mm-dd, saves column // jj/mm/aa -- shorter than the API's ISO yyyy-mm-dd, saves column
// width in a table already packed with 8 compte columns. // width in a table already packed with 8 compte columns.
@@ -161,10 +161,10 @@
<span <span
v-if="versementStatus(item)" v-if="versementStatus(item)"
class="figli-recon-badge" class="figli-recon-badge"
:class="{'is-anomalie': versementStatus(item).kind !== 'reste', 'is-reste': versementStatus(item).kind === 'reste', 'is-clickable': item.entreeLieeIds.length}" :class="[versementStatusClasses(versementStatus(item).kind), {'is-clickable': item.entreeLieeIds.length}]"
:title="item.entreeLieeIds.length ? versementStatus(item).detail + ' -- cliquer pour voir la ou les entrées liées' : versementStatus(item).detail" :title="item.entreeLieeIds.length ? versementStatus(item).detail + ' -- cliquer pour voir la ou les entrées liées' : versementStatus(item).detail"
@click="item.entreeLieeIds.length && toggleEntreeFilter(item.entreeLieeIds[0])" @click="item.entreeLieeIds.length && toggleEntreeFilter(item.entreeLieeIds[0])"
>{{ versementStatusLabel(versementStatus(item).kind) }}</span> >{{ versementStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ versementStatusLabel(versementStatus(item).kind) }}</span>
</td> </td>
<td class="amount" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td> <td class="amount" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td>
<td class="amount">{{ formatEur(item.montant_ttc) }}</td> <td class="amount">{{ formatEur(item.montant_ttc) }}</td>