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:
2026-06-04 18:01:05 +08:00
parent c2eac2fafc
commit 65eaeecf8c
139 changed files with 8852 additions and 1435 deletions

View File

@@ -3,6 +3,7 @@
import { KeyRound, Pencil, Trash2 } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options";
import { useExportLabels } from "@/hooks/use-export-labels";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
@@ -18,6 +19,7 @@ import {
putAdminUserRoles,
} from "@/api/admin-users";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
@@ -36,6 +38,13 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Table,
@@ -49,7 +58,11 @@ import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { PRD_ADMIN_USER_MANAGE } from "@/lib/admin-prd";
import { cn } from "@/lib/utils";
import { useAdminProfile } from "@/stores/admin-session";
import type { AdminPermissionCatalogData, AdminUserPermissionRow } from "@/types/api/index";
import type {
AdminPermissionCatalogData,
AdminUserPermissionRow,
AdminUserSiteBinding,
} from "@/types/api/index";
import { LotteryApiBizError } from "@/types/api/errors";
export function AdminUsersConsole(): React.ReactElement {
@@ -58,6 +71,17 @@ export function AdminUsersConsole(): React.ReactElement {
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const exportLabels = useExportLabels("adminUsers");
const profile = useAdminProfile();
const { sites: hookSiteOptions } = useAdminSiteCodeOptions();
const siteOptions = useMemo(() => {
if (hookSiteOptions.length > 0) {
return hookSiteOptions;
}
return (profile?.accessible_sites ?? []).map((site) => ({
id: site.id,
code: site.code,
name: site.name,
}));
}, [hookSiteOptions, profile?.accessible_sites]);
const canManageUsers = adminHasAnyPermission(profile?.permissions, [PRD_ADMIN_USER_MANAGE]);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
@@ -86,6 +110,8 @@ export function AdminUsersConsole(): React.ReactElement {
const [formPassword, setFormPassword] = useState("");
const [formStatus, setFormStatus] = useState(0);
const [formCreateRoles, setFormCreateRoles] = useState<string[]>([]);
const [formAdminSiteId, setFormAdminSiteId] = useState<number | null>(null);
const [roleEditSiteId, setRoleEditSiteId] = useState<number | null>(null);
const [deleteTarget, setDeleteTarget] = useState<AdminUserPermissionRow | null>(null);
const [deleteBusy, setDeleteBusy] = useState(false);
@@ -99,6 +125,39 @@ export function AdminUsersConsole(): React.ReactElement {
[catalog],
);
const defaultSiteId = useMemo(() => siteOptions[0]?.id ?? null, [siteOptions]);
const roleEditSiteLabel = useMemo(() => {
const site = siteOptions.find((item) => item.id === roleEditSiteId);
return site ? `${site.name} (${site.code})` : null;
}, [roleEditSiteId, siteOptions]);
const formAdminSiteLabel = useMemo(() => {
const site = siteOptions.find((item) => item.id === formAdminSiteId);
return site ? `${site.name} (${site.code})` : null;
}, [formAdminSiteId, siteOptions]);
useAsyncEffect(() => {
if (formAdminSiteId === null && defaultSiteId !== null && accountOpen && accountMode === "create") {
setFormAdminSiteId(defaultSiteId);
}
}, [accountOpen, accountMode, defaultSiteId, formAdminSiteId]);
function formatSiteBindings(bindings: AdminUserSiteBinding[] | undefined): string {
if (!bindings || bindings.length === 0) {
return "";
}
return bindings
.map((b) => `${b.site_code}${b.role_slugs.length > 0 ? ` (${b.role_slugs.length})` : ""}`)
.join(", ");
}
function rolesForSite(bindings: AdminUserSiteBinding[] | undefined, siteId: number | null): string[] {
if (siteId === null) {
return [];
}
const match = bindings?.find((b) => b.site_id === siteId);
return match ? [...match.role_slugs].sort() : [];
}
const load = useCallback(async () => {
setLoading(true);
setErr(null);
@@ -149,8 +208,16 @@ export function AdminUsersConsole(): React.ReactElement {
}
function openPermissionEditor(row: AdminUserPermissionRow): void {
const bindings = row.site_bindings ?? [];
const initialSiteId =
bindings[0]?.site_id ?? defaultSiteId ?? siteOptions[0]?.id ?? null;
setSelectedId(row.id);
setDraftRoles([...row.roles].sort());
setRoleEditSiteId(initialSiteId);
setDraftRoles(
rolesForSite(bindings, initialSiteId).length > 0
? rolesForSite(bindings, initialSiteId)
: [...row.roles].sort(),
);
setPermissionOpen(true);
}
@@ -158,6 +225,7 @@ export function AdminUsersConsole(): React.ReactElement {
setPermissionOpen(open);
if (!open) {
setSelectedId(null);
setRoleEditSiteId(null);
}
}
@@ -170,6 +238,7 @@ export function AdminUsersConsole(): React.ReactElement {
setFormPassword("");
setFormStatus(0);
setFormCreateRoles([]);
setFormAdminSiteId(defaultSiteId);
setAccountOpen(true);
}
@@ -205,6 +274,10 @@ export function AdminUsersConsole(): React.ReactElement {
toast.error(t("roleRequired"));
return;
}
if (accountMode === "create" && (formAdminSiteId === null || formAdminSiteId <= 0)) {
toast.error(t("siteRequired"));
return;
}
setAccountSaving(true);
try {
@@ -224,6 +297,7 @@ export function AdminUsersConsole(): React.ReactElement {
email: formEmail.trim() === "" ? null : formEmail.trim(),
password: formPassword,
status: formStatus,
admin_site_id: formAdminSiteId as number,
role_slugs: formCreateRoles,
});
setItems((prev) => [created, ...prev]);
@@ -265,9 +339,16 @@ export function AdminUsersConsole(): React.ReactElement {
if (!selectedUser) {
return;
}
if (roleEditSiteId === null || roleEditSiteId <= 0) {
toast.error(t("siteRequired"));
return;
}
setSavingRoles(true);
try {
const result = await putAdminUserRoles(selectedUser.id, draftRoles);
const result = await putAdminUserRoles(selectedUser.id, {
admin_site_id: roleEditSiteId,
role_slugs: draftRoles,
});
setDraftRoles([...result.roles].sort());
setItems((prev) =>
prev.map((row) =>
@@ -275,6 +356,7 @@ export function AdminUsersConsole(): React.ReactElement {
? {
...row,
roles: result.roles,
site_bindings: result.site_bindings ?? row.site_bindings,
effective_permissions: result.effective_permissions,
}
: row,
@@ -378,6 +460,7 @@ export function AdminUsersConsole(): React.ReactElement {
<TableHead>{t("table.account")}</TableHead>
<TableHead>{t("table.nickname")}</TableHead>
<TableHead className="w-20 whitespace-nowrap">{t("table.status")}</TableHead>
<TableHead>{t("table.sites")}</TableHead>
<TableHead>{t("table.roles")}</TableHead>
<TableHead>{t("table.effective")}</TableHead>
<TableHead className="sticky right-0 z-20 bg-muted whitespace-nowrap text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("table.actions")}</TableHead>
@@ -385,13 +468,9 @@ export function AdminUsersConsole(): React.ReactElement {
</TableHeader>
<TableBody>
{loading && items.length === 0 ? (
<AdminTableLoadingRow colSpan={7} />
<AdminTableLoadingRow colSpan={8} />
) : items.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-muted-foreground">
{t("states.noData", { ns: "common" })}
</TableCell>
</TableRow>
<AdminTableNoResourceRow colSpan={8} className="text-muted-foreground" />
) : (
items.map((row) => (
<TableRow key={row.id}>
@@ -408,6 +487,9 @@ export function AdminUsersConsole(): React.ReactElement {
{row.status === 0 ? t("status.enabled") : t("status.disabled")}
</AdminStatusBadge>
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{formatSiteBindings(row.site_bindings) || "—"}
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{row.roles.length === 0 ? (
@@ -494,6 +576,33 @@ export function AdminUsersConsole(): React.ReactElement {
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-4">
<div className="space-y-3">
<p className="text-xs text-muted-foreground">{t("permissionDialog.rolesDescription")}</p>
{siteOptions.length > 0 ? (
<div className="space-y-1.5">
<Label htmlFor="role-edit-site">{t("permissionDialog.site")}</Label>
<Select
value={roleEditSiteId !== null ? String(roleEditSiteId) : undefined}
disabled={siteOptions.length <= 1}
onValueChange={(value) => {
const siteId = Number(value);
setRoleEditSiteId(siteId);
setDraftRoles(rolesForSite(selectedUser?.site_bindings, siteId));
}}
>
<SelectTrigger id="role-edit-site" className="h-9 w-full max-w-md">
<SelectValue placeholder={t("accountDialog.sitePlaceholder")}>
{roleEditSiteLabel ?? t("accountDialog.sitePlaceholder")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{siteOptions.map((site) => (
<SelectItem key={site.id} value={String(site.id)}>
{site.name} ({site.code})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
<div className="grid gap-3 rounded-md border p-3 sm:grid-cols-2">
{(catalog?.roles ?? []).map((role) => {
const checked = draftRoles.includes(role.slug);
@@ -606,6 +715,29 @@ export function AdminUsersConsole(): React.ReactElement {
onChange={(e) => setFormPassword(e.target.value)}
/>
</div>
{accountMode === "create" && siteOptions.length > 0 ? (
<div className="space-y-1.5">
<div className="text-sm font-medium leading-none">{t("accountDialog.site")}</div>
<Select
value={formAdminSiteId !== null ? String(formAdminSiteId) : undefined}
disabled={siteOptions.length <= 1}
onValueChange={(value) => setFormAdminSiteId(Number(value))}
>
<SelectTrigger className="h-9 w-full">
<SelectValue placeholder={t("accountDialog.sitePlaceholder")}>
{formAdminSiteLabel ?? t("accountDialog.sitePlaceholder")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{siteOptions.map((site) => (
<SelectItem key={site.id} value={String(site.id)}>
{site.name} ({site.code})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
{accountMode === "create" ? (
<div className="space-y-2">
<div className="text-sm font-medium leading-none">{t("accountDialog.rolesRequired")}</div>