feat(Formules.php): Ajoute relation OneToOne avec FormulesRestriction.
 feat(Dashboard/FormulesController.php): Gère restrictions formules et formulaire.
🎨 refactor(template/formules): Améliore interface configuration restriction formule.
🐛 fix(assets/RepeatLine.js): Corrige réinitialisation TomSelect et selects "Type".
 feat(assets/initTomSelect.js): Gère cache options et init TomSelect.
```
This commit is contained in:
Serreau Jovann
2026-01-28 15:59:49 +01:00
parent 17cf110cf0
commit 034210d91d
11 changed files with 491 additions and 94 deletions

View File

@@ -42,39 +42,41 @@ export class RepeatLine extends HTMLDivElement {
addRow() {
if (!this.rowHTML || this.$refs.rows.children.length >= this.$props.maxRows) return;
// Création de la nouvelle ligne
let newRow = this.createFromHTML(this.rowHTML);
newRow.removeAttribute('id');
// Nettoyage spécifique pour TomSelect avant insertion
// Si on clone une ligne qui avait déjà TomSelect, on reset le select
newRow.querySelectorAll('select').forEach(select => {
// Supprimer les classes et éléments injectés par TomSelect si présents dans le template
select.classList.remove('tomselect', 'ts-hidden-visually');
select.innerHTML = '<option value="">Sélectionner...</option>';
// Supprimer le wrapper TomSelect s'il a été cloné par erreur
const wrapper = select.nextElementSibling;
if (wrapper && wrapper.classList.contains('ts-wrapper')) {
wrapper.remove();
// 1. Si c'est un select TomSelect (sans attribut 'is')
if (!select.hasAttribute('ds')) {
select.classList.remove('tomselect', 'ts-hidden-visually');
select.innerHTML = '<option value="">Sélectionner...</option>';
const wrapper = select.nextElementSibling;
if (wrapper && wrapper.classList.contains('ts-wrapper')) {
wrapper.remove();
}
}
// 2. Si c'est votre select "Type" (avec l'attribut 'is')
else {
// On ne touche PAS au innerHTML pour garder les options (Structure, etc.)
select.value = ""; // On remet juste à zéro la sélection
}
});
this.setUpRow(newRow);
this.$refs.rows.appendChild(newRow);
// Réinitialisation des valeurs
newRow.querySelectorAll('input,textarea,select').forEach(el => {
// Réinitialisation des autres champs
newRow.querySelectorAll('input,textarea').forEach(el => {
el.value = "";
if (el.tagName === 'SELECT') el.selectedIndex = 0;
});
// --- INITIALISATION TOMSELECT SUR LA NOUVELLE LIGNE ---
// Initialisation TomSelect uniquement sur ce qui doit l'être
initTomSelect(newRow);
this.updateFieldNames();
this.updateAddButton();
// Focus sur le premier élément de la nouvelle ligne
const firstInput = newRow.querySelector('input,textarea,select');
if (firstInput) firstInput.focus();
}

View File

@@ -1,14 +1,17 @@
// Cache pour éviter les requêtes HTTP répétitives
import TomSelect from "tom-select";
// Cache séparé pour éviter les conflits entre produits et options
let productCache = null;
let optionsCache = null;
/**
* Initialise TomSelect sur un élément ou un groupe d'éléments
*/
export function initTomSelect(parent = document) {
parent.querySelectorAll('select').forEach((el) => {
if (el.tomselect) return;
// --- CLAUSES DE GARDE ---
// On ignore si déjà initialisé OU si l'élément possède l'attribut "is"
if (el.tomselect || el.hasAttribute('ds')) return;
// --- CONFIGURATION PRODUITS ---
if (el.getAttribute('data-load') === "product") {
@@ -19,34 +22,23 @@ export function initTomSelect(parent = document) {
searchField: 'name',
options: data,
maxOptions: null,
// LORSQU'ON SÉLECTIONNE UN PRODUIT
// Dans admin.js, section onChange de TomSelect :
onChange: (id) => {
if (!id) return;
// On s'assure de trouver le produit (id peut être string ou int)
const product = data.find(p => String(p.id) === String(id));
if (product) {
// On remonte au parent le plus proche (le bloc de ligne du devis)
const row = el.closest('.form-repeater__row') || el.closest('fieldset');
if (!row) return;
// Ciblage précis des inputs
const priceInput = row.querySelector('input[name*="[price_ht]"]');
const priceSupInput = row.querySelector('input[name*="[price_sup_ht]"]');
if (priceInput) {
priceInput.value = product.price1day;
// Indispensable pour que d'autres scripts (calcul totaux) voient le changement
priceInput.dispatchEvent(new Event('input', { bubbles: true }));
priceInput.dispatchEvent(new Event('change', { bubbles: true }));
}
if (priceSupInput) {
priceSupInput.value = product.priceSup;
priceSupInput.dispatchEvent(new Event('input', { bubbles: true }));
priceSupInput.dispatchEvent(new Event('change', { bubbles: true }));
}
}
},
@@ -59,16 +51,11 @@ export function initTomSelect(parent = document) {
<div class="text-[10px] text-slate-400">J1: ${escape(data.price1day)}€ | Sup: ${escape(data.priceSup)}€</div>
</div>
</div>`,
item: (data, escape) => `
<div class="text-blue-400 font-bold flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
${escape(data.name)}
</div>`
item: (data, escape) => `<div class="text-blue-400 font-bold flex items-center gap-2"><span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>${escape(data.name)}</div>`
}
});
};
// Utilisation du cache ou fetch
if (productCache) {
setupSelect(productCache);
} else {
@@ -80,6 +67,7 @@ export function initTomSelect(parent = document) {
});
}
}
// --- CONFIGURATION OPTIONS ---
else if (el.getAttribute('data-load') === "options") {
const setupSelect = (data) => {
new TomSelect(el, {
@@ -88,12 +76,8 @@ export function initTomSelect(parent = document) {
searchField: 'name',
options: data,
maxOptions: null,
// LORSQU'ON SÉLECTIONNE UN PRODUIT
// Dans admin.js, section onChange de TomSelect :
onChange: (id) => {
if (!id) return;
// On s'assure de trouver le produit (id peut être string ou int)
const product = data.find(p => String(p.id) === String(id));
const row = el.closest('.form-repeater__row') || el.closest('fieldset');
if (!row) return;
@@ -101,11 +85,8 @@ export function initTomSelect(parent = document) {
if (priceInput) {
priceInput.value = product.price;
// Indispensable pour que d'autres scripts (calcul totaux) voient le changement
priceInput.dispatchEvent(new Event('input', { bubbles: true }));
priceInput.dispatchEvent(new Event('change', { bubbles: true }));
}
},
render: {
option: (data, escape) => `
@@ -116,28 +97,23 @@ export function initTomSelect(parent = document) {
<div class="text-[10px] text-slate-400">${escape(data.price)}€</div>
</div>
</div>`,
item: (data, escape) => `
<div class="text-blue-400 font-bold flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
${escape(data.name)}
</div>`
item: (data, escape) => `<div class="text-blue-400 font-bold flex items-center gap-2"><span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>${escape(data.name)}</div>`
}
});
};
// Utilisation du cache ou fetch
if (productCache) {
setupSelect(productCache);
if (optionsCache) {
setupSelect(optionsCache);
} else {
fetch("/crm/options/json")
.then(r => r.json())
.then(data => {
productCache = data;
optionsCache = data;
setupSelect(data);
});
}
}
// --- AUTRES SELECTS ---
// --- AUTRES SELECTS STANDARDS ---
else {
new TomSelect(el, {
controlInput: null,