import { prisma } from '@/lib/prisma'; import Link from 'next/link'; import { BarChart3, MousePointer2, Clock, Eye, ArrowUpRight, TrendingUp, Globe, Users, Zap, Search, ArrowUp, ArrowDown, Calendar, Map as MapIcon, BookOpen } from 'lucide-react'; import AnalyticsMap from '@/components/AnalyticsMap'; import CountryHistogram from '@/components/CountryHistogram'; function ComparisonBadge({ value, isPercent = true }: { value: number | null, isPercent?: boolean }) { if (value === null) return null; const isPositive = value > 0; const isZero = Math.abs(value) < 0.1; if (isZero) return ( = 0% ); return ( {isPositive ? : } {Math.abs(value).toFixed(1)}{isPercent ? '%' : ''} ); } async function getMetrics(range: string = 'week', from?: string, to?: string) { let startDate: Date; let endDate: Date = new Date(); const now = new Date(); // 1. Determine Current Range if (from && to) { startDate = new Date(from); endDate = new Date(to); endDate.setHours(23, 59, 59, 999); } else { if (range === 'day') startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); else if (range === 'month') startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); else if (range === 'all') startDate = new Date(0); // All time else startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // Default Week } const currentWhere = { createdAt: { gte: startDate, lte: endDate } }; // 2. Determine Previous Range (for comparison) let prevWhere: any = null; if (range !== 'all') { const duration = endDate.getTime() - startDate.getTime(); const prevEndDate = new Date(startDate.getTime() - 1); const prevStartDate = new Date(startDate.getTime() - duration); prevWhere = { createdAt: { gte: prevStartDate, lte: prevEndDate } }; } // 3. Fetch Current Data const topPages = await prisma.analyticsEvent.groupBy({ by: ['path'], where: { type: 'PAGE_VIEW', ...currentWhere }, _count: { id: true }, orderBy: { _count: { id: 'desc' } }, take: 10 }); const topCountries = await prisma.analyticsEvent.groupBy({ by: ['country'], where: { type: 'PAGE_VIEW', ...currentWhere }, _count: { id: true }, orderBy: { _count: { id: 'desc' } }, take: 50 // Increased to get more map data }); // Unique visitors per country const uniqueVisitorsPerCountryRaw = await prisma.analyticsEvent.groupBy({ by: ['country', 'ip'], where: { type: 'PAGE_VIEW', ...currentWhere } }); const uniqueVisitorsPerCountryMap: Record = {}; uniqueVisitorsPerCountryRaw.forEach(item => { const country = item.country || 'Unknown'; uniqueVisitorsPerCountryMap[country] = (uniqueVisitorsPerCountryMap[country] || 0) + 1; }); const uniqueVisitorsPerCountry = Object.entries(uniqueVisitorsPerCountryMap) .map(([country, count]) => ({ country, count })) .sort((a, b) => b.count - a.count); const devices = await prisma.analyticsEvent.groupBy({ by: ['device'], where: currentWhere, _count: { id: true }, orderBy: { _count: { id: 'desc' } } }); const browsers = await prisma.analyticsEvent.groupBy({ by: ['browser'], where: currentWhere, _count: { id: true }, orderBy: { _count: { id: 'desc' } }, take: 5 }); const totalEvents = await prisma.analyticsEvent.count({ where: currentWhere }); const uniqueVisitorsRes = await prisma.analyticsEvent.groupBy({ by: ['ip'], where: currentWhere }); 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 }); const prevVisitorsRes = await prisma.analyticsEvent.groupBy({ by: ['ip'], where: prevWhere }); prevUniqueVisitors = prevVisitorsRes.length; const prevDevices = await prisma.analyticsEvent.groupBy({ by: ['device'], where: prevWhere, _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 const recentUniqueIPs = await prisma.analyticsEvent.groupBy({ by: ['ip'], where: currentWhere, _max: { createdAt: true }, orderBy: { _max: { createdAt: 'desc' } }, take: 20 }); const recentIPs = await Promise.all( recentUniqueIPs.map(async (item) => { if (!item.ip || !item._max.createdAt) return null; return await prisma.analyticsEvent.findFirst({ where: { ip: item.ip, createdAt: item._max.createdAt } }); }) ); const currentMobileCount = devices.find(d => d.device === 'Mobile')?._count.id || 0; return { topPages, topCountries, devices, browsers, totalEvents, 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 } }; } 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, totalArticleViews, articleStats } = await getMetrics(range, searchParams.from, searchParams.to); const rangeLabels: Record = { day: 'Dernières 24h', week: '7 derniers jours', month: '30 derniers jours', all: 'Depuis le début' }; return (

Metrics & Analytics

Comportement des utilisateurs et engagement géographique en temps réel.

Capture : {totalEvents.toLocaleString()} événements total
{/* Controls: Shortcuts + Custom Calendar */}
{Object.entries(rangeLabels).map(([id, label]) => ( {label} ))}
{/* Summary Cards */}
{uniqueVisitors.toLocaleString()}

Visiteurs uniques (IP)

Personnes réelles distinctes

{totalEvents.toLocaleString()}

Impressions (Pages vues)

Nombre total de fois où les pages ont été vues

{totalArticleViews.toLocaleString()}

Articles consultés

Vues totales des articles de blog

{totalEvents > 0 ? Math.round((devices.find(d => d.device === 'Mobile')?._count.id || 0) / totalEvents * 100) : 0}%

Trafic Mobile

Part des appareils mobiles

{/* World Map Section */} {/* Histogram Section */} ({ country: c.country || 'Inconnu', impressions: c._count.id, visitors: uniqueVisitorsPerCountry.find(uv => uv.country === c.country)?.count || 0 }))} />
{/* Top Countries Table */}

TOP 10 - Géographie

{topCountries.map((c) => { const uniqueCount = uniqueVisitorsPerCountry.find(uv => uv.country === c.country)?.count || 0; return ( ); })}
Pays Impressions V. Uniques
{c.country || 'Inconnu'} {c._count.id.toLocaleString()} {uniqueCount.toLocaleString()}
{/* Device & Browser Stats */}

Appareils

{devices.map(d => (
{d.device || 'Autre'} {Math.round(d._count.id / totalEvents * 100)}%
))}

Navigateurs

{browsers.map(b => (
{b._count.id}
{b.browser || 'Autre'}
))}
{/* Top Pages Table (Restored) */}

Pages les plus visitées

{topPages.map((page) => ( ))}
URL / Chemin Vues
{page.path} {page._count.id}
{/* Article Statistics Table */}

Statistiques par article

{articleStats.map((post) => ( ))}
Article Auteur Vues V. Uniques Temps de lecture moyen Date 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 */}

Dernières Connexions (IP)

{recentIPs.map((e, idx) => e && ( ))}
Adresse IP Localisation Appareil / OS Navigateur Dernière Page Date / Heure
{e.ip || '---.---.---.---'}
{e.country || 'Inconnu'} {e.city || '---'}
{e.device} {e.os}
{e.browser} {e.path}
{new Date(e.createdAt).toLocaleDateString('fr-FR')} {new Date(e.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
); }