feat(settlement): 优化结算中心待处理视图与账单显示
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
- 新增接口参数 agent_scope 支持筛选直接代理 - 结算中心导航新增“待处理”视图,并设置默认视图为“待处理” - 结算中心页面新增待处理账单工作台,支持待确认、待收付和异常账单分类显示 - 调整账单状态文本,明确“已确认”改为“待收付”,“部分结清”改为“部分收付” - 优化账单明细中的代理结算说明和收益展示文案 - 账单详情页添加当前账单主要操作提示区,提示确认、登记付款或复核状态 - 结算中心流水模块及多语言资源关于“账务流水”统一改为“流水审计” - 优化结算主面板中待处理模式与全部模式的筛选和分页逻辑 - 结合管理员权限控制结算中心可见站点列表,增强集成站点加载逻辑
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -216,7 +216,7 @@ export function SettlementBillAmountBreakdown({
|
||||
})
|
||||
: bill.bill_type === "agent"
|
||||
? t("settlementCenter:billDisplay.agentBreakdownIntro", {
|
||||
defaultValue: "代理只与直属上级结算,净额 = 团队净额 − 下级占成 − 本级占成。",
|
||||
defaultValue: "先看本期团队赢亏,扣除玩家回水和各级保留后,就是本级需要上缴或应收的金额。",
|
||||
})
|
||||
: null;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -359,7 +359,7 @@ export function SettlementCreditLedgerPanel({
|
||||
<AdminTableNoResourceRow
|
||||
colSpan={COL_SPAN}
|
||||
message={t("creditLedger.emptyPeriod", {
|
||||
defaultValue: "本账期暂无账务流水。",
|
||||
defaultValue: "本账期暂无流水审计记录。",
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user