import { Suspense } from "react"; import Link from "next/link"; import { ArrowLeft } from "lucide-react"; import { verifySessionToken } from "@/lib/admin-auth"; import { createServiceClient } from "@/lib/supabase"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import type { Metadata } from "next"; import { AnalyticsTabs } from "@/components/admin/AnalyticsTabs"; export const metadata: Metadata = { title: "Web-Analytics" }; export const dynamic = "force-dynamic"; interface PageView { id: string; path: string; timestamp: string; device_type: string | null; browser: string | null; os: string | null; session_id: string; duration_ms: number | null; } interface FilterOption { label: string; tage: number; beschreibung: string; } const FILTER_OPTIONS: FilterOption[] = [ { label: "Heute", tage: 1, beschreibung: "letzter Tag" }, { label: "Woche", tage: 7, beschreibung: "letzte 7 Tage" }, { label: "Monat", tage: 30, beschreibung: "letzte 30 Tage" }, { label: "3 Monate", tage: 90, beschreibung: "letzte 90 Tage" }, { label: "Jahr", tage: 365, beschreibung: "letztes Jahr" }, ]; // Hilfsfunktionen (aus statistik/page.tsx übernommen) function fmt(n: number, nachkomma = 0) { return n.toLocaleString("de-DE", { minimumFractionDigits: nachkomma, maximumFractionDigits: nachkomma, }); } function balkenBreite(wert: number, max: number): string { if (max === 0) return "0%"; return `${Math.max(4, Math.round((wert / max) * 100))}%`; } function fmtDuration(ms: number | null): string { if (!ms) return "–"; if (ms < 60_000) return `${Math.round(ms / 1000)}s`; return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`; } // Hilfsfunktion: Deutsches Datumsformat (DD.MM.YYYY) function fmtDateDE(isoDate: string): string { const [year, month, day] = isoDate.split("-"); return `${day}.${month}.${year}`; } async function AnalyticsContent({ vonISO, bisISO, tage = null }: { vonISO: string; bisISO: string; tage?: number | null; }) { // ──── Auth Check ──────────────────────────────────────────────────────── const cookieStore = await cookies(); const token = cookieStore.get("admin_session")?.value; if (!token) { redirect("/admin/login"); } const session = await verifySessionToken(token); if (!session) { redirect("/admin/login"); } // ──── Daten abrufen (dynamischer Zeitraum, ohne Bots) ────────────────────────── const db = createServiceClient(); const heute = new Date().toISOString().slice(0, 10); // Konvertiere ISO-Dates zu vollständigen Timestamps für korrekte DB-Filterung const vonTimestamp = vonISO + "T00:00:00Z"; const bisTimestamp = bisISO + "T23:59:59.999Z"; const { data: allViews, error } = await db .from("page_views") .select("id, path, timestamp, device_type, browser, os, session_id, duration_ms") .eq("is_bot", false) .gte("timestamp", vonTimestamp) .lte("timestamp", bisTimestamp) .order("timestamp", { ascending: false }); if (error) { console.error("❌ Analytics Query Error:", error); } const views: PageView[] = allViews ?? []; // ──── KPIs ────────────────────────────────────────────────────────────── const heuteViews = views.filter((v) => v.timestamp.startsWith(heute)); const uniqueSessionsHeute = new Set(heuteViews.map((v) => v.session_id)).size; const mitDauer = views.filter((v) => v.duration_ms != null && v.duration_ms > 0); const avgDuration = mitDauer.length > 0 ? Math.round(mitDauer.reduce((s, v) => s + v.duration_ms!, 0) / mitDauer.length) : 0; const mobileCount = views.filter((v) => v.device_type === "mobile").length; const mobileRate = views.length > 0 ? Math.round((mobileCount / views.length) * 100) : 0; // ──── Tagesstatistik (letzte 30 Tage) ──────────────────────────────────── const tagesMap: Record = {}; for (const v of views) { const tag = v.timestamp.slice(0, 10); tagesMap[tag] = (tagesMap[tag] ?? 0) + 1; } const tagesStats = Object.entries(tagesMap) .sort(([a], [b]) => a.localeCompare(b)) .map(([date, count]) => ({ date, count })); const maxTagesWert = Math.max(0, ...tagesStats.map((s) => s.count)); // ──── Top-Seiten ──────────────────────────────────────────────────────── const pathMap: Record = {}; for (const v of views) { if (!pathMap[v.path]) pathMap[v.path] = { count: 0, durSum: 0, durCount: 0 }; pathMap[v.path].count++; if (v.duration_ms) { pathMap[v.path].durSum += v.duration_ms; pathMap[v.path].durCount++; } } const topSeiten = Object.entries(pathMap) .map(([path, d]) => ({ path, count: d.count, avgDuration: d.durCount > 0 ? Math.round(d.durSum / d.durCount) : null, })) .sort((a, b) => b.count - a.count) .slice(0, 20); const maxPathCount = Math.max(0, ...topSeiten.map((s) => s.count)); // ──── Browser-Verteilung ──────────────────────────────────────────────── const browserMap: Record = {}; for (const v of views) browserMap[v.browser ?? "Other"] = (browserMap[v.browser ?? "Other"] ?? 0) + 1; const sortedBrowsers = Object.entries(browserMap) .sort((a, b) => b[1] - a[1]) .slice(0, 8); // ──── Gerät-Verteilung ────────────────────────────────────────────────── const deviceMap: Record = {}; for (const v of views) deviceMap[v.device_type ?? "desktop"] = (deviceMap[v.device_type ?? "desktop"] ?? 0) + 1; // ──── OS-Verteilung ───────────────────────────────────────────────────── const osMap: Record = {}; for (const v of views) osMap[v.os ?? "Other"] = (osMap[v.os ?? "Other"] ?? 0) + 1; const sortedOs = Object.entries(osMap) .sort((a, b) => b[1] - a[1]) .slice(0, 8); // ──── Filter-Button Label bestimmen ────────────────────────────────── let filterDesc: string; if (tage !== null) { // Schnellauswahl filterDesc = FILTER_OPTIONS.find((f) => f.tage === tage)?.beschreibung || `letzte ${tage} Tage`; } else { // Freier Zeitraum (deutsches Datumsformat) filterDesc = `${fmtDateDE(vonISO)} – ${fmtDateDE(bisISO)}`; } // Debug: Wenn Fehler oder keine Daten if (error) { return (

Web-Analytics

❌ Fehler beim Laden der Daten:

{error.message}

Prüfe in Supabase, ob die page_views Tabelle existiert und RLS korrekt konfiguriert ist.

); } const overviewContent = (
{/* Header */}

Web-Analytics

{filterDesc} · Bots gefiltert · IPs anonymisiert

Zurück
{/* Filter Buttons & Custom Date Range */}
{/* Schnellauswahl-Buttons */}
{FILTER_OPTIONS.map((option) => ( {option.label} ))}
{/* Datumsauswahl-Formular */}
{/* KPI-Cards */}
Seitenaufrufe heute
{heuteViews.length}
Unique Sessions heute
{uniqueSessionsHeute}
Ø Verweildauer
{fmtDuration(avgDuration)}
Mobile-Anteil
{mobileRate}%
{/* Tagesdiagramm */}

Seitenaufrufe ({filterDesc})

{tagesStats.length === 0 ? (

Keine Daten vorhanden

) : ( tagesStats.map(({ date, count }) => (
{date}
{count}
)) )}
{/* Top-Seiten */}

Top-Seiten

{topSeiten.length === 0 ? ( ) : ( topSeiten.map(({ path, count, avgDuration }) => ( )) )}
Seite Aufrufe Ø Verweildauer
Keine Daten vorhanden
{path}
{count}
{fmtDuration(avgDuration)}
{/* Browser & OS Verteilung */}
{/* Browser */}

Browser

{sortedBrowsers.length === 0 ? (

Keine Daten vorhanden

) : ( sortedBrowsers.map(([browser, count]) => (
{browser}
s[1]))), }} />
{count}
)) )}
{/* OS */}

Betriebssystem

{sortedOs.length === 0 ? (

Keine Daten vorhanden

) : ( sortedOs.map(([os, count]) => (
{os}
s[1]))), }} />
{count}
)) )}
{/* Gerät-Verteilung */}

Geräte-Verteilung

{Object.entries(deviceMap).map(([device, count]) => (
{count}
{device === "desktop" ? "Desktop" : device === "tablet" ? "Tablet" : "Mobile"}
{views.length > 0 ? `${Math.round((count / views.length) * 100)}%` : "0%"}
))}
{/* DSGVO-Hinweis */}

🔒 Datenschutz: IP-Adressen werden anonymisiert (letztes Oktett = 0). Es werden keine Cookies für Analytics gesetzt. Session-IDs sind Tab-gebunden und werden nicht persistent gespeichert. Weitere Details in der{" "} Datenschutzerklärung .

); return ; } export default async function AnalyticsPage({ searchParams }: { searchParams: Promise> }) { const params = await searchParams; const von = String(params.von || ""); const bis = String(params.bis || ""); const tageParam = String(params.tage || ""); const heute = new Date().toISOString().slice(0, 10); let vonISO: string; let bisISO: string; let tage: number | null; // Hilfsfunktion: Datum um X Tage zurück (ohne Zeitzone-Probleme) function datumMinusTage(isoDate: string, tage: number): string { const [year, month, day] = isoDate.split("-").map(Number); const date = new Date(year, month - 1, day); date.setDate(date.getDate() - tage); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } // Berechne den Zeitraum basierend auf Parametern if (von && bis) { // Freier Datumsbereich vonISO = von; bisISO = bis; tage = null; // null bedeutet: freier Bereich, nicht Schnellauswahl } else if (tageParam) { // Schnellauswahl: von heute rückwärts tage = Math.max(1, Math.min(365, parseInt(tageParam, 10) || 30)); bisISO = heute; vonISO = datumMinusTage(heute, tage); } else { // Standard: 30 Tage von heute rückwärts tage = 30; bisISO = heute; vonISO = datumMinusTage(heute, 30); } return (

Lädt Analytics-Daten...

} >
); }