```
✨ feat(crm/planning): Ajoute le planning logistique
Ce commit ajoute le planning logistique utilisant FullCalendar pour
visualiser les réservations. Il inclut la récupération des données
de l'API et l'affichage des détails dans une modale.
```
This commit is contained in:
@@ -130,3 +130,8 @@ details {
|
||||
@apply scale-95;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-event{
|
||||
background: rgba(177, 59, 246, 0.4) !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
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="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">
|
||||
@@ -27,24 +26,27 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
<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">Départs Jour</p>
|
||||
<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>
|
||||
</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">
|
||||
<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="M11 19l-7-7 7-7M19 19l-7-7 7-7"></path></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" 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">Retours Jour</p>
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
<div id="calendar-root"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +55,7 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
<!-- 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 animate-in fade-in zoom-in duration-200">
|
||||
<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="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>
|
||||
@@ -62,15 +64,13 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Badges Statut -->
|
||||
<div class="flex flex-wrap gap-2 mb-6" id="modal-status-container"></div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 id="modal-title" class="text-2xl font-extrabold text-gray-900 dark:text-white leading-tight mb-1"></h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Informations de contact -->
|
||||
<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>
|
||||
@@ -82,7 +82,6 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Adresse -->
|
||||
<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="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>
|
||||
@@ -93,7 +92,6 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dates -->
|
||||
<div class="grid grid-cols-2 gap-6 py-4 border-t border-gray-100 dark:border-gray-800">
|
||||
<div>
|
||||
<span class="block text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1">Départ / Enlèvement</span>
|
||||
@@ -106,16 +104,12 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<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>
|
||||
Voir Contrat
|
||||
</a>
|
||||
<button class="flex-1 bg-indigo-600 text-white py-4 rounded-2xl font-bold text-sm hover:bg-indigo-700 shadow-lg shadow-indigo-500/20 transition-all flex items-center justify-center gap-2">
|
||||
<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="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
|
||||
Éditer
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,7 +134,6 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
}
|
||||
|
||||
#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; }
|
||||
|
||||
@@ -170,11 +163,9 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
cursor: pointer;
|
||||
}
|
||||
.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; }
|
||||
.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>
|
||||
`;
|
||||
@@ -185,45 +176,7 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
|
||||
initCalendar() {
|
||||
const calendarEl = this.querySelector('#calendar-root');
|
||||
|
||||
// Données exemples intégrant contractNumber
|
||||
/*const events = [
|
||||
{
|
||||
title: 'Retrait Pack Mariage',
|
||||
start: '2026-01-30T14:00:00',
|
||||
end: '2026-02-01T12:00:00',
|
||||
backgroundColor: '#10b981',
|
||||
extendedProps: {
|
||||
contractNumber: 'CTR-2026-001',
|
||||
client: 'M. et Mme. Lefebvre',
|
||||
clientEmail: 'lefebvre.m@gmail.com',
|
||||
clientPhone: '06 12 34 56 78',
|
||||
eventAdresse: '15 Rue de la Paix, 75002 Paris',
|
||||
linkContrat: 'https://admin.votre-erp.com/contrats/12345',
|
||||
caution: true,
|
||||
acompte: true,
|
||||
solde: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Livraison Podium Sonorisé',
|
||||
start: '2026-01-29T08:00:00',
|
||||
end: '2026-01-29T18:00:00',
|
||||
backgroundColor: '#4f46e5',
|
||||
extendedProps: {
|
||||
contractNumber: 'CTR-2026-042',
|
||||
client: 'Mairie de Nanterre',
|
||||
clientEmail: 'logistique@nanterre.fr',
|
||||
clientPhone: '01 47 29 50 00',
|
||||
eventAdresse: 'Place Gabriel Péri, 92000 Nanterre',
|
||||
linkContrat: 'https://admin.votre-erp.com/contrats/98765',
|
||||
caution: true,
|
||||
acompte: true,
|
||||
solde: false
|
||||
}
|
||||
}
|
||||
];*/
|
||||
const events = {};
|
||||
const loader = this.querySelector('#calendar-loading');
|
||||
|
||||
this.calendar = new Calendar(calendarEl, {
|
||||
plugins: [ dayGridPlugin, timeGridPlugin, interactionPlugin ],
|
||||
@@ -234,16 +187,49 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek'
|
||||
},
|
||||
events: events,
|
||||
|
||||
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 => {
|
||||
if (!response.ok) throw new Error('Erreur réseau');
|
||||
return 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'
|
||||
}));
|
||||
|
||||
successCallback(formattedData);
|
||||
this.updateStats(formattedData);
|
||||
loader.classList.add('hidden');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Erreur chargement:", error);
|
||||
failureCallback(error);
|
||||
loader.classList.add('hidden');
|
||||
});
|
||||
},
|
||||
|
||||
eventContent: (arg) => {
|
||||
const { caution, acompte, solde } = arg.event.extendedProps;
|
||||
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">${arg.event.title}</div>
|
||||
<div class="text-[11px] font-bold leading-tight line-clamp-1 flex items-center gap-1">
|
||||
${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 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4z"></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>
|
||||
</div>
|
||||
@@ -254,50 +240,53 @@ export default class PlaningLogestics extends HTMLElement {
|
||||
});
|
||||
|
||||
this.calendar.render();
|
||||
this.updateStats(events);
|
||||
}
|
||||
|
||||
updateStats(events) {
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
const departs = events.filter(e => e.start.split('T')[0] === todayStr).length;
|
||||
const retours = events.filter(e => e.end && e.end.split('T')[0] === todayStr).length;
|
||||
const total = events.length;
|
||||
const signed = events.filter(e => e.extendedProps && e.extendedProps.isSigned).length;
|
||||
|
||||
this.querySelector('#stat-departs').innerText = departs;
|
||||
this.querySelector('#stat-retours').innerText = retours;
|
||||
this.querySelector('#stat-departs').innerText = total;
|
||||
this.querySelector('#stat-retours').innerText = signed;
|
||||
}
|
||||
|
||||
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(', ');
|
||||
}
|
||||
|
||||
showEventDetails(event) {
|
||||
const modal = this.querySelector('#calendar-modal');
|
||||
const props = event.extendedProps;
|
||||
|
||||
// Mise à jour des informations contractuelles
|
||||
this.querySelector('#modal-contract-number').innerText = props.contractNumber || 'SANS NUMÉRO';
|
||||
this.querySelector('#modal-contract-number').innerText = `Contrat n°${props.contractNumber || '?'}`;
|
||||
this.querySelector('#modal-title').innerText = event.title;
|
||||
this.querySelector('#modal-client').innerText = props.client;
|
||||
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 = props.eventAdresse || 'Adresse non spécifiée';
|
||||
|
||||
// Formatage des dates
|
||||
// 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 = event.start.toLocaleString('fr-FR', dateOptions);
|
||||
this.querySelector('#modal-end').innerText = event.end ? event.end.toLocaleString('fr-FR', dateOptions) : '--';
|
||||
|
||||
// Mise à jour des liens interactifs
|
||||
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 || '#';
|
||||
|
||||
// Couleur thématique
|
||||
this.querySelector('#modal-contract-number').style.borderColor = event.backgroundColor;
|
||||
this.querySelector('#modal-contract-number').style.color = event.backgroundColor;
|
||||
|
||||
// Génération dynamique des badges de statut
|
||||
const statusContainer = this.querySelector('#modal-status-container');
|
||||
const statusList = [
|
||||
{ label: 'Caution', val: props.caution, color: 'orange', icon: 'M4 4a2 2 0 00-2 2v1h16V6a2 2 0 00-2-2H4z' },
|
||||
{ label: 'Acompte', val: props.acompte, color: 'blue', icon: 'M4 4a2 2 0 002 2V6h10a2 2 0 00-2-2H4z' },
|
||||
{ label: 'Solde', val: props.solde, color: 'emerald', icon: 'M5 13l4 4L19 7' }
|
||||
{ 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' }
|
||||
];
|
||||
|
||||
statusContainer.innerHTML = statusList.map(s => `
|
||||
|
||||
Reference in New Issue
Block a user