```
✨ feat(UtmEvent): Ajoute le tracking Umami des utilisateurs connectés.
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.
```
This commit is contained in:
1
.env
1
.env
@@ -102,3 +102,4 @@ TVA_ENABLED=false
|
|||||||
MAINTENANCE_ENABLED=false
|
MAINTENANCE_ENABLED=false
|
||||||
UMAMI_USER=api
|
UMAMI_USER=api
|
||||||
UMAMI_PASSWORD=Analytics_8962@
|
UMAMI_PASSWORD=Analytics_8962@
|
||||||
|
CLOUDFLARE_DEPLOY=zG2jXpdDqlgZPSz7WwZSalWsEtn7-cQiNyrqaxts
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import './reserve.scss';
|
import './reserve.scss';
|
||||||
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
|
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
|
||||||
|
import { CookieBanner } from "./tools/CookieBanner.js";
|
||||||
import * as Turbo from "@hotwired/turbo";
|
import * as Turbo from "@hotwired/turbo";
|
||||||
|
|
||||||
// --- DÉTECTION BOT / PERFORMANCE ---
|
// --- DÉTECTION BOT / PERFORMANCE ---
|
||||||
@@ -10,38 +11,54 @@ const isLighthouse = () => {
|
|||||||
return patterns.some(pattern => userAgent.includes(pattern));
|
return patterns.some(pattern => userAgent.includes(pattern));
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- INITIALISATION SENTRY ---
|
// --- GESTION DYNAMIQUE DE SENTRY ---
|
||||||
const initSentry = async () => {
|
const toggleSentry = async (status) => {
|
||||||
if (!isLighthouse() && !window.SentryInitialized) {
|
if (isLighthouse()) return;
|
||||||
try {
|
|
||||||
const Sentry = await import("@sentry/browser");
|
try {
|
||||||
Sentry.init({
|
const Sentry = await import("@sentry/browser");
|
||||||
dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24",
|
|
||||||
tunnel: "/sentry-tunnel",
|
if (status === 'accepted') {
|
||||||
integrations: [Sentry.browserTracingIntegration()],
|
if (!window.SentryInitialized) {
|
||||||
tracesSampleRate: 1.0,
|
window.sentryClient = Sentry.init({
|
||||||
});
|
dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24",
|
||||||
window.SentryInitialized = true; // Empêche la ré-initialisation
|
tunnel: "/sentry-tunnel",
|
||||||
} catch (e) {
|
integrations: [Sentry.browserTracingIntegration()],
|
||||||
console.warn("Sentry load failed", e);
|
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 = () => {
|
const initLoader = () => {
|
||||||
if (document.getElementById('turbo-loader')) return;
|
if (document.getElementById('turbo-loader')) return;
|
||||||
|
|
||||||
const loaderEl = document.createElement('div');
|
const loaderEl = document.createElement('div');
|
||||||
loaderEl.id = 'turbo-loader';
|
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.className = 'fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-300 opacity-0 pointer-events-none';
|
||||||
loaderEl.innerHTML = `
|
loaderEl.innerHTML = `
|
||||||
<div class="relative flex items-center justify-center">
|
<div class="relative flex items-center justify-center">
|
||||||
<div class="absolute w-24 h-24 border-4 border-[#f39e36] border-t-transparent rounded-full animate-spin"></div>
|
<div class="absolute w-24 h-24 border-4 border-[#f39e36] border-t-transparent rounded-full animate-spin"></div>
|
||||||
<img src="/provider/images/favicon.webp" class="w-12 h-12 relative z-10 animate-pulse" alt="Logo">
|
<img src="/provider/images/favicon.webp" class="w-12 h-12 relative z-10 animate-pulse" alt="Logo">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(loaderEl);
|
document.body.appendChild(loaderEl);
|
||||||
|
|
||||||
document.addEventListener("turbo:click", () => {
|
document.addEventListener("turbo:click", () => {
|
||||||
loaderEl.classList.replace('opacity-0', 'opacity-100');
|
loaderEl.classList.replace('opacity-0', 'opacity-100');
|
||||||
@@ -59,13 +76,11 @@ const initLoader = () => {
|
|||||||
document.addEventListener("turbo:render", hideLoader);
|
document.addEventListener("turbo:render", hideLoader);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- LOGIQUE DU MENU MOBILE (Compatible Turbo) ---
|
// --- LOGIQUE INTERFACE (Menu, Filtres, Redirect, Register) ---
|
||||||
const initMobileMenu = () => {
|
const initMobileMenu = () => {
|
||||||
const btn = document.getElementById('menu-button');
|
const btn = document.getElementById('menu-button');
|
||||||
const menu = document.getElementById('mobile-menu');
|
const menu = document.getElementById('mobile-menu');
|
||||||
|
|
||||||
if (btn && menu) {
|
if (btn && menu) {
|
||||||
// On enlève l'ancien listener pour éviter les doublons au retour arrière
|
|
||||||
btn.onclick = null;
|
btn.onclick = null;
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const isExpanded = btn.getAttribute('aria-expanded') === 'true';
|
const isExpanded = btn.getAttribute('aria-expanded') === 'true';
|
||||||
@@ -75,57 +90,46 @@ const initMobileMenu = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- LOGIQUE FILTRE CATALOGUE ---
|
|
||||||
const initCatalogueSearch = () => {
|
const initCatalogueSearch = () => {
|
||||||
const filters = document.querySelectorAll('.filter-btn');
|
const filters = document.querySelectorAll('.filter-btn');
|
||||||
const products = document.querySelectorAll('.product-item');
|
const products = document.querySelectorAll('.product-item');
|
||||||
const emptyMsg = document.getElementById('empty-msg');
|
const emptyMsg = document.getElementById('empty-msg');
|
||||||
|
|
||||||
if (filters.length === 0) return;
|
if (filters.length === 0) return;
|
||||||
|
|
||||||
filters.forEach(btn => {
|
filters.forEach(btn => {
|
||||||
btn.onclick = () => {
|
btn.onclick = () => {
|
||||||
const category = btn.getAttribute('data-filter').toLowerCase();
|
const category = btn.getAttribute('data-filter').toLowerCase();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
filters.forEach(f => {
|
||||||
// Update UI des filtres
|
f.classList.replace('bg-slate-900', 'bg-white');
|
||||||
filters.forEach(f => f.classList.replace('bg-slate-900', 'bg-white'));
|
f.classList.replace('text-white', 'text-slate-500');
|
||||||
filters.forEach(f => f.classList.replace('text-white', 'text-slate-500'));
|
});
|
||||||
btn.classList.replace('bg-white', 'bg-slate-900');
|
btn.classList.replace('bg-white', 'bg-slate-900');
|
||||||
btn.classList.replace('text-slate-500', 'text-white');
|
btn.classList.replace('text-slate-500', 'text-white');
|
||||||
|
|
||||||
// Filtrage des produits
|
|
||||||
products.forEach(item => {
|
products.forEach(item => {
|
||||||
const itemCat = item.getAttribute('data-category').toLowerCase();
|
const itemCat = (item.getAttribute('data-category') || '').toLowerCase();
|
||||||
const isVisible = category === 'all' || itemCat.includes(category);
|
const isVisible = category === 'all' || itemCat.includes(category);
|
||||||
item.style.display = isVisible ? 'block' : 'none';
|
item.style.display = isVisible ? 'block' : 'none';
|
||||||
if (isVisible) count++;
|
if (isVisible) count++;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (emptyMsg) count === 0 ? emptyMsg.classList.remove('hidden') : emptyMsg.classList.add('hidden');
|
if (emptyMsg) count === 0 ? emptyMsg.classList.remove('hidden') : emptyMsg.classList.add('hidden');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- LOGIQUE DU REDIRECT ---
|
|
||||||
const initAutoRedirect = () => {
|
const initAutoRedirect = () => {
|
||||||
const container = document.getElementById('payment-check-container');
|
const container = document.getElementById('payment-check-container');
|
||||||
if (container && container.dataset.autoRedirect) {
|
if (container && container.dataset.autoRedirect) {
|
||||||
const url = container.dataset.autoRedirect;
|
const url = container.dataset.autoRedirect;
|
||||||
setTimeout(() => {
|
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);
|
}, 10000);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const initRegisterLogic = () => {
|
const initRegisterLogic = () => {
|
||||||
const siretContainer = document.getElementById('siret-container');
|
const siretContainer = document.getElementById('siret-container');
|
||||||
const typeRadios = document.querySelectorAll('input[name="type"]');
|
const typeRadios = document.querySelectorAll('input[name="type"]');
|
||||||
|
|
||||||
if (!siretContainer || typeRadios.length === 0) return;
|
if (!siretContainer || typeRadios.length === 0) return;
|
||||||
|
|
||||||
const updateSiretVisibility = () => {
|
const updateSiretVisibility = () => {
|
||||||
const selectedType = document.querySelector('input[name="type"]:checked')?.value;
|
const selectedType = document.querySelector('input[name="type"]:checked')?.value;
|
||||||
if (selectedType === 'buisness') {
|
if (selectedType === 'buisness') {
|
||||||
@@ -136,25 +140,28 @@ const initRegisterLogic = () => {
|
|||||||
siretContainer.querySelector('input')?.removeAttribute('required');
|
siretContainer.querySelector('input')?.removeAttribute('required');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
typeRadios.forEach(radio => radio.addEventListener('change', updateSiretVisibility));
|
||||||
typeRadios.forEach(radio => {
|
|
||||||
radio.addEventListener('change', updateSiretVisibility);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialisation au chargement (si redirection avec erreur par exemple)
|
|
||||||
updateSiretVisibility();
|
updateSiretVisibility();
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- INITIALISATION GLOBALE ---
|
// --- INITIALISATION GLOBALE ---
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initSentry();
|
|
||||||
initLoader();
|
initLoader();
|
||||||
|
|
||||||
// Custom Elements (une seule fois suffit)
|
// Enregistrement Custom Elements
|
||||||
if (!customElements.get('utm-event')) customElements.define('utm-event', UtmEvent);
|
if (!customElements.get('utm-event')) customElements.define('utm-event', UtmEvent);
|
||||||
if (!customElements.get('utm-account')) customElements.define('utm-account', UtmAccount);
|
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', () => {
|
document.addEventListener('turbo:load', () => {
|
||||||
initMobileMenu();
|
initMobileMenu();
|
||||||
initCatalogueSearch();
|
initCatalogueSearch();
|
||||||
@@ -162,7 +169,6 @@ document.addEventListener('turbo:load', () => {
|
|||||||
initRegisterLogic();
|
initRegisterLogic();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Nettoyage avant la mise en cache de Turbo (évite les bugs visuels au retour arrière)
|
|
||||||
document.addEventListener("turbo:before-cache", () => {
|
document.addEventListener("turbo:before-cache", () => {
|
||||||
document.querySelectorAll('.product-item').forEach(i => i.style.display = 'block');
|
document.querySelectorAll('.product-item').forEach(i => i.style.display = 'block');
|
||||||
const emptyMsg = document.getElementById('empty-msg');
|
const emptyMsg = document.getElementById('empty-msg');
|
||||||
|
|||||||
118
assets/tools/CookieBanner.js
Normal file
118
assets/tools/CookieBanner.js
Normal file
@@ -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 = `
|
||||||
|
<button id="cookie-reopen" title="Modifier vos préférences cookies"
|
||||||
|
class="fixed bottom-4 right-4 bg-white border-2 ${colorClass} ${shadowClass} p-3 rounded-full shadow-lg hover:scale-110 transition-all z-40 text-2xl duration-300 flex items-center justify-center">
|
||||||
|
<span class="relative flex h-3 w-3 mr-1">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full ${status === 'accepted' ? 'bg-green-400' : 'bg-red-400'} opacity-75"></span>
|
||||||
|
<span class="relative inline-flex rounded-full h-3 w-3 ${status === 'accepted' ? 'bg-green-500' : 'bg-red-500'}"></span>
|
||||||
|
</span>
|
||||||
|
🍪
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
this.querySelector('#cookie-reopen').addEventListener('click', () => this.renderBanner());
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBanner() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<div id="cookie-banner" class="fixed bottom-4 left-4 right-4 md:left-auto md:max-w-md z-50">
|
||||||
|
<div class="bg-white border border-gray-200 rounded-xl shadow-2xl p-6 transition-all duration-300 transform scale-100 opacity-100">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
🍪 Gestion des cookies
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="mt-2 text-sm text-gray-600 leading-relaxed">
|
||||||
|
En acceptant les cookies, vous permettez au site d'activer des outils qui mesurent la <strong>vitesse du site</strong> et la <strong>liste des pages vues</strong>. Ces analyses nous permettent d'améliorer le site en continu. Ces données sont <strong>non revendues</strong> !
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 p-3 bg-gray-50 rounded-lg border border-gray-100">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span class="flex h-2 w-2 rounded-full bg-blue-500"></span>
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-wider text-gray-700">Cookies techniques essentiels</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[12px] text-gray-500 leading-snug">
|
||||||
|
Ces cookies sont <strong>obligatoires</strong> pour permettre la navigation, la sécurisation de vos accès et le processus de réservation. Ils ne peuvent pas être désactivés.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-col sm:flex-row gap-3">
|
||||||
|
<button id="btn-accept" class="flex-1 bg-slate-900 hover:bg-slate-800 text-white text-sm font-semibold py-2.5 px-4 rounded-lg transition-all active:scale-95">
|
||||||
|
Accepter tout
|
||||||
|
</button>
|
||||||
|
<button id="btn-refuse" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 text-sm font-medium py-2.5 px-4 rounded-lg transition-all">
|
||||||
|
Refuser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex justify-center gap-4 text-[11px] text-gray-400">
|
||||||
|
<a href="/reservation/cookies" class="underline hover:text-gray-600">Politique de cookies</a>
|
||||||
|
<a href="/reservation/rgpd" class="underline hover:text-gray-600">Charte RGPD</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -1,19 +1,60 @@
|
|||||||
export class UtmAccount extends HTMLElement {
|
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') {
|
if (typeof umami === 'undefined') {
|
||||||
console.warn('Umami script non détecté.');
|
|
||||||
return;
|
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 {
|
export class UtmEvent extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
// On attend un court instant pour s'assurer qu'umami est chargé
|
// On ne tracke que si les cookies sont acceptés
|
||||||
// ou on vérifie s'il existe déjà
|
if (sessionStorage.getItem('ldk_cookie') !== 'accepted') return;
|
||||||
|
|
||||||
if (typeof umami === 'undefined') {
|
if (typeof umami === 'undefined') {
|
||||||
console.warn('Umami script non détecté.');
|
console.warn('Umami script non détecté.');
|
||||||
return;
|
return;
|
||||||
@@ -22,7 +63,7 @@ export class UtmEvent extends HTMLElement {
|
|||||||
const event = this.getAttribute('event');
|
const event = this.getAttribute('event');
|
||||||
const dataRaw = this.getAttribute('data');
|
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 umamiScript = document.querySelector('script[data-website-id]');
|
||||||
const websiteId = umamiScript ? umamiScript.getAttribute('data-website-id') : null;
|
const websiteId = umamiScript ? umamiScript.getAttribute('data-website-id') : null;
|
||||||
|
|
||||||
@@ -32,11 +73,11 @@ export class UtmEvent extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (event == "click_pdf_product") {
|
if (event === "click_pdf_product") {
|
||||||
const data = JSON.parse(dataRaw);
|
const data = JSON.parse(dataRaw);
|
||||||
umami.track({
|
umami.track({
|
||||||
website: websiteId,
|
website: websiteId,
|
||||||
name:'Téléchargement document produit',
|
name: 'Téléchargement document produit',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -52,12 +93,9 @@ export class UtmEvent extends HTMLElement {
|
|||||||
|
|
||||||
if (event === "view_product" && dataRaw) {
|
if (event === "view_product" && dataRaw) {
|
||||||
const data = JSON.parse(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({
|
umami.track({
|
||||||
website: websiteId,
|
website: websiteId,
|
||||||
name:'Affichage produit',
|
name: 'Affichage produit',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
35
migrations/Version20260127191206.php
Normal file
35
migrations/Version20260127191206.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260127191206 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|||||||
)]
|
)]
|
||||||
class DeployConfigCommand extends Command
|
class DeployConfigCommand extends Command
|
||||||
{
|
{
|
||||||
private const LIBRARY_RULE_NAME = "EsyCMS Library Cache";
|
private const LIBRARY_RULE_NAME = "CRM Cache";
|
||||||
private const PDF_RULE_NAME = "EsyCMS Disable PDF Cache";
|
|
||||||
private const CACHE_PHASE = 'http_request_cache_settings';
|
private const CACHE_PHASE = 'http_request_cache_settings';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -31,9 +30,8 @@ class DeployConfigCommand extends Command
|
|||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$projectDir = $this->parameterBag->get('kernel.project_dir');
|
||||||
|
|
||||||
$hostIntranet = "intranet.ludikevent.fr";
|
|
||||||
$hostReservation = "reservation.ludikevent.fr";
|
|
||||||
$mainHost = "ludikevent.fr";
|
$mainHost = "ludikevent.fr";
|
||||||
|
|
||||||
$host = parse_url($mainHost, PHP_URL_HOST) ?: $mainHost;
|
$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;
|
$fqdn = count($parts) >= 2 ? implode('.', array_slice($parts, -2)) : $host;
|
||||||
|
|
||||||
$io->success(sprintf('Hôte principal détecté : %s', $mainHost));
|
$io->success(sprintf('Hôte principal détecté : %s', $mainHost));
|
||||||
|
$io->info(sprintf('FQDN extrait : %s', $fqdn));
|
||||||
|
|
||||||
// 1. Gestion du cache local
|
// 1. Gestion du cache local
|
||||||
$io->section('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é.');
|
$io->note('Dossier esycms-cache local supprimé.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Configuration Cloudflare
|
// 2. Configuration Cloudflare via Rulesets
|
||||||
$io->section('Configuration Cloudflare (Rulesets)');
|
$io->section('Configuration Cloudflare (Rulesets)');
|
||||||
$cfToken = $_ENV['CLOUDFLARE_DEPLOY'] ?? null;
|
$cfToken = $_ENV['CLOUDFLARE_DEPLOY'] ?? null;
|
||||||
|
|
||||||
@@ -76,7 +75,7 @@ class DeployConfigCommand extends Command
|
|||||||
|
|
||||||
$zoneId = $data['result'][0]['id'];
|
$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", [
|
$rulesetsResponse = $this->httpClient->request('GET', "https://api.cloudflare.com/client/v4/zones/$zoneId/rulesets", [
|
||||||
'headers' => ['Authorization' => 'Bearer ' . $cfToken]
|
'headers' => ['Authorization' => 'Bearer ' . $cfToken]
|
||||||
]);
|
]);
|
||||||
@@ -91,10 +90,15 @@ class DeployConfigCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$rulesetId) {
|
if (!$rulesetId) {
|
||||||
|
$io->note('Création du ruleset de cache...');
|
||||||
$createResponse = $this->httpClient->request('POST', "https://api.cloudflare.com/client/v4/zones/$zoneId/rulesets", [
|
$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' => [
|
'json' => [
|
||||||
'name' => 'EsyCMS Cache Ruleset',
|
'name' => 'CRM Cache Ruleset',
|
||||||
|
'description' => 'Ruleset pour la gestion du cache CRM',
|
||||||
'kind' => 'zone',
|
'kind' => 'zone',
|
||||||
'phase' => self::CACHE_PHASE
|
'phase' => self::CACHE_PHASE
|
||||||
]
|
]
|
||||||
@@ -102,19 +106,20 @@ class DeployConfigCommand extends Command
|
|||||||
$rulesetId = $createResponse->toArray()['result']['id'];
|
$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", [
|
$rulesResponse = $this->httpClient->request('GET', "https://api.cloudflare.com/client/v4/zones/$zoneId/rulesets/$rulesetId", [
|
||||||
'headers' => ['Authorization' => 'Bearer ' . $cfToken]
|
'headers' => ['Authorization' => 'Bearer ' . $cfToken]
|
||||||
]);
|
]);
|
||||||
$currentRules = $rulesResponse->toArray()['result']['rules'] ?? [];
|
$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 = [];
|
$sanitizedRules = [];
|
||||||
foreach ($currentRules as $rule) {
|
foreach ($currentRules as $rule) {
|
||||||
$desc = $rule['description'] ?? '';
|
if (($rule['description'] ?? '') === self::LIBRARY_RULE_NAME) {
|
||||||
if ($desc === self::LIBRARY_RULE_NAME || $desc === self::PDF_RULE_NAME) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On ne conserve que les champs mutables autorisés par l'API
|
||||||
$sanitizedRules[] = [
|
$sanitizedRules[] = [
|
||||||
'expression' => $rule['expression'],
|
'expression' => $rule['expression'],
|
||||||
'description' => $rule['description'] ?? '',
|
'description' => $rule['description'] ?? '',
|
||||||
@@ -124,32 +129,9 @@ class DeployConfigCommand extends Command
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$hostPart = sprintf('(http.host in {"%s", "%s"})', $hostIntranet, $hostReservation);
|
// E. Ajout de la règle pour /library/
|
||||||
|
|
||||||
// --- RÈGLE 1 : DESACTIVER LE CACHE POUR LES PDF ---
|
|
||||||
$sanitizedRules[] = [
|
$sanitizedRules[] = [
|
||||||
'expression' => "$hostPart and (http.request.uri.path.extension eq \"pdf\")",
|
'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::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",
|
|
||||||
'description' => self::LIBRARY_RULE_NAME,
|
'description' => self::LIBRARY_RULE_NAME,
|
||||||
'action' => 'set_cache_settings',
|
'action' => 'set_cache_settings',
|
||||||
'action_parameters' => [
|
'action_parameters' => [
|
||||||
@@ -166,7 +148,7 @@ class DeployConfigCommand extends Command
|
|||||||
'enabled' => true
|
'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", [
|
$this->httpClient->request('PUT', "https://api.cloudflare.com/client/v4/zones/$zoneId/rulesets/$rulesetId", [
|
||||||
'headers' => [
|
'headers' => [
|
||||||
'Authorization' => 'Bearer ' . $cfToken,
|
'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) {
|
} catch (\Exception $e) {
|
||||||
$io->error('Erreur Cloudflare : ' . $e->getMessage());
|
$io->error('Erreur Cloudflare Ruleset : ' . $e->getMessage());
|
||||||
return Command::FAILURE;
|
return Command::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ namespace App\Controller;
|
|||||||
use App\Entity\Account;
|
use App\Entity\Account;
|
||||||
use App\Entity\AccountResetPasswordRequest;
|
use App\Entity\AccountResetPasswordRequest;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\CustomerTracking;
|
||||||
use App\Entity\Product;
|
use App\Entity\Product;
|
||||||
use App\Form\RequestPasswordConfirmType;
|
use App\Form\RequestPasswordConfirmType;
|
||||||
use App\Form\RequestPasswordRequestType;
|
use App\Form\RequestPasswordRequestType;
|
||||||
use App\Logger\AppLogger;
|
use App\Logger\AppLogger;
|
||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
|
use App\Repository\CustomerTrackingRepository;
|
||||||
use App\Repository\ProductRepository;
|
use App\Repository\ProductRepository;
|
||||||
use App\Service\Mailer\Mailer;
|
use App\Service\Mailer\Mailer;
|
||||||
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
||||||
@@ -60,6 +62,47 @@ class ReserverController extends AbstractController
|
|||||||
'products' => $products
|
'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')]
|
#[Route('/reservation/catalogue', name: 'reservation_catalogue')]
|
||||||
public function revervationCatalogue(ProductRepository $productRepository): Response
|
public function revervationCatalogue(ProductRepository $productRepository): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\OneToMany(targetEntity: Contrats::class, mappedBy: 'customer')]
|
#[ORM\OneToMany(targetEntity: Contrats::class, mappedBy: 'customer')]
|
||||||
private Collection $contrats;
|
private Collection $contrats;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, CustomerTracking>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: CustomerTracking::class, mappedBy: 'customer')]
|
||||||
|
private Collection $customerTrackings;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->customerAddresses = new ArrayCollection();
|
$this->customerAddresses = new ArrayCollection();
|
||||||
@@ -99,6 +105,7 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
// Configuration par défaut
|
// Configuration par défaut
|
||||||
$this->roles = [self::ROLE_CUSTOMER];
|
$this->roles = [self::ROLE_CUSTOMER];
|
||||||
$this->isAccountConfigured = false;
|
$this->isAccountConfigured = false;
|
||||||
|
$this->customerTrackings = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MÉTHODES INTERFACES (SECURITY) ---
|
// --- MÉTHODES INTERFACES (SECURITY) ---
|
||||||
@@ -320,4 +327,34 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
{
|
{
|
||||||
$this->verificationCode = $verificationCode;
|
$this->verificationCode = $verificationCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, CustomerTracking>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/Entity/CustomerTracking.php
Normal file
65
src/Entity/CustomerTracking.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\CustomerTrackingRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: CustomerTrackingRepository::class)]
|
||||||
|
class CustomerTracking
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'customerTrackings')]
|
||||||
|
private ?Customer $customer = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $trackId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?\DateTime $createAT = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Repository/CustomerTrackingRepository.php
Normal file
43
src/Repository/CustomerTrackingRepository.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\CustomerTracking;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<CustomerTracking>
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
|
|
||||||
{# SIDEBAR #}
|
{# SIDEBAR #}
|
||||||
|
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
|
||||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
|
<aside id="sidebar" class="fixed inset-y-0 left-0 z-40 w-72 bg-white dark:bg-[#1e293b] border-r border-slate-200 dark:border-slate-800 lg:translate-x-0 transition-all duration-300 ease-in-out">
|
||||||
|
|
||||||
<div class="flex items-center px-8 h-20 border-b border-slate-100 dark:border-slate-800">
|
<div class="flex items-center px-8 h-20 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
|||||||
@@ -81,8 +81,8 @@
|
|||||||
|
|
||||||
<body class="bg-gray-50 text-gray-900 font-sans antialiased min-h-screen flex flex-col">
|
<body class="bg-gray-50 text-gray-900 font-sans antialiased min-h-screen flex flex-col">
|
||||||
|
|
||||||
{% if is_granted('ROLE_ADMINADMIN') %}
|
{% if is_granted('ROLE_USER') %}
|
||||||
<utm-account id="{{ app.user.id }}" email="{{ app.user.email }}" name="{{ app.user.username }}"></utm-account>
|
<utm-account id="{{ app.user.id }}" email="{{ app.user.email }}" name="{{ app.user.name }} {{ app.user.surname }}"></utm-account>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# --- NAVIGATION --- #}
|
{# --- NAVIGATION --- #}
|
||||||
@@ -238,6 +238,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<cookie-banner></cookie-banner>
|
||||||
{% block javascripts %}{% endblock %}
|
{% block javascripts %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ GREEN='\033[0;32m'
|
|||||||
CYAN='\033[0;36m'
|
CYAN='\033[0;36m'
|
||||||
RESET='\033[0m' # Reset color to default
|
RESET='\033[0m' # Reset color to default
|
||||||
|
|
||||||
|
sudo update-alternatives --set php /usr/bin/php8.4
|
||||||
echo "${CYAN}####################################${RESET}"
|
echo "${CYAN}####################################${RESET}"
|
||||||
echo "${CYAN}# LUDIKEVENT INTRANET UPDATE START #${RESET}"
|
echo "${CYAN}# LUDIKEVENT INTRANET UPDATE START #${RESET}"
|
||||||
echo "${CYAN}####################################${RESET}"
|
echo "${CYAN}####################################${RESET}"
|
||||||
@@ -13,3 +13,4 @@ ansible-playbook -i ansible/hosts.ini ansible/playbook.yml
|
|||||||
echo "${CYAN}##############${RESET}"
|
echo "${CYAN}##############${RESET}"
|
||||||
echo "${CYAN}# END UPDATE #${RESET}"
|
echo "${CYAN}# END UPDATE #${RESET}"
|
||||||
echo "${CYAN}##############${RESET}"
|
echo "${CYAN}##############${RESET}"
|
||||||
|
sudo update-alternatives --set php /usr/bin/php8.3
|
||||||
|
|||||||
Reference in New Issue
Block a user