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:
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user