feat: 重构管理端列表与风控/结算导航,新增表格导出和结算审核确认
This commit is contained in:
@@ -17,6 +17,15 @@ import { AdminListPaginationFooter } from "@/components/admin/admin-list-paginat
|
||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -25,9 +34,13 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { formatAdminMinorUnits } from "@/lib/money";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "@/modules/draws/draw-prd";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type {
|
||||
AdminSettlementBatchDetailsData,
|
||||
@@ -38,8 +51,13 @@ type Props = {
|
||||
batchId: number;
|
||||
};
|
||||
|
||||
type SettlementAction = "approve" | "reject" | "payout";
|
||||
|
||||
export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
const { t } = useTranslation(["settlement", "common"]);
|
||||
const profile = useAdminProfile();
|
||||
const canReviewSettlement = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_REVIEW]);
|
||||
const canManagePayout = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_MANAGE]);
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const [summary, setSummary] = useState<AdminSettlementBatchShowData | null>(null);
|
||||
const [details, setDetails] = useState<AdminSettlementBatchDetailsData | null>(null);
|
||||
@@ -48,6 +66,8 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(25);
|
||||
const [acting, setActing] = useState<string | null>(null);
|
||||
const [pendingAction, setPendingAction] = useState<SettlementAction | null>(null);
|
||||
const [reviewRemark, setReviewRemark] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -73,6 +93,8 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
try {
|
||||
await action();
|
||||
toast.success(t("actionSuccess", { name: label }));
|
||||
setPendingAction(null);
|
||||
setReviewRemark("");
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name: label }));
|
||||
@@ -81,6 +103,37 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const actionLabel = (action: SettlementAction): string => {
|
||||
if (action === "approve") {
|
||||
return t("approve");
|
||||
}
|
||||
if (action === "reject") {
|
||||
return t("reject");
|
||||
}
|
||||
return t("runPayout");
|
||||
};
|
||||
|
||||
const confirmPendingAction = (): void => {
|
||||
if (!summary || pendingAction === null) {
|
||||
return;
|
||||
}
|
||||
const remark = reviewRemark.trim() || undefined;
|
||||
if (pendingAction === "approve") {
|
||||
void runAction(actionLabel(pendingAction), () => postAdminApproveSettlementBatch(batchId, remark));
|
||||
return;
|
||||
}
|
||||
if (pendingAction === "reject") {
|
||||
void runAction(actionLabel(pendingAction), () => postAdminRejectSettlementBatch(batchId, remark));
|
||||
return;
|
||||
}
|
||||
void runAction(actionLabel(pendingAction), () => postAdminPayoutSettlementBatch(batchId));
|
||||
};
|
||||
|
||||
const openActionDialog = (action: SettlementAction): void => {
|
||||
setReviewRemark("");
|
||||
setPendingAction(action);
|
||||
};
|
||||
|
||||
async function exportCsv(): Promise<void> {
|
||||
setActing(t("export"));
|
||||
try {
|
||||
@@ -156,6 +209,14 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
<span className="text-muted-foreground">{t("winTotal")}</span>{" "}
|
||||
<span className="tabular-nums">{summary.total_win_count}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground">{t("totalBet")}</span>{" "}
|
||||
<span className="font-mono tabular-nums">{formatAdminMinorUnits(summary.total_bet_amount)}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground">{t("actualDeduct")}</span>{" "}
|
||||
<span className="font-mono tabular-nums">{formatAdminMinorUnits(summary.total_actual_deduct)}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground">{t("payoutAmount")}</span>{" "}
|
||||
<span className="font-mono tabular-nums">{formatAdminMinorUnits(summary.total_payout_amount)}</span>
|
||||
@@ -166,6 +227,17 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
{formatAdminMinorUnits(summary.total_jackpot_payout_amount)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground">{t("platformProfit")}</span>{" "}
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono tabular-nums",
|
||||
summary.platform_profit < 0 ? "text-destructive" : "text-emerald-700",
|
||||
)}
|
||||
>
|
||||
{formatAdminMinorUnits(summary.platform_profit)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground">{t("startedAt")}</span> {formatDt(summary.started_at)}
|
||||
</p>
|
||||
@@ -177,8 +249,8 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={acting !== null || summary.status !== "pending_review"}
|
||||
onClick={() => void runAction(t("approve"), () => postAdminApproveSettlementBatch(batchId))}
|
||||
disabled={!canReviewSettlement || acting !== null || summary.status !== "pending_review"}
|
||||
onClick={() => openActionDialog("approve")}
|
||||
>
|
||||
{t("approve")}
|
||||
</Button>
|
||||
@@ -186,16 +258,16 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={acting !== null || summary.status !== "pending_review"}
|
||||
onClick={() => void runAction(t("reject"), () => postAdminRejectSettlementBatch(batchId))}
|
||||
disabled={!canReviewSettlement || acting !== null || summary.status !== "pending_review"}
|
||||
onClick={() => openActionDialog("reject")}
|
||||
>
|
||||
{t("reject")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={acting !== null || summary.status !== "approved"}
|
||||
onClick={() => void runAction(t("runPayout"), () => postAdminPayoutSettlementBatch(batchId))}
|
||||
disabled={!canManagePayout || acting !== null || summary.status !== "approved"}
|
||||
onClick={() => openActionDialog("payout")}
|
||||
>
|
||||
{t("runPayout")}
|
||||
</Button>
|
||||
@@ -216,7 +288,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
<CardContent>
|
||||
{details ? (
|
||||
<>
|
||||
<Table>
|
||||
<Table id={`settlement-details-table-${batchId}`}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("ticketNo")}</TableHead>
|
||||
@@ -267,6 +339,71 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Dialog
|
||||
open={pendingAction !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && acting === null) {
|
||||
setPendingAction(null);
|
||||
setReviewRemark("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
{summary && pendingAction ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("confirmAction", { name: actionLabel(pendingAction) })}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{pendingAction === "payout"
|
||||
? t("confirmPayoutDesc")
|
||||
: t("confirmActionDesc", { drawNo: summary.draw_no ?? "—" })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<p className="rounded-md border bg-muted/30 p-3 text-sm">
|
||||
{t("confirmAmountLine", {
|
||||
actual: formatAdminMinorUnits(summary.total_actual_deduct),
|
||||
payout: formatAdminMinorUnits(summary.total_payout_amount),
|
||||
profit: formatAdminMinorUnits(summary.platform_profit),
|
||||
})}
|
||||
</p>
|
||||
{pendingAction !== "payout" ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="settlement-detail-review-remark">{t("reviewRemark")}</Label>
|
||||
<Textarea
|
||||
id="settlement-detail-review-remark"
|
||||
value={reviewRemark}
|
||||
placeholder={t("reviewRemarkPlaceholder")}
|
||||
onChange={(event) => setReviewRemark(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={acting !== null}
|
||||
onClick={() => {
|
||||
setPendingAction(null);
|
||||
setReviewRemark("");
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={pendingAction === "reject" ? "destructive" : "default"}
|
||||
disabled={acting !== null}
|
||||
onClick={confirmPendingAction}
|
||||
>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ModuleScaffold>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user