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 %}