Allow selecting several comptes/types at once in the filters

Compte and Type were single-value <select> elements. Converted both to
<select multiple> -- Vue binds those to an array natively, so
ctrl/cmd+click and shift+click just work with no custom JS. Matching
semantics are OR within a filter (any of the selected comptes/types) and
AND across filters, same as the existing client/year/écarts filters.

The URL hash (compte=Maud,Bachir&type=versement,achat) now carries
comma-separated lists instead of a single value, so the shareable/
reloadable filtered view still works with multiple selections.
This commit is contained in:
2026-09-05 21:48:59 +02:00
parent 0479f3fbbb
commit 319b22207d
3 changed files with 30 additions and 14 deletions
@@ -58,6 +58,15 @@ html.gin--dark-mode #figli-home-app {
border-radius: 4px;
}
/* Compte/Type filters: <select multiple> so ctrl/shift+click can select
several values, rendered as a short scrollable list (size="4") rather
than the single-line native default -- needs an explicit width, since
a multi-row select sizes to its widest option's natural width
otherwise, unlike a single-line select's fixed line box. */
#figli-home-app .figli-toolbar select[multiple] {
width: 9rem;
}
#figli-home-app .figli-client-input {
font-size: 0.85rem;
padding: 0.2rem 0.4rem;
@@ -205,9 +205,11 @@
return res.json();
}
// URL hash (#compte=Maud&type=versement&annee=2023&ecarts=1&aller=2024)
// URL hash (#compte=Maud,Bachir&type=versement,achat&annee=2023&ecarts=1&aller=2024)
// makes the filters and "Aller à" shareable/reloadable -- reload the
// page or send the link and you land back in the same filtered view.
// compte/type can each hold several values (comma-separated) since
// both filters are now <select multiple>.
// "aller" isn't an ongoing filter (jumpYearValue itself always resets
// to '' right after firing) but its target year is worth remembering
// for this purpose, so it's tracked separately (lastJumpYear) purely
@@ -216,9 +218,9 @@
function readHashState() {
const params = new URLSearchParams(location.hash.replace(/^#/, ''));
return {
filterCompte: params.get('compte') || '',
filterCompte: params.get('compte') ? params.get('compte').split(',') : [],
filterClient: params.get('client') || '',
filterType: params.get('type') || '',
filterType: params.get('type') ? params.get('type').split(',') : [],
filterYear: params.get('annee') || '',
onlyErrors: params.get('ecarts') === '1',
aller: params.get('aller') || '',
@@ -227,9 +229,9 @@
function buildHashString(state) {
const params = new URLSearchParams();
if (state.filterCompte) params.set('compte', state.filterCompte);
if (state.filterCompte.length) params.set('compte', state.filterCompte.join(','));
if (state.filterClient) params.set('client', state.filterClient);
if (state.filterType) params.set('type', state.filterType);
if (state.filterType.length) params.set('type', state.filterType.join(','));
if (state.filterYear) params.set('annee', state.filterYear);
if (state.onlyErrors) params.set('ecarts', '1');
// Redundant/ambiguous alongside an active "Année" filter -- that
@@ -302,9 +304,13 @@
allClientsList: [],
allYearsList: [],
ouvertureEcarts: {},
filterCompte: '',
// Arrays -- both are native <select multiple>, so ctrl/cmd+click
// and shift+click just work (Vue's v-model binds a <select
// multiple> to an array natively, no custom handling needed). An
// empty array means "tous", same as the single-value '' used to.
filterCompte: [],
filterClient: '',
filterType: '',
filterType: [],
filterYear: '',
jumpYearValue: '',
// Not a filter itself (jumpYearValue always resets to '' right
@@ -482,9 +488,12 @@
},
filteredRows() {
return this.rows.filter((r) => {
if (this.filterCompte && r.parCompte[this.filterCompte] === undefined) return false;
// Several selected comptes/types match with OR semantics ("any
// of these") -- a line either touches one of the selected
// comptes or it doesn't, same idea for type.
if (this.filterCompte.length && !this.filterCompte.some((c) => r.parCompte[c] !== undefined)) return false;
if (this.filterClient && r.client !== this.filterClient) return false;
if (this.filterType && r.type !== this.filterType) return false;
if (this.filterType.length && !this.filterType.includes(r.type)) return false;
if (this.filterYear && (r.date || '').slice(0, 4) !== this.filterYear) return false;
if (this.onlyErrors && !r.hasError) return false;
return true;
@@ -960,7 +969,7 @@
// filter with only a few matches a year could search almost forever
// without surfacing more of them.
isFiltering() {
return !!(this.filterCompte || this.filterClient || this.filterType || this.onlyErrors);
return !!(this.filterCompte.length || this.filterClient || this.filterType.length || this.onlyErrors);
},
// Keeps extending the window (both directions) as long as a filter
// leaves too few matching rows to fill the viewport -- otherwise
@@ -20,8 +20,7 @@
{% verbatim %}
<label>Compte
<select v-model="filterCompte">
<option value="">Tous</option>
<select v-model="filterCompte" multiple size="4" title="Ctrl/Cmd+clic ou Maj+clic pour plusieurs comptes">
<option v-for="c in allComptes" :key="c" :value="c">{{ c }}</option>
</select>
</label>
@@ -34,8 +33,7 @@
</label>
<label>Type
<select v-model="filterType">
<option value="">Tous</option>
<select v-model="filterType" multiple size="4" title="Ctrl/Cmd+clic ou Maj+clic pour plusieurs types">
<option v-for="t in allTypes" :key="t.value" :value="t.value">{{ t.label }}</option>
</select>
</label>