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 = `
-
-
-
-
- `;
- 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 = `
+
+
+
+
+ `;
+ 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 #}
+