Added new types and API functions for settlement period summaries and credit ledgers, improving the management of agent settlements. Updated the admin console to reflect these changes, enhancing user experience with better navigation and data presentation. Additionally, expanded multi-language support by incorporating new translations in English, Nepali, and Chinese for settlement-related terms, ensuring consistency across the platform.
272 lines
8.0 KiB
TypeScript
272 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { ChevronRight } from "lucide-react";
|
|
import type { TFunction } from "i18next";
|
|
import { useEffect, useMemo, useState, type ReactElement } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import {
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarMenuSub,
|
|
SidebarMenuSubButton,
|
|
SidebarMenuSubItem,
|
|
useSidebar,
|
|
} from "@/components/ui/sidebar";
|
|
import {
|
|
ADMIN_NAV_GROUP_ICON,
|
|
ADMIN_NAV_GROUP_ORDER,
|
|
groupAdminNavItems,
|
|
} from "@/lib/admin-nav-groups";
|
|
import { adminNavLabel } from "@/lib/admin-nav-label";
|
|
import { cn } from "@/lib/utils";
|
|
import { resolveAdminNavIcon } from "@/modules/_config/admin-nav-icons";
|
|
import { ADMIN_BASE, type AdminNavGroup, type AdminNavItem } from "@/modules/_config/admin-nav";
|
|
|
|
const NAV_BTN =
|
|
"h-8 gap-2 px-2.5 py-0 text-[13px] leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground [&_svg]:size-4";
|
|
const NAV_ACTIVE = "data-active:bg-red-600 data-active:text-white data-active:font-medium data-active:shadow-sm";
|
|
const SUB_NAV =
|
|
"h-8 min-h-8 gap-2 rounded-sm px-2.5 py-0 text-sm leading-snug font-normal text-sidebar-foreground/90 hover:text-sidebar-accent-foreground data-[size=md]:text-sm data-[size=sm]:text-sm [&>span]:text-sm [&_svg]:size-4";
|
|
|
|
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}/`;
|
|
}
|
|
if (segment === "settings") {
|
|
return pathname === href;
|
|
}
|
|
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
}
|
|
|
|
function defaultOpenGroups(
|
|
groups: { group: AdminNavGroup; items: AdminNavItem[] }[],
|
|
pathname: string,
|
|
): Record<AdminNavGroup, boolean> {
|
|
const open = Object.fromEntries(
|
|
ADMIN_NAV_GROUP_ORDER.map((g) => [g, true]),
|
|
) as Record<AdminNavGroup, boolean>;
|
|
|
|
for (const { group, items } of groups) {
|
|
if (items.some((item) => isActive(pathname, item))) {
|
|
open[group] = true;
|
|
}
|
|
}
|
|
|
|
return open;
|
|
}
|
|
|
|
function NavLeaf({
|
|
item,
|
|
pathname,
|
|
t,
|
|
}: {
|
|
item: AdminNavItem;
|
|
pathname: string;
|
|
t: TFunction;
|
|
}): ReactElement {
|
|
const Icon = resolveAdminNavIcon(item.segment);
|
|
const active = isActive(pathname, item);
|
|
const label = adminNavLabel(item.segment, t, item.label);
|
|
|
|
return (
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
size="sm"
|
|
tooltip={label}
|
|
isActive={active}
|
|
render={<Link href={item.href} />}
|
|
className={cn(NAV_BTN, NAV_ACTIVE)}
|
|
>
|
|
<Icon data-icon="inline-start" aria-hidden />
|
|
<span>{label}</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
);
|
|
}
|
|
|
|
function NavSubLeaf({
|
|
item,
|
|
pathname,
|
|
t,
|
|
}: {
|
|
item: AdminNavItem;
|
|
pathname: string;
|
|
t: TFunction;
|
|
}): ReactElement {
|
|
const Icon = resolveAdminNavIcon(item.segment);
|
|
const active = isActive(pathname, item);
|
|
const label = adminNavLabel(item.segment, t, item.label);
|
|
|
|
return (
|
|
<SidebarMenuSubItem>
|
|
<SidebarMenuSubButton
|
|
size="md"
|
|
isActive={active}
|
|
render={<Link href={item.href} />}
|
|
className={cn(SUB_NAV, NAV_ACTIVE)}
|
|
>
|
|
<Icon aria-hidden />
|
|
<span>{label}</span>
|
|
</SidebarMenuSubButton>
|
|
</SidebarMenuSubItem>
|
|
);
|
|
}
|
|
|
|
function NavCollapsibleGroup({
|
|
group,
|
|
items,
|
|
pathname,
|
|
open,
|
|
onToggle,
|
|
t,
|
|
}: {
|
|
group: AdminNavGroup;
|
|
items: AdminNavItem[];
|
|
pathname: string;
|
|
open: boolean;
|
|
onToggle: () => void;
|
|
t: TFunction;
|
|
}): ReactElement {
|
|
const GroupIcon = ADMIN_NAV_GROUP_ICON[group];
|
|
const groupLabel = t(`sidebar.group.${group}`, { ns: "common", defaultValue: group });
|
|
const hasActiveChild = items.some((item) => isActive(pathname, item));
|
|
|
|
return (
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
size="sm"
|
|
type="button"
|
|
data-open={open ? "" : undefined}
|
|
isActive={hasActiveChild && !open}
|
|
className={cn(NAV_BTN, hasActiveChild && !open && NAV_ACTIVE)}
|
|
onClick={onToggle}
|
|
>
|
|
<GroupIcon aria-hidden />
|
|
<span className="flex-1 truncate">{groupLabel}</span>
|
|
<ChevronRight
|
|
aria-hidden
|
|
className={cn(
|
|
"ml-auto size-4 shrink-0 text-sidebar-foreground/50 transition-transform duration-200",
|
|
open && "rotate-90",
|
|
)}
|
|
/>
|
|
</SidebarMenuButton>
|
|
{open ? (
|
|
<SidebarMenuSub className="mx-2 gap-0.5 border-sidebar-border/50 px-1.5 py-0.5">
|
|
{items.map((item) => (
|
|
<NavSubLeaf key={item.segment} item={item} pathname={pathname} t={t} />
|
|
))}
|
|
</SidebarMenuSub>
|
|
) : null}
|
|
</SidebarMenuItem>
|
|
);
|
|
}
|
|
|
|
export function AdminSidebarNav({
|
|
items,
|
|
}: {
|
|
items: readonly AdminNavItem[];
|
|
}): ReactElement {
|
|
const { t } = useTranslation("common");
|
|
const pathname = usePathname();
|
|
const { state } = useSidebar();
|
|
const navGroups = useMemo(() => groupAdminNavItems(items), [items]);
|
|
const flatItems = useMemo(() => navGroups.flatMap((g) => g.items), [navGroups]);
|
|
|
|
const [openGroups, setOpenGroups] = useState<Record<AdminNavGroup, boolean>>(() =>
|
|
defaultOpenGroups(navGroups, pathname),
|
|
);
|
|
|
|
useEffect(() => {
|
|
setOpenGroups((prev) => {
|
|
const next = { ...prev };
|
|
let changed = false;
|
|
for (const { group, items: groupItems } of navGroups) {
|
|
if (groupItems.some((item) => isActive(pathname, item)) && !next[group]) {
|
|
next[group] = true;
|
|
changed = true;
|
|
}
|
|
}
|
|
return changed ? next : prev;
|
|
});
|
|
}, [pathname, navGroups]);
|
|
|
|
if (state === "collapsed") {
|
|
return (
|
|
<SidebarMenu className="gap-0.5 px-1.5 py-1.5">
|
|
{flatItems.map((item) => (
|
|
<NavLeaf key={item.segment} item={item} pathname={pathname} t={t} />
|
|
))}
|
|
</SidebarMenu>
|
|
);
|
|
}
|
|
|
|
const overview = navGroups.find((g) => g.group === "overview");
|
|
const collapsible = navGroups.filter((g) => g.group !== "overview");
|
|
|
|
return (
|
|
<SidebarMenu className="gap-0.5 px-1.5 py-1.5">
|
|
{overview?.items.map((item) => (
|
|
<NavLeaf key={item.segment} item={item} pathname={pathname} t={t} />
|
|
))}
|
|
{collapsible.map(({ group, items: groupItems }) => (
|
|
<NavCollapsibleGroup
|
|
key={group}
|
|
group={group}
|
|
items={groupItems}
|
|
pathname={pathname}
|
|
open={openGroups[group] ?? true}
|
|
onToggle={() =>
|
|
setOpenGroups((prev) => ({
|
|
...prev,
|
|
[group]: !(prev[group] ?? true),
|
|
}))
|
|
}
|
|
t={t}
|
|
/>
|
|
))}
|
|
</SidebarMenu>
|
|
);
|
|
}
|
|
|
|
export function AdminSidebarNavSkeleton(): ReactElement {
|
|
const { t } = useTranslation("common");
|
|
const widths = ["68%", "74%", "58%", "70%", "62%"] as const;
|
|
|
|
return (
|
|
<SidebarMenu className="gap-0.5 px-1.5 py-1.5 motion-safe:opacity-90">
|
|
<SidebarMenuItem>
|
|
<div aria-hidden className="flex h-8 items-center gap-2 rounded-md px-2.5">
|
|
<span className="size-4 rounded-sm bg-white/12 motion-safe:animate-pulse" />
|
|
<span className="h-2.5 w-14 rounded-full bg-white/12 motion-safe:animate-pulse" />
|
|
</div>
|
|
</SidebarMenuItem>
|
|
{widths.map((width, i) => (
|
|
<SidebarMenuItem key={i}>
|
|
<div aria-hidden className="flex h-8 items-center gap-2 rounded-md px-2.5">
|
|
<span
|
|
className="size-4 rounded-sm bg-white/12 motion-safe:animate-pulse"
|
|
style={{ animationDelay: `${i * 55}ms` }}
|
|
/>
|
|
<span
|
|
className="h-2 rounded-full bg-white/12 motion-safe:animate-pulse"
|
|
style={{ width, animationDelay: `${i * 55 + 40}ms` }}
|
|
/>
|
|
<span className="ml-auto size-3 rounded-sm bg-white/10 motion-safe:animate-pulse" />
|
|
</div>
|
|
</SidebarMenuItem>
|
|
))}
|
|
<span className="sr-only">{t("auth.checking")}</span>
|
|
</SidebarMenu>
|
|
);
|
|
}
|