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.
229 lines
8.1 KiB
TypeScript
229 lines
8.1 KiB
TypeScript
"use client";
|
||
|
||
import { ArrowRight } from "lucide-react";
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
import type { SettlementBillRow } from "@/api/admin-agent-settlement";
|
||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||
import { cn } from "@/lib/utils";
|
||
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||
import {
|
||
buildBillAmountBreakdown,
|
||
describeBillPaymentDirection,
|
||
type BillBreakdownLine,
|
||
type DownlineShareBreakdown,
|
||
} from "@/modules/settlement/settlement-bill-display";
|
||
import {
|
||
formatSignedSettlementMoney,
|
||
signedSettlementMoneyClass,
|
||
} from "@/modules/settlement/settlement-signed-money";
|
||
import {
|
||
settlementBillStatusLabel,
|
||
settlementBillTypeLabel,
|
||
} from "@/modules/settlement/settlement-status-label";
|
||
|
||
type SettlementBillSummaryHeaderProps = {
|
||
bill: SettlementBillRow;
|
||
currencyCode: string;
|
||
};
|
||
|
||
export function SettlementBillSummaryHeader({
|
||
bill,
|
||
currencyCode,
|
||
}: SettlementBillSummaryHeaderProps): React.ReactElement {
|
||
const { t } = useTranslation(["settlementCenter", "agents"]);
|
||
const direction = describeBillPaymentDirection(bill, t);
|
||
const unpaid = bill.unpaid_amount > 0;
|
||
|
||
return (
|
||
<div className="space-y-4 rounded-xl border border-border/70 bg-card p-5 shadow-sm">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<AdminStatusBadge status={bill.status}>
|
||
{settlementBillStatusLabel(bill.status, t)}
|
||
</AdminStatusBadge>
|
||
<span className="text-sm text-muted-foreground">
|
||
{settlementBillTypeLabel(bill.bill_type, t)}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||
<div className="min-w-0 space-y-3">
|
||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
||
<span className="text-muted-foreground">
|
||
{t("settlementCenter:billDisplay.payerLabel", { defaultValue: "付款方" })}
|
||
</span>
|
||
<span className="font-semibold text-foreground">{direction.payer}</span>
|
||
<ArrowRight className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
||
<span className="text-muted-foreground">
|
||
{t("settlementCenter:billDisplay.payeeLabel", { defaultValue: "收款方" })}
|
||
</span>
|
||
<span className="font-semibold text-foreground">{direction.payee}</span>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("settlementCenter:billDisplay.settlementAmount", { defaultValue: "结算金额" })}
|
||
</p>
|
||
<p className="mt-0.5 text-2xl font-bold tabular-nums tracking-tight text-foreground">
|
||
{formatDashboardMoneyMinor(direction.amount, currencyCode)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid w-full gap-2 sm:grid-cols-2 lg:w-auto lg:min-w-[15rem]">
|
||
<div className="rounded-lg border border-border/50 bg-background/50 px-4 py-3">
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("settlementCenter:columns.paid", { defaultValue: "已收付" })}
|
||
</p>
|
||
<p className="mt-0.5 font-medium tabular-nums">
|
||
{formatDashboardMoneyMinor(bill.paid_amount ?? 0, currencyCode)}
|
||
</p>
|
||
</div>
|
||
<div
|
||
className={cn(
|
||
"rounded-lg border px-4 py-3",
|
||
unpaid
|
||
? "border-amber-200/80 bg-amber-50/80 dark:border-amber-900/50 dark:bg-amber-950/20"
|
||
: "border-border/50 bg-background/50",
|
||
)}
|
||
>
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("settlementCenter:columns.unpaid", { defaultValue: "未结" })}
|
||
</p>
|
||
<p
|
||
className={cn(
|
||
"mt-0.5 font-semibold tabular-nums",
|
||
unpaid && "text-amber-900 dark:text-amber-200",
|
||
)}
|
||
>
|
||
{formatDashboardMoneyMinor(bill.unpaid_amount, currencyCode)}
|
||
</p>
|
||
{unpaid ? (
|
||
<p className="mt-1 text-xs text-muted-foreground">
|
||
{bill.status === "pending_confirm"
|
||
? t("settlementCenter:billDisplay.unpaidPendingConfirm", {
|
||
defaultValue: "确认账单后可登记收付",
|
||
})
|
||
: t("settlementCenter:billDisplay.unpaidAwaitingPayment", {
|
||
defaultValue: "请登记线下收付",
|
||
})}
|
||
</p>
|
||
) : (
|
||
<p className="mt-1 text-xs text-emerald-700 dark:text-emerald-400">
|
||
{t("settlementCenter:billDisplay.fullySettled", { defaultValue: "本期已结清" })}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
type SettlementBillAmountBreakdownProps = {
|
||
bill: SettlementBillRow;
|
||
currencyCode: string;
|
||
downlineShares?: DownlineShareBreakdown | null;
|
||
};
|
||
|
||
function BreakdownAmountLine({
|
||
line,
|
||
currencyCode,
|
||
nested = false,
|
||
}: {
|
||
line: BillBreakdownLine;
|
||
currencyCode: string;
|
||
nested?: boolean;
|
||
}): React.ReactElement {
|
||
const prefix =
|
||
line.kind === "subtract"
|
||
? "−"
|
||
: line.kind === "add" && line.key !== "gross"
|
||
? "+"
|
||
: line.kind === "subtotal" || line.kind === "total"
|
||
? "="
|
||
: "";
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
className={cn(
|
||
"grid grid-cols-[minmax(0,1fr)_7.5rem] items-start gap-x-4 py-1.5 text-sm",
|
||
nested && "py-1 pl-4",
|
||
!nested && (line.kind === "subtotal" || line.kind === "total") &&
|
||
"border-t border-border/60 pt-2.5 font-medium",
|
||
!nested && line.kind === "total" && "text-base",
|
||
)}
|
||
>
|
||
<div className="min-w-0">
|
||
<span
|
||
className={cn(
|
||
nested ? "text-muted-foreground/90" : line.kind === "total" ? "text-foreground" : "text-muted-foreground",
|
||
)}
|
||
>
|
||
{prefix ? <span className="mr-1.5 tabular-nums">{prefix}</span> : null}
|
||
{line.label}
|
||
</span>
|
||
{line.hint ? (
|
||
<p className="mt-0.5 text-xs leading-relaxed text-muted-foreground/80">{line.hint}</p>
|
||
) : null}
|
||
</div>
|
||
<span
|
||
className={cn(
|
||
"text-right tabular-nums",
|
||
signedSettlementMoneyClass(line.signedAmount, line.kind === "total"),
|
||
)}
|
||
>
|
||
{formatSignedSettlementMoney(line.signedAmount, currencyCode)}
|
||
</span>
|
||
</div>
|
||
{line.children?.map((child) => (
|
||
<BreakdownAmountLine key={child.key} line={child} currencyCode={currencyCode} nested />
|
||
))}
|
||
</>
|
||
);
|
||
}
|
||
|
||
export function SettlementBillAmountBreakdown({
|
||
bill,
|
||
currencyCode,
|
||
downlineShares = null,
|
||
}: SettlementBillAmountBreakdownProps): React.ReactElement | null {
|
||
const { t } = useTranslation(["settlementCenter", "agents"]);
|
||
const lines = buildBillAmountBreakdown(bill, t, downlineShares);
|
||
|
||
if (lines.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const breakdownIntro =
|
||
bill.bill_type === "player"
|
||
? t("settlementCenter:billDisplay.playerBreakdownIntro", {
|
||
defaultValue: "玩家只与直属代理结算,净额 = 输赢 − 回水。",
|
||
})
|
||
: bill.bill_type === "agent"
|
||
? t("settlementCenter:billDisplay.agentBreakdownIntro", {
|
||
defaultValue: "代理只与直属上级结算,净额 = 团队净额 − 下级占成 − 本级占成。",
|
||
})
|
||
: null;
|
||
|
||
return (
|
||
<div className="space-y-4 rounded-xl border border-border/70 bg-card p-5 shadow-sm">
|
||
<div className="space-y-1">
|
||
<p className="font-semibold tracking-tight text-foreground">
|
||
{t("settlementCenter:billDisplay.howAmountWorks", { defaultValue: "结算明细" })}
|
||
</p>
|
||
{breakdownIntro ? (
|
||
<p className="text-xs leading-relaxed text-muted-foreground">{breakdownIntro}</p>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-border/50 bg-muted/15 p-4">
|
||
{lines.map((line) => (
|
||
<BreakdownAmountLine key={line.key} line={line} currencyCode={currencyCode} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|