feat(admin, i18n): enhance admin dashboard and user management with new features and translations
Added the ability to filter admin dashboard data by site code and agent node ID, improving data retrieval capabilities. Introduced new functions for fetching dashboard data based on these parameters. Updated the admin users and roles management components to reflect these changes. Enhanced multi-language support by adding new translations for agent management and permission levels in English, Nepali, and Chinese, ensuring a consistent user experience across the admin interface.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
postAdminPlayerUnfreeze,
|
||||
putAdminPlayer,
|
||||
} from "@/api/admin-player";
|
||||
import { AdminAgentFilter } from "@/components/admin/admin-agent-filter";
|
||||
import { AdminAgentCell, AdminAgentHead } from "@/components/admin/admin-agent-columns";
|
||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||
@@ -57,7 +57,6 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
|
||||
import { formatAdminMinorUnits } from "@/lib/money";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminPlayerRow, AdminPlayerWalletRow } from "@/types/api/admin-player";
|
||||
@@ -91,18 +90,17 @@ export function PlayersConsole(): React.ReactElement {
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const exportLabels = useExportLabels("players");
|
||||
const profile = useAdminProfile();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const keywordFromUrl = (searchParams.get("keyword") ?? "").trim();
|
||||
useAdminCurrencyCatalog();
|
||||
const canManagePlayers = adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE]);
|
||||
const canFreezePlayers = adminHasAnyPermission(profile?.permissions, [PRD_PLAYER_FREEZE_MANAGE]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
const { sites: siteOptions, canChooseSite } = useAdminSiteCodeOptions();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [query, setQuery] = useState("");
|
||||
const [siteCode, setSiteCode] = useState("");
|
||||
const [appliedSiteCode, setAppliedSiteCode] = useState("");
|
||||
const [agentNodeId, setAgentNodeId] = useState<number | undefined>(undefined);
|
||||
const [appliedAgentNodeId, setAppliedAgentNodeId] = useState<number | undefined>(undefined);
|
||||
const [keyword, setKeyword] = useState(keywordFromUrl);
|
||||
const [query, setQuery] = useState(keywordFromUrl);
|
||||
|
||||
const [items, setItems] = useState<AdminPlayerRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -138,8 +136,6 @@ export function PlayersConsole(): React.ReactElement {
|
||||
page,
|
||||
per_page: perPage,
|
||||
keyword: query.trim() || undefined,
|
||||
site_code: appliedSiteCode.trim() || undefined,
|
||||
agent_node_id: appliedAgentNodeId,
|
||||
});
|
||||
setItems(data.items);
|
||||
setTotal(data.meta.total);
|
||||
@@ -153,11 +149,17 @@ export function PlayersConsole(): React.ReactElement {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, perPage, query, appliedSiteCode, appliedAgentNodeId]);
|
||||
}, [page, perPage, query]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load();
|
||||
}, [page, perPage, query, appliedSiteCode, appliedAgentNodeId]);
|
||||
}, [page, perPage, query]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
setKeyword(keywordFromUrl);
|
||||
setQuery(keywordFromUrl);
|
||||
setPage(1);
|
||||
}, [keywordFromUrl]);
|
||||
|
||||
function openCreateAccount(): void {
|
||||
setAccountMode("create");
|
||||
@@ -311,42 +313,6 @@ export function PlayersConsole(): React.ReactElement {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="admin-list-toolbar">
|
||||
{canChooseSite ? (
|
||||
<div className="admin-list-field">
|
||||
<Label className="sm:w-20 sm:shrink-0">{t("filterSite")}</Label>
|
||||
<Select
|
||||
value={siteCode || "__all__"}
|
||||
onValueChange={(v) => setSiteCode(v === "__all__" ? "" : v ?? "")}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[12rem]">
|
||||
<SelectValue>
|
||||
{(v) => {
|
||||
const value = String(v ?? "__all__");
|
||||
if (value === "__all__") {
|
||||
return t("filterAllSites");
|
||||
}
|
||||
const site = siteOptions.find((item) => item.code === value);
|
||||
return site ? `${site.code} — ${site.name}` : value;
|
||||
}}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">{t("filterAllSites")}</SelectItem>
|
||||
{siteOptions.map((site) => (
|
||||
<SelectItem key={site.code} value={site.code}>
|
||||
{site.code} — {site.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : null}
|
||||
<AdminAgentFilter
|
||||
id="players-agent-filter"
|
||||
className="admin-list-field sm:w-[14rem]"
|
||||
value={agentNodeId}
|
||||
onChange={setAgentNodeId}
|
||||
/>
|
||||
<div className="admin-list-field xl:min-w-0">
|
||||
<Label htmlFor="player-search" className="sm:w-20 sm:shrink-0">
|
||||
{t("search")}
|
||||
@@ -361,8 +327,14 @@ export function PlayersConsole(): React.ReactElement {
|
||||
if (e.key === "Enter") {
|
||||
setPage(1);
|
||||
setQuery(keyword.trim());
|
||||
setAppliedSiteCode(siteCode.trim());
|
||||
setAppliedAgentNodeId(agentNodeId);
|
||||
const nextParams = new URLSearchParams(searchParams.toString());
|
||||
if (keyword.trim()) {
|
||||
nextParams.set("keyword", keyword.trim());
|
||||
} else {
|
||||
nextParams.delete("keyword");
|
||||
}
|
||||
const queryString = nextParams.toString();
|
||||
router.replace(`${pathname}${queryString ? `?${queryString}` : ""}`, { scroll: false });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -378,8 +350,14 @@ export function PlayersConsole(): React.ReactElement {
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
setQuery(keyword.trim());
|
||||
setAppliedSiteCode(siteCode.trim());
|
||||
setAppliedAgentNodeId(agentNodeId);
|
||||
const nextParams = new URLSearchParams(searchParams.toString());
|
||||
if (keyword.trim()) {
|
||||
nextParams.set("keyword", keyword.trim());
|
||||
} else {
|
||||
nextParams.delete("keyword");
|
||||
}
|
||||
const queryString = nextParams.toString();
|
||||
router.replace(`${pathname}${queryString ? `?${queryString}` : ""}`, { scroll: false });
|
||||
}}
|
||||
>
|
||||
{t("search")}
|
||||
@@ -407,7 +385,7 @@ export function PlayersConsole(): React.ReactElement {
|
||||
<TableHead className="whitespace-nowrap text-center">{t("available")}</TableHead>
|
||||
<TableHead className="w-20 whitespace-nowrap">{t("status")}</TableHead>
|
||||
<TableHead className="whitespace-nowrap">{t("lastLogin")}</TableHead>
|
||||
<TableHead className="w-14 text-center">{t("actions")}</TableHead>
|
||||
<TableHead className="sticky right-0 z-20 bg-muted w-14 text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("table.actions", { ns: "common" })}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -489,7 +467,7 @@ export function PlayersConsole(): React.ReactElement {
|
||||
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
|
||||
{row.last_login_at ? formatDt(row.last_login_at) : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<TableCell className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
|
||||
{canManagePlayers ? (
|
||||
<AdminRowActionsMenu
|
||||
actions={[
|
||||
@@ -499,6 +477,16 @@ export function PlayersConsole(): React.ReactElement {
|
||||
icon: Pencil,
|
||||
onClick: () => openEditAccount(row),
|
||||
},
|
||||
{
|
||||
key: "tickets",
|
||||
label: t("viewTickets", { defaultValue: "查看注单" }),
|
||||
href: `/admin/tickets?player_id=${row.id}`,
|
||||
},
|
||||
{
|
||||
key: "wallet",
|
||||
label: t("viewWallet", { defaultValue: "查看钱包流水" }),
|
||||
href: `/admin/wallet/transactions?player_id=${row.id}`,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: t("delete"),
|
||||
|
||||
Reference in New Issue
Block a user