486 lines
28 KiB
TypeScript
486 lines
28 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { isAuthenticated, getUser, clearAuth } from '@/lib/auth';
|
|
|
|
interface Agency {
|
|
id: string;
|
|
name: string;
|
|
subdomain: string;
|
|
domain: string;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
interface AgencyDetails {
|
|
access_url: string;
|
|
tenant: {
|
|
id: string;
|
|
name: string;
|
|
domain: string;
|
|
subdomain: string;
|
|
cnpj?: string;
|
|
razao_social?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
website?: string;
|
|
address?: string;
|
|
city?: string;
|
|
state?: string;
|
|
zip?: string;
|
|
description?: string;
|
|
industry?: string;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
admin?: {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
role: string;
|
|
created_at: string;
|
|
tenant_id?: string;
|
|
};
|
|
}
|
|
|
|
export default function PainelPage() {
|
|
const router = useRouter();
|
|
const [userData, setUserData] = useState<any>(null);
|
|
const [agencies, setAgencies] = useState<Agency[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [loadingAgencies, setLoadingAgencies] = useState(true);
|
|
const [selectedAgencyId, setSelectedAgencyId] = useState<string | null>(null);
|
|
const [selectedDetails, setSelectedDetails] = useState<AgencyDetails | null>(null);
|
|
const [detailsLoadingId, setDetailsLoadingId] = useState<string | null>(null);
|
|
const [detailsError, setDetailsError] = useState<string | null>(null);
|
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Verificar se usuário está logado
|
|
if (!isAuthenticated()) {
|
|
router.push('/login');
|
|
return;
|
|
}
|
|
|
|
const user = getUser();
|
|
if (user) {
|
|
// Verificar se é SUPERADMIN
|
|
if (user.role !== 'SUPERADMIN') {
|
|
alert('Acesso negado. Apenas SUPERADMIN pode acessar este painel.');
|
|
clearAuth();
|
|
router.push('/login');
|
|
return;
|
|
}
|
|
setUserData(user);
|
|
setLoading(false);
|
|
loadAgencies();
|
|
} else {
|
|
router.push('/login');
|
|
}
|
|
}, [router]);
|
|
|
|
const loadAgencies = async () => {
|
|
setLoadingAgencies(true);
|
|
try {
|
|
const response = await fetch('/api/admin/agencies');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setAgencies(data);
|
|
if (selectedAgencyId && !data.some((agency: Agency) => agency.id === selectedAgencyId)) {
|
|
setSelectedAgencyId(null);
|
|
setSelectedDetails(null);
|
|
}
|
|
} else {
|
|
console.error('Erro ao carregar agências');
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao carregar agências:', error);
|
|
} finally {
|
|
setLoadingAgencies(false);
|
|
}
|
|
};
|
|
|
|
const handleViewDetails = async (agencyId: string) => {
|
|
setDetailsError(null);
|
|
setDetailsLoadingId(agencyId);
|
|
setSelectedAgencyId(agencyId);
|
|
setSelectedDetails(null);
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/agencies/${agencyId}`);
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
setDetailsError(data?.error || 'Não foi possível carregar os detalhes da agência.');
|
|
setSelectedAgencyId(null);
|
|
return;
|
|
}
|
|
|
|
setSelectedDetails(data);
|
|
} catch (error) {
|
|
console.error('Erro ao carregar detalhes da agência:', error);
|
|
setDetailsError('Erro ao carregar detalhes da agência.');
|
|
setSelectedAgencyId(null);
|
|
} finally {
|
|
setDetailsLoadingId(null);
|
|
}
|
|
};
|
|
|
|
const handleDeleteAgency = async (agencyId: string) => {
|
|
const confirmDelete = window.confirm('Tem certeza que deseja excluir esta agência? Esta ação não pode ser desfeita.');
|
|
if (!confirmDelete) {
|
|
return;
|
|
}
|
|
|
|
setDeletingId(agencyId);
|
|
try {
|
|
const response = await fetch(`/api/admin/agencies/${agencyId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
if (!response.ok && response.status !== 204) {
|
|
const data = await response.json().catch(() => ({ error: 'Erro ao excluir agência.' }));
|
|
alert(data?.error || 'Erro ao excluir agência.');
|
|
return;
|
|
}
|
|
|
|
alert('Agência excluída com sucesso!');
|
|
if (selectedAgencyId === agencyId) {
|
|
setSelectedAgencyId(null);
|
|
setSelectedDetails(null);
|
|
}
|
|
|
|
await loadAgencies();
|
|
} catch (error) {
|
|
console.error('Erro ao excluir agência:', error);
|
|
alert('Erro ao excluir agência.');
|
|
} finally {
|
|
setDeletingId(null);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#FF3A05] mx-auto mb-4"></div>
|
|
<p className="text-gray-600 dark:text-gray-400">Carregando...</p>
|
|
</div>
|
|
{detailsLoadingId && (
|
|
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-dashed border-[#FF3A05] p-6 text-sm text-gray-600 dark:text-gray-300">
|
|
Carregando detalhes da agência selecionada...
|
|
</div>
|
|
)}
|
|
|
|
{detailsError && !detailsLoadingId && (
|
|
<div className="mt-8 bg-red-50 dark:bg-red-900/40 border border-red-200 dark:border-red-800 rounded-lg p-6 text-red-700 dark:text-red-200">
|
|
{detailsError}
|
|
</div>
|
|
)}
|
|
|
|
{selectedDetails && !detailsLoadingId && (
|
|
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Detalhes da Agência</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Informações enviadas no cadastro e dados administrativos</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<a
|
|
href={selectedDetails.access_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-[#FF3A05] hover:text-[#FF0080]"
|
|
>
|
|
Abrir painel da agência
|
|
</a>
|
|
<button
|
|
onClick={() => {
|
|
setSelectedAgencyId(null);
|
|
setSelectedDetails(null);
|
|
setDetailsError(null);
|
|
}}
|
|
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
|
>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-6 py-6 space-y-6">
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Dados da Agência</h4>
|
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Nome Fantasia</p>
|
|
<p className="text-gray-900 dark:text-white font-medium">{selectedDetails.tenant.name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Razão Social</p>
|
|
<p className="text-gray-900 dark:text-white font-medium">{selectedDetails.tenant.razao_social || '—'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">CNPJ</p>
|
|
<p className="text-gray-900 dark:text-white font-medium">{selectedDetails.tenant.cnpj || '—'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Segmento</p>
|
|
<p className="text-gray-900 dark:text-white font-medium">{selectedDetails.tenant.industry || '—'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Descrição</p>
|
|
<p className="text-gray-900 dark:text-white">{selectedDetails.tenant.description || '—'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Status</p>
|
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${selectedDetails.tenant.is_active ? 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300' : 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-200'}`}>
|
|
{selectedDetails.tenant.is_active ? 'Ativa' : 'Inativa'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Endereço e Contato</h4>
|
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Endereço completo</p>
|
|
<p className="text-gray-900 dark:text-white">{selectedDetails.tenant.address || '—'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Cidade / Estado</p>
|
|
<p className="text-gray-900 dark:text-white">{selectedDetails.tenant.city || '—'} {selectedDetails.tenant.state ? `- ${selectedDetails.tenant.state}` : ''}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">CEP</p>
|
|
<p className="text-gray-900 dark:text-white">{selectedDetails.tenant.zip || '—'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Website</p>
|
|
{selectedDetails.tenant.website ? (
|
|
<a href={selectedDetails.tenant.website} target="_blank" rel="noopener noreferrer" className="text-[#FF3A05] hover:text-[#FF0080]">
|
|
{selectedDetails.tenant.website}
|
|
</a>
|
|
) : (
|
|
<p className="text-gray-900 dark:text-white">—</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">E-mail comercial</p>
|
|
<p className="text-gray-900 dark:text-white">{selectedDetails.tenant.email || '—'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Telefone</p>
|
|
<p className="text-gray-900 dark:text-white">{selectedDetails.tenant.phone || '—'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Administrador da Agência</h4>
|
|
{selectedDetails.admin ? (
|
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Nome</p>
|
|
<p className="text-gray-900 dark:text-white font-medium">{selectedDetails.admin.name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">E-mail</p>
|
|
<p className="text-gray-900 dark:text-white">{selectedDetails.admin.email}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Perfil</p>
|
|
<p className="text-gray-900 dark:text-white">{selectedDetails.admin.role}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 dark:text-gray-400">Criado em</p>
|
|
<p className="text-gray-900 dark:text-white">{new Date(selectedDetails.admin.created_at).toLocaleString('pt-BR')}</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400">Nenhum administrador associado encontrado.</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-xs text-gray-400 dark:text-gray-500">
|
|
Última atualização: {new Date(selectedDetails.tenant.updated_at).toLocaleString('pt-BR')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
{/* Header */}
|
|
<header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shadow-sm">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center justify-center w-10 h-10 bg-gradient-to-r from-[#FF3A05] to-[#FF0080] rounded-lg">
|
|
<span className="text-white font-bold text-lg">A</span>
|
|
</div>
|
|
<div>
|
|
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Aggios</h1>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Painel Administrativo</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-right">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-white">Admin AGGIOS</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">{userData?.email}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
clearAuth();
|
|
router.push('/login');
|
|
}}
|
|
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
|
|
>
|
|
Sair
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total de Agências</p>
|
|
<p className="text-3xl font-bold text-gray-900 dark:text-white mt-2">{agencies.length}</p>
|
|
</div>
|
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
|
|
<svg className="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Agências Ativas</p>
|
|
<p className="text-3xl font-bold text-gray-900 dark:text-white mt-2">{agencies.filter(a => a.is_active).length}</p>
|
|
</div>
|
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
|
|
<svg className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Agências Inativas</p>
|
|
<p className="text-3xl font-bold text-gray-900 dark:text-white mt-2">{agencies.filter(a => !a.is_active).length}</p>
|
|
</div>
|
|
<div className="w-12 h-12 bg-red-100 dark:bg-red-900 rounded-lg flex items-center justify-center">
|
|
<svg className="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Agencies Table */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Agências Cadastradas</h2>
|
|
</div>
|
|
|
|
{loadingAgencies ? (
|
|
<div className="p-8 text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#FF3A05] mx-auto mb-4"></div>
|
|
<p className="text-gray-600 dark:text-gray-400">Carregando agências...</p>
|
|
</div>
|
|
) : agencies.length === 0 ? (
|
|
<div className="p-8 text-center">
|
|
<p className="text-gray-600 dark:text-gray-400">Nenhuma agência cadastrada ainda.</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 dark:bg-gray-900">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Agência</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Subdomínio</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Domínio</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Data de Criação</th>
|
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
{agencies.map((agency) => (
|
|
<tr
|
|
key={agency.id}
|
|
className={`hover:bg-gray-50 dark:hover:bg-gray-700 ${selectedAgencyId === agency.id ? 'bg-orange-50/60 dark:bg-gray-700/60' : ''}`}
|
|
>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10 bg-gradient-to-br from-[#FF3A05] to-[#FF0080] rounded-lg flex items-center justify-center">
|
|
<span className="text-white font-bold">{agency.name.charAt(0).toUpperCase()}</span>
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900 dark:text-white">{agency.name}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900 dark:text-white font-mono">{agency.subdomain}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">{agency.domain || '-'}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${agency.is_active
|
|
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
|
|
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'
|
|
}`}>
|
|
{agency.is_active ? 'Ativa' : 'Inativa'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
{new Date(agency.created_at).toLocaleDateString('pt-BR')}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
|
<button
|
|
onClick={() => handleViewDetails(agency.id)}
|
|
className="inline-flex items-center px-3 py-1.5 rounded-md bg-[#FF3A05] text-white hover:bg-[#FF0080] transition"
|
|
disabled={detailsLoadingId === agency.id || deletingId === agency.id}
|
|
>
|
|
{detailsLoadingId === agency.id ? 'Carregando...' : 'Visualizar'}
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteAgency(agency.id)}
|
|
className="inline-flex items-center px-3 py-1.5 rounded-md border border-red-500 text-red-600 hover:bg-red-500 hover:text-white transition disabled:opacity-60"
|
|
disabled={deletingId === agency.id || detailsLoadingId === agency.id}
|
|
>
|
|
{deletingId === agency.id ? 'Excluindo...' : 'Excluir'}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|