Files
drupal-figli-compta/web/modules/custom/figli_compta_ledger/templates/figli-compta-home.html.twig
T
bachir 1ab38bc2a7 Highlight versements not (fully) backed by their entrée
For "versement freelance" rows specifically: an amber outline plus a
badge when the versement either isn't linked to any entrée client at
all ("Non liée"), or is linked but reconciliationByEntree still shows
a residual on that entrée ("Reste à verser" / "Sur-versé", with the
amount in the tooltip). The residual belongs to the entrée as a whole,
not to any one sortie -- when several versements share an entrée, each
shows the same aggregate figure, since there's no way to attribute the
shortfall to one specific payment. No highlight when the linked entrée
isn't in the currently loaded window (can't tell either way -- same
accepted trade-off as reconciliationByEntree itself).

Verified: filtering to "Versement freelance" shows 21 of 25 loaded
rows flagged (17 unlinked, 4 with a reste-à-verser residual), the
remaining 4 fully-reconciled rows correctly unflagged, and the amber
outline resolves correctly in dark mode.
2026-09-05 10:40:01 +02:00

198 lines
10 KiB
Twig

{#
Dashboard shell: Drupal renders the page (nav, auth, permissions).
dashboard.js (Vue 3) fetches JSON:API and renders a spreadsheet-like table
of every ligne comptable, with filters and month/year grouping, client-side.
"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">
<a href="/node/add/ligne_comptable" class="button button--primary" @click.prevent="openAddForm">+ Ajouter une ligne</a>
{% endverbatim %}
{% if can_view_history %}
<a href="{{ path('figli_compta_ledger.history') }}" class="button">Historique</a>
{% endif %}
{% verbatim %}
<label>Compte
<select v-model="filterCompte">
<option value="">Tous</option>
<option v-for="c in allComptes" :key="c" :value="c">{{ c }}</option>
</select>
</label>
<label>Client
<select v-model="filterClient">
<option value="">Tous</option>
<option v-for="c in allClientsList" :key="c" :value="c">{{ c }}</option>
</select>
</label>
<label>Type
<select v-model="filterType">
<option value="">Tous</option>
<option v-for="t in allTypes" :key="t.value" :value="t.value">{{ t.label }}</option>
</select>
</label>
<label>Année
<select v-model="filterYear">
<option value="">Toutes</option>
<option v-for="y in allYearsList" :key="y" :value="y">{{ y }}</option>
</select>
</label>
<label>Aller à
<select v-model="jumpYearValue" @change="onJumpYearChange">
<option value="">Aller à…</option>
<option v-for="y in allYearsList" :key="y" :value="y">{{ y }}</option>
</select>
</label>
<label>Regrouper par
<select v-model="groupBy">
<option value="none">Aucun</option>
<option value="month">Mois</option>
<option value="year">Année</option>
</select>
</label>
<label class="figli-checkbox">
<input type="checkbox" v-model="onlyErrors" /> Écarts uniquement
</label>
<button type="button" class="button figli-clear-drilldown" v-if="filterEntreeId" @click="filterEntreeId = null">✕ Entrée + sorties liées uniquement</button>
<span class="figli-count" v-if="!loading">{{ filteredRows.length }} / {{ rows.length }} lignes chargées{{ errorCount ? ' — ' + errorCount + ' avec écart' : '' }}</span>
</div>
<p v-if="typeUpdateError" class="figli-error figli-inline-error">Erreur : {{ typeUpdateError }} <button type="button" class="figli-clear-drilldown" @click="typeUpdateError = null">✕</button></p>
<p v-if="loading">Chargement des données…</p>
<p v-else-if="error" class="figli-error">Erreur de chargement : {{ error }}</p>
<div v-else ref="tableWrap" class="figli-table-wrap">
<table ref="tableEl" @mouseover="onCellHover" @mouseleave="clearColHover">
<thead>
<tr>
<th class="actions-col"></th>
<th class="actions-col"></th>
<th>Date</th>
<th>Client</th>
<th>Type</th>
<th>Libellé / Détail</th>
<th class="amount">Montant HT</th>
<th class="amount">Montant TTC</th>
<th v-for="c in allComptes" :key="c" class="amount compte-col">{{ c }}</th>
<th class="amount">Écart</th>
</tr>
</thead>
<tbody>
<tr ref="topSentinel" class="figli-sentinel-row">
<td :colspan="6 + allComptes.length + 3">
<span v-if="loadingOlder">Chargement des mois précédents…</span>
</td>
</tr>
<template v-for="item in groupedRows" :key="item.key">
<tr v-if="item.isGroup" class="figli-group-row" :data-year="item.year">
<td :colspan="6 + allComptes.length + 3">{{ item.label }} <span class="figli-note">({{ item.count }} lignes)</span></td>
</tr>
<tr v-else :class="{'figli-error-row': item.hasError, 'figli-versement-warning-row': versementStatus(item)}" :data-year="item.date ? item.date.slice(0, 4) : null">
<td class="actions-col">
<button
v-if="item.linkable"
type="button"
class="figli-link-btn"
:class="{'is-linked': item.entreeLieeId}"
:title="item.entreeLieeId ? 'Lié à : ' + item.entreeLieeLabel : 'Lier à une entrée client'"
@click="openLinkForm(item.nid)"
>
<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 12a3 3 0 0 0 4.24 0l2-2a3 3 0 0 0-4.24-4.24l-1 1" />
<path d="M12 8a3 3 0 0 0-4.24 0l-2 2a3 3 0 0 0 4.24 4.24l1-1" />
</svg>
</button>
</td>
<td class="actions-col">
<button type="button" class="figli-edit-btn" title="Modifier" @click="openEditForm(item.nid)">
<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M13.5 3.5l3 3L6 17l-3.5.5.5-3.5L13.5 3.5z" />
</svg>
</button>
</td>
<td>{{ formatDate(item.date) }}</td>
<td>{{ item.client || '—' }}</td>
<td>
<select
v-if="editingTypeId === item.id"
class="figli-type-select"
:value="item.type"
@change="saveType(item, $event)"
@blur="editingTypeId = null"
>
<option v-for="t in allTypes" :key="t.value" :value="t.value">{{ t.label }}</option>
</select>
<span
v-else
class="figli-badge figli-badge-editable"
:class="'type-' + item.type"
title="Cliquer pour changer le type"
@click="startEditType(item)"
>{{ typeLabel(item.type) }}</span>
</td>
<td class="figli-libelle">
{{ item.libelle }}
<span
v-if="item.type === 'entree' && reconciliationByEntree.get(item.id) && reconciliationByEntree.get(item.id).count > 0"
class="figli-recon-badge"
:class="{'is-anomalie': reconciliationByEntree.get(item.id).surVerse > 0, 'is-reste': reconciliationByEntree.get(item.id).resteAVerser > 0}"
:title="reconciliationByEntree.get(item.id).detail"
@click="toggleEntreeFilter(item.id)"
>{{ reconciliationByEntree.get(item.id).count }} sortie{{ reconciliationByEntree.get(item.id).count > 1 ? 's' : '' }} liée{{ reconciliationByEntree.get(item.id).count > 1 ? 's' : '' }}<template v-if="reconciliationByEntree.get(item.id).resteAVerser > 0"> · reste {{ formatEur(reconciliationByEntree.get(item.id).resteAVerser) }}</template><template v-if="reconciliationByEntree.get(item.id).surVerse > 0"> · sur-versé {{ formatEur(reconciliationByEntree.get(item.id).surVerse) }}</template></span>
<span
v-if="item.type === 'ouverture' && ouvertureEcart(item)"
class="figli-recon-badge is-anomalie"
:title="'Écart avec la clôture calculée de ' + (item.date.slice(0, 4) - 1) + ' : ' + ouvertureEcart(item).detail"
>⚠ écart clôture {{ item.date.slice(0, 4) - 1 }} ({{ ouvertureEcart(item).comptes }} compte{{ ouvertureEcart(item).comptes > 1 ? 's' : '' }})</span>
<span
v-if="versementStatus(item)"
class="figli-recon-badge"
:class="{'is-anomalie': versementStatus(item).kind !== 'reste', 'is-reste': versementStatus(item).kind === 'reste'}"
:title="versementStatus(item).detail"
>⚠ {{ versementStatusLabel(versementStatus(item).kind) }}</span>
</td>
<td class="amount" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td>
<td class="amount">{{ formatEur(item.montant_ttc) }}</td>
<td v-for="c in allComptes" :key="c" class="amount compte-col">{{ item.parCompte[c] !== undefined ? formatEur(item.parCompte[c]) : '' }}</td>
<td class="amount" :class="{'figli-ecart': item.hasError}">{{ item.hasError ? formatEur(item.ecart) : '' }}</td>
</tr>
</template>
<tr ref="bottomSentinel" class="figli-sentinel-row">
<td :colspan="6 + allComptes.length + 3">
<span v-if="loadingNewer">Chargement des mois suivants…</span>
</td>
</tr>
</tbody>
<tfoot>
<tr class="figli-totals-row">
<td class="actions-col"></td>
<td class="actions-col"></td>
<td colspan="4">
Solde {{ currentYear || '…' }} (créditeur / débiteur)
<span v-if="currentYearLoading" class="figli-note">chargement…</span>
</td>
<td class="amount">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ht) : '' }}</td>
<td class="amount">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ttc) : '' }}</td>
<td v-for="c in allComptes" :key="c" class="amount compte-col" :class="currentYearTotals ? soldeClass(currentYearTotals.par_compte[c]) : ''">{{ currentYearTotals && currentYearTotals.par_compte[c] !== undefined ? formatEur(currentYearTotals.par_compte[c]) : '' }}</td>
<td class="amount" :class="currentYearTotals ? soldeClass(currentYearTotals.ecart) : ''">{{ currentYearTotals ? formatEur(currentYearTotals.ecart) : '' }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
{% endverbatim %}