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 },
|
items: { type: Array, required: true },
|
||||||
formatValue: { type: Function, required: true },
|
formatValue: { type: Function, required: true },
|
||||||
colorFor: { type: Function, default: null },
|
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: {
|
computed: {
|
||||||
hasNegative() {
|
hasNegative() {
|
||||||
@@ -79,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
template:
|
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-row" v-for="item in items" :key="item.label">' +
|
||||||
'<div class="figli-hbar-label" :title="item.label">{{ item.label }}</div>' +
|
'<div class="figli-hbar-label" :title="item.label">{{ item.label }}</div>' +
|
||||||
'<div class="figli-hbar-track" :class="{\'is-diverging\': hasNegative}">' +
|
'<div class="figli-hbar-track" :class="{\'is-diverging\': hasNegative}">' +
|
||||||
@@ -178,6 +184,36 @@
|
|||||||
if (!this.stats) return [];
|
if (!this.stats) return [];
|
||||||
return this.stats.top_clients.map((c) => ({ label: c.client, value: c.ca }));
|
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
|
// Comptes ordered by all-time solde (richest first) -- same order
|
||||||
// as soldeParCompteItems, so the trend grid below reads as a
|
// as soldeParCompteItems, so the trend grid below reads as a
|
||||||
// continuation of the bar chart above it rather than an unrelated
|
// 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. ---
|
// --- Aggregate the node-level rows in PHP. ---
|
||||||
$caParAnnee = [];
|
$caParAnnee = [];
|
||||||
$totalParType = [];
|
$totalParType = [];
|
||||||
|
$totalParTypeParAnnee = [];
|
||||||
$caParClient = [];
|
$caParClient = [];
|
||||||
|
$caParClientParAnnee = [];
|
||||||
$annees = [];
|
$annees = [];
|
||||||
foreach ($nodeRows as $row) {
|
foreach ($nodeRows as $row) {
|
||||||
$montant = $row->montant_ht !== NULL ? (float) $row->montant_ht : 0.0;
|
$montant = $row->montant_ht !== NULL ? (float) $row->montant_ht : 0.0;
|
||||||
@@ -104,6 +106,17 @@ class DashboardStatsController extends ControllerBase {
|
|||||||
$annees[$row->annee] = TRUE;
|
$annees[$row->annee] = TRUE;
|
||||||
if ($row->type === 'entree') {
|
if ($row->type === 'entree') {
|
||||||
$caParAnnee[$row->annee] = ($caParAnnee[$row->annee] ?? 0) + $montant;
|
$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);
|
arsort($caParClient);
|
||||||
@@ -116,6 +129,25 @@ class DashboardStatsController extends ControllerBase {
|
|||||||
$topClients[] = ['client' => $client, 'ca' => round($total, 2)];
|
$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. ---
|
// --- Aggregate the répartition-level rows in PHP. ---
|
||||||
$soldeParCompte = [];
|
$soldeParCompte = [];
|
||||||
$soldeParCompteParAnnee = [];
|
$soldeParCompteParAnnee = [];
|
||||||
@@ -170,12 +202,19 @@ class DashboardStatsController extends ControllerBase {
|
|||||||
// year, which can differ year to year.
|
// year, which can differ year to year.
|
||||||
ksort($soldeParCompteParAnnee);
|
ksort($soldeParCompteParAnnee);
|
||||||
ksort($totalParTypeParCompteParAnnee);
|
ksort($totalParTypeParCompteParAnnee);
|
||||||
|
ksort($totalParTypeParAnnee);
|
||||||
|
ksort($topClientsParAnnee);
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'annees' => $anneesList,
|
'annees' => $anneesList,
|
||||||
'ca_par_annee' => array_map(fn ($v) => round($v, 2), $caParAnnee),
|
'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' => 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' => $topClients,
|
||||||
|
'top_clients_par_annee' => $topClientsParAnnee,
|
||||||
'solde_par_compte' => array_map(fn ($v) => round($v, 2), $soldeParCompte),
|
'solde_par_compte' => array_map(fn ($v) => round($v, 2), $soldeParCompte),
|
||||||
'solde_par_compte_par_annee' => $soldeParCompteParAnnee,
|
'solde_par_compte_par_annee' => $soldeParCompteParAnnee,
|
||||||
'total_par_type_par_compte' => array_map(
|
'total_par_type_par_compte' => array_map(
|
||||||
|
|||||||
@@ -59,12 +59,24 @@
|
|||||||
<h2>Répartition de l'activité par type</h2>
|
<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>
|
<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>
|
<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>
|
||||||
|
|
||||||
<section class="figli-chart-section">
|
<section class="figli-chart-section">
|
||||||
<h2>Top clients par chiffre d'affaires</h2>
|
<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>
|
<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>
|
<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>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user