diff --git a/web/modules/custom/figli_compta_ledger/js/dashboard-compte.js b/web/modules/custom/figli_compta_ledger/js/dashboard-compte.js
index cb2506e..f8f6f34 100644
--- a/web/modules/custom/figli_compta_ledger/js/dashboard-compte.js
+++ b/web/modules/custom/figli_compta_ledger/js/dashboard-compte.js
@@ -26,6 +26,30 @@
const EUR = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
const EUR_ROUND = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
const MONTHS_SHORT = ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'];
+ // Same labels/colors as dashboard.js's "Répartition de l'activité par
+ // type" -- duplicated rather than shared, see this file's docblock.
+ const TYPE_LABELS = {
+ entree: 'Entrée client',
+ charge: 'Charge structurelle',
+ versement: 'Versement freelance',
+ achat: 'Achat client',
+ hebergement: 'Hébergement',
+ sous_traitant: 'Sous-traitant',
+ salaire_stage: 'Salaire / stage',
+ charges_local_pro: 'Charges local pro',
+ autre: 'Autre',
+ };
+ const TYPE_COLORS = {
+ entree: '#1a7f37',
+ charge: '#6b7280',
+ versement: '#d97a0a',
+ achat: '#3b6fe0',
+ hebergement: '#0e9182',
+ sous_traitant: '#c9312b',
+ salaire_stage: '#0891b2',
+ charges_local_pro: '#65a30d',
+ autre: '#9061f0',
+ };
function resolve(includedMap, ref) {
if (!ref) return null;
@@ -181,22 +205,31 @@
'',
};
+ // Same shape as dashboard.js's HBarChart, colorFor included (used by
+ // the type-breakdown chart; Top clients below just omits it and gets
+ // the plain positive-green default).
const HBarChart = {
props: {
items: { type: Array, required: true },
formatValue: { type: Function, required: true },
+ colorFor: { type: Function, default: null },
},
computed: {
maxAbs() {
return Math.max(1, ...this.items.map((i) => Math.abs(i.value)));
},
},
+ methods: {
+ fillColor(item) {
+ return this.colorFor ? this.colorFor(item) : 'var(--figli-positive)';
+ },
+ },
template:
'
' +
'
' +
'
{{ item.label }}
' +
'
' +
'
{{ formatValue(item.value) }}
' +
'
' +
@@ -378,6 +411,20 @@
.sort((a, b) => b.value - a.value)
.slice(0, 10);
},
+ // Same chart as /dashboard's "Répartition de l'activité par type",
+ // filtered to this compte -- unlike the reconciliation tables above
+ // (deliberately entrée/versement only, see this file's docblock),
+ // this comes straight from /dashboard/api/stats's per-compte
+ // breakdown, so it covers every type touching this compte's
+ // répartition (charge, achat, hébergement...), matching what the
+ // general dashboard shows for the whole ledger.
+ typeItems() {
+ if (!this.stats || !this.selectedCompte) return [];
+ const parType = this.stats.total_par_type_par_compte[this.selectedCompte] || {};
+ return Object.entries(parType)
+ .map(([type, value]) => ({ label: TYPE_LABELS[type] || type, value, type }))
+ .sort((a, b) => b.value - a.value);
+ },
},
methods: {
formatEur(v) {
@@ -386,6 +433,9 @@
formatEurRound(v) {
return EUR_ROUND.format(v);
},
+ typeColor(item) {
+ return TYPE_COLORS[item.type] || '#6b7280';
+ },
formatDate(d) {
if (!d) return '';
const parts = d.split('-');
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 e834192..4974864 100644
--- a/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php
+++ b/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php
@@ -57,11 +57,14 @@ class DashboardStatsController extends ControllerBase {
$nodeRows = $nodeQuery->execute()->fetchAll();
// Répartition-level aggregate: one row per (node, compte) répartition
- // share, pre-summed per (annee, compte) in SQL -- backs solde par
- // compte, both all-time and per-year (each year's own total already
- // includes that year's ouverture line, so it *is* that year's closing
- // balance -- same logic as LedgerStatsController::totauxAnnee()).
+ // share, pre-summed per (annee, compte, type) in SQL -- backs solde
+ // par compte, both all-time and per-year (each year's own total
+ // already includes that year's ouverture line, so it *is* that
+ // year's closing balance -- same logic as
+ // LedgerStatsController::totauxAnnee()), and the per-compte type
+ // breakdown used by /dashboard/compte.
$compteQuery = $connection->select('node__field_date_ligne', 'd');
+ $compteQuery->innerJoin('node__field_type_ligne', 't2', 't2.entity_id = d.entity_id');
$compteQuery->innerJoin('node__field_repartition', 'r', 'r.entity_id = d.entity_id');
$compteQuery->innerJoin('paragraph__field_montant', 'm', 'm.entity_id = r.field_repartition_target_id');
$compteQuery->innerJoin('paragraph__field_compte', 'c', 'c.entity_id = r.field_repartition_target_id');
@@ -69,9 +72,11 @@ class DashboardStatsController extends ControllerBase {
$compteQuery->condition('d.bundle', 'ligne_comptable');
$compteQuery->addExpression('SUBSTRING(d.field_date_ligne_value, 1, 4)', 'annee');
$compteQuery->addField('tc', 'name', 'compte');
+ $compteQuery->addField('t2', 'field_type_ligne_value', 'type');
$compteQuery->addExpression('SUM(m.field_montant_value)', 'total');
$compteQuery->groupBy('annee');
$compteQuery->groupBy('compte');
+ $compteQuery->groupBy('type');
$compteRows = $compteQuery->execute()->fetchAll();
// --- Aggregate the node-level rows in PHP. ---
@@ -114,16 +119,31 @@ class DashboardStatsController extends ControllerBase {
// --- Aggregate the répartition-level rows in PHP. ---
$soldeParCompte = [];
$soldeParCompteParAnnee = [];
+ $totalParTypeParCompte = [];
foreach ($compteRows as $row) {
$total = (float) $row->total;
// Same reasoning: the all-time balance includes every line; the
// per-year trend only makes sense for a real year.
$soldeParCompte[$row->compte] = ($soldeParCompte[$row->compte] ?? 0) + $total;
+
+ // Per-compte équivalent of $totalParType above -- every type
+ // counts here (not just entree/versement), same "hors ouverture,
+ // valeur absolue" convention.
+ if ($row->type !== 'ouverture') {
+ $totalParTypeParCompte[$row->compte][$row->type] =
+ ($totalParTypeParCompte[$row->compte][$row->type] ?? 0) + abs($total);
+ }
+
if (!$this->isAnneeValide($row->annee)) {
continue;
}
$annees[$row->annee] = TRUE;
- $soldeParCompteParAnnee[$row->annee][$row->compte] = round($total, 2);
+ // compteRows now has one row per (année, compte, type) -- several
+ // types can share the same (année, compte), so this has to
+ // accumulate, not overwrite, or only the last type processed for
+ // that year+compte would survive.
+ $soldeParCompteParAnnee[$row->annee][$row->compte] =
+ round(($soldeParCompteParAnnee[$row->annee][$row->compte] ?? 0) + $total, 2);
}
// array_keys() alone would leak PHP's array-key int-casting here: a
@@ -148,6 +168,10 @@ class DashboardStatsController extends ControllerBase {
'top_clients' => $topClients,
'solde_par_compte' => array_map(fn ($v) => round($v, 2), $soldeParCompte),
'solde_par_compte_par_annee' => $soldeParCompteParAnnee,
+ 'total_par_type_par_compte' => array_map(
+ fn ($parType) => array_map(fn ($v) => round($v, 2), $parType),
+ $totalParTypeParCompte
+ ),
]);
}
diff --git a/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard-compte.html.twig b/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard-compte.html.twig
index a964041..278e66d 100644
--- a/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard-compte.html.twig
+++ b/web/modules/custom/figli_compta_ledger/templates/figli-compta-dashboard-compte.html.twig
@@ -144,6 +144,12 @@
+
+ Répartition de l'activité par type
+ Montant total (HT, valeur absolue) par type de ligne pour {{ selectedCompte }}, hors ouvertures -- contrairement aux tableaux ci-dessus, tous les types comptent ici (pas seulement entrée/versement).
+
+
+
Top clients
Clients ayant généré le plus d'entrées attribuées à {{ selectedCompte }}, toutes années confondues.