refactor: update agent API schemas, standardize UI text styling, and enhance settlement credit ledger components
This commit is contained in:
@@ -1,52 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { useDeferredValue, useEffect, useMemo, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
AdminSubnav,
|
||||
AdminSubnavBar,
|
||||
AdminSubnavLink,
|
||||
} from "@/components/admin/admin-subnav";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
|
||||
import { isAgentLineSubnavTabVisible } from "@/modules/agents/agent-line-subnav-visibility";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { PRD_INTEGRATION_ACCESS_ANY } from "@/lib/admin-prd";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { useAgentManagementSiteStore } from "@/stores/agent-management-site";
|
||||
|
||||
const primaryTabs: {
|
||||
href: string;
|
||||
labelKey: string;
|
||||
matchPrefix: string;
|
||||
}[] = [
|
||||
{
|
||||
href: "/admin/agents",
|
||||
labelKey: "subnav.operations",
|
||||
matchPrefix: "/admin/agents",
|
||||
},
|
||||
];
|
||||
|
||||
const provisionTab = {
|
||||
href: "/admin/agents/provision",
|
||||
labelKey: "subnav.provision",
|
||||
matchPrefix: "/admin/agents/provision",
|
||||
} as const;
|
||||
|
||||
function isTabActive(pathname: string, href: string, matchPrefix: string): boolean {
|
||||
if (href === "/admin/agents") {
|
||||
return (
|
||||
pathname === "/admin/agents" ||
|
||||
pathname === "/admin/agents/list" ||
|
||||
(pathname.startsWith("/admin/agents/") &&
|
||||
!pathname.startsWith("/admin/agents/provision"))
|
||||
);
|
||||
}
|
||||
|
||||
return pathname === href || pathname.startsWith(`${matchPrefix}/`) || pathname === matchPrefix;
|
||||
}
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function AgentsSubnav(): React.ReactElement {
|
||||
const { t } = useTranslation("agents");
|
||||
@@ -55,18 +26,14 @@ export function AgentsSubnav(): React.ReactElement {
|
||||
const { sites: siteOptions } = useAdminSiteCodeOptions();
|
||||
const adminSiteId = useAgentManagementSiteStore((s) => s.adminSiteId);
|
||||
const setAdminSiteId = useAgentManagementSiteStore((s) => s.setAdminSiteId);
|
||||
const [sitePickerOpen, setSitePickerOpen] = useState(false);
|
||||
const [siteKeyword, setSiteKeyword] = useState("");
|
||||
const deferredKeyword = useDeferredValue(siteKeyword);
|
||||
|
||||
const canSwitchSite =
|
||||
profile?.is_super_admin === true ||
|
||||
adminHasAnyPermission(profile?.permissions, [...PRD_INTEGRATION_ACCESS_ANY]);
|
||||
|
||||
const showProvision = isAgentLineSubnavTabVisible(provisionTab.href, profile);
|
||||
|
||||
const visiblePrimaryTabs = useMemo(
|
||||
() => primaryTabs.filter((tab) => isAgentLineSubnavTabVisible(tab.href, profile)),
|
||||
[profile],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (adminSiteId !== null || siteOptions.length === 0) {
|
||||
return;
|
||||
@@ -80,58 +47,94 @@ export function AgentsSubnav(): React.ReactElement {
|
||||
}, [adminSiteId, profile?.agent?.admin_site_id, setAdminSiteId, siteOptions]);
|
||||
|
||||
const selectSiteId = adminSiteId ?? siteOptions[0]?.id ?? null;
|
||||
const selectedSiteLabel = useMemo(() => {
|
||||
const selectedSite = useMemo(() => {
|
||||
const site = siteOptions.find((item) => item.id === selectSiteId);
|
||||
return site ? `${site.name} (${site.code})` : null;
|
||||
return site ?? null;
|
||||
}, [selectSiteId, siteOptions]);
|
||||
|
||||
if (visiblePrimaryTabs.length === 0 && !showProvision) {
|
||||
return <></>;
|
||||
}
|
||||
const filteredSites = useMemo(() => {
|
||||
const normalized = deferredKeyword.trim().toLowerCase();
|
||||
if (normalized === "") {
|
||||
return siteOptions;
|
||||
}
|
||||
|
||||
return siteOptions.filter((site) => site.name.toLowerCase().includes(normalized));
|
||||
}, [deferredKeyword, siteOptions]);
|
||||
|
||||
const siteSelector =
|
||||
canSwitchSite && siteOptions.length > 0 && selectSiteId !== null ? (
|
||||
<Select
|
||||
value={String(selectSiteId)}
|
||||
onValueChange={(value) => setAdminSiteId(Number(value))}
|
||||
>
|
||||
<SelectTrigger className="h-9 w-[200px] bg-background">
|
||||
<SelectValue placeholder={t("lineFilter", { defaultValue: "一级代理" })}>
|
||||
{selectedSiteLabel ?? t("lineFilter", { defaultValue: "一级代理" })}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{siteOptions.map((site) => (
|
||||
<SelectItem key={site.id} value={String(site.id)}>
|
||||
{site.name} ({site.code})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
pathname !== "/admin/agents/list" && canSwitchSite && siteOptions.length > 0 && selectSiteId !== null ? (
|
||||
<Popover open={sitePickerOpen} onOpenChange={setSitePickerOpen}>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "lg" }),
|
||||
"h-10 w-[240px] justify-between gap-2 bg-background px-3 text-left font-normal",
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{selectedSite?.name ?? t("lineFilter", { defaultValue: "一级代理" })}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-muted-foreground">
|
||||
{selectedSite?.code ?? ""}
|
||||
</span>
|
||||
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[320px] p-0">
|
||||
<div className="border-b border-border/60 p-3">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={siteKeyword}
|
||||
onChange={(event) => setSiteKeyword(event.target.value)}
|
||||
placeholder={t("siteSearch", { defaultValue: "搜索站点名称" })}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="max-h-72">
|
||||
<div className="p-2">
|
||||
{filteredSites.map((site) => {
|
||||
const active = site.id === selectSiteId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={site.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-md px-3 py-2 text-left transition-colors",
|
||||
active ? "bg-primary/10 text-primary" : "hover:bg-muted/70",
|
||||
)}
|
||||
onClick={() => {
|
||||
setAdminSiteId(site.id);
|
||||
setSitePickerOpen(false);
|
||||
setSiteKeyword("");
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">{site.name}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{site.code}</div>
|
||||
</div>
|
||||
{active ? <Check className="size-4 shrink-0" /> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{filteredSites.length === 0 ? (
|
||||
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
||||
{t("common:states.empty", { defaultValue: "暂无数据" })}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<AdminSubnavBar trailing={siteSelector}>
|
||||
<AdminSubnav aria-label={t("subnav.label", { defaultValue: "代理管理导航" })}>
|
||||
{visiblePrimaryTabs.map((tab) => (
|
||||
<AdminSubnavLink
|
||||
key={tab.href}
|
||||
href={tab.href}
|
||||
active={isTabActive(pathname, tab.href, tab.matchPrefix)}
|
||||
>
|
||||
{t(tab.labelKey)}
|
||||
</AdminSubnavLink>
|
||||
))}
|
||||
|
||||
{showProvision ? (
|
||||
<AdminSubnavLink
|
||||
href={provisionTab.href}
|
||||
active={isTabActive(pathname, provisionTab.href, provisionTab.matchPrefix)}
|
||||
>
|
||||
{t(provisionTab.labelKey)}
|
||||
</AdminSubnavLink>
|
||||
) : null}
|
||||
</AdminSubnav>
|
||||
<div className="pb-1">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{t("title", { defaultValue: "代理管理" })}
|
||||
</p>
|
||||
</div>
|
||||
</AdminSubnavBar>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user