feat(settlement, admin): introduce new types and functions for downline share and settlement period hints

Added new types for downline share breakdown and settlement period open hints to enhance the agent settlement API. Updated the admin console components to support these new features, improving the user experience with better data presentation and interaction. Additionally, refined the date range field to accommodate new calendar markers and hints, ensuring a more intuitive interface for managing settlement periods.
This commit is contained in:
2026-06-12 16:01:42 +08:00
parent 1eb6702c51
commit 24fd7c10bd
50 changed files with 1821 additions and 618 deletions

View File

@@ -14,15 +14,17 @@ import {
type RebateAllocationRow,
type SettlementBillRow,
type SettlementBillPaymentRow,
type DownlineShareBreakdown,
} from "@/api/admin-agent-settlement";
import { AdminLoadingState } from "@/components/admin/admin-loading-state";
import { formatAdminMinorDecimal, parseAdminMajorToMinor, parseSignedAdminMajorToMinor } from "@/lib/money";
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
import {
SettlementBillAmountBreakdown,
SettlementBillPartiesRow,
SettlementBillSummaryHeader,
} from "@/modules/settlement/settlement-bill-breakdown";
import { describeBillPaymentDirection } from "@/modules/settlement/settlement-bill-display";
import { settlementBillOperableByBoundAgent } from "@/modules/settlement/settlement-bill-operable";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -33,6 +35,8 @@ type AgentBillDetailProps = {
billId: number;
currencyCode: string;
canManage?: boolean;
canFinanceAdjustments?: boolean;
boundAgent?: { id: number } | null;
onUpdated?: () => void;
};
@@ -40,12 +44,15 @@ export function AgentBillDetail({
billId,
currencyCode,
canManage = true,
canFinanceAdjustments = false,
boundAgent = null,
onUpdated,
}: AgentBillDetailProps): React.ReactElement {
const { t } = useTranslation(["agents", "settlementCenter", "common"]);
const [bill, setBill] = useState<SettlementBillRow | null>(null);
const [payments, setPayments] = useState<SettlementBillPaymentRow[]>([]);
const [rebateAllocations, setRebateAllocations] = useState<RebateAllocationRow[]>([]);
const [downlineShares, setDownlineShares] = useState<DownlineShareBreakdown | null>(null);
const [loading, setLoading] = useState(true);
const [payAmount, setPayAmount] = useState("");
const [payMethod, setPayMethod] = useState("");
@@ -63,11 +70,12 @@ export function AgentBillDetail({
setBill(data.bill);
setPayments(data.payments ?? []);
setRebateAllocations(data.rebate_allocations ?? []);
setPayAmount(String(data.bill.unpaid_amount ?? 0));
setDownlineShares(data.downline_shares ?? null);
setPayAmount(formatAdminMinorDecimal(data.bill.unpaid_amount ?? 0, currencyCode));
} finally {
setLoading(false);
}
}, [billId]);
}, [billId, currencyCode]);
useEffect(() => {
void load();
@@ -78,6 +86,7 @@ export function AgentBillDetail({
}
const direction = describeBillPaymentDirection(bill, t);
const canOperateBill = canManage && settlementBillOperableByBoundAgent(bill, boundAgent);
const locked = ["confirmed", "partial_paid", "settled", "overdue"].includes(bill.status);
const paymentTitle = direction.ownerOwes
? t("settlementBills.submitReceipt", { defaultValue: "登记收款" })
@@ -86,7 +95,7 @@ export function AgentBillDetail({
? t("settlementBills.submitReceipt", { defaultValue: "确认收款" })
: t("settlementBills.submitPayout", { defaultValue: "确认付款" });
const canWriteOff =
canManage &&
canFinanceAdjustments &&
bill.unpaid_amount > 0 &&
["confirmed", "partial_paid", "overdue"].includes(bill.status) &&
!["adjustment", "reversal", "bad_debt"].includes(bill.bill_type);
@@ -119,14 +128,6 @@ export function AgentBillDetail({
toast.error(err instanceof LotteryApiBizError ? err.message : fallback);
};
const parseWholeAmount = (raw: string): number | null => {
const value = Number(raw);
if (!Number.isFinite(value) || !Number.isInteger(value)) {
return null;
}
return value;
};
const requestConfirmBill = (): void => {
requestConfirm({
title: t("settlementBills.confirmBillTitle", { defaultValue: "确认账单?" }),
@@ -152,9 +153,9 @@ export function AgentBillDetail({
};
const requestPayment = (): void => {
const amount = parseWholeAmount(payAmount);
const amount = parseAdminMajorToMinor(payAmount, currencyCode);
if (amount === null || amount <= 0) {
toast.error(t("settlementBills.paymentAmountInvalid", { defaultValue: "请输入大于 0 的整数金额" }));
toast.error(t("settlementBills.paymentAmountInvalid", { defaultValue: "请输入大于 0 的有效金额" }));
return;
}
if (amount > bill.unpaid_amount) {
@@ -220,10 +221,10 @@ export function AgentBillDetail({
};
const requestAdjustment = (): void => {
const amount = parseWholeAmount(adjustAmount);
const amount = parseSignedAdminMajorToMinor(adjustAmount, currencyCode);
const reason = adjustReason.trim();
if (amount === null || amount === 0) {
toast.error(t("settlementBills.adjustmentAmountInvalid", { defaultValue: "请输入非 0 的整数调整金额" }));
toast.error(t("settlementBills.adjustmentAmountInvalid", { defaultValue: "请输入非 0 的有效金额" }));
return;
}
if (!reason) {
@@ -262,38 +263,38 @@ export function AgentBillDetail({
return (
<>
<ConfirmDialog />
<div className="grid gap-6 md:grid-cols-[minmax(0,1.35fr)_minmax(340px,0.95fr)]">
<div className="space-y-5 text-sm">
<SettlementBillSummaryHeader bill={bill} currencyCode={currencyCode} />
<SettlementBillPartiesRow bill={bill} />
<SettlementBillAmountBreakdown bill={bill} currencyCode={currencyCode} />
<div className="flex flex-col gap-5 text-sm">
<SettlementBillSummaryHeader bill={bill} currencyCode={currencyCode} />
<SettlementBillAmountBreakdown
bill={bill}
currencyCode={currencyCode}
downlineShares={downlineShares}
/>
{payments.length > 0 ? (
<div className="space-y-2 rounded-xl border border-border/70 p-4">
<p className="font-medium">
{t("settlementBills.paymentsHistory", { defaultValue: "收付记录" })}
</p>
<ul className="space-y-1.5 text-muted-foreground">
{payments.map((p) => (
<li key={p.id} className="flex justify-between gap-2">
<span>
{p.method
? `${p.method}`
: t("settlementCenter:billDisplay.payment", { defaultValue: "收付" })}
{p.remark ? ` · ${p.remark}` : ""}
</span>
<span className="shrink-0 tabular-nums">
{formatDashboardMoneyMinor(p.amount, currencyCode)}
</span>
</li>
))}
</ul>
</div>
) : null}
</div>
{payments.length > 0 ? (
<div className="space-y-2 rounded-xl border border-border/70 p-4">
<p className="font-medium">
{t("settlementBills.paymentsHistory", { defaultValue: "收付记录" })}
</p>
<ul className="space-y-1.5 text-muted-foreground">
{payments.map((p) => (
<li key={p.id} className="flex justify-between gap-2">
<span>
{p.method
? `${p.method}`
: t("settlementCenter:billDisplay.payment", { defaultValue: "收付" })}
{p.remark ? ` · ${p.remark}` : ""}
</span>
<span className="shrink-0 tabular-nums">
{formatDashboardMoneyMinor(p.amount, currencyCode)}
</span>
</li>
))}
</ul>
</div>
) : null}
<div className="space-y-5 text-sm">
{rebateAllocations.length > 0 ? (
{rebateAllocations.length > 0 ? (
<div className="space-y-2 rounded-xl border border-border/70 bg-card p-5 shadow-sm">
<p className="font-semibold tracking-tight">
{t("settlementBills.rebateAllocations", { defaultValue: "回水分摊" })}
@@ -353,30 +354,30 @@ export function AgentBillDetail({
</div>
) : null}
{canManage && bill.status === "pending_confirm" ? (
<div className="space-y-4 rounded-xl border border-border/70 bg-primary/5 p-5 shadow-sm">
<div className="space-y-1">
<p className="font-semibold tracking-tight text-primary">
{t("settlementBills.confirm", { defaultValue: "确认账单" })}
</p>
<p className="text-xs text-muted-foreground/80">
{t("settlementCenter:billDisplay.confirmHint", {
defaultValue: "确认后才可以登记收款或付款。",
})}
</p>
</div>
<Button
type="button"
className="w-full"
disabled={confirmBusy}
onClick={requestConfirmBill}
>
{canOperateBill && bill.status === "pending_confirm" ? (
<div className="space-y-3 rounded-xl border border-primary/20 bg-primary/5 p-5 shadow-sm">
<div className="space-y-1">
<p className="font-semibold tracking-tight text-primary">
{t("settlementBills.confirm", { defaultValue: "确认账单" })}
</Button>
</p>
<p className="text-xs text-muted-foreground/80">
{t("settlementCenter:billDisplay.confirmHint", {
defaultValue: "确认后才可以登记收款或付款。",
})}
</p>
</div>
) : null}
<Button
type="button"
className="w-full sm:w-auto sm:min-w-[10rem]"
disabled={confirmBusy}
onClick={requestConfirmBill}
>
{t("settlementBills.confirm", { defaultValue: "确认账单" })}
</Button>
</div>
) : null}
{canManage && ["confirmed", "partial_paid", "overdue"].includes(bill.status) && bill.unpaid_amount > 0 ? (
{canOperateBill && ["confirmed", "partial_paid", "overdue"].includes(bill.status) && bill.unpaid_amount > 0 ? (
<div className="space-y-4 rounded-xl border border-border/70 bg-card p-5 shadow-sm">
<div className="space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
@@ -398,7 +399,8 @@ export function AgentBillDetail({
<Input
value={payAmount}
onChange={(e) => setPayAmount(e.target.value)}
placeholder={String(bill.unpaid_amount)}
inputMode="decimal"
placeholder={formatAdminMinorDecimal(bill.unpaid_amount, currencyCode)}
className="bg-background/50 transition-colors focus:bg-background"
/>
</div>
@@ -436,7 +438,7 @@ export function AgentBillDetail({
</div>
) : null}
{canWriteOff ? (
{canWriteOff ? (
<div className="space-y-4 rounded-xl border border-destructive/20 bg-destructive/5 p-5 shadow-sm">
<div className="space-y-1">
<p className="font-semibold tracking-tight text-destructive">
@@ -471,7 +473,7 @@ export function AgentBillDetail({
</div>
) : null}
{canManage && locked ? (
{canFinanceAdjustments && locked ? (
<div className="space-y-4 rounded-xl border border-dashed border-border/70 bg-card p-5 shadow-sm">
<div className="space-y-1">
<p className="font-semibold tracking-tight">
@@ -488,9 +490,9 @@ export function AgentBillDetail({
<Input
value={adjustAmount}
onChange={(e) => setAdjustAmount(e.target.value)}
type="number"
inputMode="decimal"
placeholder={t("settlementBills.adjustmentAmountPlaceholder", {
defaultValue: "输入正数或负数",
defaultValue: "例如35.20 或 -10.00",
})}
className="bg-background/50 transition-colors focus:bg-background"
/>
@@ -517,7 +519,6 @@ export function AgentBillDetail({
</Button>
</div>
) : null}
</div>
</div>
</>
);