' +
@@ -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.
+
Top clients par chiffre d'affaires
Les 12 clients ayant généré le plus de chiffre d'affaires, toutes années confondues.
+