feat(api, agents, i18n): enhance settlement features and multi-language support

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.
This commit is contained in:
2026-06-05 18:00:59 +08:00
parent 65eaeecf8c
commit af982bb9f7
73 changed files with 4307 additions and 2494 deletions

View File

@@ -81,7 +81,9 @@ export function AdminPerPagePicker({
}}
>
<SelectTrigger id={selectId} size="sm" className="w-[6.75rem]">
<SelectValue placeholder={t("pagination.selectPlaceholder", { defaultValue: "Select" })} />
<SelectValue>
{(v) => (v == null || v === "" ? String(perPage) : String(v))}
</SelectValue>
</SelectTrigger>
<SelectContent align="start" sideOffset={6}>
{ADMIN_LIST_PER_PAGE_OPTIONS.map((n) => (

View File

@@ -30,7 +30,7 @@ export function AdminNoResourceState({
<div
className={cn(
"flex w-full flex-col items-center justify-center text-center",
compact ? "gap-2 py-4" : "gap-3 py-8",
compact ? "min-h-[120px] gap-2 py-4" : "min-h-[200px] gap-3 py-8",
className,
)}
role="status"
@@ -41,14 +41,14 @@ export function AdminNoResourceState({
width={compact ? 120 : 160}
height={compact ? 120 : 160}
className={cn(
"h-auto w-auto object-contain",
"h-auto w-auto object-contain self-center object-center -mt-4",
compact ? "max-h-24 max-w-[120px]" : "max-h-40 max-w-[160px]",
imageClassName,
)}
/>
<p
className={cn(
"text-muted-foreground",
"text-muted-foreground self-center",
compact ? "text-[11px] leading-snug" : "text-sm",
)}
>

View File

@@ -14,6 +14,7 @@ import {
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from "@/components/ui/sidebar";
import {
ADMIN_NAV_GROUP_ICON,
@@ -29,7 +30,7 @@ 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 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";
"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,
@@ -101,6 +102,7 @@ function NavSubLeaf({
pathname: string;
t: TFunction;
}): ReactElement {
const Icon = resolveAdminNavIcon(item.segment);
const active = isActive(pathname, item);
const label = adminNavLabel(item.segment, t, item.label);
@@ -112,6 +114,7 @@ function NavSubLeaf({
render={<Link href={item.href} />}
className={cn(SUB_NAV, NAV_ACTIVE)}
>
<Icon aria-hidden />
<span>{label}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
@@ -175,7 +178,9 @@ export function AdminSidebarNav({
}): 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),
@@ -195,6 +200,16 @@ export function AdminSidebarNav({
});
}, [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");

View File

@@ -0,0 +1,128 @@
"use client";
import Link from "next/link";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export function adminSubnavItemClassName(active: boolean, disabled = false): string {
if (disabled) {
return "border-b-2 border-transparent px-4 py-3 text-sm font-medium text-muted-foreground/45 cursor-not-allowed";
}
return cn(
"relative -mb-px shrink-0 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",
);
}
type AdminSubnavProps = {
children: ReactNode;
className?: string;
/** Accessible name for the tab list */
"aria-label": string;
};
/** Underline-style horizontal tab bar used across admin page headers. */
export function AdminSubnav({ children, className, "aria-label": ariaLabel }: AdminSubnavProps) {
return (
<nav
aria-label={ariaLabel}
className={cn(
"flex w-full flex-wrap items-end gap-1 border-b border-border/60 px-1",
className,
)}
>
{children}
</nav>
);
}
type AdminSubnavLinkProps = {
href: string;
active: boolean;
children: ReactNode;
className?: string;
disabled?: boolean;
disabledTitle?: string;
};
export function AdminSubnavLink({
href,
active,
children,
className,
disabled = false,
disabledTitle,
}: AdminSubnavLinkProps) {
if (disabled) {
return (
<span className={cn(adminSubnavItemClassName(false, true), className)} title={disabledTitle}>
{children}
</span>
);
}
return (
<Link href={href} className={cn(adminSubnavItemClassName(active), className)}>
{children}
</Link>
);
}
type AdminSubnavButtonProps = {
active: boolean;
onClick: () => void;
children: ReactNode;
className?: string;
count?: number;
disabled?: boolean;
};
export function AdminSubnavButton({
active,
onClick,
children,
className,
count,
disabled = false,
}: AdminSubnavButtonProps) {
return (
<button
type="button"
disabled={disabled}
onClick={onClick}
className={cn(adminSubnavItemClassName(active, disabled), className)}
>
{children}
{count !== undefined && count > 0 ? (
<span
className={cn(
"ml-1.5 inline-flex min-w-[1.25rem] items-center justify-center rounded-full px-1.5 py-0.5 text-[10px] font-medium tabular-nums",
active ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground",
)}
>
{count}
</span>
) : null}
</button>
);
}
type AdminSubnavBarProps = {
children: ReactNode;
trailing?: ReactNode;
className?: string;
};
/** Wrapper for subnav plus optional trailing controls (site selector, back link, etc.). */
export function AdminSubnavBar({ children, trailing, className }: AdminSubnavBarProps) {
return (
<div className={cn("flex w-full flex-wrap items-end justify-between gap-3", className)}>
{children}
{trailing ? <div className="shrink-0 pb-1">{trailing}</div> : null}
</div>
);
}

View File

@@ -24,7 +24,7 @@ function Tabs({
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
"group/tabs-list inline-flex w-fit items-end justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
@@ -59,9 +59,10 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:-mb-px group-data-[variant=line]/tabs-list:h-auto group-data-[variant=line]/tabs-list:shrink-0 group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:border-b-2 group-data-[variant=line]/tabs-list:border-transparent group-data-[variant=line]/tabs-list:px-4 group-data-[variant=line]/tabs-list:py-3 group-data-[variant=line]/tabs-list:text-muted-foreground group-data-[variant=line]/tabs-list:hover:border-border/80 group-data-[variant=line]/tabs-list:hover:text-foreground group-data-[variant=line]/tabs-list:data-active:border-primary group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=line]/tabs-list:data-active:text-primary dark:group-data-[variant=line]/tabs-list:data-active:border-primary dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:after:hidden group-data-[variant=line]/tabs-list:data-active:after:opacity-0",
className
)}
{...props}