```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:
@@ -37,8 +37,11 @@ export class CrmEditor extends HTMLTextAreaElement {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="wysiwyg-area" contenteditable="true" class="p-6 min-h-[300px] focus:outline-none bg-white text-slate-900 prose prose-slate max-w-none shadow-inner">${this.value || '<p><br></p>'}</div>
|
||||
|
||||
<div id="wysiwyg-area"
|
||||
contenteditable="true"
|
||||
class="p-6 min-h-[300px] max-h-[600px] overflow-y-auto focus:outline-none bg-white text-slate-900 prose prose-slate max-w-none shadow-inner break-words w-full">
|
||||
${this.value || '<p><br></p>'}
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-4 py-2 border-t border-white/10 bg-black/20 text-[11px] font-medium tracking-wide">
|
||||
<div id="char-counter" class="flex items-center gap-2">
|
||||
<span id="char-count" class="text-white bg-white/10 px-2 py-0.5 rounded-full">0 caractères</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import frLocale from '@fullcalendar/core/locales/fr';
|
||||
|
||||
export default class PlaningLogestics extends HTMLElement {
|
||||
export default class PlaningLogistics extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.calendar = null;
|
||||
@@ -17,49 +17,46 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div class="p-4 md:p-8 bg-gray-50 dark:bg-gray-950 min-h-screen transition-colors duration-300 font-sans">
|
||||
<div class="p-4 md:p-8 bg-[#0f172a] min-h-screen font-sans text-slate-200">
|
||||
<div class="mx-auto space-y-6">
|
||||
<!-- En-tête Statistiques -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="bg-white dark:bg-gray-900 p-5 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 flex items-center gap-4">
|
||||
<div class="p-3 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl text-indigo-600">
|
||||
<div class="bg-[#1e293b] p-5 rounded-2xl shadow-xl border border-slate-800 flex items-center gap-4">
|
||||
<div class="p-3 bg-blue-600/20 rounded-xl text-blue-500">
|
||||
<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="M13 5l7 7-7 7M5 5l7 7-7 7"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Réservations</p>
|
||||
<p class="text-2xl font-black text-gray-900 dark:text-white" id="stat-departs">0</p>
|
||||
<p class="text-[10px] font-bold text-slate-500 uppercase tracking-[0.2em]">Réservations</p>
|
||||
<p class="text-2xl font-black text-white" id="stat-departs">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-900 p-5 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-800 flex items-center gap-4">
|
||||
<div class="p-3 bg-emerald-100 dark:bg-emerald-900/30 rounded-xl text-emerald-600">
|
||||
<div class="bg-[#1e293b] p-5 rounded-2xl shadow-xl border border-slate-800 flex items-center gap-4">
|
||||
<div class="p-3 bg-emerald-600/20 rounded-xl text-emerald-500">
|
||||
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Contrats Signés</p>
|
||||
<p class="text-2xl font-black text-gray-900 dark:text-white" id="stat-retours">0</p>
|
||||
<p class="text-[10px] font-bold text-slate-500 uppercase tracking-[0.2em]">Contrats Signés</p>
|
||||
<p class="text-2xl font-black text-white" id="stat-retours">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendrier Principal -->
|
||||
<div class="bg-white dark:bg-gray-900 p-2 md:p-6 rounded-3xl shadow-sm border border-gray-100 dark:border-gray-800 relative">
|
||||
<div id="calendar-loading" class="absolute inset-0 bg-white/50 dark:bg-gray-900/50 backdrop-blur-[2px] z-10 flex items-center justify-center hidden">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
||||
<div class="bg-[#1e293b] p-2 md:p-6 rounded-3xl shadow-2xl border border-slate-800 relative">
|
||||
<div id="calendar-loading" class="absolute inset-0 bg-[#0f172a]/60 backdrop-blur-[2px] z-10 flex items-center justify-center hidden">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
<div id="calendar-root"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modale de Détails -->
|
||||
<div id="calendar-modal" class="fixed inset-0 z-[100] hidden items-center justify-center p-4">
|
||||
<div class="absolute inset-0 bg-gray-900/60 backdrop-blur-sm modal-overlay"></div>
|
||||
<div class="relative bg-white dark:bg-gray-900 rounded-3xl shadow-2xl w-full max-w-lg overflow-hidden border border-gray-100 dark:border-gray-800">
|
||||
<div class="absolute inset-0 bg-[#0f172a]/80 backdrop-blur-sm modal-overlay"></div>
|
||||
<div class="relative bg-[#1e293b] rounded-3xl shadow-2xl w-full max-w-lg overflow-hidden border border-slate-700">
|
||||
<div class="p-6 md:p-8">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<span id="modal-contract-number" class="px-3 py-1 bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded-lg text-[10px] font-black uppercase tracking-tighter border border-gray-200 dark:border-gray-700"></span>
|
||||
<button class="close-modal text-gray-400 hover:text-gray-600 dark:hover:text-white transition-colors">
|
||||
<span id="modal-contract-number" class="px-3 py-1 bg-slate-900/50 text-blue-500 rounded-lg text-[10px] font-black uppercase tracking-widest border border-blue-500/30"></span>
|
||||
<button class="close-modal 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"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -67,106 +64,109 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
<div class="flex flex-wrap gap-2 mb-6" id="modal-status-container"></div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 id="modal-title" class="text-xl font-extrabold text-gray-900 dark:text-white leading-tight mb-1"></h2>
|
||||
<p id="modal-client" class="text-indigo-600 dark:text-indigo-400 font-bold text-lg"></p>
|
||||
<h2 id="modal-title" class="text-xl font-extrabold text-white leading-tight mb-1"></h2>
|
||||
<p id="modal-client" class="text-blue-500 font-bold text-lg"></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<a id="link-phone" href="#" class="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors group">
|
||||
<div class="text-indigo-500 group-hover:scale-110 transition-transform"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg></div>
|
||||
<span id="modal-phone" class="text-sm font-medium text-gray-700 dark:text-gray-300"></span>
|
||||
<a id="link-phone" href="#" class="flex items-center gap-3 p-3 bg-slate-900/50 rounded-xl hover:bg-slate-800 transition-colors group border border-slate-800">
|
||||
<div class="text-blue-500 group-hover:scale-110 transition-transform"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg></div>
|
||||
<span id="modal-phone" class="text-sm font-medium text-slate-300"></span>
|
||||
</a>
|
||||
<a id="link-email" href="#" class="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors group">
|
||||
<div class="text-indigo-500 group-hover:scale-110 transition-transform"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg></div>
|
||||
<span id="modal-email" class="text-sm font-medium text-gray-700 dark:text-gray-300 truncate"></span>
|
||||
<a id="link-email" href="#" class="flex items-center gap-3 p-3 bg-slate-900/50 rounded-xl hover:bg-slate-800 transition-colors group border border-slate-800">
|
||||
<div class="text-blue-500 group-hover:scale-110 transition-transform"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg></div>
|
||||
<span id="modal-email" class="text-sm font-medium text-slate-300 truncate"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-2xl border border-gray-100 dark:border-gray-700">
|
||||
<div class="mb-6 p-4 bg-slate-900/50 rounded-2xl border border-slate-800">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="text-emerald-500 mt-0.5"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg></div>
|
||||
<div>
|
||||
<span class="block text-[10px] font-bold text-gray-400 uppercase tracking-widest">Lieu de l'événement</span>
|
||||
<span id="modal-adresse" class="text-sm text-gray-700 dark:text-gray-300 font-semibold leading-snug"></span>
|
||||
<span class="block text-[10px] font-bold text-slate-500 uppercase tracking-[0.2em]">Lieu de l'événement</span>
|
||||
<span id="modal-adresse" class="text-sm text-slate-300 font-semibold leading-snug"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-6 py-4 border-t border-gray-100 dark:border-gray-800">
|
||||
<div class="grid grid-cols-2 gap-6 py-4 border-t border-slate-800">
|
||||
<div>
|
||||
<span class="block text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1">Départ / Enlèvement</span>
|
||||
<span id="modal-start" class="text-sm text-gray-900 dark:text-white font-bold"></span>
|
||||
<span class="block text-[10px] font-bold text-slate-500 uppercase tracking-[0.2em] mb-1">Départ / Enlèvement</span>
|
||||
<span id="modal-start" class="text-sm text-white font-bold"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1">Retour prévu</span>
|
||||
<span id="modal-end" class="text-sm text-gray-900 dark:text-white font-bold"></span>
|
||||
<span class="block text-[10px] font-bold text-slate-500 uppercase tracking-[0.2em] mb-1">Retour prévu</span>
|
||||
<span id="modal-end" class="text-sm text-white font-bold"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-gray-50 dark:bg-gray-800/50 flex gap-3">
|
||||
<a id="modal-link-contrat" href="#" target="_blank" class="flex-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 py-4 rounded-2xl font-bold text-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-all flex items-center justify-center gap-2 shadow-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
<div class="p-6 bg-slate-900/50 flex gap-3">
|
||||
<a id="modal-link-contrat" href="#" target="_blank" class="flex-1 bg-[#1e293b] border border-slate-700 text-slate-200 py-4 rounded-2xl font-bold text-sm hover:bg-slate-800 hover:border-blue-500/50 transition-all flex items-center justify-center gap-2 shadow-xl">
|
||||
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
Voir Contrat
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:host {
|
||||
--fc-border-color: #f1f5f9;
|
||||
--fc-button-bg-color: #ffffff;
|
||||
--fc-button-border-color: #e2e8f0;
|
||||
--fc-button-text-color: #64748b;
|
||||
--fc-today-bg-color: rgba(79, 70, 229, 0.05);
|
||||
--fc-border-color: #334155;
|
||||
--fc-button-bg-color: #1e293b;
|
||||
--fc-button-border-color: #334155;
|
||||
--fc-button-text-color: #94a3b8;
|
||||
--fc-today-bg-color: rgba(37, 99, 235, 0.1);
|
||||
--fc-page-bg-color: #0f172a;
|
||||
--fc-list-event-hover-bg-color: #1e293b;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--fc-border-color: #1f2937;
|
||||
--fc-button-bg-color: #111827;
|
||||
--fc-button-border-color: #374151;
|
||||
--fc-button-text-color: #9ca3af;
|
||||
--fc-today-bg-color: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
#calendar-root { min-height: 750px; }
|
||||
.fc-toolbar-title { font-weight: 900 !important; color: inherit; font-size: 1.25rem !important; }
|
||||
.fc-col-header-cell { padding: 12px 0 !important; text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.1em; color: #94a3b8; }
|
||||
#calendar-root { min-height: 750px; color: #f1f5f9; }
|
||||
.fc-toolbar-title { font-weight: 900 !important; color: #ffffff; font-size: 1.25rem !important; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.fc-col-header-cell { padding: 12px 0 !important; text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.15em; color: #64748b; }
|
||||
|
||||
.fc .fc-button-primary {
|
||||
background: var(--fc-button-bg-color) !important;
|
||||
color: var(--fc-button-text-color) !important;
|
||||
border: 1px solid var(--fc-button-border-color) !important;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2) !important;
|
||||
border-radius: 12px !important;
|
||||
font-weight: 700 !important;
|
||||
text-transform: capitalize !important;
|
||||
font-weight: 800 !important;
|
||||
text-transform: uppercase !important;
|
||||
font-size: 0.75rem !important;
|
||||
padding: 8px 16px !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.fc .fc-button-primary:hover {
|
||||
background: #334155 !important;
|
||||
border-color: #475569 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.fc .fc-button-active {
|
||||
background: #4f46e5 !important;
|
||||
background: #2563eb !important;
|
||||
color: white !important;
|
||||
border-color: #4f46e5 !important;
|
||||
border-color: #2563eb !important;
|
||||
box-shadow: 0 0 15px rgba(37, 99, 235, 0.4) !important;
|
||||
}
|
||||
|
||||
.fc-theme-standard td, .fc-theme-standard th { border-color: #334155 !important; }
|
||||
.fc-day-other { background: #0f172a; opacity: 0.5; }
|
||||
|
||||
.fc-event {
|
||||
border: none !important;
|
||||
border-radius: 10px !important;
|
||||
padding: 4px !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 3px !important;
|
||||
margin: 1px 2px !important;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.fc-event:hover { transform: translateY(-1px); filter: brightness(1.1); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); }
|
||||
.fc-event-main-frame { padding: 4px; }
|
||||
.status-indicators { display: flex; gap: 3px; margin-top: 4px; }
|
||||
.fc-event:hover { transform: scale(1.02); filter: brightness(1.2); }
|
||||
.fc-event-main-frame { padding: 2px 4px; }
|
||||
.status-indicators { display: flex; gap: 4px; margin-top: 5px; }
|
||||
.status-dot { width: 14px; height: 14px; border-radius: 4px; display: flex; align-items: center; justify-content: center; }
|
||||
.dark .fc-theme-standard td, .dark .fc-theme-standard th { border-color: #1f2937 !important; }
|
||||
</style>
|
||||
`;
|
||||
|
||||
@@ -187,50 +187,38 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek'
|
||||
},
|
||||
|
||||
events: (fetchInfo, successCallback, failureCallback) => {
|
||||
loader.classList.remove('hidden');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
start: fetchInfo.startStr,
|
||||
end: fetchInfo.endStr
|
||||
});
|
||||
const params = new URLSearchParams({ start: fetchInfo.startStr, end: fetchInfo.endStr });
|
||||
|
||||
fetch(`/crm/reservation/data?${params.toString()}`)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Erreur réseau');
|
||||
return response.json();
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// On ajoute les propriétés de style si l'API ne les fournit pas
|
||||
const formattedData = data.map(item => ({
|
||||
...item,
|
||||
backgroundColor: item.extendedProps.isSigned ? '#10b981' : '#4f46e5'
|
||||
backgroundColor: item.extendedProps.isSigned ? '#059669' : '#2563eb'
|
||||
}));
|
||||
|
||||
successCallback(formattedData);
|
||||
this.updateStats(formattedData);
|
||||
loader.classList.add('hidden');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Erreur chargement:", error);
|
||||
failureCallback(error);
|
||||
loader.classList.add('hidden');
|
||||
failureCallback(error);
|
||||
});
|
||||
},
|
||||
|
||||
eventContent: (arg) => {
|
||||
const { caution, acompte, solde, isSigned } = arg.event.extendedProps;
|
||||
return {
|
||||
html: `
|
||||
<div class="fc-event-main-frame">
|
||||
<div class="text-[11px] font-bold leading-tight line-clamp-1 flex items-center gap-1">
|
||||
${isSigned ? '✅' : ''} ${arg.event.title}
|
||||
<div class="text-[10px] font-black uppercase tracking-tight leading-tight line-clamp-1 flex items-center gap-1 text-white">
|
||||
${isSigned ? '✅' : '⏳'} ${arg.event.title}
|
||||
</div>
|
||||
<div class="status-indicators">
|
||||
<div class="status-dot" style="background:${caution ? '#f59e0b' : '#4b5563'}; opacity:${caution ? 1 : 0.3}"><svg class="w-2.5 h-2.5 text-white" fill="currentColor" viewBox="0 0 20 20"><path d="M4 4a2 2 0 00-2 2v1h16V6a2 2 0 00-2-2H4z"></path><path d="M2 9h16v5a2 2 0 01-2 2H4a2 2 0 01-2-2V9z"></path></svg></div>
|
||||
<div class="status-dot" style="background:${acompte ? '#3b82f6' : '#4b5563'}; opacity:${acompte ? 1 : 0.3}"><svg class="w-2.5 h-2.5 text-white" fill="currentColor" viewBox="0 0 20 20"><path d="M4 4a2 2 0 002 2V6h10a2 2 0 00-2-2H4z"></path></svg></div>
|
||||
<div class="status-dot" style="background:${solde ? '#10b981' : '#4b5563'}; opacity:${solde ? 1 : 0.3}"><svg class="w-2.5 h-2.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg></div>
|
||||
<div class="status-dot" style="background:${caution ? '#d97706' : '#1e293b'}; opacity:${caution ? 1 : 0.4}"><svg class="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20"><path d="M4 4a2 2 0 00-2 2v1h16V6a2 2 0 00-2-2H4z"></path><path d="M2 9h16v5a2 2 0 01-2 2H4a2 2 0 01-2-2V9z"></path></svg></div>
|
||||
<div class="status-dot" style="background:${acompte ? '#2563eb' : '#1e293b'}; opacity:${acompte ? 1 : 0.4}"><svg class="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20"><path d="M4 4a2 2 0 002 2V6h10a2 2 0 00-2-2H4z"></path></svg></div>
|
||||
<div class="status-dot" style="background:${solde ? '#059669' : '#1e293b'}; opacity:${solde ? 1 : 0.4}"><svg class="w-2 h-2 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M5 13l4 4L19 7"></path></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -238,50 +226,36 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
},
|
||||
eventClick: (info) => this.showEventDetails(info.event)
|
||||
});
|
||||
|
||||
this.calendar.render();
|
||||
}
|
||||
|
||||
updateStats(events) {
|
||||
const total = events.length;
|
||||
const signed = events.filter(e => e.extendedProps && e.extendedProps.isSigned).length;
|
||||
|
||||
this.querySelector('#stat-departs').innerText = total;
|
||||
this.querySelector('#stat-retours').innerText = signed;
|
||||
this.querySelector('#stat-departs').innerText = events.length;
|
||||
this.querySelector('#stat-retours').innerText = events.filter(e => e.extendedProps?.isSigned).length;
|
||||
}
|
||||
|
||||
formatAddress(addr) {
|
||||
if (!addr || typeof addr !== 'object') return 'Adresse non renseignée';
|
||||
const parts = [addr.address, addr.address1, addr.address2, addr.zipCode, addr.city];
|
||||
return parts.filter(p => p && p !== 'null' && p !== '').join(', ');
|
||||
return [addr.address, addr.address1, addr.zipCode, addr.city].filter(p => p && p !== 'null').join(', ');
|
||||
}
|
||||
|
||||
showEventDetails(event) {
|
||||
const modal = this.querySelector('#calendar-modal');
|
||||
const props = event.extendedProps;
|
||||
const modalStatus = this.querySelector('#modal-status-container');
|
||||
|
||||
this.querySelector('#modal-contract-number').innerText = `Contrat n°${props.contractNumber || '?'}`;
|
||||
this.querySelector('#modal-title').innerText = event.title;
|
||||
this.querySelector('#modal-client').innerText = props.client || 'Client inconnu';
|
||||
this.querySelector('#modal-email').innerText = props.clientEmail || 'Non renseigné';
|
||||
this.querySelector('#modal-phone').innerText = props.clientPhone || 'Non renseigné';
|
||||
|
||||
// Gestion de l'objet adresse spécifique à votre API
|
||||
this.querySelector('#modal-adresse').innerText = this.formatAddress(props.eventAdresse);
|
||||
|
||||
const dateOptions = { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' };
|
||||
|
||||
this.querySelector('#modal-start').innerText = props.start;
|
||||
this.querySelector('#modal-end').innerText = props.end;
|
||||
|
||||
this.querySelector('#link-phone').href = `tel:${props.clientPhone}`;
|
||||
this.querySelector('#link-email').href = `mailto:${props.clientEmail}`;
|
||||
this.querySelector('#modal-link-contrat').href = props.linkContrat || '#';
|
||||
|
||||
this.querySelector('#modal-contract-number').style.borderColor = event.backgroundColor;
|
||||
this.querySelector('#modal-contract-number').style.color = event.backgroundColor;
|
||||
|
||||
const statusContainer = this.querySelector('#modal-status-container');
|
||||
const statusList = [
|
||||
{ label: 'Signé', val: props.isSigned, color: 'emerald' },
|
||||
{ label: 'Caution', val: props.caution, color: 'orange' },
|
||||
@@ -289,20 +263,17 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
{ label: 'Solde', val: props.solde, color: 'emerald' }
|
||||
];
|
||||
|
||||
statusContainer.innerHTML = statusList.map(s => `
|
||||
<span class="px-2.5 py-1 rounded-lg text-[10px] font-bold uppercase tracking-wider flex items-center gap-1.5 ${s.val ? `bg-${s.color}-50 text-${s.color}-700 dark:bg-${s.color}-900/20 dark:text-${s.color}-400 border border-${s.color}-100 dark:border-${s.color}-800` : 'bg-gray-50 text-gray-400 dark:bg-gray-800/50 dark:text-gray-600 border border-transparent'}">
|
||||
<div class="w-1.5 h-1.5 rounded-full ${s.val ? `bg-${s.color}-500` : 'bg-gray-400'}"></div>
|
||||
modalStatus.innerHTML = statusList.map(s => `
|
||||
<span class="px-2.5 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest flex items-center gap-1.5 ${s.val ? `bg-${s.color}-500/10 text-${s.color}-500 border border-${s.color}-500/20` : 'bg-slate-800 text-slate-600 border border-transparent'}">
|
||||
<div class="w-1.5 h-1.5 rounded-full ${s.val ? `bg-${s.color}-500 shadow-[0_0_5px_currentColor]` : 'bg-slate-600'}"></div>
|
||||
${s.label}
|
||||
</span>
|
||||
`).join('');
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
modal.classList.replace('hidden', 'flex');
|
||||
}
|
||||
|
||||
hideModal() {
|
||||
const modal = this.querySelector('#calendar-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
this.querySelector('#calendar-modal').classList.replace('flex', 'hidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export class SearchOptions 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="text-slate-400 p-8 text-center animate-pulse tracking-[0.2em] text-[10px] uppercase font-black">Chargement du catalogue options...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/crm/options/json');
|
||||
@@ -29,40 +29,40 @@ export class SearchOptions extends HTMLButtonElement {
|
||||
this.renderOptions(this.allOptions, container, modal);
|
||||
searchInput.focus();
|
||||
} 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 font-bold uppercase text-xs">Erreur de liaison catalogue.</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]/95 backdrop-blur-xl 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="p-8 border-b border-white/5 bg-white/5 space-y-6">
|
||||
<div class="bg-[#1e293b] border border-slate-800 w-full max-w-2xl max-h-[85vh] rounded-[2.5rem] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden flex flex-col scale-in-center">
|
||||
<div class="p-8 border-b border-slate-800 bg-slate-900/30 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>
|
||||
Sélection Option
|
||||
<span class="w-2.5 h-2.5 bg-blue-600 rounded-full mr-3 shadow-[0_0_10px_rgba(37,99,235,0.5)] animate-pulse"></span>
|
||||
Catalogue Options
|
||||
</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>
|
||||
<button type="button" onclick="this.closest('#modal-search-options').classList.add('hidden')" class="p-2 text-slate-500 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 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 une option..."
|
||||
class="w-full bg-[#0f172a] border border-slate-700 rounded-2xl py-4 pl-12 pr-5 text-white text-xs font-bold tracking-wide focus:ring-2 focus:ring-blue-600/20 focus:border-blue-600 transition-all placeholder:text-slate-600 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 id="results-container-options" class="overflow-y-auto p-6 space-y-3 custom-scrollbar flex-grow min-h-[350px] bg-[#1e293b]"></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-slate-900/50 border-t border-slate-800 text-center">
|
||||
<p class="text-[9px] text-slate-500 font-black uppercase tracking-widest">ESC pour quitter • Sélectionner pour ajouter</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -75,9 +75,7 @@ export class SearchOptions 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);
|
||||
};
|
||||
|
||||
@@ -90,28 +88,28 @@ export class SearchOptions extends HTMLButtonElement {
|
||||
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>';
|
||||
container.innerHTML = '<div class="text-slate-600 p-12 text-center text-[10px] font-black uppercase tracking-[0.2em]">Aucun résultat correspondant</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-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-slate-900/40 border border-slate-800 rounded-2xl hover:bg-blue-600/5 hover:border-blue-600/30 transition-all cursor-pointer group animate-in fade-in slide-in-from-bottom-1';
|
||||
|
||||
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'">`
|
||||
: `<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>`;
|
||||
? `<img src="${option.image}" class="w-14 h-14 rounded-xl object-cover shadow-2xl border border-slate-700 group-hover:scale-105 transition-transform">`
|
||||
: `<div class="w-14 h-14 bg-[#0f172a] rounded-xl flex items-center justify-center text-[8px] text-slate-600 font-black border border-slate-800 uppercase tracking-tighter">Pas d'image</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="text-slate-100 font-bold text-sm 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-widest">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">
|
||||
<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 class="w-10 h-10 rounded-xl bg-slate-800 group-hover:bg-blue-600 text-slate-500 group-hover:text-white flex items-center justify-center 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="2.5" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -124,30 +122,29 @@ export class SearchOptions 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*="[name]"]');
|
||||
const priceInput = row.querySelector('input[name*="[priceHt]"]');
|
||||
|
||||
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-600/40', 'bg-blue-600/5');
|
||||
setTimeout(() => fieldset.classList.remove('border-blue-600/40', 'bg-blue-600/5'), 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RECHERCHE PRODUITS (SearchProduct)
|
||||
*/
|
||||
export class SearchProduct extends HTMLButtonElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.allProducts = []; // Stockage local pour la recherche
|
||||
this.allProducts = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -166,8 +163,8 @@ export class SearchProduct 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="text-slate-400 p-8 text-center animate-pulse tracking-[0.2em] text-[10px] uppercase font-black">Mise à jour du catalogue produits...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/crm/products/json');
|
||||
@@ -175,40 +172,40 @@ export class SearchProduct extends HTMLButtonElement {
|
||||
this.renderProducts(this.allProducts, container, modal);
|
||||
searchInput.focus();
|
||||
} 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 font-bold uppercase text-xs">Échec de synchronisation.</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]/95 backdrop-blur-xl 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="p-8 border-b border-white/5 bg-white/5 space-y-6">
|
||||
<div class="bg-[#1e293b] border border-slate-800 w-full max-w-2xl max-h-[85vh] rounded-[2.5rem] shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden flex flex-col scale-in-center">
|
||||
<div class="p-8 border-b border-slate-800 bg-slate-900/30 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>
|
||||
Sélection Produit
|
||||
<span class="w-2.5 h-2.5 bg-purple-600 rounded-full mr-3 shadow-[0_0_10px_rgba(147,51,234,0.5)] animate-pulse"></span>
|
||||
Catalogue Location
|
||||
</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>
|
||||
<button type="button" onclick="this.closest('#modal-search-product').classList.add('hidden')" class="p-2 text-slate-500 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="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="Rechercher un produit..."
|
||||
class="w-full bg-[#0f172a] border border-slate-700 rounded-2xl py-4 pl-12 pr-5 text-white text-xs font-bold tracking-wide focus:ring-2 focus:ring-purple-600/20 focus:border-purple-600 transition-all placeholder:text-slate-600 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 id="results-container" class="overflow-y-auto p-6 space-y-3 custom-scrollbar flex-grow min-h-[350px] bg-[#1e293b]"></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-slate-900/50 border-t border-slate-800 text-center">
|
||||
<p class="text-[9px] text-slate-500 font-black uppercase tracking-widest">ESC pour quitter • Sélectionner pour ajouter</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -221,13 +218,10 @@ export class SearchProduct 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');
|
||||
});
|
||||
@@ -243,24 +237,24 @@ export class SearchProduct 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-slate-900/40 border border-slate-800 rounded-2xl hover:bg-purple-600/5 hover:border-purple-600/30 transition-all cursor-pointer group animate-in fade-in slide-in-from-bottom-1';
|
||||
|
||||
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">`
|
||||
: `<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>`;
|
||||
? `<img src="${product.image}" class="w-14 h-14 rounded-xl object-cover shadow-2xl border border-slate-700 group-hover:scale-105 transition-transform">`
|
||||
: `<div class="w-14 h-14 bg-[#0f172a] rounded-xl flex items-center justify-center text-[8px] text-slate-600 font-black border border-slate-800 uppercase tracking-tighter">Pas d'image</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="text-slate-100 font-bold text-sm 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 tracking-widest uppercase">1J: ${product.price1day}€</span>
|
||||
<span class="text-slate-500 text-[10px] font-black tracking-widest uppercase">Sup: ${product.priceSup}€</span>
|
||||
<span class="text-amber-600 text-[10px] font-black tracking-widest uppercase">Caution: ${product.caution}€</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">
|
||||
<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 class="w-10 h-10 rounded-xl bg-slate-800 group-hover:bg-purple-600 text-slate-500 group-hover:text-white flex items-center justify-center 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="2.5" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -281,9 +275,10 @@ export class SearchProduct extends HTMLButtonElement {
|
||||
row.querySelector('input[name*="[caution]"]').value = product.caution;
|
||||
|
||||
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-emerald-600/40', 'bg-emerald-600/5');
|
||||
setTimeout(() => fieldset.classList.remove('border-emerald-600/40', 'bg-emerald-600/5'), 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
/**
|
||||
* RECHERCHE OPTIONS POUR FORMULES
|
||||
*/
|
||||
export class SearchOptionsFormule extends HTMLButtonElement {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -6,11 +8,14 @@ export class SearchOptionsFormule extends HTMLButtonElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', () => this.openModal());
|
||||
this.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.openModal();
|
||||
});
|
||||
}
|
||||
|
||||
async openModal() {
|
||||
let modal = document.getElementById('modal-search-options');
|
||||
let modal = document.getElementById('modal-search-options-formule');
|
||||
if (!modal) {
|
||||
modal = this.createModalStructure();
|
||||
document.body.appendChild(modal);
|
||||
@@ -22,48 +27,50 @@ export class SearchOptionsFormule 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-6 h-6 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-black uppercase tracking-widest">Erreur catalogue options</div>';
|
||||
}
|
||||
}
|
||||
|
||||
createModalStructure() {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'modal-search-options';
|
||||
div.id = 'modal-search-options-formule';
|
||||
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.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-slate-900 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 text-white">
|
||||
<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>
|
||||
Sélection Option
|
||||
<h3 class="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 (Formule)
|
||||
</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>
|
||||
<button type="button" onclick="this.closest('#modal-search-options-formule').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 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="FILTRER LES OPTIONS..."
|
||||
class="w-full bg-slate-950 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">
|
||||
<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">ESC pour fermer • Sélectionnez une ligne pour valider</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -76,9 +83,7 @@ export class SearchOptionsFormule 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);
|
||||
};
|
||||
|
||||
@@ -89,30 +94,27 @@ export class SearchOptionsFormule 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>';
|
||||
container.innerHTML = '<div class="text-slate-600 p-12 text-center text-[10px] font-black uppercase tracking-[0.2em]">Aucun résultat</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-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'">`
|
||||
: `<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>`;
|
||||
? `<img src="${option.image}" class="w-12 h-12 rounded-xl object-cover shadow-lg group-hover:scale-105 transition-transform" onerror="this.src='/provider/images/favicon.png'">`
|
||||
: `<div class="w-12 h-12 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-400 text-[10px] font-mono">PRIX HT: ${option.price}€</span>
|
||||
</div>
|
||||
<div class="text-white font-black text-[11px] uppercase tracking-wider">${option.name}</div>
|
||||
<div class="text-blue-400 text-[9px] font-mono mt-1">BASE HT: ${option.price}€</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">
|
||||
<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 class="w-8 h-8 rounded-full bg-slate-800 group-hover:bg-blue-600 flex items-center justify-center text-slate-500 group-hover:text-white transition-all">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4"/></svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -125,15 +127,11 @@ export class SearchOptionsFormule 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]"]');
|
||||
|
||||
if(nameInput) nameInput.value = option.name;
|
||||
|
||||
// Feedback visuel (Bleu pour les options)
|
||||
const fieldset = row.querySelector('fieldset');
|
||||
if (fieldset) {
|
||||
fieldset.classList.add('border-blue-500/50', 'bg-blue-500/5');
|
||||
@@ -142,18 +140,25 @@ export class SearchOptionsFormule extends HTMLButtonElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RECHERCHE PRODUITS POUR FORMULES
|
||||
*/
|
||||
export class SearchProductFormule 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() {
|
||||
let modal = document.getElementById('modal-search-product');
|
||||
let modal = document.getElementById('modal-search-product-formule');
|
||||
if (!modal) {
|
||||
modal = this.createModalStructure();
|
||||
document.body.appendChild(modal);
|
||||
@@ -164,49 +169,51 @@ export class SearchProductFormule 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-6 h-6 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 du 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-black uppercase tracking-widest">Erreur catalogue produits</div>';
|
||||
}
|
||||
}
|
||||
|
||||
createModalStructure() {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'modal-search-product';
|
||||
div.id = 'modal-search-product-formule';
|
||||
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.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-slate-900 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 text-white">
|
||||
<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>
|
||||
Sélection Produit
|
||||
<h3 class="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 (Formule)
|
||||
</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>
|
||||
<button type="button" onclick="this.closest('#modal-search-product-formule').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="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="CHERCHER UN PRODUIT..."
|
||||
class="w-full bg-slate-950 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">
|
||||
<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">ESC pour annuler • Cliquez pour importer</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -219,13 +226,10 @@ export class SearchProductFormule 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');
|
||||
});
|
||||
@@ -233,7 +237,6 @@ export class SearchProductFormule 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;
|
||||
@@ -241,24 +244,23 @@ export class SearchProductFormule 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">`
|
||||
: `<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>`;
|
||||
? `<img src="${product.image}" class="w-12 h-12 rounded-xl object-cover shadow-lg group-hover:scale-105 transition-transform">`
|
||||
: `<div class="w-12 h-12 bg-slate-950 rounded-xl flex items-center justify-center text-[8px] text-slate-700 font-black border border-white/5">PRD</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="text-white font-black text-[11px] 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-[9px] font-mono">1J: ${product.price1day}€</span>
|
||||
<span class="text-slate-500 text-[9px] font-mono">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">
|
||||
<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 class="w-8 h-8 rounded-full bg-slate-800 group-hover:bg-purple-600 flex items-center justify-center text-slate-500 group-hover:text-white transition-all">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4"/></svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -273,12 +275,14 @@ export class SearchProductFormule extends HTMLButtonElement {
|
||||
fillFormLine(product) {
|
||||
const row = this.closest('.form-repeater__row');
|
||||
if (row) {
|
||||
row.querySelector('input[name*="[product]"]').value = product.name;
|
||||
const nameInput = row.querySelector('input[name*="[product]"]');
|
||||
if(nameInput) nameInput.value = product.name;
|
||||
|
||||
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-emerald-500/50', 'bg-emerald-500/5');
|
||||
setTimeout(() => fieldset.classList.remove('border-emerald-500/50', 'bg-emerald-500/5'), 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,16 +6,35 @@ let optionsCache = null;
|
||||
|
||||
/**
|
||||
* Initialise TomSelect sur un élément ou un groupe d'éléments
|
||||
* Adapté pour le thème sombre forcé.
|
||||
*/
|
||||
export function initTomSelect(parent = document) {
|
||||
parent.querySelectorAll('select').forEach((el) => {
|
||||
// --- 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 COMMUNE RENDU ---
|
||||
const commonRender = {
|
||||
option: (data, escape) => `
|
||||
<div class="flex items-center gap-3 py-3 px-4 border-b border-slate-700/50 hover:bg-slate-800 transition-colors">
|
||||
<img src="${escape(data.image)}" class="w-10 h-10 object-cover rounded-lg shadow-lg border border-slate-700">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-[13px] font-bold text-slate-100">${escape(data.name)}</div>
|
||||
<div class="text-[10px] text-blue-500 font-bold uppercase tracking-wider">
|
||||
${data.price1day ? `J1: ${escape(data.price1day)}€ | Sup: ${escape(data.priceSup)}€` : `${escape(data.price)}€`}
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
item: (data, escape) => `
|
||||
<div class="text-blue-400 font-bold flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]"></span>
|
||||
${escape(data.name)}
|
||||
</div>`
|
||||
};
|
||||
|
||||
// --- CONFIGURATION PRODUITS ---
|
||||
if (el.getAttribute('data-load') === "product") {
|
||||
const setupSelect = (data) => {
|
||||
const setupProduct = (data) => {
|
||||
new TomSelect(el, {
|
||||
valueField: 'id',
|
||||
labelField: 'name',
|
||||
@@ -42,34 +61,21 @@ export function initTomSelect(parent = document) {
|
||||
}
|
||||
}
|
||||
},
|
||||
render: {
|
||||
option: (data, escape) => `
|
||||
<div class="flex items-center gap-3 py-2 px-3 border-b border-slate-800/50">
|
||||
<img src="${escape(data.image)}" class="w-8 h-8 object-cover rounded shadow-sm">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-[13px] font-bold text-white">${escape(data.name)}</div>
|
||||
<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>`
|
||||
}
|
||||
render: commonRender
|
||||
});
|
||||
};
|
||||
|
||||
if (productCache) {
|
||||
setupSelect(productCache);
|
||||
} else {
|
||||
if (productCache) setupProduct(productCache);
|
||||
else {
|
||||
fetch("/crm/products/json")
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
productCache = data;
|
||||
setupSelect(data);
|
||||
});
|
||||
.then(data => { productCache = data; setupProduct(data); });
|
||||
}
|
||||
}
|
||||
|
||||
// --- CONFIGURATION OPTIONS ---
|
||||
else if (el.getAttribute('data-load') === "options") {
|
||||
const setupSelect = (data) => {
|
||||
const setupOptions = (data) => {
|
||||
new TomSelect(el, {
|
||||
valueField: 'id',
|
||||
labelField: 'name',
|
||||
@@ -88,31 +94,18 @@ export function initTomSelect(parent = document) {
|
||||
priceInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
},
|
||||
render: {
|
||||
option: (data, escape) => `
|
||||
<div class="flex items-center gap-3 py-2 px-3 border-b border-slate-800/50">
|
||||
<img src="${escape(data.image)}" class="w-8 h-8 object-cover rounded shadow-sm">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-[13px] font-bold text-white">${escape(data.name)}</div>
|
||||
<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>`
|
||||
}
|
||||
render: commonRender
|
||||
});
|
||||
};
|
||||
|
||||
if (optionsCache) {
|
||||
setupSelect(optionsCache);
|
||||
} else {
|
||||
if (optionsCache) setupOptions(optionsCache);
|
||||
else {
|
||||
fetch("/crm/options/json")
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
optionsCache = data;
|
||||
setupSelect(data);
|
||||
});
|
||||
.then(data => { optionsCache = data; setupOptions(data); });
|
||||
}
|
||||
}
|
||||
|
||||
// --- AUTRES SELECTS STANDARDS ---
|
||||
else {
|
||||
new TomSelect(el, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" class="dark">
|
||||
<html lang="fr" class="dark" style="color-scheme: dark;">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -10,30 +10,29 @@
|
||||
{% endif %}
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-slate-50 dark:bg-[#0f172a] text-slate-900 dark:text-slate-200 antialiased overflow-hidden font-sans">
|
||||
<body class="bg-[#0f172a] text-slate-200 antialiased overflow-hidden font-sans">
|
||||
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
|
||||
{# SIDEBAR #}
|
||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
|
||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-[#1e293b] border-r border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
|
||||
|
||||
<div class="flex items-center px-8 h-20 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center px-8 h-20 border-b border-slate-800">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shadow-lg shadow-blue-500/30">
|
||||
<span class="text-white font-bold text-xl font-serif">L</span>
|
||||
</div>
|
||||
<span class="text-lg font-bold tracking-tight text-slate-800 dark:text-white uppercase italic">Intranet <span class="text-blue-600 not-italic">Ludikevent</span></span>
|
||||
<span class="text-lg font-bold tracking-tight text-white uppercase italic">Intranet <span class="text-blue-600 not-italic">Ludikevent</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-col p-6 space-y-8 h-[calc(100vh-80px)] overflow-y-auto custom-scrollbar">
|
||||
<div>
|
||||
<p class="px-4 mb-4 text-[10px] font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">Menu Principal</p>
|
||||
<p class="px-4 mb-4 text-[10px] font-semibold text-slate-500 uppercase tracking-[0.2em]">Menu Principal</p>
|
||||
<div class="space-y-1">
|
||||
{% macro nav_link(path, label, icon_svg, current_route) %}
|
||||
{% set isActive = app.request.get('_route') == current_route %}
|
||||
<a data-turbo="false" href="{{ path }}" class="flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group {{ isActive ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30' : 'hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-400' }}">
|
||||
<a data-turbo="false" href="{{ path }}" class="flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group {{ isActive ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30' : 'hover:bg-slate-800 text-slate-400' }}">
|
||||
<svg class="w-5 h-5 {{ isActive ? 'text-white' : 'text-slate-400 group-hover:text-blue-500' }}" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{ icon_svg|raw }}</svg>
|
||||
<span class="font-semibold text-sm">{{ label }}</span>
|
||||
</a>
|
||||
@@ -44,17 +43,15 @@
|
||||
{{ menu.nav_link(path('app_crm_reservation'), 'Planing de réservation', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_reservation') }}
|
||||
{{ menu.nav_link(path('app_crm_product'), 'Produits', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_product') }}
|
||||
{{ menu.nav_link(path('app_crm_formules'), 'Formules', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_formules') }}
|
||||
{# {{ menu.nav_link(path('app_crm_contrats'), 'Contrat de location', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_contrats') }}#}
|
||||
{{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_facture') }}
|
||||
{# {{ menu.nav_link(path('app_crm_devis'), 'Devis', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_devis') }}#}
|
||||
{{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_clients') }}
|
||||
{{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>', 'app_crm_facture') }}
|
||||
{{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>', 'app_clients') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="px-4 mb-4 text-[10px] font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">Configuration</p>
|
||||
<p class="px-4 mb-4 text-[10px] font-semibold text-slate-500 uppercase tracking-[0.2em]">Configuration</p>
|
||||
<details class="group" {{ (app.request.get('_route') matches '/^app_crm_administrateur/' or app.request.get('_route') == 'app_crm_audit_logs') ? 'open' }}>
|
||||
<summary class="list-none w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-400 transition-all duration-200 cursor-pointer">
|
||||
<summary class="list-none w-full flex items-center justify-between px-4 py-3 rounded-xl hover:bg-slate-800 text-slate-400 transition-all duration-200 cursor-pointer">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-slate-400 group-hover:text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 002.572-1.065z"></path></svg>
|
||||
<span class="font-semibold text-sm">Paramètres</span>
|
||||
@@ -62,9 +59,9 @@
|
||||
<svg class="w-4 h-4 transition-transform duration-300 arrow-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</summary>
|
||||
<div class="mt-2 space-y-1">
|
||||
<a data-turbo="false" href="{{ path('app_crm_administrateur') }}" class="block px-12 py-2 text-sm hover:text-blue-600 transition-colors">Gestion Admins</a>
|
||||
<a data-turbo="false" href="{{ path('app_crm_audit_logs') }}" class="block px-12 py-2 text-sm hover:text-blue-600 transition-colors">Audit Logs</a>
|
||||
<a data-turbo="false" href="{{ path('app_crm_backup') }}" class="block px-12 py-2 text-sm hover:text-blue-600 transition-colors">Sauvegarde</a>
|
||||
<a data-turbo="false" href="{{ path('app_crm_administrateur') }}" class="block px-12 py-2 text-sm text-slate-400 hover:text-blue-600 transition-colors">Gestion Admins</a>
|
||||
<a data-turbo="false" href="{{ path('app_crm_audit_logs') }}" class="block px-12 py-2 text-sm text-slate-400 hover:text-blue-600 transition-colors">Audit Logs</a>
|
||||
<a data-turbo="false" href="{{ path('app_crm_backup') }}" class="block px-12 py-2 text-sm text-slate-400 hover:text-blue-600 transition-colors">Sauvegarde</a>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -72,45 +69,36 @@
|
||||
</aside>
|
||||
|
||||
{# MAIN CONTENT #}
|
||||
<main class="flex-1 flex flex-col min-w-0 lg:ml-72 bg-slate-50 dark:bg-[#0f172a] h-screen overflow-y-auto custom-scrollbar relative">
|
||||
<main class="flex-1 flex flex-col min-w-0 lg:ml-72 bg-[#0f172a] h-screen overflow-y-auto custom-scrollbar relative">
|
||||
|
||||
{# HEADER #}
|
||||
{# HEADER CORRIGÉ #}
|
||||
<header class="h-20 flex items-center justify-between px-8 bg-white dark:bg-[#1e293b] border-b border-slate-200 dark:border-slate-800 sticky top-0 z-30 gap-6">
|
||||
<header class="h-20 flex items-center justify-between px-8 bg-[#1e293b] border-b border-slate-800 sticky top-0 z-30 gap-6">
|
||||
|
||||
{# Barre de recherche parfaitement alignée #}
|
||||
<div class="flex-1 max-w-2xl relative">
|
||||
<form action="{{ path('app_crm_search') }}" method="GET" class="flex items-center">
|
||||
|
||||
{# Input avec padding droit suffisant pour le bouton #}
|
||||
<input required type="text" name="q" placeholder="Recherche rapide..."
|
||||
class="w-full pl-2 py-3 bg-slate-50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-700 rounded-2xl text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all outline-none text-slate-700 dark:text-slate-200">
|
||||
|
||||
{# Bouton ancré à droite SANS chevauchement #}
|
||||
class="w-full pl-4 py-3 bg-slate-900/50 border border-slate-700 rounded-2xl text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all outline-none text-slate-200">
|
||||
<button type="submit" class="ml-2 px-5 py-2 bg-blue-600 text-white text-[10px] font-bold uppercase tracking-widest rounded-xl hover:bg-blue-700 transition-colors shadow-md">
|
||||
Chercher
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Profil utilisateur #}
|
||||
<div class="flex items-center gap-2">
|
||||
{# Zone Profil cliquable #}
|
||||
<a data-turbo="false" href="{{ path('app_crm_profils') }}"
|
||||
class="flex items-center space-x-3 px-4 py-2 bg-slate-50 dark:bg-slate-900/50 rounded-2xl border border-slate-200 dark:border-slate-700 shrink-0 hover:bg-slate-100 dark:hover:bg-slate-800 transition-all group">
|
||||
<a data-turbo="false" href="{{ path('app_crm_profils') }}"
|
||||
class="flex items-center space-x-3 px-4 py-2 bg-slate-900/50 rounded-2xl border border-slate-700 shrink-0 hover:bg-slate-800 transition-all group">
|
||||
|
||||
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-bold text-xs shadow-lg shadow-blue-500/20 group-hover:scale-105 transition-transform">
|
||||
{{ app.user.firstName|first|upper }}
|
||||
</div>
|
||||
|
||||
<div class="text-left hidden sm:block">
|
||||
<p class="text-xs font-bold text-slate-800 dark:text-white leading-none mb-0.5">{{ app.user.firstName }}</p>
|
||||
<p class="text-xs font-bold text-white leading-none mb-0.5">{{ app.user.firstName }}</p>
|
||||
<span class="text-[8px] text-blue-500 font-bold uppercase tracking-widest opacity-70 group-hover:opacity-100 transition-opacity">Mon Compte</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{# Bouton Déconnexion séparé #}
|
||||
<a data-turbo="false" href="{{ path('app_logout') }}"
|
||||
<a data-turbo="false" href="{{ path('app_logout') }}"
|
||||
title="Déconnexion"
|
||||
class="w-10 h-10 flex items-center justify-center bg-red-500/10 hover:bg-red-500 text-red-500 hover:text-white border border-red-500/20 rounded-xl transition-all duration-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -122,10 +110,10 @@
|
||||
|
||||
{# CONTENT #}
|
||||
<div class="p-6 md:p-10 page-transition w-full">
|
||||
<div class="flex items-end justify-between mb-10 pb-8 border-b border-slate-200 dark:border-slate-800/50">
|
||||
<div class="flex items-end justify-between mb-10 pb-8 border-b border-slate-800/50">
|
||||
<div>
|
||||
<p class="text-blue-600 font-bold text-[10px] uppercase tracking-[0.4em] mb-2">Ludikevent Intranet</p>
|
||||
<h1 class="text-4xl font-extrabold text-slate-900 dark:text-white">
|
||||
<h1 class="text-4xl font-extrabold text-white">
|
||||
{% block title_header %}{{ block('title') }}{% endblock %}
|
||||
</h1>
|
||||
</div>
|
||||
@@ -136,7 +124,7 @@
|
||||
|
||||
{# MESSAGE D'ERREUR STRIPE #}
|
||||
{% if syncStripe().state == false %}
|
||||
<div class="mb-8 flex items-center p-6 backdrop-blur-xl bg-rose-500/5 border border-rose-500/20 rounded-[2rem] shadow-xl shadow-rose-500/5 animate-in fade-in slide-in-from-top-4 duration-500">
|
||||
<div class="mb-8 flex items-center p-6 backdrop-blur-xl bg-rose-500/5 border border-rose-500/20 rounded-[2rem] shadow-xl shadow-rose-500/5">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-2xl bg-rose-500/10 border border-rose-500/20 flex items-center justify-center text-rose-500 mr-5">
|
||||
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
@@ -150,34 +138,33 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# SECTION DES MESSAGES FLASH #}
|
||||
|
||||
{# FLASH MESSAGES #}
|
||||
<div class="w-full space-y-4 mb-8">
|
||||
{% for label, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
{% set bgColor = label == 'success' ? 'bg-emerald-500/5' : (label == 'error' ? 'bg-rose-500/5' : 'bg-blue-500/5') %}
|
||||
{% set borderColor = label == 'success' ? 'border-emerald-500/20' : (label == 'error' ? 'border-rose-500/20' : 'border-blue-500/20') %}
|
||||
{% set textColor = label == 'success' ? 'text-emerald-500' : (label == 'error' ? 'text-rose-500' : 'text-blue-500') %}
|
||||
{% set icon = label == 'success'
|
||||
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />'
|
||||
%}
|
||||
|
||||
<div class="flex items-center p-5 backdrop-blur-xl {{ bgColor }} border {{ borderColor }} rounded-[1.5rem] shadow-xl animate-in slide-in-from-right-4 duration-500" role="alert">
|
||||
<div class="flex items-center p-5 backdrop-blur-xl {{ bgColor }} border {{ borderColor }} rounded-[1.5rem] shadow-xl">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-xl {{ bgColor }} border {{ borderColor }} flex items-center justify-center {{ textColor }} mr-4">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{{ icon|raw }}
|
||||
{% if label == 'success' %}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{% else %}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{% endif %}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-[10px] font-black {{ textColor }} uppercase tracking-[0.2em] mb-0.5">
|
||||
{{ label == 'success' ? 'Succès' : (label == 'error' ? 'Erreur' : 'Information') }}
|
||||
</p>
|
||||
<p class="text-sm text-slate-400 font-medium">
|
||||
{{ message }}
|
||||
{{ label|capitalize }}
|
||||
</p>
|
||||
<p class="text-sm text-slate-400 font-medium">{{ message }}</p>
|
||||
</div>
|
||||
<button type="button" onclick="this.parentElement.remove()" class="ml-auto text-slate-500 hover:text-slate-300 transition-colors">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<button type="button" onclick="this.parentElement.remove()" class="ml-auto text-slate-500 hover:text-slate-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -186,37 +173,35 @@
|
||||
|
||||
<div class="w-full">
|
||||
{% block body %}{% endblock %}
|
||||
{# FOOTER GLASSMORPHISM #}
|
||||
<footer class="relative mt-auto p-6 md:p-10">
|
||||
<div class="bg-white/40 dark:bg-slate-800/40 backdrop-blur-xl border border-white/20 dark:border-slate-700/50 rounded-[2rem] p-6 shadow-xl">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4 text-center md:text-left">
|
||||
|
||||
{# Copyright & Branding #}
|
||||
{# FOOTER #}
|
||||
<footer class="relative mt-auto p-6 md:p-10">
|
||||
<div class="bg-slate-800/40 backdrop-blur-xl border border-slate-700/50 rounded-[2rem] p-6 shadow-xl">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4 text-center md:text-left">
|
||||
<div class="space-y-1">
|
||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]">Propulsé par</p>
|
||||
<p class="text-sm font-bold text-slate-800 dark:text-white">
|
||||
<p class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">Propulsé par</p>
|
||||
<p class="text-sm font-bold text-white">
|
||||
Développé par <span class="text-blue-600">SARL SITECONSEIL</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Versioning & Links #}
|
||||
<div class="flex flex-col items-center md:items-end space-y-1">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="https://www.siteconseil.fr" target="_blank" class="text-xs font-medium text-slate-500 hover:text-blue-600 transition-colors underline decoration-blue-500/30 underline-offset-4">www.siteconseil.fr</a>
|
||||
<span class="w-1 h-1 bg-slate-300 dark:bg-slate-700 rounded-full"></span>
|
||||
<span class="w-1 h-1 bg-slate-700 rounded-full"></span>
|
||||
<a href="mailto:s.com@siteconseil.fr" class="text-xs font-medium text-slate-500 hover:text-blue-600 transition-colors">s.com@siteconseil.fr</a>
|
||||
</div>
|
||||
<p class="text-[9px] font-black text-slate-400/60 uppercase tracking-widest">
|
||||
Crm Engine <span class="text-slate-500 dark:text-slate-300">1.0.0</span>
|
||||
<p class="text-[9px] font-black text-slate-500 uppercase tracking-widest">
|
||||
Crm Engine <span class="text-slate-300">1.0.0</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</main> {# Fin du main existant #}
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user