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(', ');
return { detail, comptes: relevant.length };
},
// Flags a "versement freelance" row that isn't (fully) backed by
// the entrée client(s) it pays out against: either not linked at
// all, or linked but its own compte(s) still show a residual
// against at least one of them. Deliberately scoped to just the
// compte(s) this versement's own répartition touches
// (reconciliationByEntree's parCompteResidual), not the entrée's
// overall resteAVerser/surVerse -- those can be driven entirely by
// a *different* compte tied to some other sortie linked to the
// same entrée, which says nothing about 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.
// Entrées outside the currently loaded window are skipped (same
// accepted trade-off as reconciliationByEntree itself); null only
// if none of them could be checked at all.
// Status badge for a "versement freelance" row -- always present
// when the row is a versement, specifically so a linked-but-settled
// versement still gets a badge to drill down through (it used to
// return null there, silently losing the only way to open the
// linked entrée's filtered view for versements with nothing wrong
// to report). "kind" distinguishes not-linked-at-all, a residual
// against at least one linked entrée, or linked-and-settled:
// - not linked at all
// - linked but its own compte(s) still show a residual against at
// least one linked entrée -- deliberately scoped to just the
// compte(s) this versement's own répartition touches
// (reconciliationByEntree's parCompteResidual), not the
// entrée's overall resteAVerser/surVerse, which can be driven
// entirely by a *different* compte tied to some other sortie
// linked to the same entrée and would say nothing about
// 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) {
if (item.type !== 'versement') return null;
if (!item.entreeLieeIds.length) {
@@ -531,30 +538,39 @@
}
let resteAVerser = 0;
let surVerse = 0;
let checked = 0;
for (const entreeId of item.entreeLieeIds) {
const recon = this.reconciliationByEntree.get(entreeId);
if (!recon) continue;
checked++;
for (const c of Object.keys(item.parCompte)) {
const residual = recon.parCompteResidual[c] || 0;
if (residual > 0.01) resteAVerser += residual;
else if (residual < -0.01) surVerse += -residual;
}
}
if (!checked) return null;
if (resteAVerser > 0.01) {
return { kind: 'reste', detail: 'Reste à verser (comptes de cette ligne) : ' + this.formatEur(Math.round(resteAVerser * 100) / 100) };
}
if (surVerse > 0.01) {
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) {
if (kind === 'non-liee') return 'Non liée';
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
// width in a table already packed with 8 compte columns.
@@ -161,10 +161,10 @@
<span
v-if="versementStatus(item)"
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"
@click="item.entreeLieeIds.length && toggleEntreeFilter(item.entreeLieeIds[0])"
>{{ versementStatusLabel(versementStatus(item).kind) }}</span>
>{{ versementStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ versementStatusLabel(versementStatus(item).kind) }}</span>
</td>
<td class="amount" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td>
<td class="amount">{{ formatEur(item.montant_ttc) }}</td>