import { useState, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { rbacApi, AdminRole, PermissionSection, CreateRolePayload, UpdateRolePayload, } from '@/api/rbac'; import { PermissionGate } from '@/components/auth/PermissionGate'; import { usePermissionStore } from '@/store/permissions'; import { usePlatform } from '@/platform/hooks/usePlatform'; // === Icons === const BackIcon = () => ( ); const PlusIcon = () => ( ); const EditIcon = () => ( ); const TrashIcon = () => ( ); const ChevronDownIcon = ({ className }: { className?: string }) => ( ); const XMarkIcon = () => ( ); const ShieldIcon = () => ( ); // === Constants === const ROLE_COLORS = [ '#6366f1', // indigo '#8b5cf6', // violet '#a855f7', // purple '#ec4899', // pink '#ef4444', // red '#f97316', // orange '#eab308', // yellow '#22c55e', // green '#14b8a6', // teal '#06b6d4', // cyan '#3b82f6', // blue '#6b7280', // gray ]; const PRESETS: Record = { moderator: ['users:read', 'users:edit', 'users:block', 'tickets:*', 'ban_system:*'], marketer: [ 'campaigns:*', 'broadcasts:*', 'promocodes:*', 'promo_offers:*', 'promo_groups:*', 'stats:read', 'pinned_messages:*', 'wheel:*', ], support: ['tickets:read', 'tickets:reply', 'users:read'], }; // === Types === interface RoleFormData { name: string; description: string; level: number; color: string; permissions: string[]; } const INITIAL_FORM: RoleFormData = { name: '', description: '', level: 50, color: ROLE_COLORS[0], permissions: [], }; // === Sub-components === interface PermissionMatrixProps { registry: PermissionSection[]; selectedPermissions: string[]; onToggle: (perm: string) => void; onToggleSection: (section: string, actions: string[]) => void; } function PermissionMatrix({ registry, selectedPermissions, onToggle, onToggleSection, }: PermissionMatrixProps) { const { t } = useTranslation(); const [expanded, setExpanded] = useState>({}); const toggleExpand = useCallback((section: string) => { setExpanded((prev) => ({ ...prev, [section]: !prev[section] })); }, []); const isSectionFullySelected = useCallback( (section: string, actions: string[]) => { return actions.every((action) => { const perm = `${section}:${action}`; return selectedPermissions.includes(perm) || selectedPermissions.includes(`${section}:*`); }); }, [selectedPermissions], ); const isSectionPartiallySelected = useCallback( (section: string, actions: string[]) => { const hasAny = actions.some((action) => { const perm = `${section}:${action}`; return selectedPermissions.includes(perm) || selectedPermissions.includes(`${section}:*`); }); return hasAny && !isSectionFullySelected(section, actions); }, [selectedPermissions, isSectionFullySelected], ); const isPermSelected = useCallback( (section: string, action: string) => { const perm = `${section}:${action}`; return selectedPermissions.includes(perm) || selectedPermissions.includes(`${section}:*`); }, [selectedPermissions], ); return (
{registry.map((section) => { const isExpanded = expanded[section.section] ?? false; const allSelected = isSectionFullySelected(section.section, section.actions); const partialSelected = isSectionPartiallySelected(section.section, section.actions); return (
{/* Section header */}
{/* Actions list */} {isExpanded && (
{section.actions.map((action) => { const perm = `${section.section}:${action}`; const selected = isPermSelected(section.section, action); return ( ); })}
)}
); })}
); } // === Main Page === export default function AdminRoles() { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { capabilities } = usePlatform(); const canManageRole = usePermissionStore((s) => s.canManageRole); // Modal state const [modalOpen, setModalOpen] = useState(false); const [editingRole, setEditingRole] = useState(null); const [formData, setFormData] = useState(INITIAL_FORM); const [deleteConfirm, setDeleteConfirm] = useState(null); const [formError, setFormError] = useState(null); // Queries const { data: roles, isLoading: rolesLoading, error: rolesError, } = useQuery({ queryKey: ['admin-roles'], queryFn: rbacApi.getRoles, }); const { data: permissionRegistry } = useQuery({ queryKey: ['admin-permission-registry'], queryFn: rbacApi.getPermissionRegistry, }); // Mutations const createMutation = useMutation({ mutationFn: (payload: CreateRolePayload) => rbacApi.createRole(payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-roles'] }); closeModal(); }, onError: () => { setFormError(t('admin.roles.errors.createFailed')); }, }); const updateMutation = useMutation({ mutationFn: ({ id, payload }: { id: number; payload: UpdateRolePayload }) => rbacApi.updateRole(id, payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-roles'] }); closeModal(); }, onError: () => { setFormError(t('admin.roles.errors.updateFailed')); }, }); const deleteMutation = useMutation({ mutationFn: rbacApi.deleteRole, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-roles'] }); setDeleteConfirm(null); }, onError: () => { setDeleteConfirm(null); setFormError(t('admin.roles.errors.deleteFailed')); }, }); // Handlers const closeModal = useCallback(() => { setModalOpen(false); setEditingRole(null); setFormData(INITIAL_FORM); setFormError(null); }, []); const openCreateModal = useCallback(() => { setEditingRole(null); setFormData(INITIAL_FORM); setFormError(null); setModalOpen(true); }, []); const openEditModal = useCallback((role: AdminRole) => { setEditingRole(role); setFormData({ name: role.name, description: role.description || '', level: role.level, color: role.color || ROLE_COLORS[0], permissions: [...role.permissions], }); setFormError(null); setModalOpen(true); }, []); const handleTogglePermission = useCallback((perm: string) => { setFormData((prev) => { const has = prev.permissions.includes(perm); return { ...prev, permissions: has ? prev.permissions.filter((p) => p !== perm) : [...prev.permissions, perm], }; }); }, []); const handleToggleSection = useCallback((section: string, actions: string[]) => { setFormData((prev) => { const allPerms = actions.map((a) => `${section}:${a}`); const allSelected = allPerms.every( (p) => prev.permissions.includes(p) || prev.permissions.includes(`${section}:*`), ); if (allSelected) { // Deselect all in this section (both individual and wildcard) const sectionPerms = new Set([...allPerms, `${section}:*`]); return { ...prev, permissions: prev.permissions.filter((p) => !sectionPerms.has(p)), }; } // Select all: use wildcard const withoutSection = prev.permissions.filter((p) => !p.startsWith(`${section}:`)); return { ...prev, permissions: [...withoutSection, `${section}:*`], }; }); }, []); const handleApplyPreset = useCallback((presetKey: string) => { const presetPerms = PRESETS[presetKey]; if (!presetPerms) return; setFormData((prev) => ({ ...prev, permissions: [...presetPerms], })); }, []); const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); setFormError(null); if (!formData.name.trim()) { setFormError(t('admin.roles.errors.nameRequired')); return; } const payload = { name: formData.name.trim(), description: formData.description.trim() || null, level: formData.level, permissions: formData.permissions, color: formData.color, }; if (editingRole) { updateMutation.mutate({ id: editingRole.id, payload }); } else { createMutation.mutate(payload); } }, [formData, editingRole, createMutation, updateMutation, t], ); const isSaving = createMutation.isPending || updateMutation.isPending; // Sorted roles by level descending const sortedRoles = useMemo(() => { if (!roles) return []; return [...roles].sort((a, b) => b.level - a.level); }, [roles]); return (
{/* Header */}
{!capabilities.hasBackButton && ( )}

{t('admin.roles.title')}

{t('admin.roles.subtitle')}

{/* Stats Overview */} {sortedRoles.length > 0 && (
{sortedRoles.length}
{t('admin.roles.stats.totalRoles')}
{sortedRoles.filter((r) => r.is_active).length}
{t('admin.roles.stats.active')}
{sortedRoles.filter((r) => r.is_system).length}
{t('admin.roles.stats.system')}
)} {/* Roles List */} {rolesLoading ? (
) : rolesError ? (

{t('admin.roles.errors.loadFailed')}

) : sortedRoles.length === 0 ? (

{t('admin.roles.noRoles')}

) : (
{sortedRoles.map((role) => (
{/* Role name with color badge */}
{/* Info */}
{t('admin.roles.levelLabel')}: {role.level} {role.description && {role.description}} {t('admin.roles.usersCount', { count: role.user_count ?? 0 })} {t('admin.roles.permissionsCount', { count: role.permissions.length, })}
{/* Actions */}
))}
)} {/* Create / Edit Modal */} {modalOpen && (
{/* Backdrop */}