Add spreadsheet-style home page, migrate full 2026 ledger

- New /lignes route (set as site front page): full line-by-line table of
  every ligne comptable, Vue app with filters (compte/client/type/année)
  and month/year grouping, columns matching the original spreadsheet
  (one per compte). Rows with répartition ≠ montant HT are visibly
  flagged (red row + écart column), not hidden or auto-corrected.
- /dashboard now only holds the aggregate solde-par-compte/par-client view
- Migrated all 199 real 2026 transaction lines + 9 opening balances (from
  REPORT CLOTURE 2025) via a drush import script, preserving raw source
  data (known répartition mismatches included) -- validated with a new
  state-flag bypass of the presave check, used only for historical import
- Added "Autre" as an allowed field_type_ligne value for edge-case rows
- Client taxonomy grew from 15 seeded terms to the full unified list

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 22:52:00 +02:00
co-authored by Claude Sonnet 5
parent ca800be21a
commit 10f6271fec
12 changed files with 496 additions and 61 deletions
@@ -1,11 +1,10 @@
/**
* @file
* Progressive decoupling: Drupal renders the page shell (nav, auth via
* session cookie, the "Ajouter une ligne" modal form); this Vue app fetches
* JSON:API and does all the aggregation (solde par compte, solde par
* client) client-side.
* Aggregate dashboard: solde par compte / solde par client, computed
* client-side from JSON:API. The line-by-line spreadsheet view is the
* site's home page (home.js), not this one.
*/
(function (Drupal, Vue, jQuery) {
(function (Drupal, Vue) {
'use strict';
const API_BASE = '/jsonapi/node/ligne_comptable';
@@ -15,12 +14,9 @@
let url = API_BASE + '?include=field_repartition,field_repartition.field_compte,field_client&page[limit]=50';
const allData = [];
const includedMap = new Map();
while (url) {
const res = await fetch(url, { headers: { Accept: 'application/vnd.api+json' } });
if (!res.ok) {
throw new Error('JSON:API a répondu ' + res.status);
}
if (!res.ok) throw new Error('JSON:API a répondu ' + res.status);
const json = await res.json();
allData.push(...(json.data || []));
(json.included || []).forEach((item) => includedMap.set(item.type + ':' + item.id, item));
@@ -44,21 +40,17 @@
function computeAggregations(data, includedMap) {
const parComptes = new Map();
const parClients = new Map();
for (const node of data) {
const rels = node.relationships || {};
const clientTerm = resolve(includedMap, rels.field_client && rels.field_client.data);
const clientName = clientTerm ? clientTerm.attributes.name : '(sans client)';
const repartitionRefs = (rels.field_repartition && rels.field_repartition.data) || [];
for (const ref of repartitionRefs) {
const paragraph = resolve(includedMap, ref);
if (!paragraph) continue;
const montant = parseFloat(paragraph.attributes.field_montant || 0);
const compteTerm = resolve(includedMap, paragraph.relationships && paragraph.relationships.field_compte && paragraph.relationships.field_compte.data);
const compteName = compteTerm ? compteTerm.attributes.name : '(compte inconnu)';
addTo(parComptes, compteName, montant);
addTo(parClients, clientName, montant);
}
@@ -81,12 +73,7 @@
const App = {
data() {
return {
loading: true,
error: null,
tables: [],
lineCount: 0,
};
return { loading: true, error: null, tables: [], lineCount: 0 };
},
methods: {
formatEur(v) {
@@ -97,14 +84,6 @@
if (solde < -0.5) return 'negative';
return '';
},
openAddForm() {
Drupal.ajax({
url: '/node/add/ligne_comptable',
dialogType: 'modal',
dialog: { width: 700, title: 'Ajouter une ligne comptable' },
progress: { type: 'throbber' },
}).execute();
},
async load() {
this.loading = true;
this.error = null;
@@ -115,18 +94,8 @@
const comptesRows = mapToRows(parComptes);
const clientsRows = mapToRows(parClients);
this.tables = [
{
title: 'Solde par compte',
note: this.lineCount + ' lignes comptables chargées.',
rows: comptesRows,
totals: totalsOf(comptesRows),
},
{
title: 'Solde par client',
note: 'Entrées créditées par client vs. montants sortis (versements, achats, charges) sur les lignes rattachées à ce client.',
rows: clientsRows,
totals: totalsOf(clientsRows),
},
{ title: 'Solde par compte', note: this.lineCount + ' lignes comptables chargées.', rows: comptesRows, totals: totalsOf(comptesRows) },
{ title: 'Solde par client', note: 'Entrées créditées par client vs. montants sortis (versements, achats, charges) sur les lignes rattachées à ce client.', rows: clientsRows, totals: totalsOf(clientsRows) },
];
} catch (err) {
this.error = err.message;
@@ -137,10 +106,6 @@
},
mounted() {
this.load();
// Drupal's dialog system fires this on <body> (via jQuery) whenever a
// modal closes -- refresh after "Ajouter une ligne" without a full
// page reload.
jQuery(document).on('dialog:afterclose', () => this.load());
},
};
@@ -150,11 +115,7 @@
if (root && !root.dataset.figliInitialized) {
root.dataset.figliInitialized = '1';
Vue.createApp(App).mount(root);
// Vue's mount() replaces the DOM node Drupal's own behaviors (like
// use-ajax on the "Ajouter une ligne" link) already saw at page
// load -- reattach so the modal link keeps working.
Drupal.attachBehaviors(root);
}
},
};
})(Drupal, Vue, jQuery);
})(Drupal, Vue);