feat(api, ui, i18n): 增强奖池管理与钱包功能
新增奖池余额调整与调整记录查询相关 API,提升后台对奖池的管理与控制能力。 更新奖池与钱包相关多语言文案,新增余额调整与转账完成提示信息,提升用户理解与反馈体验。 优化奖池管理相关 UI 组件,新增余额调整功能并改进页面布局,提升操作易用性。 重构相关组件以整合新功能,并进一步优化后台管理界面的整体用户体验。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Copy } from "lucide-react";
|
||||
import { Copy, Loader2, MoreHorizontal } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -11,12 +11,20 @@ import {
|
||||
getAdminWalletTransactions,
|
||||
reverseTransferOrder,
|
||||
manuallyProcessTransferOrder,
|
||||
completeTransferInCredit,
|
||||
} from "@/api/admin-wallet";
|
||||
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
|
||||
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
|
||||
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -44,9 +52,11 @@ import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
import { useExportLabels } from "@/hooks/use-export-labels";
|
||||
import { formatAdminMinorUnits } from "@/lib/money";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type {
|
||||
AdminPlayerWalletsData,
|
||||
AdminTransferOrderItem,
|
||||
AdminTransferOrderListData,
|
||||
AdminWalletTxnListData,
|
||||
} from "@/types/api/admin-wallet";
|
||||
@@ -213,8 +223,29 @@ function canReverseTransferOrder(
|
||||
return canWriteWallet && (row.can_reverse ?? row.status === "pending_reconcile");
|
||||
}
|
||||
|
||||
function canCompleteTransferInCredit(
|
||||
row: {
|
||||
direction: string;
|
||||
status: string;
|
||||
fail_reason?: string | null;
|
||||
external_ref_no?: string | null;
|
||||
can_complete_credit?: boolean;
|
||||
},
|
||||
canWriteWallet: boolean,
|
||||
): boolean {
|
||||
return (
|
||||
canWriteWallet &&
|
||||
(row.can_complete_credit ??
|
||||
(row.direction === "in" &&
|
||||
row.status === "pending_reconcile" &&
|
||||
row.fail_reason === "lottery_credit_failed" &&
|
||||
Boolean(row.external_ref_no?.trim())))
|
||||
);
|
||||
}
|
||||
|
||||
function canManuallyProcessTransferOrder(
|
||||
row: {
|
||||
direction?: string;
|
||||
status: string;
|
||||
can_manually_process?: boolean;
|
||||
},
|
||||
@@ -222,7 +253,80 @@ function canManuallyProcessTransferOrder(
|
||||
): boolean {
|
||||
return (
|
||||
canWriteWallet &&
|
||||
(row.can_manually_process ?? ["processing", "failed", "pending_reconcile"].includes(row.status))
|
||||
(row.can_manually_process ??
|
||||
(["processing", "failed", "pending_reconcile"].includes(row.status) &&
|
||||
!(row.direction === "out" && row.status === "pending_reconcile")))
|
||||
);
|
||||
}
|
||||
|
||||
type TransferOrderRowActionsProps = {
|
||||
row: AdminTransferOrderItem;
|
||||
canWriteWallet: boolean;
|
||||
busy: boolean;
|
||||
onCompleteCredit: (transferNo: string) => void;
|
||||
onReverse: (transferNo: string) => void;
|
||||
onManualProcess: (transferNo: string) => void;
|
||||
t: (key: string) => string;
|
||||
};
|
||||
|
||||
function TransferOrderRowActions({
|
||||
row,
|
||||
canWriteWallet,
|
||||
busy,
|
||||
onCompleteCredit,
|
||||
onReverse,
|
||||
onManualProcess,
|
||||
t,
|
||||
}: TransferOrderRowActionsProps): React.ReactElement {
|
||||
const showComplete = canCompleteTransferInCredit(row, canWriteWallet);
|
||||
const showReverse = canReverseTransferOrder(row, canWriteWallet);
|
||||
const showManual = canManuallyProcessTransferOrder(row, canWriteWallet);
|
||||
|
||||
if (!showComplete && !showReverse && !showManual) {
|
||||
return <span className="text-xs text-muted-foreground">—</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
disabled={busy}
|
||||
aria-label={t("actionsMenuAriaLabel")}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon-sm" }),
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{busy ? (
|
||||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
||||
) : (
|
||||
<MoreHorizontal className="size-4" aria-hidden />
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-[11rem]">
|
||||
{showComplete ? (
|
||||
<DropdownMenuItem disabled={busy} onClick={() => onCompleteCredit(row.transfer_no)}>
|
||||
{t("completeCredit")}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{showManual ? (
|
||||
<DropdownMenuItem disabled={busy} onClick={() => onManualProcess(row.transfer_no)}>
|
||||
{t("manualProcess")}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{showReverse ? (
|
||||
<>
|
||||
{showComplete || showManual ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
disabled={busy}
|
||||
onClick={() => onReverse(row.transfer_no)}
|
||||
>
|
||||
{t("reverse")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -280,6 +384,14 @@ export function TransferOrdersPanel(): React.ReactElement {
|
||||
doAction(transferNo, () => manuallyProcessTransferOrder(transferNo), t("manualProcessSuccess")),
|
||||
});
|
||||
|
||||
const handleCompleteCredit = (transferNo: string) =>
|
||||
requestConfirm({
|
||||
title: t("confirm.completeCreditTitle"),
|
||||
description: t("confirm.completeCreditDescription", { transferNo }),
|
||||
onConfirm: () =>
|
||||
doAction(transferNo, () => completeTransferInCredit(transferNo), t("completeCreditSuccess")),
|
||||
});
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setErr(null);
|
||||
@@ -464,7 +576,7 @@ export function TransferOrdersPanel(): React.ReactElement {
|
||||
<TableHead className="min-w-0 max-w-[14rem]">{t("failReason")}</TableHead>
|
||||
<TableHead className="min-w-0 whitespace-normal leading-tight">{t("requestTime")}</TableHead>
|
||||
<TableHead className="min-w-0 whitespace-normal leading-tight">{t("finishedTime")}</TableHead>
|
||||
<TableHead className="w-24">{t("actions")}</TableHead>
|
||||
<TableHead className="w-12 text-center">{t("actions")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -506,36 +618,18 @@ export function TransferOrdersPanel(): React.ReactElement {
|
||||
<TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground">
|
||||
{formatTs(row.finished_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{canReverseTransferOrder(row, canWriteWallet) ||
|
||||
canManuallyProcessTransferOrder(row, canWriteWallet) ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
{canReverseTransferOrder(row, canWriteWallet) ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="text-xs"
|
||||
disabled={actionLoading.has(row.transfer_no)}
|
||||
onClick={() => handleReverse(row.transfer_no)}
|
||||
>
|
||||
{actionLoading.has(row.transfer_no) ? t("processing") : t("reverse")}
|
||||
</Button>
|
||||
) : null}
|
||||
{canManuallyProcessTransferOrder(row, canWriteWallet) ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
disabled={actionLoading.has(row.transfer_no)}
|
||||
onClick={() => handleManuallyProcess(row.transfer_no)}
|
||||
>
|
||||
{actionLoading.has(row.transfer_no) ? t("processing") : t("manualProcess")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
)}
|
||||
<TableCell className="text-center align-middle">
|
||||
<div className="flex justify-center">
|
||||
<TransferOrderRowActions
|
||||
row={row}
|
||||
canWriteWallet={canWriteWallet}
|
||||
busy={actionLoading.has(row.transfer_no)}
|
||||
onCompleteCredit={handleCompleteCredit}
|
||||
onReverse={handleReverse}
|
||||
onManualProcess={handleManuallyProcess}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user