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:
439
src/modules/settlement/settlement-center-shell.tsx
Normal file
439
src/modules/settlement/settlement-center-shell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user