refactor(admin-reports, i18n): remove rebate commission report and enhance localization
Removed the `getAdminReportRebateCommission` function and its references from the admin reports API and localization files. Updated CSS for improved money display handling in admin components. Enhanced localization support by adding new finance and support workspace entries in English, Nepali, and Chinese, improving user experience across the application.
This commit is contained in:
196
src/modules/dashboard/site-cs-dashboard-console.tsx
Normal file
196
src/modules/dashboard/site-cs-dashboard-console.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ClipboardList, RefreshCw, Search, Users } 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 { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import {
|
||||
DashboardKpiCard,
|
||||
DashboardScopeMetric,
|
||||
} from "@/modules/dashboard/dashboard-visuals";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import type {
|
||||
AdminDashboardSiteCsOverview,
|
||||
AdminDashboardWarning,
|
||||
} from "@/types/api/admin-dashboard";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
|
||||
export function SiteCsDashboardConsole(): ReactElement {
|
||||
const { t } = useTranslation(["dashboard", "common"]);
|
||||
const tRef = useTranslationRef(["dashboard", "common"]);
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const profile = useAdminProfile();
|
||||
const site = profile?.site ?? null;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiWarnings, setApiWarnings] = useState<AdminDashboardWarning[]>([]);
|
||||
const [overview, setOverview] = useState<AdminDashboardSiteCsOverview | null>(null);
|
||||
|
||||
const load = useCallback(async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const d = await getAdminDashboard();
|
||||
setOverview(d.site_cs_overview);
|
||||
setApiWarnings(d.warnings ?? []);
|
||||
} 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 activityHint = useMemo(() => {
|
||||
if (!overview) {
|
||||
return "";
|
||||
}
|
||||
if (overview.latest_ticket_at) {
|
||||
return t("cs.latestTicketAt", { time: formatDt(overview.latest_ticket_at) });
|
||||
}
|
||||
return t("cs.noTicketToday");
|
||||
}, [formatDt, overview, t]);
|
||||
|
||||
const quickLinks = useMemo(
|
||||
() => [
|
||||
{ href: "/admin/players", label: t("cs.quickLinks.players"), icon: Users },
|
||||
{ href: "/admin/tickets", label: t("cs.quickLinks.tickets"), icon: ClipboardList },
|
||||
{ href: "/admin/wallet/transactions", label: t("cs.quickLinks.wallet"), icon: Search },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
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("cs.title")}</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{site
|
||||
? t("cs.subtitle", { name: site.name || site.code })
|
||||
: t("cs.subtitleFallback")}
|
||||
</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-3">
|
||||
{Array.from({ length: 3 }).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-3">
|
||||
<DashboardKpiCard
|
||||
label={t("cs.playerCount")}
|
||||
value={overview.player_count}
|
||||
icon={<Users className="size-4" />}
|
||||
hint={t("cs.playerCountHint")}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("cs.ticketsToday")}
|
||||
value={overview.ticket_order_count_today}
|
||||
icon={<ClipboardList className="size-4" />}
|
||||
hint={activityHint}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("cs.activePlayersToday")}
|
||||
value={overview.active_player_count_today}
|
||||
icon={<Search className="size-4" />}
|
||||
hint={t("cs.activePlayersHint")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("cs.workspaceTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 sm:grid-cols-3">
|
||||
{quickLinks.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="flex flex-col gap-2 rounded-xl border bg-muted/20 px-4 py-4 transition-colors hover:bg-muted/40"
|
||||
>
|
||||
<Icon className="size-5 text-primary" aria-hidden />
|
||||
<span className="text-sm font-medium">{link.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{t("cs.openModule")}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("cs.scopeTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||
<DashboardScopeMetric label={t("cs.playerCount")} value={String(overview.player_count)} />
|
||||
<DashboardScopeMetric
|
||||
label={t("cs.ticketsToday")}
|
||||
value={String(overview.ticket_order_count_today)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
) : (
|
||||
<AdminNoResourceState className="py-12 text-sm text-muted-foreground">
|
||||
{t("cs.overviewEmpty")}
|
||||
</AdminNoResourceState>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user