Ajoute la répartition par type et le top clients par année sur /dashboard
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).
This commit is contained in:
@@ -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:
|
||||
'<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" :class="{\'is-diverging\': hasNegative}">' +
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -59,12 +59,24 @@
|
||||
<h2>Répartition de l'activité par type</h2>
|
||||
<p class="figli-note">Montant total (HT, valeur absolue) par type de ligne, hors ouvertures.</p>
|
||||
<h-bar-chart :items="typeItems" :format-value="formatEurRound" :color-for="typeColor"></h-bar-chart>
|
||||
<div class="figli-year-hbar-grid">
|
||||
<div class="figli-year-hbar-card" v-for="y in typeItemsParAnnee" :key="y.annee">
|
||||
<div class="figli-year-hbar-title">{{ y.annee }}</div>
|
||||
<h-bar-chart :items="y.items" :format-value="formatEurRound" :color-for="typeColor" compact></h-bar-chart>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="figli-chart-section">
|
||||
<h2>Top clients par chiffre d'affaires</h2>
|
||||
<p class="figli-note">Les 12 clients ayant généré le plus de chiffre d'affaires, toutes années confondues.</p>
|
||||
<h-bar-chart :items="topClientsItems" :format-value="formatEurRound"></h-bar-chart>
|
||||
<div class="figli-year-hbar-grid">
|
||||
<div class="figli-year-hbar-card" v-for="y in topClientsParAnnee" :key="y.annee">
|
||||
<div class="figli-year-hbar-title">{{ y.annee }}</div>
|
||||
<h-bar-chart :items="y.items" :format-value="formatEurRound" compact></h-bar-chart>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user