diff --git a/src/api/admin-player.ts b/src/api/admin-player.ts index da06c9d..178f652 100644 --- a/src/api/admin-player.ts +++ b/src/api/admin-player.ts @@ -18,6 +18,7 @@ export async function getAdminPlayers(params?: { status?: number; site_code?: string; agent_node_id?: number; + agent_scope?: "subtree" | "direct"; }): Promise { return adminRequest.get(`${A}/players`, { params }); } diff --git a/src/i18n/locales/en/settlementCenter.json b/src/i18n/locales/en/settlementCenter.json index cde45d5..f16a5da 100644 --- a/src/i18n/locales/en/settlementCenter.json +++ b/src/i18n/locales/en/settlementCenter.json @@ -65,10 +65,13 @@ }, "nav": { "periods": "Periods", + "pendingWorkbench": "To do", + "allBills": "All bills", "bills": "Bills", "operations": "Payments & adjustments", "aria": "Period views", "ledger": "Account ledger", + "ledgerAudit": "Ledger audit", "creditLedger": "Credit ledger", "playerBills": "Player bills", "agentBills": "Agent bills", @@ -194,9 +197,9 @@ }, "billStatus": { "pending_confirm": "Pending confirm", - "confirmed": "Confirmed", - "partial_paid": "Partial paid", - "settled": "Settled", + "confirmed": "Awaiting payment", + "partial_paid": "Partial payment", + "settled": "Fully settled", "overdue": "Overdue", "reversed": "Reversed" }, @@ -229,26 +232,34 @@ "payerLabel": "Payer", "payeeLabel": "Payee", "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", "playerLostHint": "Player lost; owes agent", "playerWonHint": "Player won; agent owes player", "playerNet": "Player net payable", "playerNetReceive": "Agent pays player", - "teamGross": "Team game win/loss", - "teamGrossHint": "Includes this agent and all downline players", + "teamGross": "Team win/loss this period", + "teamGrossHint": "Total for this agent and downline players in this period", "teamGrossShort": "Team", "playerGrossShort": "Player", - "teamRebate": "Team rebate", - "teamNet": "Team net", + "teamRebate": "Deduct player rebates", + "teamNet": "Profit available to split", "rebate": "Rebate", - "agentShareKeep": "Share kept at this tier", - "agentShareKeepHint": "Profit retained at this tier by share ratio", - "agentDownlineShare": "Downline share", - "agentDownlineShareHint": "Profit retained by downline agents (see breakdown below)", + "agentShareKeep": "This tier keeps", + "agentShareKeepHint": "Amount retained by this tier under the agreed share", + "agentDownlineShare": "Downline agents keep", + "agentDownlineShareHint": "Amount retained by downline agents first (see breakdown below)", "agentDownlineShareItem": "{{agent}} kept", - "agentNet": "Pay {{counterparty}}", - "agentNetReceive": "{{counterparty}} pays this tier", + "agentNet": "Remit to {{counterparty}}", + "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", "billCounterparty": "Counterparty", "unpaidPendingConfirm": "Confirm the bill before recording payment", @@ -340,6 +351,15 @@ "clientFilterHint": "Owner filter: showing {{shown}} / {{total}}", "emptyFiltered": "No bills match the filter. Try All statuses or reset.", "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": { "playerOwner": "Player owner", "agentOwner": "Agent owner", diff --git a/src/i18n/locales/ne/settlementCenter.json b/src/i18n/locales/ne/settlementCenter.json index 4cef75a..48d8b06 100644 --- a/src/i18n/locales/ne/settlementCenter.json +++ b/src/i18n/locales/ne/settlementCenter.json @@ -64,9 +64,12 @@ }, "nav": { "periods": "अवधिहरू", + "pendingWorkbench": "To do", + "allBills": "All bills", "bills": "बिलहरू", "operations": "भुक्तानी र समायोजन", "ledger": "लेजर", + "ledgerAudit": "Ledger audit", "creditLedger": "क्रेडिट लेजर", "playerBills": "Player bills", "agentBills": "Agent bills", @@ -192,9 +195,9 @@ }, "billStatus": { "pending_confirm": "Pending confirm", - "confirmed": "Confirmed", - "partial_paid": "Partial paid", - "settled": "Settled", + "confirmed": "Awaiting payment", + "partial_paid": "Partial payment", + "settled": "Fully settled", "overdue": "Overdue", "reversed": "Reversed" }, @@ -226,26 +229,34 @@ "payerLabel": "Payer", "payeeLabel": "Payee", "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", "playerLostHint": "Player lost; owes agent", "playerWonHint": "Player won; agent owes player", "playerNet": "Player net payable", "playerNetReceive": "Agent pays player", - "teamGross": "Team game win/loss", - "teamGrossHint": "Includes this agent and all downline players", + "teamGross": "Team win/loss this period", + "teamGrossHint": "Total for this agent and downline players in this period", "teamGrossShort": "Team", "playerGrossShort": "Player", - "teamRebate": "Team rebate", - "teamNet": "Team net", + "teamRebate": "Deduct player rebates", + "teamNet": "Profit available to split", "rebate": "Rebate", - "agentShareKeep": "Share kept at this tier", - "agentShareKeepHint": "Profit retained at this tier by share ratio", - "agentDownlineShare": "Downline share", - "agentDownlineShareHint": "Profit retained by downline agents (see breakdown below)", + "agentShareKeep": "This tier keeps", + "agentShareKeepHint": "Amount retained by this tier under the agreed share", + "agentDownlineShare": "Downline agents keep", + "agentDownlineShareHint": "Amount retained by downline agents first (see breakdown below)", "agentDownlineShareItem": "{{agent}} kept", - "agentNet": "Pay {{counterparty}}", - "agentNetReceive": "{{counterparty}} pays this tier", + "agentNet": "Remit to {{counterparty}}", + "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", "billCounterparty": "Counterparty", "unpaidPendingConfirm": "Confirm the bill before recording payment", @@ -330,6 +341,15 @@ "clientFilterHint": "Owner filter: showing {{shown}} / {{total}}", "emptyFiltered": "No bills match the filter. Try All statuses or reset.", "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": { "playerOwner": "Player owner", "agentOwner": "Agent owner", diff --git a/src/i18n/locales/zh/settlementCenter.json b/src/i18n/locales/zh/settlementCenter.json index 2b418d8..ea1324d 100644 --- a/src/i18n/locales/zh/settlementCenter.json +++ b/src/i18n/locales/zh/settlementCenter.json @@ -50,10 +50,13 @@ }, "nav": { "periods": "账期", + "pendingWorkbench": "待处理", + "allBills": "全部账单", "bills": "账单", "operations": "收付与调账", "aria": "账期视图", - "ledger": "账务流水", + "ledger": "流水审计", + "ledgerAudit": "流水审计", "creditLedger": "信用流水", "playerBills": "玩家账单", "agentBills": "代理账单", @@ -65,7 +68,7 @@ "reports": "账期报表" }, "operations": { - "hint": "登记收付、坏账核销与补差/冲正的操作台账。玩家信用变动请查看「账务流水」。", + "hint": "登记收付、坏账核销与补差/冲正的操作台账。玩家信用变动请查看「流水审计」。", "adjustmentsTitle": "调账 / 冲正 / 坏账", "loadFailed": "收付与调账记录加载失败", "operationType": "操作类型", @@ -179,8 +182,8 @@ }, "billStatus": { "pending_confirm": "待确认", - "confirmed": "已确认", - "partial_paid": "部分结清", + "confirmed": "待收付", + "partial_paid": "部分收付", "settled": "已结清", "overdue": "逾期", "reversed": "已冲正" @@ -214,26 +217,34 @@ "payerLabel": "付款方", "payeeLabel": "收款方", "playerBreakdownIntro": "玩家只与直属代理结算,净额 = 输赢 − 回水。", - "agentBreakdownIntro": "代理只与直属上级结算,净额 = 团队净额 − 下级占成 − 本级占成。", + "agentBreakdownIntro": "先看本期团队赢亏,扣除玩家回水和各级保留后,就是本级需要上缴或应收的金额。", "playerGross": "游戏输赢", "playerLostHint": "玩家输了,应付代理", "playerWonHint": "玩家赢了,代理应付玩家", "playerNet": "玩家应付净额", "playerNetReceive": "代理应付玩家", - "teamGross": "团队游戏输赢", - "teamGrossHint": "含本级及下级玩家的合计", + "teamGross": "本期团队赢亏", + "teamGrossHint": "本级及下级玩家本期合计", "teamGrossShort": "团队", "playerGrossShort": "玩家", - "teamRebate": "团队回水", - "teamNet": "团队净额", + "teamRebate": "扣玩家回水", + "teamNet": "可分配利润", "rebate": "回水", - "agentShareKeep": "本级占成", - "agentShareKeepHint": "本级按占成比例留下的利润", - "agentDownlineShare": "下级占成", - "agentDownlineShareHint": "下级代理按占成比例保留的利润(明细见下行)", + "agentShareKeep": "本级保留", + "agentShareKeepHint": "本级按约定比例保留的金额", + "agentDownlineShare": "下级代理保留", + "agentDownlineShareHint": "下级代理按约定比例先保留的金额(明细见下行)", "agentDownlineShareItem": "{{agent}} 保留", - "agentNet": "应付 {{counterparty}}", - "agentNetReceive": "{{counterparty}} 应付本级", + "agentNet": "应上缴 {{counterparty}}", + "agentNetReceive": "{{counterparty}} 应补给本级", + "actionPendingConfirmTitle": "当前应确认账单", + "actionPendingConfirmDesc": "确认金额和双方无误后,账单会进入待收付。", + "actionAwaitingPaymentTitle": "当前应登记收付", + "actionAwaitingPaymentDesc": "按谁付谁登记线下收付;未结清可分多次登记。", + "actionSettledTitle": "本账单已结清", + "actionSettledDesc": "如需复核,可查看下方结算明细和收付记录。", + "actionReviewTitle": "请复核账单状态", + "actionReviewDesc": "如金额或方向有差异,可通过调账、冲正或坏账处理。", "billOwner": "账单主体", "billCounterparty": "结算对手", "unpaidPendingConfirm": "确认账单后可登记收付", @@ -327,6 +338,15 @@ "hierarchyHint": "同一账期会生成多笔账单:玩家先与直属代理结,代理扣除本级占成后再向上级缴纳。因此「输赢」可能相同,但「结算金额」会逐级减少。", "emptyFiltered": "当前筛选下暂无账单,请改为「全部状态」或重置筛选。", "emptyClosed": "本期已关账但暂无账单。常见原因:账期内无信用盘玩家的已结算注单,或占成流水不在本账期时间范围内。", + "workbenchEmpty": "本期暂无待处理账单。需要查历史或已结清账单,请进入「全部账单」。", + "workbench": { + "pendingTitle": "待确认账单", + "pendingHint": "先确认金额与双方,确认后进入待收付。", + "awaitingTitle": "待收付账单", + "awaitingHint": "按谁付谁登记线下收付,全部收付后账单结清。", + "exceptionTitle": "异常处理", + "exceptionHint": "调账、冲正与坏账记录集中在这里复核。" + }, "intro": "关账后生成的占成账单。可按类型或状态筛选,详情内确认或登记收付。", "confirmOneTitle": "确认账单 #{{id}}?", "confirmOneDesc": "确认后进入待收付,可登记线下收付。", @@ -343,7 +363,7 @@ "title": "结算概览" }, "ledger": { - "title": "账务流水" + "title": "流水审计" }, "bills": { "title": "全部账单" diff --git a/src/modules/agents/agents-console.tsx b/src/modules/agents/agents-console.tsx index 03b6700..20895fd 100644 --- a/src/modules/agents/agents-console.tsx +++ b/src/modules/agents/agents-console.tsx @@ -582,6 +582,7 @@ export function AgentsConsole(): React.ReactElement { void getAdminPlayers({ site_code: activeSiteCode.trim(), agent_node_id: selectedNodeId, + agent_scope: "direct", page: 1, per_page: 1, }) @@ -617,6 +618,7 @@ export function AgentsConsole(): React.ReactElement { void getAdminPlayers({ site_code: activeSiteCode.trim(), agent_node_id: child.id, + agent_scope: "direct", page: 1, per_page: 1, }) diff --git a/src/modules/agents/agents-players-panel.tsx b/src/modules/agents/agents-players-panel.tsx index 30dd8ae..d7eb3a7 100644 --- a/src/modules/agents/agents-players-panel.tsx +++ b/src/modules/agents/agents-players-panel.tsx @@ -254,6 +254,7 @@ export function AgentsPlayersPanel({ site_code: siteCode.trim(), ...(appliedKeyword.trim() !== "" ? { keyword: appliedKeyword.trim() } : {}), ...(effectiveAgentId !== null ? { agent_node_id: effectiveAgentId } : {}), + ...(effectiveAgentId !== null ? { agent_scope: "direct" } : {}), }); setItems(data.items); setTotal(data.meta.total); diff --git a/src/modules/settlement/agent-bill-detail.tsx b/src/modules/settlement/agent-bill-detail.tsx index ff0058b..d63d884 100644 --- a/src/modules/settlement/agent-bill-detail.tsx +++ b/src/modules/settlement/agent-bill-detail.tsx @@ -155,6 +155,50 @@ export function AgentBillDetail({ bill.unpaid_amount > 0 && ["confirmed", "partial_paid", "overdue"].includes(bill.status) && !["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( rebateAllocations.reduce>( (acc, row) => { @@ -337,6 +381,22 @@ export function AgentBillDetail({ <>
+
+
+

{primaryAction.title}

+

{primaryAction.description}

+
+ {primaryAction.button && primaryAction.onClick ? ( + + ) : null} +
0) { lines.push({ key: "downline-share", - label: billDisplayLabel(t, "agentDownlineShare", "下级占成"), + label: billDisplayLabel(t, "agentDownlineShare", "下级代理保留"), amount: Math.abs(downlineShares.total), signedAmount: -Math.abs(downlineShares.total), kind: "subtract", hint: billDisplayLabel( t, "agentDownlineShareHint", - "下级代理按占成比例保留的利润(明细见下行)", + "下级代理按约定比例先保留的金额(明细见下行)", ), children: downlineShares.items.map((item) => ({ key: `downline-share-${item.owner_id}`, @@ -324,11 +324,11 @@ export function buildBillAmountBreakdown( if (meta.share_profit != null) { lines.push({ key: "share", - label: billDisplayLabel(t, "agentShareKeep", "本级占成"), + label: billDisplayLabel(t, "agentShareKeep", "本级保留"), amount: Math.abs(shareProfit), signedAmount: -Math.abs(shareProfit), kind: "subtract", - hint: billDisplayLabel(t, "agentShareKeepHint", "本级按占成比例留下的利润"), + hint: billDisplayLabel(t, "agentShareKeepHint", "本级按约定比例保留的金额"), }); } if (rounding !== 0) { @@ -344,8 +344,8 @@ export function buildBillAmountBreakdown( key: "net", label: bill.net_amount > 0 - ? billDisplayLabel(t, "agentNet", "应付 {{counterparty}}", { counterparty }) - : billDisplayLabel(t, "agentNetReceive", "{{counterparty}} 应付本级", { counterparty }), + ? billDisplayLabel(t, "agentNet", "应上缴 {{counterparty}}", { counterparty }) + : billDisplayLabel(t, "agentNetReceive", "{{counterparty}} 应补给本级", { counterparty }), amount: Math.abs(bill.net_amount), signedAmount: bill.net_amount, kind: "total", diff --git a/src/modules/settlement/settlement-center-nav.ts b/src/modules/settlement/settlement-center-nav.ts index 5005564..23d2289 100644 --- a/src/modules/settlement/settlement-center-nav.ts +++ b/src/modules/settlement/settlement-center-nav.ts @@ -1,10 +1,10 @@ import type { SettlementBillListScope } from "@/api/admin-agent-settlement"; -export type SettlementPeriodView = "bills" | "ledger"; +export type SettlementPeriodView = "pending" | "bills" | "ledger"; export type SettlementBillScopeFilter = "all" | SettlementBillListScope; -const VALID_VIEWS: SettlementPeriodView[] = ["bills", "ledger"]; +const VALID_VIEWS: SettlementPeriodView[] = ["pending", "bills", "ledger"]; const VALID_BILL_SCOPES: SettlementBillScopeFilter[] = [ "all", @@ -44,7 +44,7 @@ export function settlementCenterScopeHref( export function settlementPeriodViewHref( periodId: number, - view: SettlementPeriodView = "bills", + view: SettlementPeriodView = "pending", adminSiteId?: number | null, billScope?: SettlementBillScopeFilter | null, ): string { @@ -92,7 +92,7 @@ export function parseSettlementCenterView( const view = normalizedView !== null && VALID_VIEWS.includes(normalizedView as SettlementPeriodView) ? (normalizedView as SettlementPeriodView) - : "bills"; + : "pending"; const billScope = scopeRaw !== null && diff --git a/src/modules/settlement/settlement-center-period-detail.tsx b/src/modules/settlement/settlement-center-period-detail.tsx index be2d30c..e8a2044 100644 --- a/src/modules/settlement/settlement-center-period-detail.tsx +++ b/src/modules/settlement/settlement-center-period-detail.tsx @@ -45,13 +45,11 @@ export function SettlementCenterPeriodDetail({ const { t } = useTranslation("settlementCenter"); const subViews: { key: SettlementPeriodView; label: string }[] = [ - { key: "bills", label: t("nav.bills", { defaultValue: "账单" }) }, - { key: "ledger", label: t("nav.ledger", { defaultValue: "账务流水" }) }, + { key: "pending", label: t("nav.pendingWorkbench", { 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 (
+ {view === "pending" ? ( + + ) : null} + {view === "bills" ? ( @@ -104,4 +115,4 @@ export function SettlementCenterPeriodDetail({ ) : null}
); -} \ No newline at end of file +} diff --git a/src/modules/settlement/settlement-center-shell.tsx b/src/modules/settlement/settlement-center-shell.tsx index 644eb23..d2a29e6 100644 --- a/src/modules/settlement/settlement-center-shell.tsx +++ b/src/modules/settlement/settlement-center-shell.tsx @@ -23,7 +23,11 @@ import { import { SettlementPeriodWorkbench } from "@/modules/settlement/settlement-period-workbench"; import { formatAdminSiteLabel } from "@/lib/admin-site-display"; 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 { Dialog, DialogContent, @@ -39,6 +43,30 @@ import { useAdminProfile } from "@/stores/admin-session"; type SiteOption = { id: number; label: string; code: string; currency_code: string }; +function siteOptionsFromProfile(profile: ReturnType): SiteOption[] { + const byId = new Map(); + + 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 { const { t } = useTranslation(["settlementCenter", "common"]); const router = useRouter(); @@ -70,6 +98,9 @@ export function SettlementCenterShell(): React.ReactElement { adminHasAnyPermission(profile?.permissions, [PRD_SETTLEMENT_AGENT_MANAGE]); const canManagePeriods = 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([]); const [sitesReady, setSitesReady] = useState(() => boundAgent?.admin_site_id != null); @@ -120,6 +151,23 @@ export function SettlementCenterShell(): React.ReactElement { 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); setSitesLoadErr(null); try { @@ -148,7 +196,7 @@ export function SettlementCenterShell(): React.ReactElement { } finally { setSitesReady(true); } - }, [boundAgent, siteFromUrl, t]); + }, [boundAgent, canLoadIntegrationSites, profile, siteFromUrl, t]); useEffect(() => { void loadIntegrationSites(); @@ -277,7 +325,7 @@ export function SettlementCenterShell(): React.ReactElement { const activePeriod = 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 scope = preferredBillScopeForPeriod(period?.summary); router.push(settlementPeriodViewHref(periodId, view, siteId, scope)); @@ -384,7 +432,7 @@ export function SettlementCenterShell(): React.ReactElement { canManage={canManagePeriods} periods={periods} headerActions={headerActions} - onViewDetail={(id) => openPeriodView(id, "bills")} + onViewDetail={(id) => openPeriodView(id, "pending")} onReloadPeriods={loadPeriods} onPeriodOpened={() => { setRefreshKey((n) => n + 1); diff --git a/src/modules/settlement/settlement-credit-ledger-panel.tsx b/src/modules/settlement/settlement-credit-ledger-panel.tsx index 107d367..860ea88 100644 --- a/src/modules/settlement/settlement-credit-ledger-panel.tsx +++ b/src/modules/settlement/settlement-credit-ledger-panel.tsx @@ -359,7 +359,7 @@ export function SettlementCreditLedgerPanel({ ) : ( diff --git a/src/modules/settlement/settlement-main-panel.tsx b/src/modules/settlement/settlement-main-panel.tsx index f7fe264..9a9c3ce 100644 --- a/src/modules/settlement/settlement-main-panel.tsx +++ b/src/modules/settlement/settlement-main-panel.tsx @@ -1,6 +1,5 @@ "use client"; -import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; 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 { SettlementBillsTable } from "@/modules/settlement/settlement-bills-table"; import { settlementBillOperableByBoundAgent } from "@/modules/settlement/settlement-bill-operable"; -import { - settlementPeriodViewHref, - type SettlementBillScopeFilter, -} from "@/modules/settlement/settlement-center-nav"; -import { settlementBillStatusLabel } from "@/modules/settlement/settlement-status-label"; +import type { SettlementBillScopeFilter } from "@/modules/settlement/settlement-center-nav"; import { LotteryApiBizError } from "@/types/api/errors"; -import { cn } from "@/lib/utils"; type BillTypeFilter = "all" | "player" | "agent"; +type SettlementMainPanelMode = "workbench" | "all"; type BillFilters = { ownerKeyword: string; @@ -85,11 +80,10 @@ export type SettlementMainPanelProps = { currencyCode: string; periodFilter: AgentSettlementPeriodFilter; billScope?: SettlementBillScopeFilter; + mode?: SettlementMainPanelMode; onOpenBillDetail: (billId: number, focus?: "confirm" | "payment") => void; refreshKey?: number; canOperateBills?: boolean; - pendingConfirm: number; - awaitingPayment: number; selectedPeriodStatus?: string | null; boundAgentId?: number | null; }; @@ -99,11 +93,10 @@ export function SettlementMainPanel({ currencyCode, periodFilter, billScope = "all", + mode = "all", onOpenBillDetail, refreshKey = 0, canOperateBills = false, - pendingConfirm, - awaitingPayment, selectedPeriodStatus, boundAgentId = null, }: SettlementMainPanelProps): React.ReactElement { @@ -112,17 +105,17 @@ export function SettlementMainPanel({ const periodOpen = selectedPeriodStatus === "open"; const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); - const initialFilters = useMemo( - () => filtersForPeriod(boundAgentId, billScope), - [boundAgentId, billScope], - ); + const initialFilters = useMemo(() => { + const base = filtersForPeriod(boundAgentId, billScope); + return mode === "workbench" ? { ...base, statusScope: "all" as const } : base; + }, [boundAgentId, billScope, mode]); const [draft, setDraft] = useState(initialFilters); const [applied, setApplied] = useState(initialFilters); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(20); + const [perPage, setPerPage] = useState(mode === "workbench" ? 100 : 20); const [total, setTotal] = useState(0); useEffect(() => { @@ -141,8 +134,8 @@ export function SettlementMainPanel({ bill_type: q.bill_type, scope: q.scope, keyword: q.keyword, - page, - per_page: perPage, + page: mode === "workbench" ? 1 : page, + per_page: mode === "workbench" ? 100 : perPage, }); setRows(data.items ?? []); setTotal(data.total ?? data.items?.length ?? 0); @@ -157,7 +150,7 @@ export function SettlementMainPanel({ } finally { setLoading(false); } - }, [adminSiteId, applied, page, perPage, periodId, t]); + }, [adminSiteId, applied, mode, page, perPage, periodId, t]); useAsyncEffect(() => { void load(); @@ -174,24 +167,6 @@ export function SettlementMainPanel({ 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 => { if (periodOpen) { 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 => { requestConfirm({ title: t("billsPanel.confirmOneTitle", { defaultValue: "确认账单 #{{id}}?", id: row.id }), @@ -264,32 +224,115 @@ export function SettlementMainPanel({ const canOperateRow = (row: SettlementBillRow): boolean => 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 => ( + 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 ( +
+ + {loading && rows.length === 0 ? ( + + ) : periodOpen ? ( + renderBillsTable([], emptyBillMessage) + ) : !hasWorkbenchRows ? ( + renderBillsTable([], t("billsPanel.workbenchEmpty", { + defaultValue: "本期暂无待处理账单。需要查历史或已结清账单,请进入「全部账单」。", + })) + ) : ( + <> + {pendingConfirmRows.length > 0 ? ( +
+
+

+ {t("billsPanel.workbench.pendingTitle", { defaultValue: "待确认账单" })} +

+

+ {t("billsPanel.workbench.pendingHint", { + defaultValue: "先确认金额与双方,确认后进入待收付。", + })} +

+
+ {renderBillsTable(pendingConfirmRows)} +
+ ) : null} + {awaitingPaymentRows.length > 0 ? ( +
+
+

+ {t("billsPanel.workbench.awaitingTitle", { defaultValue: "待收付账单" })} +

+

+ {t("billsPanel.workbench.awaitingHint", { + defaultValue: "按谁付谁登记线下收付,全部收付后账单结清。", + })} +

+
+ {renderBillsTable(awaitingPaymentRows)} +
+ ) : null} + {exceptionRows.length > 0 ? ( +
+
+

+ {t("billsPanel.workbench.exceptionTitle", { defaultValue: "异常处理" })} +

+

+ {t("billsPanel.workbench.exceptionHint", { + defaultValue: "逾期、调账、冲正与坏账记录集中在这里复核。", + })} +

+
+ {renderBillsTable(exceptionRows)} +
+ ) : null} + + )} +
+ ); + } + return (
- {!periodOpen && periodId != null ? ( -
- {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 ( - - {statusOptionLabel(scope)} - {count != null && count > 0 ? ` ${count}` : null} - - ); - })} -
- ) : null} -