+
@@ -62,15 +64,13 @@ export default class PlaningLogestics extends HTMLElement {
-
-
-
@@ -93,7 +92,6 @@ export default class PlaningLogestics extends HTMLElement {
-
Départ / Enlèvement
@@ -106,16 +104,12 @@ export default class PlaningLogestics extends HTMLElement {
-
@@ -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; }
`;
@@ -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: `
-
${arg.event.title}
+
+ ${isSigned ? '✅' : ''} ${arg.event.title}
+
@@ -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 => `
diff --git a/src/Controller/Dashboard/ReservationController.php b/src/Controller/Dashboard/ReservationController.php
index 50cdc30..7c07a7f 100644
--- a/src/Controller/Dashboard/ReservationController.php
+++ b/src/Controller/Dashboard/ReservationController.php
@@ -3,9 +3,11 @@
namespace App\Controller\Dashboard;
use App\Entity\AuditLog;
+use App\Entity\Contrats;
use App\Logger\AppLogger;
use App\Repository\AccountRepository;
use App\Repository\AuditLogRepository;
+use App\Repository\ContratsRepository;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
@@ -32,32 +34,46 @@ class ReservationController extends AbstractController
* Endpoint pour alimenter le calendrier en JSON
*/
#[Route(path: '/crm/reservation/data', name: 'app_crm_reservation_data', methods: ['GET'])]
- public function getReservationData(): JsonResponse
+ public function getReservationData(Request $request,ContratsRepository $contratsRepository): JsonResponse
{
- /*
- $reservations = $repository->findAll(); // Adaptez avec vos critères de dates
-
+ $start = new \DateTimeImmutable($request->query->get('start'));
+ $end = new \DateTimeImmutable($request->query->get('end'));
+ /** @var Contrats[] $reservations */
+ $reservations = $contratsRepository->findBetweenDates($start, $end);
$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 =[];
+ foreach ($reservations as $reservation) {
+ if($reservation->getProductReserves()->count() >0) {
+ $events[] = [
+ 'title' => 'Contrat #'.$reservation->getNumReservation(),
+ 'start' => $reservation->getDateAt()->format(\DateTimeInterface::ATOM),
+ 'end' => $reservation->getEndAt()->format(\DateTimeInterface::ATOM),
+ 'extendedProps' => [
+ 'start' => $reservation->getDateAt()->format('d/m/Y'),
+ 'end' => $reservation->getEndAt()->format('d/m/Y'),
+ 'contractNumber' => $reservation->getCustomer()->getPhone(),
+ 'client' => $reservation->getCustomer()->getName()." ".$reservation->getCustomer()->getSurname(),
+ 'clientEmail' => $reservation->getCustomer()->getEmail(),
+ 'clientPhone' => $reservation->getCustomer()->getPhone(),
+ 'eventAdresse' => [
+ 'address' => $reservation->GetAddressEvent(),
+ 'address1' => $reservation->getAddress2Event(),
+ 'address2' => $reservation->getAddress3Event(),
+ 'zipCode' => $reservation->getZipCodeEvent(),
+ 'city' => $reservation->getZipCodeEvent(),
+ ],
+ '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);
}
}
diff --git a/src/Entity/Contrats.php b/src/Entity/Contrats.php
index 0e2f9d4..2e0da42 100644
--- a/src/Entity/Contrats.php
+++ b/src/Entity/Contrats.php
@@ -855,6 +855,23 @@ class Contrats
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;
+ }
}
diff --git a/src/Repository/ContratsRepository.php b/src/Repository/ContratsRepository.php
index 2edf493..595c126 100644
--- a/src/Repository/ContratsRepository.php
+++ b/src/Repository/ContratsRepository.php
@@ -40,4 +40,14 @@ class ContratsRepository extends ServiceEntityRepository
// ->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();
+ }
}