Some checks failed
lotteryadmin CI / build (push) Has been cancelled
- 各角色仪表盘:KPI 可点击跳转、快捷入口、去重冗余区块,代理/站点降级 analytics - 代理线路:创建玩家权限、侧栏搜索、深链 URL、档案保存与删除预检等修复 - 统一密码最短 6 位;代理模块表格/侧栏背景色一致(admin-table-inset) - 报表、钱包、对账、开奖、结算、配置等模块结构与 i18n 同步更新
586 lines
22 KiB
TypeScript
586 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import dynamic from "next/dynamic";
|
|
import Link from "next/link";
|
|
import { useCallback, useEffect, useMemo, useState, type ReactElement } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { AlertTriangle, BarChart3, ClipboardList, RefreshCw, Shield, TrendingUp, Wallet } from "lucide-react";
|
|
|
|
import { getAdminDashboardByScope } from "@/api/admin-dashboard";
|
|
import { useAdminPlayTypeCatalog } from "@/hooks/use-admin-play-type-catalog";
|
|
import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options";
|
|
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
|
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
|
import {
|
|
DashboardAnalyticsMain,
|
|
DashboardAgentRankingCard,
|
|
DashboardPlayRankingCard,
|
|
} from "@/modules/dashboard/dashboard-analytics-panel";
|
|
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
|
|
import { useDashboardAnalytics } from "@/modules/dashboard/use-dashboard-analytics";
|
|
import {
|
|
formatDashboardMoneyMinor,
|
|
} from "@/modules/dashboard/use-dashboard-analytics";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
|
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
|
import { normalizeAdminLanguage } from "@/i18n";
|
|
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
|
import {
|
|
coerceAdminMinor,
|
|
formatAdminMinorUnits,
|
|
getAdminCurrencyDecimalPlaces,
|
|
} from "@/lib/money";
|
|
import { cn } from "@/lib/utils";
|
|
import { LotteryApiBizError } from "@/types/api/errors";
|
|
import type {
|
|
AdminDashboardLifetimeFinance,
|
|
AdminDashboardPlatformRisk,
|
|
AdminDashboardResultBatchQueue,
|
|
AdminDashboardTodayFinance,
|
|
AdminDashboardWarning,
|
|
} from "@/types/api/admin-dashboard";
|
|
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
|
import type { AdminRiskPoolRow } from "@/types/api/admin-risk";
|
|
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
|
|
|
// recharts 图表组件懒加载,避免 ~200KB 进入主 bundle
|
|
const DashboardKpiCard = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.DashboardKpiCard })),
|
|
{ ssr: false },
|
|
);
|
|
const DashboardPanelCard = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.DashboardPanelCard })),
|
|
{ ssr: false },
|
|
);
|
|
const AbnormalTransferPanelFooter = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.AbnormalTransferPanelFooter })),
|
|
{ ssr: false },
|
|
);
|
|
const CapUsageBar = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.CapUsageBar })),
|
|
{ ssr: false },
|
|
);
|
|
const FinanceStructureChart = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.FinanceStructureChart })),
|
|
{ ssr: false },
|
|
);
|
|
const HotUsageBars = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.HotUsageBars })),
|
|
{ ssr: false },
|
|
);
|
|
const ResultBatchQueueSummary = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.ResultBatchQueueSummary })),
|
|
{ ssr: false },
|
|
);
|
|
const PlatformLifetimePayoutSnapshot = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.PlatformLifetimePayoutSnapshot })),
|
|
{ ssr: false },
|
|
);
|
|
const SettlementStatusChart = dynamic(
|
|
() => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.SettlementStatusChart })),
|
|
{ ssr: false },
|
|
);
|
|
|
|
type HotPlayTab = "4D" | "3D" | "2D" | "special";
|
|
|
|
function formatMoneyMinor(minor: number, currencyCode: string | null): string {
|
|
const safeMinor = coerceAdminMinor(minor);
|
|
const code = (currencyCode ?? "NPR").toUpperCase();
|
|
const decimals = getAdminCurrencyDecimalPlaces(code);
|
|
const major = safeMinor / 10 ** decimals;
|
|
try {
|
|
return new Intl.NumberFormat(getAdminRequestLocale(), {
|
|
style: "currency",
|
|
currency: code,
|
|
minimumFractionDigits: decimals,
|
|
maximumFractionDigits: decimals,
|
|
}).format(major);
|
|
} catch {
|
|
return formatAdminMinorUnits(safeMinor, code, decimals);
|
|
}
|
|
}
|
|
|
|
function drawScopedHref(
|
|
drawId: number | null,
|
|
suffix = "",
|
|
fallback = "/admin/draws",
|
|
): string {
|
|
return drawId != null ? `/admin/draws/${drawId}${suffix}` : fallback;
|
|
}
|
|
|
|
function pendingReviewHref(
|
|
drawId: number | null,
|
|
queue: AdminDashboardResultBatchQueue | null,
|
|
): string {
|
|
if (queue != null && queue.pending_review_total > 0 && queue.first_pending_draw_id != null) {
|
|
return `/admin/draws/${queue.first_pending_draw_id}/review`;
|
|
}
|
|
|
|
return drawScopedHref(drawId, "/review");
|
|
}
|
|
|
|
function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" {
|
|
const raw = normalizedNumber.trim();
|
|
const digits = raw.replace(/\D/g, "");
|
|
const digitLen = digits.length;
|
|
const hasLetter = /[A-Za-z]/.test(raw);
|
|
|
|
if (hasLetter && digitLen < 3) {
|
|
return "special";
|
|
}
|
|
if (digitLen >= 4) {
|
|
return "4D";
|
|
}
|
|
if (digitLen === 3) {
|
|
return "3D";
|
|
}
|
|
if (digitLen === 2) {
|
|
return "2D";
|
|
}
|
|
if (hasLetter) {
|
|
return "special";
|
|
}
|
|
return "other";
|
|
}
|
|
|
|
function topPoolsForTab(pools: AdminRiskPoolRow[], tab: HotPlayTab): AdminRiskPoolRow[] {
|
|
return [...pools]
|
|
.filter((p) => poolPlayCategory(p.normalized_number) === tab)
|
|
.sort((a, b) => (b.usage_ratio ?? 0) - (a.usage_ratio ?? 0))
|
|
.slice(0, 10);
|
|
}
|
|
|
|
export function DashboardConsole(): ReactElement {
|
|
const { t, i18n } = useTranslation(["dashboard", "common"]);
|
|
useAdminCurrencyCatalog();
|
|
useAdminPlayTypeCatalog();
|
|
const todayLabel = useMemo(() => {
|
|
const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language);
|
|
const weekday = t(`date.weekdays.${adminWeekdayKeyForDate()}`, { ns: "common" });
|
|
|
|
return formatAdminCalendarToday(locale, weekday);
|
|
}, [i18n.language, i18n.resolvedLanguage, t]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null);
|
|
const [drawId, setDrawId] = useState<number | null>(null);
|
|
const [finance, setFinance] = useState<AdminDrawFinanceSummaryData | null>(null);
|
|
const [capabilities, setCapabilities] = useState<{ draw_finance_risk: boolean; wallet_transfer_view: boolean } | null>(null);
|
|
const [resultBatchQueue, setResultBatchQueue] = useState<AdminDashboardResultBatchQueue | null>(
|
|
null,
|
|
);
|
|
const [lifetimeFinance, setLifetimeFinance] = useState<AdminDashboardLifetimeFinance | null>(
|
|
null,
|
|
);
|
|
const [todayFinance, setTodayFinance] = useState<AdminDashboardTodayFinance | null>(null);
|
|
const [apiWarnings, setApiWarnings] = useState<AdminDashboardWarning[]>([]);
|
|
const [platformRisk, setPlatformRisk] = useState<AdminDashboardPlatformRisk | null>(null);
|
|
const [riskLocked, setRiskLocked] = useState(0);
|
|
const [riskCap, setRiskCap] = useState(0);
|
|
const [hotPoolSample, setHotPoolSample] = useState<AdminRiskPoolRow[]>([]);
|
|
const [abnormalTransferTotal, setAbnormalTransferTotal] = useState<number | null>(null);
|
|
const [hotTab, setHotTab] = useState<HotPlayTab>("4D");
|
|
const playOptions = useCachedPlayTypeOptions();
|
|
const tRef = useTranslationRef(["dashboard", "common"]);
|
|
|
|
const load = useCallback(async (isRefresh = false) => {
|
|
if (isRefresh) {
|
|
setRefreshing(true);
|
|
} else {
|
|
setLoading(true);
|
|
}
|
|
setError(null);
|
|
setFinance(null);
|
|
setCapabilities(null);
|
|
setResultBatchQueue(null);
|
|
setLifetimeFinance(null);
|
|
setTodayFinance(null);
|
|
setApiWarnings([]);
|
|
setPlatformRisk(null);
|
|
setDrawId(null);
|
|
setRiskLocked(0);
|
|
setRiskCap(0);
|
|
setHotPoolSample([]);
|
|
setAbnormalTransferTotal(null);
|
|
|
|
try {
|
|
const d = await getAdminDashboardByScope({});
|
|
setHall(d.hall);
|
|
|
|
if (d.resolved_draw != null) {
|
|
setDrawId(d.resolved_draw.id);
|
|
}
|
|
|
|
setCapabilities(d.capabilities);
|
|
if (d.finance != null) {
|
|
setFinance(d.finance);
|
|
}
|
|
setResultBatchQueue(d.result_batch_queue);
|
|
setLifetimeFinance(d.lifetime_finance);
|
|
setTodayFinance(d.today_finance);
|
|
setApiWarnings(d.warnings ?? []);
|
|
setPlatformRisk(d.platform_risk);
|
|
if (d.risk != null) {
|
|
setRiskLocked(d.risk.locked_amount);
|
|
setRiskCap(d.risk.cap_amount);
|
|
setHotPoolSample(d.risk.hot_pool_rows);
|
|
}
|
|
setAbnormalTransferTotal(d.abnormal_transfer_total);
|
|
} catch (e) {
|
|
const msg =
|
|
e instanceof LotteryApiBizError ? e.message : tRef.current("warnings.loadFailed");
|
|
setError(msg);
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useAsyncEffect(() => {
|
|
void load(false);
|
|
}, []);
|
|
|
|
const currency =
|
|
todayFinance?.currency_code
|
|
?? lifetimeFinance?.currency_code
|
|
?? finance?.currency_code
|
|
?? null;
|
|
const canFinance = capabilities?.draw_finance_risk ?? false;
|
|
const platformLocked = coerceAdminMinor(platformRisk?.locked_amount);
|
|
const platformCap = coerceAdminMinor(platformRisk?.cap_amount);
|
|
const rawPlatformUsagePct = platformRisk?.usage_percent;
|
|
const useCurrentDrawRisk = drawId != null;
|
|
const capUsageLocked = useCurrentDrawRisk ? riskLocked : platformLocked;
|
|
const capUsageCap = useCurrentDrawRisk ? riskCap : platformCap;
|
|
const capUsagePct =
|
|
capUsageCap > 0
|
|
? (capUsageLocked / capUsageCap) * 100
|
|
: typeof rawPlatformUsagePct === "number" && Number.isFinite(rawPlatformUsagePct)
|
|
? Math.min(100, Math.max(0, rawPlatformUsagePct))
|
|
: platformCap > 0
|
|
? (platformLocked / platformCap) * 100
|
|
: 0;
|
|
const showCapUsageCard = useCurrentDrawRisk || platformRisk != null;
|
|
|
|
const hotRows = useMemo(() => topPoolsForTab(hotPoolSample, hotTab), [hotPoolSample, hotTab]);
|
|
|
|
const pendingReviewTotal = resultBatchQueue?.pending_review_total ?? 0;
|
|
|
|
const analytics = useDashboardAnalytics({
|
|
enabled: canFinance,
|
|
playOptions,
|
|
scope: { siteCode: "", agentNodeId: undefined },
|
|
});
|
|
const showAnalytics = canFinance;
|
|
|
|
return (
|
|
<div className="flex min-w-0 w-full max-w-none flex-col gap-5">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="min-w-0">
|
|
<h1 className="admin-list-title">{t("title")}</h1>
|
|
<p className="mt-0.5 text-xs text-muted-foreground">{todayLabel}</p>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-8"
|
|
disabled={loading || refreshing}
|
|
onClick={() => void load(true)}
|
|
>
|
|
<RefreshCw className={cn("size-3.5", refreshing && "animate-spin")} />
|
|
{t("actions.refresh", { ns: "common" })}
|
|
</Button>
|
|
</div>
|
|
|
|
{error ? (
|
|
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
|
<AlertTitle>{t("notice")}</AlertTitle>
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
|
|
{!loading && capabilities && !capabilities.draw_finance_risk ? (
|
|
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
|
<AlertTitle>{t("notice")}</AlertTitle>
|
|
<AlertDescription>{t("warnings.drawPermission")}</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
|
|
{!loading && apiWarnings.length > 0 ? (
|
|
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
|
<AlertTitle>{t("notice")}</AlertTitle>
|
|
<AlertDescription>{apiWarnings.map((w) => w.message).join(" ")}</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
|
|
<section className="flex min-w-0 flex-col gap-4">
|
|
<DashboardCurrentDrawCard
|
|
key={`${hall?.draw_no ?? "empty"}:${hall?.seconds_to_close ?? 0}:${loading ? "loading" : "ready"}`}
|
|
hall={hall}
|
|
drawId={drawId}
|
|
loading={loading}
|
|
/>
|
|
|
|
{canFinance && !loading && todayFinance ? (
|
|
<div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
<DashboardKpiCard
|
|
label={t("todayBetTotal")}
|
|
value={formatDashboardMoneyMinor(todayFinance.total_bet_minor, currency)}
|
|
icon={<TrendingUp className="size-4" />}
|
|
hint={t("todayBusinessDateHint", { date: todayFinance.business_date })}
|
|
/>
|
|
<DashboardKpiCard
|
|
label={t("todayProfit")}
|
|
signedAmountMinor={todayFinance.approx_house_gross_minor}
|
|
currencyCode={currency}
|
|
icon={<BarChart3 className="size-4" />}
|
|
hint={t("todayPayoutHint", {
|
|
amount: formatDashboardMoneyMinor(todayFinance.total_payout_minor, currency),
|
|
})}
|
|
/>
|
|
<DashboardKpiCard
|
|
label={t("lifetimeProfit")}
|
|
signedAmountMinor={lifetimeFinance?.approx_house_gross_minor}
|
|
currencyCode={currency}
|
|
icon={<Wallet className="size-4" />}
|
|
hint={
|
|
lifetimeFinance
|
|
? t("lifetimeActivityHint", {
|
|
draws: lifetimeFinance.draw_count,
|
|
days: lifetimeFinance.business_day_count,
|
|
})
|
|
: undefined
|
|
}
|
|
value={lifetimeFinance ? undefined : "—"}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
<DashboardPanelCard
|
|
href={pendingReviewHref(drawId, resultBatchQueue)}
|
|
title={t("pendingReviewResults")}
|
|
value={resultBatchQueue != null ? pendingReviewTotal : "—"}
|
|
subtitle={t("resultBatchQueueScope")}
|
|
actionLabel={
|
|
pendingReviewTotal > 0
|
|
? t("actions.reviewNow", { ns: "common" })
|
|
: t("drawDetails")
|
|
}
|
|
icon={<ClipboardList className="size-5" aria-hidden />}
|
|
accent={pendingReviewTotal > 0 ? "destructive" : "muted"}
|
|
highlight={pendingReviewTotal > 0}
|
|
loading={loading}
|
|
>
|
|
{resultBatchQueue != null ? (
|
|
<ResultBatchQueueSummary queue={resultBatchQueue} compact />
|
|
) : null}
|
|
</DashboardPanelCard>
|
|
|
|
<DashboardPanelCard
|
|
href="/admin/wallet/transfer-orders"
|
|
title={t("abnormalTransferOrders")}
|
|
value={abnormalTransferTotal ?? "—"}
|
|
subtitle={t("abnormalTransferScope")}
|
|
actionLabel={t("actions.viewAll", { ns: "common" })}
|
|
icon={<AlertTriangle className="size-5" aria-hidden />}
|
|
accent={(abnormalTransferTotal ?? 0) > 0 ? "warning" : "muted"}
|
|
loading={loading}
|
|
highlight={(abnormalTransferTotal ?? 0) > 0}
|
|
>
|
|
<AbnormalTransferPanelFooter
|
|
total={abnormalTransferTotal}
|
|
walletPermission={capabilities?.wallet_transfer_view ?? false}
|
|
/>
|
|
</DashboardPanelCard>
|
|
|
|
<DashboardPanelCard
|
|
href={drawScopedHref(drawId, "/risk/pools")}
|
|
title={t("riskCapUsage")}
|
|
value={`${capUsagePct.toFixed(1)}%`}
|
|
actionLabel={t("occupancyDetails")}
|
|
icon={<Shield className="size-5" aria-hidden />}
|
|
accent={
|
|
capUsagePct >= 90
|
|
? "destructive"
|
|
: capUsagePct >= 70
|
|
? "primary"
|
|
: "muted"
|
|
}
|
|
loading={loading}
|
|
>
|
|
{showCapUsageCard ? (
|
|
<CapUsageBar
|
|
locked={capUsageLocked}
|
|
cap={capUsageCap}
|
|
usagePct={capUsagePct}
|
|
formatMoney={formatMoneyMinor}
|
|
currency={currency}
|
|
compact
|
|
/>
|
|
) : null}
|
|
</DashboardPanelCard>
|
|
|
|
<DashboardPanelCard
|
|
href="/admin/reports"
|
|
title={t("lifetimePayout")}
|
|
value={
|
|
lifetimeFinance
|
|
? formatMoneyMinor(lifetimeFinance.total_payout_minor, currency)
|
|
: "—"
|
|
}
|
|
subtitle={
|
|
lifetimeFinance
|
|
? t("platformOrderAndTicket", {
|
|
orders: lifetimeFinance.order_count,
|
|
tickets: lifetimeFinance.ticket_item_count,
|
|
})
|
|
: t("states.noResource", { ns: "common" })
|
|
}
|
|
actionLabel={t("actions.viewAll", { ns: "common" })}
|
|
icon={<Wallet className="size-5" aria-hidden />}
|
|
accent="primary"
|
|
loading={loading}
|
|
>
|
|
{lifetimeFinance ? (
|
|
<PlatformLifetimePayoutSnapshot
|
|
finance={lifetimeFinance}
|
|
formatMoney={formatMoneyMinor}
|
|
/>
|
|
) : null}
|
|
</DashboardPanelCard>
|
|
</div>
|
|
</section>
|
|
|
|
<section
|
|
className={cn(
|
|
"grid min-w-0 grid-cols-1 gap-4",
|
|
showAnalytics ? "xl:grid-cols-12" : "xl:grid-cols-1",
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"flex min-w-0 flex-col gap-4",
|
|
showAnalytics ? "xl:col-span-8" : "xl:col-span-12",
|
|
)}
|
|
>
|
|
{showAnalytics ? <DashboardAnalyticsMain analytics={analytics} /> : null}
|
|
|
|
<div className="grid min-w-0 grid-cols-1 gap-4 md:grid-cols-2">
|
|
<Card className="admin-list-card py-0">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 border-b border-border/60 px-4 py-3">
|
|
<CardTitle className="text-sm font-semibold">{t("settlementOverview")}</CardTitle>
|
|
{drawId != null ? (
|
|
<Link
|
|
href="/admin/settlement-batches"
|
|
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 px-2 text-xs")}
|
|
>
|
|
{t("actions.viewAll", { ns: "common" })}
|
|
</Link>
|
|
) : null}
|
|
</CardHeader>
|
|
<CardContent className="px-4 py-4">
|
|
{loading ? (
|
|
<Skeleton className="h-40 w-full" />
|
|
) : finance ? (
|
|
<SettlementStatusChart finance={finance} />
|
|
) : (
|
|
<AdminNoResourceState className="py-10 text-center text-xs text-muted-foreground" />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="admin-list-card py-0">
|
|
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-2 space-y-0 border-b border-border/60 px-4 py-3">
|
|
<CardTitle className="text-sm font-semibold">{t("hotNumbersTop10")}</CardTitle>
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
<div role="tablist" aria-label={t("playDimension")} className="flex gap-0.5">
|
|
{([
|
|
{ value: "4D", label: t("tabs.4d") },
|
|
{ value: "3D", label: t("tabs.3d") },
|
|
{ value: "2D", label: t("tabs.2d") },
|
|
{ value: "special", label: t("tabs.special") },
|
|
] as const).map((tab) => (
|
|
<button
|
|
key={tab.value}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={hotTab === tab.value}
|
|
className={cn(
|
|
"rounded px-2 py-0.5 text-[11px] font-medium transition-colors",
|
|
hotTab === tab.value
|
|
? "bg-primary text-primary-foreground"
|
|
: "text-muted-foreground hover:bg-muted",
|
|
)}
|
|
onClick={() => setHotTab(tab.value)}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="px-4 py-4">
|
|
{loading ? (
|
|
<Skeleton className="h-48 w-full" />
|
|
) : (
|
|
<HotUsageBars rows={hotRows} compact />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{showAnalytics ? (
|
|
<Card className="admin-list-card min-w-0 py-0">
|
|
<CardHeader className="border-b border-border/60 px-4 py-3 pb-0">
|
|
<CardTitle className="text-sm font-semibold">{t("financeStructure")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 py-4">
|
|
{loading ? (
|
|
<Skeleton className="h-52 w-full" />
|
|
) : finance ? (
|
|
<FinanceStructureChart finance={finance} formatMoney={formatMoneyMinor} />
|
|
) : (
|
|
<AdminNoResourceState className="py-10 text-center text-xs text-muted-foreground" />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
|
|
{!showAnalytics ? (
|
|
<Card className="admin-list-card min-w-0 py-0">
|
|
<CardHeader className="border-b border-border/60 px-4 py-3 pb-0">
|
|
<CardTitle className="text-sm font-semibold">{t("financeStructure")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 py-4">
|
|
{loading ? (
|
|
<Skeleton className="h-52 w-full" />
|
|
) : finance ? (
|
|
<FinanceStructureChart finance={finance} formatMoney={formatMoneyMinor} />
|
|
) : (
|
|
<AdminNoResourceState className="py-10 text-center text-xs text-muted-foreground" />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
</div>
|
|
|
|
{showAnalytics ? (
|
|
<aside className="flex min-w-0 flex-col gap-4 xl:col-span-4">
|
|
<DashboardPlayRankingCard analytics={analytics} />
|
|
<DashboardAgentRankingCard analytics={analytics} />
|
|
</aside>
|
|
) : null}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|