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

@@ -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,
})

View File

@@ -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);

View File

@@ -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<Record<string, { key: string; label: string; amount: number; rows: number }>>(
(acc, row) => {
@@ -337,6 +381,22 @@ export function AgentBillDetail({
<>
<ConfirmDialog />
<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} />
<SettlementBillAmountBreakdown
bill={bill}

View File

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

View File

@@ -271,19 +271,19 @@ export function buildBillAmountBreakdown(
if (bill.gross_win_loss != null) {
lines.push({
key: "gross",
label: t("settlementCenter:billDisplay.teamGross", { defaultValue: "团队游戏输赢" }),
label: t("settlementCenter:billDisplay.teamGross", { defaultValue: "本期团队赢亏" }),
amount: Math.abs(gross),
signedAmount: gross,
kind: "add",
hint: t("settlementCenter:billDisplay.teamGrossHint", {
defaultValue: "本级及下级玩家合计",
defaultValue: "本级及下级玩家本期合计",
}),
});
}
if (bill.rebate_amount != null && rebate !== 0) {
lines.push({
key: "rebate",
label: t("settlementCenter:billDisplay.teamRebate", { defaultValue: "团队回水" }),
label: t("settlementCenter:billDisplay.teamRebate", { defaultValue: "扣玩家回水" }),
amount: Math.abs(rebate),
signedAmount: -Math.abs(rebate),
kind: "subtract",
@@ -292,7 +292,7 @@ export function buildBillAmountBreakdown(
if (bill.gross_win_loss != null || bill.rebate_amount != null) {
lines.push({
key: "team-net",
label: t("settlementCenter:billDisplay.teamNet", { defaultValue: "团队净额" }),
label: t("settlementCenter:billDisplay.teamNet", { defaultValue: "可分配利润" }),
amount: Math.abs(teamNet),
signedAmount: teamNet,
kind: "subtotal",
@@ -301,14 +301,14 @@ export function buildBillAmountBreakdown(
if (downlineShares && downlineShares.total > 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",

View File

@@ -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 &&

View File

@@ -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 (
<div className="flex flex-col gap-4">
<SettlementPeriodHeader
@@ -76,6 +74,21 @@ export function SettlementCenterPeriodDetail({
))}
</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" ? (
<SettlementMainPanel
key={`${adminSiteId}-${period.id}-${refreshKey}-${billScope}`}
@@ -86,8 +99,6 @@ export function SettlementCenterPeriodDetail({
onOpenBillDetail={onOpenBillDetail}
refreshKey={refreshKey}
canOperateBills={canOperateBills}
pendingConfirm={pendingConfirm}
awaitingPayment={awaitingPayment}
selectedPeriodStatus={period.status}
boundAgentId={boundAgentId}
/>
@@ -104,4 +115,4 @@ export function SettlementCenterPeriodDetail({
) : null}
</div>
);
}
}

View File

@@ -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<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 {
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<SiteOption[]>([]);
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);

View File

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

View File

@@ -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<BillFilters>(initialFilters);
const [applied, setApplied] = useState<BillFilters>(initialFilters);
const [rows, setRows] = useState<SettlementBillRow[]>([]);
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 => (
<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 (
<div className="space-y-4">
<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-field min-w-[12rem] flex-1">
<Label htmlFor="sb-owner" className="sm:shrink-0">
@@ -352,30 +395,7 @@ export function SettlementMainPanel({
<AdminLoadingState />
) : (
<>
<SettlementBillsTable
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");
}
}}
/>
{renderBillsTable(rows, emptyBillMessage)}
<AdminListPaginationFooter
selectId="settlement-bills-per-page"
page={page}
@@ -393,4 +413,4 @@ export function SettlementMainPanel({
)}
</div>
);
}
}