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:
@@ -2,60 +2,43 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import {
|
||||
PRD_AGENT_LINE_PROVISION_ACCESS_ANY,
|
||||
PRD_AGENT_SITES_ACCESS_ANY,
|
||||
PRD_AGENTS_ACCESS_ANY,
|
||||
PRD_SETTLEMENT_AGENT_ACCESS_ANY,
|
||||
} from "@/lib/admin-prd";
|
||||
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
|
||||
import { isAgentLineSubnavTabVisible } from "@/modules/agents/agent-line-subnav-visibility";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { useAgentManagementSiteStore } from "@/stores/agent-management-site";
|
||||
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";
|
||||
|
||||
const tabs: {
|
||||
const primaryTabs: {
|
||||
href: string;
|
||||
labelKey: string;
|
||||
matchPrefix: string;
|
||||
requiredAny: readonly string[];
|
||||
}[] = [
|
||||
{
|
||||
href: "/admin/agents",
|
||||
labelKey: "subnav.operations",
|
||||
matchPrefix: "/admin/agents",
|
||||
requiredAny: PRD_AGENTS_ACCESS_ANY,
|
||||
},
|
||||
{
|
||||
href: "/admin/agents/provision",
|
||||
labelKey: "subnav.provision",
|
||||
matchPrefix: "/admin/agents/provision",
|
||||
requiredAny: PRD_AGENT_LINE_PROVISION_ACCESS_ANY,
|
||||
},
|
||||
{
|
||||
href: "/admin/agents/sites",
|
||||
labelKey: "subnav.sites",
|
||||
matchPrefix: "/admin/agents/sites",
|
||||
requiredAny: PRD_AGENT_SITES_ACCESS_ANY,
|
||||
},
|
||||
{
|
||||
href: "/admin/agents/settlement-bills",
|
||||
labelKey: "subnav.settlementBills",
|
||||
matchPrefix: "/admin/agents/settlement",
|
||||
requiredAny: PRD_SETTLEMENT_AGENT_ACCESS_ANY,
|
||||
},
|
||||
];
|
||||
|
||||
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") &&
|
||||
!pathname.startsWith("/admin/agents/sites") &&
|
||||
!pathname.startsWith("/admin/agents/settlement"))
|
||||
!pathname.startsWith("/admin/agents/provision"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,45 +49,105 @@ export function AgentsSubnav(): React.ReactElement {
|
||||
const { t } = useTranslation("agents");
|
||||
const pathname = usePathname();
|
||||
const profile = useAdminProfile();
|
||||
const perms = profile?.permissions;
|
||||
const { sites: siteOptions } = useAdminSiteCodeOptions();
|
||||
const adminSiteId = useAgentManagementSiteStore((s) => s.adminSiteId);
|
||||
const setAdminSiteId = useAgentManagementSiteStore((s) => s.setAdminSiteId);
|
||||
|
||||
const visibleTabs = useMemo(
|
||||
() =>
|
||||
tabs.filter(
|
||||
(tab) =>
|
||||
profile?.is_super_admin === true ||
|
||||
adminHasAnyPermission(perms, [...tab.requiredAny]),
|
||||
),
|
||||
[perms, profile?.is_super_admin],
|
||||
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],
|
||||
);
|
||||
|
||||
if (visibleTabs.length === 0) {
|
||||
useEffect(() => {
|
||||
if (adminSiteId !== null || siteOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
const boundSiteId = profile?.agent?.admin_site_id;
|
||||
if (boundSiteId != null) {
|
||||
setAdminSiteId(boundSiteId);
|
||||
return;
|
||||
}
|
||||
setAdminSiteId(siteOptions[0]?.id ?? null);
|
||||
}, [adminSiteId, profile?.agent?.admin_site_id, setAdminSiteId, siteOptions]);
|
||||
|
||||
const selectSiteId = adminSiteId ?? siteOptions[0]?.id ?? null;
|
||||
const selectedSiteLabel = useMemo(() => {
|
||||
const site = siteOptions.find((item) => item.id === selectSiteId);
|
||||
return site ? `${site.name} (${site.code})` : null;
|
||||
}, [selectSiteId, siteOptions]);
|
||||
|
||||
if (visiblePrimaryTabs.length === 0 && !showProvision) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label={t("subnav.label", { defaultValue: "代理线路导航" })}
|
||||
className="flex w-full flex-wrap items-end gap-1 border-b border-border/60 px-1"
|
||||
>
|
||||
{visibleTabs.map((tab) => {
|
||||
const active = isTabActive(pathname, tab.href, tab.matchPrefix);
|
||||
<div className="flex w-full flex-wrap items-center justify-between gap-3 rounded-lg bg-muted/50 p-1">
|
||||
<nav
|
||||
aria-label={t("subnav.label", { defaultValue: "代理管理导航" })}
|
||||
className="inline-flex max-w-full flex-wrap items-center gap-1"
|
||||
>
|
||||
{visiblePrimaryTabs.map((tab) => {
|
||||
const active = isTabActive(pathname, tab.href, tab.matchPrefix);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tab.href}
|
||||
href={tab.href}
|
||||
className={cn(
|
||||
"border-b-2 px-4 py-3 text-sm font-medium transition-colors",
|
||||
active
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:border-border/80 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{t(tab.labelKey)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
return (
|
||||
<Link
|
||||
key={tab.href}
|
||||
href={tab.href}
|
||||
className={cn(
|
||||
"rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||
active
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{t(tab.labelKey)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{showProvision ? (
|
||||
<>
|
||||
<span className="mx-1 hidden h-5 w-px bg-border/80 sm:inline-block" aria-hidden />
|
||||
<Link
|
||||
href={provisionTab.href}
|
||||
className={cn(
|
||||
"rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||
isTabActive(pathname, provisionTab.href, provisionTab.matchPrefix)
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{t(provisionTab.labelKey)}
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
</nav>
|
||||
|
||||
{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>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user