Files
ludikevent_crm/assets/libs/PlaningLogestics.js
Serreau Jovann d993a545d9 ```
 feat(Product): Ajoute la publication des produits et les périodes bloquées

Ajoute la possibilité de publier ou masquer un produit.
Permet de bloquer des périodes pour un produit.
Corrige des bugs liés à la suppression des produits du panier.
Mise à jour de l'affichage du calendrier pour les blocages.
```
2026-02-03 14:53:11 +01:00

330 lines
20 KiB
JavaScript

import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
import frLocale from '@fullcalendar/core/locales/fr';
export default class PlaningLogistics extends HTMLElement {
constructor() {
super();
this.calendar = null;
}
connectedCallback() {
this.render();
this.initCalendar();
}
render() {
this.innerHTML = `
<div class="p-4 md:p-8 bg-[#0f172a] min-h-screen font-sans text-slate-200">
<div class="mx-auto space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<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-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-[#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-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>
<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>
<div id="calendar-modal" class="fixed inset-0 z-[100] hidden items-center justify-center p-4">
<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-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>
<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-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-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-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-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-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-slate-800">
<div>
<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-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-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: #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;
}
#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 4px 6px -1px rgba(0, 0, 0, 0.2) !important;
border-radius: 12px !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: #2563eb !important;
color: white !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: 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: 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; }
</style>
`;
this.querySelector('.modal-overlay').addEventListener('click', () => this.hideModal());
this.querySelector('.close-modal').addEventListener('click', () => this.hideModal());
}
initCalendar() {
const calendarEl = this.querySelector('#calendar-root');
const loader = this.querySelector('#calendar-loading');
this.calendar = new Calendar(calendarEl, {
plugins: [ dayGridPlugin, timeGridPlugin, interactionPlugin ],
initialView: 'dayGridMonth',
locale: frLocale,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek'
},
events: (fetchInfo, successCallback, failureCallback) => {
loader.classList.remove('hidden');
const params = new URLSearchParams({ start: fetchInfo.startStr, end: fetchInfo.endStr });
fetch(`/crm/reservation/data?${params.toString()}`)
.then(response => response.json())
.then(data => {
const formattedData = data.map(item => {
let bgColor = '#2563eb';
if (item.extendedProps.type === 'blocked') {
bgColor = '#ef4444';
} else if (item.extendedProps.isSigned) {
bgColor = '#059669';
}
return {
...item,
backgroundColor: bgColor,
borderColor: bgColor
};
});
successCallback(formattedData);
this.updateStats(formattedData);
loader.classList.add('hidden');
})
.catch(error => {
loader.classList.add('hidden');
failureCallback(error);
});
},
eventContent: (arg) => {
const props = arg.event.extendedProps;
if (props.type === 'blocked') {
return {
html: `
<div class="fc-event-main-frame">
<div class="text-[10px] font-black uppercase tracking-tight leading-tight line-clamp-2 flex items-center gap-1 text-white">
${arg.event.title.replace('BLOCAGE: ', '')}
</div>
<div class="text-[9px] text-white/80 italic truncate mt-0.5">${props.reason || 'Pas de raison'}</div>
</div>
`
};
}
const { caution, acompte, solde, isSigned } = props;
return {
html: `
<div class="fc-event-main-frame">
<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 ? '#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>
`
};
},
eventClick: (info) => this.showEventDetails(info.event)
});
this.calendar.render();
}
updateStats(events) {
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';
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');
// Reset visibility
this.querySelector('#link-phone').style.display = 'flex';
this.querySelector('#link-email').style.display = 'flex';
this.querySelector('#modal-link-contrat').parentElement.style.display = 'flex';
this.querySelector('.bg-slate-900\\/50.rounded-2xl.border').style.display = 'block'; // Address container
if (props.type === 'blocked') {
this.querySelector('#modal-contract-number').innerText = 'INDISPONIBILITÉ';
this.querySelector('#modal-title').innerText = props.productName;
this.querySelector('#modal-client').innerText = props.reason || 'Aucune raison spécifiée';
this.querySelector('#modal-email').innerText = '-';
this.querySelector('#modal-phone').innerText = '-';
this.querySelector('#modal-start').innerText = props.start;
this.querySelector('#modal-end').innerText = props.end;
// Hide irrelevant sections
this.querySelector('#link-phone').style.display = 'none';
this.querySelector('#link-email').style.display = 'none';
this.querySelector('#modal-link-contrat').parentElement.style.display = 'none';
this.querySelector('.bg-slate-900\\/50.rounded-2xl.border').style.display = 'none';
modalStatus.innerHTML = '<span class="px-2.5 py-1 rounded-lg text-[9px] font-black uppercase tracking-widest bg-rose-500/10 text-rose-500 border border-rose-500/20">Blocage Matériel</span>';
modal.classList.replace('hidden', 'flex');
return;
}
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é';
this.querySelector('#modal-adresse').innerText = this.formatAddress(props.eventAdresse);
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 || '#';
const statusList = [
{ label: 'Signé', val: props.isSigned, color: 'emerald' },
{ label: 'Caution', val: props.caution, color: 'orange' },
{ label: 'Acompte', val: props.acompte, color: 'blue' },
{ label: 'Solde', val: props.solde, color: 'emerald' }
];
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.replace('hidden', 'flex');
}
hideModal() {
this.querySelector('#calendar-modal').classList.replace('flex', 'hidden');
}
}