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
284 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|