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:
Serreau Jovann
2026-01-29 14:23:58 +01:00
parent 85afa1b31b
commit 21ecf299e5
5 changed files with 143 additions and 106 deletions

View File

@@ -130,3 +130,8 @@ details {
@apply scale-95; @apply scale-95;
} }
} }
.fc-event{
background: rgba(177, 59, 246, 0.4) !important;
display: block !important;
}

View File

@@ -19,7 +19,6 @@ export default class PlaningLogestics extends HTMLElement {
this.innerHTML = ` 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-gray-50 dark:bg-gray-950 min-h-screen transition-colors duration-300 font-sans">
<div class="mx-auto space-y-6"> <div class="mx-auto space-y-6">
<!-- En-tête Statistiques --> <!-- En-tête Statistiques -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <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="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> <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>
<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> <p class="text-2xl font-black text-gray-900 dark:text-white" id="stat-departs">0</p>
</div> </div>
</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="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="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>
<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> <p class="text-2xl font-black text-gray-900 dark:text-white" id="stat-retours">0</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Calendrier Principal --> <!-- 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 id="calendar-root"></div>
</div> </div>
</div> </div>
@@ -53,7 +55,7 @@ export default class PlaningLogestics extends HTMLElement {
<!-- Modale de Détails --> <!-- Modale de Détails -->
<div id="calendar-modal" class="fixed inset-0 z-[100] hidden items-center justify-center p-4"> <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="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="p-6 md:p-8">
<div class="flex justify-between items-start mb-4"> <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> <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> </button>
</div> </div>
<!-- Badges Statut -->
<div class="flex flex-wrap gap-2 mb-6" id="modal-status-container"></div> <div class="flex flex-wrap gap-2 mb-6" id="modal-status-container"></div>
<div class="mb-6"> <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> <p id="modal-client" class="text-indigo-600 dark:text-indigo-400 font-bold text-lg"></p>
</div> </div>
<!-- Informations de contact -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> <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"> <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> <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> </a>
</div> </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="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="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 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>
</div> </div>
<!-- Dates -->
<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-gray-100 dark:border-gray-800">
<div> <div>
<span class="block text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1">Départ / Enlèvement</span> <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>
</div> </div>
<!-- Actions -->
<div class="p-6 bg-gray-50 dark:bg-gray-800/50 flex gap-3"> <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"> <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> <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 Voir Contrat
</a> </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> </div>
</div> </div>
@@ -140,7 +134,6 @@ export default class PlaningLogestics extends HTMLElement {
} }
#calendar-root { min-height: 750px; } #calendar-root { min-height: 750px; }
.fc-toolbar-title { font-weight: 900 !important; color: inherit; font-size: 1.25rem !important; } .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; } .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; 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: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; } .fc-event-main-frame { padding: 4px; }
.status-indicators { display: flex; gap: 3px; margin-top: 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; } .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; } .dark .fc-theme-standard td, .dark .fc-theme-standard th { border-color: #1f2937 !important; }
</style> </style>
`; `;
@@ -185,45 +176,7 @@ export default class PlaningLogestics extends HTMLElement {
initCalendar() { initCalendar() {
const calendarEl = this.querySelector('#calendar-root'); const calendarEl = this.querySelector('#calendar-root');
const loader = this.querySelector('#calendar-loading');
// 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 = {};
this.calendar = new Calendar(calendarEl, { this.calendar = new Calendar(calendarEl, {
plugins: [ dayGridPlugin, timeGridPlugin, interactionPlugin ], plugins: [ dayGridPlugin, timeGridPlugin, interactionPlugin ],
@@ -234,16 +187,49 @@ export default class PlaningLogestics extends HTMLElement {
center: 'title', center: 'title',
right: 'dayGridMonth,timeGridWeek' 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) => { eventContent: (arg) => {
const { caution, acompte, solde } = arg.event.extendedProps; const { caution, acompte, solde, isSigned } = arg.event.extendedProps;
return { return {
html: ` html: `
<div class="fc-event-main-frame"> <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-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:${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 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>
</div> </div>
@@ -254,50 +240,53 @@ export default class PlaningLogestics extends HTMLElement {
}); });
this.calendar.render(); this.calendar.render();
this.updateStats(events);
} }
updateStats(events) { updateStats(events) {
const todayStr = new Date().toISOString().split('T')[0]; const total = events.length;
const departs = events.filter(e => e.start.split('T')[0] === todayStr).length; const signed = events.filter(e => e.extendedProps && e.extendedProps.isSigned).length;
const retours = events.filter(e => e.end && e.end.split('T')[0] === todayStr).length;
this.querySelector('#stat-departs').innerText = departs; this.querySelector('#stat-departs').innerText = total;
this.querySelector('#stat-retours').innerText = retours; 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) { showEventDetails(event) {
const modal = this.querySelector('#calendar-modal'); const modal = this.querySelector('#calendar-modal');
const props = event.extendedProps; const props = event.extendedProps;
// Mise à jour des informations contractuelles this.querySelector('#modal-contract-number').innerText = `Contrat n°${props.contractNumber || '?'}`;
this.querySelector('#modal-contract-number').innerText = props.contractNumber || 'SANS NUMÉRO';
this.querySelector('#modal-title').innerText = event.title; 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-email').innerText = props.clientEmail || 'Non renseigné';
this.querySelector('#modal-phone').innerText = props.clientPhone || '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' }; 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-phone').href = `tel:${props.clientPhone}`;
this.querySelector('#link-email').href = `mailto:${props.clientEmail}`; this.querySelector('#link-email').href = `mailto:${props.clientEmail}`;
this.querySelector('#modal-link-contrat').href = props.linkContrat || '#'; 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.borderColor = event.backgroundColor;
this.querySelector('#modal-contract-number').style.color = 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 statusContainer = this.querySelector('#modal-status-container');
const statusList = [ const statusList = [
{ label: 'Caution', val: props.caution, color: 'orange', icon: 'M4 4a2 2 0 00-2 2v1h16V6a2 2 0 00-2-2H4z' }, { label: 'Signé', val: props.isSigned, color: 'emerald' },
{ label: 'Acompte', val: props.acompte, color: 'blue', icon: 'M4 4a2 2 0 002 2V6h10a2 2 0 00-2-2H4z' }, { label: 'Caution', val: props.caution, color: 'orange' },
{ label: 'Solde', val: props.solde, color: 'emerald', icon: 'M5 13l4 4L19 7' } { label: 'Acompte', val: props.acompte, color: 'blue' },
{ label: 'Solde', val: props.solde, color: 'emerald' }
]; ];
statusContainer.innerHTML = statusList.map(s => ` statusContainer.innerHTML = statusList.map(s => `

View File

@@ -3,9 +3,11 @@
namespace App\Controller\Dashboard; namespace App\Controller\Dashboard;
use App\Entity\AuditLog; use App\Entity\AuditLog;
use App\Entity\Contrats;
use App\Logger\AppLogger; use App\Logger\AppLogger;
use App\Repository\AccountRepository; use App\Repository\AccountRepository;
use App\Repository\AuditLogRepository; use App\Repository\AuditLogRepository;
use App\Repository\ContratsRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface; use Knp\Component\Pager\PaginatorInterface;
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Spreadsheet;
@@ -32,32 +34,46 @@ class ReservationController extends AbstractController
* Endpoint pour alimenter le calendrier en JSON * Endpoint pour alimenter le calendrier en JSON
*/ */
#[Route(path: '/crm/reservation/data', name: 'app_crm_reservation_data', methods: ['GET'])] #[Route(path: '/crm/reservation/data', name: 'app_crm_reservation_data', methods: ['GET'])]
public function getReservationData(): JsonResponse public function getReservationData(Request $request,ContratsRepository $contratsRepository): JsonResponse
{ {
/* $start = new \DateTimeImmutable($request->query->get('start'));
$reservations = $repository->findAll(); // Adaptez avec vos critères de dates $end = new \DateTimeImmutable($request->query->get('end'));
/** @var Contrats[] $reservations */
$reservations = $contratsRepository->findBetweenDates($start, $end);
$events = []; $events = [];
foreach ($reservations as $res) { foreach ($reservations as $reservation) {
$events[] = [ if($reservation->getProductReserves()->count() >0) {
'title' => $res->getLabel(), // ou $res->getProduit() $events[] = [
'start' => $res->getStartAt()->format(\DateTimeInterface::ATOM), 'title' => 'Contrat #'.$reservation->getNumReservation(),
'end' => $res->getEndAt()->format(\DateTimeInterface::ATOM), 'start' => $reservation->getDateAt()->format(\DateTimeInterface::ATOM),
'backgroundColor' => $res->getStatusColor(), // Méthode personnalisée 'end' => $reservation->getEndAt()->format(\DateTimeInterface::ATOM),
'extendedProps' => [ 'extendedProps' => [
'contractNumber' => $res->getContractNumber(), // Le champ que vous avez demandé 'start' => $reservation->getDateAt()->format('d/m/Y'),
'client' => $res->getClientName(), 'end' => $reservation->getEndAt()->format('d/m/Y'),
'clientEmail' => $res->getClientEmail(), 'contractNumber' => $reservation->getCustomer()->getPhone(),
'clientPhone' => $res->getClientPhone(), 'client' => $reservation->getCustomer()->getName()." ".$reservation->getCustomer()->getSurname(),
'eventAdresse' => $res->getAdresse(), 'clientEmail' => $reservation->getCustomer()->getEmail(),
'linkContrat' => $this->generateUrl('app_crm_reservation_data', ['id' => $res->getId()]), 'clientPhone' => $reservation->getCustomer()->getPhone(),
'caution' => $res->isCautionReceived(), 'eventAdresse' => [
'acompte' => $res->isAcomptePaid(), 'address' => $reservation->GetAddressEvent(),
'solde' => $res->isSoldePaid(), 'address1' => $reservation->getAddress2Event(),
] 'address2' => $reservation->getAddress3Event(),
]; 'zipCode' => $reservation->getZipCodeEvent(),
}*/ 'city' => $reservation->getZipCodeEvent(),
$events =[]; ],
'linkContrat' => $this->generateUrl('app_crm_reservation_data', ['id' => $reservation->getId()]),
'caution' => $reservation->isCaution(),
'acompte' => $reservation->isAccompte(),
'solde' => $reservation->isSolde(),
'isSigned' => $reservation->isSigned(),
]
];
}
}
return new JsonResponse($events); return new JsonResponse($events);
} }
} }

View File

@@ -855,6 +855,23 @@ class Contrats
return $this; return $this;
} }
public function isCaution()
{
return $this->contratsPayments->filter(function (ContratsPayments $contratsPayments){
return $contratsPayments->getType() == "caution" && $contratsPayments->getState() == "complete";
})->first() instanceof ContratsPayments;
}
public function isAccompte()
{
return $this->contratsPayments->filter(function (ContratsPayments $contratsPayments){
return $contratsPayments->getType() == "accompte" && $contratsPayments->getState() == "complete";
})->first() instanceof ContratsPayments;
}
public function isSolde()
{
return $this->contratsPayments->filter(function (ContratsPayments $contratsPayments){
return $contratsPayments->getType() == "solde" && $contratsPayments->getState() == "complete";
})->first() instanceof ContratsPayments;
}
} }

View File

@@ -40,4 +40,14 @@ class ContratsRepository extends ServiceEntityRepository
// ->getOneOrNullResult() // ->getOneOrNullResult()
// ; // ;
// } // }
public function findBetweenDates(\DateTimeImmutable $start, \DateTimeImmutable $end)
{
return $this->createQueryBuilder('c')
->andWhere('c.dateAt > :start')
->andWhere('c.dateAt < :end')
->setParameter('start', $start)
->setParameter('end', $end)
->getQuery()
->getResult();
}
} }