import Link from "next/link"; import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; import { ArrowLeft } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { prisma } from "@/lib/prisma"; const DAY_MS = 24 * 60 * 60 * 1000; interface WeeklyBucket { weekStart: Date; label: string; count: number; } function thirtyDaysAgoDate(): Date { return new Date(Date.now() - 30 * DAY_MS); } async function getSignupsPerWeek(weeks = 8): Promise { const now = new Date(); const startMs = now.getTime() - weeks * 7 * DAY_MS; const startDate = new Date(startMs); const users = await prisma.user.findMany({ where: { createdAt: { gte: startDate } }, select: { createdAt: true }, }); // Bucketize per week (Senin sebagai start, supaya konsisten dengan kalender Indonesia). const buckets: WeeklyBucket[] = []; for (let i = weeks - 1; i >= 0; i--) { const bucketStart = new Date(now.getTime() - (i + 1) * 7 * DAY_MS); bucketStart.setUTCHours(0, 0, 0, 0); const bucketEnd = new Date(bucketStart.getTime() + 7 * DAY_MS); const count = users.filter( (u) => u.createdAt >= bucketStart && u.createdAt < bucketEnd ).length; buckets.push({ weekStart: bucketStart, label: bucketStart.toLocaleDateString("id-ID", { day: "2-digit", month: "short", }), count, }); } return buckets; } export default async function AdminUserStatsPage() { const session = await getServerSession(authOptions); if (!session?.user) redirect("/login?callbackUrl=/admin/users/stats"); if (!isAdminEmail(session.user.email)) { return (

Halaman ini hanya untuk admin SeTrip.

); } const thirtyDaysAgo = thirtyDaysAgoDate(); const [ totalUsers, suspendedUsers, verifiedOrganizers, activeOrganizers30d, paidParticipants30d, weekly, ] = await Promise.all([ prisma.user.count(), prisma.user.count({ where: { suspended: true } }), prisma.organizerVerification.count({ where: { status: "APPROVED" } }), prisma.user.count({ where: { trips: { some: { createdAt: { gte: thirtyDaysAgo } } }, }, }), prisma.user.count({ where: { bookings: { some: { status: "PAID", createdAt: { gte: thirtyDaysAgo }, }, }, }, }), getSignupsPerWeek(8), ]); const maxWeeklyCount = Math.max(1, ...weekly.map((w) => w.count)); return (
Kembali ke list users

User Analytics

Snapshot pertumbuhan user. Real-time read langsung dari DB — tidak ada cache, refresh halaman untuk angka terbaru.

Signup per Minggu (8 minggu terakhir)

Tiap bar = 1 minggu (mulai hari ini mundur). Angka di atas bar = total signup minggu itu.

{weekly.map((w) => { const heightPct = (w.count / maxWeeklyCount) * 100; return (
{w.count}
{w.label}
); })}
); } function StatCard({ label, value, sub, accent = "neutral", }: { label: string; value: number; sub?: string; accent?: "neutral" | "primary" | "secondary" | "emerald" | "red"; }) { const map = { neutral: "text-neutral-800", primary: "text-primary-700", secondary: "text-secondary-700", emerald: "text-emerald-700", red: "text-red-700", }; return (

{label}

{value}

{sub &&

{sub}

}
); }