import { ArrowLeftRight, TrendingDown, TrendingUp } from "lucide-react"; import { redirect } from "next/navigation"; import { RevenueChart } from "@/components/admin/revenue-chart"; import { StatsCard } from "@/components/admin/stats-card"; import { CurrencyIcon } from "@/components/shared/currency-icon"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; type EconomyData = { totalCredits: number; totalPixels: number; totalPoints: number; shopPurchases: number; shopCreditsSpent: number; shopPointsSpent: number; marketplaceActive: number; marketplaceSold: number; weekTrades: number; revenueData: { label: string; credits: number; points: number }[]; }; const EMPTY: EconomyData = { totalCredits: 0, totalPixels: 0, totalPoints: 0, shopPurchases: 0, shopCreditsSpent: 0, shopPointsSpent: 0, marketplaceActive: 0, marketplaceSold: 0, weekTrades: 0, revenueData: [], }; async function safeCount(fn: () => Promise): Promise { try { return await fn(); } catch { return 0; } } async function getEconomyData(): Promise { const now = Math.floor(Date.now() / 1000); const todayStart = now - (now % 86400); const weekAgo = now - 7 * 86400; const rangeStart = todayStart - 6 * 86400; try { const [creditsAgg, pixelsAgg, pointsAgg] = await Promise.all([ prisma.user.aggregate({ _sum: { credits: true } }).catch(() => null), prisma.user.aggregate({ _sum: { pixels: true } }).catch(() => null), prisma.user.aggregate({ _sum: { points: true } }).catch(() => null), ]); const totalCredits = creditsAgg?._sum.credits ?? 0; const totalPixels = pixelsAgg?._sum.pixels ?? 0; const totalPoints = pointsAgg?._sum.points ?? 0; let shopPurchases = 0; let shopCreditsSpent = 0; let shopPointsSpent = 0; try { const shopRevenue = await prisma.logsShopPurchases.aggregate({ where: { timestamp: { gte: weekAgo } }, _sum: { costCredits: true, costPoints: true }, _count: true, }); shopPurchases = typeof shopRevenue._count === "number" ? shopRevenue._count : 0; shopCreditsSpent = shopRevenue._sum?.costCredits ?? 0; shopPointsSpent = shopRevenue._sum?.costPoints ?? 0; } catch { /* shop logs table/columns may be missing */ } const [marketplaceActive, marketplaceSold, weekTrades] = await Promise.all( [ safeCount(() => prisma.marketplaceItems.count({ where: { state: 1 } }), ), safeCount(() => prisma.marketplaceItems.count({ where: { state: 2, soldTimestamp: { gte: weekAgo } }, }), ), safeCount(() => prisma.roomTradeLog.count({ where: { timestamp: { gte: weekAgo } } }), ), ], ); const revenueData: EconomyData["revenueData"] = []; for (let i = 6; i >= 0; i--) { const dayStart = todayStart - i * 86400; const date = new Date(dayStart * 1000); revenueData.push({ label: date.toLocaleDateString("en", { weekday: "short" }), credits: 0, points: 0, }); } try { const revRows = await prisma.$queryRaw< { day_bucket: bigint; credits: number | bigint; points: number | bigint }[] >` SELECT FLOOR(timestamp / 86400) AS day_bucket, COALESCE(SUM(cost_credits), 0) AS credits, COALESCE(SUM(cost_points), 0) AS points FROM logs_shop_purchases WHERE timestamp >= ${rangeStart} GROUP BY day_bucket ORDER BY day_bucket `; const revMap = new Map( revRows.map((r) => [ Number(r.day_bucket), { credits: Number(r.credits), points: Number(r.points) }, ]), ); for (let i = 0; i < revenueData.length; i++) { const dayStart = todayStart - (6 - i) * 86400; const bucket = Math.floor(dayStart / 86400); const entry = revMap.get(bucket); if (entry) { revenueData[i] = { ...revenueData[i], ...entry }; } } } catch { /* keep zero revenue series */ } return { totalCredits, totalPixels, totalPoints, shopPurchases, shopCreditsSpent, shopPointsSpent, marketplaceActive, marketplaceSold, weekTrades, revenueData, }; } catch { return EMPTY; } } export default async function EconomyPage() { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.ANALYTICS_VIEW, session.user.rank)) { redirect("/admin"); } const data = await getEconomyData(); return (
} /> } /> } /> } />
Shop Revenue (7d)
Purchases {data.shopPurchases}
Credits spent {data.shopCreditsSpent.toLocaleString()}
Points spent {data.shopPointsSpent.toLocaleString()}
Marketplace
Active listings {data.marketplaceActive}
Sold (7d) {data.marketplaceSold}
Currency Distribution
Credits {data.totalCredits.toLocaleString()}
Pixels {data.totalPixels.toLocaleString()}
Points {data.totalPoints.toLocaleString()}
); }