Files
lotteryAdmin/src/modules/settlement/settlement-bills-table.tsx
kang 6ea0a6feec feat(agents, config, dashboard, i18n): add agent line provision wizard, site deletion, and site dashboard with multi-language support
Added agent line provision wizard page with permission gating, replacing redirect placeholder. Introduced site deletion API and UI with confirmation dialog in integration sites management. Added new site-scoped dashboard panel showing bet metrics, P/L trends, active players, and quick links. Enhanced chart tooltip to support custom formatters and fix indicator color
2026-06-12 20:47:53 +08:00

284 lines
11 KiB
TypeScript

"use client";
import { ArrowRight, Eye } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { SettlementBillRow } from "@/api/admin-agent-settlement";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { signedMoneyClass } from "@/lib/admin-signed-money";
import { cn } from "@/lib/utils";
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
import {
formatSignedSettlementMoney,
} from "@/modules/settlement/settlement-signed-money";
import {
describeBillPaymentDirection,
} from "@/modules/settlement/settlement-bill-display";
import {
formatPlatformPartyLabel,
SettlementDashCell,
} from "@/modules/settlement/settlement-party-cells";
import {
settlementBillStatusLabel,
settlementBillTypeLabel,
} from "@/modules/settlement/settlement-status-label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
type BillTypeFilter = "all" | "player" | "agent";
type SettlementBillsTableProps = {
rows: SettlementBillRow[];
loading: boolean;
currencyCode: string;
billTypeFilter?: BillTypeFilter;
emptyMessage?: string;
onOpenDetail: (billId: number) => void;
};
function billRowTone(row: SettlementBillRow): string {
if (row.bill_type === "player") {
return "border-l-2 border-l-sky-300/80";
}
if (row.bill_type === "agent") {
return "border-l-2 border-l-amber-300/80 bg-amber-50/20";
}
if (row.bill_type === "adjustment" || row.bill_type === "reversal") {
return "border-l-2 border-l-emerald-300/80 bg-emerald-50/20";
}
if (row.bill_type === "bad_debt") {
return "border-l-2 border-l-rose-300/80 bg-rose-50/20";
}
return "";
}
function billTypeTone(row: SettlementBillRow): string {
if (row.bill_type === "player") {
return "border-sky-200 bg-sky-50 text-sky-700";
}
if (row.bill_type === "agent") {
return "border-amber-200 bg-amber-50 text-amber-800";
}
if (row.bill_type === "adjustment" || row.bill_type === "reversal") {
return "border-emerald-200 bg-emerald-50 text-emerald-700";
}
if (row.bill_type === "bad_debt") {
return "border-rose-200 bg-rose-50 text-rose-700";
}
return "border-border/70 bg-muted/25 text-muted-foreground";
}
function unpaidMoneyClass(row: SettlementBillRow): string {
if (row.unpaid_amount <= 0) {
return "text-muted-foreground";
}
if (row.status === "overdue") {
return "font-medium text-destructive";
}
return "font-medium text-amber-800 dark:text-amber-300";
}
function paidMoneyClass(row: SettlementBillRow): string {
if ((row.paid_amount ?? 0) <= 0) {
return "text-muted-foreground";
}
if (row.unpaid_amount > 0) {
return "font-medium text-amber-800 dark:text-amber-300";
}
return "font-medium text-emerald-700";
}
function ownerPartyLabel(row: SettlementBillRow): string | null {
if (row.bill_type === "player") {
return row.player_username ?? row.owner_label ?? null;
}
if (row.bill_type === "agent") {
return row.owner_party_label ?? row.owner_label ?? null;
}
return row.owner_label ?? null;
}
export function SettlementBillsTable({
rows,
loading,
currencyCode,
billTypeFilter = "all",
emptyMessage,
onOpenDetail,
}: SettlementBillsTableProps): React.ReactElement {
const { t } = useTranslation(["settlementCenter", "agents", "common"]);
const agentView = billTypeFilter === "agent";
const playerView = billTypeFilter === "player";
const mixedView = billTypeFilter === "all";
if (loading) {
return <AdminLoadingState />;
}
if (rows.length === 0) {
return <AdminNoResourceState message={emptyMessage} />;
}
return (
<div className="admin-table-shell overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("columns.billId", { defaultValue: "账单 ID" })}</TableHead>
<TableHead>{t("columns.period", { defaultValue: "账期" })}</TableHead>
<TableHead>{t("columns.type", { defaultValue: "类型" })}</TableHead>
{playerView ? (
<>
<TableHead>{t("columns.playerAccount", { defaultValue: "玩家账号" })}</TableHead>
<TableHead>{t("columns.playerId", { defaultValue: "玩家 ID" })}</TableHead>
<TableHead>{t("columns.directAgent", { defaultValue: "直属代理" })}</TableHead>
</>
) : null}
{agentView ? (
<TableHead>{t("columns.owner", { defaultValue: "本方" })}</TableHead>
) : null}
{mixedView ? (
<TableHead>{t("columns.owner", { defaultValue: "本方" })}</TableHead>
) : null}
<TableHead>{t("billDisplay.settlementFlow", { defaultValue: "谁付谁" })}</TableHead>
<TableHead>{t("columns.superiorAgent", { defaultValue: "上级" })}</TableHead>
{!playerView ? (
<TableHead className="text-right">{t("columns.gross", { defaultValue: "输赢" })}</TableHead>
) : null}
<TableHead className="text-right">{t("billDisplay.settlementAmount", { defaultValue: "结算金额" })}</TableHead>
<TableHead className="text-right">{t("columns.paid", { defaultValue: "已收付" })}</TableHead>
<TableHead className="text-right">{t("columns.unpaid", { defaultValue: "未结" })}</TableHead>
<TableHead>{t("columns.status", { defaultValue: "状态" })}</TableHead>
<TableHead className="sticky right-0 z-10 w-14 bg-muted text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
{t("common:table.actions", { defaultValue: "操作" })}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => {
const isPlayerBill = row.bill_type === "player";
const direction = describeBillPaymentDirection(row, t);
return (
<TableRow key={row.id} className={billRowTone(row)}>
<TableCell className="font-mono text-xs">{row.id}</TableCell>
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
{formatSettlementPeriodSpan(row.period_start, row.period_end)}
</TableCell>
<TableCell>
<span
className={cn(
"inline-flex rounded-full border px-2 py-0.5 text-xs font-medium",
billTypeTone(row),
)}
>
{settlementBillTypeLabel(row.bill_type, t)}
</span>
</TableCell>
{playerView ? (
<>
<TableCell>
<SettlementDashCell value={row.player_username ?? row.owner_label} />
</TableCell>
<TableCell className="font-mono text-xs">
<SettlementDashCell
value={row.player_site_player_id ?? row.player_id_display ?? row.owner_id}
mono
/>
</TableCell>
<TableCell className="text-sm">
<SettlementDashCell value={row.direct_agent_label} />
</TableCell>
</>
) : null}
{agentView ? (
<TableCell className="text-sm">
<SettlementDashCell value={ownerPartyLabel(row)} />
</TableCell>
) : null}
{mixedView ? (
<TableCell className="text-sm">
<SettlementDashCell value={ownerPartyLabel(row)} />
</TableCell>
) : null}
<TableCell className="min-w-[10rem] text-sm">
<div className="flex flex-wrap items-center gap-1 text-foreground">
<span className="font-medium">{direction.payer}</span>
<ArrowRight className="size-3.5 shrink-0 text-muted-foreground" aria-hidden />
<span className="font-medium">{direction.payee}</span>
</div>
</TableCell>
<TableCell className="text-sm">
{formatPlatformPartyLabel(row.superior_agent_label, t)}
</TableCell>
{!playerView ? (
<TableCell
className={cn(
"text-right tabular-nums",
row.gross_win_loss != null
? signedMoneyClass(row.gross_win_loss)
: "text-muted-foreground",
)}
>
{row.gross_win_loss != null ? (
<div>{formatSignedSettlementMoney(row.gross_win_loss, currencyCode)}</div>
) : (
"—"
)}
</TableCell>
) : null}
<TableCell className="text-right tabular-nums">
<div className={cn("font-semibold", signedMoneyClass(row.net_amount, true))}>
{formatDashboardMoneyMinor(direction.amount, currencyCode)}
</div>
</TableCell>
<TableCell className={cn("text-right tabular-nums", paidMoneyClass(row))}>
{formatDashboardMoneyMinor(row.paid_amount ?? 0, currencyCode)}
</TableCell>
<TableCell className={cn("text-right tabular-nums", unpaidMoneyClass(row))}>
{formatDashboardMoneyMinor(row.unpaid_amount, currencyCode)}
</TableCell>
<TableCell>
<AdminStatusBadge status={row.status}>
{settlementBillStatusLabel(row.status, t)}
</AdminStatusBadge>
</TableCell>
<TableCell
className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]"
onClick={(e) => e.stopPropagation()}
>
<AdminRowActionsMenu
actions={[
{
key: "detail",
label: t("actions.detail", { defaultValue: "详情" }),
icon: Eye,
onClick: () => onOpenDetail(row.id),
},
]}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}