Rebuild /dashboard with charts, add Grand livre/Dashboard nav
Nav: a small "Grand livre" / "Dashboard" switcher, top-right on both
pages, with the current page highlighted. Needed hook_theme() to
declare current_route as an accepted variable for both theme hooks --
same class of bug as the earlier can_view_history fix: an undeclared
variable passed via the render array is silently dropped rather than
reaching Twig.
Dashboard: replaced the old "solde par compte / par client" tables
(computed client-side from a full unwindowed JSON:API fetch of every
node -- the same performance problem the /lignes sliding window was
built to avoid, just not yet felt at 1500+ lines) with a single
aggregate endpoint (DashboardStatsController, plain SQL GROUP BY) and
five chart panels: CA par année, solde par compte (diverging,
red/green), solde par compte trend (small multiples per compte),
répartition par type, top clients par CA. No charting library --
small dependency-free div/CSS bar charts (HBarChart/ColumnChart/
MiniTrend components in dashboard.js), consistent with this project
vendoring its own JS.
Two data-correctness fixes along the way: (1) the historical stray
mistyped dates (0213-06-15, 2015-08-29 -- preserved as-is per this
module's policy) needed excluding from per-year buckets without
excluding their money from all-time totals, so the filtering happens
per-output-field in PHP rather than as a blanket SQL date range. (2)
PHP silently casts numeric-looking array keys ("2021") to actual
integers, so array_keys() on a year-keyed map produces a mix of ints
and strings -- json_encode emits the int ones as bare JSON numbers in
a list (unlike object keys, which JSON always stringifies), which
broke the frontend's annees[i].slice(2) trend-card labels. Fixed with
an explicit array_map('strval', ...).
Verified: bar widths/colors match the underlying data exactly (e.g.
EXT.'s red bar is proportionally sized against Maud's green one per
their actual solde ratio), all 8 trend cards render correct year
labels, and both nav links correctly highlight on their own page.
This commit is contained in:
@@ -1,36 +1,69 @@
|
||||
{#
|
||||
Aggregate view: solde par compte / solde par client. The spreadsheet-like
|
||||
Charts and aggregate totals: solde par compte, chiffre d'affaires par
|
||||
année, répartition par type, top clients. The spreadsheet-like
|
||||
line-by-line view is the site's home page (figli-compta-home.html.twig).
|
||||
|
||||
{% verbatim %} below: this is Vue template syntax, not Twig -- both use
|
||||
{{ }}, so verbatim tells Twig to leave it alone and let Vue compile it
|
||||
in the browser.
|
||||
#}
|
||||
<nav class="figli-page-nav">
|
||||
<a href="{{ path('figli_compta_ledger.home') }}" class="{{ current_route == 'figli_compta_ledger.home' ? 'is-active' : '' }}">Grand livre</a>
|
||||
<a href="{{ path('figli_compta_ledger.dashboard') }}" class="{{ current_route == 'figli_compta_ledger.dashboard' ? 'is-active' : '' }}">Dashboard</a>
|
||||
</nav>
|
||||
{% verbatim %}
|
||||
<div id="figli-dashboard-app">
|
||||
<p v-if="loading">Chargement des données…</p>
|
||||
<p v-else-if="error" class="figli-error">Erreur de chargement du tableau de bord : {{ error }}</p>
|
||||
<template v-else>
|
||||
<section v-for="table in tables" :key="table.title">
|
||||
<h2>{{ table.title }}</h2>
|
||||
<p class="figli-note" v-if="table.note">{{ table.note }}</p>
|
||||
<table>
|
||||
<thead><tr><th></th><th>Entrées (+)</th><th>Sorties (-)</th><th>Solde</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="row in table.rows" :key="row.name" :class="rowClass(row.solde)">
|
||||
<td>{{ row.name }}</td>
|
||||
<td class="amount">{{ formatEur(row.entrees) }}</td>
|
||||
<td class="amount">{{ formatEur(row.sorties) }}</td>
|
||||
<td class="amount">{{ formatEur(row.solde) }}</td>
|
||||
</tr>
|
||||
<tr class="total">
|
||||
<td>TOTAL</td>
|
||||
<td class="amount">{{ formatEur(table.totals.entrees) }}</td>
|
||||
<td class="amount">{{ formatEur(table.totals.sorties) }}</td>
|
||||
<td class="amount">{{ formatEur(table.totals.solde) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="figli-summary-row">
|
||||
<div class="figli-summary-card">
|
||||
<div class="figli-summary-label">Chiffre d'affaires total</div>
|
||||
<div class="figli-summary-value">{{ formatEurRound(totalCA) }}</div>
|
||||
</div>
|
||||
<div class="figli-summary-card" v-if="caAnneeEnCours">
|
||||
<div class="figli-summary-label">CA {{ caAnneeEnCours.annee }}</div>
|
||||
<div class="figli-summary-value">{{ formatEurRound(caAnneeEnCours.value) }}</div>
|
||||
</div>
|
||||
<div class="figli-summary-card" v-for="item in soldeParCompteItems.slice(0, 1)" :key="'top-' + item.label">
|
||||
<div class="figli-summary-label">Meilleur solde</div>
|
||||
<div class="figli-summary-value" :class="item.value < 0 ? 'is-negative' : 'is-positive'">{{ item.label }} · {{ formatEurRound(item.value) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="figli-chart-section">
|
||||
<h2>Chiffre d'affaires par année</h2>
|
||||
<p class="figli-note">Total des entrées client (montant HT) facturées chaque année.</p>
|
||||
<column-chart :items="caParAnneeItems" :format-value="formatEurRound"></column-chart>
|
||||
</section>
|
||||
|
||||
<section class="figli-chart-section">
|
||||
<h2>Solde par compte</h2>
|
||||
<p class="figli-note">Solde cumulé de chaque compte depuis l'origine (ouverture comprise) -- vert = créditeur, rouge = débiteur.</p>
|
||||
<h-bar-chart :items="soldeParCompteItems" :format-value="formatEurRound"></h-bar-chart>
|
||||
</section>
|
||||
|
||||
<section class="figli-chart-section">
|
||||
<h2>Évolution du solde par compte</h2>
|
||||
<p class="figli-note">Solde de clôture de chaque compte, année par année.</p>
|
||||
<div class="figli-trend-grid">
|
||||
<div class="figli-trend-card" v-for="compte in comptesOrdonnes" :key="compte">
|
||||
<div class="figli-trend-title">{{ compte }}</div>
|
||||
<mini-trend :annees="stats.annees" :values="trendValues(compte)" :format-value="formatEurRound"></mini-trend>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="figli-chart-section">
|
||||
<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>
|
||||
<h-bar-chart :items="typeItems" :format-value="formatEurRound" :color-for="typeColor"></h-bar-chart>
|
||||
</section>
|
||||
|
||||
<section class="figli-chart-section">
|
||||
<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>
|
||||
<h-bar-chart :items="topClientsItems" :format-value="formatEurRound"></h-bar-chart>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
"Ajouter une ligne" opens the real Drupal node form in a modal
|
||||
(core/drupal.dialog.ajax) -- no form logic duplicated in JS.
|
||||
#}
|
||||
<nav class="figli-page-nav">
|
||||
<a href="{{ path('figli_compta_ledger.home') }}" class="{{ current_route == 'figli_compta_ledger.home' ? 'is-active' : '' }}">Grand livre</a>
|
||||
<a href="{{ path('figli_compta_ledger.dashboard') }}" class="{{ current_route == 'figli_compta_ledger.dashboard' ? 'is-active' : '' }}">Dashboard</a>
|
||||
</nav>
|
||||
{% verbatim %}
|
||||
<div id="figli-home-app">
|
||||
<div class="figli-toolbar">
|
||||
|
||||
Reference in New Issue
Block a user