Add "Répartition de l'activité par type" chart to /dashboard/compte
Extends DashboardStatsController::stats()'s répartition-level SQL query to also group by type (not just année/compte), producing a new total_par_type_par_compte breakdown alongside the existing global one. Unlike the reconciliation tables on this page (deliberately entrée/ versement only), this chart covers every type touching the selected compte's répartition, matching what the general /dashboard already shows for the whole ledger. Fixed a latent bug the query change would otherwise have introduced: solde_par_compte_par_annee[année][compte] used to be a 1:1 assignment because each (année, compte) pair was unique in the old query -- adding type to the GROUP BY means several rows can now share that same pair, so it has to accumulate instead of overwrite (verified the accumulated totals exactly match a query without the type split, so this preserves existing behavior for the fields already in use). HBarChart in dashboard-compte.js gained the same colorFor prop dashboard.js's version already has (existing Top clients usage keeps its default green, unaffected). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 @@
|
||||
'</div>',
|
||||
};
|
||||
|
||||
// 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:
|
||||
'<div class="figli-hbar-chart">' +
|
||||
'<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">' +
|
||||
'<div class="figli-hbar-fill" :style="{left: 0, width: (Math.abs(item.value) / maxAbs * 100) + \'%\', background: \'var(--figli-positive)\'}"></div>' +
|
||||
'<div class="figli-hbar-fill" :style="{left: 0, width: (Math.abs(item.value) / maxAbs * 100) + \'%\', background: fillColor(item)}"></div>' +
|
||||
'</div>' +
|
||||
'<div class="figli-hbar-value">{{ formatValue(item.value) }}</div>' +
|
||||
'</div>' +
|
||||
@@ -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('-');
|
||||
|
||||
Reference in New Issue
Block a user