Link sorties to the entrée client they pay out against
New field_entree_liee (entity reference, node -> node) on
ligne_comptable, restricted to entrée-type nodes via a custom
EntreeClientSelection plugin -- narrows further to the same client as
the sortie being linked when one is already known, using the
referencing-entity context Drupal's selection handler API passes
through (getSelectionHandler($field, $entity)).
Quick-link UI (per the associates' explicit ask: no need to open the
full ligne_comptable form just for this):
- A chain-link icon next to the edit pencil on versement/achat/
hébergement rows (the only types that pay out against a client
invoice) opens LinkEntreeForm, a one-field AJAX modal, reusing the
same modal/close-on-save plumbing as the edit form. Filled/colored
when already linked, with the linked entrée's label on hover.
Also present (states-hidden unless one of those three types is
selected) on the full node form for whoever's already there anyway.
- Entrée rows get a reconciliation badge once at least one sortie
links back to them, clickable to drill the table down to just that
entrée and its linked sorties.
Conformity check assumes multi-compte répartition on both sides (an
entrée's répartition and each linked sortie's répartition can each
split across several comptes -- confirmed this is the real shape of
"hébergement" sorties, e.g. OVH/HETZNER renewals split across all 8
comptes, even though versement/achat lines happen to always be
single-compte in the current data). Per compte, compares the entrée's
répartition share (positive, owed) against the summed répartition of
every linked sortie (negative, paid) -- a residual near zero means
settled, positive means still owed ("reste à verser"), negative means
overpaid ("sur-versé", flagged for a closer look).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ dependencies:
|
||||
config:
|
||||
- field.field.node.ligne_comptable.field_client
|
||||
- field.field.node.ligne_comptable.field_date_ligne
|
||||
- field.field.node.ligne_comptable.field_entree_liee
|
||||
- field.field.node.ligne_comptable.field_montant_ht
|
||||
- field.field.node.ligne_comptable.field_montant_ttc
|
||||
- field.field.node.ligne_comptable.field_notes
|
||||
@@ -42,6 +43,16 @@ content:
|
||||
region: content
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
field_entree_liee:
|
||||
type: entity_reference_autocomplete
|
||||
weight: 2
|
||||
region: content
|
||||
settings:
|
||||
match_operator: CONTAINS
|
||||
match_limit: 10
|
||||
size: 60
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
field_montant_ht:
|
||||
type: number
|
||||
weight: 3
|
||||
|
||||
@@ -5,6 +5,7 @@ dependencies:
|
||||
config:
|
||||
- field.field.node.ligne_comptable.field_client
|
||||
- field.field.node.ligne_comptable.field_date_ligne
|
||||
- field.field.node.ligne_comptable.field_entree_liee
|
||||
- field.field.node.ligne_comptable.field_montant_ht
|
||||
- field.field.node.ligne_comptable.field_montant_ttc
|
||||
- field.field.node.ligne_comptable.field_notes
|
||||
@@ -38,6 +39,14 @@ content:
|
||||
third_party_settings: { }
|
||||
weight: 0
|
||||
region: content
|
||||
field_entree_liee:
|
||||
type: entity_reference_label
|
||||
label: above
|
||||
settings:
|
||||
link: true
|
||||
third_party_settings: { }
|
||||
weight: 2
|
||||
region: content
|
||||
field_montant_ht:
|
||||
type: number_decimal
|
||||
label: above
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
uuid: c2d68287-8e24-4529-a035-39395acca4d3
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_entree_liee
|
||||
- node.type.ligne_comptable
|
||||
id: node.ligne_comptable.field_entree_liee
|
||||
field_name: field_entree_liee
|
||||
entity_type: node
|
||||
bundle: ligne_comptable
|
||||
label: 'Entrée client liée'
|
||||
description: ''
|
||||
required: false
|
||||
translatable: true
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
handler: 'figli_compta_ledger:entree_client'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
ligne_comptable: ligne_comptable
|
||||
field_type: entity_reference
|
||||
@@ -0,0 +1,19 @@
|
||||
uuid: d7148451-7a96-4515-b0b5-42ad9fecbdce
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
id: node.field_entree_liee
|
||||
field_name: field_entree_liee
|
||||
entity_type: node
|
||||
type: entity_reference
|
||||
settings:
|
||||
target_type: node
|
||||
module: core
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
@@ -0,0 +1,5 @@
|
||||
# EntreeClientSelection only adds query conditions on top of NodeSelection
|
||||
# (itself a DefaultSelection subclass) -- no new handler_settings keys, so
|
||||
# it uses the same schema shape as the core 'default' handler.
|
||||
entity_reference_selection.figli_compta_ledger:
|
||||
type: entity_reference_selection.default
|
||||
@@ -204,7 +204,14 @@ html.gin--dark-mode #figli-home-app {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#figli-home-app .figli-edit-btn {
|
||||
#figli-home-app td.actions-col {
|
||||
display: flex;
|
||||
gap: 0.15rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#figli-home-app .figli-edit-btn,
|
||||
#figli-home-app .figli-link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -218,12 +225,45 @@ html.gin--dark-mode #figli-home-app {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#figli-home-app .figli-edit-btn:hover {
|
||||
#figli-home-app .figli-edit-btn:hover,
|
||||
#figli-home-app .figli-link-btn:hover {
|
||||
background: var(--figli-bg-alt);
|
||||
border-color: var(--figli-border);
|
||||
color: var(--figli-text);
|
||||
}
|
||||
|
||||
#figli-home-app .figli-link-btn.is-linked {
|
||||
color: #3b6fe0;
|
||||
}
|
||||
|
||||
/* Reconciliation badge on entrée rows: répartition (money owed) vs the
|
||||
combined répartition of every linked sortie (money actually paid out). */
|
||||
#figli-home-app .figli-recon-badge {
|
||||
display: inline-block;
|
||||
margin-left: 0.4rem;
|
||||
padding: 0.05rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
background: #1a7f371a;
|
||||
color: var(--figli-positive);
|
||||
}
|
||||
#figli-home-app .figli-recon-badge.is-reste {
|
||||
background: #d97a0a1a;
|
||||
color: #d97a0a;
|
||||
}
|
||||
#figli-home-app .figli-recon-badge.is-anomalie {
|
||||
background: #c9312b1a;
|
||||
color: var(--figli-error);
|
||||
}
|
||||
|
||||
#figli-home-app .figli-clear-drilldown {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
}
|
||||
|
||||
#figli-home-app .figli-error {
|
||||
background: #fde8e8;
|
||||
border: 1px solid #f4a3a3;
|
||||
|
||||
@@ -51,6 +51,21 @@ function figli_compta_ledger_form_alter(&$form, FormStateInterface $form_state,
|
||||
$form['revision_log']['#access'] = FALSE;
|
||||
}
|
||||
|
||||
// field_entree_liee only makes sense on the "sortie" types that pay out
|
||||
// against a client invoice -- charge/autre/ouverture aren't tied to a
|
||||
// specific entrée, and entree lines don't link to themselves.
|
||||
if (isset($form['field_entree_liee'])) {
|
||||
$form['field_entree_liee']['#states'] = [
|
||||
'visible' => [
|
||||
[':input[name="field_type_ligne"]' => ['value' => 'versement']],
|
||||
'or',
|
||||
[':input[name="field_type_ligne"]' => ['value' => 'achat']],
|
||||
'or',
|
||||
[':input[name="field_type_ligne"]' => ['value' => 'hebergement']],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$request = \Drupal::request();
|
||||
$wrapper_formats = ['drupal_ajax', 'drupal_modal', 'drupal_dialog'];
|
||||
$is_ajax_modal = in_array($request->query->get('_wrapper_format'), $wrapper_formats, TRUE)
|
||||
|
||||
@@ -21,3 +21,15 @@ figli_compta_ledger.history:
|
||||
_title: 'Historique du grand livre - SAS Figures Libres'
|
||||
requirements:
|
||||
_permission: 'view ligne_comptable revisions'
|
||||
|
||||
figli_compta_ledger.link_entree:
|
||||
path: '/lignes/{node}/lier'
|
||||
defaults:
|
||||
_form: '\Drupal\figli_compta_ledger\Form\LinkEntreeForm'
|
||||
_title: 'Lier à une entrée client'
|
||||
requirements:
|
||||
_entity_access: 'node.update'
|
||||
options:
|
||||
parameters:
|
||||
node:
|
||||
type: entity:node
|
||||
|
||||
@@ -21,13 +21,16 @@
|
||||
autre: 'Autre',
|
||||
ouverture: 'Ouverture',
|
||||
};
|
||||
// Sorties that can be linked to the entrée client they pay out against
|
||||
// (field_entree_liee) -- charge/autre/ouverture aren't client-specific.
|
||||
const LINKABLE_TYPES = ['versement', 'achat', 'hebergement'];
|
||||
|
||||
async function fetchAllLignes() {
|
||||
// sort includes drupal_internal__nid as a tie-breaker: field_date_ligne
|
||||
// alone is not unique (many lines share a date), and without a unique
|
||||
// secondary sort key, offset pagination can silently duplicate or skip
|
||||
// rows across pages.
|
||||
let url = API_BASE + '?include=field_repartition,field_repartition.field_compte,field_client&page[limit]=50&sort=field_date_ligne,drupal_internal__nid';
|
||||
let url = API_BASE + '?include=field_repartition,field_repartition.field_compte,field_client,field_entree_liee&page[limit]=50&sort=field_date_ligne,drupal_internal__nid';
|
||||
const allData = [];
|
||||
const includedMap = new Map();
|
||||
while (url) {
|
||||
@@ -55,6 +58,7 @@
|
||||
const rels = node.relationships || {};
|
||||
const attrs = node.attributes;
|
||||
const clientTerm = resolve(includedMap, rels.field_client && rels.field_client.data);
|
||||
const entreeLieeNode = resolve(includedMap, rels.field_entree_liee && rels.field_entree_liee.data);
|
||||
const parCompte = {};
|
||||
let somme = 0;
|
||||
const repartitionRefs = (rels.field_repartition && rels.field_repartition.data) || [];
|
||||
@@ -82,6 +86,9 @@
|
||||
somme,
|
||||
ecart,
|
||||
hasError: Math.abs(ecart) > 0.01,
|
||||
linkable: LINKABLE_TYPES.includes(attrs.field_type_ligne),
|
||||
entreeLieeId: entreeLieeNode ? entreeLieeNode.id : null,
|
||||
entreeLieeLabel: entreeLieeNode ? (entreeLieeNode.attributes.title || null) : null,
|
||||
});
|
||||
}
|
||||
rows.sort((a, b) => (a.date || '').localeCompare(b.date || ''));
|
||||
@@ -102,6 +109,7 @@
|
||||
groupBy: 'month',
|
||||
onlyErrors: false,
|
||||
hoverCol: null,
|
||||
filterEntreeId: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -117,6 +125,53 @@
|
||||
errorCount() {
|
||||
return this.rows.filter((r) => r.hasError).length;
|
||||
},
|
||||
sortiesByEntree() {
|
||||
const map = new Map();
|
||||
for (const r of this.rows) {
|
||||
if (!r.entreeLieeId) continue;
|
||||
if (!map.has(r.entreeLieeId)) map.set(r.entreeLieeId, []);
|
||||
map.get(r.entreeLieeId).push(r);
|
||||
}
|
||||
return map;
|
||||
},
|
||||
// Compares each entrée's répartition (positive shares owed) against
|
||||
// the combined répartition of every sortie linked to it (negative
|
||||
// amounts paid out), per compte -- both sides can be split across
|
||||
// several comptes. A residual near zero means fully settled;
|
||||
// positive means still owed ("reste à verser"); negative means more
|
||||
// was paid out than the entrée allocated ("sur-versé", worth a
|
||||
// closer look). Precomputed once for all entrée rows rather than
|
||||
// recomputed per template read.
|
||||
reconciliationByEntree() {
|
||||
const map = new Map();
|
||||
for (const entreeRow of this.rows) {
|
||||
if (entreeRow.type !== 'entree') continue;
|
||||
const linked = this.sortiesByEntree.get(entreeRow.id) || [];
|
||||
const versementsParCompte = {};
|
||||
for (const s of linked) {
|
||||
for (const [compte, montant] of Object.entries(s.parCompte)) {
|
||||
versementsParCompte[compte] = (versementsParCompte[compte] || 0) + montant;
|
||||
}
|
||||
}
|
||||
const comptes = new Set([...Object.keys(entreeRow.parCompte), ...Object.keys(versementsParCompte)]);
|
||||
let resteAVerser = 0;
|
||||
let surVerse = 0;
|
||||
const detail = [];
|
||||
for (const c of comptes) {
|
||||
const residual = Math.round(((entreeRow.parCompte[c] || 0) + (versementsParCompte[c] || 0)) * 100) / 100;
|
||||
if (residual > 0.01) resteAVerser += residual;
|
||||
else if (residual < -0.01) surVerse += -residual;
|
||||
if (Math.abs(residual) > 0.01) detail.push(c + ' : ' + this.formatEur(residual));
|
||||
}
|
||||
map.set(entreeRow.id, {
|
||||
count: linked.length,
|
||||
resteAVerser: Math.round(resteAVerser * 100) / 100,
|
||||
surVerse: Math.round(surVerse * 100) / 100,
|
||||
detail: detail.join(', ') || 'Entièrement soldé',
|
||||
});
|
||||
}
|
||||
return map;
|
||||
},
|
||||
footerTotals() {
|
||||
const parCompte = {};
|
||||
this.allComptes.forEach((c) => { parCompte[c] = 0; });
|
||||
@@ -132,6 +187,13 @@
|
||||
return { montant_ht: montantHt, montant_ttc: montantTtc, parCompte, ecart };
|
||||
},
|
||||
filteredRows() {
|
||||
// Drill-down mode: an entrée and only the sorties linked to it,
|
||||
// ignoring the other filters -- clicking its badge again clears it.
|
||||
if (this.filterEntreeId) {
|
||||
const entree = this.rows.find((r) => r.id === this.filterEntreeId);
|
||||
const linked = this.sortiesByEntree.get(this.filterEntreeId) || [];
|
||||
return entree ? [entree, ...linked] : linked;
|
||||
}
|
||||
return this.rows.filter((r) => {
|
||||
if (this.filterCompte && r.parCompte[this.filterCompte] === undefined) return false;
|
||||
if (this.filterClient && r.client !== this.filterClient) return false;
|
||||
@@ -196,6 +258,17 @@
|
||||
progress: { type: 'throbber' },
|
||||
}).execute();
|
||||
},
|
||||
openLinkForm(nid) {
|
||||
Drupal.ajax({
|
||||
url: '/lignes/' + nid + '/lier',
|
||||
dialogType: 'modal',
|
||||
dialog: { width: 500, title: 'Lier à une entrée client' },
|
||||
progress: { type: 'throbber' },
|
||||
}).execute();
|
||||
},
|
||||
toggleEntreeFilter(id) {
|
||||
this.filterEntreeId = this.filterEntreeId === id ? null : id;
|
||||
},
|
||||
onCellHover(evt) {
|
||||
const cell = evt.target.closest('td, th');
|
||||
if (!cell) return;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\figli_compta_ledger\Form;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\CloseModalDialogCommand;
|
||||
use Drupal\Core\Ajax\ReplaceCommand;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\node\NodeInterface;
|
||||
|
||||
/**
|
||||
* Quick-link form: sets field_entree_liee on a single sortie line without
|
||||
* opening the full ligne_comptable edit form -- the associates only ever
|
||||
* need to touch this one field to link a versement/achat/hébergement to
|
||||
* the entrée client it pays out against.
|
||||
*/
|
||||
class LinkEntreeForm extends FormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'figli_compta_ledger_link_entree_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, ?NodeInterface $node = NULL) {
|
||||
$form_state->set('node', $node);
|
||||
|
||||
$form['field_entree_liee'] = [
|
||||
'#type' => 'entity_autocomplete',
|
||||
'#title' => $this->t('Entrée client liée'),
|
||||
'#target_type' => 'node',
|
||||
'#selection_handler' => 'figli_compta_ledger:entree_client',
|
||||
'#selection_settings' => [
|
||||
'target_bundles' => ['ligne_comptable' => 'ligne_comptable'],
|
||||
// Narrows the autocomplete to the same client as this sortie --
|
||||
// read by EntreeClientSelection::buildEntityQuery().
|
||||
'entity' => $node,
|
||||
],
|
||||
'#default_value' => $node->get('field_entree_liee')->entity,
|
||||
'#description' => $this->t('Laisser vide pour retirer le lien.'),
|
||||
];
|
||||
|
||||
$form['actions'] = ['#type' => 'actions'];
|
||||
$form['actions']['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Enregistrer'),
|
||||
'#ajax' => [
|
||||
'callback' => '::ajaxSubmit',
|
||||
],
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
/** @var \Drupal\node\NodeInterface $node */
|
||||
$node = $form_state->get('node');
|
||||
$node->set('field_entree_liee', $form_state->getValue('field_entree_liee') ?: NULL);
|
||||
$node->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* #ajax callback: close the modal on success (the /lignes table picks up
|
||||
* "dialog:afterclose" to refresh), or re-render the form in place.
|
||||
*/
|
||||
public function ajaxSubmit(array &$form, FormStateInterface $form_state) {
|
||||
$response = new AjaxResponse();
|
||||
if ($form_state->getErrors()) {
|
||||
unset($form['#prefix'], $form['#suffix']);
|
||||
$response->addCommand(new ReplaceCommand('#' . $form['#id'], $form));
|
||||
return $response;
|
||||
}
|
||||
$response->addCommand(new CloseModalDialogCommand());
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\figli_compta_ledger\Plugin\EntityReferenceSelection;
|
||||
|
||||
use Drupal\Core\Entity\Attribute\EntityReferenceSelection;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
|
||||
|
||||
/**
|
||||
* Restricts field_entree_liee's candidates to "entrée" ligne_comptable
|
||||
* nodes, scoped to the same client as the sortie being linked when one is
|
||||
* already set on it.
|
||||
*/
|
||||
#[EntityReferenceSelection(
|
||||
id: "figli_compta_ledger:entree_client",
|
||||
label: new TranslatableMarkup("Entrées client (SAS Figures Libres)"),
|
||||
entity_types: ["node"],
|
||||
group: "figli_compta_ledger",
|
||||
weight: 0,
|
||||
)]
|
||||
class EntreeClientSelection extends NodeSelection {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
|
||||
$query = parent::buildEntityQuery($match, $match_operator);
|
||||
$query->condition('field_type_ligne', 'entree');
|
||||
|
||||
$entity = $this->configuration['entity'] ?? NULL;
|
||||
if ($entity && $entity->hasField('field_client') && !$entity->get('field_client')->isEmpty()) {
|
||||
$query->condition('field_client', $entity->get('field_client')->target_id);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -55,6 +55,8 @@
|
||||
<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{{ errorCount ? ' — ' + errorCount + ' avec écart' : '' }}</span>
|
||||
</div>
|
||||
|
||||
@@ -84,12 +86,34 @@
|
||||
<td>{{ item.date }}</td>
|
||||
<td>{{ item.client || '—' }}</td>
|
||||
<td><span class="figli-badge" :class="'type-' + item.type">{{ typeLabel(item.type) }}</span></td>
|
||||
<td class="figli-libelle">{{ item.libelle }}</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>
|
||||
</td>
|
||||
<td class="amount">{{ 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>
|
||||
<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>
|
||||
<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" />
|
||||
|
||||
Reference in New Issue
Block a user