import { useState, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { rbacApi, PermissionSection, CreateRolePayload, UpdateRolePayload } from '@/api/rbac'; import { AdminBackButton } from '@/components/admin'; // === Icons === const ChevronDownIcon = ({ className }: { className?: string }) => ( ); // === Constants === const ROLE_COLORS = [ '#6366f1', '#8b5cf6', '#a855f7', '#ec4899', '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#6b7280', ]; const PRESETS: Record = { moderator: ['users:read', 'users:edit', 'users:block', 'tickets:*', 'ban_system:*'], marketer: [ 'campaigns:*', 'broadcasts:*', 'promocodes:*', 'promo_offers:*', 'promo_groups:*', 'stats:read', 'sales_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 (
{isExpanded && (
{section.actions.map((action) => { const perm = `${section.section}:${action}`; const selected = isPermSelected(section.section, action); return ( ); })}
)}
); })}
); } // === Main Page === export default function AdminRoleEdit() { const { t } = useTranslation(); const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const queryClient = useQueryClient(); const isEdit = !!id; const [formData, setFormData] = useState(INITIAL_FORM); const [formError, setFormError] = useState(null); // Fetch role for editing const { isLoading: isLoadingRole } = useQuery({ queryKey: ['admin-role', id], queryFn: () => rbacApi.getRoles(), enabled: isEdit, select: useCallback( (roles: import('@/api/rbac').AdminRole[]) => { const role = roles.find((r) => r.id === Number(id)); if (role) { setFormData({ name: role.name, description: role.description || '', level: role.level, color: role.color || ROLE_COLORS[0], permissions: [...role.permissions], }); } return role; }, [id], ), }); 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'] }); navigate('/admin/roles'); }, onError: () => { setFormError(t('admin.roles.errors.createFailed')); }, }); const updateMutation = useMutation({ mutationFn: ({ roleId, payload }: { roleId: number; payload: UpdateRolePayload }) => rbacApi.updateRole(roleId, payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-roles'] }); navigate('/admin/roles'); }, onError: () => { setFormError(t('admin.roles.errors.updateFailed')); }, }); // Handlers 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) { const sectionPerms = new Set([...allPerms, `${section}:*`]); return { ...prev, permissions: prev.permissions.filter((p) => !sectionPerms.has(p)), }; } 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 (isEdit) { updateMutation.mutate({ roleId: Number(id), payload }); } else { createMutation.mutate(payload); } }, [formData, isEdit, id, createMutation, updateMutation, t], ); const isSaving = createMutation.isPending || updateMutation.isPending; // Loading state if (isEdit && isLoadingRole) { return (
); } return (
{/* Header */}

{isEdit ? t('admin.roles.modal.editTitle') : t('admin.roles.modal.createTitle')}

{/* Form */}
{/* Name */}
setFormData((prev) => ({ ...prev, name: e.target.value }))} className="w-full rounded-lg border border-dark-600 bg-dark-900 px-3 py-2 text-dark-100 placeholder-dark-500 outline-none transition-colors focus:border-accent-500" placeholder={t('admin.roles.form.namePlaceholder')} autoFocus />
{/* Description */}