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:
313
src/modules/settlement/agent-bill-detail.tsx
Normal file
313
src/modules/settlement/agent-bill-detail.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
getSettlementBill,
|
||||
postSettlementBillAdjustment,
|
||||
postSettlementBillBadDebtWriteOff,
|
||||
postSettlementBillConfirm,
|
||||
postSettlementBillPayment,
|
||||
type RebateAllocationRow,
|
||||
type SettlementBillRow,
|
||||
type SettlementPaymentRow,
|
||||
} from "@/api/admin-agent-settlement";
|
||||
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
|
||||
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
function parseBillMeta(metaJson: SettlementBillRow["meta_json"]): {
|
||||
share_profit?: number;
|
||||
platform_share_profit?: number;
|
||||
} {
|
||||
if (metaJson == null || metaJson === "") {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed =
|
||||
typeof metaJson === "string" ? (JSON.parse(metaJson) as Record<string, unknown>) : metaJson;
|
||||
return {
|
||||
share_profit: parsed.share_profit != null ? Number(parsed.share_profit) : undefined,
|
||||
platform_share_profit:
|
||||
parsed.platform_share_profit != null ? Number(parsed.platform_share_profit) : undefined,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
type AgentBillDetailProps = {
|
||||
billId: number;
|
||||
currencyCode: string;
|
||||
canManage?: boolean;
|
||||
onUpdated?: () => void;
|
||||
};
|
||||
|
||||
export function AgentBillDetail({
|
||||
billId,
|
||||
currencyCode,
|
||||
canManage = true,
|
||||
onUpdated,
|
||||
}: AgentBillDetailProps): React.ReactElement {
|
||||
const { t } = useTranslation(["agents", "common"]);
|
||||
const [bill, setBill] = useState<SettlementBillRow | null>(null);
|
||||
const [payments, setPayments] = useState<SettlementPaymentRow[]>([]);
|
||||
const [rebateAllocations, setRebateAllocations] = useState<RebateAllocationRow[]>([]);
|
||||
const [tierEdge, setTierEdge] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [payAmount, setPayAmount] = useState("");
|
||||
const [payMethod, setPayMethod] = useState("");
|
||||
const [payProof, setPayProof] = useState("");
|
||||
const [adjustAmount, setAdjustAmount] = useState("");
|
||||
const [badDebtReason, setBadDebtReason] = useState("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getSettlementBill(billId);
|
||||
setBill(data.bill);
|
||||
setPayments(data.payments ?? []);
|
||||
setRebateAllocations(data.rebate_allocations ?? []);
|
||||
setTierEdge(data.tier_edge ?? null);
|
||||
setPayAmount(String(data.bill.unpaid_amount ?? 0));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [billId]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
if (loading || !bill) {
|
||||
return <AdminLoadingState />;
|
||||
}
|
||||
|
||||
const owner =
|
||||
bill.owner_label ??
|
||||
`${bill.owner_type}#${bill.owner_id}`;
|
||||
const counterparty =
|
||||
bill.counterparty_label === "platform"
|
||||
? t("settlementBills.platform", { defaultValue: "平台" })
|
||||
: bill.counterparty_label ?? `${bill.counterparty_type}#${bill.counterparty_id}`;
|
||||
|
||||
const locked = ["confirmed", "partial_paid", "settled", "overdue"].includes(bill.status);
|
||||
const ownerOwes = bill.net_amount > 0;
|
||||
const paymentTitle = ownerOwes
|
||||
? t("settlementBills.recordReceipt", { defaultValue: "登记收款" })
|
||||
: t("settlementBills.recordPayout", { defaultValue: "登记付款" });
|
||||
const paymentSubmit = ownerOwes
|
||||
? t("settlementBills.submitReceipt", { defaultValue: "确认收款" })
|
||||
: t("settlementBills.submitPayout", { defaultValue: "确认付款" });
|
||||
const canWriteOff =
|
||||
canManage &&
|
||||
bill.unpaid_amount > 0 &&
|
||||
["confirmed", "partial_paid", "overdue"].includes(bill.status) &&
|
||||
!["adjustment", "reversal", "bad_debt"].includes(bill.bill_type);
|
||||
const meta = parseBillMeta(bill.meta_json);
|
||||
const hasSubtreeFields =
|
||||
bill.gross_win_loss != null ||
|
||||
bill.rebate_amount != null ||
|
||||
bill.platform_rounding_adjustment != null ||
|
||||
meta.share_profit != null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t("settlementBills.columns.party", { defaultValue: "本方" })}: </span>
|
||||
{owner}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
{t("settlementBills.columns.counterparty", { defaultValue: "对方" })}:{" "}
|
||||
</span>
|
||||
{counterparty}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t("settlementBills.columns.type", { defaultValue: "类型" })}: </span>
|
||||
{bill.bill_type} / {bill.status}
|
||||
{tierEdge ? ` · ${tierEdge}` : ""}
|
||||
</div>
|
||||
{hasSubtreeFields ? (
|
||||
<div className="space-y-1 rounded-md border border-border/60 p-3">
|
||||
<p className="font-medium">
|
||||
{t("settlementBills.subtreeSummary", { defaultValue: "子树汇总" })}
|
||||
</p>
|
||||
{bill.gross_win_loss != null ? (
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
{t("settlementBills.grossWinLoss", { defaultValue: "输赢 (gross_win_loss)" })}:{" "}
|
||||
</span>
|
||||
{formatDashboardMoneyMinor(bill.gross_win_loss, currencyCode)}
|
||||
</div>
|
||||
) : null}
|
||||
{bill.rebate_amount != null ? (
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
{t("settlementBills.rebateAmount", { defaultValue: "回水" })}:{" "}
|
||||
</span>
|
||||
{formatDashboardMoneyMinor(bill.rebate_amount, currencyCode)}
|
||||
</div>
|
||||
) : null}
|
||||
{meta.share_profit != null ? (
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
{t("settlementBills.shareProfit", { defaultValue: "占成利润" })}:{" "}
|
||||
</span>
|
||||
{formatDashboardMoneyMinor(meta.share_profit, currencyCode)}
|
||||
</div>
|
||||
) : null}
|
||||
{bill.platform_rounding_adjustment != null && bill.platform_rounding_adjustment !== 0 ? (
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
{t("settlementBills.platformRounding", { defaultValue: "平台尾差" })}:{" "}
|
||||
</span>
|
||||
{formatDashboardMoneyMinor(bill.platform_rounding_adjustment, currencyCode)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t("settlementBills.columns.net", { defaultValue: "净额" })}: </span>
|
||||
{formatDashboardMoneyMinor(bill.net_amount, currencyCode)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t("settlementBills.columns.unpaid", { defaultValue: "未结" })}: </span>
|
||||
{formatDashboardMoneyMinor(bill.unpaid_amount, currencyCode)}
|
||||
</div>
|
||||
|
||||
{rebateAllocations.length > 0 ? (
|
||||
<div className="space-y-1 rounded-md border border-border/60 p-3">
|
||||
<p className="font-medium">{t("settlementBills.rebateAllocations", { defaultValue: "回水分摊" })}</p>
|
||||
<ul className="space-y-1 text-muted-foreground">
|
||||
{rebateAllocations.map((row) => (
|
||||
<li key={row.id}>
|
||||
{row.participant_type}#{row.participant_id} · {row.allocation_rule} ·{" "}
|
||||
{formatDashboardMoneyMinor(row.allocated_amount, currencyCode)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{payments.length > 0 ? (
|
||||
<div className="space-y-1 rounded-md border border-border/60 p-3">
|
||||
<p className="font-medium">{t("settlementBills.paymentsHistory", { defaultValue: "收付记录" })}</p>
|
||||
<ul className="space-y-1 text-muted-foreground">
|
||||
{payments.map((p) => (
|
||||
<li key={p.id}>
|
||||
{formatDashboardMoneyMinor(p.amount, currencyCode)}
|
||||
{p.method ? ` · ${p.method}` : ""}
|
||||
{p.remark ? ` · ${p.remark}` : ""}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{canManage && bill.status === "pending_confirm" ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void postSettlementBillConfirm(billId)
|
||||
.then(load)
|
||||
.then(onUpdated)
|
||||
.then(() => toast.success(t("settlementBills.confirmed", { defaultValue: "已确认" })))
|
||||
}
|
||||
>
|
||||
{t("settlementBills.confirm", { defaultValue: "确认账单" })}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{canManage && ["confirmed", "partial_paid", "overdue"].includes(bill.status) && bill.unpaid_amount > 0 ? (
|
||||
<div className="space-y-2 rounded-md border border-border/60 p-3">
|
||||
<p className="font-medium">{paymentTitle}</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<Label>{t("settlementBills.paymentAmount", { defaultValue: "收付金额" })}</Label>
|
||||
<Input value={payAmount} onChange={(e) => setPayAmount(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("settlementBills.paymentMethod", { defaultValue: "方式" })}</Label>
|
||||
<Input value={payMethod} onChange={(e) => setPayMethod(e.target.value)} placeholder="cash" />
|
||||
</div>
|
||||
<div className="space-y-1 sm:col-span-2">
|
||||
<Label>{t("settlementBills.paymentProof", { defaultValue: "凭证/备注" })}</Label>
|
||||
<Input value={payProof} onChange={(e) => setPayProof(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void postSettlementBillPayment(billId, {
|
||||
amount: Number(payAmount),
|
||||
method: payMethod.trim() || undefined,
|
||||
proof: payProof.trim() || undefined,
|
||||
})
|
||||
.then(load)
|
||||
.then(onUpdated)
|
||||
.then(() => toast.success(t("settlementBills.paid", { defaultValue: "已登记收付" })))
|
||||
}
|
||||
>
|
||||
{paymentSubmit}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{canWriteOff ? (
|
||||
<div className="space-y-2 rounded-md border border-border/60 p-3">
|
||||
<p className="font-medium">{t("settlementBills.badDebtWriteOff", { defaultValue: "坏账核销" })}</p>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("settlementBills.badDebtReason", { defaultValue: "核销原因" })}</Label>
|
||||
<Input value={badDebtReason} onChange={(e) => setBadDebtReason(e.target.value)} />
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
void postSettlementBillBadDebtWriteOff(billId, {
|
||||
reason: badDebtReason.trim() || undefined,
|
||||
})
|
||||
.then(load)
|
||||
.then(onUpdated)
|
||||
.then(() =>
|
||||
toast.success(t("settlementBills.badDebtDone", { defaultValue: "已核销坏账" })),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("settlementBills.confirmBadDebt", { defaultValue: "确认核销" })}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{canManage && locked ? (
|
||||
<div className="space-y-2 rounded-md border border-dashed border-border/60 p-3">
|
||||
<p className="font-medium">{t("settlementBills.adjustment", { defaultValue: "补差/冲正单" })}</p>
|
||||
<div className="space-y-1">
|
||||
<Label>{t("settlementBills.adjustmentAmount", { defaultValue: "调整金额(可负)" })}</Label>
|
||||
<Input value={adjustAmount} onChange={(e) => setAdjustAmount(e.target.value)} type="number" />
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
void postSettlementBillAdjustment(billId, {
|
||||
amount: Number(adjustAmount),
|
||||
reason: "manual_adjustment",
|
||||
})
|
||||
.then(() => toast.success(t("settlementBills.adjustmentCreated", { defaultValue: "已创建补差单" })))
|
||||
.then(onUpdated)
|
||||
}
|
||||
>
|
||||
{t("settlementBills.createAdjustment", { defaultValue: "创建补差单" })}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user