From 454b748973e36e7b207f162d9a878adeba5f36a3 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Tue, 27 Jan 2026 20:24:02 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(UtmEvent):=20Ajoute=20le?= =?UTF-8?q?=20tracking=20Umami=20des=20utilisateurs=20connect=C3=A9s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute l'identification des utilisateurs Umami et enregistre la session. Implémente une bannière de consentement pour les cookies et gère l'état. ``` --- .env | 1 + assets/reserve.js | 120 +++++++++--------- assets/tools/CookieBanner.js | 118 +++++++++++++++++ assets/tools/UtmEvent.js | 66 ++++++++-- migrations/Version20260127191206.php | 35 +++++ src/Command/DeployConfigCommand.php | 62 ++++----- src/Controller/ReserverController.php | 43 +++++++ src/Entity/Customer.php | 37 ++++++ src/Entity/CustomerTracking.php | 65 ++++++++++ src/Repository/CustomerTrackingRepository.php | 43 +++++++ templates/dashboard/base.twig | 1 + templates/revervation/base.twig | 5 +- update.sh | 3 +- 13 files changed, 485 insertions(+), 114 deletions(-) create mode 100644 assets/tools/CookieBanner.js create mode 100644 migrations/Version20260127191206.php create mode 100644 src/Entity/CustomerTracking.php create mode 100644 src/Repository/CustomerTrackingRepository.php diff --git a/.env b/.env index be762c0..ea73d91 100644 --- a/.env +++ b/.env @@ -102,3 +102,4 @@ TVA_ENABLED=false MAINTENANCE_ENABLED=false UMAMI_USER=api UMAMI_PASSWORD=Analytics_8962@ +CLOUDFLARE_DEPLOY=zG2jXpdDqlgZPSz7WwZSalWsEtn7-cQiNyrqaxts diff --git a/assets/reserve.js b/assets/reserve.js index a705804..4c89524 100644 --- a/assets/reserve.js +++ b/assets/reserve.js @@ -1,5 +1,6 @@ import './reserve.scss'; import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js"; +import { CookieBanner } from "./tools/CookieBanner.js"; import * as Turbo from "@hotwired/turbo"; // --- DÉTECTION BOT / PERFORMANCE --- @@ -10,38 +11,54 @@ const isLighthouse = () => { return patterns.some(pattern => userAgent.includes(pattern)); }; -// --- INITIALISATION SENTRY --- -const initSentry = async () => { - if (!isLighthouse() && !window.SentryInitialized) { - try { - const Sentry = await import("@sentry/browser"); - Sentry.init({ - dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24", - tunnel: "/sentry-tunnel", - integrations: [Sentry.browserTracingIntegration()], - tracesSampleRate: 1.0, - }); - window.SentryInitialized = true; // Empêche la ré-initialisation - } catch (e) { - console.warn("Sentry load failed", e); +// --- GESTION DYNAMIQUE DE SENTRY --- +const toggleSentry = async (status) => { + if (isLighthouse()) return; + + try { + const Sentry = await import("@sentry/browser"); + + if (status === 'accepted') { + if (!window.SentryInitialized) { + window.sentryClient = Sentry.init({ + dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24", + tunnel: "/sentry-tunnel", + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, + }); + window.SentryInitialized = true; + console.log("✔️ Sentry initialisé et activé"); + } else { + // Réactivation si déjà chargé + if (window.sentryClient) window.sentryClient.getOptions().enabled = true; + console.log("✔️ Sentry ré-activé"); + } } + + if (status === 'refused' && window.SentryInitialized) { + // Désactivation sans décharger le script + if (window.sentryClient) window.sentryClient.getOptions().enabled = false; + console.log("🛑 Sentry désactivé (Client muet)"); + } + } catch (e) { + console.warn("Sentry toggle failed", e); } }; -// --- LOGIQUE DU LOADER TURBO (Unique à travers les pages) --- +// --- LOGIQUE DU LOADER TURBO --- const initLoader = () => { if (document.getElementById('turbo-loader')) return; - const loaderEl = document.createElement('div'); - loaderEl.id = 'turbo-loader'; - loaderEl.className = 'fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-300 opacity-100 pointer-events-none'; - loaderEl.innerHTML = ` -
-
- Logo -
- `; - document.body.appendChild(loaderEl); + const loaderEl = document.createElement('div'); + loaderEl.id = 'turbo-loader'; + loaderEl.className = 'fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-300 opacity-0 pointer-events-none'; + loaderEl.innerHTML = ` +
+
+ Logo +
+ `; + document.body.appendChild(loaderEl); document.addEventListener("turbo:click", () => { loaderEl.classList.replace('opacity-0', 'opacity-100'); @@ -59,13 +76,11 @@ const initLoader = () => { document.addEventListener("turbo:render", hideLoader); }; -// --- LOGIQUE DU MENU MOBILE (Compatible Turbo) --- +// --- LOGIQUE INTERFACE (Menu, Filtres, Redirect, Register) --- const initMobileMenu = () => { const btn = document.getElementById('menu-button'); const menu = document.getElementById('mobile-menu'); - if (btn && menu) { - // On enlève l'ancien listener pour éviter les doublons au retour arrière btn.onclick = null; btn.addEventListener('click', () => { const isExpanded = btn.getAttribute('aria-expanded') === 'true'; @@ -75,57 +90,46 @@ const initMobileMenu = () => { } }; -// --- LOGIQUE FILTRE CATALOGUE --- const initCatalogueSearch = () => { const filters = document.querySelectorAll('.filter-btn'); const products = document.querySelectorAll('.product-item'); const emptyMsg = document.getElementById('empty-msg'); - if (filters.length === 0) return; - filters.forEach(btn => { btn.onclick = () => { const category = btn.getAttribute('data-filter').toLowerCase(); let count = 0; - - // Update UI des filtres - filters.forEach(f => f.classList.replace('bg-slate-900', 'bg-white')); - filters.forEach(f => f.classList.replace('text-white', 'text-slate-500')); + filters.forEach(f => { + f.classList.replace('bg-slate-900', 'bg-white'); + f.classList.replace('text-white', 'text-slate-500'); + }); btn.classList.replace('bg-white', 'bg-slate-900'); btn.classList.replace('text-slate-500', 'text-white'); - - // Filtrage des produits products.forEach(item => { - const itemCat = item.getAttribute('data-category').toLowerCase(); + const itemCat = (item.getAttribute('data-category') || '').toLowerCase(); const isVisible = category === 'all' || itemCat.includes(category); item.style.display = isVisible ? 'block' : 'none'; if (isVisible) count++; }); - if (emptyMsg) count === 0 ? emptyMsg.classList.remove('hidden') : emptyMsg.classList.add('hidden'); }; }); }; -// --- LOGIQUE DU REDIRECT --- const initAutoRedirect = () => { const container = document.getElementById('payment-check-container'); if (container && container.dataset.autoRedirect) { const url = container.dataset.autoRedirect; setTimeout(() => { - // On vérifie que l'utilisateur est toujours sur la page de paiement - if (document.getElementById('payment-check-container')) { - Turbo.visit(url); - } + if (document.getElementById('payment-check-container')) Turbo.visit(url); }, 10000); } -} +}; + const initRegisterLogic = () => { const siretContainer = document.getElementById('siret-container'); const typeRadios = document.querySelectorAll('input[name="type"]'); - if (!siretContainer || typeRadios.length === 0) return; - const updateSiretVisibility = () => { const selectedType = document.querySelector('input[name="type"]:checked')?.value; if (selectedType === 'buisness') { @@ -136,25 +140,28 @@ const initRegisterLogic = () => { siretContainer.querySelector('input')?.removeAttribute('required'); } }; - - typeRadios.forEach(radio => { - radio.addEventListener('change', updateSiretVisibility); - }); - - // Initialisation au chargement (si redirection avec erreur par exemple) + typeRadios.forEach(radio => radio.addEventListener('change', updateSiretVisibility)); updateSiretVisibility(); }; + // --- INITIALISATION GLOBALE --- document.addEventListener('DOMContentLoaded', () => { - initSentry(); initLoader(); - // Custom Elements (une seule fois suffit) + // Enregistrement Custom Elements if (!customElements.get('utm-event')) customElements.define('utm-event', UtmEvent); if (!customElements.get('utm-account')) customElements.define('utm-account', UtmAccount); + if (!customElements.get('cookie-banner')) customElements.define('cookie-banner', CookieBanner); + + // Initialisation Sentry basée sur le choix existant + const currentConsent = sessionStorage.getItem('ldk_cookie'); + if (currentConsent) toggleSentry(currentConsent); + + // Écouteurs pour changements de choix cookies + window.addEventListener('cookieAccepted', () => toggleSentry('accepted')); + window.addEventListener('cookieRefused', () => toggleSentry('refused')); }); -// À chaque changement de page Turbo document.addEventListener('turbo:load', () => { initMobileMenu(); initCatalogueSearch(); @@ -162,7 +169,6 @@ document.addEventListener('turbo:load', () => { initRegisterLogic(); }); -// Nettoyage avant la mise en cache de Turbo (évite les bugs visuels au retour arrière) document.addEventListener("turbo:before-cache", () => { document.querySelectorAll('.product-item').forEach(i => i.style.display = 'block'); const emptyMsg = document.getElementById('empty-msg'); diff --git a/assets/tools/CookieBanner.js b/assets/tools/CookieBanner.js new file mode 100644 index 0000000..d601cd5 --- /dev/null +++ b/assets/tools/CookieBanner.js @@ -0,0 +1,118 @@ +export class CookieBanner extends HTMLElement { + connectedCallback() { + const currentStatus = sessionStorage.getItem('ldk_cookie'); + + if (currentStatus === 'accepted' || currentStatus === 'refused') { + this.triggerEvent(currentStatus); + this.renderTrigger(currentStatus); // On passe le statut au trigger + return; + } + + if (!currentStatus) { + sessionStorage.setItem('ldk_cookie', 'non'); + } + + this.renderBanner(); + } + + triggerEvent(status) { + const eventName = status === 'accepted' ? 'cookieAccepted' : 'cookieRefused'; + window.dispatchEvent(new CustomEvent(eventName, { + detail: { status, timestamp: new Date().toISOString() } + })); + } + + // Le badge affiche maintenant une couleur selon le statut + renderTrigger(status) { + const colorClass = status === 'accepted' ? 'border-green-500' : 'border-red-500'; + const shadowClass = status === 'accepted' ? 'shadow-green-100' : 'shadow-red-100'; + + this.innerHTML = ` + + `; + this.querySelector('#cookie-reopen').addEventListener('click', () => this.renderBanner()); + } + + renderBanner() { + this.innerHTML = ` + + `; + + this.querySelector('#btn-accept').addEventListener('click', () => this.updateStatus('accepted')); + this.querySelector('#btn-refuse').addEventListener('click', () => this.updateStatus('refused')); + } + + updateStatus(status) { + sessionStorage.setItem('ldk_cookie', status); + + // Synchronisation avec le script Umami + if (status === 'accepted') { + localStorage.removeItem('umami.disabled'); + console.log("✅ Umami activé dans le localStorage"); + } else { + localStorage.setItem('umami.disabled', '1'); + console.log("🛑 Umami désactivé dans le localStorage"); + } + + // Déclenchement de tes Custom Events + this.triggerEvent(status); + + // Animation de sortie + const banner = this.querySelector('#cookie-banner > div'); + if (banner) { + banner.classList.add('opacity-0', 'translate-y-8', 'scale-95'); + setTimeout(() => { + this.renderTrigger(status); + }, 300); + } + } +} + +if (!customElements.get('cookie-banner')) { + customElements.define('cookie-banner', CookieBanner); +} diff --git a/assets/tools/UtmEvent.js b/assets/tools/UtmEvent.js index b1bf454..06b38a1 100644 --- a/assets/tools/UtmEvent.js +++ b/assets/tools/UtmEvent.js @@ -1,19 +1,60 @@ export class UtmAccount extends HTMLElement { - connectedCallback() { + async connectedCallback() { + // 1. Vérification du consentement + if (sessionStorage.getItem('ldk_cookie') !== 'accepted') return; + + // 2. Vérification de la présence d'Umami (nécessaire si tu utilises l'objet global umami ailleurs) if (typeof umami === 'undefined') { - console.warn('Umami script non détecté.'); return; } - const umamiScript = document.querySelector('script[data-website-id]'); - const websiteId = umamiScript ? umamiScript.getAttribute('data-website-id') : null; - umami.identify('user_'+this.getAttribute('id'), { name: this.getAttribute('name'), email: this.getAttribute('email') }); + const userId = this.getAttribute('id'); + const userName = this.getAttribute('name'); + const userEmail = this.getAttribute('email'); + + try { + // 3. Envoi de l'identification à ton instance Umami (Tools Security) + const response = await fetch("https://tools-security.esy-web.dev/api/send", { + method: "POST", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + "type": "identify", + "payload": { + "website": "bc640e0d-43fb-4c3a-bb17-1ac01cec9643", + "screen": `${window.screen.width}x${window.screen.height}`, + "language": navigator.language, + "title": document.title, + "hostname": window.location.hostname, + "url": window.location.pathname, + "referrer": document.referrer, + "id": `user_${userId}`, + "data": { "name": userName, "email": userEmail } + } + }) + }); + + const result = await response.json(); + const sessionId = result.sessionId; + + // 4. Envoi du sessionId à ton backend Symfony + if (sessionId) { + await fetch('/reservation/umami', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ umami_session: sessionId }) + }); + } + + } catch (e) { + } } } + export class UtmEvent extends HTMLElement { connectedCallback() { - // On attend un court instant pour s'assurer qu'umami est chargé - // ou on vérifie s'il existe déjà + // On ne tracke que si les cookies sont acceptés + if (sessionStorage.getItem('ldk_cookie') !== 'accepted') return; + if (typeof umami === 'undefined') { console.warn('Umami script non détecté.'); return; @@ -22,7 +63,7 @@ export class UtmEvent extends HTMLElement { const event = this.getAttribute('event'); const dataRaw = this.getAttribute('data'); - // Extraction dynamique du website-id depuis le script existant + // Extraction dynamique du website-id const umamiScript = document.querySelector('script[data-website-id]'); const websiteId = umamiScript ? umamiScript.getAttribute('data-website-id') : null; @@ -32,11 +73,11 @@ export class UtmEvent extends HTMLElement { } try { - if (event == "click_pdf_product") { + if (event === "click_pdf_product") { const data = JSON.parse(dataRaw); umami.track({ website: websiteId, - name:'Téléchargement document produit', + name: 'Téléchargement document produit', data: data }); } @@ -52,12 +93,9 @@ export class UtmEvent extends HTMLElement { if (event === "view_product" && dataRaw) { const data = JSON.parse(dataRaw); - - // Umami track accepte soit un nom seul, - // soit un objet complet pour des propriétés personnalisées umami.track({ website: websiteId, - name:'Affichage produit', + name: 'Affichage produit', data: data }); } diff --git a/migrations/Version20260127191206.php b/migrations/Version20260127191206.php new file mode 100644 index 0000000..06f4f18 --- /dev/null +++ b/migrations/Version20260127191206.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE customer_tracking (id SERIAL NOT NULL, customer_id INT DEFAULT NULL, track_id VARCHAR(255) NOT NULL, create_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_84B921339395C3F3 ON customer_tracking (customer_id)'); + $this->addSql('ALTER TABLE customer_tracking ADD CONSTRAINT FK_84B921339395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + 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 customer_tracking DROP CONSTRAINT FK_84B921339395C3F3'); + $this->addSql('DROP TABLE customer_tracking'); + } +} diff --git a/src/Command/DeployConfigCommand.php b/src/Command/DeployConfigCommand.php index a1ad7b5..4c08806 100644 --- a/src/Command/DeployConfigCommand.php +++ b/src/Command/DeployConfigCommand.php @@ -17,8 +17,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; )] class DeployConfigCommand extends Command { - private const LIBRARY_RULE_NAME = "EsyCMS Library Cache"; - private const PDF_RULE_NAME = "EsyCMS Disable PDF Cache"; + private const LIBRARY_RULE_NAME = "CRM Cache"; private const CACHE_PHASE = 'http_request_cache_settings'; public function __construct( @@ -31,9 +30,8 @@ class DeployConfigCommand extends Command protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); + $projectDir = $this->parameterBag->get('kernel.project_dir'); - $hostIntranet = "intranet.ludikevent.fr"; - $hostReservation = "reservation.ludikevent.fr"; $mainHost = "ludikevent.fr"; $host = parse_url($mainHost, PHP_URL_HOST) ?: $mainHost; @@ -41,6 +39,7 @@ class DeployConfigCommand extends Command $fqdn = count($parts) >= 2 ? implode('.', array_slice($parts, -2)) : $host; $io->success(sprintf('Hôte principal détecté : %s', $mainHost)); + $io->info(sprintf('FQDN extrait : %s', $fqdn)); // 1. Gestion du cache local $io->section('Gestion du cache local'); @@ -52,7 +51,7 @@ class DeployConfigCommand extends Command $io->note('Dossier esycms-cache local supprimé.'); } - // 2. Configuration Cloudflare + // 2. Configuration Cloudflare via Rulesets $io->section('Configuration Cloudflare (Rulesets)'); $cfToken = $_ENV['CLOUDFLARE_DEPLOY'] ?? null; @@ -76,7 +75,7 @@ class DeployConfigCommand extends Command $zoneId = $data['result'][0]['id']; - // B. Récupération/Création du Ruleset + // B. Récupération ou Création du Ruleset ID pour la phase de cache $rulesetsResponse = $this->httpClient->request('GET', "https://api.cloudflare.com/client/v4/zones/$zoneId/rulesets", [ 'headers' => ['Authorization' => 'Bearer ' . $cfToken] ]); @@ -91,10 +90,15 @@ class DeployConfigCommand extends Command } if (!$rulesetId) { + $io->note('Création du ruleset de cache...'); $createResponse = $this->httpClient->request('POST', "https://api.cloudflare.com/client/v4/zones/$zoneId/rulesets", [ - 'headers' => ['Authorization' => 'Bearer ' . $cfToken, 'Content-Type' => 'application/json'], + 'headers' => [ + 'Authorization' => 'Bearer ' . $cfToken, + 'Content-Type' => 'application/json', + ], 'json' => [ - 'name' => 'EsyCMS Cache Ruleset', + 'name' => 'CRM Cache Ruleset', + 'description' => 'Ruleset pour la gestion du cache CRM', 'kind' => 'zone', 'phase' => self::CACHE_PHASE ] @@ -102,19 +106,20 @@ class DeployConfigCommand extends Command $rulesetId = $createResponse->toArray()['result']['id']; } - // C. Récupération des règles actuelles pour nettoyage + // C. Récupération des règles actuelles pour filtrage $rulesResponse = $this->httpClient->request('GET', "https://api.cloudflare.com/client/v4/zones/$zoneId/rulesets/$rulesetId", [ 'headers' => ['Authorization' => 'Bearer ' . $cfToken] ]); $currentRules = $rulesResponse->toArray()['result']['rules'] ?? []; - // D. Nettoyage des anciennes règles gérées par ce script + // D. Reconstruction de la liste des règles (Sanitization pour éviter l'erreur 400) $sanitizedRules = []; foreach ($currentRules as $rule) { - $desc = $rule['description'] ?? ''; - if ($desc === self::LIBRARY_RULE_NAME || $desc === self::PDF_RULE_NAME) { + if (($rule['description'] ?? '') === self::LIBRARY_RULE_NAME) { continue; } + + // On ne conserve que les champs mutables autorisés par l'API $sanitizedRules[] = [ 'expression' => $rule['expression'], 'description' => $rule['description'] ?? '', @@ -124,32 +129,9 @@ class DeployConfigCommand extends Command ]; } - $hostPart = sprintf('(http.host in {"%s", "%s"})', $hostIntranet, $hostReservation); - - // --- RÈGLE 1 : DESACTIVER LE CACHE POUR LES PDF --- + // E. Ajout de la règle pour /library/ $sanitizedRules[] = [ - 'expression' => "$hostPart and (http.request.uri.path.extension eq \"pdf\")", - 'description' => self::PDF_RULE_NAME, - 'action' => 'set_cache_settings', - 'action_parameters' => [ - 'cache' => false // Désactive explicitement le cache - ], - 'enabled' => true - ]; - - // --- RÈGLE 2 : CACHE LONGUE DURÉE POUR MÉDIAS (Images/Vidéos) --- - $paths = ['/storage', '/media', '/image', '/provider']; - $pathPrefixes = array_map(fn($p) => "starts_with(http.request.uri.path, \"$p/\")", $paths); - $pathPart = "(" . implode(" or ", $pathPrefixes) . ")"; - - $extensions = [ - 'jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg', 'ico', - 'mp4', 'webm', 'ogg', 'mov', 'm4v' - ]; - $extensionPart = '(http.request.uri.path.extension in {"' . implode('", "', $extensions) . '"})'; - - $sanitizedRules[] = [ - 'expression' => "$hostPart and $pathPart and $extensionPart", + 'expression' => sprintf('http.host in {"intranet.ludikevent.fr" "reservation.ludikevent.fr"} and (http.request.uri.path contains "/provider") or (http.request.uri.path contains "/images/image_options") or (http.request.uri.path contains "/images/image_product") or (http.request.uri.path contains "/media")'), 'description' => self::LIBRARY_RULE_NAME, 'action' => 'set_cache_settings', 'action_parameters' => [ @@ -166,7 +148,7 @@ class DeployConfigCommand extends Command 'enabled' => true ]; - // F. Mise à jour Cloudflare + // F. Mise à jour globale du ruleset via PUT $this->httpClient->request('PUT', "https://api.cloudflare.com/client/v4/zones/$zoneId/rulesets/$rulesetId", [ 'headers' => [ 'Authorization' => 'Bearer ' . $cfToken, @@ -177,10 +159,10 @@ class DeployConfigCommand extends Command ] ]); - $io->success('Configuration Cloudflare déployée : Cache désactivé pour les PDF, activé 1 an pour les médias.'); + $io->success(sprintf('Ruleset Cloudflare mis à jour pour %s (Cache 1 an sur /storage/).', $fqdn)); } catch (\Exception $e) { - $io->error('Erreur Cloudflare : ' . $e->getMessage()); + $io->error('Erreur Cloudflare Ruleset : ' . $e->getMessage()); return Command::FAILURE; } diff --git a/src/Controller/ReserverController.php b/src/Controller/ReserverController.php index 3945bff..88ae941 100644 --- a/src/Controller/ReserverController.php +++ b/src/Controller/ReserverController.php @@ -5,11 +5,13 @@ namespace App\Controller; use App\Entity\Account; use App\Entity\AccountResetPasswordRequest; use App\Entity\Customer; +use App\Entity\CustomerTracking; use App\Entity\Product; use App\Form\RequestPasswordConfirmType; use App\Form\RequestPasswordRequestType; use App\Logger\AppLogger; use App\Repository\CustomerRepository; +use App\Repository\CustomerTrackingRepository; use App\Repository\ProductRepository; use App\Service\Mailer\Mailer; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; @@ -60,6 +62,47 @@ class ReserverController extends AbstractController 'products' => $products ]); } + + #[Route('/reservation/umami', name: 'reservation_umami', methods: ['POST'])] + public function umami( + Request $request, + CustomerTrackingRepository $customerTrackingRepository, + EntityManagerInterface $em + ): Response { + /** @var Customer $user */ + $user = $this->getUser(); + if (!$user) { + return new JsonResponse(['error' => 'User not found'], Response::HTTP_UNAUTHORIZED); + } + + $data = json_decode($request->getContent(), true); + $umamiSessionId = $data['umami_session'] ?? null; + + if (!$umamiSessionId) { + return new JsonResponse(['error' => 'No session provided'], Response::HTTP_BAD_REQUEST); + } + + // On cherche si un tracking existe déjà pour cet ID Umami + $track = $customerTrackingRepository->findOneBy(['trackId' => $umamiSessionId]); + + if (!$track) { + $track = new CustomerTracking(); + $track->setTrackId($umamiSessionId); + $track->setCreateAT(new \DateTime()); // Utilise Immutable si possible + $track->setCustomer($user); + + $em->persist($track); + } else { + // Si le track existe déjà mais n'était pas lié à l'utilisateur + if ($track->getCustomer() !== $user) { + $track->setCustomer($user); + } + } + + $em->flush(); + + return new JsonResponse(['status' => 'success']); + } #[Route('/reservation/catalogue', name: 'reservation_catalogue')] public function revervationCatalogue(ProductRepository $productRepository): Response { diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 8f437b8..edfea50 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -89,6 +89,12 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(targetEntity: Contrats::class, mappedBy: 'customer')] private Collection $contrats; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: CustomerTracking::class, mappedBy: 'customer')] + private Collection $customerTrackings; + public function __construct() { $this->customerAddresses = new ArrayCollection(); @@ -99,6 +105,7 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface // Configuration par défaut $this->roles = [self::ROLE_CUSTOMER]; $this->isAccountConfigured = false; + $this->customerTrackings = new ArrayCollection(); } // --- MÉTHODES INTERFACES (SECURITY) --- @@ -320,4 +327,34 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface { $this->verificationCode = $verificationCode; } + + /** + * @return Collection + */ + public function getCustomerTrackings(): Collection + { + return $this->customerTrackings; + } + + public function addCustomerTracking(CustomerTracking $customerTracking): static + { + if (!$this->customerTrackings->contains($customerTracking)) { + $this->customerTrackings->add($customerTracking); + $customerTracking->setCustomer($this); + } + + return $this; + } + + public function removeCustomerTracking(CustomerTracking $customerTracking): static + { + if ($this->customerTrackings->removeElement($customerTracking)) { + // set the owning side to null (unless already changed) + if ($customerTracking->getCustomer() === $this) { + $customerTracking->setCustomer(null); + } + } + + return $this; + } } diff --git a/src/Entity/CustomerTracking.php b/src/Entity/CustomerTracking.php new file mode 100644 index 0000000..f7e6054 --- /dev/null +++ b/src/Entity/CustomerTracking.php @@ -0,0 +1,65 @@ +id; + } + + public function getCustomer(): ?Customer + { + return $this->customer; + } + + public function setCustomer(?Customer $customer): static + { + $this->customer = $customer; + + return $this; + } + + public function getTrackId(): ?string + { + return $this->trackId; + } + + public function setTrackId(string $trackId): static + { + $this->trackId = $trackId; + + return $this; + } + + public function getCreateAT(): ?\DateTime + { + return $this->createAT; + } + + public function setCreateAT(?\DateTime $createAT): static + { + $this->createAT = $createAT; + + return $this; + } +} diff --git a/src/Repository/CustomerTrackingRepository.php b/src/Repository/CustomerTrackingRepository.php new file mode 100644 index 0000000..972eb32 --- /dev/null +++ b/src/Repository/CustomerTrackingRepository.php @@ -0,0 +1,43 @@ + + */ +class CustomerTrackingRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, CustomerTracking::class); + } + + // /** + // * @return CustomerTracking[] Returns an array of CustomerTracking objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('c') + // ->andWhere('c.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('c.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?CustomerTracking + // { + // return $this->createQueryBuilder('c') + // ->andWhere('c.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/templates/dashboard/base.twig b/templates/dashboard/base.twig index b80c2de..c6f65cc 100644 --- a/templates/dashboard/base.twig +++ b/templates/dashboard/base.twig @@ -16,6 +16,7 @@
{# SIDEBAR #} +