partirdezero/templates/home/stats_dashboard.html

210 lines
7.4 KiB
HTML

{% extends "layout.html" %}
{% load static %}
{% block title %}- Tableau de bord{% endblock %}
{% block extra_head %}
{% include "partials/_stats_head.html" %}
{% endblock %}
{% block content %}
<div class="container">
<h1>Tableau de bord statistiques</h1>
<p style="margin:8px 0">
<a href="{% url 'home:stats_charts' %}">→ Voir la page de graphiques</a>
</p>
{% include "partials/_stats_toolbar.html" %}
<section class="stats-grid">
<div class="card">
<h3>Visiteurs uniques (période)</h3>
<div class="kpi">{{ kpi.unique_visitors }}</div>
</div>
<div class="card">
<h3>Visiteurs revenants (période)</h3>
<div class="kpi">{{ kpi.returning_visitors }}</div>
</div>
<div class="card">
<h3>Conversions en utilisateurs (période)</h3>
<div class="kpi">{{ kpi.converted_visitors }}</div>
</div>
<div class="card">
<h3>Utilisateurs (total)</h3>
<div class="kpi">{{ kpi.total_users }}</div>
</div>
<div class="card">
<h3>Nouveaux utilisateurs (période)</h3>
<div class="kpi">{{ kpi.new_users_period }}</div>
</div>
<div class="card">
<h3>Utilisateurs actifs (période)</h3>
<div class="kpi">{{ kpi.active_users_period }}</div>
</div>
<div class="card">
<h3>Cours (publiés / total)</h3>
<div class="kpi">{{ kpi.courses_enabled }} / {{ kpi.total_courses }}</div>
</div>
<div class="card">
<h3>Leçons (total)</h3>
<div class="kpi">{{ kpi.total_lessons }}</div>
</div>
<div class="card">
<h3>Articles de blog (total)</h3>
<div class="kpi">{{ kpi.total_posts }}</div>
</div>
<div class="card">
<h3>Revenus</h3>
{% if revenus_disponibles %}
<div class="kpi"></div>
{% else %}
<div class="kpi" title="Aucune source de données">N/A</div>
{% endif %}
</div>
<div class="card">
<h3>Technique</h3>
{% if technique_disponible %}
<div class="kpi"></div>
{% else %}
<div class="kpi" title="Aucune source de données">N/A</div>
{% endif %}
</div>
</section>
<section class="tables" id="live-activity">
<div class="card" style="grid-column: 1 / -1;">
<h3>Activité en direct (5 dernières minutes)</h3>
<table id="live-activity-table" style="width:100%">
<thead>
<tr><th>Type</th><th>Identité</th><th>Page</th><th>Il y a</th><th>Source</th><th>Pays</th></tr>
</thead>
<tbody>
<tr id="live-empty"><td colspan="6">Chargement…</td></tr>
</tbody>
</table>
</div>
</section>
<section class="charts">
<div class="card">
<h3>Évolution quotidienne</h3>
<canvas id="chartMain" height="120"></canvas>
</div>
</section>
<section class="tables">
<div class="card">
<h3>Nouveaux utilisateurs par jour</h3>
<table>
<thead><tr><th>Jour</th><th>Nb</th></tr></thead>
<tbody>
{% for row in new_users_table %}
<tr><td>{{ row.0 }}</td><td>{{ row.1 }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h3>Nouveaux cours par jour</h3>
<table>
<thead><tr><th>Jour</th><th>Nb</th></tr></thead>
<tbody>
{% for row in new_courses_table %}
<tr><td>{{ row.0 }}</td><td>{{ row.1 }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<section class="tables">
<div class="card">
<h3>Top sources (visiteurs uniques)</h3>
<table>
<thead><tr><th>Source</th><th>Visiteurs</th></tr></thead>
<tbody>
{% for row in top_sources_table %}
<tr><td>{{ row.0 }}</td><td>{{ row.1 }}</td></tr>
{% empty %}
<tr><td colspan="2">Aucune donnée</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h3>Top pays (visiteurs uniques)</h3>
<table>
<thead><tr><th>Pays</th><th>Visiteurs</th></tr></thead>
<tbody>
{% for row in top_countries_table %}
<tr><td>{{ row.0 }}</td><td>{{ row.1 }}</td></tr>
{% empty %}
<tr><td colspan="2">Aucune donnée</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
<script>
const labels = {{ labels_json|safe }};
const series = {{ series_json|safe }};
const ctx = document.getElementById('chartMain');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{label: 'Nouveaux utilisateurs', data: series.new_users, borderColor: '#4f46e5', backgroundColor: 'rgba(79,70,229,.2)', tension:.2},
{label: 'Nouveaux cours', data: series.new_courses, borderColor: '#059669', backgroundColor: 'rgba(5,150,105,.2)', tension:.2},
{label: 'Nouveaux articles', data: series.new_posts, borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,.2)', tension:.2},
{label: 'Activité progression', data: series.activity_progress, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,.2)', tension:.2}
]
},
options: {
responsive: true,
scales: {y: {beginAtZero: true}},
plugins: {legend: {position: 'bottom'}}
}
});
// Live activity polling
function humanizeSeconds(s) {
if (s < 60) return s + 's';
const m = Math.floor(s/60); const r = s % 60;
if (m < 60) return m + 'm' + (r?(' '+r+'s'):'');
const h = Math.floor(m/60); const mr = m % 60; return h + 'h' + (mr?(' '+mr+'m'):'');
}
async function fetchLive() {
try {
const res = await fetch('{% url "home:live_activity" %}', {headers: {'Accept': 'application/json'}});
if (!res.ok) throw new Error('HTTP '+res.status);
const payload = await res.json();
const tbody = document.querySelector('#live-activity-table tbody');
tbody.innerHTML = '';
const items = payload.items || [];
if (items.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td'); td.colSpan = 6; td.textContent = 'Aucune activité récente';
tr.appendChild(td); tbody.appendChild(tr); return;
}
for (const it of items) {
const tr = document.createElement('tr');
const type = document.createElement('td'); type.textContent = it.is_user ? 'Utilisateur' : 'Visiteur';
const ident = document.createElement('td'); ident.textContent = it.is_user ? (it.username || 'Utilisateur') : it.visitor;
const page = document.createElement('td'); page.textContent = it.path || '/';
const ago = document.createElement('td'); ago.textContent = humanizeSeconds(it.seconds_ago);
const src = document.createElement('td'); src.textContent = it.source || '';
const country = document.createElement('td'); country.textContent = it.country || '';
tr.append(type, ident, page, ago, src, country);
tbody.appendChild(tr);
}
} catch (e) {
// silent fail in UI
}
}
fetchLive();
setInterval(fetchLive, 10000);
</script>
{% endblock %}