feat(api, i18n): add agent_node_id to various admin queries and enhance multi-language support
Introduced the agent_node_id field in AdminDrawListQuery, AdminPlayerListQuery, AdminSettlementBatchListQuery, TicketItemsListQuery, and TransferOrderListQuery to improve filtering capabilities. Updated the admin-breadcrumb and admin-sidebar components to include new translations for agent-related terms in English, Nepali, and Chinese, enhancing the overall user experience and multi-language support across the admin interface.
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useMemo, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
AdminSidebarNav,
|
||||
AdminSidebarNavSkeleton,
|
||||
} from "@/components/admin/admin-sidebar-nav";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
@@ -18,63 +18,28 @@ import {
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { adminNavLabel } from "@/lib/admin-nav-label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveAdminNavIcon } from "@/modules/_config/admin-nav-icons";
|
||||
import { ADMIN_BASE } from "@/modules/_config/admin-nav";
|
||||
import { useAdminProfile, useAdminSessionStore } from "@/stores/admin-session";
|
||||
|
||||
/** 与常见导航项文字宽度接近,避免整齐灰条 */
|
||||
const SIDEBAR_NAV_SKELETON_WIDTHS = ["68%", "82%", "58%", "74%", "64%", "78%", "55%", "70%", "62%"] as const;
|
||||
|
||||
function SidebarNavSkeletonRow({
|
||||
labelWidth,
|
||||
delayMs,
|
||||
}: {
|
||||
labelWidth: string;
|
||||
delayMs: number;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<div
|
||||
aria-hidden
|
||||
className="flex h-8 w-full items-center gap-2 rounded-md px-2 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-1.5"
|
||||
style={{ animationDelay: `${delayMs}ms` }}
|
||||
>
|
||||
<span
|
||||
className="size-4 shrink-0 rounded-[4px] bg-white/12 motion-safe:animate-pulse"
|
||||
style={{ animationDelay: `${delayMs}ms` }}
|
||||
/>
|
||||
<span
|
||||
className="h-2.5 shrink-0 rounded-full bg-white/12 motion-safe:animate-pulse group-data-[collapsible=icon]:hidden"
|
||||
style={{ width: labelWidth, animationDelay: `${delayMs + 40}ms` }}
|
||||
/>
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminSidebarSkeleton(): ReactElement {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon" className="overflow-hidden">
|
||||
<SidebarHeader className="flex h-14 shrink-0 items-center gap-0 border-b border-sidebar-border p-0 px-2">
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className="flex shrink-0 flex-col gap-0 border-b border-sidebar-border px-2 py-2">
|
||||
<SidebarMenu className="h-full w-full">
|
||||
<SidebarMenuItem className="h-full">
|
||||
<div className="flex h-12 w-full items-center px-1 group-data-[collapsible=icon]:justify-center">
|
||||
<div className="flex h-10 w-full items-center px-1 group-data-[collapsible=icon]:justify-center">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="N lotto"
|
||||
className="h-auto max-h-11 w-full object-contain object-left opacity-95 group-data-[collapsible=icon]:max-h-8 group-data-[collapsible=icon]:object-center"
|
||||
className="h-auto max-h-10 w-full object-contain object-left opacity-95 group-data-[collapsible=icon]:max-h-8 group-data-[collapsible=icon]:object-center"
|
||||
/>
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="relative overflow-hidden">
|
||||
<SidebarContent className="relative min-h-0 overflow-hidden p-0">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-[22rem] opacity-55 group-data-[collapsible=icon]:hidden"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-40 opacity-50 group-data-[collapsible=icon]:hidden"
|
||||
aria-hidden
|
||||
>
|
||||
<img
|
||||
@@ -82,81 +47,64 @@ function AdminSidebarSkeleton(): ReactElement {
|
||||
alt=""
|
||||
className="h-full w-full object-cover object-bottom"
|
||||
/>
|
||||
<div className="absolute inset-x-0 top-0 h-28 bg-linear-to-b from-sidebar to-transparent" />
|
||||
<div className="absolute inset-x-0 top-0 h-20 bg-linear-to-b from-sidebar to-transparent" />
|
||||
<div className="absolute inset-0 bg-sidebar/20" />
|
||||
</div>
|
||||
<SidebarGroup className="relative z-10">
|
||||
<SidebarGroupLabel className="text-sidebar-foreground/55">
|
||||
{t("sidebar.workspace", { defaultValue: "Workspace" })}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className={cn("gap-0.5", "motion-safe:opacity-90")}>
|
||||
{SIDEBAR_NAV_SKELETON_WIDTHS.map((width, i) => (
|
||||
<SidebarNavSkeletonRow key={i} labelWidth={width} delayMs={i * 55} />
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<div className="relative z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain pb-2">
|
||||
<AdminSidebarNavSkeleton />
|
||||
</div>
|
||||
</SidebarContent>
|
||||
<SidebarSeparator />
|
||||
<SidebarRail />
|
||||
<span className="sr-only" role="status" aria-live="polite">
|
||||
{t("auth.checking")}
|
||||
</span>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
function isActive(pathname: string, item: { href: string; activeMatchPrefix?: string; segment?: string }): boolean {
|
||||
const { href, activeMatchPrefix, segment } = item;
|
||||
const prefix = activeMatchPrefix ?? href;
|
||||
if (prefix === ADMIN_BASE || prefix === `${ADMIN_BASE}/`) {
|
||||
return pathname === ADMIN_BASE || pathname === `${ADMIN_BASE}/`;
|
||||
}
|
||||
// Keep "settings" independent from its child routes like /admin/settings/currencies.
|
||||
if (segment === "settings") {
|
||||
return pathname === href;
|
||||
}
|
||||
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
||||
}
|
||||
|
||||
export function AdminAppSidebar() {
|
||||
const { t } = useTranslation(["common", "dashboard", "players", "draws", "config", "wallet", "risk", "settlement", "jackpot", "reconcile", "tickets", "audit", "reports"]);
|
||||
const pathname = usePathname();
|
||||
const shellAuthPending = useAdminSessionStore((s) => s.shellAuthPending);
|
||||
const profile = useAdminProfile();
|
||||
|
||||
if (shellAuthPending) {
|
||||
return <AdminSidebarSkeleton />;
|
||||
}
|
||||
const visibleNav = useMemo(
|
||||
() => (profile?.navigation ?? []).filter((item) => item.segment !== "risk"),
|
||||
[profile?.navigation],
|
||||
);
|
||||
|
||||
if (shellAuthPending) {
|
||||
return <AdminSidebarSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon" className="overflow-hidden">
|
||||
<SidebarHeader className="flex h-14 shrink-0 items-center gap-0 border-b border-sidebar-border p-0 px-2">
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className="flex shrink-0 flex-col gap-0 border-b border-sidebar-border px-2 py-2">
|
||||
<SidebarMenu className="h-full w-full">
|
||||
<SidebarMenuItem className="h-full">
|
||||
<SidebarMenuButton
|
||||
render={<Link href={ADMIN_BASE} />}
|
||||
className="h-full min-h-0 justify-start px-1 py-0 hover:bg-transparent group-data-[collapsible=icon]:justify-center"
|
||||
className="h-10 min-h-0 justify-start px-1 py-0 hover:bg-transparent group-data-[collapsible=icon]:justify-center"
|
||||
>
|
||||
<div className="flex h-12 w-full items-center group-data-[collapsible=icon]:size-10 group-data-[collapsible=icon]:justify-center">
|
||||
<div className="flex h-10 w-full items-center group-data-[collapsible=icon]:size-10 group-data-[collapsible=icon]:justify-center">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="N lotto"
|
||||
className="h-auto max-h-11 w-full object-contain object-left group-data-[collapsible=icon]:max-h-8 group-data-[collapsible=icon]:object-center"
|
||||
className="h-auto max-h-10 w-full object-contain object-left group-data-[collapsible=icon]:max-h-8 group-data-[collapsible=icon]:object-center"
|
||||
/>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
{profile?.agent ? (
|
||||
<p
|
||||
className="truncate px-1 pb-1 text-[11px] leading-tight font-medium text-sidebar-foreground/60 group-data-[collapsible=icon]:hidden"
|
||||
title={profile.agent.name}
|
||||
>
|
||||
{profile.agent.name}
|
||||
<span className="text-sidebar-foreground/40"> · {profile.agent.code}</span>
|
||||
</p>
|
||||
) : null}
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="relative overflow-hidden">
|
||||
<SidebarContent className="relative min-h-0 overflow-hidden p-0">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-[22rem] opacity-55 group-data-[collapsible=icon]:hidden"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-40 opacity-50 group-data-[collapsible=icon]:hidden"
|
||||
aria-hidden
|
||||
>
|
||||
<img
|
||||
@@ -164,32 +112,12 @@ export function AdminAppSidebar() {
|
||||
alt=""
|
||||
className="h-full w-full object-cover object-bottom"
|
||||
/>
|
||||
<div className="absolute inset-x-0 top-0 h-28 bg-linear-to-b from-sidebar to-transparent" />
|
||||
<div className="absolute inset-x-0 top-0 h-20 bg-linear-to-b from-sidebar to-transparent" />
|
||||
<div className="absolute inset-0 bg-sidebar/20" />
|
||||
</div>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{t("sidebar.workspace", { ns: "common", defaultValue: "Workspace" })}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{visibleNav.map((item) => {
|
||||
const Icon = resolveAdminNavIcon(item.segment);
|
||||
return (
|
||||
<SidebarMenuItem key={item.segment}>
|
||||
<SidebarMenuButton
|
||||
tooltip={adminNavLabel(item.segment, t, item.label)}
|
||||
isActive={isActive(pathname, item)}
|
||||
render={<Link href={item.href} />}
|
||||
className="font-medium text-sidebar-foreground/90 hover:text-sidebar-accent-foreground data-active:bg-red-600 data-active:text-white data-active:shadow-sm"
|
||||
>
|
||||
<Icon data-icon="inline-start" aria-hidden />
|
||||
<span>{adminNavLabel(item.segment, t, item.label)}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<div className="relative z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain pb-2">
|
||||
<AdminSidebarNav items={visibleNav} />
|
||||
</div>
|
||||
</SidebarContent>
|
||||
<SidebarSeparator />
|
||||
<SidebarRail />
|
||||
|
||||
Reference in New Issue
Block a user