field_cotisation_urssaf stockait round(HT * 1.011, 2) (le montant augmenté) -- "1,1%" comme libellé de champ/colonne désigne la cotisation elle-même, pas HT + cotisation. Change pour round(HT * 0.011, 2). La TVA continue de s'appliquer sur la base augmentée (HT + ce champ) : Montant TTC est inchangé par cette correction, seule la valeur affichée/stockée dans "1,1%" change. Migration (update hook 8008) : recalcule field_cotisation_urssaf pour les 154 lignes que figli_compta_ledger_update_8007() avait marquées cotisation active, en soustrayant simplement le HT déjà correct -- contrairement à cette dernière, ce n'est pas un arbitrage sur des données historiques ambiguës, juste un bug dans du code écrit plus tôt le même jour, donc recalculé sans condition ni prudence particulière. Ajoute aussi le total "1,1%" au pied du tableau (solde de l'année), absent jusqu'ici : LedgerStatsController::totauxAnnee() somme maintenant field_cotisation_urssaf comme il le fait déjà pour HT/TTC.
373 lines
20 KiB
Twig
373 lines
20 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>
|
|
<a href="{{ path('figli_compta_ledger.dashboard_compte') }}" class="{{ current_route == 'figli_compta_ledger.dashboard_compte' ? 'is-active' : '' }}">Par compte</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
|
|
<span class="figli-filter-row">
|
|
<div class="figli-multiselect">
|
|
<div class="figli-multiselect-trigger">{{ filterCompte.length ? filterCompte.join(', ') : 'Tous' }} <span class="figli-multiselect-arrow">▾</span></div>
|
|
<div class="figli-multiselect-panel">
|
|
<label v-for="c in allComptes" :key="c" class="figli-multiselect-option">
|
|
<input type="checkbox" :value="c" v-model="filterCompte" /> {{ c }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<button v-if="filterCompte.length" type="button" class="figli-filter-clear" @click="filterCompte = []" title="Effacer ce filtre">✕</button>
|
|
</span>
|
|
</label>
|
|
|
|
<label>Client
|
|
<span class="figli-filter-row">
|
|
<input type="text" v-model.lazy="filterClient" list="figli-client-datalist" placeholder="Tous" autocomplete="off" class="figli-client-input" />
|
|
<datalist id="figli-client-datalist">
|
|
<option v-for="c in allClientsList" :key="c" :value="c"></option>
|
|
</datalist>
|
|
<button v-if="filterClient" type="button" class="figli-filter-clear" @click="filterClient = ''" title="Effacer ce filtre">✕</button>
|
|
</span>
|
|
</label>
|
|
|
|
<label>Type
|
|
<span class="figli-filter-row">
|
|
<div class="figli-multiselect">
|
|
<div class="figli-multiselect-trigger">{{ filterType.length ? filterType.map(typeLabel).join(', ') : 'Tous' }} <span class="figli-multiselect-arrow">▾</span></div>
|
|
<div class="figli-multiselect-panel">
|
|
<label v-for="t in allTypes" :key="t.value" class="figli-multiselect-option">
|
|
<input type="checkbox" :value="t.value" v-model="filterType" /> {{ t.label }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<button v-if="filterType.length" type="button" class="figli-filter-clear" @click="filterType = []" title="Effacer ce filtre">✕</button>
|
|
</span>
|
|
</label>
|
|
|
|
<label>Signalement
|
|
<span class="figli-filter-row">
|
|
<div class="figli-multiselect">
|
|
<div class="figli-multiselect-trigger">{{ filterFlag.length ? filterFlag.join(', ') : 'Tous' }} <span class="figli-multiselect-arrow">▾</span></div>
|
|
<div class="figli-multiselect-panel">
|
|
<label v-for="f in allFlagsList" :key="f" class="figli-multiselect-option">
|
|
<input type="checkbox" :value="f" v-model="filterFlag" /> {{ f }}
|
|
</label>
|
|
<p v-if="!allFlagsList.length" class="figli-note">Aucun signalement existant.</p>
|
|
</div>
|
|
</div>
|
|
<button v-if="filterFlag.length" type="button" class="figli-filter-clear" @click="filterFlag = []" title="Effacer ce filtre">✕</button>
|
|
</span>
|
|
</label>
|
|
|
|
<label>Année
|
|
<span class="figli-filter-row">
|
|
<select v-model="filterYear">
|
|
<option value="">Toutes</option>
|
|
<option v-for="y in allYearsList" :key="y" :value="y">{{ y }}</option>
|
|
</select>
|
|
<button v-if="filterYear" type="button" class="figli-filter-clear" @click="filterYear = ''" title="Effacer ce filtre">✕</button>
|
|
</span>
|
|
</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>
|
|
|
|
<label class="figli-checkbox">
|
|
<input type="checkbox" v-model="onlyFlagged" /> Signalées uniquement
|
|
</label>
|
|
|
|
<datalist id="figli-flag-datalist">
|
|
<option v-for="f in allFlagsList" :key="f" :value="f"></option>
|
|
</datalist>
|
|
|
|
<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>Date</th>
|
|
<th>Client</th>
|
|
<th>Type</th>
|
|
<th>Facture</th>
|
|
<th>Libellé / Détail</th>
|
|
<th>Signalement</th>
|
|
<th class="amount figli-ht-col">HT</th>
|
|
<th class="amount figli-cotisation-col">1,1%</th>
|
|
<th class="amount figli-tva-col">TVA</th>
|
|
<th class="amount figli-ttc-col">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="7 + allComptes.length + 5">
|
|
<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="7 + allComptes.length + 5">{{ item.label }} <span class="figli-note">({{ item.count }} lignes)</span></td>
|
|
</tr>
|
|
<tr v-else :class="{'figli-flag-row': item.hasFlag}" :data-year="item.date ? item.date.slice(0, 4) : null">
|
|
<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>
|
|
<input
|
|
v-if="isEditingCell(item, 'client')"
|
|
v-focus
|
|
type="text"
|
|
class="figli-inline-input"
|
|
list="figli-client-datalist"
|
|
:value="item.client"
|
|
@change="saveCell(item, 'client', $event)"
|
|
@blur="editingCell = null"
|
|
@keyup.enter="$event.target.blur()"
|
|
/>
|
|
<span v-else class="figli-editable-cell" title="Cliquer pour modifier" @click="startEditCell(item, 'client')">{{ item.client || '—' }}</span>
|
|
</td>
|
|
<td>
|
|
<select
|
|
v-if="editingTypeId === item.id"
|
|
v-focus
|
|
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>
|
|
<input
|
|
v-if="isEditingCell(item, 'facture')"
|
|
v-focus
|
|
type="text"
|
|
class="figli-inline-input"
|
|
:value="item.facture"
|
|
@change="saveCell(item, 'facture', $event)"
|
|
@blur="editingCell = null"
|
|
@keyup.enter="$event.target.blur()"
|
|
/>
|
|
<span v-else class="figli-editable-cell" title="Cliquer pour modifier" @click="startEditCell(item, 'facture')">{{ item.facture || '—' }}</span>
|
|
</td>
|
|
<td class="figli-libelle">
|
|
<input
|
|
v-if="isEditingCell(item, 'libelle')"
|
|
v-focus
|
|
type="text"
|
|
class="figli-inline-input"
|
|
:value="item.libelle"
|
|
@change="saveCell(item, 'libelle', $event)"
|
|
@blur="editingCell = null"
|
|
@keyup.enter="$event.target.blur()"
|
|
/>
|
|
<template v-else>
|
|
<span class="figli-editable-cell" title="Cliquer pour modifier" @click="startEditCell(item, 'libelle')">{{ item.libelle || '—' }}</span>
|
|
<span
|
|
v-if="item.type === 'entree' && reconciliationByEntree.get(item.id) && reconciliationByEntree.get(item.id).count > 0"
|
|
class="figli-recon-badge is-clickable"
|
|
: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="linkStatus(item) && item.type !== 'hebergement'"
|
|
class="figli-recon-badge is-clickable"
|
|
:class="linkStatusClasses(linkStatus(item).kind)"
|
|
:title="item.entreeLieeIds.length ? linkStatus(item).detail + ' -- cliquer pour voir la ou les entrées liées' : linkStatus(item).detail + ' -- cliquer pour lier une entrée client'"
|
|
@click="item.entreeLieeIds.length ? toggleEntreeFilter(item.entreeLieeIds[0], item.nid) : openLinkForm(item.nid)"
|
|
>{{ linkStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ linkStatusLabel(linkStatus(item)) }}</span>
|
|
</template>
|
|
</td>
|
|
<td class="figli-flag-cell">
|
|
<span class="figli-editable-cell" title="Cliquer pour gérer le signalement" @click="openFlagModal(item)">
|
|
<template v-if="item.flags.length"><span v-for="f in item.flags" :key="f" class="figli-flag-badge">{{ f }}</span></template>
|
|
<template v-else>—</template>
|
|
</span>
|
|
</td>
|
|
<td class="amount figli-ht-col" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td>
|
|
<td class="amount figli-cotisation-col">{{ formatEur(item.cotisation) }}</td>
|
|
<td class="amount figli-tva-col">{{ formatPct(item.tva) }}</td>
|
|
<td class="amount figli-ttc-col">{{ 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="7 + allComptes.length + 5">
|
|
<span v-if="loadingNewer">Chargement des mois suivants…</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr class="figli-totals-row">
|
|
<td class="actions-col"></td>
|
|
<td colspan="6">
|
|
Solde {{ currentYear || '…' }} (créditeur / débiteur)
|
|
<span v-if="currentYearLoading" class="figli-note">chargement…</span>
|
|
</td>
|
|
<td class="amount figli-ht-col">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ht) : '' }}</td>
|
|
<td class="amount figli-cotisation-col">{{ currentYearTotals ? formatEur(currentYearTotals.cotisation) : '' }}</td>
|
|
<td class="amount figli-tva-col"></td>
|
|
<td class="amount figli-ttc-col">{{ 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 v-if="filterEntreeId" class="figli-modal-backdrop" @click.self="closeDrilldown">
|
|
<div class="figli-modal">
|
|
<div class="figli-modal-header">
|
|
<h3>Entrées et sorties liées <span v-if="groupLoading" class="figli-note">vérification…</span></h3>
|
|
<button type="button" class="figli-modal-close" @click="closeDrilldown">✕</button>
|
|
</div>
|
|
<div class="figli-modal-body">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th class="actions-col"></th>
|
|
<th>Date</th>
|
|
<th>Client</th>
|
|
<th>Type</th>
|
|
<th>Facture</th>
|
|
<th>Libellé / Détail</th>
|
|
<th class="amount">Montant HT</th>
|
|
<th class="amount">Montant TTC</th>
|
|
<th v-for="c in modalComptes" :key="c" class="amount compte-col">{{ c }}</th>
|
|
<th class="amount">Écart</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="item in filterEntreeGroup" :key="item.id">
|
|
<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><span class="figli-badge" :class="'type-' + item.type">{{ typeLabel(item.type) }}</span></td>
|
|
<td>{{ item.facture }}</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}"
|
|
>{{ 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="linkStatus(item) && item.type !== 'hebergement'"
|
|
class="figli-recon-badge"
|
|
:class="linkStatusClasses(linkStatus(item).kind)"
|
|
>{{ linkStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ linkStatusLabel(linkStatus(item)) }}</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 modalComptes" :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>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr class="figli-totals-row">
|
|
<td class="actions-col"></td>
|
|
<td colspan="5">Solde entrées et sorties liées (créditeur / débiteur)</td>
|
|
<td class="amount">{{ drilldownTotals ? formatEur(drilldownTotals.montant_ht) : '' }}</td>
|
|
<td class="amount">{{ drilldownTotals ? formatEur(drilldownTotals.montant_ttc) : '' }}</td>
|
|
<td v-for="c in modalComptes" :key="c" class="amount compte-col" :class="drilldownTotals ? soldeClass(drilldownTotals.par_compte[c]) : ''">{{ drilldownTotals && drilldownTotals.par_compte[c] !== undefined ? formatEur(drilldownTotals.par_compte[c]) : '' }}</td>
|
|
<td class="amount" :class="drilldownTotals ? soldeClass(drilldownTotals.ecart) : ''">{{ drilldownTotals ? formatEur(drilldownTotals.ecart) : '' }}</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="flagModalItem" class="figli-modal-backdrop" @click.self="closeFlagModal">
|
|
<div class="figli-modal figli-flag-modal">
|
|
<div class="figli-modal-header">
|
|
<h3>Signalement -- {{ formatDate(flagModalItem.date) }}{{ flagModalItem.client ? ' · ' + flagModalItem.client : '' }}</h3>
|
|
<button type="button" class="figli-modal-close" @click="closeFlagModal">✕ Fermer</button>
|
|
</div>
|
|
<div class="figli-flag-modal-body">
|
|
<p v-if="!flagModalItem.flags.length" class="figli-note">Aucun signalement pour cette ligne.</p>
|
|
<div v-for="f in flagModalItem.flags" :key="f" class="figli-flag-modal-row">
|
|
<span class="figli-flag-badge">{{ f }}</span>
|
|
<button type="button" class="figli-flag-remove" title="Retirer ce signalement" @click="removeFlagTag(f)">✕</button>
|
|
</div>
|
|
<div class="figli-flag-modal-add">
|
|
<input
|
|
v-focus
|
|
type="text"
|
|
list="figli-flag-datalist"
|
|
placeholder="Ajouter un signalement…"
|
|
v-model="flagModalNewTag"
|
|
@keyup.enter="addFlagTag"
|
|
/>
|
|
<button type="button" class="button" @click="addFlagTag">Ajouter</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endverbatim %}
|