Added new translations for agent management and settlement features in English, Nepali, and Chinese, improving multi-language support. Updated the agents console to reflect changes in funding modes and player details, enhancing user experience. Refactored the admin permission gate to include new logic for handling bound line agents, ensuring better permission management. Additionally, streamlined the UI for agent-related pages and improved navigation to the settlement center, consolidating related functionalities for better accessibility.
580 lines
22 KiB
TypeScript
580 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
AlertTriangle,
|
|
ClipboardList,
|
|
Diamond,
|
|
FileSearch,
|
|
RefreshCw,
|
|
ScrollText,
|
|
Settings,
|
|
Shield,
|
|
Ticket,
|
|
Wallet,
|
|
BarChart3,
|
|
Scale,
|
|
} 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 { 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 {
|
|
AbnormalTransferPanelFooter,
|
|
CapUsageBar,
|
|
FinanceStructureChart,
|
|
HotUsageBars,
|
|
ResultBatchQueueSummary,
|
|
PlatformLifetimePayoutSnapshot,
|
|
DashboardPanelCard,
|
|
SettlementStatusChart,
|
|
} from "@/modules/dashboard/dashboard-visuals";
|
|
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 {
|
|
AdminDashboardDrawPanel,
|
|
AdminDashboardLifetimeFinance,
|
|
AdminDashboardPlatformRisk,
|
|
AdminDashboardResultBatchQueue,
|
|
} 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";
|
|
|
|
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 [drawPanel, setDrawPanel] = useState<AdminDashboardDrawPanel | 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 [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);
|
|
setDrawPanel(null);
|
|
setResultBatchQueue(null);
|
|
setLifetimeFinance(null);
|
|
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);
|
|
setPlatformRisk(d.platform_risk);
|
|
if (d.draw != null) {
|
|
setDrawPanel(d.draw);
|
|
}
|
|
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 =
|
|
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 platformUsagePct =
|
|
typeof rawPlatformUsagePct === "number" && Number.isFinite(rawPlatformUsagePct)
|
|
? Math.min(100, Math.max(0, rawPlatformUsagePct))
|
|
: platformCap > 0
|
|
? (platformLocked / platformCap) * 100
|
|
: 0;
|
|
|
|
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;
|
|
|
|
const quickLinks: { href: string; label: string; icon: ReactNode }[] = [
|
|
{ href: "/admin/draws", label: t("quickLinks.createDrawPlan"), icon: <Diamond className="size-4" /> },
|
|
{ href: "/admin/draws", label: t("quickLinks.drawSchedule"), icon: <Ticket className="size-4" /> },
|
|
{
|
|
href: drawId != null ? `/admin/draws/${drawId}/results` : "/admin/draws",
|
|
label: t("quickLinks.results"),
|
|
icon: <FileSearch className="size-4" />,
|
|
},
|
|
{ href: "/admin/tickets", label: t("quickLinks.tickets"), icon: <Shield className="size-4" /> },
|
|
{ href: "/admin/wallet/transactions", label: t("quickLinks.walletTransactions"), icon: <Wallet className="size-4" /> },
|
|
{ href: "/admin/audit-logs", label: t("quickLinks.auditLogs"), icon: <ScrollText className="size-4" /> },
|
|
{ href: "/admin/reports", label: t("quickLinks.reports"), icon: <BarChart3 className="size-4" /> },
|
|
{ href: "/admin/rules/odds", label: t("quickLinks.payoutRules"), icon: <Scale className="size-4" /> },
|
|
{ href: "/admin/risk", label: t("quickLinks.riskMonitor"), icon: <Shield className="size-4" /> },
|
|
{ href: "/admin/settings", label: t("quickLinks.systemSettings"), icon: <Settings className="size-4" /> },
|
|
];
|
|
|
|
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}
|
|
|
|
<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}
|
|
/>
|
|
<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 ?? true}
|
|
/>
|
|
</DashboardPanelCard>
|
|
|
|
<DashboardPanelCard
|
|
href="/admin/risk"
|
|
title={t("riskCapUsage")}
|
|
value={`${platformUsagePct.toFixed(1)}%`}
|
|
subtitle={
|
|
platformCap > 0
|
|
? t("platformLockedAndCap", {
|
|
locked: formatMoneyMinor(platformLocked, currency),
|
|
cap: formatMoneyMinor(platformCap, currency),
|
|
})
|
|
: t("platformCapNotConfigured", {
|
|
locked: formatMoneyMinor(platformLocked, currency),
|
|
})
|
|
}
|
|
actionLabel={t("occupancyDetails")}
|
|
icon={<Shield className="size-5" aria-hidden />}
|
|
accent={
|
|
platformUsagePct >= 90
|
|
? "destructive"
|
|
: platformUsagePct >= 70
|
|
? "primary"
|
|
: "muted"
|
|
}
|
|
loading={loading}
|
|
>
|
|
{platformRisk != null ? (
|
|
<CapUsageBar
|
|
locked={platformLocked}
|
|
cap={platformCap}
|
|
usagePct={platformUsagePct}
|
|
formatMoney={formatMoneyMinor}
|
|
currency={currency}
|
|
compact
|
|
/>
|
|
) : null}
|
|
</DashboardPanelCard>
|
|
|
|
<DashboardPanelCard
|
|
href="/admin/reports"
|
|
title={t("payoutComposition")}
|
|
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 ? (
|
|
<div className="grid min-w-0 grid-cols-1 gap-4 md:grid-cols-2">
|
|
<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>
|
|
|
|
<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("quickLinksTitle")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid grid-cols-2 gap-2 px-4 py-4 sm:grid-cols-3">
|
|
{quickLinks.map((q) => (
|
|
<Link
|
|
key={q.href + q.label}
|
|
href={q.href}
|
|
className="flex min-w-0 flex-col items-center gap-2 rounded-lg border border-border/70 bg-muted/10 px-2 py-3 text-center text-[11px] font-medium leading-snug text-foreground transition-colors hover:border-primary/30 hover:bg-muted/30"
|
|
>
|
|
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-card text-primary shadow-sm">
|
|
{q.icon}
|
|
</span>
|
|
<span className="line-clamp-2">{q.label}</span>
|
|
</Link>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
{showAnalytics ? (
|
|
<aside className="flex min-w-0 flex-col gap-4 xl:col-span-4">
|
|
<DashboardPlayRankingCard analytics={analytics} />
|
|
<DashboardAgentRankingCard analytics={analytics} />
|
|
|
|
<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>
|
|
|
|
<Card className="admin-list-card py-0">
|
|
<CardHeader className="border-b border-border/60 px-4 py-3 pb-0">
|
|
<CardTitle className="text-sm font-semibold">{t("quickLinksTitle")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid grid-cols-2 gap-2 px-4 py-4 sm:grid-cols-3">
|
|
{quickLinks.map((q) => (
|
|
<Link
|
|
key={q.href + q.label}
|
|
href={q.href}
|
|
className="flex min-w-0 flex-col items-center gap-2 rounded-lg border border-border/70 bg-muted/10 px-2 py-3 text-center text-[11px] font-medium leading-snug text-foreground transition-colors hover:border-primary/30 hover:bg-muted/30"
|
|
>
|
|
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-card text-primary shadow-sm">
|
|
{q.icon}
|
|
</span>
|
|
<span className="line-clamp-2">{q.label}</span>
|
|
</Link>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</aside>
|
|
) : null}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|