feat(dashboard, i18n): enhance agent dashboard and localization support
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.
This commit is contained in:
@@ -10,40 +10,43 @@ 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_REPORTS_VIEW_ACCESS_ANY } from "@/lib/admin-prd";
|
||||
import { PRD_DASHBOARD_ANALYTICS_ACCESS_ANY } from "@/lib/admin-prd";
|
||||
import { normalizeAdminLanguage } from "@/i18n";
|
||||
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
||||
import { signedMoneyClass } from "@/lib/admin-signed-money";
|
||||
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 } from "@/modules/dashboard/dashboard-visuals";
|
||||
import {
|
||||
DashboardKpiCard,
|
||||
DashboardScopeMetric,
|
||||
DashboardSignedStatRow,
|
||||
DashboardStatRow,
|
||||
} from "@/modules/dashboard/dashboard-visuals";
|
||||
import {
|
||||
formatDashboardMoneyMinor,
|
||||
formatDashboardSignedMoneyMinor,
|
||||
} from "@/modules/dashboard/use-dashboard-analytics";
|
||||
import type { AdminDashboardSiteOverview } from "@/types/api/admin-dashboard";
|
||||
import type { AdminDashboardSiteOverview, AdminDashboardWarning } from "@/types/api/admin-dashboard";
|
||||
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
|
||||
function SiteMetric({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className="rounded-lg border bg-muted/30 px-3 py-2.5">
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<p className="mt-1 text-base font-semibold tabular-nums text-foreground">{value}</p>
|
||||
</div>
|
||||
);
|
||||
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 {
|
||||
@@ -53,6 +56,7 @@ export function SiteDashboardConsole(): ReactElement {
|
||||
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);
|
||||
@@ -66,6 +70,7 @@ export function SiteDashboardConsole(): ReactElement {
|
||||
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);
|
||||
@@ -78,7 +83,7 @@ export function SiteDashboardConsole(): ReactElement {
|
||||
[overview?.site_code, site?.code],
|
||||
);
|
||||
|
||||
const canAnalytics = adminHasAnyPermission(permissions, [...PRD_REPORTS_VIEW_ACCESS_ANY]);
|
||||
const canAnalytics = adminHasAnyPermission(permissions, [...PRD_DASHBOARD_ANALYTICS_ACCESS_ANY]);
|
||||
|
||||
const load = useCallback(async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
@@ -92,6 +97,7 @@ export function SiteDashboardConsole(): ReactElement {
|
||||
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 {
|
||||
@@ -144,31 +150,36 @@ export function SiteDashboardConsole(): ReactElement {
|
||||
</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 xl:grid-cols-4">
|
||||
<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 xl:grid-cols-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={
|
||||
overview.latest_bet_at
|
||||
? t("site.latestBetAt", { time: formatDt(overview.latest_bet_at) })
|
||||
: t("site.noBetToday")
|
||||
}
|
||||
hint={buildTodayBetHint(businessDateToday, overview.latest_bet_at, t, formatDt)}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("site.todayProfit")}
|
||||
value={formatDashboardSignedMoneyMinor(overview.today_profit_minor, displayCurrency)}
|
||||
signedAmountMinor={overview.today_profit_minor}
|
||||
currencyCode={displayCurrency}
|
||||
icon={<BarChart3 className="size-4" />}
|
||||
hint={t("site.profitScopeHint")}
|
||||
valueClassName={signedMoneyClass(overview.today_profit_minor, true)}
|
||||
hint={t("todayPayoutHint", {
|
||||
amount: formatDashboardMoneyMinor(overview.today_payout_minor, displayCurrency),
|
||||
})}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("site.activePlayersToday")}
|
||||
@@ -192,24 +203,21 @@ export function SiteDashboardConsole(): ReactElement {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("site.sevenDayTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">{t("site.todayBet")}</span>
|
||||
<span className="font-semibold tabular-nums">
|
||||
{formatDashboardMoneyMinor(overview.seven_day_bet_minor, displayCurrency)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">{t("site.sevenDayProfit")}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold tabular-nums",
|
||||
signedMoneyClass(overview.seven_day_profit_minor, true),
|
||||
)}
|
||||
>
|
||||
{formatDashboardSignedMoneyMinor(overview.seven_day_profit_minor, displayCurrency)}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -218,24 +226,31 @@ export function SiteDashboardConsole(): ReactElement {
|
||||
<CardTitle className="text-sm font-semibold">{t("site.scaleTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||
<SiteMetric label={t("site.agentCount")} value={String(overview.agent_count)} />
|
||||
<SiteMetric label={t("site.playerCount")} value={String(overview.player_count)} />
|
||||
<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 text-xs text-muted-foreground">
|
||||
{t("site.topAgentToday", {
|
||||
name: overview.top_agent_today.agent_name || overview.top_agent_today.agent_code,
|
||||
amount: formatDashboardMoneyMinor(
|
||||
<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>
|
||||
) : null}
|
||||
) : (
|
||||
<AdminNoResourceState className="py-12 text-sm text-muted-foreground">
|
||||
{t("site.overviewEmpty")}
|
||||
</AdminNoResourceState>
|
||||
)}
|
||||
|
||||
<DashboardCurrentDrawCard
|
||||
key={`${hall?.draw_no ?? "empty"}:${loading ? "loading" : "ready"}`}
|
||||
|
||||
Reference in New Issue
Block a user