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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lieu de l'événement
+
+
+
+
+
+
+
+
+ Départ / Enlèvement
+
+
+
+ Retour prévu
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ 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: `
+
+ `
+ };
+ },
+ 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 %}