photum/frontend/pages/UserApproval.tsx
NANDO9322 8bea8d1162 feat(ui): refina detalhes do evento e aprovação de usuários
- Dashboard: Ajusta exibição do campo LOCAL para mostrar o nome do local.
- Dashboard: Atualiza exibição do ENDEREÇO para formato completo.
- UserApproval: Remove funcionalidade e botão de rejeitar usuários.
2025-12-18 10:23:25 -03:00

342 lines
14 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { useAuth } from "../contexts/AuthContext";
import {
getPendingUsers,
approveUser as apiApproveUser,
rejectUser as apiRejectUser,
} from "../services/apiService";
import { UserApprovalStatus } from "../types";
import {
CheckCircle,
XCircle,
Clock,
Search,
Filter,
Users,
Briefcase,
} from "lucide-react";
import { Button } from "../components/Button";
interface UserApprovalProps {
onNavigate?: (page: string) => void;
}
export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
const { token } = useAuth();
const [users, setUsers] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState<"ALL" | UserApprovalStatus>(
"ALL"
);
const [activeTab, setActiveTab] = useState<"cliente" | "profissional">(
"cliente"
);
const [isProcessing, setIsProcessing] = useState<string | null>(null);
const fetchUsers = async () => {
if (!token) {
setIsLoading(false);
return;
}
setIsLoading(true);
try {
const result = await getPendingUsers(token);
if (result.data) {
// Mapear dados do backend para o formato esperado pelo componente, se necessário
// Supondo que o backend retorna estrutura compatível ou fazemos o map aqui
const mappedUsers = result.data.map((u: any) => ({
...u,
approvalStatus: u.ativo
? UserApprovalStatus.APPROVED
: UserApprovalStatus.PENDING, // Simplificação, backend deve retornar status real se houver rejected
// Se o backend não retornar status explícito, assumimos pendente se !ativo
// Mas idealmente o backend retornaria um status enum
}));
setUsers(mappedUsers);
}
} catch (error) {
console.error("Erro ao buscar usuários:", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, [token]);
const handleApprove = async (userId: string) => {
if (!token) return;
setIsProcessing(userId);
try {
await apiApproveUser(userId, token);
// Atualizar lista após aprovação
await fetchUsers();
} catch (error) {
console.error("Erro ao aprovar usuário:", error);
alert("Erro ao aprovar usuário");
} finally {
setIsProcessing(null);
}
};
// Separar usuários Clientes (EVENT_OWNER) e Profissionais (PHOTOGRAPHER)
// Backend roles: PHOTOGRAPHER, EVENT_OWNER, BUSINESS_OWNER, SUPERADMIN
const clientUsers = users.filter(
(user) => user.role === "EVENT_OWNER"
);
const professionalUsers = users.filter(
(user) => user.role === "PHOTOGRAPHER"
);
// Filtrar usuários baseado na aba ativa
const currentUsers = activeTab === "cliente" ? clientUsers : professionalUsers;
const filteredUsers = currentUsers.filter((user) => {
const matchesSearch =
(user.name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(user.email || "").toLowerCase().includes(searchTerm.toLowerCase());
// Remover filtro por registeredInstitution se não vier do backend ainda
// Por enquanto, como o backend retorna apenas pendentes na rota /pending (conforme nome da rota),
// o statusFilter pode ser redundante se a rota só traz pendentes.
// Mas se o backend trouxer todos, o filtro funciona.
// Se a rota for /users/pending, assumimos que todos são pendentes.
// VAMOS ASSUMIR QUE O BACKEND SÓ RETORNA PENDENTES POR ENQUANTO.
// Mas para manter a UI, vamos considerar todos como Pendentes se status não vier.
return matchesSearch;
});
const getStatusBadge = (status: UserApprovalStatus) => {
// Se status undefined, assume pendente para visualização
const s = status || UserApprovalStatus.PENDING;
switch (s) {
case UserApprovalStatus.PENDING:
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<Clock className="w-3 h-3 mr-1" />
Pendente
</span>
);
case UserApprovalStatus.APPROVED:
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle className="w-3 h-3 mr-1" />
Aprovado
</span>
);
case UserApprovalStatus.REJECTED:
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<XCircle className="w-3 h-3 mr-1" />
Rejeitado
</span>
);
default:
return null;
}
};
return (
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-6 sm:mb-8">
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black">
Aprovação de Cadastros
</h1>
<p className="text-sm sm:text-base text-gray-600 mt-1">
Gerencie os cadastros pendentes de aprovação
</p>
</div>
{/* Tabs */}
<div className="mb-6 border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab("cliente")}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${activeTab === "cliente"
? "border-[#B9CF33] text-[#B9CF33]"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<Users className="w-5 h-5" />
Cadastros Empresas
<span
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "cliente"
? "bg-[#B9CF33] text-white"
: "bg-gray-200 text-gray-600"
}`}
>
{clientUsers.length}
</span>
</button>
<button
onClick={() => setActiveTab("profissional")}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${activeTab === "profissional"
? "border-[#B9CF33] text-[#B9CF33]"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<Briefcase className="w-5 h-5" />
Cadastros Profissionais
<span
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "profissional"
? "bg-[#B9CF33] text-white"
: "bg-gray-200 text-gray-600"
}`}
>
{professionalUsers.length}
</span>
</button>
</nav>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex flex-col sm:flex-row gap-4">
{/* Search */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Buscar por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-gold focus:border-transparent"
/>
</div>
</div>
</div>
{/* Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
{isLoading ? (
<div className="p-8 text-center text-gray-500">
Carregando solicitações...
</div>
) : (
<table key={activeTab} className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nome
</th>
{activeTab === "cliente" && (
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Empresa
</th>
)}
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Telefone
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Data de Cadastro
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredUsers.length === 0 ? (
<tr>
<td
colSpan={activeTab === "cliente" ? 7 : 6}
className="px-6 py-12 text-center text-gray-500"
>
<div className="flex flex-col items-center justify-center">
{activeTab === "cliente" ? (
<Users className="w-12 h-12 text-gray-300 mb-3" />
) : (
<Briefcase className="w-12 h-12 text-gray-300 mb-3" />
)}
<p className="text-lg font-medium">
{activeTab === "cliente"
? "Nenhum cadastro de empresa encontrado"
: "Nenhum cadastro profissional encontrado"}
</p>
</div>
</td>
</tr>
) : (
filteredUsers.map((user, index) => (
<tr
key={`${user.id}-${index}`}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{user.name || user.email}
</div>
</td>
{activeTab === "cliente" && (
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.company_name || "-"}
</div>
</td>
)}
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.email}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.phone || "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">
{user.created_at
? new Date(user.created_at).toLocaleDateString(
"pt-BR"
)
: "-"}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(
user.approvalStatus || UserApprovalStatus.PENDING
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex gap-2">
<Button
size="sm"
onClick={() => handleApprove(user.id)}
isLoading={isProcessing === user.id}
disabled={isProcessing !== null}
className="bg-green-600 hover:bg-green-700 text-white"
>
<CheckCircle className="w-4 h-4 mr-1" />
Aprovar
</Button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
)
}
</div >
</div >
</div >
</div >
);
};