feat(settlement): 优化结算中心待处理视图与账单显示
Some checks failed
lotteryadmin CI / build (push) Has been cancelled

- 新增接口参数 agent_scope 支持筛选直接代理
- 结算中心导航新增“待处理”视图,并设置默认视图为“待处理”
- 结算中心页面新增待处理账单工作台,支持待确认、待收付和异常账单分类显示
- 调整账单状态文本,明确“已确认”改为“待收付”,“部分结清”改为“部分收付”
- 优化账单明细中的代理结算说明和收益展示文案
- 账单详情页添加当前账单主要操作提示区,提示确认、登记付款或复核状态
- 结算中心流水模块及多语言资源关于“账务流水”统一改为“流水审计”
- 优化结算主面板中待处理模式与全部模式的筛选和分页逻辑
- 结合管理员权限控制结算中心可见站点列表,增强集成站点加载逻辑
This commit is contained in:
2026-07-01 15:35:25 +08:00
parent 4484a7a77a
commit 2f9e06fb78
14 changed files with 374 additions and 171 deletions

View File

@@ -18,6 +18,7 @@ export async function getAdminPlayers(params?: {
status?: number; status?: number;
site_code?: string; site_code?: string;
agent_node_id?: number; agent_node_id?: number;
agent_scope?: "subtree" | "direct";
}): Promise<AdminPlayerListData> { }): Promise<AdminPlayerListData> {
return adminRequest.get<AdminPlayerListData>(`${A}/players`, { params }); return adminRequest.get<AdminPlayerListData>(`${A}/players`, { params });
} }

View File

@@ -65,10 +65,13 @@
}, },
"nav": { "nav": {
"periods": "Periods", "periods": "Periods",
"pendingWorkbench": "To do",
"allBills": "All bills",
"bills": "Bills", "bills": "Bills",
"operations": "Payments & adjustments", "operations": "Payments & adjustments",
"aria": "Period views", "aria": "Period views",
"ledger": "Account ledger", "ledger": "Account ledger",
"ledgerAudit": "Ledger audit",
"creditLedger": "Credit ledger", "creditLedger": "Credit ledger",
"playerBills": "Player bills", "playerBills": "Player bills",
"agentBills": "Agent bills", "agentBills": "Agent bills",
@@ -194,9 +197,9 @@
}, },
"billStatus": { "billStatus": {
"pending_confirm": "Pending confirm", "pending_confirm": "Pending confirm",
"confirmed": "Confirmed", "confirmed": "Awaiting payment",
"partial_paid": "Partial paid", "partial_paid": "Partial payment",
"settled": "Settled", "settled": "Fully settled",
"overdue": "Overdue", "overdue": "Overdue",
"reversed": "Reversed" "reversed": "Reversed"
}, },
@@ -229,26 +232,34 @@
"payerLabel": "Payer", "payerLabel": "Payer",
"payeeLabel": "Payee", "payeeLabel": "Payee",
"playerBreakdownIntro": "Players settle only with their direct agent: net = win/loss rebate.", "playerBreakdownIntro": "Players settle only with their direct agent: net = win/loss rebate.",
"agentBreakdownIntro": "Agents settle only with their upline: net = team net downline share own share.", "agentBreakdownIntro": "Start with this period's team win/loss, deduct player rebates and retained shares, then settle the remaining amount with the upline.",
"playerGross": "Game win/loss", "playerGross": "Game win/loss",
"playerLostHint": "Player lost; owes agent", "playerLostHint": "Player lost; owes agent",
"playerWonHint": "Player won; agent owes player", "playerWonHint": "Player won; agent owes player",
"playerNet": "Player net payable", "playerNet": "Player net payable",
"playerNetReceive": "Agent pays player", "playerNetReceive": "Agent pays player",
"teamGross": "Team game win/loss", "teamGross": "Team win/loss this period",
"teamGrossHint": "Includes this agent and all downline players", "teamGrossHint": "Total for this agent and downline players in this period",
"teamGrossShort": "Team", "teamGrossShort": "Team",
"playerGrossShort": "Player", "playerGrossShort": "Player",
"teamRebate": "Team rebate", "teamRebate": "Deduct player rebates",
"teamNet": "Team net", "teamNet": "Profit available to split",
"rebate": "Rebate", "rebate": "Rebate",
"agentShareKeep": "Share kept at this tier", "agentShareKeep": "This tier keeps",
"agentShareKeepHint": "Profit retained at this tier by share ratio", "agentShareKeepHint": "Amount retained by this tier under the agreed share",
"agentDownlineShare": "Downline share", "agentDownlineShare": "Downline agents keep",
"agentDownlineShareHint": "Profit retained by downline agents (see breakdown below)", "agentDownlineShareHint": "Amount retained by downline agents first (see breakdown below)",
"agentDownlineShareItem": "{{agent}} kept", "agentDownlineShareItem": "{{agent}} kept",
"agentNet": "Pay {{counterparty}}", "agentNet": "Remit to {{counterparty}}",
"agentNetReceive": "{{counterparty}} pays this tier", "agentNetReceive": "{{counterparty}} owes this tier",
"actionPendingConfirmTitle": "Confirm this bill next",
"actionPendingConfirmDesc": "Confirm the amount and parties, then the bill moves to awaiting payment.",
"actionAwaitingPaymentTitle": "Record payment next",
"actionAwaitingPaymentDesc": "Record offline payment according to the payer/payee direction. Partial payments are allowed.",
"actionSettledTitle": "This bill is fully settled",
"actionSettledDesc": "Review the breakdown and payment history below if needed.",
"actionReviewTitle": "Review this bill status",
"actionReviewDesc": "Use adjustment, reversal, or bad debt actions if the amount or direction is wrong.",
"billOwner": "Bill owner", "billOwner": "Bill owner",
"billCounterparty": "Counterparty", "billCounterparty": "Counterparty",
"unpaidPendingConfirm": "Confirm the bill before recording payment", "unpaidPendingConfirm": "Confirm the bill before recording payment",
@@ -340,6 +351,15 @@
"clientFilterHint": "Owner filter: showing {{shown}} / {{total}}", "clientFilterHint": "Owner filter: showing {{shown}} / {{total}}",
"emptyFiltered": "No bills match the filter. Try All statuses or reset.", "emptyFiltered": "No bills match the filter. Try All statuses or reset.",
"emptyClosed": "Period closed but no bills. Often no settled credit tickets in range.", "emptyClosed": "Period closed but no bills. Often no settled credit tickets in range.",
"workbenchEmpty": "No bills need action in this period. Use All bills for settled or historical bills.",
"workbench": {
"pendingTitle": "Bills pending confirm",
"pendingHint": "Confirm the amount and parties first; confirmed bills move to awaiting payment.",
"awaitingTitle": "Bills awaiting payment",
"awaitingHint": "Record offline payment by payer/payee direction. Bills close when fully paid.",
"exceptionTitle": "Exceptions",
"exceptionHint": "Review adjustment, reversal, and bad debt records here."
},
"rowHint": { "rowHint": {
"playerOwner": "Player owner", "playerOwner": "Player owner",
"agentOwner": "Agent owner", "agentOwner": "Agent owner",

View File

@@ -64,9 +64,12 @@
}, },
"nav": { "nav": {
"periods": "अवधिहरू", "periods": "अवधिहरू",
"pendingWorkbench": "To do",
"allBills": "All bills",
"bills": "बिलहरू", "bills": "बिलहरू",
"operations": "भुक्तानी र समायोजन", "operations": "भुक्तानी र समायोजन",
"ledger": "लेजर", "ledger": "लेजर",
"ledgerAudit": "Ledger audit",
"creditLedger": "क्रेडिट लेजर", "creditLedger": "क्रेडिट लेजर",
"playerBills": "Player bills", "playerBills": "Player bills",
"agentBills": "Agent bills", "agentBills": "Agent bills",
@@ -192,9 +195,9 @@
}, },
"billStatus": { "billStatus": {
"pending_confirm": "Pending confirm", "pending_confirm": "Pending confirm",
"confirmed": "Confirmed", "confirmed": "Awaiting payment",
"partial_paid": "Partial paid", "partial_paid": "Partial payment",
"settled": "Settled", "settled": "Fully settled",
"overdue": "Overdue", "overdue": "Overdue",
"reversed": "Reversed" "reversed": "Reversed"
}, },
@@ -226,26 +229,34 @@
"payerLabel": "Payer", "payerLabel": "Payer",
"payeeLabel": "Payee", "payeeLabel": "Payee",
"playerBreakdownIntro": "Players settle only with their direct agent: net = win/loss rebate.", "playerBreakdownIntro": "Players settle only with their direct agent: net = win/loss rebate.",
"agentBreakdownIntro": "Agents settle only with their upline: net = team net downline share own share.", "agentBreakdownIntro": "Start with this period's team win/loss, deduct player rebates and retained shares, then settle the remaining amount with the upline.",
"playerGross": "Game win/loss", "playerGross": "Game win/loss",
"playerLostHint": "Player lost; owes agent", "playerLostHint": "Player lost; owes agent",
"playerWonHint": "Player won; agent owes player", "playerWonHint": "Player won; agent owes player",
"playerNet": "Player net payable", "playerNet": "Player net payable",
"playerNetReceive": "Agent pays player", "playerNetReceive": "Agent pays player",
"teamGross": "Team game win/loss", "teamGross": "Team win/loss this period",
"teamGrossHint": "Includes this agent and all downline players", "teamGrossHint": "Total for this agent and downline players in this period",
"teamGrossShort": "Team", "teamGrossShort": "Team",
"playerGrossShort": "Player", "playerGrossShort": "Player",
"teamRebate": "Team rebate", "teamRebate": "Deduct player rebates",
"teamNet": "Team net", "teamNet": "Profit available to split",
"rebate": "Rebate", "rebate": "Rebate",
"agentShareKeep": "Share kept at this tier", "agentShareKeep": "This tier keeps",
"agentShareKeepHint": "Profit retained at this tier by share ratio", "agentShareKeepHint": "Amount retained by this tier under the agreed share",
"agentDownlineShare": "Downline share", "agentDownlineShare": "Downline agents keep",
"agentDownlineShareHint": "Profit retained by downline agents (see breakdown below)", "agentDownlineShareHint": "Amount retained by downline agents first (see breakdown below)",
"agentDownlineShareItem": "{{agent}} kept", "agentDownlineShareItem": "{{agent}} kept",
"agentNet": "Pay {{counterparty}}", "agentNet": "Remit to {{counterparty}}",
"agentNetReceive": "{{counterparty}} pays this tier", "agentNetReceive": "{{counterparty}} owes this tier",
"actionPendingConfirmTitle": "Confirm this bill next",
"actionPendingConfirmDesc": "Confirm the amount and parties, then the bill moves to awaiting payment.",
"actionAwaitingPaymentTitle": "Record payment next",
"actionAwaitingPaymentDesc": "Record offline payment according to the payer/payee direction. Partial payments are allowed.",
"actionSettledTitle": "This bill is fully settled",
"actionSettledDesc": "Review the breakdown and payment history below if needed.",
"actionReviewTitle": "Review this bill status",
"actionReviewDesc": "Use adjustment, reversal, or bad debt actions if the amount or direction is wrong.",
"billOwner": "Bill owner", "billOwner": "Bill owner",
"billCounterparty": "Counterparty", "billCounterparty": "Counterparty",
"unpaidPendingConfirm": "Confirm the bill before recording payment", "unpaidPendingConfirm": "Confirm the bill before recording payment",
@@ -330,6 +341,15 @@
"clientFilterHint": "Owner filter: showing {{shown}} / {{total}}", "clientFilterHint": "Owner filter: showing {{shown}} / {{total}}",
"emptyFiltered": "No bills match the filter. Try All statuses or reset.", "emptyFiltered": "No bills match the filter. Try All statuses or reset.",
"emptyClosed": "Period closed but no bills. Often no settled credit tickets in range.", "emptyClosed": "Period closed but no bills. Often no settled credit tickets in range.",
"workbenchEmpty": "No bills need action in this period. Use All bills for settled or historical bills.",
"workbench": {
"pendingTitle": "Bills pending confirm",
"pendingHint": "Confirm the amount and parties first; confirmed bills move to awaiting payment.",
"awaitingTitle": "Bills awaiting payment",
"awaitingHint": "Record offline payment by payer/payee direction. Bills close when fully paid.",
"exceptionTitle": "Exceptions",
"exceptionHint": "Review adjustment, reversal, and bad debt records here."
},
"rowHint": { "rowHint": {
"playerOwner": "Player owner", "playerOwner": "Player owner",
"agentOwner": "Agent owner", "agentOwner": "Agent owner",

View File

@@ -50,10 +50,13 @@
}, },
"nav": { "nav": {
"periods": "账期", "periods": "账期",
"pendingWorkbench": "待处理",
"allBills": "全部账单",
"bills": "账单", "bills": "账单",
"operations": "收付与调账", "operations": "收付与调账",
"aria": "账期视图", "aria": "账期视图",
"ledger": "账务流水", "ledger": "流水审计",
"ledgerAudit": "流水审计",
"creditLedger": "信用流水", "creditLedger": "信用流水",
"playerBills": "玩家账单", "playerBills": "玩家账单",
"agentBills": "代理账单", "agentBills": "代理账单",
@@ -65,7 +68,7 @@
"reports": "账期报表" "reports": "账期报表"
}, },
"operations": { "operations": {
"hint": "登记收付、坏账核销与补差/冲正的操作台账。玩家信用变动请查看「账务流水」。", "hint": "登记收付、坏账核销与补差/冲正的操作台账。玩家信用变动请查看「流水审计」。",
"adjustmentsTitle": "调账 / 冲正 / 坏账", "adjustmentsTitle": "调账 / 冲正 / 坏账",
"loadFailed": "收付与调账记录加载失败", "loadFailed": "收付与调账记录加载失败",
"operationType": "操作类型", "operationType": "操作类型",
@@ -179,8 +182,8 @@
}, },
"billStatus": { "billStatus": {
"pending_confirm": "待确认", "pending_confirm": "待确认",
"confirmed": "已确认", "confirmed": "待收付",
"partial_paid": "部分结清", "partial_paid": "部分收付",
"settled": "已结清", "settled": "已结清",
"overdue": "逾期", "overdue": "逾期",
"reversed": "已冲正" "reversed": "已冲正"
@@ -214,26 +217,34 @@
"payerLabel": "付款方", "payerLabel": "付款方",
"payeeLabel": "收款方", "payeeLabel": "收款方",
"playerBreakdownIntro": "玩家只与直属代理结算,净额 = 输赢 回水。", "playerBreakdownIntro": "玩家只与直属代理结算,净额 = 输赢 回水。",
"agentBreakdownIntro": "代理只与直属上级结算,净额 = 团队净额 下级占成 本级占成。", "agentBreakdownIntro": "先看本期团队赢亏,扣除玩家回水和各级保留后,就是本级需要上缴或应收的金额。",
"playerGross": "游戏输赢", "playerGross": "游戏输赢",
"playerLostHint": "玩家输了,应付代理", "playerLostHint": "玩家输了,应付代理",
"playerWonHint": "玩家赢了,代理应付玩家", "playerWonHint": "玩家赢了,代理应付玩家",
"playerNet": "玩家应付净额", "playerNet": "玩家应付净额",
"playerNetReceive": "代理应付玩家", "playerNetReceive": "代理应付玩家",
"teamGross": "团队游戏输赢", "teamGross": "本期团队赢亏",
"teamGrossHint": "本级及下级玩家合计", "teamGrossHint": "本级及下级玩家本期合计",
"teamGrossShort": "团队", "teamGrossShort": "团队",
"playerGrossShort": "玩家", "playerGrossShort": "玩家",
"teamRebate": "团队回水", "teamRebate": "扣玩家回水",
"teamNet": "团队净额", "teamNet": "可分配利润",
"rebate": "回水", "rebate": "回水",
"agentShareKeep": "本级占成", "agentShareKeep": "本级保留",
"agentShareKeepHint": "本级按占成比例留下的利润", "agentShareKeepHint": "本级按约定比例保留的金额",
"agentDownlineShare": "下级占成", "agentDownlineShare": "下级代理保留",
"agentDownlineShareHint": "下级代理按占成比例保留的利润(明细见下行)", "agentDownlineShareHint": "下级代理按约定比例保留的金额(明细见下行)",
"agentDownlineShareItem": "{{agent}} 保留", "agentDownlineShareItem": "{{agent}} 保留",
"agentNet": "应 {{counterparty}}", "agentNet": "应上缴 {{counterparty}}",
"agentNetReceive": "{{counterparty}} 应本级", "agentNetReceive": "{{counterparty}} 应补给本级",
"actionPendingConfirmTitle": "当前应确认账单",
"actionPendingConfirmDesc": "确认金额和双方无误后,账单会进入待收付。",
"actionAwaitingPaymentTitle": "当前应登记收付",
"actionAwaitingPaymentDesc": "按谁付谁登记线下收付;未结清可分多次登记。",
"actionSettledTitle": "本账单已结清",
"actionSettledDesc": "如需复核,可查看下方结算明细和收付记录。",
"actionReviewTitle": "请复核账单状态",
"actionReviewDesc": "如金额或方向有差异,可通过调账、冲正或坏账处理。",
"billOwner": "账单主体", "billOwner": "账单主体",
"billCounterparty": "结算对手", "billCounterparty": "结算对手",
"unpaidPendingConfirm": "确认账单后可登记收付", "unpaidPendingConfirm": "确认账单后可登记收付",
@@ -327,6 +338,15 @@
"hierarchyHint": "同一账期会生成多笔账单:玩家先与直属代理结,代理扣除本级占成后再向上级缴纳。因此「输赢」可能相同,但「结算金额」会逐级减少。", "hierarchyHint": "同一账期会生成多笔账单:玩家先与直属代理结,代理扣除本级占成后再向上级缴纳。因此「输赢」可能相同,但「结算金额」会逐级减少。",
"emptyFiltered": "当前筛选下暂无账单,请改为「全部状态」或重置筛选。", "emptyFiltered": "当前筛选下暂无账单,请改为「全部状态」或重置筛选。",
"emptyClosed": "本期已关账但暂无账单。常见原因:账期内无信用盘玩家的已结算注单,或占成流水不在本账期时间范围内。", "emptyClosed": "本期已关账但暂无账单。常见原因:账期内无信用盘玩家的已结算注单,或占成流水不在本账期时间范围内。",
"workbenchEmpty": "本期暂无待处理账单。需要查历史或已结清账单,请进入「全部账单」。",
"workbench": {
"pendingTitle": "待确认账单",
"pendingHint": "先确认金额与双方,确认后进入待收付。",
"awaitingTitle": "待收付账单",
"awaitingHint": "按谁付谁登记线下收付,全部收付后账单结清。",
"exceptionTitle": "异常处理",
"exceptionHint": "调账、冲正与坏账记录集中在这里复核。"
},
"intro": "关账后生成的占成账单。可按类型或状态筛选,详情内确认或登记收付。", "intro": "关账后生成的占成账单。可按类型或状态筛选,详情内确认或登记收付。",
"confirmOneTitle": "确认账单 #{{id}}", "confirmOneTitle": "确认账单 #{{id}}",
"confirmOneDesc": "确认后进入待收付,可登记线下收付。", "confirmOneDesc": "确认后进入待收付,可登记线下收付。",
@@ -343,7 +363,7 @@
"title": "结算概览" "title": "结算概览"
}, },
"ledger": { "ledger": {
"title": "账务流水" "title": "流水审计"
}, },
"bills": { "bills": {
"title": "全部账单" "title": "全部账单"

View File

@@ -582,6 +582,7 @@ export function AgentsConsole(): React.ReactElement {
void getAdminPlayers({ void getAdminPlayers({
site_code: activeSiteCode.trim(), site_code: activeSiteCode.trim(),
agent_node_id: selectedNodeId, agent_node_id: selectedNodeId,
agent_scope: "direct",
page: 1, page: 1,
per_page: 1, per_page: 1,
}) })
@@ -617,6 +618,7 @@ export function AgentsConsole(): React.ReactElement {
void getAdminPlayers({ void getAdminPlayers({
site_code: activeSiteCode.trim(), site_code: activeSiteCode.trim(),
agent_node_id: child.id, agent_node_id: child.id,
agent_scope: "direct",
page: 1, page: 1,
per_page: 1, per_page: 1,
}) })

View File

@@ -254,6 +254,7 @@ export function AgentsPlayersPanel({
site_code: siteCode.trim(), site_code: siteCode.trim(),
...(appliedKeyword.trim() !== "" ? { keyword: appliedKeyword.trim() } : {}), ...(appliedKeyword.trim() !== "" ? { keyword: appliedKeyword.trim() } : {}),
...(effectiveAgentId !== null ? { agent_node_id: effectiveAgentId } : {}), ...(effectiveAgentId !== null ? { agent_node_id: effectiveAgentId } : {}),
...(effectiveAgentId !== null ? { agent_scope: "direct" } : {}),
}); });
setItems(data.items); setItems(data.items);
setTotal(data.meta.total); setTotal(data.meta.total);

View File

@@ -155,6 +155,50 @@ export function AgentBillDetail({
bill.unpaid_amount > 0 && bill.unpaid_amount > 0 &&
["confirmed", "partial_paid", "overdue"].includes(bill.status) && ["confirmed", "partial_paid", "overdue"].includes(bill.status) &&
!["adjustment", "reversal", "bad_debt"].includes(bill.bill_type); !["adjustment", "reversal", "bad_debt"].includes(bill.bill_type);
const primaryAction =
bill.status === "pending_confirm"
? {
title: t("settlementCenter:billDisplay.actionPendingConfirmTitle", {
defaultValue: "当前应确认账单",
}),
description: t("settlementCenter:billDisplay.actionPendingConfirmDesc", {
defaultValue: "确认金额和双方无误后,账单会进入待收付。",
}),
button: t("settlementBills.confirm", { defaultValue: "确认账单" }),
onClick: () => confirmRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" }),
}
: ["confirmed", "partial_paid", "overdue"].includes(bill.status) && bill.unpaid_amount > 0
? {
title: t("settlementCenter:billDisplay.actionAwaitingPaymentTitle", {
defaultValue: "当前应登记收付",
}),
description: t("settlementCenter:billDisplay.actionAwaitingPaymentDesc", {
defaultValue: "按谁付谁登记线下收付;未结清可分多次登记。",
}),
button: paymentTitle,
onClick: () => paymentRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" }),
}
: bill.status === "settled"
? {
title: t("settlementCenter:billDisplay.actionSettledTitle", {
defaultValue: "本账单已结清",
}),
description: t("settlementCenter:billDisplay.actionSettledDesc", {
defaultValue: "如需复核,可查看下方结算明细和收付记录。",
}),
button: null,
onClick: null,
}
: {
title: t("settlementCenter:billDisplay.actionReviewTitle", {
defaultValue: "请复核账单状态",
}),
description: t("settlementCenter:billDisplay.actionReviewDesc", {
defaultValue: "如金额或方向有差异,可通过调账、冲正或坏账处理。",
}),
button: null,
onClick: null,
};
const rebateAllocationSummary = Object.values( const rebateAllocationSummary = Object.values(
rebateAllocations.reduce<Record<string, { key: string; label: string; amount: number; rows: number }>>( rebateAllocations.reduce<Record<string, { key: string; label: string; amount: number; rows: number }>>(
(acc, row) => { (acc, row) => {
@@ -337,6 +381,22 @@ export function AgentBillDetail({
<> <>
<ConfirmDialog /> <ConfirmDialog />
<div className="flex flex-col gap-5 text-sm"> <div className="flex flex-col gap-5 text-sm">
<div className="flex flex-col gap-3 rounded-xl border border-primary/20 bg-primary/5 p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<p className="font-semibold tracking-tight text-primary">{primaryAction.title}</p>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">{primaryAction.description}</p>
</div>
{primaryAction.button && primaryAction.onClick ? (
<Button
type="button"
size="sm"
className="w-full shrink-0 sm:w-auto"
onClick={primaryAction.onClick}
>
{primaryAction.button}
</Button>
) : null}
</div>
<SettlementBillSummaryHeader bill={bill} currencyCode={currencyCode} /> <SettlementBillSummaryHeader bill={bill} currencyCode={currencyCode} />
<SettlementBillAmountBreakdown <SettlementBillAmountBreakdown
bill={bill} bill={bill}

View File

@@ -216,7 +216,7 @@ export function SettlementBillAmountBreakdown({
}) })
: bill.bill_type === "agent" : bill.bill_type === "agent"
? t("settlementCenter:billDisplay.agentBreakdownIntro", { ? t("settlementCenter:billDisplay.agentBreakdownIntro", {
defaultValue: "代理只与直属上级结算,净额 = 团队净额 下级占成 本级占成。", defaultValue: "先看本期团队赢亏,扣除玩家回水和各级保留后,就是本级需要上缴或应收的金额。",
}) })
: null; : null;

View File

@@ -271,19 +271,19 @@ export function buildBillAmountBreakdown(
if (bill.gross_win_loss != null) { if (bill.gross_win_loss != null) {
lines.push({ lines.push({
key: "gross", key: "gross",
label: t("settlementCenter:billDisplay.teamGross", { defaultValue: "团队游戏输赢" }), label: t("settlementCenter:billDisplay.teamGross", { defaultValue: "本期团队赢亏" }),
amount: Math.abs(gross), amount: Math.abs(gross),
signedAmount: gross, signedAmount: gross,
kind: "add", kind: "add",
hint: t("settlementCenter:billDisplay.teamGrossHint", { hint: t("settlementCenter:billDisplay.teamGrossHint", {
defaultValue: "本级及下级玩家合计", defaultValue: "本级及下级玩家本期合计",
}), }),
}); });
} }
if (bill.rebate_amount != null && rebate !== 0) { if (bill.rebate_amount != null && rebate !== 0) {
lines.push({ lines.push({
key: "rebate", key: "rebate",
label: t("settlementCenter:billDisplay.teamRebate", { defaultValue: "团队回水" }), label: t("settlementCenter:billDisplay.teamRebate", { defaultValue: "扣玩家回水" }),
amount: Math.abs(rebate), amount: Math.abs(rebate),
signedAmount: -Math.abs(rebate), signedAmount: -Math.abs(rebate),
kind: "subtract", kind: "subtract",
@@ -292,7 +292,7 @@ export function buildBillAmountBreakdown(
if (bill.gross_win_loss != null || bill.rebate_amount != null) { if (bill.gross_win_loss != null || bill.rebate_amount != null) {
lines.push({ lines.push({
key: "team-net", key: "team-net",
label: t("settlementCenter:billDisplay.teamNet", { defaultValue: "团队净额" }), label: t("settlementCenter:billDisplay.teamNet", { defaultValue: "可分配利润" }),
amount: Math.abs(teamNet), amount: Math.abs(teamNet),
signedAmount: teamNet, signedAmount: teamNet,
kind: "subtotal", kind: "subtotal",
@@ -301,14 +301,14 @@ export function buildBillAmountBreakdown(
if (downlineShares && downlineShares.total > 0) { if (downlineShares && downlineShares.total > 0) {
lines.push({ lines.push({
key: "downline-share", key: "downline-share",
label: billDisplayLabel(t, "agentDownlineShare", "下级占成"), label: billDisplayLabel(t, "agentDownlineShare", "下级代理保留"),
amount: Math.abs(downlineShares.total), amount: Math.abs(downlineShares.total),
signedAmount: -Math.abs(downlineShares.total), signedAmount: -Math.abs(downlineShares.total),
kind: "subtract", kind: "subtract",
hint: billDisplayLabel( hint: billDisplayLabel(
t, t,
"agentDownlineShareHint", "agentDownlineShareHint",
"下级代理按占成比例保留的利润(明细见下行)", "下级代理按约定比例保留的金额(明细见下行)",
), ),
children: downlineShares.items.map((item) => ({ children: downlineShares.items.map((item) => ({
key: `downline-share-${item.owner_id}`, key: `downline-share-${item.owner_id}`,
@@ -324,11 +324,11 @@ export function buildBillAmountBreakdown(
if (meta.share_profit != null) { if (meta.share_profit != null) {
lines.push({ lines.push({
key: "share", key: "share",
label: billDisplayLabel(t, "agentShareKeep", "本级占成"), label: billDisplayLabel(t, "agentShareKeep", "本级保留"),
amount: Math.abs(shareProfit), amount: Math.abs(shareProfit),
signedAmount: -Math.abs(shareProfit), signedAmount: -Math.abs(shareProfit),
kind: "subtract", kind: "subtract",
hint: billDisplayLabel(t, "agentShareKeepHint", "本级按占成比例留下的利润"), hint: billDisplayLabel(t, "agentShareKeepHint", "本级按约定比例保留的金额"),
}); });
} }
if (rounding !== 0) { if (rounding !== 0) {
@@ -344,8 +344,8 @@ export function buildBillAmountBreakdown(
key: "net", key: "net",
label: label:
bill.net_amount > 0 bill.net_amount > 0
? billDisplayLabel(t, "agentNet", "应 {{counterparty}}", { counterparty }) ? billDisplayLabel(t, "agentNet", "应上缴 {{counterparty}}", { counterparty })
: billDisplayLabel(t, "agentNetReceive", "{{counterparty}} 应本级", { counterparty }), : billDisplayLabel(t, "agentNetReceive", "{{counterparty}} 应补给本级", { counterparty }),
amount: Math.abs(bill.net_amount), amount: Math.abs(bill.net_amount),
signedAmount: bill.net_amount, signedAmount: bill.net_amount,
kind: "total", kind: "total",

View File

@@ -1,10 +1,10 @@
import type { SettlementBillListScope } from "@/api/admin-agent-settlement"; import type { SettlementBillListScope } from "@/api/admin-agent-settlement";
export type SettlementPeriodView = "bills" | "ledger"; export type SettlementPeriodView = "pending" | "bills" | "ledger";
export type SettlementBillScopeFilter = "all" | SettlementBillListScope; export type SettlementBillScopeFilter = "all" | SettlementBillListScope;
const VALID_VIEWS: SettlementPeriodView[] = ["bills", "ledger"]; const VALID_VIEWS: SettlementPeriodView[] = ["pending", "bills", "ledger"];
const VALID_BILL_SCOPES: SettlementBillScopeFilter[] = [ const VALID_BILL_SCOPES: SettlementBillScopeFilter[] = [
"all", "all",
@@ -44,7 +44,7 @@ export function settlementCenterScopeHref(
export function settlementPeriodViewHref( export function settlementPeriodViewHref(
periodId: number, periodId: number,
view: SettlementPeriodView = "bills", view: SettlementPeriodView = "pending",
adminSiteId?: number | null, adminSiteId?: number | null,
billScope?: SettlementBillScopeFilter | null, billScope?: SettlementBillScopeFilter | null,
): string { ): string {
@@ -92,7 +92,7 @@ export function parseSettlementCenterView(
const view = const view =
normalizedView !== null && VALID_VIEWS.includes(normalizedView as SettlementPeriodView) normalizedView !== null && VALID_VIEWS.includes(normalizedView as SettlementPeriodView)
? (normalizedView as SettlementPeriodView) ? (normalizedView as SettlementPeriodView)
: "bills"; : "pending";
const billScope = const billScope =
scopeRaw !== null && scopeRaw !== null &&

View File

@@ -45,13 +45,11 @@ export function SettlementCenterPeriodDetail({
const { t } = useTranslation("settlementCenter"); const { t } = useTranslation("settlementCenter");
const subViews: { key: SettlementPeriodView; label: string }[] = [ const subViews: { key: SettlementPeriodView; label: string }[] = [
{ key: "bills", label: t("nav.bills", { defaultValue: "账单" }) }, { key: "pending", label: t("nav.pendingWorkbench", { defaultValue: "待处理" }) },
{ key: "ledger", label: t("nav.ledger", { defaultValue: "账务流水" }) }, { key: "bills", label: t("nav.allBills", { defaultValue: "全部账单" }) },
{ key: "ledger", label: t("nav.ledgerAudit", { defaultValue: "流水审计" }) },
]; ];
const pendingConfirm = period.summary?.pending_confirm ?? 0;
const awaitingPayment = period.summary?.awaiting_payment ?? 0;
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<SettlementPeriodHeader <SettlementPeriodHeader
@@ -76,6 +74,21 @@ export function SettlementCenterPeriodDetail({
))} ))}
</AdminSubnav> </AdminSubnav>
{view === "pending" ? (
<SettlementMainPanel
key={`${adminSiteId}-${period.id}-${refreshKey}-pending`}
adminSiteId={adminSiteId}
currencyCode={currencyCode}
periodFilter={period.id}
mode="workbench"
onOpenBillDetail={onOpenBillDetail}
refreshKey={refreshKey}
canOperateBills={canOperateBills}
selectedPeriodStatus={period.status}
boundAgentId={boundAgentId}
/>
) : null}
{view === "bills" ? ( {view === "bills" ? (
<SettlementMainPanel <SettlementMainPanel
key={`${adminSiteId}-${period.id}-${refreshKey}-${billScope}`} key={`${adminSiteId}-${period.id}-${refreshKey}-${billScope}`}
@@ -86,8 +99,6 @@ export function SettlementCenterPeriodDetail({
onOpenBillDetail={onOpenBillDetail} onOpenBillDetail={onOpenBillDetail}
refreshKey={refreshKey} refreshKey={refreshKey}
canOperateBills={canOperateBills} canOperateBills={canOperateBills}
pendingConfirm={pendingConfirm}
awaitingPayment={awaitingPayment}
selectedPeriodStatus={period.status} selectedPeriodStatus={period.status}
boundAgentId={boundAgentId} boundAgentId={boundAgentId}
/> />
@@ -104,4 +115,4 @@ export function SettlementCenterPeriodDetail({
) : null} ) : null}
</div> </div>
); );
} }

View File

@@ -23,7 +23,11 @@ import {
import { SettlementPeriodWorkbench } from "@/modules/settlement/settlement-period-workbench"; import { SettlementPeriodWorkbench } from "@/modules/settlement/settlement-period-workbench";
import { formatAdminSiteLabel } from "@/lib/admin-site-display"; import { formatAdminSiteLabel } from "@/lib/admin-site-display";
import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { PRD_SETTLEMENT_AGENT_MANAGE } from "@/lib/admin-prd"; import {
PRD_INTEGRATION_MANAGE,
PRD_INTEGRATION_VIEW,
PRD_SETTLEMENT_AGENT_MANAGE,
} from "@/lib/admin-prd";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -39,6 +43,30 @@ import { useAdminProfile } from "@/stores/admin-session";
type SiteOption = { id: number; label: string; code: string; currency_code: string }; type SiteOption = { id: number; label: string; code: string; currency_code: string };
function siteOptionsFromProfile(profile: ReturnType<typeof useAdminProfile>): SiteOption[] {
const byId = new Map<number, SiteOption>();
if (profile?.site != null) {
byId.set(profile.site.id, {
id: profile.site.id,
label: formatAdminSiteLabel(profile.site.name, profile.site.code),
code: profile.site.code,
currency_code: "NPR",
});
}
for (const site of profile?.accessible_sites ?? []) {
byId.set(site.id, {
id: site.id,
label: formatAdminSiteLabel(site.name, site.code),
code: site.code,
currency_code: "NPR",
});
}
return Array.from(byId.values());
}
export function SettlementCenterShell(): React.ReactElement { export function SettlementCenterShell(): React.ReactElement {
const { t } = useTranslation(["settlementCenter", "common"]); const { t } = useTranslation(["settlementCenter", "common"]);
const router = useRouter(); const router = useRouter();
@@ -70,6 +98,9 @@ export function SettlementCenterShell(): React.ReactElement {
adminHasAnyPermission(profile?.permissions, [PRD_SETTLEMENT_AGENT_MANAGE]); adminHasAnyPermission(profile?.permissions, [PRD_SETTLEMENT_AGENT_MANAGE]);
const canManagePeriods = canOperateBills && boundAgent === null; const canManagePeriods = canOperateBills && boundAgent === null;
const canFinanceAdjustments = canOperateBills && boundAgent === null; const canFinanceAdjustments = canOperateBills && boundAgent === null;
const canLoadIntegrationSites =
profile?.is_super_admin === true ||
adminHasAnyPermission(profile?.permissions, [PRD_INTEGRATION_VIEW, PRD_INTEGRATION_MANAGE]);
const [siteOptions, setSiteOptions] = useState<SiteOption[]>([]); const [siteOptions, setSiteOptions] = useState<SiteOption[]>([]);
const [sitesReady, setSitesReady] = useState(() => boundAgent?.admin_site_id != null); const [sitesReady, setSitesReady] = useState(() => boundAgent?.admin_site_id != null);
@@ -120,6 +151,23 @@ export function SettlementCenterShell(): React.ReactElement {
return; return;
} }
const profileSites = siteOptionsFromProfile(profile);
if (!canLoadIntegrationSites && profileSites.length > 0) {
setSiteOptions(profileSites);
setAdminSiteId((current) => {
if (siteFromUrl !== null && profileSites.some((site) => site.id === siteFromUrl)) {
return siteFromUrl;
}
if (current !== null && profileSites.some((site) => site.id === current)) {
return current;
}
return profileSites[0]?.id ?? null;
});
setSitesLoadErr(null);
setSitesReady(true);
return;
}
setSitesReady(false); setSitesReady(false);
setSitesLoadErr(null); setSitesLoadErr(null);
try { try {
@@ -148,7 +196,7 @@ export function SettlementCenterShell(): React.ReactElement {
} finally { } finally {
setSitesReady(true); setSitesReady(true);
} }
}, [boundAgent, siteFromUrl, t]); }, [boundAgent, canLoadIntegrationSites, profile, siteFromUrl, t]);
useEffect(() => { useEffect(() => {
void loadIntegrationSites(); void loadIntegrationSites();
@@ -277,7 +325,7 @@ export function SettlementCenterShell(): React.ReactElement {
const activePeriod = const activePeriod =
activePeriodId !== null ? (periods.find((row) => row.id === activePeriodId) ?? null) : null; activePeriodId !== null ? (periods.find((row) => row.id === activePeriodId) ?? null) : null;
const openPeriodView = (periodId: number, view: SettlementPeriodView = "bills"): void => { const openPeriodView = (periodId: number, view: SettlementPeriodView = "pending"): void => {
const period = periods.find((row) => row.id === periodId); const period = periods.find((row) => row.id === periodId);
const scope = preferredBillScopeForPeriod(period?.summary); const scope = preferredBillScopeForPeriod(period?.summary);
router.push(settlementPeriodViewHref(periodId, view, siteId, scope)); router.push(settlementPeriodViewHref(periodId, view, siteId, scope));
@@ -384,7 +432,7 @@ export function SettlementCenterShell(): React.ReactElement {
canManage={canManagePeriods} canManage={canManagePeriods}
periods={periods} periods={periods}
headerActions={headerActions} headerActions={headerActions}
onViewDetail={(id) => openPeriodView(id, "bills")} onViewDetail={(id) => openPeriodView(id, "pending")}
onReloadPeriods={loadPeriods} onReloadPeriods={loadPeriods}
onPeriodOpened={() => { onPeriodOpened={() => {
setRefreshKey((n) => n + 1); setRefreshKey((n) => n + 1);

View File

@@ -359,7 +359,7 @@ export function SettlementCreditLedgerPanel({
<AdminTableNoResourceRow <AdminTableNoResourceRow
colSpan={COL_SPAN} colSpan={COL_SPAN}
message={t("creditLedger.emptyPeriod", { message={t("creditLedger.emptyPeriod", {
defaultValue: "本账期暂无账务流水。", defaultValue: "本账期暂无流水审计记录。",
})} })}
/> />
) : ( ) : (

View File

@@ -1,6 +1,5 @@
"use client"; "use client";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -28,15 +27,11 @@ import { useConfirmAction } from "@/hooks/use-confirm-action";
import type { AgentSettlementPeriodFilter } from "@/modules/settlement/agent-settlement-period-select"; import type { AgentSettlementPeriodFilter } from "@/modules/settlement/agent-settlement-period-select";
import { SettlementBillsTable } from "@/modules/settlement/settlement-bills-table"; import { SettlementBillsTable } from "@/modules/settlement/settlement-bills-table";
import { settlementBillOperableByBoundAgent } from "@/modules/settlement/settlement-bill-operable"; import { settlementBillOperableByBoundAgent } from "@/modules/settlement/settlement-bill-operable";
import { import type { SettlementBillScopeFilter } from "@/modules/settlement/settlement-center-nav";
settlementPeriodViewHref,
type SettlementBillScopeFilter,
} from "@/modules/settlement/settlement-center-nav";
import { settlementBillStatusLabel } from "@/modules/settlement/settlement-status-label";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
import { cn } from "@/lib/utils";
type BillTypeFilter = "all" | "player" | "agent"; type BillTypeFilter = "all" | "player" | "agent";
type SettlementMainPanelMode = "workbench" | "all";
type BillFilters = { type BillFilters = {
ownerKeyword: string; ownerKeyword: string;
@@ -85,11 +80,10 @@ export type SettlementMainPanelProps = {
currencyCode: string; currencyCode: string;
periodFilter: AgentSettlementPeriodFilter; periodFilter: AgentSettlementPeriodFilter;
billScope?: SettlementBillScopeFilter; billScope?: SettlementBillScopeFilter;
mode?: SettlementMainPanelMode;
onOpenBillDetail: (billId: number, focus?: "confirm" | "payment") => void; onOpenBillDetail: (billId: number, focus?: "confirm" | "payment") => void;
refreshKey?: number; refreshKey?: number;
canOperateBills?: boolean; canOperateBills?: boolean;
pendingConfirm: number;
awaitingPayment: number;
selectedPeriodStatus?: string | null; selectedPeriodStatus?: string | null;
boundAgentId?: number | null; boundAgentId?: number | null;
}; };
@@ -99,11 +93,10 @@ export function SettlementMainPanel({
currencyCode, currencyCode,
periodFilter, periodFilter,
billScope = "all", billScope = "all",
mode = "all",
onOpenBillDetail, onOpenBillDetail,
refreshKey = 0, refreshKey = 0,
canOperateBills = false, canOperateBills = false,
pendingConfirm,
awaitingPayment,
selectedPeriodStatus, selectedPeriodStatus,
boundAgentId = null, boundAgentId = null,
}: SettlementMainPanelProps): React.ReactElement { }: SettlementMainPanelProps): React.ReactElement {
@@ -112,17 +105,17 @@ export function SettlementMainPanel({
const periodOpen = selectedPeriodStatus === "open"; const periodOpen = selectedPeriodStatus === "open";
const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const initialFilters = useMemo( const initialFilters = useMemo(() => {
() => filtersForPeriod(boundAgentId, billScope), const base = filtersForPeriod(boundAgentId, billScope);
[boundAgentId, billScope], return mode === "workbench" ? { ...base, statusScope: "all" as const } : base;
); }, [boundAgentId, billScope, mode]);
const [draft, setDraft] = useState<BillFilters>(initialFilters); const [draft, setDraft] = useState<BillFilters>(initialFilters);
const [applied, setApplied] = useState<BillFilters>(initialFilters); const [applied, setApplied] = useState<BillFilters>(initialFilters);
const [rows, setRows] = useState<SettlementBillRow[]>([]); const [rows, setRows] = useState<SettlementBillRow[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20); const [perPage, setPerPage] = useState(mode === "workbench" ? 100 : 20);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
useEffect(() => { useEffect(() => {
@@ -141,8 +134,8 @@ export function SettlementMainPanel({
bill_type: q.bill_type, bill_type: q.bill_type,
scope: q.scope, scope: q.scope,
keyword: q.keyword, keyword: q.keyword,
page, page: mode === "workbench" ? 1 : page,
per_page: perPage, per_page: mode === "workbench" ? 100 : perPage,
}); });
setRows(data.items ?? []); setRows(data.items ?? []);
setTotal(data.total ?? data.items?.length ?? 0); setTotal(data.total ?? data.items?.length ?? 0);
@@ -157,7 +150,7 @@ export function SettlementMainPanel({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [adminSiteId, applied, page, perPage, periodId, t]); }, [adminSiteId, applied, mode, page, perPage, periodId, t]);
useAsyncEffect(() => { useAsyncEffect(() => {
void load(); void load();
@@ -174,24 +167,6 @@ export function SettlementMainPanel({
setPage(1); setPage(1);
}; };
const statusOptionLabel = (value: SettlementBillScopeFilter): string => {
if (value === "all") {
return t("billsPanel.filterAll", { defaultValue: "全部状态" });
}
if (value === "pending_confirm") {
const label = t("billsPanel.category.pendingConfirm", { defaultValue: "待确认" });
return pendingConfirm > 0 ? `${label} (${pendingConfirm})` : label;
}
if (value === "awaiting_payment") {
const label = t("billsPanel.category.awaitingPayment", { defaultValue: "待收付" });
return awaitingPayment > 0 ? `${label} (${awaitingPayment})` : label;
}
if (value === "settled") {
return settlementBillStatusLabel("settled", t);
}
return t("billsPanel.filterAdjustment", { defaultValue: "调账 / 冲正" });
};
const emptyBillMessage = useMemo((): string | undefined => { const emptyBillMessage = useMemo((): string | undefined => {
if (periodOpen) { if (periodOpen) {
return t("empty.billsNeedClose", { return t("empty.billsNeedClose", {
@@ -223,21 +198,6 @@ export function SettlementMainPanel({
} }
}; };
const scopeChipClass = (active: boolean): string =>
cn(
"inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium transition-colors",
active
? "border-primary/40 bg-primary/10 text-primary"
: "border-border/70 bg-muted/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground",
);
const scopeChips: SettlementBillScopeFilter[] = [
"all",
"pending_confirm",
"awaiting_payment",
"settled",
];
const requestConfirmBill = (row: SettlementBillRow): void => { const requestConfirmBill = (row: SettlementBillRow): void => {
requestConfirm({ requestConfirm({
title: t("billsPanel.confirmOneTitle", { defaultValue: "确认账单 #{{id}}", id: row.id }), title: t("billsPanel.confirmOneTitle", { defaultValue: "确认账单 #{{id}}", id: row.id }),
@@ -264,32 +224,115 @@ export function SettlementMainPanel({
const canOperateRow = (row: SettlementBillRow): boolean => const canOperateRow = (row: SettlementBillRow): boolean =>
canOperateBills && settlementBillOperableByBoundAgent(row, boundAgentId ? { id: boundAgentId } : null); canOperateBills && settlementBillOperableByBoundAgent(row, boundAgentId ? { id: boundAgentId } : null);
const pendingConfirmRows = rows.filter((row) => row.status === "pending_confirm");
const awaitingPaymentRows = rows.filter((row) =>
["confirmed", "partial_paid", "overdue"].includes(row.status) && row.unpaid_amount > 0,
);
const exceptionRows = rows.filter((row) =>
["adjustment", "reversal", "bad_debt"].includes(row.bill_type),
);
const hasWorkbenchRows =
pendingConfirmRows.length > 0 || awaitingPaymentRows.length > 0 || exceptionRows.length > 0;
const renderBillsTable = (
tableRows: SettlementBillRow[],
emptyMessage?: string,
): React.ReactElement => (
<SettlementBillsTable
rows={tableRows}
loading={loading}
currencyCode={currencyCode}
billTypeFilter={applied.billType}
emptyMessage={emptyMessage}
canOperate={canOperateBills}
boundAgentId={boundAgentId}
onOpenDetail={(billId) => onOpenBillDetail(billId)}
onConfirmBill={(row) => {
if (canOperateRow(row) && row.status === "pending_confirm") {
requestConfirmBill(row);
}
}}
onPayBill={(row) => {
if (
canOperateRow(row) &&
["confirmed", "partial_paid", "overdue"].includes(row.status) &&
row.unpaid_amount > 0
) {
onOpenBillDetail(row.id, "payment");
}
}}
/>
);
if (mode === "workbench") {
return (
<div className="space-y-4">
<ConfirmDialog />
{loading && rows.length === 0 ? (
<AdminLoadingState />
) : periodOpen ? (
renderBillsTable([], emptyBillMessage)
) : !hasWorkbenchRows ? (
renderBillsTable([], t("billsPanel.workbenchEmpty", {
defaultValue: "本期暂无待处理账单。需要查历史或已结清账单,请进入「全部账单」。",
}))
) : (
<>
{pendingConfirmRows.length > 0 ? (
<section className="space-y-2">
<div>
<h3 className="text-sm font-semibold">
{t("billsPanel.workbench.pendingTitle", { defaultValue: "待确认账单" })}
</h3>
<p className="text-xs text-muted-foreground">
{t("billsPanel.workbench.pendingHint", {
defaultValue: "先确认金额与双方,确认后进入待收付。",
})}
</p>
</div>
{renderBillsTable(pendingConfirmRows)}
</section>
) : null}
{awaitingPaymentRows.length > 0 ? (
<section className="space-y-2">
<div>
<h3 className="text-sm font-semibold">
{t("billsPanel.workbench.awaitingTitle", { defaultValue: "待收付账单" })}
</h3>
<p className="text-xs text-muted-foreground">
{t("billsPanel.workbench.awaitingHint", {
defaultValue: "按谁付谁登记线下收付,全部收付后账单结清。",
})}
</p>
</div>
{renderBillsTable(awaitingPaymentRows)}
</section>
) : null}
{exceptionRows.length > 0 ? (
<section className="space-y-2">
<div>
<h3 className="text-sm font-semibold">
{t("billsPanel.workbench.exceptionTitle", { defaultValue: "异常处理" })}
</h3>
<p className="text-xs text-muted-foreground">
{t("billsPanel.workbench.exceptionHint", {
defaultValue: "逾期、调账、冲正与坏账记录集中在这里复核。",
})}
</p>
</div>
{renderBillsTable(exceptionRows)}
</section>
) : null}
</>
)}
</div>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<ConfirmDialog /> <ConfirmDialog />
{!periodOpen && periodId != null ? (
<div className="flex flex-wrap items-center gap-2">
{scopeChips.map((scope) => {
const active = applied.statusScope === scope;
const href = settlementPeriodViewHref(periodId, "bills", adminSiteId, scope);
const count =
scope === "pending_confirm"
? pendingConfirm
: scope === "awaiting_payment"
? awaitingPayment
: undefined;
return (
<Link key={scope} href={href} className={scopeChipClass(active)}>
{statusOptionLabel(scope)}
{count != null && count > 0 ? ` ${count}` : null}
</Link>
);
})}
</div>
) : null}
<div className="admin-list-toolbar"> <div className="admin-list-toolbar">
<div className="admin-list-field min-w-[12rem] flex-1"> <div className="admin-list-field min-w-[12rem] flex-1">
<Label htmlFor="sb-owner" className="sm:shrink-0"> <Label htmlFor="sb-owner" className="sm:shrink-0">
@@ -352,30 +395,7 @@ export function SettlementMainPanel({
<AdminLoadingState /> <AdminLoadingState />
) : ( ) : (
<> <>
<SettlementBillsTable {renderBillsTable(rows, emptyBillMessage)}
rows={rows}
loading={loading}
currencyCode={currencyCode}
billTypeFilter={applied.billType}
emptyMessage={emptyBillMessage}
canOperate={canOperateBills}
boundAgentId={boundAgentId}
onOpenDetail={(billId) => onOpenBillDetail(billId)}
onConfirmBill={(row) => {
if (canOperateRow(row) && row.status === "pending_confirm") {
requestConfirmBill(row);
}
}}
onPayBill={(row) => {
if (
canOperateRow(row) &&
["confirmed", "partial_paid", "overdue"].includes(row.status) &&
row.unpaid_amount > 0
) {
onOpenBillDetail(row.id, "payment");
}
}}
/>
<AdminListPaginationFooter <AdminListPaginationFooter
selectId="settlement-bills-per-page" selectId="settlement-bills-per-page"
page={page} page={page}
@@ -393,4 +413,4 @@ export function SettlementMainPanel({
)} )}
</div> </div>
); );
} }