'use client'; import React, { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { useAuth } from '@/contexts/AuthContext'; import { useRouter } from 'next/navigation'; // ─── Types ──────────────────────────────────────────────────────────────────── interface RolePermission { id: string; role: string; can_publish_articles: boolean; can_edit_articles: boolean; can_delete_articles: boolean; can_manage_comments: boolean; can_moderate_forum: boolean; can_pin_threads: boolean; can_lock_threads: boolean; can_flag_content: boolean; can_view_analytics: boolean; can_manage_newsletter: boolean; can_invite_users: boolean; can_manage_users: boolean; updated_at: string; } interface UserProfile { id: string; email: string; full_name: string; role: string; is_active: boolean; created_at: string; avatar_url: string | null; } interface ActivityLog { id: string; action: string; performed_by: string | null; performed_by_email: string | null; affected_item_id: string | null; affected_item_type: string | null; affected_item_title: string | null; old_value: Record | null; new_value: Record | null; metadata: Record | null; created_at: string; user_profiles?: { full_name: string; email: string; role: string } | null; } type Tab = 'assign' | 'permissions' | 'activity'; // ─── Constants ──────────────────────────────────────────────────────────────── const ROLES = ['admin', 'editor', 'moderator', 'contributor', 'reader']; const ROLE_COLORS: Record = { admin: 'text-red-400 bg-red-400/10 border-red-400/20', editor: 'text-purple-400 bg-purple-400/10 border-purple-400/20', moderator: 'text-orange-400 bg-orange-400/10 border-orange-400/20', contributor: 'text-blue-400 bg-blue-400/10 border-blue-400/20', reader: 'text-[#888] bg-[#1a1a1a] border-[#333]', }; const ROLE_ICONS: Record = { admin: ( ), editor: ( ), moderator: ( ), contributor: ( ), reader: ( ), }; const PERMISSION_GROUPS = [ { label: 'Content', permissions: [ { key: 'can_publish_articles', label: 'Publish Articles' }, { key: 'can_edit_articles', label: 'Edit Articles' }, { key: 'can_delete_articles', label: 'Delete Articles' }, { key: 'can_manage_comments', label: 'Manage Comments' }, ], }, { label: 'Forum', permissions: [ { key: 'can_moderate_forum', label: 'Moderate Forum' }, { key: 'can_pin_threads', label: 'Pin Threads' }, { key: 'can_lock_threads', label: 'Lock Threads' }, { key: 'can_flag_content', label: 'Flag Content' }, ], }, { label: 'Platform', permissions: [ { key: 'can_view_analytics', label: 'View Analytics' }, { key: 'can_manage_newsletter', label: 'Manage Newsletter' }, { key: 'can_invite_users', label: 'Invite Users' }, { key: 'can_manage_users', label: 'Manage Users' }, ], }, ]; const ACTION_LABELS: Record = { article_created: 'Article Created', article_edited: 'Article Edited', article_published: 'Article Published', article_unpublished: 'Article Unpublished', article_deleted: 'Article Deleted', article_archived: 'Article Archived', article_status_changed: 'Status Changed', user_role_changed: 'Role Changed', role_assigned: 'Role Assigned', role_permission_updated: 'Permissions Updated', user_activated: 'User Activated', user_deactivated: 'User Deactivated', user_invited: 'User Invited', user_invitation_revoked: 'Invitation Revoked', import_completed: 'Import Completed', import_failed: 'Import Failed', }; const ACTION_COLORS: Record = { user_role_changed: 'text-orange-400 bg-orange-400/10', role_assigned: 'text-blue-400 bg-blue-400/10', role_permission_updated: 'text-purple-400 bg-purple-400/10', article_published: 'text-green-400 bg-green-400/10', article_deleted: 'text-red-400 bg-red-400/10', user_invited: 'text-blue-400 bg-blue-400/10', user_deactivated: 'text-red-400 bg-red-400/10', user_activated: 'text-green-400 bg-green-400/10', }; // ─── Helpers ────────────────────────────────────────────────────────────────── function timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); if (days < 30) return `${days}d ago`; return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } function getInitials(name: string): string { return name .split(' ') .map((n) => n[0]) .join('') .toUpperCase() .slice(0, 2); } // ─── Sub-components ─────────────────────────────────────────────────────────── function RoleBadge({ role }: { role: string }) { const color = ROLE_COLORS[role] || ROLE_COLORS['reader']; const icon = ROLE_ICONS[role] || ROLE_ICONS['reader']; return ( {icon} {role} ); } // ─── Main Page ──────────────────────────────────────────────────────────────── export default function AdminRolesPage() { const { user, loading } = useAuth(); const router = useRouter(); const [activeTab, setActiveTab] = useState('assign'); const [permissions, setPermissions] = useState([]); const [users, setUsers] = useState([]); const [activityLogs, setActivityLogs] = useState([]); const [loadingData, setLoadingData] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [selectedRole, setSelectedRole] = useState('all'); const [activityRoleFilter, setActivityRoleFilter] = useState('all'); const [editingRole, setEditingRole] = useState(null); const [editedPermissions, setEditedPermissions] = useState>({}); const [savingPermissions, setSavingPermissions] = useState(false); const [processingUserId, setProcessingUserId] = useState(null); const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null); useEffect(() => { if (!loading && !user) router.replace('/login?next=' + encodeURIComponent(window.location.pathname)); }, [user, loading, router]); const showNotification = (type: 'success' | 'error', message: string) => { setNotification({ type, message }); setTimeout(() => setNotification(null), 4000); }; const fetchPermissions = useCallback(async () => { const res = await fetch('/api/admin/roles?type=permissions'); if (res.ok) { const data = await res.json(); setPermissions(data.permissions || []); } }, []); const fetchUsers = useCallback(async () => { const res = await fetch('/api/admin/roles?type=users'); if (res.ok) { const data = await res.json(); setUsers(data.users || []); } }, []); const fetchActivity = useCallback(async () => { const res = await fetch(`/api/admin/roles?type=activity&role=${activityRoleFilter}&limit=60`); if (res.ok) { const data = await res.json(); setActivityLogs(data.logs || []); } }, [activityRoleFilter]); useEffect(() => { if (!user) return; setLoadingData(true); Promise.all([fetchPermissions(), fetchUsers()]).finally(() => setLoadingData(false)); }, [user, fetchPermissions, fetchUsers]); useEffect(() => { if (user && activeTab === 'activity') fetchActivity(); }, [user, activeTab, fetchActivity]); const handleAssignRole = async (userId: string, newRole: string) => { setProcessingUserId(userId); try { const res = await fetch('/api/admin/roles', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, newRole }), }); if (res.ok) { setUsers((prev) => prev.map((u) => u.id === userId ? { ...u, role: newRole } : u)); showNotification('success', 'Role updated successfully'); } else { showNotification('error', 'Failed to update role'); } } catch { showNotification('error', 'Failed to update role'); } finally { setProcessingUserId(null); } }; const startEditPermissions = (role: string) => { const perm = permissions.find((p) => p.role === role); if (perm) { setEditingRole(role); setEditedPermissions({ ...perm }); } }; const handleSavePermissions = async () => { if (!editingRole) return; setSavingPermissions(true); try { const res = await fetch('/api/admin/roles', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role: editingRole, permissions: editedPermissions }), }); if (res.ok) { setPermissions((prev) => prev.map((p) => p.role === editingRole ? { ...p, ...editedPermissions } : p) ); showNotification('success', `Permissions for ${editingRole} updated`); setEditingRole(null); setEditedPermissions({}); } else { showNotification('error', 'Failed to save permissions'); } } catch { showNotification('error', 'Failed to save permissions'); } finally { setSavingPermissions(false); } }; const filteredUsers = users.filter((u) => { const matchSearch = !searchQuery || u.email.toLowerCase().includes(searchQuery.toLowerCase()) || u.full_name.toLowerCase().includes(searchQuery.toLowerCase()); const matchRole = selectedRole === 'all' || u.role === selectedRole; return matchSearch && matchRole; }); const roleCounts = ROLES.reduce((acc, r) => { acc[r] = users.filter((u) => u.role === r).length; return acc; }, {} as Record); if (loading || loadingData) { return (

Loading roles panel…

); } if (!user) return null; return (
{/* Header */}

Roles & Permissions

Assign roles, manage permissions, and view role-based activity

{/* Notification */} {notification && (
{notification.type === 'success' ? : <> }

{notification.message}

)} {/* Role Summary Cards */}
{ROLES.map((role) => { const color = ROLE_COLORS[role] || ROLE_COLORS['reader']; return ( ); })}
{/* Tabs */}
{([ { id: 'assign', label: 'Assign Roles', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z' }, { id: 'permissions', label: 'Permissions', icon: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z' }, { id: 'activity', label: 'Activity Logs', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2' }, ] as { id: Tab; label: string; icon: string }[]).map((tab) => ( ))}
{/* ── Tab: Assign Roles ── */} {activeTab === 'assign' && (
{/* Toolbar */}
setSearchQuery(e.target.value)} className="w-full bg-[#0d0d0d] border border-[#252525] rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder-[#555] font-body focus:outline-none focus:border-[#3b82f6]" />
{/* User Table */}
{filteredUsers.length === 0 ? ( ) : ( filteredUsers.map((u) => ( )) )}
User Current Role Status Joined Assign Role
No users found
{u.avatar_url ? {u.full_name} : getInitials(u.full_name || u.email) }

{u.full_name || '—'}

{u.email}

{u.is_active ? 'Active' : 'Inactive'} {new Date(u.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
{processingUserId === u.id && (
)}
{filteredUsers.length > 0 && (

{filteredUsers.length} user{filteredUsers.length !== 1 ? 's' : ''} shown

)}
)} {/* ── Tab: Permissions ── */} {activeTab === 'permissions' && (
{permissions.map((perm) => { const isEditing = editingRole === perm.role; const current = isEditing ? editedPermissions : perm; const color = ROLE_COLORS[perm.role] || ROLE_COLORS['reader']; return (
{/* Role Header */}
{ROLE_ICONS[perm.role]} {perm.role}
{roleCounts[perm.role] || 0} users
{isEditing ? ( <> ) : ( perm.role !== 'admin' && ( ) )}
{/* Permission Groups */}
{PERMISSION_GROUPS.map((group) => (

{group.label}

{group.permissions.map(({ key, label }) => { const val = (current as Record)[key] as boolean; return (
{label} {isEditing ? ( ) : ( {val ? ( ) : ( )} )}
); })}
))}
{perm.role === 'admin' && (

Admin role has all permissions and cannot be modified.

)}
); })}
)} {/* ── Tab: Activity Logs ── */} {activeTab === 'activity' && (
{/* Toolbar */}

Role-Based Activity

Filter by role:
{['all', ...ROLES].map((r) => ( ))}
{/* Log List */}
{activityLogs.length === 0 ? (

No activity logs found for this filter

) : ( activityLogs.map((log) => { const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]'; const actionLabel = ACTION_LABELS[log.action] || log.action.replace(/_/g, ' '); const performer = log.user_profiles?.full_name || log.performed_by_email || 'System'; const performerRole = log.user_profiles?.role; return (
{/* Action badge */}
{actionLabel}
{/* Details */}
{performer} {performerRole && }
{log.affected_item_title && (

{log.affected_item_type && {log.affected_item_type}: } {log.affected_item_title}

)} {(log.old_value || log.new_value) && (
{log.old_value && ( {Object.entries(log.old_value).map(([k, v]) => `${k}: ${v}`).join(', ')} )} {log.old_value && log.new_value && ( )} {log.new_value && ( {Object.entries(log.new_value).map(([k, v]) => `${k}: ${v}`).join(', ')} )}
)}
{/* Time */} {timeAgo(log.created_at)}
); }) )}
{activityLogs.length > 0 && (

{activityLogs.length} log entries shown

)}
)}
); }