feat(agents, i18n): enhance agent management and settlement features with new translations and UI updates

Added new translations for agent management and settlement features in English, Nepali, and Chinese, improving multi-language support. Updated the agents console to reflect changes in funding modes and player details, enhancing user experience. Refactored the admin permission gate to include new logic for handling bound line agents, ensuring better permission management. Additionally, streamlined the UI for agent-related pages and improved navigation to the settlement center, consolidating related functionalities for better accessibility.
This commit is contained in:
2026-06-04 18:01:05 +08:00
parent c2eac2fafc
commit 65eaeecf8c
139 changed files with 8852 additions and 1435 deletions

View File

@@ -0,0 +1,439 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { CalendarClock, CircleDollarSign, ClipboardCheck, Landmark } from "lucide-react";
import { toast } from "sonner";
import { getSettlementPeriods, type SettlementPeriodRow } from "@/api/admin-agent-settlement";
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
import { AdminPageCard } from "@/components/admin/admin-page-card";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AgentBillDetail } from "@/modules/settlement/agent-bill-detail";
import { AgentPeriodsConsole } from "@/modules/settlement/agent-periods-console";
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
import type { AgentSettlementPeriodFilter } from "@/modules/settlement/agent-settlement-period-select";
import {
SettlementCenterNav,
type SettlementCenterSection,
} from "@/modules/settlement/settlement-center-nav";
import {
SettlementBillsPanel,
type BillCategory,
} from "@/modules/settlement/settlement-bills-panel";
import { SettlementLedgerPanel } from "@/modules/settlement/settlement-ledger-panel";
import { SettlementPeriodToolbar } from "@/modules/settlement/settlement-period-toolbar";
import { formatSettlementPeriodSpan } from "@/lib/agent-settlement-period-range";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { PRD_SETTLEMENT_AGENT_MANAGE } from "@/lib/admin-prd";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useAdminProfile } from "@/stores/admin-session";
type SiteOption = { id: number; label: string; currency_code: string };
function pickDefaultPeriodId(periods: SettlementPeriodRow[]): number | "all" {
const closed = periods
.filter((row) => row.status === "closed" || row.status === "completed")
.sort((a, b) => b.id - a.id);
if (closed[0]) {
return closed[0].id;
}
const open = periods.filter((row) => row.status === "open").sort((a, b) => b.id - a.id);
if (open[0]) {
return open[0].id;
}
return "all";
}
function sectionTitle(
section: SettlementCenterSection,
t: ReturnType<typeof useTranslation<["settlementCenter", "agents", "common"]>>["t"],
): string {
switch (section) {
case "overview":
return t("panels.overview.title", { defaultValue: "结算概览" });
case "periods":
return t("nav.periods", { defaultValue: "账期管理" });
case "ledger":
return t("panels.ledger.title", { defaultValue: "账务流水" });
case "bills":
return t("panels.bills.title", { defaultValue: "账单" });
default:
return "";
}
}
export function SettlementCenterShell(): React.ReactElement {
const { t } = useTranslation(["settlementCenter", "agents", "common"]);
const profile = useAdminProfile();
const boundAgent = profile?.agent ?? null;
const canManagePeriods =
profile?.is_super_admin === true ||
adminHasAnyPermission(profile?.permissions, [PRD_SETTLEMENT_AGENT_MANAGE]);
const [activeSection, setActiveSection] = useState<SettlementCenterSection>("overview");
const [siteOptions, setSiteOptions] = useState<SiteOption[]>([]);
const [adminSiteId, setAdminSiteId] = useState<number | null>(null);
const [periods, setPeriods] = useState<SettlementPeriodRow[]>([]);
const [periodFilter, setPeriodFilter] = useState<AgentSettlementPeriodFilter>("all");
const [periodFilterReady, setPeriodFilterReady] = useState(false);
const [detailBillId, setDetailBillId] = useState<number | null>(null);
const [billsInitialCategory, setBillsInitialCategory] = useState<BillCategory>("all");
const [listRevision, setListRevision] = useState(0);
useEffect(() => {
if (boundAgent?.admin_site_id) {
const label = boundAgent.name
? `${boundAgent.name} (${boundAgent.site_code || boundAgent.code})`
: boundAgent.code;
setSiteOptions([{ id: boundAgent.admin_site_id, label, currency_code: "NPR" }]);
setAdminSiteId(boundAgent.admin_site_id);
return;
}
void getAdminIntegrationSites().then((sites) => {
const options = (sites.items ?? []).map((site) => ({
id: site.id,
label: site.name ? `${site.name} (${site.code})` : site.code,
currency_code: site.currency_code ?? "NPR",
}));
setSiteOptions(options);
if (adminSiteId === null && options[0]) {
setAdminSiteId(options[0].id);
}
});
}, [adminSiteId, boundAgent]);
const loadPeriods = useCallback(async () => {
if (adminSiteId === null) {
setPeriods([]);
return;
}
try {
const data = await getSettlementPeriods({ admin_site_id: adminSiteId });
setPeriods(data.items ?? []);
} catch {
setPeriods([]);
toast.error(t("periods.loadFailed", { defaultValue: "账期列表加载失败" }));
}
}, [adminSiteId, t]);
useEffect(() => {
if (canManagePeriods || adminSiteId === null) {
return;
}
void loadPeriods();
}, [adminSiteId, canManagePeriods, loadPeriods]);
const handlePeriodsChange = useCallback((items: SettlementPeriodRow[]) => {
setPeriods(items);
}, []);
useEffect(() => {
if (periodFilterReady || adminSiteId === null) {
return;
}
setPeriodFilter(periods.length === 0 ? "all" : pickDefaultPeriodId(periods));
setPeriodFilterReady(true);
}, [adminSiteId, periodFilterReady, periods]);
const activeCurrency =
siteOptions.find((site) => site.id === adminSiteId)?.currency_code ?? "NPR";
const openPeriod = useMemo(
() => periods.filter((row) => row.status === "open").sort((a, b) => b.id - a.id)[0] ?? null,
[periods],
);
const summaryTotals = useMemo(
() =>
periods.reduce(
(acc, row) => {
acc.pendingConfirm += row.summary?.pending_confirm ?? 0;
acc.awaitingPayment += row.summary?.awaiting_payment ?? 0;
acc.totalUnpaid += row.summary?.total_unpaid ?? 0;
return acc;
},
{ pendingConfirm: 0, awaitingPayment: 0, totalUnpaid: 0 },
),
[periods],
);
const handlePeriodClosed = useCallback(
(result?: { unsettled_ticket_count?: number }) => {
void loadPeriods();
setActiveSection("bills");
setBillsInitialCategory("pending_confirm");
setListRevision((n) => n + 1);
const unsettled = result?.unsettled_ticket_count ?? 0;
if (unsettled > 0) {
toast.warning(
t("toast.periodClosedUnsettled", {
defaultValue: "账期已关账;仍有 {{count}} 笔注单未结算,请尽快处理。",
count: unsettled,
}),
);
} else {
toast.success(t("toast.periodClosed", { defaultValue: "账期已关账" }));
}
},
[loadPeriods, t],
);
const selectSiteId = adminSiteId ?? siteOptions[0]?.id ?? null;
const selectedSiteLabel = siteOptions.find((site) => site.id === selectSiteId)?.label ?? null;
const panelTitle = sectionTitle(activeSection, t);
const allPeriodsCompleted =
periods.length > 0 && periods.every((row) => row.status === "completed");
const showPeriodToolbar =
(activeSection === "ledger" || activeSection === "bills") && periods.length > 0;
const selectedPeriod =
periodFilter !== "all" ? (periods.find((row) => row.id === periodFilter) ?? null) : openPeriod;
const pipelineCounts = selectedPeriod?.pipeline ?? {
credit_ledger_count: 0,
share_ledger_count: 0,
};
const overviewStats = [
{
label: t("overview.pendingConfirm", { defaultValue: "待确认" }),
value: String(summaryTotals.pendingConfirm),
icon: ClipboardCheck,
},
{
label: t("overview.awaitingPayment", { defaultValue: "待收付" }),
value: String(summaryTotals.awaitingPayment),
icon: CircleDollarSign,
},
{
label: t("overview.totalUnpaid", { defaultValue: "未结合计" }),
value: formatDashboardMoneyMinor(summaryTotals.totalUnpaid, activeCurrency),
icon: Landmark,
},
{
label: t("overview.openPeriod", { defaultValue: "进行中账期" }),
value: openPeriod
? formatSettlementPeriodSpan(openPeriod.period_start, openPeriod.period_end)
: "—",
icon: CalendarClock,
},
{
label: t("overview.creditLedger", { defaultValue: "信用流水(账期内)" }),
value: String(pipelineCounts.credit_ledger_count),
icon: CalendarClock,
},
{
label: t("overview.shareLedger", { defaultValue: "占成流水(账期内)" }),
value: String(pipelineCounts.share_ledger_count),
icon: CalendarClock,
},
];
function renderMainPanel(): React.ReactElement {
if (activeSection === "overview") {
return (
<div className="space-y-5">
<p className="text-sm text-muted-foreground">
{t("overview.pipelineHint", {
defaultValue: "账单须关账后生成;下方为账期内实时流水笔数。",
})}
</p>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
{overviewStats.map((stat) => {
const Icon = stat.icon;
return (
<button
key={stat.label}
type="button"
className="rounded-xl border border-border/70 bg-card px-4 py-4 text-left transition-colors hover:border-primary/30 hover:bg-muted/30"
onClick={() => {
if (stat.label === t("overview.pendingConfirm", { defaultValue: "待确认" })) {
setBillsInitialCategory("pending_confirm");
setActiveSection("bills");
} else if (
stat.label === t("overview.awaitingPayment", { defaultValue: "待收付" })
) {
setBillsInitialCategory("awaiting_payment");
setActiveSection("bills");
} else if (
stat.label === t("overview.creditLedger", { defaultValue: "信用流水(账期内)" })
) {
setActiveSection("ledger");
}
}}
>
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-muted-foreground">{stat.label}</p>
<Icon className="size-4 text-muted-foreground" />
</div>
<p className="mt-2 text-base font-semibold tabular-nums">{stat.value}</p>
</button>
);
})}
</div>
</div>
);
}
if (activeSection === "periods" && adminSiteId !== null) {
return (
<AgentPeriodsConsole
adminSiteId={adminSiteId}
canManagePeriods={canManagePeriods}
settlementCycle="weekly"
siteCurrencyCode={activeCurrency}
embedded
onPeriodsChange={handlePeriodsChange}
onPeriodClosed={handlePeriodClosed}
/>
);
}
if (activeSection === "ledger" && adminSiteId !== null && periodFilterReady) {
return (
<SettlementLedgerPanel
adminSiteId={adminSiteId}
periodFilter={periodFilter}
currencyCode={activeCurrency}
canManage={canManagePeriods}
onOpenBill={setDetailBillId}
refreshKey={listRevision}
/>
);
}
if (activeSection === "bills" && adminSiteId !== null && periodFilterReady) {
return (
<SettlementBillsPanel
adminSiteId={adminSiteId}
periodFilter={periodFilter}
currencyCode={activeCurrency}
onOpenDetail={setDetailBillId}
initialCategory={billsInitialCategory}
refreshKey={listRevision}
/>
);
}
return <AdminNoResourceState />;
}
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
<header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<h1 className="text-xl font-semibold tracking-tight">
{t("title", { defaultValue: "结算中心" })}
</h1>
<AdminStatusBadge
status={openPeriod ? "processing" : allPeriodsCompleted ? "completed" : "idle"}
>
{openPeriod
? t("header.statusRunning", { defaultValue: "账期进行中" })
: allPeriodsCompleted
? t("header.statusCompleted", { defaultValue: "账期已结清" })
: t("header.statusIdle", { defaultValue: "等待开期" })}
</AdminStatusBadge>
</div>
<p className="text-sm text-muted-foreground">
{t("header.subtitle", { defaultValue: "信用占成账务" })}
</p>
</div>
{siteOptions.length <= 1 && selectedSiteLabel ? (
<p className="text-sm text-muted-foreground">{selectedSiteLabel}</p>
) : null}
</header>
{adminSiteId === null ? (
<p className="text-sm text-muted-foreground">{t("empty.noSite", { defaultValue: "请选择接入站点。" })}</p>
) : (
<div className="min-w-0 space-y-4">
<SettlementCenterNav
active={activeSection}
onChange={(section) => {
if (section === "bills") {
setBillsInitialCategory("all");
}
setActiveSection(section);
}}
counts={{
pendingConfirm: summaryTotals.pendingConfirm,
awaitingPayment: summaryTotals.awaitingPayment,
}}
siteSelector={
siteOptions.length > 1 && selectSiteId !== null ? (
<Select
value={String(selectSiteId)}
onValueChange={(value) => {
setAdminSiteId(Number(value));
setPeriodFilter("all");
setPeriodFilterReady(false);
}}
>
<SelectTrigger className="h-9 w-[220px] bg-background">
<SelectValue>{selectedSiteLabel}</SelectValue>
</SelectTrigger>
<SelectContent>
{siteOptions.map((site) => (
<SelectItem key={site.id} value={String(site.id)}>
{site.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : null
}
/>
{showPeriodToolbar && periodFilterReady ? (
<SettlementPeriodToolbar
periods={periods}
value={periodFilter}
onChange={(next) => {
setPeriodFilter(next);
setPeriodFilterReady(true);
}}
/>
) : null}
<AdminPageCard title={panelTitle}>{renderMainPanel()}</AdminPageCard>
</div>
)}
<Dialog open={detailBillId !== null} onOpenChange={(open) => !open && setDetailBillId(null)}>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{t("actions.billDetail", { defaultValue: "账单详情 · 确认 / 收付" })}
</DialogTitle>
</DialogHeader>
{detailBillId !== null ? (
<AgentBillDetail
billId={detailBillId}
currencyCode={activeCurrency}
canManage={canManagePeriods}
onUpdated={() => {
void loadPeriods();
setListRevision((n) => n + 1);
}}
/>
) : null}
</DialogContent>
</Dialog>
</div>
);
}