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"; import { AdminNav } from "@/components/admin/AdminNav"; export const metadata: Metadata = { title: "Web-Analytics – MBO Tech IT" }; 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" }, ]; 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`; } 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; }) { 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"); const db = createServiceClient(); const heute = new Date().toISOString().slice(0, 10); 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 }); const views: PageView[] = allViews ?? []; 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; 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)); 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)); 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); const deviceMap: Record = {}; for (const v of views) deviceMap[v.device_type ?? "desktop"] = (deviceMap[v.device_type ?? "desktop"] ?? 0) + 1; 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); let filterDesc: string; if (tage !== null) { filterDesc = FILTER_OPTIONS.find((f) => f.tage === tage)?.beschreibung || `letzte ${tage} Tage`; } else { filterDesc = `${fmtDateDE(vonISO)} – ${fmtDateDE(bisISO)}`; } if (error) { return (

Fehler beim Laden der Daten:

{error.message}

); } const overviewContent = (

Web-Analytics

{filterDesc} · Bots gefiltert · IPs anonymisiert

Zurück
{FILTER_OPTIONS.map((option) => ( {option.label} ))}
{[ { label: "Seitenaufrufe heute", wert: heuteViews.length }, { label: "Unique Sessions heute", wert: uniqueSessionsHeute }, { label: "Ø Verweildauer", wert: fmtDuration(avgDuration) }, { label: "Mobile-Anteil", wert: `${mobileRate}%` }, ].map((k) => (
{k.label}
{k.wert}
))}

Seitenaufrufe ({filterDesc})

{tagesStats.length === 0 ? (

Keine Daten vorhanden

) : ( tagesStats.map(({ date, count }) => (
{date}
{count}
)) )}

Top-Seiten

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

Browser

{sortedBrowsers.length === 0 ? (

Keine Daten vorhanden

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

Betriebssystem

{sortedOs.length === 0 ? (

Keine Daten vorhanden

) : ( sortedOs.map(([os, count]) => (
{os}
s[1]))) }} />
{count}
)) )}

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%"}
))}

Datenschutz: IP-Adressen werden anonymisiert (letztes Oktett = 0). Keine Cookies für Analytics. Session-IDs sind Tab-gebunden und nicht persistent.

); 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); 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")}`; } let vonISO: string; let bisISO: string; let tage: number | null; if (von && bis) { vonISO = von; bisISO = bis; tage = null; } else if (tageParam) { tage = Math.max(1, Math.min(365, parseInt(tageParam, 10) || 30)); bisISO = heute; vonISO = datumMinusTage(heute, tage); } else { tage = 30; bisISO = heute; vonISO = datumMinusTage(heute, 30); } return (

Lädt Analytics-Daten…

} >
); }