diff --git a/web/modules/custom/figli_compta_ledger/css/dashboard.css b/web/modules/custom/figli_compta_ledger/css/dashboard.css index e68991d..27a414a 100644 --- a/web/modules/custom/figli_compta_ledger/css/dashboard.css +++ b/web/modules/custom/figli_compta_ledger/css/dashboard.css @@ -275,6 +275,48 @@ html.gin--dark-mode #figli-dashboard-app { white-space: nowrap; } +/* Compact variant -- same chart, narrower fixed columns and smaller + text so it fits inside a small-multiples grid card (see + .figli-year-hbar-grid below) instead of the full-width layout. */ +#figli-dashboard-app .figli-hbar-chart.is-compact { + gap: 0.3rem; +} +#figli-dashboard-app .figli-hbar-chart.is-compact .figli-hbar-row { + grid-template-columns: 5.5rem 1fr 3.5rem; + gap: 0.4rem; +} +#figli-dashboard-app .figli-hbar-chart.is-compact .figli-hbar-label { + font-size: 0.72rem; +} +#figli-dashboard-app .figli-hbar-chart.is-compact .figli-hbar-track { + height: 0.65rem; +} +#figli-dashboard-app .figli-hbar-chart.is-compact .figli-hbar-value { + font-size: 0.7rem; +} + +/* --- Year small multiples (par année, next to the all-time chart) --- */ +#figli-dashboard-app .figli-year-hbar-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); + gap: 0.75rem; + margin-top: 1.25rem; + padding-top: 1.25rem; + border-top: 1px solid var(--figli-border); +} +#figli-dashboard-app .figli-year-hbar-card { + background: var(--figli-bg-alt); + border: 1px solid var(--figli-border); + border-radius: 6px; + padding: 0.65rem 0.75rem; +} +#figli-dashboard-app .figli-year-hbar-title { + font-size: 0.78rem; + font-weight: 700; + color: var(--figli-text-light); + margin-bottom: 0.5rem; +} + /* --- Vertical bar chart (CA par année) --- */ #figli-dashboard-app .figli-vbar-chart { display: flex; diff --git a/web/modules/custom/figli_compta_ledger/js/dashboard-compte.js b/web/modules/custom/figli_compta_ledger/js/dashboard-compte.js index f8f6f34..a8cb172 100644 --- a/web/modules/custom/figli_compta_ledger/js/dashboard-compte.js +++ b/web/modules/custom/figli_compta_ledger/js/dashboard-compte.js @@ -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: - '
' + + '
' + '
' + '
{{ item.label }}
' + '
' + @@ -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) { diff --git a/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php b/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php index 4974864..eef2117 100644 --- a/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php +++ b/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php @@ -120,6 +120,7 @@ class DashboardStatsController extends ControllerBase { $soldeParCompte = []; $soldeParCompteParAnnee = []; $totalParTypeParCompte = []; + $totalParTypeParCompteParAnnee = []; foreach ($compteRows as $row) { $total = (float) $row->total; // Same reasoning: the all-time balance includes every line; the @@ -144,6 +145,14 @@ class DashboardStatsController extends ControllerBase { // that year+compte would survive. $soldeParCompteParAnnee[$row->annee][$row->compte] = round(($soldeParCompteParAnnee[$row->annee][$row->compte] ?? 0) + $total, 2); + + // Per-année version of $totalParTypeParCompte above, for the + // "par année" small multiples on /dashboard/compte -- same rows, + // no extra query. + if ($row->type !== 'ouverture') { + $totalParTypeParCompteParAnnee[$row->annee][$row->compte][$row->type] = + ($totalParTypeParCompteParAnnee[$row->annee][$row->compte][$row->type] ?? 0) + abs($total); + } } // array_keys() alone would leak PHP's array-key int-casting here: a @@ -160,6 +169,7 @@ class DashboardStatsController extends ControllerBase { // key order for soldeParCompteParAnnee follows first-seen compte per // year, which can differ year to year. ksort($soldeParCompteParAnnee); + ksort($totalParTypeParCompteParAnnee); return new JsonResponse([ 'annees' => $anneesList, @@ -172,6 +182,13 @@ class DashboardStatsController extends ControllerBase { fn ($parType) => array_map(fn ($v) => round($v, 2), $parType), $totalParTypeParCompte ), + 'total_par_type_par_compte_par_annee' => array_map( + fn ($parCompte) => array_map( + fn ($parType) => array_map(fn ($v) => round($v, 2), $parType), + $parCompte + ), + $totalParTypeParCompteParAnnee + ), ]); } diff --git a/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard-compte.html.twig b/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard-compte.html.twig index 278e66d..d4a4fab 100644 --- a/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard-compte.html.twig +++ b/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard-compte.html.twig @@ -85,6 +85,9 @@ {{ formatEur(-r.residual) }} + + Total{{ formatEurRound(totalSurVerse) }} + @@ -103,6 +106,9 @@ {{ formatEur(r.parCompte[selectedCompte]) }} + + Total{{ formatEurRound(-totalNonLies) }} + @@ -148,12 +154,24 @@

Répartition de l'activité par type

Montant total (HT, valeur absolue) par type de ligne pour {{ selectedCompte }}, hors ouvertures -- contrairement aux tableaux ci-dessus, tous les types comptent ici (pas seulement entrée/versement).

+
+
+
{{ y.annee }}
+ +
+

Top clients

Clients ayant généré le plus d'entrées attribuées à {{ selectedCompte }}, toutes années confondues.

+
+
+
{{ y.annee }}
+ +
+