Files
e-ticket/templates/admin/analytics.html.twig
Serreau Jovann 176b70650b Add SRI integrity hashes for CDN scripts and replace md5 with xxh128 for cache keys
- Add integrity/crossorigin attributes to chart.js and html5-qrcode CDN scripts
- Replace md5() with hash('xxh128') for Meilisearch cache key generation (non-sensitive context)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:06:00 +01:00

163 lines
7.7 KiB
Twig

{% extends 'admin/base.html.twig' %}
{% block title %}Analytics{% endblock %}
{% block body %}
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page">Analytics</h1>
<div class="flex gap-2">
{% for key, label in {today: "Aujourd'hui", '7d': '7 jours', '30d': '30 jours', all: 'Tout'} %}
<a href="{{ path('app_admin_analytics', {period: key}) }}"
class="px-3 py-1.5 text-xs font-black uppercase tracking-widest border-2 transition-all {{ period == key ? 'border-gray-900 bg-gray-900 text-white' : 'border-gray-300 bg-white hover:bg-gray-100' }}">
{{ label }}
</a>
{% endfor %}
</div>
</div>
{# KPIs #}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="admin-card text-center">
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Visiteurs uniques</p>
<p class="text-3xl font-black">{{ visitors|number_format(0, '.', ' ') }}</p>
</div>
<div class="admin-card text-center">
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Pages vues</p>
<p class="text-3xl font-black">{{ pageviews|number_format(0, '.', ' ') }}</p>
</div>
<div class="admin-card text-center">
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Pages / visiteur</p>
<p class="text-3xl font-black">{{ visitors > 0 ? (pageviews / visitors)|number_format(1) : '0' }}</p>
</div>
<div class="admin-card text-center">
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Bounce Rate</p>
<p class="text-3xl font-black {% if bounce_rate < 40 %}text-green-600{% elseif bounce_rate < 60 %}text-yellow-600{% else %}text-red-600{% endif %}">{{ bounce_rate }}%</p>
</div>
</div>
{# Charts #}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Visiteurs par jour</h2>
<div style="height:150px"><canvas id="chart-visitors"></canvas></div>
</div>
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Pages vues par jour</h2>
<div style="height:150px"><canvas id="chart-pageviews"></canvas></div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{# Top pages #}
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Pages les plus visitees</h2>
{% if top_pages|length > 0 %}
<div class="overflow-x-auto">
<table class="admin-table w-full">
<thead>
<tr>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">URL</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Vues</th>
</tr>
</thead>
<tbody>
{% for page in top_pages %}
<tr>
<td class="text-sm font-bold truncate max-w-[300px]">{{ page.url }}</td>
<td class="text-right font-black text-sm">{{ page.hits }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-sm text-gray-400">Aucune donnee.</p>
{% endif %}
</div>
{# Top referrers #}
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Referrers</h2>
{% if top_referrers|length > 0 %}
<div class="overflow-x-auto">
<table class="admin-table w-full">
<thead>
<tr>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">Source</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Vues</th>
</tr>
</thead>
<tbody>
{% for ref in top_referrers %}
<tr>
<td class="text-sm font-bold truncate max-w-[300px]">{{ ref.referrer }}</td>
<td class="text-right font-black text-sm">{{ ref.hits }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-sm text-gray-400">Aucun referrer.</p>
{% endif %}
</div>
</div>
{# Devices, Browsers, OS #}
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Appareils</h2>
{% for d in devices %}
<div class="flex justify-between py-1 text-sm">
<span class="font-bold text-gray-500">{{ d.deviceType|capitalize }}</span>
<span class="font-black">{{ d.cnt }}</span>
</div>
{% endfor %}
</div>
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Navigateurs</h2>
{% for b in browsers %}
<div class="flex justify-between py-1 text-sm">
<span class="font-bold text-gray-500">{{ b.browser }}</span>
<span class="font-black">{{ b.cnt }}</span>
</div>
{% endfor %}
</div>
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Systemes</h2>
{% for o in os_list %}
<div class="flex justify-between py-1 text-sm">
<span class="font-bold text-gray-500">{{ o.os }}</span>
<span class="font-black">{{ o.cnt }}</span>
</div>
{% endfor %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous">
<script>
(function() {
const labels = {{ chart_labels|json_encode|raw }};
const opts = {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, ticks: { font: { weight: 'bold', size: 10 } } },
y: { beginAtZero: true, ticks: { font: { weight: 'bold', size: 10 } } }
}
};
new Chart(document.getElementById('chart-visitors'), {
type: 'bar',
data: { labels, datasets: [{ data: {{ chart_visitors|json_encode|raw }}, backgroundColor: '#fabf04', borderColor: '#111827', borderWidth: 2 }] },
options: opts
});
new Chart(document.getElementById('chart-pageviews'), {
type: 'line',
data: { labels, datasets: [{ data: {{ chart_pageviews|json_encode|raw }}, borderColor: '#111827', backgroundColor: 'rgba(250,191,4,0.2)', borderWidth: 3, fill: true, tension: 0.3, pointBackgroundColor: '#fabf04', pointBorderColor: '#111827', pointBorderWidth: 2 }] },
options: opts
});
})();
</script>
{% endblock %}