```text
✨ 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.
```
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* RECHERCHE OPTIONS DEVIS
|
||||
*/
|
||||
export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -5,7 +8,10 @@ export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', () => this.openModal());
|
||||
this.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.openModal();
|
||||
});
|
||||
}
|
||||
|
||||
async openModal() {
|
||||
@@ -21,39 +27,44 @@ export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
const searchInput = modal.querySelector('#modal-search-input-options');
|
||||
|
||||
searchInput.value = '';
|
||||
container.innerHTML = '<div class="text-white p-8 text-center animate-pulse tracking-widest text-[10px] uppercase font-black">Chargement des options...</div>';
|
||||
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);
|
||||
searchInput.focus();
|
||||
setTimeout(() => searchInput.focus(), 100);
|
||||
} catch (error) {
|
||||
container.innerHTML = '<div class="text-red-400 p-8 text-center">Erreur catalogue options.</div>';
|
||||
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-slate-950/90 backdrop-blur-md hidden animate-in fade-in duration-300';
|
||||
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-slate-900 border border-white/10 w-full max-w-2xl max-h-[85vh] rounded-[3rem] shadow-2xl overflow-hidden flex flex-col scale-in-center">
|
||||
<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"></span>
|
||||
<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" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
<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 UNE OPTION..."
|
||||
class="w-full bg-slate-950/50 border border-white/10 rounded-2xl py-4 pl-12 pr-5 text-white text-xs font-black tracking-widest focus:ring-blue-500/20 focus:border-blue-500 transition-all uppercase placeholder:text-slate-600">
|
||||
<svg class="w-5 h-5 absolute left-4 top-3.5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
@@ -61,8 +72,8 @@ export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
|
||||
<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">
|
||||
<p class="text-[9px] text-slate-500 font-black uppercase tracking-widest line-clamp-1">Appuyez sur Échap pour fermer</p>
|
||||
<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>
|
||||
`;
|
||||
@@ -75,9 +86,7 @@ export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
|
||||
input.oninput = () => {
|
||||
const query = input.value.toLowerCase().trim();
|
||||
const filtered = this.allOptions.filter(o =>
|
||||
o.name.toLowerCase().includes(query)
|
||||
);
|
||||
const filtered = this.allOptions.filter(o => o.name.toLowerCase().includes(query));
|
||||
this.renderOptions(filtered, container, modal);
|
||||
};
|
||||
|
||||
@@ -88,7 +97,6 @@ export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
|
||||
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;
|
||||
@@ -96,10 +104,10 @@ export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
|
||||
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-500/10 hover:border-blue-500/40 transition-all cursor-pointer group animate-in fade-in slide-in-from-bottom-2';
|
||||
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 group-hover:scale-110 transition-transform" onerror="this.src='/provider/images/favicon.png'">`
|
||||
? `<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 = `
|
||||
@@ -107,10 +115,10 @@ export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
<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-400 text-[10px] font-mono">PRIX HT: ${option.price}€</span>
|
||||
<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-full bg-blue-500/0 group-hover:bg-blue-500/20 flex items-center justify-center text-blue-500 transition-all">
|
||||
<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>
|
||||
`;
|
||||
@@ -124,34 +132,36 @@ export class SearchOptionsDevis extends HTMLButtonElement {
|
||||
}
|
||||
|
||||
fillOptionLine(option) {
|
||||
// On cherche la ligne parente (ajuste le sélecteur si différent de celui des produits)
|
||||
const row = this.closest('.form-repeater__row');
|
||||
if (row) {
|
||||
// Mapping selon ta structure de DevisOption
|
||||
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;
|
||||
|
||||
// Feedback visuel (Bleu pour les options)
|
||||
const fieldset = row.querySelector('fieldset');
|
||||
if (fieldset) {
|
||||
fieldset.classList.add('border-blue-500/50', 'bg-blue-500/5');
|
||||
setTimeout(() => fieldset.classList.remove('border-blue-500/50', 'bg-blue-500/5'), 800);
|
||||
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 = []; // Stockage local pour la recherche
|
||||
this.allProducts = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', () => this.openModal());
|
||||
this.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.openModal();
|
||||
});
|
||||
}
|
||||
|
||||
async openModal() {
|
||||
@@ -166,40 +176,45 @@ export class SearchProductDevis extends HTMLButtonElement {
|
||||
const container = modal.querySelector('#results-container');
|
||||
const searchInput = modal.querySelector('#modal-search-input');
|
||||
|
||||
searchInput.value = ''; // Reset recherche
|
||||
container.innerHTML = '<div class="text-white p-8 text-center animate-pulse tracking-widest text-[10px] uppercase font-black">Synchronisation catalogue...</div>';
|
||||
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);
|
||||
searchInput.focus();
|
||||
setTimeout(() => searchInput.focus(), 100);
|
||||
} catch (error) {
|
||||
container.innerHTML = '<div class="text-red-400 p-8 text-center">Erreur catalogue.</div>';
|
||||
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-slate-950/90 backdrop-blur-md hidden animate-in fade-in duration-300';
|
||||
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-slate-900 border border-white/10 w-full max-w-2xl max-h-[85vh] rounded-[3rem] shadow-2xl overflow-hidden flex flex-col scale-in-center">
|
||||
<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"></span>
|
||||
<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" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
<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="RECHERCHER UN NOM, UNE RÉFÉRENCE..."
|
||||
class="w-full bg-slate-950/50 border border-white/10 rounded-2xl py-4 pl-12 pr-5 text-white text-xs font-black tracking-widest focus:ring-purple-500/20 focus:border-purple-500 transition-all uppercase placeholder:text-slate-600">
|
||||
<svg class="w-5 h-5 absolute left-4 top-3.5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
@@ -207,8 +222,8 @@ export class SearchProductDevis extends HTMLButtonElement {
|
||||
|
||||
<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">
|
||||
<p class="text-[9px] text-slate-500 font-black uppercase tracking-widest line-clamp-1">Appuyez sur Échap pour fermer</p>
|
||||
<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>
|
||||
`;
|
||||
@@ -221,13 +236,10 @@ export class SearchProductDevis extends HTMLButtonElement {
|
||||
|
||||
input.oninput = () => {
|
||||
const query = input.value.toLowerCase().trim();
|
||||
const filtered = this.allProducts.filter(p =>
|
||||
p.name.toLowerCase().includes(query)
|
||||
);
|
||||
const filtered = this.allProducts.filter(p => p.name.toLowerCase().includes(query));
|
||||
this.renderProducts(filtered, container, modal);
|
||||
};
|
||||
|
||||
// Fermeture sur Echap
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') modal.classList.add('hidden');
|
||||
});
|
||||
@@ -235,7 +247,6 @@ export class SearchProductDevis extends HTMLButtonElement {
|
||||
|
||||
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;
|
||||
@@ -243,10 +254,10 @@ export class SearchProductDevis extends HTMLButtonElement {
|
||||
|
||||
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-500/10 hover:border-purple-500/40 transition-all cursor-pointer group animate-in fade-in slide-in-from-bottom-2';
|
||||
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 group-hover:scale-110 transition-transform">`
|
||||
? `<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 = `
|
||||
@@ -254,12 +265,11 @@ export class SearchProductDevis extends HTMLButtonElement {
|
||||
<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-mono">1J: ${product.price1day}€</span>
|
||||
<span class="text-slate-500 text-[10px] font-mono">SUP: ${product.priceSup}€</span>
|
||||
<span class="text-amber-500/80 text-[10px] font-mono">CAUTION: ${product.caution}€</span>
|
||||
<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-full bg-purple-500/0 group-hover:bg-purple-500/20 flex items-center justify-center text-purple-500 transition-all">
|
||||
<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>
|
||||
`;
|
||||
@@ -275,14 +285,19 @@ export class SearchProductDevis extends HTMLButtonElement {
|
||||
fillFormLine(product) {
|
||||
const row = this.closest('.form-repeater__row');
|
||||
if (row) {
|
||||
row.querySelector('input[name*="[product]"]').value = product.name;
|
||||
row.querySelector('input[name*="[price_ht]"]').value = product.price1day;
|
||||
row.querySelector('input[name*="[price_sup_ht]"]').value = product.priceSup;
|
||||
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');
|
||||
fieldset.classList.add('border-emerald-500/50', 'bg-emerald-500/5');
|
||||
setTimeout(() => fieldset.classList.remove('border-emerald-500/50', 'bg-emerald-500/5'), 800);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user