Resolve versement reste-à-verser/sur-versé from the server proactively
The badge on a versement freelance row previously only got the correct status once someone clicked it -- before that it could show "inconnu" (or, worse, silently default to "ok") whenever its linked entrée wasn't in the currently loaded sliding window, which is common: the entrée that got paid can be dated years before or after the versement itself. Also fixed a latent bug in the click path itself: toggleEntreeFilter() passed the *entrée's* id to loadEntreeGroup(), which needs an already-loaded row to find a starting nid for the server-side query -- if that entrée wasn't loaded (the exact case this is all about), the lookup silently found nothing and did nothing. Split the fetch+merge logic into fetchGroupFromNid() and let loadEntreeGroup() accept a fallbackNid (the versement's own, always loaded) to start the traversal from when the entrée itself isn't available locally -- LedgerStatsController::groupeEntree() finds the same connected component either way. New ensureLinkedReconciliationResolved(), triggered by a `rows` watcher after every load/loadOlder/loadNewer, calls the same server-backed resolution for every visible linkable row instead of waiting for a click, deduplicated via resolvedLinkGroups so rows sharing an entrée don't each trigger their own fetch. Verified live: the LA MINE versement (own date 2022, linked entrées dated 2023 and 2025) now shows "⚠ Sur-versé" immediately on page load with zero clicks, where before it required manually opening the badge first. Drill-down modal and the "Non liée" -> link-form click path both still work; no console errors on repeated fresh loads.
This commit is contained in:
@@ -346,6 +346,12 @@
|
|||||||
// group doesn't need to re-fetch.
|
// group doesn't need to re-fetch.
|
||||||
groupExtraRows: [],
|
groupExtraRows: [],
|
||||||
groupLoading: false,
|
groupLoading: false,
|
||||||
|
// Entrée ids whose full linked group has already been resolved
|
||||||
|
// by ensureLinkedReconciliationResolved() below (or by opening
|
||||||
|
// the drill-down modal for it) -- avoids re-fetching the same
|
||||||
|
// group again for every other sortie that happens to link to
|
||||||
|
// the same entrée.
|
||||||
|
resolvedLinkGroups: new Set(),
|
||||||
// Which row's type badge is currently showing its inline <select>
|
// Which row's type badge is currently showing its inline <select>
|
||||||
// instead of the badge -- only one at a time.
|
// instead of the badge -- only one at a time.
|
||||||
editingTypeId: null,
|
editingTypeId: null,
|
||||||
@@ -728,45 +734,83 @@
|
|||||||
// Opens/closes the entrée + linked sorties drill-down modal -- a
|
// Opens/closes the entrée + linked sorties drill-down modal -- a
|
||||||
// separate overlay (see the template), not a filter applied to the
|
// separate overlay (see the template), not a filter applied to the
|
||||||
// main table, so opening/closing it never touches the main table's
|
// main table, so opening/closing it never touches the main table's
|
||||||
// scroll position.
|
// scroll position. fallbackNid: see loadEntreeGroup() below --
|
||||||
toggleEntreeFilter(id) {
|
// clicking a versement's badge passes the versement's own nid,
|
||||||
|
// since the entrée id alone isn't enough to start the server-side
|
||||||
|
// lookup when that entrée isn't loaded at all.
|
||||||
|
toggleEntreeFilter(id, fallbackNid) {
|
||||||
if (this.filterEntreeId === id) {
|
if (this.filterEntreeId === id) {
|
||||||
this.closeDrilldown();
|
this.closeDrilldown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.filterEntreeId = id;
|
this.filterEntreeId = id;
|
||||||
this.loadEntreeGroup(id);
|
this.loadEntreeGroup(id, fallbackNid);
|
||||||
},
|
},
|
||||||
closeDrilldown() {
|
closeDrilldown() {
|
||||||
this.filterEntreeId = null;
|
this.filterEntreeId = null;
|
||||||
},
|
},
|
||||||
// Resolves the full transitive-closure group for the given row id
|
// Low-level: fetches /lignes/api/groupe/{nid}'s full member list
|
||||||
// via /lignes/api/groupe/{nid} (the sliding window can't discover
|
// (the sliding window can't discover it client-side -- see
|
||||||
// it client-side -- see LedgerStatsController::groupeEntree()), and
|
// LedgerStatsController::groupeEntree()) and merges whatever isn't
|
||||||
// fetches full ligne data for whatever member isn't already known
|
// already known locally into groupExtraRows. filterEntreeGroup/
|
||||||
// locally, merging it into groupExtraRows. filterEntreeGroup/
|
|
||||||
// reconciliationByEntree re-evaluate automatically once that
|
// reconciliationByEntree re-evaluate automatically once that
|
||||||
// lands, since they read allKnownRows (rows + groupExtraRows).
|
// lands, since they read allKnownRows (rows + groupExtraRows).
|
||||||
async loadEntreeGroup(id) {
|
// Shared by loadEntreeGroup() (click-triggered) and
|
||||||
|
// ensureLinkedReconciliationResolved() (background) below.
|
||||||
|
async fetchGroupFromNid(nid) {
|
||||||
|
const nids = await fetchGroupeIds(nid);
|
||||||
|
const known = new Set(this.allKnownRows.map((r) => r.nid));
|
||||||
|
const missingNids = nids.filter((n) => !known.has(n));
|
||||||
|
if (missingNids.length) {
|
||||||
|
const { data, includedMap } = await fetchLignesByNids(missingNids);
|
||||||
|
const newRows = buildRows(data, includedMap);
|
||||||
|
const existingIds = new Set(this.groupExtraRows.map((r) => r.id));
|
||||||
|
this.groupExtraRows = [...this.groupExtraRows, ...newRows.filter((r) => !existingIds.has(r.id))];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Resolves the full transitive-closure group for the given row id.
|
||||||
|
// id itself might not be loaded at all -- a versement's linked
|
||||||
|
// entrée can be dated years before or after it -- so fallbackNid
|
||||||
|
// lets the caller supply a *different*, guaranteed-loaded node's
|
||||||
|
// nid (e.g. the versement's own) to start the server-side
|
||||||
|
// traversal from instead; groupeEntree() finds the same connected
|
||||||
|
// component either way.
|
||||||
|
async loadEntreeGroup(id, fallbackNid) {
|
||||||
const row = this.allKnownRows.find((r) => r.id === id);
|
const row = this.allKnownRows.find((r) => r.id === id);
|
||||||
if (!row) return;
|
const startNid = row ? row.nid : fallbackNid;
|
||||||
|
if (!startNid) return;
|
||||||
this.groupLoading = true;
|
this.groupLoading = true;
|
||||||
try {
|
try {
|
||||||
const nids = await fetchGroupeIds(row.nid);
|
await this.fetchGroupFromNid(startNid);
|
||||||
const known = new Set(this.allKnownRows.map((r) => r.nid));
|
|
||||||
const missingNids = nids.filter((nid) => !known.has(nid));
|
|
||||||
if (missingNids.length) {
|
|
||||||
const { data, includedMap } = await fetchLignesByNids(missingNids);
|
|
||||||
const newRows = buildRows(data, includedMap);
|
|
||||||
const existingIds = new Set(this.groupExtraRows.map((r) => r.id));
|
|
||||||
this.groupExtraRows = [...this.groupExtraRows, ...newRows.filter((r) => !existingIds.has(r.id))];
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.typeUpdateError = err.message;
|
this.typeUpdateError = err.message;
|
||||||
} finally {
|
} finally {
|
||||||
this.groupLoading = false;
|
this.groupLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Proactively resolves the same group for every visible linkable
|
||||||
|
// row instead of waiting for a click -- a linked entrée can be
|
||||||
|
// dated years before or after the sortie that pays it out, so the
|
||||||
|
// reste à verser/sur-versé badge can never be answered correctly
|
||||||
|
// from whatever the sliding window happens to have loaded.
|
||||||
|
// Triggered by the `rows` watcher, so it re-runs after every
|
||||||
|
// load/loadOlder/loadNewer. Marks every entrée id a row links to
|
||||||
|
// (not just the first) before fetching, so another row sharing
|
||||||
|
// one of the same entrées doesn't queue a redundant fetch for it.
|
||||||
|
// Errors are swallowed here (unlike loadEntreeGroup() above) --
|
||||||
|
// this is unattended background work, not something the user
|
||||||
|
// explicitly asked for, so a failure just leaves the badge
|
||||||
|
// "inconnu" for that row instead of surfacing an error banner.
|
||||||
|
async ensureLinkedReconciliationResolved() {
|
||||||
|
const toFetch = [];
|
||||||
|
for (const r of this.rows) {
|
||||||
|
if (!LINKABLE_TYPES.includes(r.type) || !r.entreeLieeIds.length) continue;
|
||||||
|
if (r.entreeLieeIds.every((eid) => this.resolvedLinkGroups.has(eid))) continue;
|
||||||
|
r.entreeLieeIds.forEach((eid) => this.resolvedLinkGroups.add(eid));
|
||||||
|
toFetch.push(r.nid);
|
||||||
|
}
|
||||||
|
await Promise.all(toFetch.map((nid) => this.fetchGroupFromNid(nid).catch(() => {})));
|
||||||
|
},
|
||||||
startEditType(item) {
|
startEditType(item) {
|
||||||
this.typeUpdateError = null;
|
this.typeUpdateError = null;
|
||||||
this.editingTypeId = item.id;
|
this.editingTypeId = item.id;
|
||||||
@@ -1210,6 +1254,15 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
// Fires on every load()/loadOlder()/loadNewer()/jumpToYear()/
|
||||||
|
// enterYearMode()/exitYearMode() -- all of them reassign `rows`
|
||||||
|
// (never mutate it in place), so a shallow watch catches every
|
||||||
|
// window change without needing to hook each function
|
||||||
|
// individually. See ensureLinkedReconciliationResolved() itself
|
||||||
|
// for why this needs to run at all.
|
||||||
|
rows() {
|
||||||
|
this.ensureLinkedReconciliationResolved();
|
||||||
|
},
|
||||||
filterYear(newYear, oldYear) {
|
filterYear(newYear, oldYear) {
|
||||||
if (this._skipYearWatch) {
|
if (this._skipYearWatch) {
|
||||||
this._skipYearWatch = false;
|
this._skipYearWatch = false;
|
||||||
|
|||||||
@@ -220,7 +220,7 @@
|
|||||||
class="figli-recon-badge is-clickable"
|
class="figli-recon-badge is-clickable"
|
||||||
:class="linkStatusClasses(linkStatus(item).kind)"
|
:class="linkStatusClasses(linkStatus(item).kind)"
|
||||||
:title="item.entreeLieeIds.length ? linkStatus(item).detail + ' -- cliquer pour voir la ou les entrées liées' : linkStatus(item).detail + ' -- cliquer pour lier une entrée client'"
|
:title="item.entreeLieeIds.length ? linkStatus(item).detail + ' -- cliquer pour voir la ou les entrées liées' : linkStatus(item).detail + ' -- cliquer pour lier une entrée client'"
|
||||||
@click="item.entreeLieeIds.length ? toggleEntreeFilter(item.entreeLieeIds[0]) : openLinkForm(item.nid)"
|
@click="item.entreeLieeIds.length ? toggleEntreeFilter(item.entreeLieeIds[0], item.nid) : openLinkForm(item.nid)"
|
||||||
>{{ linkStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ linkStatusLabel(linkStatus(item).kind) }}</span>
|
>{{ linkStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ linkStatusLabel(linkStatus(item).kind) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user