import React, { useState, useEffect, useCallback } from 'react'; import { useUser, useOrganization } from '@clerk/clerk-react'; import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Upload, Info, Image as ImageIcon, Box, Database } from 'lucide-react'; import { clsx } from 'clsx'; import type { AppUser, UserRole } from '../types'; import { useAuth } from '../context/useAuth'; import api from '../services/api'; import { GeometrySettings } from '../components/admin/GeometrySettings'; import { BackupRestore } from '../components/admin/BackupRestore'; const roleLabels: Record = { admin: { label: 'Administrador', color: 'bg-amber-500/20 text-amber-400 border-amber-500/30', icon: }, user: { label: 'Usuário', color: 'bg-primary/20 text-primary border-primary/30', icon: }, guest: { label: 'Convidado', color: 'bg-gray-500/20 text-gray-400 border-gray-500/30', icon: }, }; export const AdminDashboard: React.FC = () => { const { user } = useUser(); const { organization } = useOrganization(); const { isAdmin } = useAuth(); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [filterRole, setFilterRole] = useState('all'); const [actionLoading, setActionLoading] = useState(null); const [activeTab, setActiveTab] = useState<'users' | 'organization' | 'settings' | 'stock' | 'backup'>('users'); const [logoLoading, setLogoLoading] = useState(false); const fetchUsers = useCallback(async () => { if (!user || !organization?.id) return; try { setIsLoading(true); const response = await api.get('/users'); setUsers(response.data.map((u: AppUser) => ({ ...u, id: u._id || u.id }))); } catch (error) { console.error('Error fetching users:', error); } finally { setIsLoading(false); } }, [user, organization?.id]); const syncOrganizationMembers = useCallback(async () => { if (!organization) return; try { setIsLoading(true); // Fetch ALL members from Clerk (handle pagination) console.log('Fetching members from Clerk organization:', organization.id); let allMembers: any[] = []; let hasMore = true; // Fetch all pages while (hasMore) { const clerkMembers = await organization.getMemberships(); console.log(`Fetched members:`, clerkMembers.data.length); if (clerkMembers.data.length === 0) { hasMore = false; } else { allMembers = clerkMembers.data; hasMore = false; // Clerk retorna todos de uma vez normalmente } } console.log('Total Clerk members fetched:', allMembers.length, allMembers); // Get current users from database const currentUsersResponse = await api.get('/users'); const currentUsers = currentUsersResponse.data; console.log('Current users in database:', currentUsers.length, currentUsers); // Create a Set of Clerk user IDs for fast lookup const clerkUserIds = new Set( allMembers .map(m => m.publicUserData?.userId) .filter(id => id != null) ); console.log('Clerk user IDs:', Array.from(clerkUserIds)); // Step 1: Add/Update users from Clerk for (const membership of allMembers) { const clerkUser = membership.publicUserData; console.log('Processing membership:', membership); console.log('Public user data:', clerkUser); if (clerkUser) { const syncData = { clerkId: clerkUser.userId, email: clerkUser.identifier || '', name: `${clerkUser.firstName || ''} ${clerkUser.lastName || ''}`.trim() || clerkUser.identifier || 'Usuário', organizationId: organization.id, clerkRole: membership.role }; console.log('Syncing user:', syncData); try { const response = await api.post('/users/sync', syncData); console.log('Sync success for', clerkUser.userId, ':', response.data); } catch (syncError) { console.error('Error syncing member:', clerkUser.userId, syncError); } } } // Step 2: Remove users from database that don't exist in Clerk anymore let removedCount = 0; for (const dbUser of currentUsers) { const clerkUserId = dbUser.clerkUserId || dbUser.clerkId; if (!clerkUserIds.has(clerkUserId)) { console.log(`User ${dbUser.name} (${clerkUserId}) is in DB but not in Clerk - removing...`); try { await api.delete(`/users/${dbUser._id}`); console.log(`Removed user ${dbUser.name} from database`); removedCount++; } catch (deleteError) { console.error(`Error removing user ${dbUser.name}:`, deleteError); } } } // Reload users after sync console.log('Reloading users from database...'); await fetchUsers(); const message = `Sincronização concluída!\n✅ ${allMembers.length} membros atualizados\n${removedCount > 0 ? `🗑️ ${removedCount} membros removidos` : ''}`; alert(message); } catch (error) { console.error('Error syncing organization members:', error); alert('Erro ao sincronizar membros. Verifique o console para mais detalhes.'); } finally { setIsLoading(false); } }, [organization, fetchUsers]); useEffect(() => { fetchUsers(); }, [fetchUsers]); const handleRoleChange = async (userId: string, newRole: UserRole) => { if (!user) return; setActionLoading(userId); try { const response = await api.patch(`/users/${userId}/role`, { role: newRole }); const updated = response.data; setUsers(users.map(u => u.id === userId ? { ...updated, id: updated._id || updated.id } : u)); } catch (error: unknown) { const err = error as { response?: { data?: { error?: string } } }; console.error('Error updating role:', error); alert(err.response?.data?.error || 'Erro ao atualizar role'); } finally { setActionLoading(null); } }; const handleToggleBan = async (userId: string, isBanned: boolean) => { if (!user) return; setActionLoading(userId); try { const response = await api.patch(`/users/${userId}/ban`, { isBanned }); const updated = response.data; setUsers(users.map(u => u.id === userId ? { ...updated, id: updated._id || updated.id } : u)); } catch (error: unknown) { const err = error as { response?: { data?: { error?: string } } }; console.error('Error toggling ban:', error); alert(err.response?.data?.error || 'Erro ao alterar status'); } finally { setActionLoading(null); } }; const filteredUsers = users.filter(u => { const matchesSearch = u.name.toLowerCase().includes(searchTerm.toLowerCase()) || u.email.toLowerCase().includes(searchTerm.toLowerCase()); const matchesRole = filterRole === 'all' || u.role === filterRole; return matchesSearch && matchesRole; }); const handleLogoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !organization) return; // Validations const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml']; if (!validTypes.includes(file.type)) { alert('Por favor, selecione uma imagem PNG, JPG ou SVG.'); return; } if (file.size > 500 * 1024) { alert('O arquivo deve ter no máximo 500KB.'); return; } setLogoLoading(true); try { await organization.setLogo({ file }); alert('Logo atualizado com sucesso!'); } catch (error) { console.error('Error uploading logo:', error); alert('Erro ao atualizar o logo.'); } finally { setLogoLoading(false); } }; if (!isAdmin()) { return (

Acesso Negado

Você não tem permissão para acessar esta página.

); } return (
{/* Header */}

Administração

Configurações globais e gerenciamento de usuários

{activeTab === 'users' && (
)}
{/* Tabs Navigation */}
{activeTab === 'users' ? ( <> {/* Stats */}

{users.length}

Total

{users.filter(u => u.role === 'admin').length}

Admins

{users.filter(u => u.role === 'user').length}

Usuários

{users.filter(u => u.isBanned).length}

Banidos

{/* Filters */}
setSearchTerm(e.target.value)} className="w-full pl-12 pr-4 py-3 bg-surface border border-border/40 rounded-xl text-text-main placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all" />
{/* Users Table */}
{isLoading ? (
) : filteredUsers.length === 0 ? (

Nenhum usuário encontrado

) : (
{filteredUsers.map((u) => { const roleInfo = roleLabels[u.role]; const isCurrentUser = u.clerkId === user?.id; const isActionDisabled = actionLoading === u.id; return ( ); })}
Usuário Email Role Status Ações
{u.name.charAt(0).toUpperCase()}

{u.name}

{isCurrentUser && ( (Você) )}
{u.email} {u.isBanned ? ( Banido ) : ( Ativo )} {!isCurrentUser && u.role !== 'admin' && ( )}
)}
) : activeTab === 'organization' ? (
{/* Organization Settings Panel */}

Identidade Visual

Gerencie o logo da sua organização

{organization?.imageUrl ? (
{organization.name}
) : (
Sem Logo
)}
{logoLoading && (
Enviando logo...
)}

Requisitos & Dicas

Regras para um visual impecável

Formatos Suportados

Aceitamos arquivos nos formatos PNG, JPG ou SVG. O formato SVG é recomendado para máxima nitidez em qualquer tamanho.

Dimensões Recomendadas

Recomendamos uma imagem quadrada de no mínimo 512x512 pixels. Logos horizontais podem não aparecer corretamente em todas as áreas.

Limite de Tamanho

O arquivo não deve ultrapassar 500 KB. Arquivos maiores serão rejeitados automaticamente para garantir rapidez no carregamento.

) : activeTab === 'settings' ? ( ) : activeTab === 'backup' ? ( ) : ( // Lazily load or direct render StockDashboard (Need to import it)

Gestão de Estoque

Acesse a nova página dedicada ao controle de estoque.

Ir para Estoque
)}
); };