feat(api, agents, i18n): enhance settlement features and multi-language support

Added new types and API functions for settlement period summaries and credit ledgers, improving the management of agent settlements. Updated the admin console to reflect these changes, enhancing user experience with better navigation and data presentation. Additionally, expanded multi-language support by incorporating new translations in English, Nepali, and Chinese for settlement-related terms, ensuring consistency across the platform.
This commit is contained in:
2026-06-05 18:00:59 +08:00
parent 65eaeecf8c
commit af982bb9f7
73 changed files with 4307 additions and 2494 deletions

View File

@@ -0,0 +1,196 @@
"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,
resolveBillPartyName,
} from "@/modules/settlement/settlement-bill-display";
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-muted/15 p-4">
<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-wrap items-center gap-2 text-base">
<span className="font-semibold text-foreground">{direction.payer}</span>
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">
{t("settlementCenter:billDisplay.pays", { defaultValue: "应付" })}
<ArrowRight className="size-3.5" aria-hidden />
</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-1 text-2xl font-bold tabular-nums tracking-tight text-foreground">
{formatDashboardMoneyMinor(direction.amount, currencyCode)}
</p>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div className="rounded-lg border border-border/50 bg-background/80 px-3 py-2">
<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-3 py-2",
unpaid
? "border-amber-200/80 bg-amber-50/80 dark:border-amber-900/50 dark:bg-amber-950/20"
: "border-border/50 bg-background/80",
)}
>
<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>
);
}
type SettlementBillAmountBreakdownProps = {
bill: SettlementBillRow;
currencyCode: string;
};
export function SettlementBillAmountBreakdown({
bill,
currencyCode,
}: SettlementBillAmountBreakdownProps): React.ReactElement | null {
const { t } = useTranslation(["settlementCenter", "agents"]);
const lines = buildBillAmountBreakdown(bill, t);
if (lines.length === 0) {
return null;
}
return (
<div className="space-y-3 rounded-xl border border-border/70 p-4">
<p className="font-medium text-foreground">
{t("settlementCenter:billDisplay.howAmountWorks", { defaultValue: "金额怎么来的" })}
</p>
<div className="space-y-2">
{lines.map((line) => {
const prefix =
line.kind === "subtract"
? ""
: line.kind === "add" && lines.indexOf(line) > 0
? "+"
: line.kind === "subtotal" || line.kind === "total"
? "="
: "";
return (
<div
key={line.key}
className={cn(
"flex items-start justify-between gap-3 text-sm",
(line.kind === "subtotal" || line.kind === "total") &&
"border-t border-border/60 pt-2 font-medium",
line.kind === "total" && "text-base",
)}
>
<div className="min-w-0">
<span className="text-muted-foreground">
{prefix ? <span className="mr-1.5 tabular-nums">{prefix}</span> : null}
{line.label}
</span>
</div>
<span className="shrink-0 tabular-nums">
{formatDashboardMoneyMinor(line.amount, currencyCode)}
</span>
</div>
);
})}
</div>
</div>
);
}
type SettlementBillPartiesRowProps = {
bill: SettlementBillRow;
};
export function SettlementBillPartiesRow({ bill }: SettlementBillPartiesRowProps): React.ReactElement {
const { t } = useTranslation(["settlementCenter", "agents"]);
const owner = resolveBillPartyName(bill, "owner", t);
const counterparty = resolveBillPartyName(bill, "counterparty", t);
return (
<div className="grid gap-2 text-sm sm:grid-cols-2">
<div className="rounded-lg border border-border/60 bg-muted/15 px-3 py-2">
<p className="text-xs text-muted-foreground">
{t("settlementCenter:billDisplay.billOwner", { defaultValue: "账单主体" })}
</p>
<p className="mt-1 font-medium text-foreground">{owner}</p>
</div>
<div className="rounded-lg border border-border/60 bg-muted/15 px-3 py-2">
<p className="text-xs text-muted-foreground">
{t("settlementCenter:billDisplay.billCounterparty", { defaultValue: "结算对手" })}
</p>
<p className="mt-1 font-medium text-foreground">{counterparty}</p>
</div>
</div>
);
}