diff --git a/assets/admin.js b/assets/admin.js index d6fdf97..75094e4 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -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); + } + } }); }; diff --git a/assets/libs/PlaningLogestics.js b/assets/libs/PlaningLogestics.js new file mode 100644 index 0000000..4406ab0 --- /dev/null +++ b/assets/libs/PlaningLogestics.js @@ -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 = ` +
+
+ + +
+
+
+ +
+
+

Départs Jour

+

0

+
+
+ +
+
+ +
+
+

Retours Jour

+

0

+
+
+
+ + +
+
+
+
+
+ + + + + + `; + + 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: ` +
+
${arg.event.title}
+
+
+
+
+
+
+ ` + }; + }, + 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 => ` + +
+ ${s.label} +
+ `).join(''); + + modal.classList.remove('hidden'); + modal.classList.add('flex'); + } + + hideModal() { + const modal = this.querySelector('#calendar-modal'); + modal.classList.add('hidden'); + modal.classList.remove('flex'); + } +} diff --git a/package.json b/package.json index 5535919..19417cb 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "vite": "^7.3.1" }, "dependencies": { + "@fullcalendar/core": "^6.1.20", + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", "@grafikart/drop-files-element": "^1.0.9", "@hotwired/turbo": "^8.0.20", "@preact/preset-vite": "^2.10.2", diff --git a/src/Controller/Dashboard/ReservationController.php b/src/Controller/Dashboard/ReservationController.php new file mode 100644 index 0000000..50cdc30 --- /dev/null +++ b/src/Controller/Dashboard/ReservationController.php @@ -0,0 +1,65 @@ +record('VIEW', 'Affichage liste des reservations'); + return $this->render('dashboard/reservation.twig'); + } + + /** + * Endpoint pour alimenter le calendrier en JSON + */ + #[Route(path: '/crm/reservation/data', name: 'app_crm_reservation_data', methods: ['GET'])] + public function getReservationData(): JsonResponse + { + /* + $reservations = $repository->findAll(); // Adaptez avec vos critères de dates + + $events = []; + foreach ($reservations as $res) { + $events[] = [ + 'title' => $res->getLabel(), // ou $res->getProduit() + 'start' => $res->getStartAt()->format(\DateTimeInterface::ATOM), + 'end' => $res->getEndAt()->format(\DateTimeInterface::ATOM), + 'backgroundColor' => $res->getStatusColor(), // Méthode personnalisée + 'extendedProps' => [ + 'contractNumber' => $res->getContractNumber(), // Le champ que vous avez demandé + 'client' => $res->getClientName(), + 'clientEmail' => $res->getClientEmail(), + 'clientPhone' => $res->getClientPhone(), + 'eventAdresse' => $res->getAdresse(), + 'linkContrat' => $this->generateUrl('app_crm_reservation_data', ['id' => $res->getId()]), + 'caution' => $res->isCautionReceived(), + 'acompte' => $res->isAcomptePaid(), + 'solde' => $res->isSoldePaid(), + ] + ]; + }*/ + $events =[]; + return new JsonResponse($events); + } +} + + diff --git a/src/Security/RedirecListener.php b/src/Security/RedirecListener.php index 9a0c831..fc9ddee 100644 --- a/src/Security/RedirecListener.php +++ b/src/Security/RedirecListener.php @@ -6,7 +6,6 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\DependencyInjection\Attribute\Parameter; #[AsEventListener(event: ResponseEvent::class, method: 'onResponse')] class RedirecListener @@ -14,10 +13,9 @@ class RedirecListener private bool $isDev; public function __construct( - #[Parameter('kernel.debug')] bool $debug ) { // On considère être en dev si le mode debug de Symfony est activé - $this->isDev = $debug; + $this->isDev = $_ENV['APP_ENV'] == "dev"; } public function onResponse(ResponseEvent $event): void diff --git a/templates/dashboard/base.twig b/templates/dashboard/base.twig index 64b5c93..4eee348 100644 --- a/templates/dashboard/base.twig +++ b/templates/dashboard/base.twig @@ -41,8 +41,9 @@ {% import _self as menu %} {{ menu.nav_link(path('app_crm'), 'Dashboard', '', 'app_crm') }} - {{ menu.nav_link(path('app_crm_product'), 'Produits', '', 'app_clients') }} - {{ menu.nav_link(path('app_crm_formules'), 'Formules', '', 'app_clients') }} + {{ menu.nav_link(path('app_crm_reservation'), 'Planing de réservation', '', 'app_crm_reservation') }} + {{ menu.nav_link(path('app_crm_product'), 'Produits', '', 'app_crm_product') }} + {{ menu.nav_link(path('app_crm_formules'), 'Formules', '', 'app_crm_formules') }} {# {{ menu.nav_link(path('app_crm_contrats'), 'Contrat de location', '', 'app_clients') }}#} {# {{ menu.nav_link(path('app_crm_facture'), 'Facture', '', 'app_clients') }}#} {# {{ menu.nav_link(path('app_crm_devis'), 'Devis', '', 'app_clients') }}#} diff --git a/templates/dashboard/reservation.twig b/templates/dashboard/reservation.twig new file mode 100644 index 0000000..1ae5949 --- /dev/null +++ b/templates/dashboard/reservation.twig @@ -0,0 +1,7 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}Planning des Réservations{% endblock %} + +{% block body %} + +{% endblock %}