feat(api, i18n): add agent_node_id to various admin queries and enhance multi-language support

Introduced the agent_node_id field in AdminDrawListQuery, AdminPlayerListQuery, AdminSettlementBatchListQuery, TicketItemsListQuery, and TransferOrderListQuery to improve filtering capabilities. Updated the admin-breadcrumb and admin-sidebar components to include new translations for agent-related terms in English, Nepali, and Chinese, enhancing the overall user experience and multi-language support across the admin interface.
This commit is contained in:
2026-06-02 14:37:08 +08:00
parent a4e7a2d228
commit b15e377187
105 changed files with 5305 additions and 1596 deletions

View File

@@ -1,8 +1,10 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { toast } from "sonner";
import {
@@ -17,6 +19,7 @@ import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { LotteryApiBizError } from "@/types/api/errors";
@@ -50,6 +53,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
export function DrawDetailConsole({ drawId }: { drawId: string }) {
const { t } = useTranslation(["draws", "common"]);
const tRef = useTranslationRef(["draws", "common"]);
const idNum = Number(drawId);
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
@@ -67,7 +71,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
const load = useCallback(async () => {
if (!Number.isFinite(idNum)) {
setError(t("invalidDrawId"));
setError(tRef.current("invalidDrawId"));
setLoading(false);
return;
}
@@ -77,11 +81,11 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
setData(await getAdminDraw(idNum));
} catch (e) {
setData(null);
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
setError(e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }));
} finally {
setLoading(false);
}
}, [idNum, t]);
}, [idNum]);
async function runAction(name: string, action: () => Promise<unknown>): Promise<void> {
if (!Number.isFinite(idNum)) return;
@@ -97,15 +101,12 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
}
}
useEffect(() => {
const timer = window.setTimeout(() => {
void load();
}, 0);
return () => window.clearTimeout(timer);
}, [load]);
useAsyncEffect(() => {
void load();
}, [idNum]);
if (loading && !data) {
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
return <AdminLoadingState minHeight="6rem" className="py-6" />;
}
if (error || !data) {

View File

@@ -1,8 +1,10 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
@@ -11,6 +13,7 @@ import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Table,
TableBody,
@@ -37,6 +40,7 @@ import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "./draw-prd";
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
const { t } = useTranslation(["draws", "settlement", "common"]);
const tRef = useTranslationRef(["draws", "settlement", "common"]);
useAdminCurrencyCatalog();
const idNum = Number(drawId);
const profile = useAdminProfile();
@@ -54,7 +58,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
const load = useCallback(async () => {
if (!Number.isFinite(idNum) || idNum < 1) {
setErr(t("invalidDrawId"));
setErr(tRef.current("invalidDrawId"));
setLoading(false);
return;
}
@@ -63,12 +67,12 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
try {
setData(await getAdminDrawFinanceSummary(idNum));
} catch (e) {
setErr(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
setErr(e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }));
setData(null);
} finally {
setLoading(false);
}
}, [idNum, t]);
}, [idNum]);
async function runSettlement(): Promise<void> {
if (!Number.isFinite(idNum) || idNum < 1) return;
@@ -84,14 +88,12 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
}
}
useEffect(() => {
queueMicrotask(() => {
void load();
});
}, [load]);
useAsyncEffect(() => {
void load();
}, [idNum]);
if (loading && !data) {
return <p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>;
return <AdminLoadingState minHeight="6rem" className="py-6" />;
}
if (err || !data) {

View File

@@ -2,8 +2,10 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { toast } from "sonner";
import {
@@ -14,6 +16,7 @@ import {
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Table,
TableBody,
@@ -34,6 +37,7 @@ import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchId: string }) {
const { t } = useTranslation(["draws", "common"]);
const tRef = useTranslationRef(["draws", "common"]);
const router = useRouter();
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
@@ -50,7 +54,7 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
const load = useCallback(async () => {
if (!Number.isFinite(idNum)) {
setError(t("invalidDrawId"));
setError(tRef.current("invalidDrawId"));
setLoading(false);
return;
}
@@ -60,18 +64,15 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
setData(await getAdminDrawResultBatches(idNum));
} catch (e) {
setData(null);
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
setError(e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }));
} finally {
setLoading(false);
}
}, [idNum, t]);
}, [idNum]);
useEffect(() => {
const timer = window.setTimeout(() => {
void load();
}, 0);
return () => window.clearTimeout(timer);
}, [load]);
useAsyncEffect(() => {
void load();
}, [idNum]);
const batch: AdminDrawBatchRow | undefined = useMemo(() => {
if (!Number.isFinite(batchNum)) return undefined;
@@ -115,7 +116,7 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
}
if (loading && !data) {
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
return <AdminLoadingState minHeight="6rem" className="py-6" />;
}
if (error || !data) {

View File

@@ -1,12 +1,15 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { getAdminDrawResultBatches } from "@/api/admin-draws";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Table,
TableBody,
@@ -28,6 +31,7 @@ import { DrawStatusBadge } from "./draw-status-badge";
export function DrawResultsConsole({ drawId }: { drawId: string }) {
const { t } = useTranslation(["draws", "common"]);
const tRef = useTranslationRef(["draws", "common"]);
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
PRD_DRAW_RESULT_MANAGE,
@@ -39,7 +43,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
const load = useCallback(async () => {
if (!Number.isFinite(idNum)) {
setError(t("invalidDrawId"));
setError(tRef.current("invalidDrawId"));
setLoading(false);
return;
}
@@ -49,21 +53,18 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
setData(await getAdminDrawResultBatches(idNum));
} catch (e) {
setData(null);
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
setError(e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }));
} finally {
setLoading(false);
}
}, [idNum, t]);
}, [idNum]);
useEffect(() => {
const timer = window.setTimeout(() => {
void load();
}, 0);
return () => window.clearTimeout(timer);
}, [load]);
useAsyncEffect(() => {
void load();
}, [idNum]);
if (loading && !data) {
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
return <AdminLoadingState minHeight="6rem" className="py-6" />;
}
if (error || !data) {

View File

@@ -1,8 +1,10 @@
"use client";
import { Dices, Rocket, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { toast } from "sonner";
import {
@@ -14,6 +16,7 @@ import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Table,
TableBody,
@@ -56,6 +59,7 @@ function randomDrawNumber4d(): string {
export function DrawReviewConsole({ drawId }: { drawId: string }) {
const { t } = useTranslation(["draws", "common"]);
const tRef = useTranslationRef(["draws", "common"]);
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
PRD_DRAW_RESULT_MANAGE,
@@ -73,7 +77,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
const load = useCallback(async () => {
if (!Number.isFinite(idNum)) {
setError(t("invalidDrawId"));
setError(tRef.current("invalidDrawId"));
setLoading(false);
return;
}
@@ -83,18 +87,15 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
setData(await getAdminDrawResultBatches(idNum));
} catch (e) {
setData(null);
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
setError(e instanceof LotteryApiBizError ? e.message : tRef.current("errors.loadFailed", { ns: "common" }));
} finally {
setLoading(false);
}
}, [idNum, t]);
}, [idNum]);
useEffect(() => {
const timer = window.setTimeout(() => {
void load();
}, 0);
return () => window.clearTimeout(timer);
}, [load]);
useAsyncEffect(() => {
void load();
}, [idNum]);
const pending = useMemo(() => data?.batches.filter((b) => b.status === "pending_review") ?? [], [
data,
@@ -148,7 +149,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
}
if (loading && !data) {
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
return <AdminLoadingState minHeight="6rem" className="py-6" />;
}
if (error || !data) {

View File

@@ -1,8 +1,10 @@
"use client";
import { Ban, Eye, Pencil, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { toast } from "sonner";
import {
@@ -14,10 +16,12 @@ import {
} from "@/api/admin-draws";
import { formatAdminInstant } from "@/lib/admin-datetime";
import { getAdminRequestLocale } from "@/lib/admin-locale";
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { Button } from "@/components/ui/button";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminAgentFilter } from "@/components/admin/admin-agent-filter";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -86,6 +90,7 @@ function drawAdminStatusSelectLabel(raw: unknown, t: (key: string) => string): s
export function DrawsIndexConsole() {
const { t } = useTranslation(["draws", "common"]);
const tRef = useTranslationRef(["draws", "common"]);
const exportLabels = useExportLabels("drawsList");
useAdminCurrencyCatalog();
const defaultCurrency = "NPR";
@@ -106,6 +111,8 @@ export function DrawsIndexConsole() {
const [draftStatus, setDraftStatus] = useState("");
const [appliedDrawNo, setAppliedDrawNo] = useState("");
const [appliedStatus, setAppliedStatus] = useState("");
const [agentNodeId, setAgentNodeId] = useState<number | undefined>(undefined);
const [appliedAgentNodeId, setAppliedAgentNodeId] = useState<number | undefined>(undefined);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState<number>(10);
const [generating, setGenerating] = useState(false);
@@ -137,17 +144,18 @@ export function DrawsIndexConsole() {
appliedStatus.trim() === "" || appliedStatus === DRAW_FILTER_ALL
? undefined
: appliedStatus.trim(),
agent_node_id: appliedAgentNodeId,
});
setData(d);
} catch (e) {
const msg =
e instanceof LotteryApiBizError ? e.message : t("loadFailed");
e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed");
setError(msg);
setData(null);
} finally {
setLoading(false);
}
}, [page, perPage, appliedDrawNo, appliedStatus, t]);
}, [page, perPage, appliedDrawNo, appliedStatus, appliedAgentNodeId]);
async function generatePlan(): Promise<void> {
setGenerating(true);
@@ -168,12 +176,9 @@ export function DrawsIndexConsole() {
}
}
useEffect(() => {
const timer = window.setTimeout(() => {
void load();
}, 0);
return () => window.clearTimeout(timer);
}, [load]);
useAsyncEffect(() => {
void load();
}, [page, perPage, appliedDrawNo, appliedStatus, appliedAgentNodeId]);
const handleSelectAll = useCallback((checked: boolean) => {
if (checked && data) {
@@ -293,6 +298,12 @@ export function DrawsIndexConsole() {
</CardHeader>
<CardContent className="admin-list-content">
<div className="admin-list-toolbar">
<AdminAgentFilter
id="draws-agent-filter"
className="admin-list-field sm:w-[14rem]"
value={agentNodeId}
onChange={setAgentNodeId}
/>
<div className="admin-list-field xl:min-w-0">
<Label htmlFor="draw-filter-no" className="sm:w-10 sm:shrink-0">
{t("drawNo")}
@@ -347,6 +358,7 @@ export function DrawsIndexConsole() {
onClick={() => {
setAppliedDrawNo(draftDrawNo);
setAppliedStatus(draftStatus);
setAppliedAgentNodeId(agentNodeId);
setPage(1);
}}
>
@@ -358,8 +370,10 @@ export function DrawsIndexConsole() {
onClick={() => {
setDraftDrawNo("");
setDraftStatus("");
setAgentNodeId(undefined);
setAppliedDrawNo("");
setAppliedStatus("");
setAppliedAgentNodeId(undefined);
setPage(1);
}}
>
@@ -410,11 +424,7 @@ export function DrawsIndexConsole() {
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={10} className="text-muted-foreground">
{t("states.loading", { ns: "common" })}
</TableCell>
</TableRow>
<AdminTableLoadingRow colSpan={10} />
) : data === null || data.items.length === 0 ? (
<TableRow>
<TableCell colSpan={10} className="text-muted-foreground">