MBO-Tech-IT-Webseite/components/admin/AnalyticsTabs.tsx

219 lines
8.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { ReactNode, useState, useEffect } from "react";
interface PhoneKpis {
callsToday: number;
callsTodayTrend: number;
uniqueNumbers: number;
topSourceElement: string | null;
topSourceElementPercent: number;
}
interface PhoneNumber {
phone_number: string;
click_count: number;
}
interface ElementStat {
source_element: string;
count: number;
percent: number;
}
interface TimeseriesEntry {
date: string;
count: number;
}
interface PhoneData {
kpis: PhoneKpis;
phoneNumbers: PhoneNumber[];
elements: ElementStat[];
timeseries: TimeseriesEntry[];
}
function balkenBreite(wert: number, max: number): string {
if (max === 0) return "0%";
return `${Math.max(4, Math.round((wert / max) * 100))}%`;
}
function PhoneCallsView() {
const [data, setData] = useState<PhoneData | null>(null);
const [loading, setLoading] = useState(true);
const [range, setRange] = useState("30days");
useEffect(() => {
setLoading(true);
fetch(`/api/admin/analytics/phone-calls?range=${range}`)
.then((r) => r.json())
.then((d) => { setData(d); setLoading(false); })
.catch(() => setLoading(false));
}, [range]);
if (loading) {
return <div className="py-12 text-center text-slate-500">Lädt Phone-Click-Daten</div>;
}
if (!data) {
return <div className="py-12 text-center text-red-500">Fehler beim Laden der Daten</div>;
}
const maxTimeseries = Math.max(0, ...data.timeseries.map((t) => t.count));
const maxElements = Math.max(0, ...data.elements.map((e) => e.count));
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-[#18212f] border border-gray-800 rounded-lg py-6 px-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-bold text-white tracking-tight">Phone-Click-Tracking</h2>
<p className="text-slate-500 text-sm mt-1">Telefon-Link-Klicks · DSGVO-konform</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{[["today", "Heute"], ["7days", "7 Tage"], ["30days", "30 Tage"]].map(([val, label]) => (
<button
key={val}
onClick={() => setRange(val)}
className={`px-3 py-1 rounded text-sm font-medium transition-all ${
range === val ? "bg-orange-500 text-white" : "bg-gray-800 text-slate-400 hover:bg-gray-700 hover:text-white"
}`}
>
{label}
</button>
))}
</div>
</div>
{/* KPI-Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-[#18212f] border border-gray-800 p-4 rounded-lg">
<div className="text-sm text-slate-500">Klicks heute</div>
<div className="text-2xl font-bold text-white">{data.kpis.callsToday}</div>
{data.kpis.callsTodayTrend !== 0 && (
<div className={`text-xs mt-1 ${data.kpis.callsTodayTrend > 0 ? "text-green-400" : "text-red-400"}`}>
{data.kpis.callsTodayTrend > 0 ? "+" : ""}{data.kpis.callsTodayTrend}% vs. gestern
</div>
)}
</div>
<div className="bg-[#18212f] border border-gray-800 p-4 rounded-lg">
<div className="text-sm text-slate-500">Eindeutige Nummern</div>
<div className="text-2xl font-bold text-white">{data.kpis.uniqueNumbers}</div>
</div>
<div className="bg-[#18212f] border border-gray-800 p-4 rounded-lg">
<div className="text-sm text-slate-500">Top Quelle</div>
<div className="text-2xl font-bold text-white">{data.kpis.topSourceElement ?? ""}</div>
{data.kpis.topSourceElementPercent > 0 && (
<div className="text-xs text-slate-500 mt-1">{data.kpis.topSourceElementPercent}% aller Klicks</div>
)}
</div>
<div className="bg-[#18212f] border border-gray-800 p-4 rounded-lg">
<div className="text-sm text-slate-500">Klicks gesamt</div>
<div className="text-2xl font-bold text-white">
{data.phoneNumbers.reduce((s, p) => s + p.click_count, 0)}
</div>
</div>
</div>
{/* Zeitreihe */}
{data.timeseries.length > 0 && (
<div className="bg-[#18212f] border border-gray-800 rounded-lg p-6">
<h3 className="text-base font-bold text-white mb-4">Klicks über Zeit</h3>
<div className="space-y-2">
{data.timeseries.map(({ date, count }) => (
<div key={date} className="flex items-center gap-3">
<div className="w-24 text-sm font-mono text-slate-500">{date}</div>
<div className="flex-1 h-7 bg-[#111925] rounded flex items-center overflow-hidden">
<div className="h-full bg-orange-500" style={{ width: balkenBreite(count, maxTimeseries) }} />
</div>
<div className="w-8 text-right text-sm font-semibold text-white">{count}</div>
</div>
))}
</div>
</div>
)}
{/* Quellen */}
{data.elements.length > 0 && (
<div className="bg-[#18212f] border border-gray-800 rounded-lg p-6">
<h3 className="text-base font-bold text-white mb-4">Klicks nach Quelle</h3>
<div className="space-y-3">
{data.elements.map(({ source_element, count }) => (
<div key={source_element} className="flex items-center gap-3">
<div className="w-36 text-sm font-medium text-slate-400 truncate">{source_element}</div>
<div className="flex-1 h-6 bg-[#111925] rounded overflow-hidden">
<div className="h-full bg-orange-500" style={{ width: balkenBreite(count, maxElements) }} />
</div>
<div className="w-8 text-right text-sm font-semibold text-white">{count}</div>
</div>
))}
</div>
</div>
)}
{/* Nummern-Tabelle */}
{data.phoneNumbers.length > 0 && (
<div className="bg-[#18212f] border border-gray-800 rounded-lg overflow-hidden">
<div className="p-4 border-b border-gray-800">
<h3 className="font-bold text-white">Gerufene Nummern</h3>
</div>
<table className="w-full text-sm">
<thead className="border-b border-gray-800">
<tr className="text-xs text-slate-500 uppercase tracking-wide">
<th className="px-4 py-3 text-left font-medium">Telefonnummer</th>
<th className="px-4 py-3 text-right font-medium">Klicks</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800/50">
{data.phoneNumbers.map((p) => (
<tr key={p.phone_number} className="hover:bg-[#111925] transition-colors">
<td className="px-4 py-3 font-mono text-slate-300">{p.phone_number}</td>
<td className="px-4 py-3 text-right font-semibold text-white">{p.click_count}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{data.phoneNumbers.length === 0 && (
<div className="text-center py-12 text-slate-500">
Noch keine Phone-Klicks im gewählten Zeitraum
</div>
)}
</div>
);
}
export function AnalyticsTabs({ overviewContent }: { overviewContent: ReactNode }) {
const [activeTab, setActiveTab] = useState<"overview" | "phone">("overview");
return (
<div>
<div className="flex gap-1 mb-0 border-b border-gray-800 bg-[#18212f] px-4">
{[
{ key: "overview", label: "Seitenaufrufe" },
{ key: "phone", label: "Phone-Klicks" },
].map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key as "overview" | "phone")}
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px ${
activeTab === tab.key
? "border-orange-500 text-orange-400"
: "border-transparent text-slate-500 hover:text-white"
}`}
>
{tab.label}
</button>
))}
</div>
<div className="pt-6">
{activeTab === "overview" ? overviewContent : <PhoneCallsView />}
</div>
</div>
);
}