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

@@ -56,7 +56,7 @@ import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { PlayerFundingModeBadge } from "@/components/admin/player-funding-badges";
import { formatPlayerCreditAmount, playerBalanceCells } from "@/lib/admin-player-display";
import { formatAdminMinorUnits } from "@/lib/money";
import { formatAdminMinorDecimal, formatAdminMinorUnits, parseAdminMajorToMinor } from "@/lib/money";
import { parsePercentUi, percentValueToUi } from "@/lib/admin-rate-percent";
import { adminPlayerDetailPath } from "@/lib/admin-player-paths";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
@@ -503,6 +503,8 @@ export function AgentsPlayersPanel({
() => billingBills.find((bill) => bill.id === selectedBillId) ?? null,
[billingBills, selectedBillId],
);
const billingCurrency =
selectedBill?.currency_code ?? billingPlayer?.default_currency ?? "NPR";
const projectedCreditLimit = useMemo(() => {
const delta = editCreditDelta.trim() === "" ? 0 : Number.parseInt(editCreditDelta, 10);
if (Number.isNaN(delta) || delta <= 0) {
@@ -540,7 +542,9 @@ export function AgentsPlayersPanel({
setBillingBills(items);
const first = items[0] ?? null;
setSelectedBillId(first?.id ?? null);
setPayAmount(first ? String(first.unpaid_amount ?? 0) : "");
setPayAmount(
first ? formatAdminMinorDecimal(first.unpaid_amount ?? 0, row.default_currency ?? "NPR") : "",
);
} catch (e) {
toast.error(
e instanceof LotteryApiBizError
@@ -576,7 +580,11 @@ export function AgentsPlayersPanel({
async function handlePayBill(): Promise<void> {
if (selectedBill === null) return;
const amount = parseBillingAmount(payAmount || String(selectedBill.unpaid_amount ?? 0));
const fallbackAmount = formatAdminMinorDecimal(
selectedBill.unpaid_amount ?? 0,
billingCurrency,
);
const amount = parseAdminMajorToMinor(payAmount || fallbackAmount, billingCurrency);
if (amount === null || amount <= 0 || amount > Number(selectedBill.unpaid_amount ?? 0)) {
toast.error(t("playersPanel.paymentAmountInvalid", { defaultValue: "请输入有效的收付金额" }));
return;
@@ -632,14 +640,6 @@ export function AgentsPlayersPanel({
}
}
function parseBillingAmount(raw: string): number | null {
const value = Number(raw);
if (!Number.isFinite(value) || !Number.isInteger(value)) {
return null;
}
return value;
}
function requestConfirmBillAction(): void {
if (selectedBill === null) return;
requestConfirm({
@@ -655,9 +655,13 @@ export function AgentsPlayersPanel({
function requestPayBillAction(): void {
if (selectedBill === null) return;
const amount = parseBillingAmount(payAmount || String(selectedBill.unpaid_amount ?? 0));
const fallbackAmount = formatAdminMinorDecimal(
selectedBill.unpaid_amount ?? 0,
billingCurrency,
);
const amount = parseAdminMajorToMinor(payAmount || fallbackAmount, billingCurrency);
if (amount === null || amount <= 0) {
toast.error(t("playersPanel.paymentAmountInvalid", { defaultValue: "请输入大于 0 的整数金额" }));
toast.error(t("playersPanel.paymentAmountInvalid", { defaultValue: "请输入大于 0 的有效金额" }));
return;
}
if (amount > Number(selectedBill.unpaid_amount ?? 0)) {
@@ -1044,7 +1048,11 @@ export function AgentsPlayersPanel({
onValueChange={(value) => {
const next = billingBills.find((bill) => bill.id === Number(value)) ?? null;
setSelectedBillId(next?.id ?? null);
setPayAmount(next ? String(next.unpaid_amount ?? 0) : "");
setPayAmount(
next
? formatAdminMinorDecimal(next.unpaid_amount ?? 0, billingCurrency)
: "",
);
setPayMethod("");
setPayProof("");
setBadDebtReason("");
@@ -1056,7 +1064,7 @@ export function AgentsPlayersPanel({
<SelectContent>
{billingBills.map((bill) => (
<SelectItem key={bill.id} value={String(bill.id)}>
{`#${bill.id} · ${bill.status} · ${bill.player_site_player_id ?? bill.owner_id} · ${bill.unpaid_amount ?? 0}`}
{`#${bill.id} · ${bill.status} · ${bill.player_site_player_id ?? bill.owner_id} · ${formatAdminMinorUnits(bill.unpaid_amount ?? 0, bill.currency_code ?? billingPlayer?.default_currency ?? "NPR")}`}
</SelectItem>
))}
</SelectContent>
@@ -1076,7 +1084,7 @@ export function AgentsPlayersPanel({
<span className="text-muted-foreground">
{t("playersPanel.billUnpaid", { defaultValue: "未结" })}:
</span>{" "}
{selectedBill.unpaid_amount ?? 0}
{formatAdminMinorUnits(selectedBill.unpaid_amount ?? 0, billingCurrency)}
</div>
</div>
@@ -1090,7 +1098,15 @@ export function AgentsPlayersPanel({
<div className="space-y-3">
<div className="space-y-1">
<Label>{t("agents:settlementBills.paymentAmount", { defaultValue: "收付金额" })}</Label>
<Input value={payAmount} onChange={(e) => setPayAmount(e.target.value)} />
<Input
value={payAmount}
onChange={(e) => setPayAmount(e.target.value)}
inputMode="decimal"
placeholder={formatAdminMinorDecimal(
selectedBill.unpaid_amount ?? 0,
billingCurrency,
)}
/>
</div>
<div className="space-y-1">
<Label>{t("agents:settlementBills.paymentMethod", { defaultValue: "收付方式" })}</Label>
@@ -1116,6 +1132,8 @@ export function AgentsPlayersPanel({
{t("agents:settlementBills.paid", { defaultValue: "登记收付" })}
</Button>
{boundAgent === null ? (
<>
<div className="space-y-1 pt-2">
<Label>{t("agents:settlementBills.badDebtReason", { defaultValue: "核销原因" })}</Label>
<Input
@@ -1129,6 +1147,8 @@ export function AgentsPlayersPanel({
<Button type="button" variant="destructive" className="w-full" disabled={billingBusy || confirmBusy} onClick={requestWriteOffBillAction}>
{t("agents:settlementBills.confirmBadDebt", { defaultValue: "确认核销" })}
</Button>
</>
) : null}
</div>
) : null}
</div>