import React, { useState, useEffect } from "react"; import { Users, Camera, Mail, Phone, MapPin, Star, Plus, Search, Filter, User, Upload, X, Video, UserCheck, Car, Building, CreditCard, Trash2, Edit2, AlertTriangle, Check, DollarSign, Eye, EyeOff, } from "lucide-react"; import { Button } from "../components/Button"; import { getFunctions, createProfessional, getProfessionals, updateProfessional, deleteProfessional, getUploadURL, uploadFileToSignedUrl, } from "../services/apiService"; import { useAuth } from "../contexts/AuthContext"; import { Professional, CreateProfessionalDTO } from "../types"; import { ProfessionalDetailsModal } from "../components/ProfessionalDetailsModal"; export const TeamPage: React.FC = () => { const { user, token: contextToken } = useAuth(); const token = contextToken || ""; // Lists const [professionals, setProfessionals] = useState([]); const [roles, setRoles] = useState<{ id: string; nome: string }[]>([]); // Loading States const [isLoading, setIsLoading] = useState(true); const [isBackendDown, setIsBackendDown] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isLoadingCep, setIsLoadingCep] = useState(false); // Filters const [searchTerm, setSearchTerm] = useState(""); const [roleFilter, setRoleFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all"); const [ratingFilter, setRatingFilter] = useState("all"); // Selection & Modals const [selectedProfessional, setSelectedProfessional] = useState(null); const [showAddModal, setShowAddModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [professionalToDelete, setProfessionalToDelete] = useState(null); const [viewProfessional, setViewProfessional] = useState(null); // Form State const initialFormState: CreateProfessionalDTO & { senha?: string; confirmarSenha?: string } = { nome: "", funcao_profissional_id: "", funcoes_ids: [], email: "", senha: "", confirmarSenha: "", whatsapp: "", cpf_cnpj_titular: "", endereco: "", cidade: "", uf: "", banco: "", agencia: "", conta_pix: "", tipo_cartao: "", carro_disponivel: false, tem_estudio: false, qtd_estudio: 0, observacao: "", qual_tec: 0, educacao_simpatia: 0, desempenho_evento: 0, disp_horario: 0, media: 0, tabela_free: "", extra_por_equipamento: false, equipamentos: "", avatar_url: "", }; const [formData, setFormData] = useState(initialFormState); const [avatarFile, setAvatarFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(""); // Password Visibility const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); // Fetch Data useEffect(() => { fetchData(); }, [token]); const fetchData = async () => { setIsLoading(true); try { const [rolesData, prosData] = await Promise.all([ getFunctions(), getProfessionals(token), ]); if (rolesData.data) setRoles(rolesData.data); if (prosData.data) { setProfessionals(prosData.data); setIsBackendDown(false); } else if (prosData.error) { console.error("Error fetching professionals:", prosData.error); if (prosData.isBackendDown) setIsBackendDown(true); } } catch (error) { console.error("Error fetching data:", error); } finally { setIsLoading(false); } }; // Helpers const GenericAvatar = "https://ui-avatars.com/api/?background=random"; const ufs = [ "AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO" ]; const maskPhone = (value: string) => { return value .replace(/\D/g, "") .replace(/^(\d{2})(\d)/g, "($1) $2") .replace(/(\d)(\d{4})$/, "$1-$2") .slice(0, 15); }; const maskCpfCnpj = (value: string) => { const clean = value.replace(/\D/g, ""); if (clean.length <= 11) { return clean .replace(/(\d{3})(\d)/, "$1.$2") .replace(/(\d{3})(\d)/, "$1.$2") .replace(/(\d{3})(\d{1,2})/, "$1-$2") .replace(/(-\d{2})\d+?$/, "$1"); // Captures 11 digits } else { return clean .replace(/^(\d{2})(\d)/, "$1.$2") .replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3") .replace(/\.(\d{3})(\d)/, ".$1/$2") .replace(/(\d{4})(\d)/, "$1-$2") .replace(/(-\d{2})\d+?$/, "$1") // Captures 14 digits .slice(0, 18); } }; const calculateMedia = (ratings: { qual_tec: number; educacao_simpatia: number; desempenho_evento: number; disp_horario: number; }) => { const weightedScore = ratings.qual_tec * 2 + ratings.educacao_simpatia + ratings.desempenho_evento + ratings.disp_horario; return weightedScore / 5; }; const handleCepBlur = async () => { const cep = formData.cep?.replace(/\D/g, "") || ""; if (cep.length !== 8) return; setIsLoadingCep(true); try { const response = await fetch( `https://viacep.com.br/ws/${cep}/json/` ); if (!response.ok) throw new Error("CEP não encontrado"); const data = await response.json(); if (data.erro) throw new Error("CEP não encontrado"); setFormData((prev) => ({ ...prev, endereco: `${data.logradouro || ""} ${data.bairro ? `- ${data.bairro}` : ""}`.trim() || prev.endereco, cidade: data.localidade || prev.cidade, uf: data.uf || prev.uf, })); } catch (error) { console.error("Erro ao buscar CEP:", error); } finally { setIsLoadingCep(false); } }; // Handlers const handleAvatarChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setAvatarFile(file); const reader = new FileReader(); reader.onloadend = () => { setAvatarPreview(reader.result as string); }; reader.readAsDataURL(file); } }; const removeAvatar = () => { setAvatarFile(null); setAvatarPreview(""); setFormData((prev) => ({ ...prev, avatar_url: "" })); }; const resetForm = () => { setFormData(initialFormState); setAvatarFile(null); setAvatarPreview(""); setSelectedProfessional(null); }; const handleEditClick = (professional: Professional) => { setFormData({ nome: professional.nome, funcao_profissional_id: professional.funcao_profissional_id, funcoes_ids: professional.functions?.map(f => f.id) || (professional.funcao_profissional_id ? [professional.funcao_profissional_id] : []), email: professional.email || "", senha: "", // Não editamos senha aqui confirmarSenha: "", whatsapp: professional.whatsapp || "", cpf_cnpj_titular: professional.cpf_cnpj_titular || "", endereco: professional.endereco || "", cidade: professional.cidade || "", uf: professional.uf || "", cep: professional.cep || "", banco: professional.banco || "", agencia: professional.agencia || "", conta_pix: professional.conta_pix || "", tipo_cartao: professional.tipo_cartao || "", carro_disponivel: professional.carro_disponivel || false, tem_estudio: professional.tem_estudio || false, qtd_estudio: professional.qtd_estudio || 0, observacao: professional.observacao || "", qual_tec: professional.qual_tec || 0, educacao_simpatia: professional.educacao_simpatia || 0, desempenho_evento: professional.desempenho_evento || 0, disp_horario: professional.disp_horario || 0, tabela_free: professional.tabela_free || "", extra_por_equipamento: professional.extra_por_equipamento || false, equipamentos: professional.equipamentos || "", avatar_url: professional.avatar_url || "", media: professional.media || 0, }); setAvatarPreview(professional.avatar_url || (professional.avatar ?? GenericAvatar)); setAvatarFile(null); setSelectedProfessional(professional); // Storing the professional being edited here setShowEditModal(true); }; const handleViewClick = (professional: Professional) => { setViewProfessional(professional); }; // Update Media when ratings change useEffect(() => { const newMedia = calculateMedia({ qual_tec: formData.qual_tec || 0, educacao_simpatia: formData.educacao_simpatia || 0, desempenho_evento: formData.desempenho_evento || 0, disp_horario: formData.disp_horario || 0, }); // Only update if it's different to avoid loops/excessive renders, though optional in this simple case setFormData((prev) => { if (prev.media === newMedia) return prev; return { ...prev, media: newMedia }; }); }, [ formData.qual_tec, formData.educacao_simpatia, formData.desempenho_evento, formData.disp_horario, ]); const handleSubmit = async (e: React.FormEvent, isEdit: boolean) => { e.preventDefault(); setIsSubmitting(true); try { // Validation for password on creation if (!isEdit && (formData.senha || formData.confirmarSenha)) { if (formData.senha !== formData.confirmarSenha) { alert("As senhas não coincidem!"); setIsSubmitting(false); return; } if (formData.senha && formData.senha.length < 6) { alert("A senha deve ter pelo menos 6 caracteres."); setIsSubmitting(false); return; } } let finalAvatarUrl = formData.avatar_url; // Handle Avatar Upload if new file selected if (avatarFile) { const uploadRes = await getUploadURL(avatarFile.name, avatarFile.type); if (uploadRes.data) { await uploadFileToSignedUrl(uploadRes.data.upload_url, avatarFile); finalAvatarUrl = uploadRes.data.public_url; } } const payload: any = { ...formData, avatar_url: finalAvatarUrl }; // Remove password fields from professional payload delete payload.senha; delete payload.confirmarSenha; if (isEdit && selectedProfessional) { await updateProfessional(selectedProfessional.id, payload, token); alert("Profissional atualizado com sucesso!"); } else { // Create User First (if password provided or mandatory logic?) // If password is provided, we must create a user account. // User requested: "ao cadastrar um novo profissional falta cadastrar a senha ... pra que esse profissional acesse a interface" // So we should try to create user first. let targetUserId = ""; if (formData.email && formData.senha) { const { adminCreateUser } = await import("../services/apiService"); const createRes = await adminCreateUser({ email: formData.email, senha: formData.senha, nome: formData.nome, role: roles.find(r => r.id === formData.funcao_profissional_id)?.nome.toUpperCase().includes("PESQUISA") ? "RESEARCHER" : "PHOTOGRAPHER", // Mapear função? Usually PHOTOGRAPHER or generic. Let's assume PHOTOGRAPHER for now as they are "Equipe". tipo_profissional: roles.find(r => r.id === formData.funcao_profissional_id)?.nome || "", ativo: true, // Auto-active as per request }, token); if (createRes.error) { // If user API fails (e.g. email exists), we stop? Or let create professional proceed unlinked? // User requirement implies linked account. // If email exists, maybe we can't create user, but we can check if we should link to existing? // For simplicity, error out. throw new Error("Erro ao criar usuário de login: " + createRes.error); } if (createRes.data && createRes.data.id) { targetUserId = createRes.data.id; } } if (targetUserId) { payload.target_user_id = targetUserId; } const res = await createProfessional(payload, token); if (res.error) throw new Error(res.error); alert("Profissional criado com sucesso!"); } setShowAddModal(false); setShowEditModal(false); fetchData(); // Reset form resetForm(); } catch (error: any) { console.error("Error submitting form:", error); alert(error.message || "Erro ao salvar profissional. Verifique o console."); } finally { setIsSubmitting(false); } }; const handleDelete = async () => { if (!professionalToDelete) return; try { await deleteProfessional(professionalToDelete.id, token); setShowDeleteModal(false); setProfessionalToDelete(null); fetchData(); } catch (error) { console.error("Error deleting professional:", error); alert("Erro ao excluir profissional."); } }; // Helper renderers const getRoleName = (id: string) => { return roles.find((r) => r.id === id)?.nome || "Desconhecido"; }; const getRoleIcon = (roleName: string) => { const lower = roleName.toLowerCase(); if (lower.includes("foto")) return Camera; if (lower.includes("video") || lower.includes("cine")) return Video; return UserCheck; }; // Filter Logic const filteredProfessionals = professionals.filter((p) => { const matchesSearch = p.nome.toLowerCase().includes(searchTerm.toLowerCase()) || (p.email && p.email.toLowerCase().includes(searchTerm.toLowerCase())); // Adjusted role logic since we have ID in database but names in roles array const roleName = getRoleName(p.funcao_profissional_id); const matchesRole = roleFilter === "all" || roleName === roleFilter; // Rating filter logic const matchesRating = (() => { if (ratingFilter === "all") return true; const rating = p.media || 0; switch (ratingFilter) { case "5": return rating >= 4.5; case "4": return rating >= 4 && rating < 4.5; case "3": return rating >= 3 && rating < 4; case "2": return rating >= 2 && rating < 3; case "1": return rating >= 1 && rating < 2; case "0": return rating < 1; default: return true; } })(); // Hide users with unknown roles if (roleName === "Desconhecido") return false; return matchesSearch && matchesRole && matchesRating; }); const stats = { total: professionals.length, photographers: professionals.filter(p => getRoleName(p.funcao_profissional_id).toLowerCase().includes("fot") || getRoleName(p.funcao_profissional_id).toLowerCase().includes("foto")).length, cine: professionals.filter(p => getRoleName(p.funcao_profissional_id).toLowerCase().includes("cine") || getRoleName(p.funcao_profissional_id).toLowerCase().includes("video")).length, recep: professionals.filter(p => getRoleName(p.funcao_profissional_id).toLowerCase().includes("recep")).length, }; return (
{/* Header */}

Equipe

Gerencie sua equipe de profissionais

{/* Stats */}
{roles.map(role => { const count = professionals.filter(p => p.funcao_profissional_id === role.id).length; const RoleIcon = getRoleIcon(role.nome); // Optional: Customize colors based on role or just cycle/default // For simplicity using existing logic or default let iconColorClass = "text-brand-black"; if (role.nome.toLowerCase().includes("foto")) iconColorClass = "text-brand-gold"; else if (role.nome.toLowerCase().includes("video") || role.nome.toLowerCase().includes("cine")) iconColorClass = "text-blue-600"; else if (role.nome.toLowerCase().includes("recep")) iconColorClass = "text-purple-600"; return (

Total de {role.nome}s

{count}

); })}
{/* Filters and Search */}
{/* Search and Add Button Row */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" />
{/* Filters Row */}
Filtros:
{/* List */} {isLoading ? (
Carregando...
) : (
{filteredProfessionals.map((p) => { const roleName = getRoleName(p.funcao_profissional_id); const RoleIcon = getRoleIcon(roleName); return ( handleViewClick(p)}> ); })}
Profissional Função Contato Ações
{p.nome}
{p.nome}
{p.media ? p.media.toFixed(1) : "N/A"}
{p.functions && p.functions.length > 0 ? p.functions.map(f => f.nome).join(", ") : getRoleName(p.funcao_profissional_id)}
{p.whatsapp}
{p.email}
)}
{/* Add/Edit Modal */} {(showAddModal || showEditModal) && (

{showEditModal ? "Editar Profissional" : "Novo Profissional"}

handleSubmit(e, showEditModal)} className="space-y-6"> {/* Photo */}
{avatarPreview ? ( Preview ) : ( )}
{avatarPreview && ( )}
{/* Basic Info */}
setFormData({ ...formData, nome: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
{roles.map(role => ( ))}
setFormData({ ...formData, email: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
{!showEditModal && ( <>
setFormData({ ...formData, senha: e.target.value })} minLength={6} className="block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border pr-10" />
setFormData({ ...formData, confirmarSenha: e.target.value })} minLength={6} className="block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border pr-10" />
)}
setFormData({ ...formData, whatsapp: maskPhone(e.target.value) })} maxLength={15} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
setFormData({ ...formData, cpf_cnpj_titular: maskCpfCnpj(e.target.value) })} maxLength={18} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />

Endereço

{ const val = e.target.value.replace(/\D/g, "").replace(/^(\d{5})(\d)/, "$1-$2"); setFormData({ ...formData, cep: val }); }} onBlur={handleCepBlur} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" placeholder="00000-000" /> {isLoadingCep && Buscando...}
setFormData({ ...formData, endereco: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
setFormData({ ...formData, cidade: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
{/* Banking */}

Dados Bancários

setFormData({ ...formData, banco: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
setFormData({ ...formData, agencia: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
setFormData({ ...formData, conta_pix: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
setFormData({ ...formData, tipo_cartao: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" placeholder="SD, XQD..." />
{/* Resources */}

Recursos

{formData.tem_estudio && (
setFormData({ ...formData, qtd_estudio: Math.max(0, parseInt(e.target.value)) })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
)}
{/* Ratings */}

Avaliações e Valores

setFormData({ ...formData, qual_tec: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
setFormData({ ...formData, educacao_simpatia: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
setFormData({ ...formData, desempenho_evento: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
setFormData({ ...formData, disp_horario: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
Média {formData.media ? (typeof formData.media === 'number' ? formData.media.toFixed(1) : parseFloat(formData.media).toFixed(1)) : "0.0"}
setFormData({ ...formData, tabela_free: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
)} {/* Delete Confirmation Modal */} {showDeleteModal && (

Confirmar Exclusão

Tem certeza que deseja excluir {professionalToDelete?.nome}? Esta ação não pode ser desfeita.

)} {/* View Modal */} {viewProfessional && ( setViewProfessional(null)} onEdit={() => { const p = viewProfessional; setViewProfessional(null); handleEditClick(p); }} /> )}
); };