feat(agents, i18n): enhance agent management and settlement features with new translations and UI updates
Added new translations for agent management and settlement features in English, Nepali, and Chinese, improving multi-language support. Updated the agents console to reflect changes in funding modes and player details, enhancing user experience. Refactored the admin permission gate to include new logic for handling bound line agents, ensuring better permission management. Additionally, streamlined the UI for agent-related pages and improved navigation to the settlement center, consolidating related functionalities for better accessibility.
This commit is contained in:
286
src/modules/settlement/agent-settlement-report-view.tsx
Normal file
286
src/modules/settlement/agent-settlement-report-view.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
"use client";
|
||||
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { formatDashboardCreditMajor, formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { AgentSettlementReportType } from "@/api/admin-agent-settlement";
|
||||
|
||||
type AgentSettlementReportViewProps = {
|
||||
reportType: AgentSettlementReportType;
|
||||
data: unknown;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function asRows(value: unknown): Record<string, unknown>[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value.filter((row): row is Record<string, unknown> => row !== null && typeof row === "object");
|
||||
}
|
||||
|
||||
function money(
|
||||
value: unknown,
|
||||
currencyCode: string,
|
||||
): string {
|
||||
return formatDashboardMoneyMinor(Number(value ?? 0), currencyCode);
|
||||
}
|
||||
|
||||
function creditMoney(value: unknown, currencyCode: string): string {
|
||||
return formatDashboardCreditMajor(Number(value ?? 0), currencyCode);
|
||||
}
|
||||
|
||||
export function AgentSettlementReportView({
|
||||
reportType,
|
||||
data,
|
||||
currencyCode,
|
||||
}: AgentSettlementReportViewProps): React.ReactElement {
|
||||
const { t } = useTranslation(["agents", "common"]);
|
||||
const root = asRecord(data);
|
||||
|
||||
if (reportType === "summary" && root) {
|
||||
const stats = [
|
||||
{ label: t("settlementReports.summary.billCount", { defaultValue: "账单数" }), value: String(root.bill_count ?? 0) },
|
||||
{ label: t("settlementReports.summary.totalNet", { defaultValue: "净额合计" }), value: money(root.total_net, currencyCode) },
|
||||
{ label: t("settlementReports.summary.totalUnpaid", { defaultValue: "未结合计" }), value: money(root.total_unpaid, currencyCode) },
|
||||
{ label: t("settlementReports.summary.overdueCount", { defaultValue: "逾期账单" }), value: String(root.overdue_count ?? 0) },
|
||||
{
|
||||
label: t("settlementReports.summary.platformRounding", { defaultValue: "平台尾差合计" }),
|
||||
value: money(root.platform_rounding_total, currencyCode),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<div key={item.label} className="rounded-md border border-border/60 px-3 py-2">
|
||||
<div className="text-xs text-muted-foreground">{item.label}</div>
|
||||
<div className="mt-1 text-sm font-semibold tabular-nums">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (reportType === "rebate" && root) {
|
||||
const byType = asRows(root.by_type);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{[
|
||||
["accrued_total", t("settlementReports.rebate.accrued", { defaultValue: "应计" })],
|
||||
["in_bill_total", t("settlementReports.rebate.inBill", { defaultValue: "已入账单" })],
|
||||
["settled_total", t("settlementReports.rebate.settled", { defaultValue: "已结算" })],
|
||||
["allocated_total", t("settlementReports.rebate.allocated", { defaultValue: "已分摊" })],
|
||||
].map(([key, label]) => (
|
||||
<div key={key} className="rounded-md border border-border/60 px-3 py-2">
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="mt-1 text-sm font-semibold tabular-nums">{money(root[key], currencyCode)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{byType.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("settlementReports.columns.rebateType", { defaultValue: "类型" })}</TableHead>
|
||||
<TableHead>{t("settlementReports.columns.status", { defaultValue: "状态" })}</TableHead>
|
||||
<TableHead className="text-right">{t("settlementReports.columns.amount", { defaultValue: "金额" })}</TableHead>
|
||||
<TableHead className="text-right">{t("settlementReports.columns.count", { defaultValue: "笔数" })}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{byType.map((row, idx) => (
|
||||
<TableRow key={`${row.rebate_type}-${row.status}-${idx}`}>
|
||||
<TableCell>{String(row.rebate_type ?? "")}</TableCell>
|
||||
<TableCell>{String(row.status ?? "")}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{money(row.total, currencyCode)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{String(row.count ?? 0)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (reportType === "credit" && root) {
|
||||
const agents = asRows(root.agents);
|
||||
const players = asRows(root.players);
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">{t("settlementReports.credit.agents", { defaultValue: "代理授信" })}</p>
|
||||
<ReportTable
|
||||
rows={agents}
|
||||
columns={[
|
||||
{ key: "code", header: t("settlementReports.columns.code", { defaultValue: "编码" }) },
|
||||
{ key: "name", header: t("settlementReports.columns.name", { defaultValue: "名称" }) },
|
||||
{ key: "credit_limit", header: t("settlementReports.columns.creditLimit", { defaultValue: "授信" }), creditMajor: true },
|
||||
{ key: "allocated_credit", header: t("settlementReports.columns.allocated", { defaultValue: "已下发" }), creditMajor: true },
|
||||
{ key: "available_credit", header: t("settlementReports.columns.available", { defaultValue: "可用" }), creditMajor: true },
|
||||
]}
|
||||
currencyCode={currencyCode}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">{t("settlementReports.credit.players", { defaultValue: "玩家授信" })}</p>
|
||||
<ReportTable
|
||||
rows={players}
|
||||
columns={[
|
||||
{ key: "username", header: t("settlementReports.columns.player", { defaultValue: "玩家" }) },
|
||||
{ key: "credit_limit", header: t("settlementReports.columns.creditLimit", { defaultValue: "授信" }), creditMajor: true },
|
||||
{ key: "used_credit", header: t("settlementReports.columns.used", { defaultValue: "已用" }), creditMajor: true },
|
||||
{ key: "frozen_credit", header: t("settlementReports.columns.frozen", { defaultValue: "冻结" }), creditMajor: true },
|
||||
{ key: "available_credit", header: t("settlementReports.columns.available", { defaultValue: "可用" }), creditMajor: true },
|
||||
]}
|
||||
currencyCode={currencyCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (reportType === "platform_pnl" && root) {
|
||||
if (root.error) {
|
||||
return (
|
||||
<p className="text-sm text-amber-800">
|
||||
{t("settlementReports.platformPnl.periodRequired", {
|
||||
defaultValue: "请选择具体账期后查看平台盈亏(需 settlement_period_id)。",
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const stats = [
|
||||
{ label: t("settlementReports.platformPnl.billNet", { defaultValue: "平台账单净额" }), value: money(root.platform_bill_net, currencyCode) },
|
||||
{
|
||||
label: t("settlementReports.platformPnl.rounding", { defaultValue: "尾差调整" }),
|
||||
value: money(root.platform_rounding_adjustment, currencyCode),
|
||||
},
|
||||
{
|
||||
label: t("settlementReports.platformPnl.shareProfit", { defaultValue: "占成利润(元数据)" }),
|
||||
value: money(root.share_profit_meta, currencyCode),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<div key={item.label} className="rounded-md border border-border/60 px-3 py-2">
|
||||
<div className="text-xs text-muted-foreground">{item.label}</div>
|
||||
<div className="mt-1 text-sm font-semibold tabular-nums">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const items = asRows(root?.items ?? (reportType === "player_win_loss" || reportType === "agent_share" || reportType === "unpaid_bills" || reportType === "overdue" || reportType === "draw_period" ? data : null));
|
||||
|
||||
const columnSets: Record<string, { key: string; header: string; money?: boolean }[]> = {
|
||||
player_win_loss: [
|
||||
{ key: "username", header: t("settlementReports.columns.player", { defaultValue: "玩家" }) },
|
||||
{ key: "game_type", header: t("settlementReports.columns.gameType", { defaultValue: "玩法" }) },
|
||||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true },
|
||||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||||
],
|
||||
agent_share: [
|
||||
{ key: "agent_node_id", header: t("settlementReports.columns.agentId", { defaultValue: "代理 ID" }) },
|
||||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true },
|
||||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||||
{ key: "entry_count", header: t("settlementReports.columns.count", { defaultValue: "笔数" }) },
|
||||
],
|
||||
unpaid_bills: [
|
||||
{ key: "bill_id", header: t("settlementReports.columns.billId", { defaultValue: "账单" }) },
|
||||
{ key: "bill_type", header: t("settlementReports.columns.billType", { defaultValue: "类型" }) },
|
||||
{ key: "unpaid_amount", header: t("settlementReports.columns.unpaid", { defaultValue: "未结" }), money: true },
|
||||
{ key: "status", header: t("settlementReports.columns.status", { defaultValue: "状态" }) },
|
||||
],
|
||||
overdue: [
|
||||
{ key: "bill_id", header: t("settlementReports.columns.billId", { defaultValue: "账单" }) },
|
||||
{ key: "overdue_days", header: t("settlementReports.columns.overdueDays", { defaultValue: "逾期天数" }) },
|
||||
{ key: "unpaid_amount", header: t("settlementReports.columns.unpaid", { defaultValue: "未结" }), money: true },
|
||||
],
|
||||
draw_period: [
|
||||
{ key: "draw_no", header: t("settlementReports.columns.drawNo", { defaultValue: "期号" }) },
|
||||
{ key: "game_win_loss", header: t("settlementReports.columns.grossWinLoss", { defaultValue: "输赢" }), money: true },
|
||||
{ key: "basic_rebate", header: t("settlementReports.columns.rebate", { defaultValue: "回水" }), money: true },
|
||||
{ key: "ticket_count", header: t("settlementReports.columns.count", { defaultValue: "笔数" }) },
|
||||
],
|
||||
};
|
||||
|
||||
const columns = columnSets[reportType];
|
||||
if (!columns) {
|
||||
return (
|
||||
<AdminNoResourceState className="text-sm text-muted-foreground" />
|
||||
);
|
||||
}
|
||||
|
||||
return <ReportTable rows={items} columns={columns} currencyCode={currencyCode} />;
|
||||
}
|
||||
|
||||
function ReportTable({
|
||||
rows,
|
||||
columns,
|
||||
currencyCode,
|
||||
}: {
|
||||
rows: Record<string, unknown>[];
|
||||
columns: { key: string; header: string; money?: boolean; creditMajor?: boolean }[];
|
||||
currencyCode: string;
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
if (rows.length === 0) {
|
||||
return <AdminNoResourceState className="text-sm text-muted-foreground" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-table-shell max-h-96 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((col) => (
|
||||
<TableHead
|
||||
key={col.key}
|
||||
className={col.money || col.creditMajor ? "text-right" : undefined}
|
||||
>
|
||||
{col.header}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row, idx) => (
|
||||
<TableRow key={idx}>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
key={col.key}
|
||||
className={col.money || col.creditMajor ? "text-right tabular-nums" : undefined}
|
||||
>
|
||||
{col.creditMajor
|
||||
? creditMoney(row[col.key], currencyCode)
|
||||
: col.money
|
||||
? money(row[col.key], currencyCode)
|
||||
: String(row[col.key] ?? "—")}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user