/** * @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 renders a spreadsheet-like table of every ligne comptable, * with filters, month/year grouping, and per-row écart (répartition sum vs * montant HT) highlighting -- inconsistencies are shown, not hidden. */ (function (Drupal, Vue, jQuery) { 'use strict'; const API_BASE = '/jsonapi/node/ligne_comptable'; const EUR = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }); const MONTHS = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']; const TYPE_LABELS = { entree: 'Entrée client', charge: 'Charge structurelle', versement: 'Versement freelance', achat: 'Achat client', autre: 'Autre', ouverture: 'Ouverture', }; async function fetchAllLignes() { let url = API_BASE + '?include=field_repartition,field_repartition.field_compte,field_client&page[limit]=50&sort=field_date_ligne'; 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); const json = await res.json(); allData.push(...(json.data || [])); (json.included || []).forEach((item) => includedMap.set(item.type + ':' + item.id, item)); url = json.links && json.links.next ? json.links.next.href : null; } return { data: allData, includedMap }; } function resolve(includedMap, ref) { if (!ref) return null; return includedMap.get(ref.type + ':' + ref.id) || null; } function buildRows(data, includedMap) { const rows = []; for (const node of data) { const rels = node.relationships || {}; const attrs = node.attributes; const clientTerm = resolve(includedMap, rels.field_client && rels.field_client.data); const parCompte = {}; let somme = 0; 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)'; parCompte[compteName] = (parCompte[compteName] || 0) + montant; somme += montant; } const montantHt = attrs.field_montant_ht !== null && attrs.field_montant_ht !== undefined ? parseFloat(attrs.field_montant_ht) : null; const ecart = montantHt !== null ? Math.round((montantHt - somme) * 100) / 100 : 0; rows.push({ id: node.id, date: attrs.field_date_ligne, type: attrs.field_type_ligne, client: clientTerm ? clientTerm.attributes.name : null, libelle: attrs.field_notes || attrs.title, montant_ht: montantHt, montant_ttc: attrs.field_montant_ttc !== null && attrs.field_montant_ttc !== undefined ? parseFloat(attrs.field_montant_ttc) : null, parCompte, somme, ecart, hasError: Math.abs(ecart) > 0.01, }); } rows.sort((a, b) => (a.date || '').localeCompare(b.date || '')); return rows; } const App = { data() { return { loading: true, error: null, rows: [], allComptes: ['Sandrine', 'Maud', 'Ouidade', 'Chloé', 'Bachir', 'Valentin', 'EXT.', 'EXT.WEB', 'Provision EPAU'], filterCompte: '', filterClient: '', filterType: '', filterYear: '', groupBy: 'month', onlyErrors: false, }; }, computed: { allTypes() { return Object.entries(TYPE_LABELS).map(([value, label]) => ({ value, label })); }, allClients() { return Array.from(new Set(this.rows.map((r) => r.client).filter(Boolean))).sort(); }, allYears() { return Array.from(new Set(this.rows.map((r) => (r.date || '').slice(0, 4)).filter(Boolean))).sort().reverse(); }, errorCount() { return this.rows.filter((r) => r.hasError).length; }, filteredRows() { return this.rows.filter((r) => { if (this.filterCompte && r.parCompte[this.filterCompte] === undefined) return false; if (this.filterClient && r.client !== this.filterClient) return false; if (this.filterType && r.type !== this.filterType) return false; if (this.filterYear && (r.date || '').slice(0, 4) !== this.filterYear) return false; if (this.onlyErrors && !r.hasError) return false; return true; }); }, groupedRows() { const list = this.filteredRows; if (this.groupBy === 'none') return list; const groups = new Map(); for (const r of list) { const d = r.date ? new Date(r.date + 'T00:00:00') : null; let key, label; if (this.groupBy === 'year') { key = d ? String(d.getFullYear()) : '?'; label = key; } else { key = d ? d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') : '?'; label = d ? MONTHS[d.getMonth()] + ' ' + d.getFullYear() : 'Date inconnue'; } if (!groups.has(key)) groups.set(key, { label, rows: [] }); groups.get(key).rows.push(r); } const sortedKeys = Array.from(groups.keys()).sort(); const out = []; for (const key of sortedKeys) { const g = groups.get(key); out.push({ isGroup: true, key: 'g-' + key, label: g.label, count: g.rows.length }); for (const r of g.rows) out.push(Object.assign({ key: 'r-' + r.id }, r)); } return out; }, }, methods: { formatEur(v) { return v === null || v === undefined ? '' : EUR.format(v); }, typeLabel(t) { return TYPE_LABELS[t] || t; }, openAddForm() { Drupal.ajax({ url: '/node/add/ligne_comptable', dialogType: 'modal', dialog: { width: 800, title: 'Ajouter une ligne comptable' }, progress: { type: 'throbber' }, }).execute(); }, async load() { this.loading = true; this.error = null; try { const { data, includedMap } = await fetchAllLignes(); this.rows = buildRows(data, includedMap); } catch (err) { this.error = err.message; } finally { this.loading = false; } }, }, mounted() { this.load(); jQuery(document).on('dialog:afterclose', () => this.load()); }, }; Drupal.behaviors.figliComptaHome = { attach(context) { const root = context.querySelector ? context.querySelector('#figli-home-app') : null; if (root && !root.dataset.figliInitialized) { root.dataset.figliInitialized = '1'; Vue.createApp(App).mount(root); } }, }; })(Drupal, Vue, jQuery);