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:
@@ -199,6 +199,22 @@
|
||||
return json;
|
||||
}
|
||||
|
||||
// POST /lignes/{nid}/champ -- change client/facture/libellé without
|
||||
// opening the full edit form. Same fresh-token-per-call reasoning as
|
||||
// updateLigneType() above.
|
||||
async function updateLigneField(nid, field, value) {
|
||||
const tokenRes = await fetch('/session/token');
|
||||
const token = await tokenRes.text();
|
||||
const res = await fetch('/lignes/' + nid + '/champ', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
|
||||
body: JSON.stringify({ field, value }),
|
||||
});
|
||||
const json = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(json.error || ('/lignes/' + nid + '/champ a répondu ' + res.status));
|
||||
return json;
|
||||
}
|
||||
|
||||
async function fetchYearTotals(annee) {
|
||||
const res = await fetch('/lignes/api/totaux?annee=' + encodeURIComponent(annee), { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error('/lignes/api/totaux a répondu ' + res.status);
|
||||
@@ -334,6 +350,10 @@
|
||||
// instead of the badge -- only one at a time.
|
||||
editingTypeId: null,
|
||||
typeUpdateError: null,
|
||||
// Which row+field (client/facture/libelle) is currently showing
|
||||
// its inline <input> instead of the plain text -- only one at a
|
||||
// time, mirroring editingTypeId above. { id, field } or null.
|
||||
editingCell: null,
|
||||
// Fenêtre glissante.
|
||||
windowStart: null,
|
||||
windowEnd: null,
|
||||
@@ -777,6 +797,29 @@
|
||||
this.typeUpdateError = err.message;
|
||||
}
|
||||
},
|
||||
startEditCell(item, field) {
|
||||
this.typeUpdateError = null;
|
||||
this.editingCell = { id: item.id, field };
|
||||
},
|
||||
isEditingCell(item, field) {
|
||||
return !!this.editingCell && this.editingCell.id === item.id && this.editingCell.field === field;
|
||||
},
|
||||
// Same optimistic-patch-then-close pattern as saveType() above --
|
||||
// client/facture/libellé don't affect linkability or
|
||||
// field_entree_liee, so there's nothing else to reconcile via
|
||||
// reloadWindow() here.
|
||||
async saveCell(item, field, event) {
|
||||
const newValue = event.target.value.trim();
|
||||
this.editingCell = null;
|
||||
if (newValue === (item[field] || '')) return;
|
||||
try {
|
||||
const result = await updateLigneField(item.nid, field, newValue);
|
||||
const row = this.rows.find((r) => r.id === item.id);
|
||||
if (row) row[field] = result.value;
|
||||
} catch (err) {
|
||||
this.typeUpdateError = err.message;
|
||||
}
|
||||
},
|
||||
onCellHover(evt) {
|
||||
const cell = evt.target.closest('td, th');
|
||||
if (!cell) return;
|
||||
|
||||
Reference in New Issue
Block a user