Make Client/Facture/Libellé editable in place, same as the type badge

New POST /lignes/{node}/champ endpoint (LedgerActionsController::updateField(),
whitelisted to client/facture/libelle -> field_client/field_numero_facture/
field_notes) mirrors updateType(): skips the répartition invariant check
for this save (client/facture/libellé never touch montant_ht or
field_repartition, so it can only ever leave a pre-existing historical
mismatch as it was, never introduce one), wrapped in the same
skip_validation state flag with a try/finally.

Client resolves the typed text against existing "Client" taxonomy terms
only (same known-names list the toolbar's Client filter already offers
via a datalist) -- a non-match is rejected with a clear error rather
than silently creating a new term from a typo.

Frontend mirrors the existing editingTypeId/startEditType/saveType
pattern exactly, generalized to any of the three fields via a single
{id, field} editingCell state.

Verified live: editing all three fields on a row with a known
répartition écart succeeds (bypass confirmed), an unknown client name
is rejected with a visible error and the display value stays unchanged,
and the database was confirmed clean of test artifacts afterward.
This commit is contained in:
2026-09-06 10:46:30 +02:00
parent 50a6432691
commit 11eb9cadb0
5 changed files with 222 additions and 24 deletions
@@ -144,7 +144,19 @@
</button>
</td>
<td>{{ formatDate(item.date) }}</td>
<td>{{ item.client || '—' }}</td>
<td>
<input
v-if="isEditingCell(item, 'client')"
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"
@@ -163,28 +175,50 @@
@click="startEditType(item)"
>{{ typeLabel(item.type) }}</span>
</td>
<td>{{ item.facture }}</td>
<td>
<input
v-if="isEditingCell(item, 'facture')"
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">
{{ item.libelle }}
<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)"
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]) : openLinkForm(item.nid)"
>{{ linkStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ linkStatusLabel(linkStatus(item).kind) }}</span>
<input
v-if="isEditingCell(item, 'libelle')"
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)"
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]) : openLinkForm(item.nid)"
>{{ linkStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ linkStatusLabel(linkStatus(item).kind) }}</span>
</template>
</td>
<td class="amount" :class="montantClass(item.montant_ht)">{{ formatEur(item.montant_ht) }}</td>
<td class="amount">{{ formatEur(item.montant_ttc) }}</td>