photum/frontend/pages/Team.tsx
NANDO9322 e78de535c1 feat: implementação do financeiro e suporte a múltiplas funções
Este commit introduz o módulo financeiro completo e refatora o sistema de profissionais para suportar múltiplas funções, corrigindo a contabilização e validação de equipes.

Principais alterações:

- **Módulo Financeiro:**
  - Criação da tabela `financial_transactions` e queries associadas.
  - Implementação do backend (Handler/Service) para gerenciar transações.
  - Nova página [Finance.tsx](cci:7://file:///c:/Projetos/photum/frontend/pages/Finance.tsx:0:0-0:0) com listagem, edição, filtros avançados e agrupamento por FOT.
  - Correção na busca de FOTs e formatação de datas.

- **Gestão de Equipe e Profissionais:**
  - Refatoração para suportar múltiplas funções por profissional (Backend & Frontend).
  - Atualização do [Dashboard](cci:1://file:///c:/Projetos/photum/frontend/pages/Dashboard.tsx:31:0-1663:2) e [EventTable](cci:1://file:///c:/Projetos/photum/frontend/components/EventTable.tsx:28:0-659:2) para contabilizar corretamente profissionais (Fotografo, Cinegrafista, Recepcionista) verificando a lista de funções.
  - Implementação de validação de cota no aceite de convites (bloqueia se a equipe da função específica já estiver completa).
  - Ajuste visual nos indicadores de "Equipe Completa" e contadores de faltantes na listagem de eventos.

- **Geral:**
  - Atualização da documentação Swagger.
  - Ajustes de tipagem e migrações de banco de dados.
2026-01-15 18:07:39 -03:00

921 lines
43 KiB
TypeScript

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<Professional[]>([]);
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<Professional | null>(null);
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [professionalToDelete, setProfessionalToDelete] = useState<Professional | null>(null);
const [viewProfessional, setViewProfessional] = useState<Professional | null>(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<CreateProfessionalDTO & { senha?: string; confirmarSenha?: string }>(initialFormState);
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string>("");
// 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://cep.awesomeapi.com.br/json/${cep}`
);
if (!response.ok) throw new Error("CEP não encontrado");
const data = await response.json();
setFormData((prev) => ({
...prev,
endereco: `${data.address || ""} ${data.district ? `- ${data.district}` : ""}`.trim() || prev.endereco,
cidade: data.city || prev.cidade,
uf: data.state || prev.uf,
}));
} catch (error) {
console.error("Erro ao buscar CEP:", error);
} finally {
setIsLoadingCep(false);
}
};
// Handlers
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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: "PHOTOGRAPHER", // Default role for professionals created here? Or map from selected role?
// 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 (
<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 mb-2">
Equipe
</h1>
<p className="text-sm sm:text-base text-gray-600">
Gerencie sua equipe de profissionais
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4 md:gap-6 mb-6 sm:mb-8">
{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 (
<div key={role.id} className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600 mb-1">Total de {role.nome}s</p>
<p className="text-3xl font-bold text-brand-black">{count}</p>
</div>
<RoleIcon className={iconColorClass} size={32} />
</div>
</div>
);
})}
</div>
{/* Filters and Search */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex flex-col gap-4">
{/* Search and Add Button Row */}
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="relative w-full md:w-96">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
<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-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
/>
</div>
<Button onClick={() => {
resetForm();
setShowAddModal(true);
}}>
<Plus size={20} className="mr-2" />
Adicionar Profissional
</Button>
</div>
{/* Filters Row */}
<div className="flex flex-col md:flex-row gap-4 items-center">
<div className="flex items-center gap-2">
<Filter size={16} className="text-gray-400" />
<span className="text-sm font-medium text-gray-700">Filtros:</span>
</div>
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="all">Todas as Funções</option>
{roles.map(role => (
<option key={role.id} value={role.nome}>{role.nome}</option>
))}
</select>
<select
value={ratingFilter}
onChange={(e) => setRatingFilter(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="all">Todas as Avaliações</option>
<option value="5"> 4.5+ Estrelas</option>
<option value="4"> 4.0 - 4.4 Estrelas</option>
<option value="3"> 3.0 - 3.9 Estrelas</option>
<option value="2"> 2.0 - 2.9 Estrelas</option>
<option value="1"> 1.0 - 1.9 Estrelas</option>
<option value="0"> Menos de 1.0</option>
</select>
</div>
</div>
</div>
{/* List */}
{isLoading ? (
<div className="text-center py-12">Carregando...</div>
) : (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Profissional</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Função</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Contato</th>
<th className="px-6 py-4 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{filteredProfessionals.map((p) => {
const roleName = getRoleName(p.funcao_profissional_id);
const RoleIcon = getRoleIcon(roleName);
return (
<tr key={p.id} className="hover:bg-gray-50 cursor-pointer" onClick={() => handleViewClick(p)}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<img
className="h-10 w-10 rounded-full object-cover"
src={p.avatar_url || p.avatar || GenericAvatar}
alt={p.nome}
/>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{p.nome}</div>
<div className="text-sm text-gray-500 flex items-center gap-1">
<Star size={12} className="text-brand-gold fill-current" />
{p.media ? p.media.toFixed(1) : "N/A"}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{p.functions && p.functions.length > 0
? p.functions.map(f => f.nome).join(", ")
: getRoleName(p.funcao_profissional_id)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{p.whatsapp}</div>
<div className="text-sm text-gray-500">{p.email}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onClick={(e) => {
e.stopPropagation();
handleEditClick(p);
}} className="text-indigo-600 hover:text-indigo-900 mr-4">
<Edit2 size={18} />
</button>
<button onClick={(e) => {
e.stopPropagation();
setProfessionalToDelete(p);
setShowDeleteModal(true);
}} className="text-red-600 hover:text-red-900">
<Trash2 size={18} />
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
</div>
{/* Add/Edit Modal */}
{(showAddModal || showEditModal) && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto">
<div className="bg-white rounded-lg max-w-4xl w-full p-8 max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold font-serif">{showEditModal ? "Editar Profissional" : "Novo Profissional"}</h2>
<button onClick={() => { setShowAddModal(false); setShowEditModal(false); }}><X size={24} /></button>
</div>
<form onSubmit={(e) => handleSubmit(e, showEditModal)} className="space-y-6">
{/* Photo */}
<div className="flex justify-center mb-6">
<div className="relative">
<div className="w-32 h-32 rounded-full overflow-hidden bg-gray-100 border-2 border-dashed border-gray-300 flex items-center justify-center">
{avatarPreview ? (
<img src={avatarPreview} alt="Preview" className="w-full h-full object-cover" />
) : (
<User size={48} className="text-gray-400" />
)}
</div>
<label className="absolute bottom-0 right-0 bg-brand-gold text-white p-2 rounded-full cursor-pointer hover:bg-brand-gold/90 transition-colors shadow-lg">
<Camera size={16} />
<input type="file" accept="image/*" className="hidden" onChange={handleAvatarChange} />
</label>
{avatarPreview && (
<button type="button" onClick={removeAvatar} className="absolute top-0 right-0 bg-red-500 text-white p-1 rounded-full shadow-lg hover:bg-red-600">
<X size={12} />
</button>
)}
</div>
</div>
{/* Basic Info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Nome *</label>
<input required type="text" value={formData.nome} onChange={e => 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" />
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Funções *
</label>
<div className="flex flex-wrap gap-4 bg-gray-50 p-3 rounded border">
{roles.map(role => (
<label key={role.id} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
value={role.id}
checked={formData.funcoes_ids?.includes(role.id)}
onChange={e => {
const checked = e.target.checked;
const currentIds = formData.funcoes_ids || [];
let newIds: string[] = [];
if (checked) {
newIds = [...currentIds, role.id];
} else {
newIds = currentIds.filter((id) => id !== role.id);
}
setFormData((prev) => ({
...prev,
funcoes_ids: newIds,
funcao_profissional_id: newIds.length > 0 ? newIds[0] : ""
}));
}}
className="w-4 h-4 text-brand-gold rounded border-gray-300 focus:ring-brand-gold"
/>
<span className="text-sm text-gray-700">{role.nome}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Email *</label>
<input required type="email" value={formData.email} onChange={e => 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" />
</div>
{!showEditModal && (
<>
<div>
<label className="block text-sm font-medium text-gray-700">Senha *</label>
<div className="relative mt-1">
<input
required
type={showPassword ? "text" : "password"}
value={formData.senha}
onChange={e => 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"
/>
<button
type="button"
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Confirmar Senha *</label>
<div className="relative mt-1">
<input
required
type={showConfirmPassword ? "text" : "password"}
value={formData.confirmarSenha}
onChange={e => 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"
/>
<button
type="button"
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
</>
)}
<div>
<label className="block text-sm font-medium text-gray-700">WhatsApp</label>
<input type="text" value={formData.whatsapp} onChange={e => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">CPF/CNPJ Titular</label>
<input type="text" value={formData.cpf_cnpj_titular} onChange={e => 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" />
</div>
</div>
<h3 className="text-lg font-medium text-gray-900 border-b pb-2 mt-4">Endereço</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">CEP</label>
<div className="relative">
<input
type="text"
maxLength={9}
value={formData.cep || ""}
onChange={(e) => {
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 && <span className="absolute right-2 top-3 text-xs text-gray-400">Buscando...</span>}
</div>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">Endereço Completo</label>
<input type="text" value={formData.endereco} onChange={e => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Cidade</label>
<input type="text" value={formData.cidade} onChange={e => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">UF</label>
<select value={formData.uf} onChange={e => setFormData({ ...formData, uf: 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">
<option value="">UF</option>
{ufs.map(uf => <option key={uf} value={uf}>{uf}</option>)}
</select>
</div>
</div>
{/* Banking */}
<h3 className="text-lg font-medium text-gray-900 border-b pb-2 mt-4">Dados Bancários</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Banco *</label>
<input type="text" required value={formData.banco} onChange={e => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Agência *</label>
<input type="text" required value={formData.agencia} onChange={e => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Chave Pix / Conta *</label>
<input type="text" required value={formData.conta_pix} onChange={e => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Tipo Cartão</label>
<input type="text" value={formData.tipo_cartao} onChange={e => 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..." />
</div>
</div>
{/* Resources */}
<h3 className="text-lg font-medium text-gray-900 border-b pb-2 mt-4">Recursos</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<label className="flex items-center gap-2">
<input type="checkbox" checked={formData.carro_disponivel} onChange={e => setFormData({ ...formData, carro_disponivel: e.target.checked })} />
<span>Carro Disponível</span>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" checked={formData.tem_estudio} onChange={e => setFormData({ ...formData, tem_estudio: e.target.checked })} />
<span>Possui Estúdio</span>
</label>
{formData.tem_estudio && (
<div>
<label className="block text-sm font-medium text-gray-700">Qtd Estúdios</label>
<input type="number" min="0" value={formData.qtd_estudio} onChange={e => 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" />
</div>
)}
</div>
{/* Ratings */}
<h3 className="text-lg font-medium text-gray-900 border-b pb-2 mt-4">Avaliações e Valores</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1 leading-tight">Qual. Técnica / Aparência</label>
<input type="number" min="0" max="5" step="1" value={formData.qual_tec} onChange={e => setFormData({ ...formData, qual_tec: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1 leading-tight">Simpatia</label>
<input type="number" min="0" max="5" step="1" value={formData.educacao_simpatia} onChange={e => setFormData({ ...formData, educacao_simpatia: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1 leading-tight">Desempenho</label>
<input type="number" min="0" max="5" step="1" value={formData.desempenho_evento} onChange={e => setFormData({ ...formData, desempenho_evento: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1 leading-tight">Disp. Horário</label>
<input type="number" min="0" max="5" step="1" value={formData.disp_horario} onChange={e => setFormData({ ...formData, disp_horario: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
</div>
<div className="bg-gray-100 p-2 rounded text-center flex flex-col justify-center">
<span className="block text-xs text-gray-500 font-bold uppercase tracking-wider">Média</span>
<span className="text-2xl font-bold text-brand-gold">{formData.media ? (typeof formData.media === 'number' ? formData.media.toFixed(1) : parseFloat(formData.media).toFixed(1)) : "0.0"}</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label className="block text-sm font-medium text-gray-700">Tabela Free</label>
<input type="text" value={formData.tabela_free} onChange={e => setFormData({ ...formData, tabela_free: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Equipamentos</label>
<textarea rows={3} value={formData.equipamentos} onChange={e => setFormData({ ...formData, equipamentos: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border" placeholder="Liste os equipamentos..." />
<label className="flex items-center gap-2 mt-2">
<input type="checkbox" checked={formData.extra_por_equipamento} onChange={e => setFormData({ ...formData, extra_por_equipamento: e.target.checked })} />
<span>Extra por Equipamento</span>
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Observações</label>
<textarea rows={3} value={formData.observacao} onChange={e => setFormData({ ...formData, observacao: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border"></textarea>
</div>
<div className="flex justify-end gap-4 pt-4">
<Button type="button" variant="secondary" onClick={() => { setShowAddModal(false); setShowEditModal(false); }}>Cancelar</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Salvando..." : showEditModal ? "Salvar Alterações" : "Criar Profissional"}
</Button>
</div>
</form>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-md w-full p-6 text-center">
<AlertTriangle className="mx-auto text-red-500 mb-4" size={48} />
<h3 className="text-xl font-bold mb-2">Confirmar Exclusão</h3>
<p className="text-gray-600 mb-6">Tem certeza que deseja excluir <strong>{professionalToDelete?.nome}</strong>? Esta ação não pode ser desfeita.</p>
<div className="flex justify-center gap-4">
<Button variant="secondary" onClick={() => setShowDeleteModal(false)}>Cancelar</Button>
<Button variant="primary" onClick={handleDelete} className="bg-red-600 hover:bg-red-700 text-white border-none">Excluir</Button>
</div>
</div>
</div>
)}
{/* View Modal */}
{viewProfessional && (
<ProfessionalDetailsModal
professional={viewProfessional}
isOpen={!!viewProfessional}
onClose={() => setViewProfessional(null)}
onEdit={() => {
const p = viewProfessional;
setViewProfessional(null);
handleEditClick(p);
}}
/>
)}
</div>
);
};