import { and, count, eq, gte, sql, sum } from "drizzle-orm"; import { ArrowLeftRight } from "lucide-react"; import dynamic from "next/dynamic"; import { redirect } from "next/navigation"; import { getLocale, getTranslations } from "next-intl/server"; import { StatsCard } from "@/components/admin/stats-card"; import { CurrencyIcon } from "@/components/shared/currency-icon"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { db, LogsShopPurchases, MarketplaceItems, RoomTradeLog, User, } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { redisCache } from "@/lib/redis-cache"; const RevenueChart = dynamic( () => import("@/components/admin/revenue-chart").then((m) => ({ default: m.RevenueChart, })), { loading: () => (
), }, ); 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 rawRows(query: ReturnType): Promise { const [rows] = (await db.execute(query)) as unknown as [T[], unknown]; return rows ?? []; } async function getEconomyData( locale: string, ): Promise<{ data: EconomyData; degraded: boolean }> { const now = Math.floor(Date.now() / 1000); const todayStart = now - (now % 86400); const weekAgo = now - 7 * 86400; const rangeStart = todayStart - 6 * 86400; let degraded = false; try { const [creditsAgg, pixelsAgg, pointsAgg] = await Promise.all([ db .select({ total: sum(User.credits) }) .from(User) .catch(() => { degraded = true; return [{ total: null }]; }), db .select({ total: sum(User.pixels) }) .from(User) .catch(() => { degraded = true; return [{ total: null }]; }), db .select({ total: sum(User.points) }) .from(User) .catch(() => { degraded = true; return [{ total: null }]; }), ]); const totalCredits = Number(creditsAgg[0]?.total ?? 0); const totalPixels = Number(pixelsAgg[0]?.total ?? 0); const totalPoints = Number(pointsAgg[0]?.total ?? 0); let shopPurchases = 0; let shopCreditsSpent = 0; let shopPointsSpent = 0; try { const [shopRevenue] = await db .select({ purchases: count(), credits: sum(LogsShopPurchases.costCredits), points: sum(LogsShopPurchases.costPoints), }) .from(LogsShopPurchases) .where(gte(LogsShopPurchases.timestamp, weekAgo)); shopPurchases = Number(shopRevenue?.purchases ?? 0); shopCreditsSpent = Number(shopRevenue?.credits ?? 0); shopPointsSpent = Number(shopRevenue?.points ?? 0); } catch { degraded = true; } const [marketplaceActive, marketplaceSold, weekTrades] = await Promise.all([ safeCount(async () => Number( ( await db .select({ total: count() }) .from(MarketplaceItems) .where(eq(MarketplaceItems.state, 1)) )[0]?.total ?? 0, ), ), safeCount(async () => Number( ( await db .select({ total: count() }) .from(MarketplaceItems) .where( and( eq(MarketplaceItems.state, 2), gte(MarketplaceItems.soldTimestamp, weekAgo), ), ) )[0]?.total ?? 0, ), ), safeCount(async () => Number( ( await db .select({ total: count() }) .from(RoomTradeLog) .where(gte(RoomTradeLog.timestamp, weekAgo)) )[0]?.total ?? 0, ), ), ]); 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(locale, { weekday: "short" }), credits: 0, points: 0, }); } try { const revRows = await rawRows<{ day_bucket: bigint; credits: number | bigint; points: number | bigint; }>(sql` 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 { degraded = true; } return { degraded, data: { totalCredits, totalPixels, totalPoints, shopPurchases, shopCreditsSpent, shopPointsSpent, marketplaceActive, marketplaceSold, weekTrades, revenueData, }, }; } catch { return { data: EMPTY, degraded: true }; } } export default async function EconomyPage() { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.ANALYTICS_VIEW, session.user.rank)) { redirect("/admin"); } const locale = await getLocale(); const t = await getTranslations("pages.admin.analytics.economy"); const tRoot = await getTranslations("pages.admin.analytics"); const { data, degraded } = await redisCache( `admin:analytics:economy:${locale}`, 120, () => getEconomyData(locale), ); return (
{degraded ? (
{tRoot("loadError")}
) : null}
} /> } /> } /> } />
{t("shopRevenueTitle")}
{t("purchases")} {data.shopPurchases}
{t("creditsSpent")} {data.shopCreditsSpent.toLocaleString(locale)}
{t("pointsSpent")} {data.shopPointsSpent.toLocaleString(locale)}
{t("marketplaceTitle")}
{t("activeListings")} {data.marketplaceActive}
{t("sold7d")} {data.marketplaceSold}
{t("currencyDistributionTitle")}
{t("credits")} {data.totalCredits.toLocaleString(locale)}
{t("pixels")} {data.totalPixels.toLocaleString(locale)}
{t("points")} {data.totalPoints.toLocaleString(locale)}
); }