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:
@@ -13,11 +13,26 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
* Small write endpoints backing inline (no-modal) edits from the /lignes
|
||||
* table. Each goes through the normal node save() lifecycle -- same as the
|
||||
* full edit form -- so figli_compta_ledger_node_presave() still forces a
|
||||
* proper revision and still enforces the répartition invariant; nothing
|
||||
* here bypasses that.
|
||||
* proper revision. The répartition invariant check is deliberately
|
||||
* skipped for these saves though (same as the type-change endpoint
|
||||
* below): none of client/facture/libellé/type touch montant_ht or
|
||||
* field_repartition, so skipping can never *introduce* a mismatch, only
|
||||
* leave a pre-existing historical one exactly as it was -- see each
|
||||
* method's own comment.
|
||||
*/
|
||||
class LedgerActionsController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Fields editable inline from /lignes without opening the full node
|
||||
* edit form -- keys are the short names the frontend sends; values are
|
||||
* the real field machine names.
|
||||
*/
|
||||
const INLINE_EDITABLE_FIELDS = [
|
||||
'client' => 'field_client',
|
||||
'facture' => 'field_numero_facture',
|
||||
'libelle' => 'field_notes',
|
||||
];
|
||||
|
||||
/**
|
||||
* Every value field_type_ligne actually allows (see the field's
|
||||
* allowed_values in config) -- validated against here rather than
|
||||
@@ -97,4 +112,76 @@ class LedgerActionsController extends ControllerBase {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /lignes/{node}/champ -- change client/facture/libellé inline,
|
||||
* for clicking directly on those cells in the table. Body:
|
||||
* {"field": "client", "value": "EPAU / POPSU"}. An empty value clears
|
||||
* the field (e.g. a structural charge with no client).
|
||||
*/
|
||||
public function updateField(Request $request, NodeInterface $node) {
|
||||
if ($node->bundle() !== 'ligne_comptable') {
|
||||
return new JsonResponse(['error' => 'Type de contenu invalide.'], 404);
|
||||
}
|
||||
|
||||
$csrfToken = $request->headers->get('X-CSRF-Token', '');
|
||||
if (!\Drupal::csrfToken()->validate($csrfToken, CsrfRequestHeaderAccessCheck::TOKEN_KEY)) {
|
||||
return new JsonResponse(['error' => 'Jeton de sécurité invalide, rechargez la page.'], 403);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), TRUE);
|
||||
$field = is_array($data) ? ($data['field'] ?? NULL) : NULL;
|
||||
$value = trim((string) (is_array($data) ? ($data['value'] ?? '') : ''));
|
||||
if (!isset(self::INLINE_EDITABLE_FIELDS[$field])) {
|
||||
return new JsonResponse(['error' => 'Champ invalide.'], 400);
|
||||
}
|
||||
$fieldName = self::INLINE_EDITABLE_FIELDS[$field];
|
||||
|
||||
if ($field === 'client') {
|
||||
// Only an existing "Client" term is accepted -- the front-end
|
||||
// offers this as a datalist of known names, not free text, so a
|
||||
// non-match almost certainly means a typo rather than a genuinely
|
||||
// new client that should be created on the fly.
|
||||
if ($value === '') {
|
||||
$node->set('field_client', NULL);
|
||||
}
|
||||
else {
|
||||
$terms = $this->entityTypeManager()->getStorage('taxonomy_term')
|
||||
->loadByProperties(['vid' => 'client', 'name' => $value]);
|
||||
if (!$terms) {
|
||||
return new JsonResponse(['error' => 'Client inconnu : "' . $value . '". Utilisez un nom existant dans la liste.'], 400);
|
||||
}
|
||||
$node->set('field_client', reset($terms)->id());
|
||||
}
|
||||
}
|
||||
else {
|
||||
$node->set($fieldName, $value !== '' ? $value : NULL);
|
||||
}
|
||||
|
||||
// Same reasoning as updateType() above: only client/facture/libellé
|
||||
// changes here, montant_ht and field_repartition are untouched, so
|
||||
// skipping the répartition check for this save can never introduce a
|
||||
// mismatch -- it can only leave a pre-existing historical one exactly
|
||||
// as it was.
|
||||
\Drupal::state()->set('figli_compta_ledger.skip_validation', TRUE);
|
||||
try {
|
||||
$node->save();
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
return new JsonResponse(['error' => $e->getMessage()], 422);
|
||||
}
|
||||
finally {
|
||||
\Drupal::state()->delete('figli_compta_ledger.skip_validation');
|
||||
}
|
||||
|
||||
$newValue = $field === 'client'
|
||||
? ($node->get('field_client')->entity ? $node->get('field_client')->entity->label() : NULL)
|
||||
: $node->get($fieldName)->value;
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => TRUE,
|
||||
'field' => $field,
|
||||
'value' => $newValue,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user