241 lines
7.4 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|