Add solde footers + per-année small multiples to /dashboard/compte
1. "Entrées sur-versées" and "Versements sans entrée liée" tables get a solde footer, same convention as "Reste à verser" above them -- reuses the totalSurVerse/totalNonLies computeds already backing the summary cards, no new computation. 2. "Répartition de l'activité par type" and "Top clients" keep their all-time chart, now followed by a small-multiples grid of the same chart per année. The type breakdown reuses total_par_type_par_compte_par_annee, a new field on DashboardStatsController::stats() built from the same répartition- level rows already fetched for total_par_type_par_compte (no extra query, just one more level of grouping in the PHP aggregation). Top clients per année is computed client-side from the same entreesDuCompte already used for the all-time version, capped to top 5 (not 10) to keep the grid readable. HBarChart gains a `compact` prop (narrower fixed columns, smaller text) for use inside the small-multiples cards -- the all-time charts above keep the full-width layout unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,10 @@
|
||||
items: { type: Array, required: true },
|
||||
formatValue: { type: Function, required: true },
|
||||
colorFor: { type: Function, default: null },
|
||||
// Narrower label/value columns, smaller text -- for the per-année
|
||||
// small-multiples grids, where a full-width chart wouldn't fit in
|
||||
// a grid card.
|
||||
compact: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
maxAbs() {
|
||||
@@ -225,7 +229,7 @@
|
||||
},
|
||||
},
|
||||
template:
|
||||
'<div class="figli-hbar-chart">' +
|
||||
'<div class="figli-hbar-chart" :class="{\'is-compact\': compact}">' +
|
||||
'<div class="figli-hbar-row" v-for="item in items" :key="item.label">' +
|
||||
'<div class="figli-hbar-label" :title="item.label">{{ item.label }}</div>' +
|
||||
'<div class="figli-hbar-track">' +
|
||||
@@ -425,6 +429,37 @@
|
||||
.map(([type, value]) => ({ label: TYPE_LABELS[type] || type, value, type }))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
},
|
||||
// Small multiples, one per year -- same source as typeItems() above
|
||||
// (total_par_type_par_compte_par_annee is the same répartition-level
|
||||
// SQL query, just also grouped by année, no extra request).
|
||||
typeItemsParAnnee() {
|
||||
if (!this.stats || !this.selectedCompte) return [];
|
||||
return this.stats.annees.map((annee) => {
|
||||
const parType = (this.stats.total_par_type_par_compte_par_annee[annee] || {})[this.selectedCompte] || {};
|
||||
const items = Object.entries(parType)
|
||||
.map(([type, value]) => ({ label: TYPE_LABELS[type] || type, value, type }))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
return { annee, items };
|
||||
}).filter((y) => y.items.length);
|
||||
},
|
||||
// Top 5 (not 10 like the all-time chart, above) -- one per year
|
||||
// keeps the small-multiples grid readable.
|
||||
topClientsParAnnee() {
|
||||
if (!this.selectedCompte) return [];
|
||||
return this.stats.annees.map((annee) => {
|
||||
const parClient = new Map();
|
||||
for (const r of this.entreesDuCompte) {
|
||||
if ((r.date || '').slice(0, 4) !== annee) continue;
|
||||
const client = r.client || '(sans client)';
|
||||
parClient.set(client, (parClient.get(client) || 0) + r.parCompte[this.selectedCompte]);
|
||||
}
|
||||
const items = Array.from(parClient.entries())
|
||||
.map(([label, value]) => ({ label, value: Math.round(value * 100) / 100 }))
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 5);
|
||||
return { annee, items };
|
||||
}).filter((y) => y.items.length);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatEur(v) {
|
||||
|
||||
Reference in New Issue
Block a user