From 4c14932fee8056775371cd6635103c0a5351342b Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Mon, 19 Jan 2026 13:52:41 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(Devis.php):=20Ajoute=20a?= =?UTF-8?q?dresses=20de=20facturation=20et=20de=20livraison=20au=20devis.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit đŸ”’ïž fix(IntranetLocked.php): Autorise l'accĂšs Ă  la route st_control en mode debug. ✹ feat(CustomerAddress.php): GĂšre les adresses de facturation et livraison. ✹ feat: Ajoute la console superadmin pour le contrĂŽle systĂšme. ✹ feat(DevisController.php): Supprime la gĂ©nĂ©ration PDF temporaire. ✹ feat(st_control.js): Ajoute la logique de contrĂŽle systĂšme via JS. ✹ feat: CrĂ©e les templates CGV, Cookies, HĂ©bergement et RGPD. 🎹 style(app.scss): Ajoute un style de fond pour la console. ✹ feat: Ajoute le template pour les informations d'hĂ©bergement. ✹ feat: CrĂ©e un template de mail d'alerte pour les accĂšs root. ✹ feat: CrĂ©e le template RGPD (donnĂ©es personnelles). 🐛 fix(ErrorListener.php): GĂšre les erreurs 404 en prod (JSON/HTML). ✹ feat: Ajoute les mentions lĂ©gales. ✹ feat(DevisPdfService.php): AmĂ©liore la gĂ©nĂ©ration PDF du devis. ✹ feat(admin.js): Charge dynamiquement les produits dans le select. ✹ feat(add.twig): Ajoute un sĂ©lecteur de produit et d'autres champs. ✅ chore(config): Ajoute INTRANET_LOCK Ă  l'env. ``` --- .env | 1 + assets/admin.js | 34 ++- assets/app.scss | 4 + config/packages/framework.yaml | 2 +- migrations/Version20260119103900.php | 42 +++ public/st_control.js | 78 +++++ src/Controller/Dashboard/DevisController.php | 6 +- .../Dashboard/ProductController.php | 4 + src/Controller/LegalController.php | 21 +- src/Controller/StController.php | 150 ++++++++++ src/Entity/CustomerAddress.php | 80 +++++ src/Entity/Devis.php | 30 ++ src/Security/ErrorListener.php | 54 ++++ src/Security/IntranetLocked.php | 2 +- src/Service/Pdf/DevisPdfService.php | 282 +++++++++++++----- templates/base.twig | 2 +- templates/dashboard/devis/add.twig | 29 +- templates/error/404.twig | 34 +++ templates/legal/cgv.html.twig | 141 +++++++++ templates/legal/cookies.html.twig | 257 ++++++++++++++++ templates/legal/hebergement.html.twig | 148 +++++++++ templates/legal/mentions.html.twig | 139 ++++++++- templates/legal/rgpd.html.twig | 106 +++++++ templates/mails/root/alert.twig | 56 ++++ templates/root/console.twig | 159 ++++++++++ 25 files changed, 1744 insertions(+), 117 deletions(-) create mode 100644 migrations/Version20260119103900.php create mode 100644 public/st_control.js create mode 100644 src/Controller/StController.php create mode 100644 src/Security/ErrorListener.php create mode 100644 templates/error/404.twig create mode 100644 templates/legal/cgv.html.twig create mode 100644 templates/legal/cookies.html.twig create mode 100644 templates/legal/hebergement.html.twig create mode 100644 templates/legal/rgpd.html.twig create mode 100644 templates/mails/root/alert.twig create mode 100644 templates/root/console.twig diff --git a/.env b/.env index 71fa52e..866689e 100644 --- a/.env +++ b/.env @@ -97,3 +97,4 @@ ESY_SEARCH_KEY=b09d9a708b427d495c39fe6e8fc5361fe33fee57a0435f3e1bf3ed8155f2a277 STRIPE_SECRET_KEY=sk_test_*** ###< stripe/stripe-php ### INTRANET_LOCK=true +TVA_ENABLED=false diff --git a/assets/admin.js b/assets/admin.js index c40e8c6..25c7f75 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -30,22 +30,30 @@ function initAdminLayout() { } document.querySelectorAll('select').forEach((el) => { if (!el.tomselect) { // Éviter la double initialisation avec Turbo - new TomSelect(el, { - controlInput: null, - allowEmptyOption: true, - highlight: true, - plugins: ['dropdown_input'], // Permet d'avoir la recherche dans le dropdown - render: { - option: function(data, escape) { - return `
+ if(el.getAttribute('data-load') == "product") { + fetch("/crm/product/json") + .then(r=>r.json()) + .then(products=>{ + + }) + } else { + new TomSelect(el, { + controlInput: null, + allowEmptyOption: true, + highlight: true, + plugins: ['dropdown_input'], // Permet d'avoir la recherche dans le dropdown + render: { + option: function (data, escape) { + return `
${escape(data.text)}
`; - }, - item: function(data, escape) { - return `
${escape(data.text)}
`; + }, + item: function (data, escape) { + return `
${escape(data.text)}
`; + } } - } - }); + }); + } } }); const imageInput = document.getElementById('product_image_input'); diff --git a/assets/app.scss b/assets/app.scss index 3d552a6..c0e2f3a 100644 --- a/assets/app.scss +++ b/assets/app.scss @@ -1,2 +1,6 @@ @import "tailwindcss"; + +.bg-console{ + background: var(--color-slate-600); +} diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 7713fb2..3669172 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -14,7 +14,7 @@ framework: #esi: true #fragments: true - trusted_proxies: '103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,104.16.0.0/13,104.24.0.0/14,108.162.192.0/18,131.0.72.0/22,141.101.64.0/18,162.158.0.0/15,172.64.0.0/13,173.245.48.0/20,188.114.96.0/20,190.93.240.0/20,197.234.240.0/22,198.41.128.0/17,REMOTE_ADDR' + trusted_proxies: '103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,104.16.0.0/13,104.24.0.0/14,108.162.192.0/18,131.0.72.0/22,141.101.64.0/18,162.158.0.0/15,172.64.0.0/13,173.245.48.0/20,188.114.96.0/20,190.93.240.0/20,197.234.240.0/22,198.41.128.0/17' trusted_headers: [ 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix' ] when@test: diff --git a/migrations/Version20260119103900.php b/migrations/Version20260119103900.php new file mode 100644 index 0000000..3ff6264 --- /dev/null +++ b/migrations/Version20260119103900.php @@ -0,0 +1,42 @@ +addSql('ALTER TABLE devis ADD address_ship_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE devis ADD bill_address_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE devis ADD CONSTRAINT FK_8B27C52B99B3C6E5 FOREIGN KEY (address_ship_id) REFERENCES customer_address (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE devis ADD CONSTRAINT FK_8B27C52B5B8A2B31 FOREIGN KEY (bill_address_id) REFERENCES customer_address (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_8B27C52B99B3C6E5 ON devis (address_ship_id)'); + $this->addSql('CREATE INDEX IDX_8B27C52B5B8A2B31 ON devis (bill_address_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE devis DROP CONSTRAINT FK_8B27C52B99B3C6E5'); + $this->addSql('ALTER TABLE devis DROP CONSTRAINT FK_8B27C52B5B8A2B31'); + $this->addSql('DROP INDEX IDX_8B27C52B99B3C6E5'); + $this->addSql('DROP INDEX IDX_8B27C52B5B8A2B31'); + $this->addSql('ALTER TABLE devis DROP address_ship_id'); + $this->addSql('ALTER TABLE devis DROP bill_address_id'); + } +} diff --git a/public/st_control.js b/public/st_control.js new file mode 100644 index 0000000..f70bcce --- /dev/null +++ b/public/st_control.js @@ -0,0 +1,78 @@ +document.addEventListener('DOMContentLoaded', () => { + const btnSuspend = document.getElementById('btn-suspend'); + const btnEnable = document.getElementById('btn-enable'); + const terminal = document.getElementById('terminal-logs'); + + const appendLog = (message, colorClass) => { + const now = new Date().toLocaleTimeString('fr-FR'); + const p = document.createElement('p'); + p.className = `${colorClass} font-bold mt-2`; + p.innerHTML = `[${now}] ${message}`; + terminal.appendChild(p); + terminal.scrollTop = terminal.scrollHeight; + }; + + const sendAction = (disableValue, logMessage, color) => { + const urlParams = new URLSearchParams(window.location.search); + const secret = urlParams.get('secret'); + + fetch(`${window.location.pathname}?secret=${secret}&disable=${disableValue}`, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(response => response.json()) + .then(data => { + // Ligne d'action principale + appendLog(`>> EXEC: ${logMessage}`, color); + + // Ligne de confirmation d'alerte + setTimeout(() => { + appendLog(`[SYSTEM] SECURITY ALERT SENT TO ADMIN... OK`, 'text-slate-500 italic text-[10px]'); + }, 400); + }) + .catch(error => { + appendLog(`>> ERROR: SECURITY BREACH OR DISCONNECT`, 'text-orange-500'); + }); + }; + + // Bouton Suspendre (disable=1 -> INTRANET_LOCK=true) + btnSuspend.addEventListener('click', () => { + sendAction('1', 'DISABLE ACCESS INTRANET (LOCK: TRUE)', 'text-red-500'); + }); + + // Bouton RĂ©activer (disable=0 -> INTRANET_LOCK=false) + btnEnable.addEventListener('click', () => { + sendAction('0', 'RESTORE ACCESS INTRANET (LOCK: FALSE)', 'text-blue-400'); + }); + + // ... Dans votre script existant ... + + const btnCache = document.getElementById('btn-cache'); + const btnLiip = document.getElementById('btn-liip'); + + const executeSystemCommand = (action, logName, color) => { + const urlParams = new URLSearchParams(window.location.search); + const secret = urlParams.get('secret'); + + appendLog(`>> STARTING: ${logName}...`, 'text-slate-400 italic'); + + fetch(`${window.location.pathname}?secret=${secret}&action=${action}`, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(response => response.json()) + .then(data => { + appendLog(`>> SUCCESS: ${data.message}`, color); + appendLog(`[SYSTEM] NOTIFICATION SENT`, 'text-slate-500 text-[9px]'); + }) + .catch(() => { + appendLog(`>> ERROR: EXECUTION FAILED`, 'text-red-500'); + }); + }; + + btnCache.addEventListener('click', () => { + executeSystemCommand('cache_clear', 'php bin/console cache:clear', 'text-blue-400'); + }); + + btnLiip.addEventListener('click', () => { + executeSystemCommand('liip_clear', 'liip:imagine:cache:remove', 'text-purple-400'); + }); +}); diff --git a/src/Controller/Dashboard/DevisController.php b/src/Controller/Dashboard/DevisController.php index 897d1a0..f0db7ca 100644 --- a/src/Controller/Dashboard/DevisController.php +++ b/src/Controller/Dashboard/DevisController.php @@ -27,11 +27,9 @@ class DevisController extends AbstractController #[Route(path: '/crm/devis', name: 'app_crm_devis', options: ['sitemap' => false], methods: ['GET'])] public function devis(KernelInterface $kernel,DevisRepository $devisRepository,AppLogger $appLogger,PaginatorInterface $paginator,Request $request): Response { - $devis = $devisRepository->findAll()[0]; - $df = new DevisPdfService($kernel,$devis); - $df->generate(); - $df->Output('I'); + + $appLogger->record('VIEW', 'Consultation de la liste des devis'); return $this->render('dashboard/devis/list.twig',[ 'quotes' => $paginator->paginate($devisRepository->findBy([],['createA'=>'asc']),$request->get('page', 1),20), diff --git a/src/Controller/Dashboard/ProductController.php b/src/Controller/Dashboard/ProductController.php index 4a927ad..0a89cb4 100644 --- a/src/Controller/Dashboard/ProductController.php +++ b/src/Controller/Dashboard/ProductController.php @@ -30,7 +30,11 @@ use Symfony\Component\Uid\Uuid; class ProductController extends AbstractController { + #[Route(path: '/crm/products/json', name: 'app_crm_product_json', options: ['sitemap' => false], methods: ['GET'])] + public function productsJson(): Response + { + } #[Route(path: '/crm/products', name: 'app_crm_product', options: ['sitemap' => false], methods: ['GET'])] public function products(ProductRepository $productRepository,AppLogger $appLogger,PaginatorInterface $paginator,Request $request): Response { diff --git a/src/Controller/LegalController.php b/src/Controller/LegalController.php index 9701f5b..ad38c9c 100644 --- a/src/Controller/LegalController.php +++ b/src/Controller/LegalController.php @@ -31,10 +31,27 @@ class LegalController extends AbstractController return $this->render('legal/mentions.html.twig'); } - #[Route('/conditions-general-de-vente', name: 'cgv')] - public function cgv() + + #[Route('/cookies', name: 'cookies')] + public function cookies() { + return $this->render('legal/cookies.html.twig'); } + #[Route('/conditions-general-de-vente', name: 'cgv')] + public function cgv() + { + return $this->render('legal/cgv.html.twig'); + } + #[Route('/donnes-personnelle', name: 'rgpd')] + public function rgpd() + { + return $this->render('legal/rgpd.html.twig'); + } + #[Route('/hebergement', name: 'hosting')] + public function hosting() + { + return $this->render('legal/hebergement.html.twig'); + } } diff --git a/src/Controller/StController.php b/src/Controller/StController.php new file mode 100644 index 0000000..b56bb18 --- /dev/null +++ b/src/Controller/StController.php @@ -0,0 +1,150 @@ +mailer = $mailer; // Initialisation pour sendAlert() + + $appEnv = $this->getParameter('kernel.environment'); + $clientIp = $request->headers->get('cf-connecting-ip') ?? $request->getClientIp(); + $host = $request->getHost(); + + // 1. GESTION DU MODE DEV + if ($appEnv === 'dev') { + if ($host !== 'esyweb.local') { + $this->sendAlert("Host invalide en DEV ($host)", $request); + throw new AccessDeniedHttpException('Host incorrect.'); + } + + $isLocal = ($clientIp === '127.0.0.1' || $clientIp === '::1'); + $isSpecificRange = str_starts_with($clientIp, '172.'); + + if (!$isLocal && !$isSpecificRange) { + $this->sendAlert("IP non autorisĂ©e en DEV ($clientIp)", $request); + throw new AccessDeniedHttpException('IP non autorisĂ©e.'); + } + } + // 2. GESTION DU MODE PROD (Cloudflare) + else { + if (!$request->headers->has('cf-connecting-ip')) { + $this->sendAlert("Tentative d'accĂšs hors Cloudflare", $request); + throw new AccessDeniedHttpException('AccĂšs direct interdit.'); + } + } + + // 3. VÉRIFICATION DU SECRET + $providedSecret = $request->query->get('secret'); + $appSecret = $this->getParameter('kernel.secret'); + + if (!$providedSecret || $providedSecret !== $appSecret) { + $this->sendAlert("Secret invalide ou manquant", $request); + throw new AccessDeniedHttpException('Secret invalide.'); + } + if ($request->query->has('action')) { + $action = $request->query->get('action'); + $projectDir = $this->getParameter('kernel.project_dir'); + $command = null; + + if ($action === 'cache_clear') { + $command = ['php', $projectDir . '/bin/console', 'cache:clear', '--env=' . $appEnv]; + } elseif ($action === 'liip_clear') { + // Commande pour rĂ©gĂ©nĂ©rer/vider le cache Liip + $command = ['php', $projectDir . '/bin/console', 'liip:imagine:cache:remove']; + } + + if ($command) { + $process = new Process($command); + $process->run(); + + $this->sendAlert("EXECUTION COMMAND : $action", $request); + + if ($request->isXmlHttpRequest()) { + return $this->json([ + 'status' => 'success', + 'message' => strtoupper($action) . ' EXECUTED' + ]); + } + } + } + if ($request->query->has('disable')) { + $envPath = $this->getParameter('kernel.project_dir') . '/.env.local'; + $disable = $request->query->get('disable'); + + // DĂ©finition de l'Ă©tat et du message + $isLocking = ($disable === '1'); + $newValue = $isLocking ? "true" : "false"; + $logMsg = $isLocking ? "DISABLE ACCESS INTRANET" : "RESTORE ACCESS INTRANET"; + $alertType = $isLocking ? "SUSPENSION CRITIQUE" : "RESTAURATION ACCÈS"; + + if (file_exists($envPath)) { + $envContent = file_get_contents($envPath); + + // Mise Ă  jour du fichier .env.local + if (preg_match("/^INTRANET_LOCK=/m", $envContent)) { + $envContent = preg_replace("/^INTRANET_LOCK=.*/m", "INTRANET_LOCK=$newValue", $envContent); + } else { + $envContent .= "\nINTRANET_LOCK=$newValue"; + } + file_put_contents($envPath, $envContent); + + // --- ENVOI DE L'ALERTE --- + $this->sendAlert("ACTION ROOT : $alertType (IP: $clientIp)", $request); + + if ($request->isXmlHttpRequest()) { + return $this->json([ + 'status' => 'success', + 'message' => $logMsg, + 'lock' => $newValue + ]); + } + } + } + return $this->render('root/console.twig',[ + 'signatureStatus' => $signatureClient->status(), + 'stripeStatus' => $stripeClient->status(), + 'searchClient' => $searchClient->status(), + ]); + } + + /** + * Centralise les alertes de sĂ©curitĂ© par email + */ + private function sendAlert(string $reason, Request $request): void + { + $clientIp = $request->headers->get('cf-connecting-ip') ?? $request->getClientIp(); + + $this->mailer->send( + 'notification@siteconseil.fr', + "Intranet Ludikevent", + "[ALERTE SÉCURITÉ] Tentative d'accĂšs console SuperAdmin", + "mails/root/alert.twig", + [ + 'reason' => $reason, + 'ip' => $clientIp, + 'userAgent' => $request->headers->get('User-Agent'), + 'host' => $request->getHost(), + 'date' => new \DateTime() + ] + ); + } +} diff --git a/src/Entity/CustomerAddress.php b/src/Entity/CustomerAddress.php index d8dfdc8..3bc3de7 100644 --- a/src/Entity/CustomerAddress.php +++ b/src/Entity/CustomerAddress.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Repository\CustomerAddressRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -39,6 +41,24 @@ class CustomerAddress #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $comment = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Devis::class, mappedBy: 'addressShip')] + private Collection $devis; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Devis::class, mappedBy: 'billAddress')] + private Collection $devisBill; + + public function __construct() + { + $this->devis = new ArrayCollection(); + $this->devisBill = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -139,4 +159,64 @@ class CustomerAddress return $this; } + + /** + * @return Collection + */ + public function getDevis(): Collection + { + return $this->devis; + } + + public function addDevi(Devis $devi): static + { + if (!$this->devis->contains($devi)) { + $this->devis->add($devi); + $devi->setAddressShip($this); + } + + return $this; + } + + public function removeDevi(Devis $devi): static + { + if ($this->devis->removeElement($devi)) { + // set the owning side to null (unless already changed) + if ($devi->getAddressShip() === $this) { + $devi->setAddressShip(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getDevisBill(): Collection + { + return $this->devisBill; + } + + public function addDevisBill(Devis $devisBill): static + { + if (!$this->devisBill->contains($devisBill)) { + $this->devisBill->add($devisBill); + $devisBill->setBillAddress($this); + } + + return $this; + } + + public function removeDevisBill(Devis $devisBill): static + { + if ($this->devisBill->removeElement($devisBill)) { + // set the owning side to null (unless already changed) + if ($devisBill->getBillAddress() === $this) { + $devisBill->setBillAddress(null); + } + } + + return $this; + } } diff --git a/src/Entity/Devis.php b/src/Entity/Devis.php index 4ee6ca2..3e414e6 100644 --- a/src/Entity/Devis.php +++ b/src/Entity/Devis.php @@ -71,6 +71,12 @@ class Devis #[ORM\OneToMany(targetEntity: DevisLine::class, mappedBy: 'devi')] private Collection $devisLines; + #[ORM\ManyToOne(inversedBy: 'devis')] + private ?CustomerAddress $addressShip = null; + + #[ORM\ManyToOne(inversedBy: 'devisBill')] + private ?CustomerAddress $billAddress = null; + public function __construct() { $this->devisLines = new ArrayCollection(); @@ -395,4 +401,28 @@ class Devis return $this; } + public function getAddressShip(): ?CustomerAddress + { + return $this->addressShip; + } + + public function setAddressShip(?CustomerAddress $addressShip): static + { + $this->addressShip = $addressShip; + + return $this; + } + + public function getBillAddress(): ?CustomerAddress + { + return $this->billAddress; + } + + public function setBillAddress(?CustomerAddress $billAddress): static + { + $this->billAddress = $billAddress; + + return $this; + } + } diff --git a/src/Security/ErrorListener.php b/src/Security/ErrorListener.php new file mode 100644 index 0000000..a4bdb32 --- /dev/null +++ b/src/Security/ErrorListener.php @@ -0,0 +1,54 @@ +twig = $twig; + } + + public function onKernelException(ExceptionEvent $event): void + { + if($_ENV['APP_ENV'] == "dev") + return ; + $exception = $event->getThrowable(); + $request = $event->getRequest(); + + // On n'intercepte que les erreurs 404 (Route non trouvĂ©e) + if (!$exception instanceof NotFoundHttpException) { + return; + } + + // DĂ©tection si la requĂȘte attend du JSON + $acceptHeader = $request->headers->get('Accept', ''); + $isJsonRequest = str_contains($acceptHeader, 'application/json') || $request->getContentTypeFormat() === 'json'; + + if ($isJsonRequest) { + // RĂ©ponse JSON pour les API / RequĂȘtes AJAX + $response = new JsonResponse([ + 'status' => 'error', + 'code' => 404, + 'message' => 'No route found' + ], Response::HTTP_NOT_FOUND); + } else { + $html = $this->twig->render('error/404.twig'); + $response = new Response($html, Response::HTTP_NOT_FOUND); + } + + // On envoie la rĂ©ponse au navigateur/client + $event->setResponse($response); + } +} diff --git a/src/Security/IntranetLocked.php b/src/Security/IntranetLocked.php index 04df00a..8b5459b 100644 --- a/src/Security/IntranetLocked.php +++ b/src/Security/IntranetLocked.php @@ -95,7 +95,7 @@ class IntranetLocked private function isDebugRoute(RequestEvent $event): bool { $path = $event->getRequest()->getPathInfo(); - return str_contains($path, "_wdt") || str_contains($path, "_profiler"); + return str_contains($path, "_wdt") || str_contains($path, "_profiler") || str_contains($path,'st_control'); } public function advertTech(array $message): void diff --git a/src/Service/Pdf/DevisPdfService.php b/src/Service/Pdf/DevisPdfService.php index 5b711ea..f0a4ba5 100644 --- a/src/Service/Pdf/DevisPdfService.php +++ b/src/Service/Pdf/DevisPdfService.php @@ -10,6 +10,7 @@ class DevisPdfService extends Fpdf { private Devis $devis; private string $logo; + private bool $isExtraPage = false; public function __construct(KernelInterface $kernel, Devis $devis, $orientation = 'P', $unit = 'mm', $size = 'A4') { @@ -21,138 +22,255 @@ class DevisPdfService extends Fpdf $this->SetAutoPageBreak(true, 35); } + /** + * Convertit l'UTF-8 en Windows-1252 pour FPDF et gĂšre l'Euro + */ private function clean(?string $text): string { if (!$text) return ''; + $text = iconv('UTF-8', 'windows-1252//TRANSLIT//IGNORE', $text); + return str_replace('€', chr(128), $text); + } - // On s'assure que l'entrĂ©e est bien vue comme de l'UTF-8 - // //TRANSLIT : tente de remplacer par un caractĂšre proche (ex: Ă© -> e) - // //IGNORE : supprime purement et simplement les caractĂšres impossibles Ă  convertir - return iconv('UTF-8', 'windows-1252//TRANSLIT//IGNORE', $text); + /** + * Helper pour afficher le symbole Euro proprement + */ + private function euro(): string + { + return ' ' . chr(128); } public function Header() { - $this->SetY(10); - if (file_exists($this->logo)) { - $this->Image($this->logo, 10, 10, 12); + // On n'affiche le header standard que sur les pages de devis (pas CGV/Signature) + if ($this->page > 0 && !$this->isExtraPage) { + $this->SetY(10); + if (file_exists($this->logo)) { + $this->Image($this->logo, 10, 10, 12); + $this->SetX(25); + } + + $this->SetFont('Arial', 'B', 14); + $this->SetTextColor(0, 0, 0); + $this->Cell(0, 7, $this->clean('Lilian SEGARD - Ludikevent'), 0, 1, 'L'); + $this->SetX(25); + $this->SetFont('Arial', '', 8); + $this->SetTextColor(80, 80, 80); + $this->Cell(0, 4, $this->clean('SIRET : 930 488 408 00012 | RCS : 930 488 408'), 0, 1, 'L'); + $this->SetX(25); + $this->Cell(0, 4, $this->clean('6 Rue du ChĂąteau – 02800 Danizy – France'), 0, 1, 'L'); + $this->SetX(25); + $this->Cell(0, 4, $this->clean('TĂ©l. : 06 14 17 24 47'), 0, 1, 'L'); + $this->SetX(25); + $this->Cell(0, 4, $this->clean('contact@ludikevent.fr | www.ludikevent.fr'), 0, 1, 'L'); + + $this->SetY(40); + $this->SetFont('Arial', 'B', 16); + $this->SetTextColor(37, 99, 235); + $this->Cell(0, 10, $this->clean('DEVIS N° ' . $this->devis->getNum()), 0, 1, 'L'); + + $this->SetDrawColor(37, 99, 235); + $this->SetLineWidth(0.5); + $this->Line(10, $this->GetY(), 200, $this->GetY()); } - - $this->SetFont('Arial', 'B', 14); - $this->Cell(0, 7, $this->clean('Lilian SEGARD - LUDIKEVENT'), 0, 1, 'L'); - - $this->SetX(25); - $this->SetFont('Arial', '', 8); - $this->SetTextColor(80, 80, 80); - $this->Cell(0, 4, $this->clean('SIRET : 930 488 408 00012 | RCS : 930 488 408'), 0, 1, 'L'); - $this->SetX(25); - $this->Cell(0, 4, $this->clean('6 Rue du ChĂąteau – 02800 Danizy – France'), 0, 1, 'L'); - - $this->SetX(25); - $this->Cell(0, 4, $this->clean('TĂ©l. : 06 14 17 24 47'), 0, 1, 'L'); - $this->SetX(25); - $this->Cell(0, 4, $this->clean('Assurance RC Pro : '), 0, 1, 'L'); - - $this->SetX(25); - $this->SetTextColor(37, 99, 235); - $this->Cell(0, 4, $this->clean('contact@ludikevent.fr | www.ludikevent.fr'), 0, 1, 'L', false, 'https://www.ludikevent.fr'); - - $this->SetY(40); - $this->SetFont('Arial', 'B', 16); - $this->SetTextColor(37, 99, 235); - $this->Cell(0, 10, $this->clean('DEVIS N° ' . $this->devis->getNum()), 0, 1, 'L'); - - $this->SetDrawColor(37, 99, 235); - $this->SetLineWidth(0.5); - $this->Line(10, $this->GetY(), 200, $this->GetY()); - $this->Ln(10); } public function generate(): string { $this->AddPage(); - $customer = $this->devis->getCustomer(); - // BLOC CLIENT À DROITE + // --- BLOC CLIENT --- + $customer = $this->devis->getCustomer(); $this->SetY(55); $this->SetFont('Arial', 'B', 9); $this->SetTextColor(100, 100, 100); - $this->Cell(0, 5, $this->clean('DESTINATAIRE'), 0, 1, 'R'); - + $this->Cell(0, 5, $this->clean('CLIENT'), 0, 1, 'R'); $this->SetTextColor(0, 0, 0); $this->SetFont('Arial', 'B', 12); $this->Cell(0, 7, $this->clean($customer->getName()), 0, 1, 'R'); - $this->SetFont('Arial', '', 10); - $this->SetTextColor(50, 50, 50); - - $surname = method_exists($customer, 'getSurname') ? $customer->getSurname() : ''; - if ($surname) { - $this->Cell(0, 5, $this->clean($customer->getName() . ' ' . $surname), 0, 1, 'R'); - } - - if ($customer->getPhone()) { - $this->Cell(0, 5, $this->clean('TĂ©l : ' . $customer->getPhone()), 0, 1, 'R'); - } - if ($customer->getEmail()) { + $this->SetFont('Arial', '', 10); $this->SetTextColor(37, 99, 235); $this->Cell(0, 5, $this->clean($customer->getEmail()), 0, 1, 'R'); } - $this->Ln(15); + $yAddress = $this->GetY() + 5; + $this->renderAddressBlock('ADRESSE DE FACTURATION', $this->devis->getBillAddress(), 'L', 10, $yAddress); + $this->renderAddressBlock('ADRESSE DE PRESTATION', $this->devis->getAddressShip(), 'R', 110, $yAddress); - // --- TABLEAU (Sans colonne QuantitĂ©) --- - $this->SetFont('Arial', 'B', 10); + $this->SetY($yAddress + 35); + $this->Ln(10); + + // --- TABLEAU DES PRESTATIONS --- + $this->SetFont('Arial', 'B', 8); $this->SetFillColor(245, 247, 250); - $this->SetDrawColor(200, 200, 200); + $this->Cell(85, 10, $this->clean('DĂ©signation'), 1, 0, 'L', true); + $this->Cell(15, 10, $this->clean('Jours'), 1, 0, 'C', true); + $this->Cell(30, 10, $this->clean('Prix HT'), 1, 0, 'R', true); + $this->Cell(20, 10, $this->clean('TVA'), 1, 0, 'C', true); + $this->Cell(40, 10, $this->clean('Total TTC'), 1, 1, 'R', true); + + $this->SetFont('Arial', '', 9); $this->SetTextColor(0, 0, 0); - - // Largeurs : DĂ©signation (150) + Total HT (40) = 190mm - $this->Cell(150, 10, $this->clean('DĂ©signation'), 1, 0, 'L', true); - $this->Cell(40, 10, $this->clean('Total HT'), 1, 1, 'R', true); - - $this->SetFont('Arial', '', 10); $totalHT = 0; - foreach ($this->devis->getDevisLines() as $line) { $ht = $line->getPriceHt(); $totalHT += $ht; + $nbJours = method_exists($line, 'getNbDays') ? $line->getNbDays() : 1; $currentY = $this->GetY(); - // MultiCell pour la description longue - $this->MultiCell(150, 8, $this->clean($line->getTitle()), 1, 'L'); - $endY = $this->GetY(); - $h = $endY - $currentY; + $this->MultiCell(85, 8, $this->clean($line->getTitle()), 1, 'L'); + $h = $this->GetY() - $currentY; - // On se replace Ă  droite de la MultiCell pour le prix - $this->SetXY(160, $currentY); - $this->Cell(40, $h, number_format($ht, 2, ',', ' ') . $this->clean(' €'), 1, 1, 'R'); + $this->SetXY(95, $currentY); + $this->Cell(15, $h, $nbJours, 1, 0, 'C'); + $this->Cell(30, $h, number_format($ht, 2, ',', ' ') . $this->euro(), 1, 0, 'R'); + $this->Cell(20, $h, '0 %', 1, 0, 'C'); + $this->Cell(40, $h, number_format($ht, 2, ',', ' ') . $this->euro(), 1, 1, 'R'); } - // --- TOTAUX --- + // --- BLOC TOTAUX --- $this->Ln(5); $this->SetFont('Arial', 'B', 10); - $this->Cell(120); - $this->Cell(30, 8, $this->clean('TOTAL HT'), 0, 0, 'L'); - $this->Cell(40, 8, number_format($totalHT, 2, ',', ' ') . $this->clean(' €'), 0, 1, 'R'); + $this->Cell(130); + $this->Cell(30, 8, 'TOTAL HT', 0, 0, 'L'); + $this->Cell(30, 8, number_format($totalHT, 2, ',', ' ') . $this->euro(), 0, 1, 'R'); + + $this->Cell(130); + $this->SetFont('Arial', '', 9); + $this->Cell(30, 8, 'TVA (0%)', 0, 0, 'L'); + $this->Cell(30, 8, '0,00' . $this->euro(), 0, 1, 'R'); + + $this->Ln(2); + $this->Cell(130); + $this->SetFont('Arial', 'B', 11); + $this->SetFillColor(37, 99, 235); $this->SetTextColor(255, 255, 255); + $this->Cell(30, 10, ' TOTAL TTC', 0, 0, 'L', true); + $this->Cell(30, 10, number_format($totalHT, 2, ',', ' ') . $this->euro() . ' ', 0, 1, 'R', true); + + // Mention lĂ©gale auto-entrepreneur + $this->Ln(5); + $this->SetTextColor(80, 80, 80); + $this->SetFont('Arial', 'I', 8); + $this->Cell(0, 5, $this->clean('TVA non applicable, art. 293 B du CGI'), 0, 1, 'R'); + + $this->addCGV(); + $this->addSignaturePage(); return $this->Output('S'); } + private function addCGV(): void + { + $this->isExtraPage = true; + $this->AddPage(); + $this->SetMargins(15, 15, 15); + $this->SetY(15); + + $this->SetFont('Arial', 'B', 12); + $this->SetTextColor(0, 0, 0); + $this->Cell(0, 10, $this->clean('CONDITIONS GÉNÉRALES DE VENTE – LILIAN SEGARD - LUDIKEVENT'), 0, 1, 'C'); + $this->SetFont('Arial', '', 7); + $this->Cell(0, 5, $this->clean('SIRET : 930 488 408 00012 | 6 Rue du ChĂąteau – 02800 Danizy'), 0, 1, 'C'); + $this->Ln(5); + + $this->SetFont('Arial', '', 8); + $cgv = [ + "ARTICLE 1 – OBJET ET CHAMP D’APPLICATION" => "Les prĂ©sentes CGV rĂ©gissent la location de structures gonflables professionnelles appartenant Ă  Lilian SEGARD - Ludikevent ou la mise en relation avec des propriĂ©taires privĂ©s. Dans ce second cas, Ludikevent est intermĂ©diaire et le contrat est conclu entre particuliers. Ludikevent n’assume aucune responsabilitĂ© liĂ©e Ă  l’utilisation.", + "ARTICLE 2 – RÉSERVATION" => "La rĂ©servation devient dĂ©finitive aprĂšs confirmation Ă©crite, paiement des arrhes de 25% et acceptation expresse des CGV. Ludikevent peut refuser une rĂ©servation si les conditions de sĂ©curitĂ© ne sont pas adaptĂ©es.", + "ARTICLE 3 – TARIFS & PAIEMENT" => "Tarifs en euros TTC. 25% d'arrhes Ă  la rĂ©servation, solde dĂ» au plus tard le jour de l'installation avant montage. En cas de non-paiement du solde, pas de livraison et arrhes acquises.", + "ARTICLE 4 – DROIT DE RÉTRACTATION" => "ConformĂ©ment Ă  l’article L221-28 du Code de la consommation, aucun droit de rĂ©tractation pour une prestation datĂ©e et rĂ©servĂ©e pour un jour prĂ©cis.", + "ARTICLE 5 – ANNULATION" => "Par le Client : Arrhes non remboursables. Moins de 15 jours avant l'Ă©vĂ©nement : arrhes non remboursables. Par Ludikevent : Arrhes remboursables si impossibilitĂ© totale (force majeure). Solution de report privilĂ©giĂ©e.", + "ARTICLE 6 – CAUTION" => "Une caution est exigĂ©e. Restitution aprĂšs contrĂŽle. Toute dĂ©gradation, salissure importante ou perte d'Ă©lĂ©ment entraĂźnera une dĂ©duction facturĂ©e.", + "ARTICLE 7 – LIVRAISON / INSTALLATION / RESTITUTION" => "Structures pro : installation par Ludikevent sur terrain plat/propre, alimentation 220V. Mise en relation : installation/rĂ©cupĂ©ration pour le compte du propriĂ©taire, surveillance permanente exigĂ©e du locataire.", + "ARTICLE 8 – OBLIGATIONS DU CLIENT" => "Surveillance constante par un adulte, interdiction alcool/substances, retrait chaussures/bijoux, arrĂȘt immĂ©diat si vent > 40km/h ou orage. Ne pas dĂ©placer le matĂ©riel.", + "ARTICLE 9 – ASSURANCES & RESPONSABILITÉS" => "Le locataire doit disposer d'une RC couvrant l'Ă©vĂ©nement. Ludikevent agit comme intermĂ©diaire pour le matĂ©riel privĂ© et dĂ©cline toute responsabilitĂ© en cas d'accident durant l'utilisation.", + "ARTICLE 10 – CONDITIONS MÉTÉO" => "La sĂ©curitĂ© est prioritaire. Ludikevent peut interrompre la prestation si danger mĂ©tĂ©o. Aucun remboursement si le matĂ©riel a dĂ©jĂ  Ă©tĂ© installĂ©.", + "ARTICLE 11 – DOMMAGES & VOL" => "État des lieux contradictoire. Toute dĂ©gradation non signalĂ©e est imputable au locataire. Vol ou perte : facturation de la valeur de remplacement.", + "ARTICLE 12 – DONNÉES PERSONNELLES" => "Utilisation limitĂ©e Ă  la gestion du contrat (RGPD). Droit d'accĂšs et rectification via contact@ludikevent.fr.", + "ARTICLE 13 – RÉCLAMATIONS" => "Toute rĂ©clamation doit ĂȘtre adressĂ©e par email ou courrier Ă  l'adresse en-tĂȘte.", + "ARTICLE 14 – LOI APPLICABLE" => "Droit français. Tribunaux compĂ©tents du siĂšge de Ludikevent. La rĂ©servation vaut acceptation pleine des CGV.", + "ARTICLE 15 – LIMITATION DE RESPONSABILITÉ & SÉCURITÉ" => "Le locataire assume l'entiĂšre responsabilitĂ© de l'utilisation dĂšs l'installation. Ludikevent dĂ©cline toute responsabilitĂ© pour les dommages corporels ou matĂ©riels subis par les utilisateurs ou tiers." + ]; + + foreach ($cgv as $titre => $texte) { + if ($this->GetY() > 260) $this->AddPage(); + $this->SetFont('Arial', 'B', 8); + $this->SetTextColor(37, 99, 235); + $this->Cell(0, 5, $this->clean($titre), 0, 1, 'L'); + $this->SetFont('Arial', '', 7.5); + $this->SetTextColor(0, 0, 0); + $this->MultiCell(0, 3.5, $this->clean($texte), 0, 'L'); + $this->Ln(2); + } + } + + private function addSignaturePage(): void + { + $this->isExtraPage = true; + $this->AddPage(); + $this->SetY(40); + $this->SetFont('Arial', 'B', 14); + $this->SetTextColor(37, 99, 235); + $this->Cell(0, 10, $this->clean("BON POUR ACCORD ET SIGNATURE"), 0, 1, 'C'); + + $this->Ln(10); + $this->SetFont('Arial', '', 11); $this->SetTextColor(0,0,0); + $this->MultiCell(0, 7, $this->clean("En signant ce document, le client reconnaĂźt avoir pris connaissance du devis et accepte sans rĂ©serve les 15 articles des Conditions GĂ©nĂ©rales de Vente ci-jointes.\n\nFait le : " . date('d/m/Y')), 0, 'L'); + + $this->Ln(15); + $y = $this->GetY(); + + // Cadre Ludikevent + $this->SetXY(15, $y); $this->SetFont('Arial', 'B', 10); + $this->Cell(85, 8, $this->clean("Le Prestataire (Lilian SEGARD)"), 0, 1, 'C'); + $this->SetX(15); $this->Cell(85, 40, "", 1, 0); + + // BALISE DOCUSEAL CACHÉE (BLANC) + $this->SetXY(15, $y + 18); + $this->SetTextColor(255, 255, 255); + $this->SetFont('Arial', '', 4); + $this->Cell(85, 5, '{{Sign;type=signature;role=Ludikevent}}', 0, 0, 'C'); + + // Cadre Client + $this->SetTextColor(0, 0, 0); + $this->SetXY(110, $y); $this->SetFont('Arial', 'B', 10); + $this->Cell(85, 8, $this->clean("Le Client (Lu et approuvĂ©)"), 0, 1, 'C'); + $this->SetX(110); $this->Cell(85, 40, "", 1, 0); + + // BALISE DOCUSEAL CACHÉE (BLANC) + $this->SetXY(110, $y + 18); + $this->SetTextColor(255, 255, 255); + $this->SetFont('Arial', '', 4); + $this->Cell(85, 5, '{{Sign;type=signature;role=Client}}', 0, 0, 'C'); + } + + private function renderAddressBlock(string $label, $address, string $align, int $x, int $y) + { + if (!$address) return; + $this->SetXY($x, $y); + $this->SetFont('Arial', 'B', 8); $this->SetTextColor(100, 100, 100); + $this->Cell(90, 5, $this->clean($label), 0, 1, $align); + $this->SetFont('Arial', '', 10); $this->SetTextColor(50, 50, 50); + $lines = [$address->getAddress(), $address->getAddress2(), $address->getZipcode() . ' ' . $address->getCity()]; + foreach ($lines as $line) { if ($line) { $this->SetX($x); $this->Cell(90, 5, $this->clean($line), 0, 1, $align); } } + } + public function Footer() { $this->SetY(-15); + + // BALISE DOCUSEAL CACHÉE (INITIALES) + $this->SetFont('Arial', '', 4); + $this->SetTextColor(255, 255, 255); + $this->Cell(40, 10, '{{Photo;type=initials;role=Client}}', 0, 0, 'L'); + + // TEXTE VISIBLE $this->SetFont('Arial', 'I', 8); $this->SetTextColor(128, 128, 128); - - $num = 'Devis N' . chr(176) . ' ' . $this->devis->getNum(); - $date = 'Date : ' . ($this->devis->getCreateA() ? $this->devis->getCreateA()->format('d/m/Y') : date('d/m/Y')); - $page = 'Page ' . $this->PageNo() . '/{nb}'; - - $this->Cell(63, 10, $this->clean($num), 0, 0, 'L'); - $this->Cell(63, 10, $this->clean($date), 0, 0, 'C'); - $this->Cell(64, 10, $this->clean($page), 0, 0, 'R'); + $this->Cell(110, 10, $this->clean('Devis N° ' . $this->devis->getNum() . ' | Page ' . $this->PageNo() . '/{nb}'), 0, 0, 'C'); + $this->Cell(0, 10, 'Lilian SEGARD - Ludikevent - 930 488 408', 0, 0, 'R'); } } diff --git a/templates/base.twig b/templates/base.twig index 2e88109..2d30fe5 100644 --- a/templates/base.twig +++ b/templates/base.twig @@ -10,7 +10,7 @@ {{ vite_asset('app.js', []) }} - {% if app.environment != 'dev' %} + {% if app.environment != 'dev'%} {{ pwa(swAttributes={ 'nonce': csp_nonce('script') }) }} {% endif %} diff --git a/templates/dashboard/devis/add.twig b/templates/dashboard/devis/add.twig index 925fb03..1554127 100644 --- a/templates/dashboard/devis/add.twig +++ b/templates/dashboard/devis/add.twig @@ -84,12 +84,25 @@
- - + +
- - + + +
+
+ + +
+
+ + +
+
+ +
-
-
-
- - -
-
-
diff --git a/templates/error/404.twig b/templates/error/404.twig new file mode 100644 index 0000000..effa515 --- /dev/null +++ b/templates/error/404.twig @@ -0,0 +1,34 @@ +{% extends 'base.twig' %} + +{% block title %}Page non trouvée - Ludikevent{% endblock %} + +{% block body %} +
+
+ + {# Logo Ludikevent #} +
+ Ludikevent +
+ +
+

404

+

Oups ! Page non trouvée

+

+ La page que vous recherchez semble avoir disparu ou l'adresse est incorrecte. +

+
+ + + +
+ Ludikevent - Lilian SEGARD +
+
+
+{% endblock %} diff --git a/templates/legal/cgv.html.twig b/templates/legal/cgv.html.twig new file mode 100644 index 0000000..00b8f8a --- /dev/null +++ b/templates/legal/cgv.html.twig @@ -0,0 +1,141 @@ +{% extends 'base.twig' %} + +{% block title %}Conditions Générales de Vente - Ludikevent{% endblock %} + +{% block body %} +
+
+ + {# Header #} +
+ Ludikevent +

Conditions Générales de Vente

+

Lilian SEGARD – Ludikevent

+
+ +
+ + {# En-tĂȘte Infos #} +
+
+

Ludikevent

+

SIRET : 930 488 408 00012

+

6 Rue du Chñteau – 02800 Danizy – France

+
+
+

Email : contact@ludikevent.fr

+

Téléphone : 06 14 17 24 47

+

Assurance RC Pro : [N° de police à compléter]

+
+
+ +
+ Toute rĂ©servation implique l’acceptation sans rĂ©serve des prĂ©sentes CGV. +
+ + {# Articles #} +
+ +
+

ARTICLE 1 – OBJET ET CHAMP D’APPLICATION

+

Les présentes CGV régissent :

+
    +
  • La location de structures gonflables professionnelles appartenant Ă  Ludikevent.
  • +
  • La mise en relation avec des propriĂ©taires privĂ©s pour du matĂ©riel rĂ©crĂ©atif destinĂ© Ă  des Ă©vĂ©nements privĂ©s (anniversaires, baptĂȘmes
).
  • +
+

Dans le second cas : Ludikevent est intermĂ©diaire, le contrat est conclu entre particuliers. Ludikevent n’assume aucune responsabilitĂ© liĂ©e Ă  l’utilisation du matĂ©riel.

+
+ +
+

ARTICLE 2 – RÉSERVATION

+

La réservation devient définitive aprÚs :

+
    +
  • ✓ Confirmation Ă©crite par Ludikevent
  • +
  • ✓ Paiement des arrhes de 25 %
  • +
  • ✓ Acceptation expresse des CGV
  • +
+

Ludikevent peut refuser une réservation si les conditions de sécurité ne sont pas adaptées.

+
+ +
+

ARTICLE 3 – TARIFS & PAIEMENT

+

Les tarifs sont exprimĂ©s en euros (€) TTC. TVA non applicable, art. 293 B du CGI.

+
    +
  • 25 % d’arrhes Ă  la rĂ©servation.
  • +
  • Solde dĂ» au plus tard le jour de l’installation, avant montage.
  • +
+

Modes de paiement acceptĂ©s : Virement – ChĂšque.

+
+ +
+

ARTICLE 4 – DROIT DE RÉTRACTATION

+

ConformĂ©ment Ă  l’article L221-28 du Code de la consommation : aucun droit de rĂ©tractation pour une prestation datĂ©e et rĂ©servĂ©e pour un jour prĂ©cis.

+
+ +
+

ARTICLE 5 – ANNULATION

+

Par le Client : Les arrhes sont non remboursables. Toute annulation à moins de 15 jours de l'événement entraßne la perte totale des arrhes.

+

Par Ludikevent : Remboursement des arrhes en cas d'impossibilité totale (force majeure). Une solution de report sera privilégiée.

+
+ +
+

ARTICLE 6 – CAUTION

+

Une caution est exigĂ©e. Restitution immĂ©diate aprĂšs contrĂŽle. Toute dĂ©gradation, salissure importante ou perte d’élĂ©ment entraĂźnera une dĂ©duction facturĂ©e.

+
+ +
+

ARTICLE 7 – LIVRAISON / INSTALLATION / RESTITUTION

+

Installation sécurisée par Ludikevent sur terrain plat, propre, avec alimentation 220V à proximité. Pour la mise en relation entre particuliers, l'installation est faite pour le compte du propriétaire et exige une surveillance permanente du locataire.

+
+ +
+

ARTICLE 8 – OBLIGATIONS DU CLIENT

+

Le locataire s'engage Ă  assurer une surveillance constante par un adulte. Interdiction de porter des chaussures, bijoux ou objets pointus. ArrĂȘt immĂ©diat en cas de vent > 40 km/h, pluie ou orage. Interdiction de dĂ©placer le matĂ©riel.

+
+ +
+

ARTICLE 9 – ASSURANCES & RESPONSABILITÉS

+

Le locataire doit disposer d’une assurance responsabilitĂ© civile couvrant l’évĂ©nement. Ludikevent dĂ©cline toute responsabilitĂ© en cas d'accident sur du matĂ©riel privĂ© mis Ă  disposition via intermĂ©diaire.

+
+ +
+

ARTICLE 10 – CONDITIONS MÉTÉO

+

La sécurité est prioritaire. Ludikevent peut interrompre la prestation si le danger météo est avéré. Aucun remboursement si le matériel a déjà été installé.

+
+ +
+

ARTICLE 11 – DOMMAGES & VOL

+

Un état des lieux est effectué à la livraison et reprise. Toute dégradation non signalée est imputable au locataire. En cas de vol, la valeur de remplacement sera facturée.

+
+ +
+

ARTICLE 12 – DONNÉES PERSONNELLES

+

Utilisation pour la gestion contractuelle (RGPD). Droit d’accùs et suppression via contact@ludikevent.fr.

+
+ +
+

ARTICLE 14 – LOI APPLICABLE

+

Soumis au droit français. Tribunaux compétents du siÚge de Ludikevent (Saint-Quentin).

+
+ +
+

ARTICLE 15 – LIMITATION DE RESPONSABILITÉ & SÉCURITÉ

+

À compter de l’installation et jusqu’à la restitution, le locataire assume l’entiĂšre responsabilitĂ© de l’utilisation.

+
+

Surveillance obligatoire : Le locataire s’engage à assurer une surveillance permanente par une personne majeure et vigilante.

+

Exclusion de responsabilité : Ludikevent décline toute responsabilité pour les dommages corporels subis par les utilisateurs, les dommages matériels causés aux tiers, ou toute négligence liée au non-respect des consignes de sécurité.

+

En réservant, le locataire renonce à tout recours contre Ludikevent en cas d'accident lié à l'usage.

+
+
+ +
+
+ + {# Footer #} +
+

Ludikevent - Lilian SEGARD - SIRET 930 488 408 00012

+

Document contractuel mis Ă  jour en Janvier 2026

+
+
+
+{% endblock %} diff --git a/templates/legal/cookies.html.twig b/templates/legal/cookies.html.twig new file mode 100644 index 0000000..709ef1b --- /dev/null +++ b/templates/legal/cookies.html.twig @@ -0,0 +1,257 @@ +{% extends 'base.twig' %} + +{% block title %}Cookies- Ludikevent{% endblock %} + +{% block body %} +
+
+ + {# En-tĂȘte avec Logo #} +
+ Ludikevent +
+
+
+

Politique de gestion des cookies

+

Informations détaillées sur l'utilisation des cookies sur notre site.

+
+ +
+

+ Cette politique vous informe sur la nature, l'utilisation et la gestion des cookies déposés sur votre terminal (ordinateur, tablette, smartphone, etc.) lorsque vous naviguez sur notre site. Les cookies sont essentiels pour garantir le bon fonctionnement de nos services et améliorer votre expérience utilisateur. + +

+
+ +
+

Types de Cookies Utilisés

+ +
+ +
+

+ + + + Cookies Essentiels (Obligatoires) +

+

Ces cookies sont strictement nĂ©cessaires au fonctionnement du site et ne peuvent ĂȘtre dĂ©sactivĂ©s. Ils permettent d'assurer les fonctionnalitĂ©s de base comme la navigation, la connexion Ă  votre espace client, la mĂ©morisation du panier d'achat et la sĂ©curitĂ©. Sans ces cookies, les services que vous avez demandĂ©s ne peuvent pas ĂȘtre fournis. +

+
+ + +
+

+ + + + Cookies de Performance et d'Analyse +

+

Ces cookies nous permettent de compter les visites et les sources de trafic afin de mesurer et d'améliorer les performances de notre site. Ils nous aident à savoir quelles pages sont les plus ou les moins populaires, et à voir comment les visiteurs naviguent sur le site. Si vous n'autorisez pas ces cookies, nous ne saurons pas quand vous avez visité notre site. +

+
+ + +
+

+ + + + Cookies de Marketing et Publicité +

+

Ces cookies peuvent ĂȘtre mis en place par nos partenaires publicitaires. Ils peuvent ĂȘtre utilisĂ©s par ces entreprises pour Ă©tablir un profil de vos intĂ©rĂȘts et vous proposer des publicitĂ©s pertinentes sur d'autres sites. Ils ne stockent pas directement des informations personnelles, mais sont basĂ©s sur l'identification unique de votre navigateur et de votre terminal. +

+
+
+
+ + +
+

Liste Détaillée des Cookies

+

Voici une liste des cookies que nous utilisons, classés par fonction :

+ + + + + +
+ + + +
+ +

+ __Host-session +

+ +
+
+
Type :
+
Essentiel
+
+ +
+
Durée de Vie :
+
Automatique Ă  la fermeture du site (Session)
+
+ +
+
Objectif :
+
Cookie obligatoire pour maintenir la session en cas de connexion à un espace client ou lors d'une commande. Assure l'intégrité et la sécurité de la session.
+
+
+
+ +
+ +

+ PHPSESSID +

+ +
+
+
Type :
+
Essentiel
+
+ +
+
Durée de Vie :
+
Automatique Ă  la fermeture du site (Session)
+
+ +
+
Objectif :
+
Utilisé par le serveur web pour stocker l'état de la session utilisateur. Nécessaire pour le bon fonctionnement des interactions sur le site.
+
+
+
+ +
+ +

+ __cf_bm +

+ +
+
+
Type :
+
Sécurité / Tiers
+
+ +
+
Durée de Vie :
+
30 minutes
+
+ +
+
Objectif :
+
Cookie de Cloudflare utilisé pour la gestion des bots. Permet d'améliorer la performance et la sécurité du site en distinguant le trafic humain du trafic automatisé.
+
+
+
+ +
+ +

+ __cf_clearance +

+ +
+
+
Type :
+
Sécurité / Tiers
+
+ +
+
Durée de Vie :
+
1 an
+
+ +
+
Objectif :
+
Cookie de Cloudflare utilisé pour des raisons de sécurité afin d'identifier le trafic de confiance. Nécessaire pour accéder au site si un défi de sécurité est présenté.
+
+
+
+
+ + + +
+

Informations complémentaires sur les cookies Cloudflare :

+ + + + + Consulter la politique de cookies de Cloudflare + +
+
+ + + +
+

+ + + + Gestion des Cookies +

+ +

Vous pouvez Ă  tout moment choisir de dĂ©sactiver certains cookies via les paramĂštres de votre navigateur. Veuillez noter que la dĂ©sactivation des cookies essentiels peut dĂ©grader votre expĂ©rience de navigation et empĂȘcher l'utilisation de certaines fonctionnalitĂ©s du site. +

+ +
+ + + + + + Comment maĂźtriser les cookies (CNIL) + +

+ (Lien externe vers la CNIL, autorité française de protection des données) +

+
+
+
+
+
+{% endblock %} diff --git a/templates/legal/hebergement.html.twig b/templates/legal/hebergement.html.twig new file mode 100644 index 0000000..ff4db2e --- /dev/null +++ b/templates/legal/hebergement.html.twig @@ -0,0 +1,148 @@ +{% extends 'base.twig' %} + +{% block title %}Hébergement et Infos Techniques - Ludikevent{% endblock %} + +{% block body %} +
+ + {# --- NOUVEAU BLOC EN-TÊTE --- #} +
+ Ludikevent +

+ Informations Légales et d'Hébergement +

+

Transparence technique et conformité infrastructure

+
+
+ {# --- FIN DU BLOC EN-TÊTE --- #} + +
+ +
+

+ + + + ResponsabilitĂ©s - Éditeur et OpĂ©rateur Technique +

+ +
+
+

+ + + + Responsabilité Technique +

+
+

SARL SITECONSEIL

+

27 RUE LE SERURIER, 02100 ST-QUENTIN

+

SIRET : 41866405800025

+

Contact : s.com@siteconseil.fr

+
+
+ +
+

Infrastructure Cloud

+

Empreinte carbone : 0,017 tCO₂e / mois

+
+

Localisation : Pays-Bas

+

Région Google Cloud : eu-west4

+
+
+ +
+

Éditeur du Site

+
+

LUDIKEVENT

+

SIRET : 930 488 408 00012

+

6, rue du ChĂąteau 02800 Danizy

+

lilian@ludikevent.fr

+
+
+
+
+ +

+ Services Techniques et Prestataires +

+ +
+ +
+

Gestion Sous-domaines

+
+
    +
  • s3.esy-web.dev
  • +
  • sentry.esy-web.dev
  • +
  • mail.esy-web.dev
  • +
+
+
+ +
+

Cloudflare

+

Protection Anti-DDoS et Proxy DNS géré par SARL SITECONSEIL.

+
+

Sécurité & CDN

+
+
+ +
+

Monitoring (Sentry)

+

Analyse d'erreurs en temps réel pour garantir la disponibilité du service.

+
+

AUTO-HÉBERGÉ

+
+
+ +
+

Noms de Domaine

+

Gestion des domaines et enregistrements DNS.

+
+

‱ OVH SAS

+

‱ Cloudflare Inc.

+
+
+ +
+

+ + Esy Mail (Envoi d'E-mails) +

+
+
+

Serveur interne : mail.esy-web.dev (Auto-hébergé).

+

Relais externe : Amazon SES (Simple Email Service).

+
+
+ IMPORTANT : En raison de cette architecture, Amazon SES peut potentiellement accéder au contenu technique des e-mails envoyés pour assurer la délivrabilité. +
+
+
+
+ +
+

+ + Conformité Légale et Sécurité +

+
+

Sécurité : Protection multicouche via GCP, Cloudflare et monitoring Sentry.

+

RGPD : Données traitées exclusivement dans l'Union Européenne (Pays-Bas).

+

Loi Française : Conformité à l'Art. 227-24 du Code Pénal (ContrÎle d'ùge).

+
+ +
+

+ HĂ©bergeur technique : SARL SITECONSEIL – Non responsable du contenu Ă©ditorial. +

+ + SIGNALER UNE INFRACTION + +
+
+ +
+
+{% endblock %} diff --git a/templates/legal/mentions.html.twig b/templates/legal/mentions.html.twig index 1a8c692..5b32a94 100644 --- a/templates/legal/mentions.html.twig +++ b/templates/legal/mentions.html.twig @@ -1,4 +1,141 @@ {% extends 'base.twig' %} -{% block title %}Mentions légal{% endblock %} + +{% block title %}Mentions légales - Ludikevent{% endblock %} + {% block body %} +
+
+ + {# En-tĂȘte avec Logo #} +
+ Ludikevent +

+ Mentions Légales +

+

Mise Ă  jour le 19 janvier 2026

+
+ + {# 1er Bloc : Entreprise #} +
+
+

1. Entreprise

+
+
+
+

Ludikevent - Lilian SEGARD

+

SIRET : 930 488 408 00012

+

Statut : Entreprise individuelle (EI)

+

SiĂšge social :
6 Rue du Chñteau – 02800 Danizy

+
+ +
+
+ + {# 2Ăšme Bloc : Description des services #} +
+

2. Description des services fournis

+
+

+ Le site www.{{ app.request.host }} a pour objet de proposer des prestations de location de structures gonflables, de jeux d’extĂ©rieurs, de Barnum et de tous matĂ©riels Ă©vĂ©nementiels. +

+
+

La plateforme permet aux utilisateurs :

+
    +
  • D'effectuer des demandes de devis personnalisĂ©es ;
  • +
  • De procĂ©der Ă  la signature Ă©lectronique des devis ;
  • +
  • D'effectuer le paiement sĂ©curisĂ© des prestations ;
  • +
  • De dĂ©poser et gĂ©rer les cautions liĂ©es aux locations.
  • +
+
+

+ Le propriĂ©taire du site s’efforce de fournir sur le site des informations aussi prĂ©cises que possible. Toutefois, il ne pourra ĂȘtre tenue responsable des omissions ou des inexactitudes. Toutes les informations proposĂ©es sont donnĂ©es Ă  titre indicatif et sont susceptibles d’évoluer, y compris pour de futurs services complĂ©mentaires. +

+
+
+ + {# 3Úme Bloc : Propriété Intellectuelle #} +
+

3. Propriété intellectuelle et contrefaçons

+
+

Le propriĂ©taire du site est propriĂ©taire des droits de propriĂ©tĂ© intellectuelle ou dĂ©tient les droits d’usage sur tous les Ă©lĂ©ments accessibles (textes, images, graphismes, logo, icĂŽnes, sons, logiciels
).

+

Toute reproduction, représentation, modification ou publication totale ou partielle des éléments du site est interdite, sauf autorisation écrite préalable.

+

+ Toute exploitation non autorisĂ©e sera considĂ©rĂ©e comme constitutive d’une contrefaçon et poursuivie conformĂ©ment aux dispositions des articles L.335-2 et suivants du Code de PropriĂ©tĂ© Intellectuelle. +

+
+
+ + {# 4Ăšme Bloc : Liens et Cookies #} +
+

4. Liens hypertextes et cookies

+
+

Le site peut contenir des liens vers des sites tiers. Le propriétaire ne peut vérifier le contenu des sites ainsi visités et décline toute responsabilité quant aux risques de contenus illicites.

+
+

Gestion des Cookies :

+

+ La navigation est susceptible de provoquer l’installation de cookie(s) visant Ă  faciliter la navigation ultĂ©rieure et Ă  permettre diverses mesures de frĂ©quentation. L’utilisateur peut configurer son navigateur pour bloquer ces installations. +

+
+
+
+ + {# 5Úme Bloc : Données Personnelles #} +
+

5. Gestion des données personnelles

+
+

Le propriĂ©taire ne collecte des informations personnelles que pour le besoin de certains services (devis, contact, signature). L’utilisateur fournit ces informations en toute connaissance de cause.

+
+

DROITS DE L'UTILISATEUR (RGPD) :

+

Tout utilisateur dispose d’un droit d’accùs, de rectification et de suppression par email : segard.lilian@gmail.com

+
+
+
+ + {# 6Ăšme Bloc : Droit applicable #} +
+

6. Droit applicable et juridiction

+
+

Tout litige est soumis au droit français. Il est fait attribution exclusive de juridiction aux tribunaux compétents de Saint-Quentin.

+
+
+ + {# 7Ăšme Bloc : Lexique #} +
+

7. Lexique

+
+

Utilisateur : Internaute se connectant et utilisant le site susnommé.

+

Informations personnelles : « les informations qui permettent l’identification des personnes physiques auxquelles elles s’appliquent » (art. 4 loi n° 78-17).

+
+
+ + {# 8Ăšme Bloc : Textes de loi #} +
+

8. Textes de loi applicables

+
+

‱ Loi n° 78-17 du 6 janvier 1978 (Informatique et LibertĂ©s)

+

‱ Loi n° 2004-575 du 21 juin 2004 (Confiance dans l’économie numĂ©rique)

+

‱ RĂšglement GĂ©nĂ©ral sur la Protection des DonnĂ©es personnelles (RGPD)

+
+
+ + {# Footer final #} +
+

© {{ "now"|date("Y") }} Ludikevent - Lilian SEGARD – Entrepreneur Individuel

+
+ +
+
{% endblock %} diff --git a/templates/legal/rgpd.html.twig b/templates/legal/rgpd.html.twig new file mode 100644 index 0000000..f253743 --- /dev/null +++ b/templates/legal/rgpd.html.twig @@ -0,0 +1,106 @@ +{% extends 'base.twig' %} + +{% block title %}Politique de Confidentialité - Ludikevent{% endblock %} + +{% block body %} +
+
+ + {# Header #} +
+ Ludikevent +

Protection des Données (RGPD)

+

Sécurité et confidentialité de vos informations

+
+ +
+ +
+

Date de la derniĂšre mise Ă  jour : 23/04/2025

+
+ + {# Article 1 #} +
+

Présentation de la politique

+

La prĂ©sente politique dĂ©crit les mĂ©thodes que Ludikevent emploie pour la collecte, l'utilisation, la protection et le partage des donnĂ©es personnelles de nos clients. Vos donnĂ©es sont traitĂ©es sur le fondement de votre consentement, de la nĂ©cessitĂ© d'exĂ©cuter un contrat ou de nos intĂ©rĂȘts lĂ©gitimes.

+
+ + {# Article 2 #} +
+

Données personnelles collectées

+

Nous collectons des données selon trois catégories :

+
    +
  • DonnĂ©es communiquĂ©es : Nom, adresse, email, tĂ©lĂ©phone, date de naissance et informations de paiement lors de vos demandes de devis ou commandes.
  • +
  • DonnĂ©es automatisĂ©es : Adresse IP, identifiants d'appareils (UDID/MEID), cookies et donnĂ©es de gĂ©olocalisation (avec votre accord).
  • +
  • Autres sources : Informations publiques issues des rĂ©seaux sociaux ou de partenaires marketing.
  • +
+
+ + {# Article 3 #} +
+

Utilisation des données

+

Vos informations sont utilisées pour :

+
    +
  • Honorer vos commandes, traiter les paiements et assurer le service client.
  • +
  • AmĂ©liorer nos produits et personnaliser votre expĂ©rience en ligne.
  • +
  • Garantir la sĂ©curitĂ© de nos rĂ©seaux et prĂ©venir la fraude.
  • +
  • Vous envoyer des offres marketing (uniquement avec votre consentement).
  • +
+
+ + {# Article 4 #} +
+

Partage des informations

+

Nous ne vendons aucune donnée personnelle. Elles sont partagées uniquement avec :

+
    +
  • Nos prestataires de services (paiement, informatique, marketing).
  • +
  • Livraison : Vos coordonnĂ©es sont transmises aux transporteurs pour acheminer votre matĂ©riel.
  • +
  • Les autoritĂ©s en cas d'obligation lĂ©gale.
  • +
+
+ + {# Article 5 #} +
+

Vos droits

+

Conformément au RGPD, vous disposez d'un droit d'accÚs, de rectification, de suppression, d'opposition et de portabilité de vos données. Vous pouvez retirer votre consentement à tout moment.

+

Pour exercer vos droits, contactez-nous : segard.lilian@gmail.com

+
+ + {# Article 6 #} +
+

Cookies et Publicité

+

Nous utilisons des cookies et balises Web pour vous identifier et mesurer la performance du site. Vous pouvez configurer votre navigateur pour les refuser, mais certaines fonctionnalitĂ©s du site pourraient ĂȘtre limitĂ©es.

+
+ + {# Article 7 #} +
+

Additif Technique (Sécurité)

+

Chiffrement AES-256 : La totalitĂ© des donnĂ©es est chiffrĂ©e. Les clĂ©s d'accĂšs subissent une rotation automatique toutes les 24 heures, rendant les donnĂ©es inaccessibles mĂȘme pour l'hĂ©bergeur ou le propriĂ©taire.

+

Hébergement : Google Cloud Platform (GCP) sécurisé par TLS/SSL via Cloudflare et Let's Encrypt.

+
+

DPO (SiteConseil) : Philippe LEGRAND – legrand@siteconseil.fr

+

Contact technique : rgpd@siteconseil.fr

+
+
+ + {# Article 8 #} +
+

Conservation et Transfert

+

Vos données sont conservées pendant 3 ans à compter de votre derniÚre activité. Elles ne sont pas transférées en dehors de l'Union Européenne.

+
+ + {# Article 9 #} +
+

Contact

+

Pour toute question : lilian@ludikevent.fr ou par courrier au 6, rue du ChĂąteau 02800 DANIZY.

+
+ +
+ + {# Footer #} +
+

Ludikevent - Protection des données personnelles conformément aux directives de la CNIL.

+
+
+
+{% endblock %} diff --git a/templates/mails/root/alert.twig b/templates/mails/root/alert.twig new file mode 100644 index 0000000..0320d9d --- /dev/null +++ b/templates/mails/root/alert.twig @@ -0,0 +1,56 @@ +{% extends 'mails/base.twig' %} + +{% block content %} + + + + + + + + + + + Alerte Sécurité Critique + + + Une tentative d'accÚs non autorisée a été bloquée sur la console SuperAdmin. + + + + + + + + Détails de l'incident : + + + + Raison + {{ datas.reason }} + + + Adresse IP + {{ datas.ip }} + + + Host + {{ datas.host }} + + + Date/Heure + {{ datas.date|date('d/m/Y H:i:s') }} + + + + + + + + + User-Agent :
+ {{ datas.userAgent }} +
+
+
+{% endblock %} diff --git a/templates/root/console.twig b/templates/root/console.twig new file mode 100644 index 0000000..b6f6cba --- /dev/null +++ b/templates/root/console.twig @@ -0,0 +1,159 @@ +{% extends 'base.twig' %} + +{% block title %}Console SuperAdmin - SiteConseil{% endblock %} + +{% block body %} +
+ + {# --- BARRE ROOT CONSOLE v1.0 --- #} +
+
+
+

+ Root Console v1.0 +

+ + Siteconseil Privileged Access + +
+ +
+ + +
+
+ Operator IP + + {{ app.request.headers.get('cf-connecting-ip') ?? app.request.clientIp }} + +
+
+
+
+
+ + {# --- BODY DOUBLE COLONNE --- #} +
+ + {# COLONNE GAUCHE #} +
+ + {# BLOCK : SERVICE HEALTH #} +
+

+ + External Service Health +

+
+ {# Stripe #} +
+ STRIPE API +
+ + {{ stripeStatus == 1 ? 'Online' : 'Offline' }} + +
+
+
+ {# Signature #} +
+ SIGNATURE +
+ + {{ signatureStatus == 1 ? 'Online' : 'Offline' }} + +
+
+
+ {# Esysearch #} +
+ ESYSEARCH +
+ + {{ searchClient == 1 ? 'Online' : 'Offline' }} + +
+
+
+
+
+ + {# BLOCK : CRITICAL COMMANDS #} +
+

+ + Critical Command Center +

+
+
+

Maintenance Mode

+ +
+
+

Live Mode

+ +
+
+
+ + {# BLOCK : OPTIMIZATION #} +
+

+ + System Optimization +

+
+
+

cache:clear

+ +
+
+

liip:cache:clear

+ +
+
+
+
+ + {# --- COLONNE DROITE : LE TERMINAL --- #} +
+
+
+
+
+
+
+ Live System Logs +
+ +
+

[{{ "now"|date('H:i:s') }}] INITIALIZING ROOT INTERFACE...

+

[{{ "now"|date('H:i:s') }}] SESSION_USER: SITECONSEIL_ADMIN

+ +

[{{ "now"|date('H:i:s') }}] DOMAIN_VALIDATION: SUCCESSFUL

+

[{{ "now"|date('H:i:s') }}] IP_OPERATOR_VAL: SUCCESSFUL

+

[{{ "now"|date('H:i:s') }}] SECRET_KEY_VAL: SUCCESSFUL

+ + {# Statuts des services en 1/0 #} +

[{{ "now"|date('H:i:s') }}] STRIPE_VAL: {{ stripeStatus == 1 ? 'ONLINE' : 'OFFLINE' }}

+

[{{ "now"|date('H:i:s') }}] SIGNATURE_VAL: {{ signatureStatus == 1 ? 'ONLINE' : 'OFFLINE' }}

+

[{{ "now"|date('H:i:s') }}] ESYSEARCH_VAL: {{ searchClient == 1 ? 'ONLINE' : 'OFFLINE' }}

+ +
+

+ *** Access granted to system core *** +

+
+ +
+ +

> _

+
+
+
+
+ +{% endblock %}