Open the entrée/versement drill-down in a modal instead of in place
Replacing the main table's rows with the filtered group made the browser clamp scrollTop to 0 the moment the drill-down shrank the visible content, so closing it never returned to where the user had been scrolled. A modal overlay leaves the main table (and its scroll position) untouched entirely.
This commit is contained in:
@@ -332,6 +332,68 @@ html.gin--dark-mode #figli-home-app {
|
||||
padding: 0.2rem 0.6rem;
|
||||
}
|
||||
|
||||
/* Entrée + sorties liées drill-down: a modal overlay rather than
|
||||
replacing the main table's rows in place, so opening/closing it never
|
||||
disturbs the main table's scroll position. */
|
||||
#figli-home-app .figli-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
#figli-home-app .figli-modal {
|
||||
background: var(--figli-bg);
|
||||
color: var(--figli-text);
|
||||
border: 1px solid var(--figli-border);
|
||||
border-radius: 8px;
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
width: 900px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
#figli-home-app .figli-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--figli-border);
|
||||
}
|
||||
#figli-home-app .figli-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
#figli-home-app .figli-modal-close {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--figli-text-light);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.15rem 0.45rem;
|
||||
}
|
||||
#figli-home-app .figli-modal-close:hover {
|
||||
background: var(--figli-bg-alt);
|
||||
border-color: var(--figli-border);
|
||||
color: var(--figli-text);
|
||||
}
|
||||
#figli-home-app .figli-modal-body {
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
}
|
||||
#figli-home-app .figli-modal-body table {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
#figli-home-app .figli-modal-body thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Sliding-window edge markers (IntersectionObserver targets) -- kept
|
||||
short so they don't add visible dead space when idle, tall enough
|
||||
(min-height) to reliably intersect the observer's root margin. */
|
||||
|
||||
@@ -409,11 +409,6 @@
|
||||
return [...entrees, ...sorties];
|
||||
},
|
||||
filteredRows() {
|
||||
// Drill-down mode: the connected entrée/sortie group, ignoring
|
||||
// the other filters -- clicking the badge again clears it.
|
||||
if (this.filterEntreeId) {
|
||||
return this.filterEntreeGroup;
|
||||
}
|
||||
return this.rows.filter((r) => {
|
||||
if (this.filterCompte && r.parCompte[this.filterCompte] === undefined) return false;
|
||||
if (this.filterClient && r.client !== this.filterClient) return false;
|
||||
@@ -423,22 +418,21 @@
|
||||
return true;
|
||||
});
|
||||
},
|
||||
// Footer totals while drilled down into one entrée + its linked
|
||||
// sorties (see filteredRows' drill-down branch above): the whole
|
||||
// point of this view is "does this entrée balance against what was
|
||||
// paid out", so the footer should answer exactly that instead of
|
||||
// the current year's totals -- and it can be computed locally
|
||||
// (unlike the per-year figures, this handful of rows is already
|
||||
// fully loaded), no server round-trip needed. Same shape as
|
||||
// /lignes/api/totaux so the template can render either the same
|
||||
// way.
|
||||
// Totals for the entrée + linked sorties drill-down modal (see
|
||||
// filterEntreeGroup above): the whole point of that view is "does
|
||||
// this entrée balance against what was paid out", so its own footer
|
||||
// answers exactly that instead of the current year's totals -- and
|
||||
// it can be computed locally (unlike the per-year figures, this
|
||||
// handful of rows is already fully loaded), no server round-trip
|
||||
// needed. Same shape as /lignes/api/totaux so the template can
|
||||
// render either the same way.
|
||||
drilldownTotals() {
|
||||
if (!this.filterEntreeId) return null;
|
||||
let montantHt = 0;
|
||||
let montantTtc = 0;
|
||||
let ecart = 0;
|
||||
const parCompte = {};
|
||||
for (const r of this.filteredRows) {
|
||||
for (const r of this.filterEntreeGroup) {
|
||||
montantHt += r.montant_ht || 0;
|
||||
montantTtc += r.montant_ttc || 0;
|
||||
ecart += r.ecart || 0;
|
||||
@@ -454,13 +448,6 @@
|
||||
par_compte: Object.fromEntries(Object.entries(parCompte).map(([c, v]) => [c, round(v)])),
|
||||
};
|
||||
},
|
||||
// What the footer actually renders -- the drill-down's own totals
|
||||
// while active, otherwise the current year's (see
|
||||
// detectCurrentYear()/loadCurrentYearTotals()). Switches back
|
||||
// automatically the moment filterEntreeId clears.
|
||||
footerTotals() {
|
||||
return this.filterEntreeId ? this.drilldownTotals : this.currentYearTotals;
|
||||
},
|
||||
groupedRows() {
|
||||
const list = this.filteredRows;
|
||||
if (this.groupBy === 'none') return list;
|
||||
@@ -621,9 +608,16 @@
|
||||
progress: { type: 'throbber' },
|
||||
}).execute();
|
||||
},
|
||||
// Opens/closes the entrée + linked sorties drill-down modal -- a
|
||||
// separate overlay (see the template), not a filter applied to the
|
||||
// main table, so opening/closing it never touches the main table's
|
||||
// scroll position.
|
||||
toggleEntreeFilter(id) {
|
||||
this.filterEntreeId = this.filterEntreeId === id ? null : id;
|
||||
},
|
||||
closeDrilldown() {
|
||||
this.filterEntreeId = null;
|
||||
},
|
||||
startEditType(item) {
|
||||
this.typeUpdateError = null;
|
||||
this.editingTypeId = item.id;
|
||||
|
||||
@@ -66,8 +66,6 @@
|
||||
<input type="checkbox" v-model="onlyErrors" /> Écarts uniquement
|
||||
</label>
|
||||
|
||||
<button type="button" class="button figli-clear-drilldown" v-if="filterEntreeId" @click="filterEntreeId = null">✕ Entrée + sorties liées uniquement</button>
|
||||
|
||||
<span class="figli-count" v-if="!loading">{{ filteredRows.length }} / {{ rows.length }} lignes chargées{{ errorCount ? ' — ' + errorCount + ' avec écart' : '' }}</span>
|
||||
</div>
|
||||
|
||||
@@ -183,19 +181,83 @@
|
||||
<td class="actions-col"></td>
|
||||
<td class="actions-col"></td>
|
||||
<td colspan="4">
|
||||
<template v-if="filterEntreeId">Solde entrée + sorties liées (créditeur / débiteur)</template>
|
||||
<template v-else>
|
||||
Solde {{ currentYear || '…' }} (créditeur / débiteur)
|
||||
<span v-if="currentYearLoading" class="figli-note">chargement…</span>
|
||||
</template>
|
||||
Solde {{ currentYear || '…' }} (créditeur / débiteur)
|
||||
<span v-if="currentYearLoading" class="figli-note">chargement…</span>
|
||||
</td>
|
||||
<td class="amount">{{ footerTotals ? formatEur(footerTotals.montant_ht) : '' }}</td>
|
||||
<td class="amount">{{ footerTotals ? formatEur(footerTotals.montant_ttc) : '' }}</td>
|
||||
<td v-for="c in allComptes" :key="c" class="amount compte-col" :class="footerTotals ? soldeClass(footerTotals.par_compte[c]) : ''">{{ footerTotals && footerTotals.par_compte[c] !== undefined ? formatEur(footerTotals.par_compte[c]) : '' }}</td>
|
||||
<td class="amount" :class="footerTotals ? soldeClass(footerTotals.ecart) : ''">{{ footerTotals ? formatEur(footerTotals.ecart) : '' }}</td>
|
||||
<td class="amount">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ht) : '' }}</td>
|
||||
<td class="amount">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ttc) : '' }}</td>
|
||||
<td v-for="c in allComptes" :key="c" class="amount compte-col" :class="currentYearTotals ? soldeClass(currentYearTotals.par_compte[c]) : ''">{{ currentYearTotals && currentYearTotals.par_compte[c] !== undefined ? formatEur(currentYearTotals.par_compte[c]) : '' }}</td>
|
||||
<td class="amount" :class="currentYearTotals ? soldeClass(currentYearTotals.ecart) : ''">{{ currentYearTotals ? formatEur(currentYearTotals.ecart) : '' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="filterEntreeId" class="figli-modal-backdrop" @click.self="closeDrilldown">
|
||||
<div class="figli-modal">
|
||||
<div class="figli-modal-header">
|
||||
<h3>Entrée + sorties liées</h3>
|
||||
<button type="button" class="figli-modal-close" @click="closeDrilldown">✕</button>
|
||||
</div>
|
||||
<div class="figli-modal-body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="actions-col"></th>
|
||||
<th>Date</th>
|
||||
<th>Client</th>
|
||||
<th>Type</th>
|
||||
<th>Libellé / Détail</th>
|
||||
<th class="amount">Montant HT</th>
|
||||
<th class="amount">Montant TTC</th>
|
||||
<th v-for="c in allComptes" :key="c" class="amount compte-col">{{ c }}</th>
|
||||
<th class="amount">Écart</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in filterEntreeGroup" :key="item.id" :class="{'figli-error-row': item.hasError}">
|
||||
<td class="actions-col">
|
||||
<button type="button" class="figli-edit-btn" title="Modifier" @click="openEditForm(item.nid)">
|
||||
<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13.5 3.5l3 3L6 17l-3.5.5.5-3.5L13.5 3.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ formatDate(item.date) }}</td>
|
||||
<td>{{ item.client || '—' }}</td>
|
||||
<td><span class="figli-badge" :class="'type-' + item.type">{{ typeLabel(item.type) }}</span></td>
|
||||
<td class="figli-libelle">
|
||||
{{ item.libelle }}
|
||||
<span
|
||||
v-if="item.type === 'entree' && reconciliationByEntree.get(item.id) && reconciliationByEntree.get(item.id).count > 0"
|
||||
class="figli-recon-badge"
|
||||
:class="{'is-anomalie': reconciliationByEntree.get(item.id).surVerse > 0, 'is-reste': reconciliationByEntree.get(item.id).resteAVerser > 0}"
|
||||
>{{ reconciliationByEntree.get(item.id).count }} sortie{{ reconciliationByEntree.get(item.id).count > 1 ? 's' : '' }} liée{{ reconciliationByEntree.get(item.id).count > 1 ? 's' : '' }}<template v-if="reconciliationByEntree.get(item.id).resteAVerser > 0"> · reste {{ formatEur(reconciliationByEntree.get(item.id).resteAVerser) }}</template><template v-if="reconciliationByEntree.get(item.id).surVerse > 0"> · sur-versé {{ formatEur(reconciliationByEntree.get(item.id).surVerse) }}</template></span>
|
||||
<span
|
||||
v-if="versementStatus(item)"
|
||||
class="figli-recon-badge"
|
||||
:class="versementStatusClasses(versementStatus(item).kind)"
|
||||
>{{ 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>
|
||||
<td v-for="c in allComptes" :key="c" class="amount compte-col">{{ item.parCompte[c] !== undefined ? formatEur(item.parCompte[c]) : '' }}</td>
|
||||
<td class="amount" :class="{'figli-ecart': item.hasError}">{{ item.hasError ? formatEur(item.ecart) : '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="figli-totals-row">
|
||||
<td class="actions-col"></td>
|
||||
<td colspan="4">Solde entrée + sorties liées (créditeur / débiteur)</td>
|
||||
<td class="amount">{{ drilldownTotals ? formatEur(drilldownTotals.montant_ht) : '' }}</td>
|
||||
<td class="amount">{{ drilldownTotals ? formatEur(drilldownTotals.montant_ttc) : '' }}</td>
|
||||
<td v-for="c in allComptes" :key="c" class="amount compte-col" :class="drilldownTotals ? soldeClass(drilldownTotals.par_compte[c]) : ''">{{ drilldownTotals && drilldownTotals.par_compte[c] !== undefined ? formatEur(drilldownTotals.par_compte[c]) : '' }}</td>
|
||||
<td class="amount" :class="drilldownTotals ? soldeClass(drilldownTotals.ecart) : ''">{{ drilldownTotals ? formatEur(drilldownTotals.ecart) : '' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endverbatim %}
|
||||
|
||||
Reference in New Issue
Block a user