MBO-Tech-IT-Webseite/modules/03-analytics/files/app/api/admin/analytics/phone-calls/route.ts

241 lines
7.4 KiB
TypeScript

import { NextResponse, NextRequest } from "next/server";
import { createServiceClient } from "@/lib/supabase";
import { requireAdmin } from "@/lib/admin-auth";
export const dynamic = "force-dynamic";
type DateRange = "today" | "7days" | "30days";
function getDateRange(range: DateRange): { start: string; end: string } {
const end = new Date();
const start = new Date();
switch (range) {
case "today":
start.setHours(0, 0, 0, 0);
break;
case "7days":
start.setDate(start.getDate() - 7);
break;
case "30days":
start.setDate(start.getDate() - 30);
break;
}
return {
start: start.toISOString(),
end: end.toISOString(),
};
}
export async function GET(request: NextRequest) {
// Admin Check
const check = await requireAdmin();
if (check instanceof NextResponse) return check;
const searchParams = request.nextUrl.searchParams;
const range = (searchParams.get("range") || "today") as DateRange;
const { start, end } = getDateRange(range);
try {
const db = createServiceClient();
// ─── 1. KPIs: Calls today, trend, unique numbers ────────────────────────
// Calls today
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const todayISO = todayStart.toISOString();
const { count: todayCount } = await db
.from("phone_clicks")
.select("*", { count: "exact" })
.gte("timestamp", todayISO)
.lte("timestamp", new Date().toISOString());
const callsToday = todayCount || 0;
// Calls yesterday for trend
const yesterdayStart = new Date(todayStart);
yesterdayStart.setDate(yesterdayStart.getDate() - 1);
const yesterdayEnd = new Date(yesterdayStart);
yesterdayEnd.setDate(yesterdayEnd.getDate() + 1);
const yesterdayISO = yesterdayStart.toISOString();
const yesterdayEndISO = yesterdayEnd.toISOString();
const { count: yesterdayCount } = await db
.from("phone_clicks")
.select("*", { count: "exact" })
.gte("timestamp", yesterdayISO)
.lt("timestamp", yesterdayEndISO);
const callsYesterday = yesterdayCount || 0;
const callsTodayTrend =
callsYesterday > 0
? Math.round(((callsToday - callsYesterday) / callsYesterday) * 100)
: callsToday > 0
? 100
: 0;
// Unique Numbers
const { data: uniqueData } = await db
.from("phone_clicks")
.select("phone_number")
.gte("timestamp", start)
.lte("timestamp", end);
const uniqueNumbers = new Set(
(uniqueData || []).map((d) => d.phone_number)
).size;
// Top Number
const numberCounts: Record<string, number> = {};
(uniqueData || []).forEach((item) => {
numberCounts[item.phone_number] =
(numberCounts[item.phone_number] || 0) + 1;
});
const topNumberEntries = Object.entries(numberCounts).sort(
([, a], [, b]) => b - a
);
const [topNumber, topNumberCount] = topNumberEntries[0] || [null, 0];
// Top Source Element
const { data: elementData } = await db
.from("phone_clicks")
.select("source_element")
.gte("timestamp", start)
.lte("timestamp", end);
const elementCounts: Record<string, number> = {};
(elementData || []).forEach((item) => {
elementCounts[item.source_element] =
(elementCounts[item.source_element] || 0) + 1;
});
const totalClicks = elementData?.length || 0;
const elementEntries = Object.entries(elementCounts).sort(
([, a], [, b]) => b - a
);
const [topSourceElement, topSourceElementCount] = elementEntries[0] || [
null,
0,
];
const topSourceElementPercent =
totalClicks > 0
? Math.round((topSourceElementCount / totalClicks) * 100)
: 0;
// ─── 2. Phone Numbers Table ─────────────────────────────────────────────
const { data: tableData } = await db
.from("phone_clicks")
.select("phone_number, source_page, source_element, country")
.gte("timestamp", start)
.lte("timestamp", end);
const phoneNumbersMap: Record<
string,
{
phone_number: string;
click_count: number;
trend_percent: number;
top_source_page: string;
top_source_element: string;
top_country: string;
}
> = {};
(tableData || []).forEach((item) => {
if (!phoneNumbersMap[item.phone_number]) {
phoneNumbersMap[item.phone_number] = {
phone_number: item.phone_number,
click_count: 0,
trend_percent: 0,
top_source_page: "",
top_source_element: "",
top_country: item.country || "—",
};
}
phoneNumbersMap[item.phone_number].click_count++;
});
const phoneNumbers = Object.values(phoneNumbersMap)
.sort((a, b) => b.click_count - a.click_count);
// ─── 3. Element Chart ───────────────────────────────────────────────────
const elements = Object.entries(elementCounts)
.map(([element, count]) => ({
source_element: element,
count,
percent: totalClicks > 0 ? Math.round((count / totalClicks) * 100) : 0,
}))
.sort((a, b) => b.count - a.count);
// ─── 4. Timeseries Chart ────────────────────────────────────────────────
const { data: timeseriesRaw } = await db
.from("phone_clicks")
.select("timestamp")
.gte("timestamp", start)
.lte("timestamp", end)
.order("timestamp", { ascending: true });
const timeseriesMap: Record<string, number> = {};
(timeseriesRaw || []).forEach((item) => {
const date = item.timestamp.split("T")[0];
timeseriesMap[date] = (timeseriesMap[date] || 0) + 1;
});
const timeseries = Object.entries(timeseriesMap).map(([date, count]) => ({
date,
count,
}));
// ─── 5. Geo Chart ───────────────────────────────────────────────────────
const { data: geoRaw } = await db
.from("phone_clicks")
.select("country")
.gte("timestamp", start)
.lte("timestamp", end)
.not("country", "is", null);
const geoCounts: Record<string, number> = {};
(geoRaw || []).forEach((item) => {
if (item.country) {
geoCounts[item.country] = (geoCounts[item.country] || 0) + 1;
}
});
const geoTotal = geoRaw?.length || 0;
const geo = Object.entries(geoCounts)
.map(([country, count]) => ({
country,
count,
percent: geoTotal > 0 ? Math.round((count / geoTotal) * 100) : 0,
}))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
return NextResponse.json({
kpis: {
callsToday,
callsTodayTrend,
uniqueNumbers,
topNumber,
topNumberCount,
topSourceElement,
topSourceElementPercent,
},
phoneNumbers,
elements,
timeseries,
geo,
});
} catch (error) {
console.error("Phone calls analytics error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}