✨ feat(crm): Améliore l'interface et la recherche de produits/options
Ce commit modernise l'interface utilisateur pour la recherche et la sélection de produits et d'options. Il améliore l'apparence
visuelle, l'ergonomie et la réactivité, en utilisant des composants plus modernes et des animations plus fluides. Les
fonctionnalités de recherche ont été optimisées pour une meilleure expérience utilisateur. Ajout de nouvelles classes
'SearchProductDevis' et 'SearchOptionsDevis' pour la gestion des options dans Devis.
```
304 lines
15 KiB
JavaScript
304 lines
15 KiB
JavaScript
/**
|
|
* RECHERCHE OPTIONS DEVIS
|
|
*/
|
|
export class SearchOptionsDevis extends HTMLButtonElement {
|
|
constructor() {
|
|
super();
|
|
this.allOptions = [];
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.openModal();
|
|
});
|
|
}
|
|
|
|
async openModal() {
|
|
let modal = document.getElementById('modal-search-options');
|
|
if (!modal) {
|
|
modal = this.createModalStructure();
|
|
document.body.appendChild(modal);
|
|
this.setupSearchEvent(modal);
|
|
}
|
|
|
|
modal.classList.remove('hidden');
|
|
const container = modal.querySelector('#results-container-options');
|
|
const searchInput = modal.querySelector('#modal-search-input-options');
|
|
|
|
searchInput.value = '';
|
|
container.innerHTML = `
|
|
<div class="flex flex-col items-center justify-center p-12 text-slate-500">
|
|
<div class="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mb-4"></div>
|
|
<p class="text-[10px] font-black uppercase tracking-[0.2em]">Chargement des options...</p>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const response = await fetch('/crm/options/json');
|
|
this.allOptions = await response.json();
|
|
this.renderOptions(this.allOptions, container, modal);
|
|
setTimeout(() => searchInput.focus(), 100);
|
|
} catch (error) {
|
|
container.innerHTML = '<div class="text-rose-500 p-8 text-center text-xs font-bold uppercase">Erreur de liaison catalogue options.</div>';
|
|
}
|
|
}
|
|
|
|
createModalStructure() {
|
|
const div = document.createElement('div');
|
|
div.id = 'modal-search-options';
|
|
div.className = 'fixed inset-0 z-[100] flex items-center justify-center p-4 bg-[#0f172a]/90 backdrop-blur-md hidden animate-in fade-in duration-300';
|
|
div.innerHTML = `
|
|
<div class="bg-[#1e293b] border border-white/10 w-full max-w-2xl max-h-[85vh] rounded-[2.5rem] shadow-2xl overflow-hidden flex flex-col scale-in-center">
|
|
<div class="p-8 border-b border-white/5 bg-white/5 space-y-6">
|
|
<div class="flex justify-between items-center">
|
|
<h3 class="text-white font-black uppercase tracking-[0.3em] text-[11px] flex items-center">
|
|
<span class="w-2 h-2 bg-blue-500 rounded-full mr-3 animate-pulse shadow-[0_0_8px_#3b82f6]"></span>
|
|
Sélection Option
|
|
</h3>
|
|
<button type="button" onclick="this.closest('#modal-search-options').classList.add('hidden')" class="p-2 text-slate-400 hover:text-white transition-colors">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="relative">
|
|
<input type="text" id="modal-search-input-options" placeholder="RECHERCHER DANS LE CATALOGUE..."
|
|
class="w-full bg-[#0f172a] border border-white/10 rounded-2xl py-4 pl-12 pr-5 text-white text-xs font-black tracking-widest focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all uppercase placeholder:text-slate-700 outline-none">
|
|
<svg class="w-5 h-5 absolute left-4 top-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="results-container-options" class="overflow-y-auto p-6 space-y-2 custom-scrollbar flex-grow min-h-[300px]"></div>
|
|
|
|
<div class="p-4 bg-white/5 text-center border-t border-white/5">
|
|
<p class="text-[9px] text-slate-500 font-black uppercase tracking-widest">Appuyez sur Échap pour annuler</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
return div;
|
|
}
|
|
|
|
setupSearchEvent(modal) {
|
|
const input = modal.querySelector('#modal-search-input-options');
|
|
const container = modal.querySelector('#results-container-options');
|
|
|
|
input.oninput = () => {
|
|
const query = input.value.toLowerCase().trim();
|
|
const filtered = this.allOptions.filter(o => o.name.toLowerCase().includes(query));
|
|
this.renderOptions(filtered, container, modal);
|
|
};
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') modal.classList.add('hidden');
|
|
});
|
|
}
|
|
|
|
renderOptions(options, container, modal) {
|
|
container.innerHTML = '';
|
|
if (options.length === 0) {
|
|
container.innerHTML = '<div class="text-slate-600 p-12 text-center text-[10px] font-black uppercase tracking-[0.2em]">Aucune option trouvée</div>';
|
|
return;
|
|
}
|
|
|
|
options.forEach(option => {
|
|
const card = document.createElement('div');
|
|
card.className = 'flex items-center gap-5 p-4 bg-white/5 border border-white/5 rounded-2xl hover:bg-blue-600/10 hover:border-blue-500/40 transition-all cursor-pointer group animate-in slide-in-from-bottom-2';
|
|
|
|
const imgHtml = option.image
|
|
? `<img src="${option.image}" class="w-14 h-14 rounded-xl object-cover shadow-lg border border-white/10 group-hover:scale-105 transition-transform" onerror="this.src='/provider/images/favicon.png'">`
|
|
: `<div class="w-14 h-14 bg-slate-950 rounded-xl flex items-center justify-center text-[8px] text-slate-700 font-black border border-white/5">OPT</div>`;
|
|
|
|
card.innerHTML = `
|
|
${imgHtml}
|
|
<div class="flex-grow">
|
|
<div class="text-white font-black text-xs uppercase tracking-wider mb-1">${option.name}</div>
|
|
<div class="flex gap-4">
|
|
<span class="text-blue-500 text-[10px] font-black uppercase tracking-tighter">PRIX HT: ${option.price}€</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-10 h-10 rounded-xl bg-slate-800 group-hover:bg-blue-600 flex items-center justify-center text-slate-500 group-hover:text-white transition-all shadow-lg">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
|
</div>
|
|
`;
|
|
|
|
card.onclick = () => {
|
|
this.fillOptionLine(option);
|
|
modal.classList.add('hidden');
|
|
};
|
|
container.appendChild(card);
|
|
});
|
|
}
|
|
|
|
fillOptionLine(option) {
|
|
const row = this.closest('.form-repeater__row');
|
|
if (row) {
|
|
const nameInput = row.querySelector('input[name*="[product]"]');
|
|
const priceInput = row.querySelector('input[name*="[price_ht]"]');
|
|
if(nameInput) nameInput.value = option.name;
|
|
if(priceInput) priceInput.value = option.price;
|
|
|
|
const fieldset = row.querySelector('fieldset');
|
|
if (fieldset) {
|
|
fieldset.classList.add('border-blue-500/50', 'bg-blue-600/5');
|
|
setTimeout(() => fieldset.classList.remove('border-blue-500/50', 'bg-blue-600/5'), 800);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* RECHERCHE PRODUITS DEVIS
|
|
*/
|
|
export class SearchProductDevis extends HTMLButtonElement {
|
|
constructor() {
|
|
super();
|
|
this.allProducts = [];
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.openModal();
|
|
});
|
|
}
|
|
|
|
async openModal() {
|
|
let modal = document.getElementById('modal-search-product');
|
|
if (!modal) {
|
|
modal = this.createModalStructure();
|
|
document.body.appendChild(modal);
|
|
this.setupSearchEvent(modal);
|
|
}
|
|
|
|
modal.classList.remove('hidden');
|
|
const container = modal.querySelector('#results-container');
|
|
const searchInput = modal.querySelector('#modal-search-input');
|
|
|
|
searchInput.value = '';
|
|
container.innerHTML = `
|
|
<div class="flex flex-col items-center justify-center p-12 text-slate-500">
|
|
<div class="w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full animate-spin mb-4"></div>
|
|
<p class="text-[10px] font-black uppercase tracking-[0.2em]">Synchronisation catalogue...</p>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const response = await fetch('/crm/products/json');
|
|
this.allProducts = await response.json();
|
|
this.renderProducts(this.allProducts, container, modal);
|
|
setTimeout(() => searchInput.focus(), 100);
|
|
} catch (error) {
|
|
container.innerHTML = '<div class="text-rose-500 p-8 text-center text-xs font-bold uppercase">Erreur de liaison catalogue produits.</div>';
|
|
}
|
|
}
|
|
|
|
createModalStructure() {
|
|
const div = document.createElement('div');
|
|
div.id = 'modal-search-product';
|
|
div.className = 'fixed inset-0 z-[100] flex items-center justify-center p-4 bg-[#0f172a]/90 backdrop-blur-md hidden animate-in fade-in duration-300';
|
|
div.innerHTML = `
|
|
<div class="bg-[#1e293b] border border-white/10 w-full max-w-2xl max-h-[85vh] rounded-[2.5rem] shadow-2xl overflow-hidden flex flex-col scale-in-center">
|
|
<div class="p-8 border-b border-white/5 bg-white/5 space-y-6">
|
|
<div class="flex justify-between items-center">
|
|
<h3 class="text-white font-black uppercase tracking-[0.3em] text-[11px] flex items-center">
|
|
<span class="w-2 h-2 bg-purple-500 rounded-full mr-3 animate-pulse shadow-[0_0_8px_#a855f7]"></span>
|
|
Sélection Produit
|
|
</h3>
|
|
<button type="button" onclick="this.closest('#modal-search-product').classList.add('hidden')" class="p-2 text-slate-400 hover:text-white transition-colors">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="relative">
|
|
<input type="text" id="modal-search-input" placeholder="NOM OU RÉFÉRENCE PRODUIT..."
|
|
class="w-full bg-[#0f172a] border border-white/10 rounded-2xl py-4 pl-12 pr-5 text-white text-xs font-black tracking-widest focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-all uppercase placeholder:text-slate-700 outline-none">
|
|
<svg class="w-5 h-5 absolute left-4 top-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="results-container" class="overflow-y-auto p-6 space-y-2 custom-scrollbar flex-grow min-h-[300px]"></div>
|
|
|
|
<div class="p-4 bg-white/5 text-center border-t border-white/5">
|
|
<p class="text-[9px] text-slate-500 font-black uppercase tracking-widest">Appuyez sur Échap pour annuler</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
return div;
|
|
}
|
|
|
|
setupSearchEvent(modal) {
|
|
const input = modal.querySelector('#modal-search-input');
|
|
const container = modal.querySelector('#results-container');
|
|
|
|
input.oninput = () => {
|
|
const query = input.value.toLowerCase().trim();
|
|
const filtered = this.allProducts.filter(p => p.name.toLowerCase().includes(query));
|
|
this.renderProducts(filtered, container, modal);
|
|
};
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') modal.classList.add('hidden');
|
|
});
|
|
}
|
|
|
|
renderProducts(products, container, modal) {
|
|
container.innerHTML = '';
|
|
if (products.length === 0) {
|
|
container.innerHTML = '<div class="text-slate-600 p-12 text-center text-[10px] font-black uppercase tracking-[0.2em]">Aucun produit trouvé</div>';
|
|
return;
|
|
}
|
|
|
|
products.forEach(product => {
|
|
const card = document.createElement('div');
|
|
card.className = 'flex items-center gap-5 p-4 bg-white/5 border border-white/5 rounded-2xl hover:bg-purple-600/10 hover:border-purple-500/40 transition-all cursor-pointer group animate-in slide-in-from-bottom-2';
|
|
|
|
const imgHtml = product.image
|
|
? `<img src="${product.image}" class="w-14 h-14 rounded-xl object-cover shadow-lg border border-white/10 group-hover:scale-105 transition-transform">`
|
|
: `<div class="w-14 h-14 bg-slate-950 rounded-xl flex items-center justify-center text-[8px] text-slate-700 font-black border border-white/5">IMG</div>`;
|
|
|
|
card.innerHTML = `
|
|
${imgHtml}
|
|
<div class="flex-grow">
|
|
<div class="text-white font-black text-xs uppercase tracking-wider mb-1">${product.name}</div>
|
|
<div class="flex gap-4">
|
|
<span class="text-purple-400 text-[10px] font-black uppercase tracking-tighter">1J: ${product.price1day}€</span>
|
|
<span class="text-slate-500 text-[10px] font-black uppercase tracking-tighter">SUP: ${product.priceSup}€</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-10 h-10 rounded-xl bg-slate-800 group-hover:bg-purple-600 flex items-center justify-center text-slate-500 group-hover:text-white transition-all shadow-lg">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
|
</div>
|
|
`;
|
|
|
|
card.onclick = () => {
|
|
this.fillFormLine(product);
|
|
modal.classList.add('hidden');
|
|
};
|
|
container.appendChild(card);
|
|
});
|
|
}
|
|
|
|
fillFormLine(product) {
|
|
const row = this.closest('.form-repeater__row');
|
|
if (row) {
|
|
const nameInput = row.querySelector('input[name*="[product]"]');
|
|
const priceInput = row.querySelector('input[name*="[price_ht]"]');
|
|
const priceSupInput = row.querySelector('input[name*="[price_sup_ht]"]');
|
|
|
|
if(nameInput) nameInput.value = product.name;
|
|
if(priceInput) priceInput.value = product.price1day;
|
|
if(priceSupInput) priceSupInput.value = product.priceSup;
|
|
|
|
const fieldset = row.querySelector('fieldset');
|
|
if (fieldset) {
|
|
fieldset.classList.add('border-purple-500/50', 'bg-purple-600/5');
|
|
setTimeout(() => fieldset.classList.remove('border-purple-500/50', 'bg-purple-600/5'), 800);
|
|
}
|
|
}
|
|
}
|
|
}
|