977 lines
44 KiB
TypeScript
977 lines
44 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { useAuth } from "../contexts/AuthContext";
|
|
import {
|
|
getPendingUsers,
|
|
getAllUsers,
|
|
createAdminUser,
|
|
approveUser as apiApproveUser,
|
|
rejectUser as apiRejectUser,
|
|
updateUserRole,
|
|
getCompanies,
|
|
getFunctions,
|
|
} from "../services/apiService";
|
|
import { UserApprovalStatus, UserRole } from "../types";
|
|
import {
|
|
CheckCircle,
|
|
XCircle,
|
|
Clock,
|
|
Search,
|
|
Users,
|
|
Briefcase,
|
|
UserPlus,
|
|
RefreshCw,
|
|
} from "lucide-react";
|
|
import { Button } from "../components/Button";
|
|
import { Input } from "../components/Input";
|
|
import { formatPhone, formatCPFCNPJ, formatCEP } from "../utils/masks";
|
|
|
|
// INTERFACES
|
|
interface UserApprovalProps {
|
|
onNavigate?: (page: string) => void;
|
|
}
|
|
|
|
interface UserDetailsModalProps {
|
|
selectedUser: any | null;
|
|
onClose: () => void;
|
|
onApprove: (userId: string) => void;
|
|
isProcessing: string | null;
|
|
viewMode: "pending" | "all";
|
|
handleRoleChange: (userId: string, newRole: string) => void;
|
|
setSelectedUser: React.Dispatch<React.SetStateAction<any | null>>;
|
|
}
|
|
|
|
interface CreateUserModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (e: React.FormEvent) => void;
|
|
isCreating: boolean;
|
|
formData: any;
|
|
setFormData: React.Dispatch<React.SetStateAction<any>>;
|
|
}
|
|
|
|
// COMPONENT DEFINITIONS
|
|
const UserDetailsModal: React.FC<UserDetailsModalProps> = ({
|
|
selectedUser,
|
|
onClose,
|
|
onApprove,
|
|
isProcessing,
|
|
viewMode,
|
|
handleRoleChange,
|
|
setSelectedUser
|
|
}) => {
|
|
if (!selectedUser) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4 fade-in">
|
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg max-h-[95vh] overflow-y-auto animate-slide-up">
|
|
<div className="flex justify-between items-center p-6 border-b border-gray-100">
|
|
<h3 className="text-xl font-bold text-gray-900 font-serif">
|
|
Detalhes do Cadastro
|
|
</h3>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
<XCircle className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6 space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
Nome
|
|
</label>
|
|
<p className="mt-1 text-base text-gray-900 font-medium">
|
|
{selectedUser.name || "-"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
Email
|
|
</label>
|
|
<p className="mt-1 text-base text-gray-900">
|
|
{selectedUser.email || "-"}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
Telefone
|
|
</label>
|
|
<p className="mt-1 text-base text-gray-900">
|
|
{selectedUser.phone || "-"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
Data Cadastro
|
|
</label>
|
|
<p className="mt-1 text-base text-gray-900">
|
|
{selectedUser.created_at ? new Date(selectedUser.created_at).toLocaleDateString("pt-BR") : "-"}
|
|
</p>
|
|
</div>
|
|
|
|
{selectedUser.role === "EVENT_OWNER" && (
|
|
<div className="col-span-2">
|
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
Empresa Vinculada
|
|
</label>
|
|
<p className="mt-1 text-base text-gray-900 font-medium">
|
|
{selectedUser.company_name || "-"}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="pt-4 border-t border-gray-100">
|
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
Função / Cargo
|
|
</label>
|
|
{selectedUser.role === "EVENT_OWNER" ? (
|
|
<span className="inline-block px-3 py-1 bg-brand-gold/10 text-brand-gold rounded-full text-sm font-medium">
|
|
Cliente (Empresa)
|
|
</span>
|
|
) : (
|
|
<select
|
|
value={
|
|
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Cinegrafista" ? "Cinegrafista" :
|
|
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Recepcionista" ? "Recepcionista" :
|
|
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Fotógrafo" ? "Fotógrafo" :
|
|
selectedUser.role === "PHOTOGRAPHER" ? "Fotógrafo" :
|
|
selectedUser.role
|
|
}
|
|
onChange={(e) => {
|
|
let newRole = e.target.value;
|
|
if (["Cinegrafista", "Recepcionista", "Fotógrafo"].includes(newRole)) {
|
|
newRole = "PHOTOGRAPHER";
|
|
}
|
|
// Update local selected user state optimistic
|
|
setSelectedUser({...selectedUser, role: newRole});
|
|
handleRoleChange(selectedUser.id, newRole);
|
|
}}
|
|
className="w-full text-sm border-gray-300 rounded-md shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2"
|
|
disabled={viewMode === "all" && selectedUser.approvalStatus === UserApprovalStatus.APPROVED}
|
|
>
|
|
<option value="Fotógrafo">Fotógrafo</option>
|
|
<option value="Cinegrafista">Cinegrafista</option>
|
|
<option value="Recepcionista">Recepcionista</option>
|
|
<option value="RESEARCHER">Pesquisador</option>
|
|
<option value="BUSINESS_OWNER">Dono do Negócio</option>
|
|
<option value="SUPERADMIN">Super Admin</option>
|
|
</select>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 bg-gray-50 flex justify-end gap-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
>
|
|
Fechar
|
|
</Button>
|
|
{selectedUser.approvalStatus === UserApprovalStatus.PENDING && (
|
|
<Button
|
|
onClick={() => {
|
|
onApprove(selectedUser.id);
|
|
onClose();
|
|
}}
|
|
isLoading={isProcessing === selectedUser.id}
|
|
>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Aprovar Cadastro
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const CreateUserModal: React.FC<CreateUserModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
onSubmit,
|
|
isCreating,
|
|
formData,
|
|
setFormData
|
|
}) => {
|
|
// Fetch companies and functions
|
|
const [companies, setCompanies] = useState<any[]>([]);
|
|
const [functions, setFunctions] = useState<any[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (formData.role === "EVENT_OWNER") {
|
|
getCompanies().then(res => {
|
|
if(res.data) setCompanies(res.data);
|
|
});
|
|
}
|
|
if (formData.role === "PHOTOGRAPHER") {
|
|
getFunctions().then(res => {
|
|
if(res.data) setFunctions(res.data);
|
|
});
|
|
}
|
|
}, [formData.role]);
|
|
|
|
if (!isOpen) return null;
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4 fade-in">
|
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[95vh] overflow-y-auto animate-slide-up">
|
|
<div className="flex justify-between items-center p-6 border-b border-gray-100">
|
|
<h3 className="text-xl font-bold text-gray-900 font-serif">
|
|
Novo Usuário
|
|
</h3>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
<XCircle className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
<form onSubmit={onSubmit} className="p-6 space-y-4">
|
|
<Input
|
|
label="Nome Completo"
|
|
required
|
|
value={formData.nome}
|
|
onChange={(e) => setFormData({...formData, nome: e.target.value})}
|
|
/>
|
|
<Input
|
|
label="Email"
|
|
type="email"
|
|
required
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
|
/>
|
|
<Input
|
|
label="Telefone (Whatsapp)"
|
|
value={formData.telefone}
|
|
onChange={(e) => setFormData({...formData, telefone: formatPhone(e.target.value)})}
|
|
/>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Input
|
|
label="Senha"
|
|
type="password"
|
|
required
|
|
minLength={6}
|
|
value={formData.senha}
|
|
onChange={(e) => setFormData({...formData, senha: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tipo de Usuário
|
|
</label>
|
|
<select
|
|
value={formData.role}
|
|
onChange={(e) => setFormData({...formData, role: e.target.value})}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-photum-green focus:border-transparent outline-none transition-all"
|
|
>
|
|
<option value="PHOTOGRAPHER">Profissional</option>
|
|
<option value="EVENT_OWNER">Cliente (Empresa)</option>
|
|
<option value="BUSINESS_OWNER">Administrador</option>
|
|
<option value="RESEARCHER">Pesquisador</option>
|
|
</select>
|
|
</div>
|
|
|
|
{formData.role === "EVENT_OWNER" && (
|
|
<div className="space-y-4 border-t pt-4 mt-2">
|
|
<h4 className="font-medium text-gray-900 border-b pb-1 mb-3">Dados Cadastrais</h4>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Input
|
|
label="CPF/CNPJ"
|
|
value={formData.cpfCnpj || ''}
|
|
onChange={(e) => setFormData({...formData, cpfCnpj: formatCPFCNPJ(e.target.value)})}
|
|
placeholder="000.000.000-00"
|
|
maxLength={18}
|
|
/>
|
|
<Input
|
|
label="CEP"
|
|
value={formData.cep || ''}
|
|
onChange={(e) => setFormData({...formData, cep: formatCEP(e.target.value)})}
|
|
placeholder="00000-000"
|
|
onBlur={(e) => {
|
|
const cep = e.target.value.replace(/\D/g, '');
|
|
if (cep.length === 8) {
|
|
fetch(`https://viacep.com.br/ws/${cep}/json/`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (!data.erro) {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
endereco: data.logradouro,
|
|
bairro: data.bairro,
|
|
cidade: data.localidade,
|
|
estado: data.uf
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-[2fr_1fr] gap-4">
|
|
<Input
|
|
label="Endereço"
|
|
value={formData.endereco || ''}
|
|
onChange={(e) => setFormData({...formData, endereco: e.target.value})}
|
|
/>
|
|
<Input
|
|
label="Número"
|
|
value={formData.numero || ''}
|
|
onChange={(e) => setFormData({...formData, numero: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Input
|
|
label="Complemento"
|
|
value={formData.complemento || ''}
|
|
onChange={(e) => setFormData({...formData, complemento: e.target.value})}
|
|
/>
|
|
<Input
|
|
label="Bairro"
|
|
value={formData.bairro || ''}
|
|
onChange={(e) => setFormData({...formData, bairro: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-[2fr_1fr] gap-4">
|
|
<Input
|
|
label="Cidade"
|
|
value={formData.cidade || ''}
|
|
onChange={(e) => setFormData({...formData, cidade: e.target.value})}
|
|
/>
|
|
<Input
|
|
label="UF"
|
|
value={formData.estado || ''}
|
|
onChange={(e) => setFormData({...formData, estado: e.target.value})}
|
|
maxLength={2}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Empresa *
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.empresa_id || ''}
|
|
onChange={(e) => setFormData({...formData, empresa_id: e.target.value})}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-photum-green focus:border-transparent outline-none transition-all"
|
|
>
|
|
<option value="">Selecione uma empresa</option>
|
|
{companies.map(c => (
|
|
<option key={c.id} value={c.id}>{c.nome}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Show Address for Professionals too if not EVENT_OWNER (since we handled it above inside the block, wait - logic change needed) */}
|
|
{/* Actually, let's make the Address block generic for PHOTOGRAPHER and BUSINESS_OWNER too */}
|
|
{(formData.role === "PHOTOGRAPHER" || formData.role === "BUSINESS_OWNER") && (
|
|
<div className="space-y-4 border-t pt-4 mt-2">
|
|
<h4 className="font-medium text-gray-900 border-b pb-1 mb-3">Dados Profissionais</h4>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Input
|
|
label="CPF/CNPJ"
|
|
value={formData.cpfCnpj || ''}
|
|
onChange={(e) => setFormData({...formData, cpfCnpj: formatCPFCNPJ(e.target.value)})}
|
|
placeholder="000.000.000-00"
|
|
maxLength={18}
|
|
/>
|
|
<Input
|
|
label="CEP"
|
|
value={formData.cep || ''}
|
|
onChange={(e) => setFormData({...formData, cep: formatCEP(e.target.value)})}
|
|
placeholder="00000-000"
|
|
onBlur={(e) => {
|
|
const cep = e.target.value.replace(/\D/g, '');
|
|
if (cep.length === 8) {
|
|
fetch(`https://viacep.com.br/ws/${cep}/json/`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (!data.erro) {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
endereco: data.logradouro,
|
|
bairro: data.bairro,
|
|
cidade: data.localidade,
|
|
estado: data.uf
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-[2fr_1fr] gap-4">
|
|
<Input
|
|
label="Endereço"
|
|
value={formData.endereco || ''}
|
|
onChange={(e) => setFormData({...formData, endereco: e.target.value})}
|
|
/>
|
|
<Input
|
|
label="Número"
|
|
value={formData.numero || ''}
|
|
onChange={(e) => setFormData({...formData, numero: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Input
|
|
label="Complemento"
|
|
value={formData.complemento || ''}
|
|
onChange={(e) => setFormData({...formData, complemento: e.target.value})}
|
|
/>
|
|
<Input
|
|
label="Bairro"
|
|
value={formData.bairro || ''}
|
|
onChange={(e) => setFormData({...formData, bairro: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-[2fr_1fr] gap-4">
|
|
<Input
|
|
label="Cidade"
|
|
value={formData.cidade || ''}
|
|
onChange={(e) => setFormData({...formData, cidade: e.target.value})}
|
|
/>
|
|
<Input
|
|
label="UF"
|
|
value={formData.estado || ''}
|
|
onChange={(e) => setFormData({...formData, estado: e.target.value})}
|
|
maxLength={2}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{formData.role === "PHOTOGRAPHER" && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tipo de Profissional
|
|
</label>
|
|
<select
|
|
value={formData.professional_type}
|
|
onChange={(e) => setFormData({...formData, professional_type: e.target.value})}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-photum-green focus:border-transparent outline-none transition-all"
|
|
>
|
|
<option value="">Selecione...</option>
|
|
{functions.map(f => (
|
|
<option key={f.id} value={f.nome}>{f.nome}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="pt-4 flex justify-end gap-3">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={onClose}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
isLoading={isCreating}
|
|
>
|
|
Criar Usuário
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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 [viewMode, setViewMode] = useState<"pending" | "all">("pending");
|
|
const [activeTab, setActiveTab] = useState<"cliente" | "profissional">(
|
|
"cliente"
|
|
);
|
|
const [isProcessing, setIsProcessing] = useState<string | null>(null);
|
|
const [selectedUser, setSelectedUser] = useState<any | null>(null);
|
|
|
|
// Create User State
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [createFormData, setCreateFormData] = useState({
|
|
nome: "",
|
|
email: "",
|
|
senha: "",
|
|
role: "PHOTOGRAPHER",
|
|
telefone: "",
|
|
professional_type: "", // For photographer subtype
|
|
empresa_id: "",
|
|
cpfCnpj: "",
|
|
cep: "",
|
|
endereco: "",
|
|
numero: "",
|
|
complemento: "",
|
|
bairro: "",
|
|
cidade: "",
|
|
estado: "",
|
|
regiao: "",
|
|
});
|
|
|
|
const fetchUsers = async () => {
|
|
if (!token) {
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
setIsLoading(true);
|
|
try {
|
|
let result;
|
|
if (viewMode === "pending") {
|
|
result = await getPendingUsers(token);
|
|
} else {
|
|
result = await getAllUsers(token);
|
|
}
|
|
|
|
if (result.data) {
|
|
const mappedUsers = result.data.map((u: any) => ({
|
|
...u,
|
|
approvalStatus: u.ativo
|
|
? UserApprovalStatus.APPROVED
|
|
: UserApprovalStatus.PENDING,
|
|
// Ensure role is mapped if needed, backend sends "role"
|
|
}));
|
|
setUsers(mappedUsers);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao buscar usuários:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchUsers();
|
|
}, [token, viewMode]);
|
|
|
|
const handleApprove = async (userId: string) => {
|
|
if (!token) return;
|
|
setIsProcessing(userId);
|
|
try {
|
|
await apiApproveUser(userId, token);
|
|
await fetchUsers();
|
|
} catch (error) {
|
|
console.error("Erro ao aprovar usuário:", error);
|
|
alert("Erro ao aprovar usuário");
|
|
} finally {
|
|
setIsProcessing(null);
|
|
}
|
|
};
|
|
|
|
const handleRoleChange = async (userId: string, newRole: string) => {
|
|
if (!token) return;
|
|
try {
|
|
// Optimistic update
|
|
setUsers(prev => prev.map(u => u.id === userId ? {...u, role: newRole} : u));
|
|
|
|
await updateUserRole(userId, newRole, token);
|
|
} catch (error) {
|
|
console.error("Erro ao atualizar role:", error);
|
|
alert("Erro ao atualizar função do usuário");
|
|
fetchUsers();
|
|
}
|
|
};
|
|
|
|
const handleCreateUser = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if(!token) return;
|
|
setIsCreating(true);
|
|
try {
|
|
// Prepare payload
|
|
const payload = {
|
|
nome: createFormData.nome,
|
|
email: createFormData.email,
|
|
senha: createFormData.senha,
|
|
role: createFormData.role,
|
|
telefone: createFormData.telefone,
|
|
professional_type: createFormData.role === "PHOTOGRAPHER" ? createFormData.professional_type : undefined,
|
|
empresa_id: createFormData.role === "EVENT_OWNER" ? createFormData.empresa_id : undefined,
|
|
cpf_cnpj: createFormData.cpfCnpj,
|
|
cep: createFormData.cep,
|
|
endereco: createFormData.endereco,
|
|
numero: createFormData.numero,
|
|
complemento: createFormData.complemento,
|
|
bairro: createFormData.bairro,
|
|
cidade: createFormData.cidade,
|
|
estado: createFormData.estado,
|
|
regiao: createFormData.regiao || undefined
|
|
};
|
|
|
|
const result = await createAdminUser(payload, token);
|
|
if (result.error) {
|
|
alert(`Erro: ${result.error}`);
|
|
} else {
|
|
alert("Usuário criado com sucesso!");
|
|
setShowCreateModal(false);
|
|
setCreateFormData({
|
|
nome: "", email: "", senha: "", role: "PHOTOGRAPHER", telefone: "", professional_type: "", empresa_id: "",
|
|
cpfCnpj: "", cep: "", endereco: "", numero: "", complemento: "", bairro: "", cidade: "", estado: "", regiao: ""
|
|
});
|
|
fetchUsers();
|
|
}
|
|
} catch (err) {
|
|
alert("Erro ao criar usuário.");
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
|
|
// Separar usuários Clientes (EVENT_OWNER) e Profissionais
|
|
const clientUsers = users.filter(
|
|
(user) => user.role === "EVENT_OWNER"
|
|
);
|
|
const professionalUsers = users.filter(
|
|
(user) => user.role !== "EVENT_OWNER"
|
|
);
|
|
|
|
// 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());
|
|
|
|
// In "pending" mode, theoretically all are pending, but let's filter just in case logic changes
|
|
// Actually getPendingUsers returns only pending. getAllUsers returns all.
|
|
// So we don't need extra status filtering here unless we add a specific status filter dropdown.
|
|
return matchesSearch;
|
|
});
|
|
|
|
const getStatusBadge = (status: UserApprovalStatus) => {
|
|
// If we are in "All" mode, approved users are common
|
|
if (status === 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" />
|
|
Ativo
|
|
</span>
|
|
);
|
|
}
|
|
|
|
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.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 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black">
|
|
{viewMode === "pending" ? "Aprovação de Cadastros" : "Gerenciamento de Usuários"}
|
|
</h1>
|
|
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
|
{viewMode === "pending"
|
|
? "Gerencie os cadastros pendentes de aprovação"
|
|
: "Visualize e gerencie todos os usuários do sistema"}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<div className="bg-white rounded-lg p-1 border border-gray-200 flex">
|
|
<button
|
|
onClick={() => setViewMode("pending")}
|
|
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
|
viewMode === "pending" ? "bg-brand-gold text-white" : "text-gray-600 hover:bg-gray-50"
|
|
}`}
|
|
>
|
|
Pendentes
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode("all")}
|
|
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
|
viewMode === "all" ? "bg-brand-gold text-white" : "text-gray-600 hover:bg-gray-50"
|
|
}`}
|
|
>
|
|
Todos
|
|
</button>
|
|
</div>
|
|
<Button onClick={() => setShowCreateModal(true)}>
|
|
<UserPlus className="w-4 h-4 mr-2" />
|
|
Novo Usuário
|
|
</Button>
|
|
</div>
|
|
</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" />
|
|
{activeTab === "cliente" ? "Clientes" : "Clientes"}
|
|
<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" />
|
|
{activeTab === "profissional" ? "Profissionais & Staff" : "Profissionais & Staff"}
|
|
<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 items-center">
|
|
{/* Search */}
|
|
<div className="flex-1 relative w-full">
|
|
<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>
|
|
<button
|
|
onClick={fetchUsers}
|
|
className="p-2 text-gray-400 hover:text-brand-gold transition-colors"
|
|
title="Atualizar lista"
|
|
>
|
|
<RefreshCw className={`w-5 h-5 ${isLoading ? "animate-spin" : ""}`} />
|
|
</button>
|
|
</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 usuários...
|
|
</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>
|
|
{/* Role Column */}
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Função
|
|
</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" ? 8 : 7}
|
|
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 cliente 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 cursor-pointer"
|
|
onClick={() => setSelectedUser(user)}
|
|
>
|
|
<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>
|
|
{/* Role Editor */}
|
|
<td className="px-6 py-4 whitespace-nowrap" onClick={(e) => e.stopPropagation()}>
|
|
{activeTab === "cliente" ? (
|
|
<span className="text-sm text-gray-900">Cliente</span>
|
|
) : (
|
|
<select
|
|
value={
|
|
(user.role === "PHOTOGRAPHER" && user.professional_type === "Cinegrafista") ? "Cinegrafista" :
|
|
(user.role === "PHOTOGRAPHER" && user.professional_type === "Recepcionista") ? "Recepcionista" :
|
|
(user.role === "PHOTOGRAPHER" && user.professional_type === "Fotógrafo") ? "Fotógrafo" :
|
|
(user.role === "PHOTOGRAPHER" && !user.professional_type) ? "Fotógrafo" : // Default fallback
|
|
(user.role === "PHOTOGRAPHER") ? "Fotógrafo" : // Catch all photographer main role
|
|
user.role
|
|
}
|
|
onChange={(e) => {
|
|
let newRole = e.target.value;
|
|
if (["Cinegrafista", "Recepcionista", "Fotógrafo"].includes(newRole)) {
|
|
newRole = "PHOTOGRAPHER";
|
|
}
|
|
handleRoleChange(user.id, newRole);
|
|
}}
|
|
className="text-sm border-gray-300 rounded-md shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50"
|
|
disabled={viewMode === "all" && user.approvalStatus === UserApprovalStatus.APPROVED}
|
|
>
|
|
<option value="Fotógrafo">Fotógrafo</option>
|
|
<option value="Cinegrafista">Cinegrafista</option>
|
|
<option value="Recepcionista">Recepcionista</option>
|
|
<option value="RESEARCHER">Pesquisador</option>
|
|
<option value="BUSINESS_OWNER">Dono do Negócio</option>
|
|
<option value="SUPERADMIN">Super Admin</option>
|
|
</select>
|
|
)}
|
|
</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" onClick={(e) => e.stopPropagation()}>
|
|
<div className="flex gap-2">
|
|
{user.approvalStatus !== UserApprovalStatus.APPROVED && (
|
|
<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>
|
|
)}
|
|
{user.approvalStatus === UserApprovalStatus.APPROVED && (
|
|
<span className="text-gray-400 text-xs">--</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
)
|
|
}
|
|
</div >
|
|
</div >
|
|
<UserDetailsModal
|
|
selectedUser={selectedUser}
|
|
onClose={() => setSelectedUser(null)}
|
|
onApprove={handleApprove}
|
|
isProcessing={isProcessing}
|
|
viewMode={viewMode}
|
|
handleRoleChange={handleRoleChange}
|
|
setSelectedUser={setSelectedUser}
|
|
/>
|
|
<CreateUserModal
|
|
isOpen={showCreateModal}
|
|
onClose={() => setShowCreateModal(false)}
|
|
onSubmit={handleCreateUser}
|
|
isCreating={isCreating}
|
|
formData={createFormData}
|
|
setFormData={setCreateFormData}
|
|
/>
|
|
</div >
|
|
</div >
|
|
);
|
|
};
|