feat: 重构管理端列表与风控/结算导航,新增表格导出和结算审核确认

This commit is contained in:
2026-05-19 17:06:56 +08:00
parent a1fb163f1b
commit 37b13278ef
47 changed files with 1255 additions and 524 deletions

View File

@@ -1,4 +1,4 @@
export const settlementModuleMeta = {
title: "Settlement",
title: "结算",
description: "",
} as const;

View File

@@ -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>
);
}

View File

@@ -6,16 +6,24 @@ import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
downloadAdminSettlementBatchExport,
getAdminSettlementBatches,
postAdminApproveSettlementBatch,
postAdminPayoutSettlementBatch,
postAdminRejectSettlementBatch,
} from "@/api/admin-settlement";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
@@ -33,25 +41,44 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { Textarea } from "@/components/ui/textarea";
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 { AdminSettlementBatchListData, AdminSettlementBatchRow } from "@/types/api/admin-settlement";
import { settlementModuleMeta } from "@/modules/settlement/meta";
const STATUS_ALL = "__all__";
const STATUS_OPTIONS: { value: string; label: string }[] = [
{ value: STATUS_ALL, label: "statusOptions.all" },
{ value: "running", label: "statusOptions.running" },
{ value: "pending_review", label: "statusOptions.pending_review" },
{ value: "approved", label: "statusOptions.approved" },
{ value: "rejected", label: "statusOptions.rejected" },
{ value: "paid", label: "statusOptions.paid" },
{ value: "completed", label: "statusOptions.completed" },
{ value: "failed", label: "statusOptions.failed" },
];
type SettlementAction = "approve" | "reject" | "payout";
type PendingAction = {
row: AdminSettlementBatchRow;
action: SettlementAction;
};
function settlementStatusText(value: string, t: (key: string) => string): string {
const option = STATUS_OPTIONS.find((item) => item.value === value);
return option ? t(option.label) : value;
}
export function SettlementBatchesConsole() {
const { t } = useTranslation(["settlement", "common"]);
const formatDt = useAdminDateTimeFormatter();
const profile = useAdminProfile();
const canReviewSettlement = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_REVIEW]);
const canManagePayout = adminHasAnyPermission(profile?.permissions, [PRD_PAYOUT_MANAGE]);
const [data, setData] = useState<AdminSettlementBatchListData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -62,6 +89,8 @@ export function SettlementBatchesConsole() {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const [actingId, setActingId] = useState<number | null>(null);
const [pendingAction, setPendingAction] = useState<PendingAction | null>(null);
const [reviewRemark, setReviewRemark] = useState("");
const load = useCallback(async () => {
setLoading(true);
@@ -101,6 +130,8 @@ export function SettlementBatchesConsole() {
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 }));
@@ -109,172 +140,184 @@ export function SettlementBatchesConsole() {
}
}
async function exportBatch(batchId: number): Promise<void> {
setActingId(batchId);
try {
const blob = await downloadAdminSettlementBatchExport(batchId);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `settlement-${batchId}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("exportFailed"));
} finally {
setActingId(null);
const actionLabel = (action: SettlementAction): string => {
if (action === "approve") {
return t("approve");
}
}
if (action === "reject") {
return t("reject");
}
return t("runPayout");
};
const confirmPendingAction = (): void => {
if (!pendingAction) {
return;
}
const { row, action } = pendingAction;
const remark = reviewRemark.trim() || undefined;
if (action === "approve") {
void runBatchAction(row.id, actionLabel(action), () => postAdminApproveSettlementBatch(row.id, remark));
return;
}
if (action === "reject") {
void runBatchAction(row.id, actionLabel(action), () => postAdminRejectSettlementBatch(row.id, remark));
return;
}
void runBatchAction(row.id, actionLabel(action), () => postAdminPayoutSettlementBatch(row.id));
};
const openActionDialog = (row: AdminSettlementBatchRow, action: SettlementAction): void => {
setReviewRemark("");
setPendingAction({ row, action });
};
return (
<ModuleScaffold>
<div className="mb-6">
<h1 className="text-lg font-semibold tracking-tight">{settlementModuleMeta.title}</h1>
</div>
<Card className="mb-6">
<CardHeader className="pb-3">
<CardTitle className="text-base">{t("filter")}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-end">
<div className="flex min-w-[12rem] flex-1 flex-col gap-1.5">
<Label htmlFor="sb-draw-no">{t("drawNo")}</Label>
<Input
id="sb-draw-no"
value={draftDrawNo}
onChange={(e) => setDraftDrawNo(e.target.value)}
placeholder={t("placeholderDrawNo")}
className="font-mono"
/>
<Card className="admin-list-card">
<CardHeader className="admin-list-header space-y-6 pb-6">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<CardTitle className="admin-list-title">{t("batchList")}</CardTitle>
</div>
<div className="flex min-w-[10rem] flex-col gap-1.5">
<Label>{t("status")}</Label>
<Select value={draftStatus} onValueChange={(v) => setDraftStatus(v ?? STATUS_ALL)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{t(o.label)}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="admin-list-toolbar">
<div className="admin-list-field">
<Label htmlFor="sb-draw-no" className="sm:w-10 sm:shrink-0">
{t("drawNo")}
</Label>
<Input
id="sb-draw-no"
value={draftDrawNo}
onChange={(e) => setDraftDrawNo(e.target.value)}
placeholder={t("placeholderDrawNo")}
className="w-full font-mono sm:w-[18rem] xl:w-[20rem]"
/>
</div>
<div className="admin-list-field">
<Label className="sm:w-10 sm:shrink-0">{t("status")}</Label>
<Select value={draftStatus} onValueChange={(v) => setDraftStatus(v ?? STATUS_ALL)}>
<SelectTrigger className="w-full sm:w-40">
<SelectValue>{settlementStatusText(draftStatus, t)}</SelectValue>
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{t(o.label)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="admin-list-actions">
<AdminTableExportButton
tableId="settlement-batches-table"
filename="结算批次"
sheetName="结算批次"
/>
<Button type="button" className="xl:shrink-0" onClick={applyFilters}>
{t("apply")}
</Button>
</div>
</div>
<Button type="button" onClick={applyFilters}>
{t("apply")}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">{t("batchList")}</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="admin-list-content pt-0">
{error ? <p className="text-destructive text-sm">{error}</p> : null}
{loading && !data ? (
<p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>{t("drawNo")}</TableHead>
<TableHead>{t("version", { ns: "draws", version: "" }).replace(" v", "").trim()}</TableHead>
<TableHead>{t("reviewStatus")}</TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead className="text-right">{t("ticketCount")}</TableHead>
<TableHead className="text-right">{t("winCount")}</TableHead>
<TableHead className="text-right">{t("payoutTotal")}</TableHead>
<TableHead className="text-right">{t("jackpot")}</TableHead>
<TableHead>{t("finishedAt")}</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{(data?.items ?? []).map((row: AdminSettlementBatchRow) => (
<TableRow key={row.id}>
<TableCell className="font-mono text-xs">{row.id}</TableCell>
<TableCell className="font-mono text-sm">{row.draw_no ?? "—"}</TableCell>
<TableCell className="font-mono text-xs">v{row.settle_version}</TableCell>
<TableCell className="text-xs text-muted-foreground">
{row.review_status ?? "—"}
</TableCell>
<TableCell>
<span
<div className="admin-table-shell">
<Table id="settlement-batches-table">
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>{t("drawNo")}</TableHead>
<TableHead className="text-right">{t("totalBet")}</TableHead>
<TableHead className="text-right">{t("actualDeduct")}</TableHead>
<TableHead className="text-right">{t("payoutTotal")}</TableHead>
<TableHead className="text-right">{t("platformProfit")}</TableHead>
<TableHead>{t("reviewStatus")}</TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{(data?.items ?? []).map((row: AdminSettlementBatchRow) => (
<TableRow key={row.id}>
<TableCell className="font-mono text-xs">{row.id}</TableCell>
<TableCell className="font-mono text-sm">{row.draw_no ?? "—"}</TableCell>
<TableCell className="text-right font-mono text-xs tabular-nums">
{formatAdminMinorUnits(row.total_bet_amount)}
</TableCell>
<TableCell className="text-right font-mono text-xs tabular-nums">
{formatAdminMinorUnits(row.total_actual_deduct)}
</TableCell>
<TableCell className="text-right font-mono text-xs tabular-nums">
{formatAdminMinorUnits(row.total_payout_amount)}
</TableCell>
<TableCell
className={cn(
"rounded px-1.5 py-0.5 text-xs font-medium",
row.status === "completed" && "bg-emerald-500/15 text-emerald-800",
row.status === "running" && "bg-amber-500/15 text-amber-900",
row.status === "failed" && "bg-destructive/15 text-destructive",
"text-right font-mono text-xs tabular-nums",
row.platform_profit < 0 ? "text-destructive" : "text-emerald-700",
)}
>
{row.status}
</span>
</TableCell>
<TableCell className="text-right tabular-nums">{row.total_ticket_count}</TableCell>
<TableCell className="text-right tabular-nums">{row.total_win_count}</TableCell>
<TableCell className="text-right font-mono text-xs tabular-nums">
{formatAdminMinorUnits(row.total_payout_amount)}
</TableCell>
<TableCell className="text-right font-mono text-xs tabular-nums">
{formatAdminMinorUnits(row.total_jackpot_payout_amount)}
</TableCell>
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
{formatDt(row.finished_at ?? row.started_at)}
</TableCell>
<TableCell>
<div className="flex flex-wrap justify-end gap-1.5">
<Link
href={`/admin/settlement-batches/${row.id}/details`}
className={cn(buttonVariants({ variant: "link", size: "sm" }), "px-0")}
{formatAdminMinorUnits(row.platform_profit)}
</TableCell>
<TableCell className="text-xs text-muted-foreground">{row.review_status ?? "—"}</TableCell>
<TableCell>
<span
className={cn(
"rounded px-1.5 py-0.5 text-xs font-medium",
["completed", "paid"].includes(row.status) && "bg-emerald-500/15 text-emerald-800",
row.status === "running" && "bg-amber-500/15 text-amber-900",
row.status === "pending_review" && "bg-blue-500/15 text-blue-800",
row.status === "approved" && "bg-indigo-500/15 text-indigo-800",
row.status === "rejected" && "bg-muted text-muted-foreground",
row.status === "failed" && "bg-destructive/15 text-destructive",
)}
>
{t("details")}
</Link>
<Button
type="button"
size="sm"
variant="outline"
disabled={actingId !== null || row.status !== "pending_review"}
onClick={() => void runBatchAction(row.id, t("approve"), () => postAdminApproveSettlementBatch(row.id))}
>
{t("pass")}
</Button>
<Button
type="button"
size="sm"
variant="outline"
disabled={actingId !== null || row.status !== "pending_review"}
onClick={() => void runBatchAction(row.id, t("reject"), () => postAdminRejectSettlementBatch(row.id))}
>
{t("reject")}
</Button>
<Button
type="button"
size="sm"
disabled={actingId !== null || row.status !== "approved"}
onClick={() => void runBatchAction(row.id, t("runPayout"), () => postAdminPayoutSettlementBatch(row.id))}
>
{t("payout")}
</Button>
<Button
type="button"
size="sm"
variant="secondary"
disabled={actingId !== null}
onClick={() => void exportBatch(row.id)}
>
{t("export")}
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{settlementStatusText(row.status, t)}
</span>
</TableCell>
<TableCell>
<div className="flex flex-wrap justify-end gap-1.5">
<Link
href={`/admin/settlement-batches/${row.id}/details`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "!border-border")}
>
{t("details")}
</Link>
<Button
type="button"
size="sm"
variant="outline"
disabled={!canReviewSettlement || actingId !== null || row.status !== "pending_review"}
onClick={() => openActionDialog(row, "approve")}
>
{t("pass")}
</Button>
<Button
type="button"
size="sm"
variant="outline"
disabled={!canReviewSettlement || actingId !== null || row.status !== "pending_review"}
onClick={() => openActionDialog(row, "reject")}
>
{t("reject")}
</Button>
<Button
type="button"
size="sm"
disabled={!canManagePayout || actingId !== null || row.status !== "approved"}
onClick={() => openActionDialog(row, "payout")}
>
{t("payout")}
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{data ? (
<AdminListPaginationFooter
@@ -293,6 +336,71 @@ export function SettlementBatchesConsole() {
) : null}
</CardContent>
</Card>
<Dialog
open={pendingAction !== null}
onOpenChange={(open) => {
if (!open && actingId === null) {
setPendingAction(null);
setReviewRemark("");
}
}}
>
<DialogContent>
{pendingAction ? (
<>
<DialogHeader>
<DialogTitle>{t("confirmAction", { name: actionLabel(pendingAction.action) })}</DialogTitle>
<DialogDescription>
{pendingAction.action === "payout"
? t("confirmPayoutDesc")
: t("confirmActionDesc", { drawNo: pendingAction.row.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(pendingAction.row.total_actual_deduct),
payout: formatAdminMinorUnits(pendingAction.row.total_payout_amount),
profit: formatAdminMinorUnits(pendingAction.row.platform_profit),
})}
</p>
{pendingAction.action !== "payout" ? (
<div className="space-y-2">
<Label htmlFor="settlement-review-remark">{t("reviewRemark")}</Label>
<Textarea
id="settlement-review-remark"
value={reviewRemark}
placeholder={t("reviewRemarkPlaceholder")}
onChange={(event) => setReviewRemark(event.target.value)}
/>
</div>
) : null}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
disabled={actingId !== null}
onClick={() => {
setPendingAction(null);
setReviewRemark("");
}}
>
{t("cancel")}
</Button>
<Button
type="button"
variant={pendingAction.action === "reject" ? "destructive" : "default"}
disabled={actingId !== null}
onClick={confirmPendingAction}
>
{t("confirm")}
</Button>
</DialogFooter>
</>
) : null}
</DialogContent>
</Dialog>
</ModuleScaffold>
);
}