From af6ec84cd0b0dec6d0f771c89dcdae786418701d Mon Sep 17 00:00:00 2001 From: streaper2 Date: Sun, 21 Jun 2026 17:24:31 +0200 Subject: [PATCH] maj metric article --- admin/src/app/metrics/page.tsx | 174 ++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 3 deletions(-) diff --git a/admin/src/app/metrics/page.tsx b/admin/src/app/metrics/page.tsx index 395f5ab..84abb5a 100644 --- a/admin/src/app/metrics/page.tsx +++ b/admin/src/app/metrics/page.tsx @@ -14,7 +14,8 @@ import { ArrowUp, ArrowDown, Calendar, - Map as MapIcon + Map as MapIcon, + BookOpen } from 'lucide-react'; import AnalyticsMap from '@/components/AnalyticsMap'; import CountryHistogram from '@/components/CountryHistogram'; @@ -123,10 +124,63 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { }); const uniqueVisitors = uniqueVisitorsRes.length; + // Article page views (impressions) + const pageViewsByPath = await prisma.analyticsEvent.groupBy({ + by: ['path'], + where: { + type: 'PAGE_VIEW', + path: { startsWith: '/actualites/' }, + ...currentWhere + }, + _count: { id: true } + }); + + // Article dwell times (reading times) + const dwellTimeByPath = await prisma.analyticsEvent.groupBy({ + by: ['path'], + where: { + type: 'DWELL_TIME', + path: { startsWith: '/actualites/' }, + ...currentWhere + }, + _avg: { value: true }, + _count: { id: true } + }); + + // Article unique visitors grouping + const uniqueVisitorsRaw = await prisma.analyticsEvent.groupBy({ + by: ['path', 'ip'], + where: { + type: 'PAGE_VIEW', + path: { startsWith: '/actualites/' }, + ...currentWhere + } + }); + + // Load blog posts to associate with analytics + const blogPosts = await prisma.blogPost.findMany({ + select: { + id: true, + title: true, + slug: true, + author: true, + publishedAt: true, + } + }); + + const totalArticleViews = await prisma.analyticsEvent.count({ + where: { + type: 'PAGE_VIEW', + path: { startsWith: '/actualites/' }, + ...currentWhere + } + }); + // 4. Fetch Previous Data (for Deltas) let prevTotalEvents = 0; let prevUniqueVisitors = 0; let prevMobileCount = 0; + let prevArticleViews = 0; if (prevWhere) { prevTotalEvents = await prisma.analyticsEvent.count({ where: prevWhere }); @@ -142,6 +196,14 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { _count: { id: true } }); prevMobileCount = prevDevices.find(d => d.device === 'Mobile')?._count.id || 0; + + prevArticleViews = await prisma.analyticsEvent.count({ + where: { + type: 'PAGE_VIEW', + path: { startsWith: '/actualites/' }, + ...prevWhere + } + }); } // 5. Recent Unique IPs @@ -176,9 +238,52 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { uniqueVisitors, recentIPs, uniqueVisitorsPerCountry, + totalArticleViews, + articleStats: blogPosts.map(post => { + const paths = [`/actualites/${post.id}`]; + if (post.slug) { + paths.push(`/actualites/${post.slug}`); + } + + let views = 0; + pageViewsByPath.forEach(item => { + if (paths.includes(item.path)) { + views += item._count.id; + } + }); + + const uniqueIps = new Set(); + uniqueVisitorsRaw.forEach(item => { + if (paths.includes(item.path) && item.ip) { + uniqueIps.add(item.ip); + } + }); + const visitors = uniqueIps.size; + + let totalDwellTime = 0; + let dwellCount = 0; + dwellTimeByPath.forEach(item => { + if (paths.includes(item.path) && item._avg.value && item._count.id) { + totalDwellTime += item._avg.value * item._count.id; + dwellCount += item._count.id; + } + }); + const avgDwellTime = dwellCount > 0 ? Math.round(totalDwellTime / dwellCount) : 0; + + return { + id: post.id, + title: post.title, + author: post.author, + publishedAt: post.publishedAt, + views, + visitors, + avgDwellTime + }; + }).sort((a, b) => b.views - a.views), deltas: { events: prevTotalEvents > 0 ? ((totalEvents - prevTotalEvents) / prevTotalEvents) * 100 : null, visitors: prevUniqueVisitors > 0 ? ((uniqueVisitors - prevUniqueVisitors) / prevUniqueVisitors) * 100 : null, + articleViews: prevArticleViews > 0 ? ((totalArticleViews - prevArticleViews) / prevArticleViews) * 100 : null, mobile: prevTotalEvents > 0 && totalEvents > 0 ? ((currentMobileCount / totalEvents) - (prevMobileCount / prevTotalEvents)) * 100 : null @@ -189,7 +294,7 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { export default async function MetricsPage({ searchParams: searchParamsPromise }: { searchParams: Promise<{ range?: string, from?: string, to?: string }> }) { const searchParams = await searchParamsPromise; const range = searchParams.range || 'week'; - const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas, uniqueVisitorsPerCountry } = await getMetrics(range, searchParams.from, searchParams.to); + const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas, uniqueVisitorsPerCountry, totalArticleViews, articleStats } = await getMetrics(range, searchParams.from, searchParams.to); const rangeLabels: Record = { day: 'Dernières 24h', @@ -250,7 +355,7 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }: {/* Summary Cards */} -
+
@@ -283,6 +388,22 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:

+
+
+
+ +
+
+
{totalArticleViews.toLocaleString()}
+ +
+
+

Articles consultés

+

+ Vues totales des articles de blog +

+
+
@@ -426,6 +547,53 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
+ {/* Article Statistics Table */} +
+
+

+ Statistiques par article +

+
+
+ + + + + + + + + + + + + {articleStats.map((post) => ( + + + + + + + + + ))} + +
ArticleAuteurVuesV. UniquesTemps de lecture moyenDate de publication
+ {post.title} + + {post.author} + + {post.views.toLocaleString()} + + {post.visitors.toLocaleString()} + + {post.avgDwellTime > 0 ? `${post.avgDwellTime}s` : '---'} + + {post.publishedAt ? new Date(post.publishedAt).toLocaleDateString('fr-FR') : 'Non publié'} +
+
+
+ {/* Recent IP Connections Table */}