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.
202 lines
7.5 KiB
TypeScript
202 lines
7.5 KiB
TypeScript
"use client";
|
|
|
|
import { ChevronDown } from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { cn } from "@/lib/utils";
|
|
import type { AdminPermissionCatalogData } from "@/types/api/admin-user";
|
|
|
|
type PermissionSelectorProps = {
|
|
catalog: AdminPermissionCatalogData | null;
|
|
selectedSlugs: string[];
|
|
onChange: (next: string[]) => void;
|
|
resolveGroupLabel: (key: string, fallback: string) => string;
|
|
resolvePermissionLabel: (slug: string, fallback: string) => string;
|
|
selectableSlugs?: string[] | null;
|
|
helperText?: string;
|
|
summaryText?: string;
|
|
emptyText: string;
|
|
heightClassName?: string;
|
|
};
|
|
|
|
export function AdminPermissionSelector({
|
|
catalog,
|
|
selectedSlugs,
|
|
onChange,
|
|
resolveGroupLabel,
|
|
resolvePermissionLabel,
|
|
selectableSlugs = null,
|
|
helperText,
|
|
summaryText,
|
|
emptyText,
|
|
heightClassName = "h-[52vh]",
|
|
}: PermissionSelectorProps): React.ReactElement {
|
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
|
const selectedSet = useMemo(() => new Set(selectedSlugs), [selectedSlugs]);
|
|
const allowedSet = useMemo(
|
|
() => (selectableSlugs ? new Set(selectableSlugs) : null),
|
|
[selectableSlugs],
|
|
);
|
|
|
|
const groups = useMemo(() => {
|
|
const rawGroups = catalog?.permission_menu_groups ?? [];
|
|
const mapped = rawGroups
|
|
.map((group) => ({
|
|
key: group.key,
|
|
label: resolveGroupLabel(group.key, group.label),
|
|
permissions: group.permissions.filter((permission) =>
|
|
allowedSet ? allowedSet.has(permission.slug) : true,
|
|
),
|
|
}))
|
|
.filter((group) => group.permissions.length > 0);
|
|
|
|
if (mapped.length > 0) {
|
|
return mapped;
|
|
}
|
|
|
|
const fallbackPermissions = (catalog?.permissions ?? []).filter((permission) =>
|
|
allowedSet ? allowedSet.has(permission.slug) : true,
|
|
);
|
|
|
|
if (fallbackPermissions.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{
|
|
key: "all",
|
|
label: resolveGroupLabel("all", "全部权限"),
|
|
permissions: fallbackPermissions,
|
|
},
|
|
];
|
|
}, [allowedSet, catalog, resolveGroupLabel]);
|
|
|
|
const totalCount = useMemo(
|
|
() => groups.reduce((sum, group) => sum + group.permissions.length, 0),
|
|
[groups],
|
|
);
|
|
|
|
const toggleOne = (slug: string, checked: boolean) => {
|
|
const next = new Set(selectedSet);
|
|
if (checked) {
|
|
next.add(slug);
|
|
} else {
|
|
next.delete(slug);
|
|
}
|
|
onChange(Array.from(next).sort());
|
|
};
|
|
|
|
const toggleGroup = (slugs: string[], checked: boolean) => {
|
|
const next = new Set(selectedSet);
|
|
for (const slug of slugs) {
|
|
if (checked) {
|
|
next.add(slug);
|
|
} else {
|
|
next.delete(slug);
|
|
}
|
|
}
|
|
onChange(Array.from(next).sort());
|
|
};
|
|
|
|
const toggleExpanded = (key: string) => {
|
|
setExpandedGroups((prev) => ({ ...prev, [key]: !(prev[key] ?? true) }));
|
|
};
|
|
|
|
const isExpanded = (key: string): boolean => expandedGroups[key] ?? true;
|
|
|
|
if (groups.length === 0 || totalCount === 0) {
|
|
return (
|
|
<div className="rounded-xl border border-dashed p-4">
|
|
<AdminNoResourceState compact className="py-4" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{helperText || summaryText ? (
|
|
<div className="flex flex-wrap items-center justify-between gap-2 rounded-xl border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
|
<span>{helperText}</span>
|
|
<span>{summaryText}</span>
|
|
</div>
|
|
) : null}
|
|
|
|
<ScrollArea className={cn("pr-3", heightClassName)}>
|
|
<div className="overflow-hidden rounded-xl border border-border/70 bg-background">
|
|
{groups.map((group) => {
|
|
const expanded = isExpanded(group.key);
|
|
const groupSlugs = group.permissions.map((permission) => permission.slug);
|
|
const selectedCount = groupSlugs.filter((slug) => selectedSet.has(slug)).length;
|
|
const checkedState =
|
|
selectedCount === 0 ? false : selectedCount === groupSlugs.length ? true : "indeterminate";
|
|
|
|
return (
|
|
<div key={group.key} className={cn("border-b border-border/60 last:border-b-0", expanded && "bg-muted/10")}>
|
|
<div className="flex items-center gap-3 px-4 py-3 text-sm transition-colors hover:bg-muted/20">
|
|
<button
|
|
type="button"
|
|
className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted"
|
|
onClick={() => toggleExpanded(group.key)}
|
|
aria-label={expanded ? "collapse" : "expand"}
|
|
>
|
|
<ChevronDown aria-hidden className={cn("size-4 transition-transform", expanded && "rotate-180")} />
|
|
</button>
|
|
<Checkbox
|
|
checked={checkedState === true}
|
|
indeterminate={checkedState === "indeterminate"}
|
|
onCheckedChange={(value) => toggleGroup(groupSlugs, value === true)}
|
|
/>
|
|
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => toggleExpanded(group.key)}>
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<span className="min-w-0 truncate text-[15px] font-medium leading-6 text-foreground">
|
|
{group.label}
|
|
</span>
|
|
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">
|
|
{group.permissions.length}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
<span className="shrink-0 tabular-nums text-xs text-muted-foreground">
|
|
{selectedCount}/{group.permissions.length}
|
|
</span>
|
|
</div>
|
|
{expanded ? (
|
|
<div className="pb-2">
|
|
{group.permissions.map((permission, index) => (
|
|
<label
|
|
key={permission.slug}
|
|
className={cn(
|
|
"flex cursor-pointer items-start gap-3 px-4 py-2.5 text-sm transition-colors hover:bg-muted/20",
|
|
index === 0 && "border-t border-border/50",
|
|
selectedSet.has(permission.slug) && "bg-muted/20",
|
|
)}
|
|
>
|
|
<span className="mt-1 flex h-4 w-8 shrink-0 items-center">
|
|
<span className="h-px w-full bg-border/70" />
|
|
</span>
|
|
<Checkbox
|
|
className="mt-0.5"
|
|
checked={selectedSet.has(permission.slug)}
|
|
onCheckedChange={(value) => toggleOne(permission.slug, value === true)}
|
|
/>
|
|
<span className="min-w-0">
|
|
<div className="whitespace-normal break-words leading-6 text-foreground">
|
|
{resolvePermissionLabel(permission.slug, permission.name)}
|
|
</div>
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
);
|
|
}
|