refactor(admin, draws, settlement): unify admin datetime display and tighten wallet write permission

This commit is contained in:
2026-06-09 15:06:52 +08:00
parent b7278e68a4
commit dfd475856e
10 changed files with 60 additions and 33 deletions

View File

@@ -47,11 +47,8 @@ export const PRD_RISK_VIEW = "prd.risk.view" as const;
export const PRD_RISK_MANAGE = "prd.risk.manage" as const; export const PRD_RISK_MANAGE = "prd.risk.manage" as const;
export const PRD_ODDS_VIEW = "prd.odds.view" as const; export const PRD_ODDS_VIEW = "prd.odds.view" as const;
/** 钱包补单/冲正(冲正 + 手工处理 */ /** 钱包补单/冲正(冲正、补入账、手工结案等会影响资金状态的动作 */
export const PRD_WALLET_WRITE_ANY = [ export const PRD_WALLET_WRITE_ANY = [PRD_WALLET_ADJUST_MANAGE] as const;
PRD_WALLET_ADJUST_MANAGE,
PRD_WALLET_RECONCILE_MANAGE,
] as const;
/** 玩家列表页(与侧栏 requiredAny 一致) */ /** 玩家列表页(与侧栏 requiredAny 一致) */
export const PRD_PLAYERS_ACCESS_ANY = [ export const PRD_PLAYERS_ACCESS_ANY = [

View File

@@ -18,6 +18,7 @@ import {
import { getAdminDashboard } from "@/api/admin-dashboard"; import { getAdminDashboard } from "@/api/admin-dashboard";
import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useTranslationRef } from "@/hooks/use-translation-ref"; import { useTranslationRef } from "@/hooks/use-translation-ref";
import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options"; import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options";
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
@@ -50,6 +51,7 @@ import { LotteryApiBizError } from "@/types/api/errors";
export function AgentDashboardConsole(): ReactElement { export function AgentDashboardConsole(): ReactElement {
const { t, i18n } = useTranslation(["dashboard", "common", "agents"]); const { t, i18n } = useTranslation(["dashboard", "common", "agents"]);
const tRef = useTranslationRef(["dashboard", "common"]); const tRef = useTranslationRef(["dashboard", "common"]);
const formatDt = useAdminDateTimeFormatter();
const profile = useAdminProfile(); const profile = useAdminProfile();
const agent = profile?.agent ?? null; const agent = profile?.agent ?? null;
const permissions = useMemo(() => profile?.permissions ?? [], [profile?.permissions]); const permissions = useMemo(() => profile?.permissions ?? [], [profile?.permissions]);
@@ -252,7 +254,7 @@ export function AgentDashboardConsole(): ReactElement {
<span>{t("agent.settlementCycle", { cycle: overview.settlement_cycle })}</span> <span>{t("agent.settlementCycle", { cycle: overview.settlement_cycle })}</span>
<span> <span>
{overview.latest_bet_at {overview.latest_bet_at
? t("agent.latestBetAt", { time: new Date(overview.latest_bet_at).toLocaleString() }) ? t("agent.latestBetAt", { time: formatDt(overview.latest_bet_at) })
: t("agent.noBetToday")} : t("agent.noBetToday")}
</span> </span>
</div> </div>

View File

@@ -9,7 +9,9 @@ import { buttonVariants } from "@/components/ui/button";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { formatAdminInstantInTimeZone } from "@/lib/admin-datetime";
import { getAdminRequestLocale } from "@/lib/admin-locale";
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { DrawCurrentSnapshot } from "@/types/api/public-draw"; import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
@@ -62,7 +64,11 @@ export function DashboardCurrentDrawCard({
loading = false, loading = false,
}: DashboardCurrentDrawCardProps): ReactElement { }: DashboardCurrentDrawCardProps): ReactElement {
const { t } = useTranslation(["dashboard", "draws"]); const { t } = useTranslation(["dashboard", "draws"]);
const formatDt = useAdminDateTimeFormatter(); const formatDt = (iso: string | null | undefined): string =>
formatAdminInstantInTimeZone(iso, {
locale: getAdminRequestLocale(),
timeZone: LOTTERY_SCHEDULE_TIMEZONE,
});
if (loading) { if (loading) {
return ( return (

View File

@@ -84,7 +84,7 @@ export function DrawCreateDialog({
<DialogHeader> <DialogHeader>
<DialogTitle>{t("createDraw.title")}</DialogTitle> <DialogTitle>{t("createDraw.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
{t("createDraw.description", { tz: "Local" })} {t("createDraw.description", { tz: scheduleTimezone ?? LOTTERY_SCHEDULE_TIMEZONE })}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>

View File

@@ -20,7 +20,9 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
import { AdminLoadingState } from "@/components/admin/admin-loading-state"; import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { formatAdminInstantInTimeZone } from "@/lib/admin-datetime";
import { getAdminRequestLocale } from "@/lib/admin-locale";
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useConfirmAction } from "@/hooks/use-confirm-action";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance"; import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
@@ -46,7 +48,14 @@ type ScheduleStep = {
}; };
function ScheduleTimeline({ steps }: { steps: ScheduleStep[] }) { function ScheduleTimeline({ steps }: { steps: ScheduleStep[] }) {
const formatDt = useAdminDateTimeFormatter(); const formatDt = useCallback(
(iso: string | null | undefined) =>
formatAdminInstantInTimeZone(iso, {
locale: getAdminRequestLocale(),
timeZone: LOTTERY_SCHEDULE_TIMEZONE,
}),
[],
);
return ( return (
<ol className="grid gap-3 sm:grid-cols-3"> <ol className="grid gap-3 sm:grid-cols-3">
@@ -112,7 +121,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [idNum]); }, [idNum, tRef]);
async function runAction(name: string, action: () => Promise<unknown>): Promise<void> { async function runAction(name: string, action: () => Promise<unknown>): Promise<void> {
if (!Number.isFinite(idNum)) return; if (!Number.isFinite(idNum)) return;

View File

@@ -17,8 +17,9 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { formatAdminInstant } from "@/lib/admin-datetime"; import { formatAdminInstantInTimeZone } from "@/lib/admin-datetime";
import { getAdminRequestLocale } from "@/lib/admin-locale"; import { getAdminRequestLocale } from "@/lib/admin-locale";
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawListItem } from "@/types/api/admin-draws"; import type { AdminDrawListItem } from "@/types/api/admin-draws";
@@ -30,9 +31,10 @@ type DrawEditDialogProps = {
onSaved: () => void | Promise<void>; onSaved: () => void | Promise<void>;
}; };
function isoToScheduleValue(iso: string | null): string { function isoToScheduleValue(iso: string | null, timeZone: string): string {
return formatAdminInstant(iso, { return formatAdminInstantInTimeZone(iso, {
locale: getAdminRequestLocale(), locale: getAdminRequestLocale(),
timeZone,
}); });
} }
@@ -55,12 +57,13 @@ export function DrawEditDialog({
if (!open || draw == null) { if (!open || draw == null) {
return; return;
} }
setDrawTime(isoToScheduleValue(draw.draw_time)); const tz = scheduleTimezone ?? LOTTERY_SCHEDULE_TIMEZONE;
setCloseTime(isoToScheduleValue(draw.close_time)); setDrawTime(isoToScheduleValue(draw.draw_time, tz));
setStartTime(isoToScheduleValue(draw.start_time)); setCloseTime(isoToScheduleValue(draw.close_time, tz));
setStartTime(isoToScheduleValue(draw.start_time, tz));
setDrawNo(draw.draw_no); setDrawNo(draw.draw_no);
}); });
}, [open, draw]); }, [open, draw, scheduleTimezone]);
async function submit(): Promise<void> { async function submit(): Promise<void> {
if (draw == null) { if (draw == null) {
@@ -95,7 +98,7 @@ export function DrawEditDialog({
<DialogTitle>{t("editDraw.title")}</DialogTitle> <DialogTitle>{t("editDraw.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
{t("editDraw.description", { {t("editDraw.description", {
tz: "Local", tz: scheduleTimezone ?? LOTTERY_SCHEDULE_TIMEZONE,
drawNo: draw?.draw_no ?? "", drawNo: draw?.draw_no ?? "",
})} })}
</DialogDescription> </DialogDescription>

View File

@@ -14,10 +14,11 @@ import {
postAdminCancelDraw, postAdminCancelDraw,
postAdminGenerateDrawPlan, postAdminGenerateDrawPlan,
} from "@/api/admin-draws"; } from "@/api/admin-draws";
import { formatAdminInstant } from "@/lib/admin-datetime"; import { formatAdminInstantInTimeZone } from "@/lib/admin-datetime";
import { getAdminRequestLocale } from "@/lib/admin-locale"; import { getAdminRequestLocale } from "@/lib/admin-locale";
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; import { AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
@@ -52,7 +53,6 @@ import {
import { formatAdminMinorUnits } from "@/lib/money"; import { formatAdminMinorUnits } from "@/lib/money";
import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useExportLabels } from "@/hooks/use-export-labels"; import { useExportLabels } from "@/hooks/use-export-labels";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useAdminProfile } from "@/stores/admin-session"; import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
@@ -60,7 +60,6 @@ import type { AdminDrawListData, AdminDrawListItem } from "@/types/api/admin-dra
import { drawStatusLabel } from "./draw-display"; import { drawStatusLabel } from "./draw-display";
import { canManageDrawResults, canViewDrawFinance } from "@/lib/draw-access"; import { canManageDrawResults, canViewDrawFinance } from "@/lib/draw-access";
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
import { DrawStatusBadge } from "./draw-status-badge"; import { DrawStatusBadge } from "./draw-status-badge";
/** 下拉「不限」;请求时不传 status */ /** 下拉「不限」;请求时不传 status */
@@ -102,8 +101,9 @@ export function DrawsIndexConsole() {
const [data, setData] = useState<AdminDrawListData | null>(null); const [data, setData] = useState<AdminDrawListData | null>(null);
const formatDt = useCallback( const formatDt = useCallback(
(iso: string | null | undefined) => (iso: string | null | undefined) =>
formatAdminInstant(iso, { formatAdminInstantInTimeZone(iso, {
locale: getAdminRequestLocale(), locale: getAdminRequestLocale(),
timeZone: LOTTERY_SCHEDULE_TIMEZONE,
}), }),
[], [],
); );
@@ -156,7 +156,7 @@ export function DrawsIndexConsole() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [page, perPage, appliedDrawNo, appliedStatus, appliedAgentNodeId]); }, [page, perPage, appliedDrawNo, appliedStatus, appliedAgentNodeId, tRef]);
async function generatePlan(): Promise<void> { async function generatePlan(): Promise<void> {
setGenerating(true); setGenerating(true);
@@ -559,6 +559,7 @@ export function DrawsIndexConsole() {
<DrawCreateDialog <DrawCreateDialog
open={createOpen} open={createOpen}
onOpenChange={setCreateOpen} onOpenChange={setCreateOpen}
scheduleTimezone={LOTTERY_SCHEDULE_TIMEZONE}
onCreated={load} onCreated={load}
/> />
) : null} ) : null}
@@ -571,6 +572,7 @@ export function DrawsIndexConsole() {
} }
}} }}
draw={editDraw} draw={editDraw}
scheduleTimezone={LOTTERY_SCHEDULE_TIMEZONE}
onSaved={load} onSaved={load}
/> />
) : null} ) : null}

View File

@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import type { SettlementAdjustmentRow } from "@/api/admin-agent-settlement"; import type { SettlementAdjustmentRow } from "@/api/admin-agent-settlement";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
import { AdminLoadingState } from "@/components/admin/admin-loading-state"; import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range"; import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics"; import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
import { import {
@@ -30,6 +31,7 @@ export function SettlementAdjustmentsTable({
onOpenBill, onOpenBill,
}: SettlementAdjustmentsTableProps): React.ReactElement { }: SettlementAdjustmentsTableProps): React.ReactElement {
const { t } = useTranslation(["settlementCenter", "common"]); const { t } = useTranslation(["settlementCenter", "common"]);
const formatTs = useAdminDateTimeFormatter();
if (loading) { if (loading) {
return <AdminLoadingState />; return <AdminLoadingState />;
@@ -71,7 +73,7 @@ export function SettlementAdjustmentsTable({
{formatDashboardMoneyMinor(row.amount, currencyCode)} {formatDashboardMoneyMinor(row.amount, currencyCode)}
</TableCell> </TableCell>
<TableCell className="max-w-[200px] truncate text-sm">{row.reason ?? "—"}</TableCell> <TableCell className="max-w-[200px] truncate text-sm">{row.reason ?? "—"}</TableCell>
<TableCell className="text-xs text-muted-foreground">{row.created_at ?? "—"}</TableCell> <TableCell className="text-xs text-muted-foreground">{formatTs(row.created_at)}</TableCell>
<TableCell> <TableCell>
{row.original_bill_id != null ? ( {row.original_bill_id != null ? (
<button <button

View File

@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import type { SettlementPaymentRow } from "@/api/admin-agent-settlement"; import type { SettlementPaymentRow } from "@/api/admin-agent-settlement";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state"; import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
import { AdminLoadingState } from "@/components/admin/admin-loading-state"; import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range"; import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics"; import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
import { settlementBillTypeLabel } from "@/modules/settlement/settlement-status-label"; import { settlementBillTypeLabel } from "@/modules/settlement/settlement-status-label";
@@ -31,6 +32,7 @@ export function SettlementPaymentsTable({
onOpenBill, onOpenBill,
}: SettlementPaymentsTableProps): React.ReactElement { }: SettlementPaymentsTableProps): React.ReactElement {
const { t } = useTranslation(["settlementCenter", "agents", "common"]); const { t } = useTranslation(["settlementCenter", "agents", "common"]);
const formatTs = useAdminDateTimeFormatter();
if (loading) { if (loading) {
return <AdminLoadingState />; return <AdminLoadingState />;
@@ -77,9 +79,13 @@ export function SettlementPaymentsTable({
{formatDashboardMoneyMinor(row.amount, currencyCode)} {formatDashboardMoneyMinor(row.amount, currencyCode)}
</TableCell> </TableCell>
<TableCell>{row.method ?? "—"}</TableCell> <TableCell>{row.method ?? "—"}</TableCell>
<TableCell>{row.status}</TableCell> <TableCell>
{t(`paymentStatus.${row.status}`, {
defaultValue: row.status === "confirmed" ? "已确认" : row.status,
})}
</TableCell>
<TableCell className="text-xs text-muted-foreground"> <TableCell className="text-xs text-muted-foreground">
{row.confirmed_at ?? row.created_at ?? "—"} {formatTs(row.confirmed_at ?? row.created_at)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<button <button

View File

@@ -414,11 +414,11 @@ export function TransferOrdersPanel(): React.ReactElement {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [page, perPage, applied]); }, [page, perPage, applied, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();
}, [page, perPage, applied]); }, [page, perPage, applied, tRef]);
const runSearch = () => { const runSearch = () => {
setApplied({ ...draft }); setApplied({ ...draft });
@@ -696,7 +696,7 @@ export function WalletTxnsPanel(): React.ReactElement {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [page, perPage, applied]); }, [page, perPage, applied, tRef]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();
@@ -971,7 +971,7 @@ export function PlayerWalletPanel(): React.ReactElement {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [playerId]); }, [playerId, tRef]);
return ( return (
<Card> <Card>