From 3552b5a79d9f81e4ff88fbd6b0f8039459de0bde Mon Sep 17 00:00:00 2001 From: bach Date: Tue, 8 Sep 2026 14:34:04 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20la=20r=C3=A9partition=20par=20type=20e?= =?UTF-8?q?t=20le=20top=20clients=20par=20ann=C3=A9e=20sur=20/dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reprend exactement le pattern des petits multiples déjà en place sur /dashboard/compte (grille figli-year-hbar-grid, variante compacte de h-bar-chart) -- mêmes classes CSS, aucun nouveau style nécessaire. Backend : DashboardStatsController::stats() calcule maintenant aussi total_par_type_par_annee et top_clients_par_annee (top 8, contre 12 en toutes années confondues) à partir des mêmes requêtes SQL déjà en place, sans requête supplémentaire. Frontend : dashboard.js n'a jamais de données ligne par ligne (contraire- ment à dashboard-compte.js qui filtre côté client) -- l'agrégation par année doit donc venir du serveur. Ajout du prop "compact" au HBarChart de dashboard.js (jusqu'ici absent, seule la copie de dashboard-compte.js l'avait). --- .../figli_compta_ledger/js/dashboard.js | 38 +++++++++++++++++- .../Controller/DashboardStatsController.php | 39 +++++++++++++++++++ .../figli-compta-dashboard.html.twig | 12 ++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/web/modules/custom/figli_compta_ledger/js/dashboard.js b/web/modules/custom/figli_compta_ledger/js/dashboard.js index 3fc9449..bcba57e 100644 --- a/web/modules/custom/figli_compta_ledger/js/dashboard.js +++ b/web/modules/custom/figli_compta_ledger/js/dashboard.js @@ -54,6 +54,12 @@ 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. Same prop as dashboard-compte.js's own HBarChart + // copy (this project duplicates the component rather than sharing + // it between dashboard.js/dashboard-compte.js, see dashboard.css). + compact: { type: Boolean, default: false }, }, computed: { hasNegative() { @@ -79,7 +85,7 @@ }, }, template: - '
' + + '
' + '
' + '
{{ item.label }}
' + '
' + @@ -178,6 +184,36 @@ if (!this.stats) return []; return this.stats.top_clients.map((c) => ({ label: c.client, value: c.ca })); }, + // Small multiples, one per year -- same source as typeItems() above + // (total_par_type_par_annee is the same node-level SQL query, just + // also grouped by année, no extra request). Mirrors + // dashboard-compte.js's typeItemsParAnnee, but that one filters a + // full row list client-side (it has one, scoped to a single + // compte); this page never fetches full rows (see this file's + // docblock), so the per-année breakdown has to already be + // pre-aggregated server-side. + typeItemsParAnnee() { + if (!this.stats) return []; + return this.stats.annees.map((annee) => { + const parType = this.stats.total_par_type_par_annee[annee] || {}; + 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); + }, + // Same idea for topClientsItems() -- top_clients_par_annee is + // already capped to 8 per year server-side (see + // DashboardStatsController::stats()), same reasoning as + // dashboard-compte.js capping its own per-année version to 5. + topClientsParAnnee() { + if (!this.stats) return []; + return this.stats.annees.map((annee) => { + const items = (this.stats.top_clients_par_annee[annee] || []) + .map((c) => ({ label: c.client, value: c.ca })); + return { annee, items }; + }).filter((y) => y.items.length); + }, // Comptes ordered by all-time solde (richest first) -- same order // as soldeParCompteItems, so the trend grid below reads as a // continuation of the bar chart above it rather than an unrelated 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 eef2117..ccd2dd4 100644 --- a/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php +++ b/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php @@ -82,7 +82,9 @@ class DashboardStatsController extends ControllerBase { // --- Aggregate the node-level rows in PHP. --- $caParAnnee = []; $totalParType = []; + $totalParTypeParAnnee = []; $caParClient = []; + $caParClientParAnnee = []; $annees = []; foreach ($nodeRows as $row) { $montant = $row->montant_ht !== NULL ? (float) $row->montant_ht : 0.0; @@ -104,6 +106,17 @@ class DashboardStatsController extends ControllerBase { $annees[$row->annee] = TRUE; if ($row->type === 'entree') { $caParAnnee[$row->annee] = ($caParAnnee[$row->annee] ?? 0) + $montant; + // Per-année équivalent of $caParClient above -- backs the "Top + // clients par année" small multiples, same rows, no extra query. + $client = $row->client ?: '(sans client)'; + $caParClientParAnnee[$row->annee][$client] = + ($caParClientParAnnee[$row->annee][$client] ?? 0) + $montant; + } + // Per-année équivalent of $totalParType above -- backs the + // "Répartition de l'activité par type" small multiples. + if ($row->type !== 'ouverture') { + $totalParTypeParAnnee[$row->annee][$row->type] = + ($totalParTypeParAnnee[$row->annee][$row->type] ?? 0) + abs($montant); } } arsort($caParClient); @@ -116,6 +129,25 @@ class DashboardStatsController extends ControllerBase { $topClients[] = ['client' => $client, 'ca' => round($total, 2)]; } + // Top 8 (not 12 like the all-time chart above) -- one per year keeps + // the small-multiples grid readable, same reasoning as the per-compte + // équivalent on /dashboard/compte (there it's top 5, computed + // client-side from full row data; here it's top 8, computed here + // since dashboard.js only ever gets pre-aggregated SQL, never full + // rows -- see this controller's docblock). + $topClientsParAnnee = []; + foreach ($caParClientParAnnee as $annee => $parClient) { + arsort($parClient); + $topClientsParAnnee[$annee] = []; + $i = 0; + foreach ($parClient as $client => $total) { + if ($i++ >= 8) { + break; + } + $topClientsParAnnee[$annee][] = ['client' => $client, 'ca' => round($total, 2)]; + } + } + // --- Aggregate the répartition-level rows in PHP. --- $soldeParCompte = []; $soldeParCompteParAnnee = []; @@ -170,12 +202,19 @@ class DashboardStatsController extends ControllerBase { // year, which can differ year to year. ksort($soldeParCompteParAnnee); ksort($totalParTypeParCompteParAnnee); + ksort($totalParTypeParAnnee); + ksort($topClientsParAnnee); return new JsonResponse([ 'annees' => $anneesList, 'ca_par_annee' => array_map(fn ($v) => round($v, 2), $caParAnnee), 'total_par_type' => array_map(fn ($v) => round($v, 2), $totalParType), + 'total_par_type_par_annee' => array_map( + fn ($parType) => array_map(fn ($v) => round($v, 2), $parType), + $totalParTypeParAnnee + ), 'top_clients' => $topClients, + 'top_clients_par_annee' => $topClientsParAnnee, 'solde_par_compte' => array_map(fn ($v) => round($v, 2), $soldeParCompte), 'solde_par_compte_par_annee' => $soldeParCompteParAnnee, 'total_par_type_par_compte' => array_map( diff --git a/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard.html.twig b/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard.html.twig index bfb69a1..1105004 100644 --- a/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard.html.twig +++ b/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard.html.twig @@ -59,12 +59,24 @@

Répartition de l'activité par type

Montant total (HT, valeur absolue) par type de ligne, hors ouvertures.

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

Top clients par chiffre d'affaires

Les 12 clients ayant généré le plus de chiffre d'affaires, toutes années confondues.

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