Ajoute "Charges structurelles par client" (+ par année) sur le dashboard SAS

Réutilise le node-level query déjà exécuté pour caParClient/totalParType
(aucune requête SQL supplémentaire) -- accumule abs(montant_ht) des
lignes type=charge par client (le vendeur/organisme : loyer, assurance,
URSSAF...). Trié par montant décroissant (pas alphabétique), pas de
plafond top-N vu le petit nombre de vendeurs distincts. Coloré en gris
"charge", même couleur que ce type dans "Répartition de l'activité par
type".
This commit is contained in:
2026-09-08 15:30:31 +02:00
parent 96bd9502d2
commit 74d8eb2aa2
3 changed files with 71 additions and 0 deletions
@@ -176,6 +176,27 @@
return { annee, items }; return { annee, items };
}).filter((y) => y.items.length); }).filter((y) => y.items.length);
}, },
// Charges structurelles by client (i.e. by vendor: loyer,
// assurance, hébergement, URSSAF...) -- sorted by value descending
// server-side already (no alphabetical option asked for here,
// unlike the versements-par-compte chart), no top-N cap needed
// (see DashboardStatsController::stats()'s own comment -- only a
// handful of distinct vendors ever show up on a "charge" line).
// `type: 'charge'` on every item is just so colorFor="typeColor"
// (same method the type-breakdown chart uses) paints every bar the
// same "charge" grey -- every row here is that type by definition.
chargeParClientItems() {
if (!this.stats) return [];
return this.stats.charge_par_client.map((c) => ({ label: c.client, value: c.total, type: 'charge' }));
},
chargeParClientParAnnee() {
if (!this.stats) return [];
return this.stats.annees.map((annee) => {
const items = (this.stats.charge_par_client_par_annee[annee] || [])
.map((c) => ({ label: c.client, value: c.total, type: 'charge' }));
return { annee, items };
}).filter((y) => y.items.length);
},
// total_par_type_par_compte is already in the stats response -- // total_par_type_par_compte is already in the stats response --
// it's the same répartition-level query /dashboard/compte uses for // it's the same répartition-level query /dashboard/compte uses for
// its own (single-compte) type breakdown, just never sliced down // its own (single-compte) type breakdown, just never sliced down
@@ -85,6 +85,8 @@ class DashboardStatsController extends ControllerBase {
$totalParTypeParAnnee = []; $totalParTypeParAnnee = [];
$caParClient = []; $caParClient = [];
$caParClientParAnnee = []; $caParClientParAnnee = [];
$chargeParClient = [];
$chargeParClientParAnnee = [];
$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;
@@ -99,6 +101,16 @@ class DashboardStatsController extends ControllerBase {
$client = $row->client ?: '(sans client)'; $client = $row->client ?: '(sans client)';
$caParClient[$client] = ($caParClient[$client] ?? 0) + $montant; $caParClient[$client] = ($caParClient[$client] ?? 0) + $montant;
} }
// Same "who does this money go to" breakdown as $caParClient above,
// but for charges structurelles instead of entrées -- field_client
// on a charge line is the vendor/organisme (loyer, assurance,
// URSSAF...), not a paying client, but it's the same field/same
// taxonomy, so the same grouping applies. Absolute value, same
// convention as $totalParType.
if ($row->type === 'charge') {
$client = $row->client ?: '(sans client)';
$chargeParClient[$client] = ($chargeParClient[$client] ?? 0) + abs($montant);
}
if (!$this->isAnneeValide($row->annee)) { if (!$this->isAnneeValide($row->annee)) {
continue; continue;
@@ -112,6 +124,11 @@ class DashboardStatsController extends ControllerBase {
$caParClientParAnnee[$row->annee][$client] = $caParClientParAnnee[$row->annee][$client] =
($caParClientParAnnee[$row->annee][$client] ?? 0) + $montant; ($caParClientParAnnee[$row->annee][$client] ?? 0) + $montant;
} }
if ($row->type === 'charge') {
$client = $row->client ?: '(sans client)';
$chargeParClientParAnnee[$row->annee][$client] =
($chargeParClientParAnnee[$row->annee][$client] ?? 0) + abs($montant);
}
// Per-année équivalent of $totalParType above -- backs the // Per-année équivalent of $totalParType above -- backs the
// "Répartition de l'activité par type" small multiples. // "Répartition de l'activité par type" small multiples.
if ($row->type !== 'ouverture') { if ($row->type !== 'ouverture') {
@@ -148,6 +165,24 @@ class DashboardStatsController extends ControllerBase {
} }
} }
// No top-N cap here (unlike top clients above) -- structural charges
// only ever go to a handful of recurring vendors (loyer, assurance,
// hébergement, URSSAF...), not the 100+ distinct clients entrées can
// have, so the full list is already short.
arsort($chargeParClient);
$chargeParClientList = [];
foreach ($chargeParClient as $client => $total) {
$chargeParClientList[] = ['client' => $client, 'total' => round($total, 2)];
}
$chargeParClientParAnneeList = [];
foreach ($chargeParClientParAnnee as $annee => $parClient) {
arsort($parClient);
$chargeParClientParAnneeList[$annee] = [];
foreach ($parClient as $client => $total) {
$chargeParClientParAnneeList[$annee][] = ['client' => $client, 'total' => round($total, 2)];
}
}
// --- Aggregate the répartition-level rows in PHP. --- // --- Aggregate the répartition-level rows in PHP. ---
$soldeParCompte = []; $soldeParCompte = [];
$soldeParCompteParAnnee = []; $soldeParCompteParAnnee = [];
@@ -204,6 +239,7 @@ class DashboardStatsController extends ControllerBase {
ksort($totalParTypeParCompteParAnnee); ksort($totalParTypeParCompteParAnnee);
ksort($totalParTypeParAnnee); ksort($totalParTypeParAnnee);
ksort($topClientsParAnnee); ksort($topClientsParAnnee);
ksort($chargeParClientParAnneeList);
return new JsonResponse([ return new JsonResponse([
'annees' => $anneesList, 'annees' => $anneesList,
@@ -215,6 +251,8 @@ class DashboardStatsController extends ControllerBase {
), ),
'top_clients' => $topClients, 'top_clients' => $topClients,
'top_clients_par_annee' => $topClientsParAnnee, 'top_clients_par_annee' => $topClientsParAnnee,
'charge_par_client' => $chargeParClientList,
'charge_par_client_par_annee' => $chargeParClientParAnneeList,
'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(
@@ -60,6 +60,18 @@
</div> </div>
</section> </section>
<section class="figli-chart-section">
<h2>Charges structurelles par client</h2>
<p class="figli-note">Montant total (HT, valeur absolue) des charges structurelles par vendeur/organisme, toutes années confondues.</p>
<h-bar-chart :items="chargeParClientItems" :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 chargeParClientParAnnee" :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"> <section class="figli-chart-section">
<h2>Total des versements par compte</h2> <h2>Total des versements par compte</h2>
<p class="figli-note">Montant total versé à chaque compte associé, plus les salaires/stages et sous-traitants, toutes années confondues.</p> <p class="figli-note">Montant total versé à chaque compte associé, plus les salaires/stages et sous-traitants, toutes années confondues.</p>