feat(Security/Listener): Utilise APP_ENV pour le mode dev.

 feat(assets/admin.js): Ajoute le composant PlaningLogestics.

 feat(assets/libs): Crée composant PlaningLogestics pour calendrier.

 feat(package.json): Ajoute les dépendances FullCalendar.

 feat(templates/base.twig): Ajoute lien vers le planing de réservation.

 feat(src/Controller): Crée controller Reservation pour le planning.

 feat(templates): Crée template pour le planning de réservation.
```
This commit is contained in:
Serreau Jovann
2026-01-29 09:31:02 +01:00
parent 03d96ff194
commit 61fe970aa6
7 changed files with 407 additions and 6 deletions

View File

@@ -9,6 +9,7 @@ import { initTomSelect } from "./libs/initTomSelect.js";
import { SearchProduct, SearchOptions } from "./libs/SearchProduct.js";
import { SearchProductDevis, SearchOptionsDevis } from "./libs/SearchProductDevis.js";
import { SearchProductFormule, SearchOptionsFormule } from "./libs/SearchProductFormule.js";
import PlaningLogestics from "./libs/PlaningLogestics.js";
// --- CONFIGURATION SENTRY ---
Sentry.init({
@@ -30,6 +31,7 @@ const registerCustomElements = () => {
{ name: 'search-product', class: SearchProduct, extends: 'button' },
{ name: 'search-productformule', class: SearchProductFormule, extends: 'button' },
{ name: 'search-optionsformule', class: SearchOptionsFormule, extends: 'button' },
{ name: 'planing-logestics', class: PlaningLogestics },
{ name: 'search-options', class: SearchOptions, extends: 'button' },
{ name: 'search-productdevis', class: SearchProductDevis, extends: 'button' },
{ name: 'search-optionsdevis', class: SearchOptionsDevis, extends: 'button' },
@@ -38,7 +40,12 @@ const registerCustomElements = () => {
elements.forEach(el => {
if (!customElements.get(el.name)) {
customElements.define(el.name, el.class, { extends: el.extends });
if(el.extends != undefined) {
customElements.define(el.name, el.class, { extends: el.extends });
} else {
customElements.define(el.name, el.class);
}
}
});
};

View File

@@ -0,0 +1,319 @@
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 PlaningLogestics extends HTMLElement {
constructor() {
super();
this.calendar = null;
}
connectedCallback() {
this.render();
this.initCalendar();
}
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="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">
<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-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>
</div>
<div>
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Retours Jour</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 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 animate-in fade-in zoom-in duration-200">
<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">
<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>
<!-- 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>
<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>
<span id="modal-phone" class="text-sm font-medium text-gray-700 dark:text-gray-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>
</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>
<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>
</div>
</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>
<span id="modal-start" class="text-sm text-gray-900 dark: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>
</div>
</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>
<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);
}
@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; }
.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;
border-radius: 12px !important;
font-weight: 700 !important;
text-transform: capitalize !important;
padding: 8px 16px !important;
}
.fc .fc-button-active {
background: #4f46e5 !important;
color: white !important;
border-color: #4f46e5 !important;
}
.fc-event {
border: none !important;
border-radius: 10px !important;
padding: 4px !important;
margin: 1px 2px !important;
transition: all 0.2s ease;
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>
`;
this.querySelector('.modal-overlay').addEventListener('click', () => this.hideModal());
this.querySelector('.close-modal').addEventListener('click', () => this.hideModal());
}
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 = {};
this.calendar = new Calendar(calendarEl, {
plugins: [ dayGridPlugin, timeGridPlugin, interactionPlugin ],
initialView: 'dayGridMonth',
locale: frLocale,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek'
},
events: events,
eventContent: (arg) => {
const { caution, acompte, solde } = 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="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:${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>
`
};
},
eventClick: (info) => this.showEventDetails(info.event)
});
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;
this.querySelector('#stat-departs').innerText = departs;
this.querySelector('#stat-retours').innerText = retours;
}
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-title').innerText = event.title;
this.querySelector('#modal-client').innerText = props.client;
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
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('#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' }
];
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>
${s.label}
</span>
`).join('');
modal.classList.remove('hidden');
modal.classList.add('flex');
}
hideModal() {
const modal = this.querySelector('#calendar-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}