Updated the agent dashboard to include new metrics for today's bets and payouts, improving visibility for users. Enhanced localization files with additional hints and labels for better user experience across English, Nepali, and Chinese. Introduced new functions for formatting business dates and improved the handling of analytics permissions in the dashboard components.
272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { BarChart3, RefreshCw, TrendingUp, Users, Wallet } from "lucide-react";
|
|
|
|
import { getAdminDashboard } from "@/api/admin-dashboard";
|
|
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
|
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
|
import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options";
|
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
|
import { PRD_DASHBOARD_ANALYTICS_ACCESS_ANY } from "@/lib/admin-prd";
|
|
import { normalizeAdminLanguage } from "@/i18n";
|
|
import { adminWeekdayKeyForDate, formatAdminBusinessDateIso, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
|
import { cn } from "@/lib/utils";
|
|
import { useAdminProfile } from "@/stores/admin-session";
|
|
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
|
|
import { DashboardAnalyticsPanel } from "@/modules/dashboard/dashboard-analytics-panel";
|
|
import {
|
|
DashboardKpiCard,
|
|
DashboardScopeMetric,
|
|
DashboardSignedStatRow,
|
|
DashboardStatRow,
|
|
} from "@/modules/dashboard/dashboard-visuals";
|
|
import {
|
|
formatDashboardMoneyMinor,
|
|
} from "@/modules/dashboard/use-dashboard-analytics";
|
|
import type { AdminDashboardSiteOverview, AdminDashboardWarning } from "@/types/api/admin-dashboard";
|
|
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
|
import { LotteryApiBizError } from "@/types/api/errors";
|
|
|
|
function buildTodayBetHint(
|
|
businessDate: string,
|
|
latestBetAt: string | null,
|
|
t: (key: string, opts?: Record<string, unknown>) => string,
|
|
formatDt: (iso: string) => string,
|
|
): string {
|
|
const dateHint = t("todayBusinessDateHint", { date: businessDate });
|
|
if (latestBetAt) {
|
|
return `${dateHint} · ${t("site.latestBetAt", { time: formatDt(latestBetAt) })}`;
|
|
}
|
|
|
|
return `${dateHint} · ${t("site.noBetToday")}`;
|
|
}
|
|
|
|
export function SiteDashboardConsole(): ReactElement {
|
|
const { t, i18n } = useTranslation(["dashboard", "common"]);
|
|
const tRef = useTranslationRef(["dashboard", "common"]);
|
|
const formatDt = useAdminDateTimeFormatter();
|
|
const profile = useAdminProfile();
|
|
const site = profile?.site ?? null;
|
|
const permissions = useMemo(() => profile?.permissions ?? [], [profile?.permissions]);
|
|
const businessDateToday = useMemo(() => formatAdminBusinessDateIso(), []);
|
|
|
|
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 playOptions = useCachedPlayTypeOptions();
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [apiWarnings, setApiWarnings] = useState<AdminDashboardWarning[]>([]);
|
|
const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null);
|
|
const [drawId, setDrawId] = useState<number | null>(null);
|
|
const [overview, setOverview] = useState<AdminDashboardSiteOverview | null>(null);
|
|
|
|
const analyticsScope = useMemo(
|
|
() => ({
|
|
siteCode: site?.code ?? overview?.site_code ?? "",
|
|
agentNodeId: undefined,
|
|
}),
|
|
[overview?.site_code, site?.code],
|
|
);
|
|
|
|
const canAnalytics = adminHasAnyPermission(permissions, [...PRD_DASHBOARD_ANALYTICS_ACCESS_ANY]);
|
|
|
|
const load = useCallback(async (isRefresh = false) => {
|
|
if (isRefresh) {
|
|
setRefreshing(true);
|
|
} else {
|
|
setLoading(true);
|
|
}
|
|
setError(null);
|
|
|
|
try {
|
|
const d = await getAdminDashboard();
|
|
setHall(d.hall);
|
|
setOverview(d.site_overview);
|
|
setApiWarnings(d.warnings ?? []);
|
|
if (d.resolved_draw != null) {
|
|
setDrawId(d.resolved_draw.id);
|
|
} else {
|
|
setDrawId(null);
|
|
}
|
|
} catch (e) {
|
|
const msg =
|
|
e instanceof LotteryApiBizError ? e.message : tRef.current("warnings.loadFailed");
|
|
setError(msg);
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, [tRef]);
|
|
|
|
useAsyncEffect(() => {
|
|
void load(false);
|
|
}, []);
|
|
|
|
const displayCurrency = overview?.currency_code ?? "NPR";
|
|
|
|
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("site.title")}</h1>
|
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
{site
|
|
? t("site.subtitle", { name: site.name || site.code })
|
|
: 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 && 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}
|
|
|
|
{loading ? (
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
<Skeleton key={i} className="h-24 rounded-xl" />
|
|
))}
|
|
</div>
|
|
) : overview ? (
|
|
<section className="space-y-4">
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
<DashboardKpiCard
|
|
label={t("site.todayBet")}
|
|
value={formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
|
icon={<TrendingUp className="size-4" />}
|
|
hint={buildTodayBetHint(businessDateToday, overview.latest_bet_at, t, formatDt)}
|
|
/>
|
|
<DashboardKpiCard
|
|
label={t("site.todayProfit")}
|
|
signedAmountMinor={overview.today_profit_minor}
|
|
currencyCode={displayCurrency}
|
|
icon={<BarChart3 className="size-4" />}
|
|
hint={t("todayPayoutHint", {
|
|
amount: formatDashboardMoneyMinor(overview.today_payout_minor, displayCurrency),
|
|
})}
|
|
/>
|
|
<DashboardKpiCard
|
|
label={t("site.activePlayersToday")}
|
|
value={overview.active_player_count_today}
|
|
icon={<Users className="size-4" />}
|
|
hint={t("site.betOrdersTodayHint", { count: overview.bet_order_count_today })}
|
|
/>
|
|
<DashboardKpiCard
|
|
label={t("site.pendingBills")}
|
|
value={overview.pending_bill_count}
|
|
icon={<Wallet className="size-4" />}
|
|
hint={t("site.pendingUnpaid", {
|
|
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
|
})}
|
|
accent={overview.pending_bill_count > 0 ? "destructive" : "muted"}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-semibold">{t("site.sevenDayTitle")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<DashboardStatRow
|
|
label={t("site.sevenDayBet")}
|
|
value={formatDashboardMoneyMinor(overview.seven_day_bet_minor, displayCurrency)}
|
|
/>
|
|
<DashboardStatRow
|
|
label={t("site.sevenDayPayout")}
|
|
value={formatDashboardMoneyMinor(overview.seven_day_payout_minor, displayCurrency)}
|
|
/>
|
|
<DashboardSignedStatRow
|
|
label={t("site.sevenDayProfit")}
|
|
amountMinor={overview.seven_day_profit_minor}
|
|
currencyCode={displayCurrency}
|
|
/>
|
|
<p className="pt-1 text-xs text-muted-foreground">{t("site.profitScopeHint")}</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-semibold">{t("site.scaleTitle")}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
|
<DashboardScopeMetric label={t("site.agentCount")} value={String(overview.agent_count)} />
|
|
<DashboardScopeMetric label={t("site.playerCount")} value={String(overview.player_count)} />
|
|
{overview.top_agent_today ? (
|
|
<div className="col-span-2 rounded-lg border bg-muted/20 px-3 py-2.5">
|
|
<p className="text-xs text-muted-foreground">{t("site.topAgentTodayLabel")}</p>
|
|
<p className="mt-1 font-medium text-foreground">
|
|
{overview.top_agent_today.agent_name || overview.top_agent_today.agent_code}
|
|
</p>
|
|
<p className="mt-0.5 break-all text-sm font-semibold tabular-nums">
|
|
{formatDashboardMoneyMinor(
|
|
overview.top_agent_today.total_bet_minor,
|
|
displayCurrency,
|
|
)}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</section>
|
|
) : (
|
|
<AdminNoResourceState className="py-12 text-sm text-muted-foreground">
|
|
{t("site.overviewEmpty")}
|
|
</AdminNoResourceState>
|
|
)}
|
|
|
|
<DashboardCurrentDrawCard
|
|
key={`${hall?.draw_no ?? "empty"}:${loading ? "loading" : "ready"}`}
|
|
hall={hall}
|
|
drawId={drawId}
|
|
loading={loading}
|
|
/>
|
|
|
|
{canAnalytics ? (
|
|
<DashboardAnalyticsPanel
|
|
enabled={canAnalytics}
|
|
playOptions={playOptions}
|
|
scope={analyticsScope}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|