/** * @file * "Répartition / Soldes": solde par compte (all-time bar chart + one * year-by-year trend per compte), split out of the general /dashboard so * that page stays focused on activity/CA/type/client breakdowns. Same * /dashboard/api/stats endpoint as dashboard.js/dashboard-compte.js * (DashboardStatsController -- plain SQL GROUP BY, not Entity API), no * new backend needed -- solde_par_compte/solde_par_compte_par_annee were * already in that response, just unused on this page until now. * * HBarChart/MiniTrend are duplicated from dashboard.js rather than * shared, same established convention as dashboard-compte.js's own * copies (see dashboard.css's comment on the Signalement filter rules). * Root element id is deliberately the same #figli-dashboard-app as the * other two dashboard pages (not a unique id) -- dashboard.css scopes * its CSS custom properties (--figli-bg, dark-mode overrides, etc.) to * that selector, and reusing it is how all three dashboard pages pick * those up without a separate stylesheet. */ (function (Drupal, Vue) { 'use strict'; const EUR_ROUND = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }); async function fetchStats() { const res = await fetch('/dashboard/api/stats', { headers: { Accept: 'application/json' } }); if (!res.ok) throw new Error('/dashboard/api/stats a répondu ' + res.status); return res.json(); } // Horizontal bar chart -- same shape as dashboard.js's own copy, // including the zero-centered "diverging" layout for negative values // (a compte's solde can be a débit). const HBarChart = { props: { items: { type: Array, required: true }, formatValue: { type: Function, required: true }, colorFor: { type: Function, default: null }, }, computed: { hasNegative() { return this.items.some((i) => i.value < 0); }, maxAbs() { return Math.max(1, ...this.items.map((i) => Math.abs(i.value))); }, }, methods: { fillStyle(item) { const pct = (Math.abs(item.value) / this.maxAbs) * 100; if (this.hasNegative) { return item.value >= 0 ? { left: '50%', width: pct / 2 + '%' } : { right: '50%', width: pct / 2 + '%' }; } return { left: 0, width: pct + '%' }; }, fillColor(item) { if (this.colorFor) return this.colorFor(item); return item.value < 0 ? 'var(--figli-error)' : 'var(--figli-positive)'; }, }, template: '
', }; // Small multiples: one compact zero-centered bar-per-year trend per // compte, instead of a single 8-series line chart -- same reasoning // and same markup as dashboard.js's own copy. const MiniTrend = { props: { annees: { type: Array, required: true }, values: { type: Array, required: true }, formatValue: { type: Function, required: true }, }, computed: { maxAbs() { return Math.max(1, ...this.values.filter((v) => v !== null).map((v) => Math.abs(v))); }, }, methods: { barHeight(v) { if (v === null) return '0%'; return Math.max(3, (Math.abs(v) / this.maxAbs) * 100) + '%'; }, }, template: '