+
+
{
+ const value = e.target.value.replace(/\D/g, "");
+ let formatted = "";
+ if (value.length <= 11) {
+ // CPF: 000.000.000-00
+ formatted = value.replace(
+ /(\d{3})(\d{3})(\d{3})(\d{0,2})/,
+ "$1.$2.$3-$4"
+ );
+ } else {
+ // CNPJ: 00.000.000/0000-00
+ formatted = value
+ .slice(0, 14)
+ .replace(
+ /(\d{2})(\d{3})(\d{3})(\d{4})(\d{0,2})/,
+ "$1.$2.$3/$4-$5"
+ );
+ }
+ handleChange("cpfCnpj", formatted);
+ }}
+ onBlur={handleCpfBlur}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
+ placeholder="000.000.000-00 ou 00.000.000/0000-00"
+ />
+
+ Informe seu CPF para verificarmos se você já possui cadastro.
+
+
+
- {activeTab === 'fot' ? (
-
Colunas Esperadas (A-J):
- ) : (
-
Colunas Esperadas (A-AB):
+ {activeTab === 'fot' && (
+
Colunas Esperadas (A-J): FOT, Empresa, Curso, Observações, Instituição, Ano Formatura, Cidade, Estado, Gastos Captação, Pré Venda.
+ )}
+ {activeTab === 'agenda' && (
+
Colunas Esperadas (A-AB): FOT, Data, ..., Tipo Evento, Obs, Local, Endereço, Horário, Qtds (Formandos, Foto, Cine...), Faltantes, Logística.
+ )}
+ {activeTab === 'profissionais' && (
+
Colunas Esperadas (A-J): Nome, Função, Endereço, Cidade, UF, Whatsapp, CPF/CNPJ (Obrigatório), Banco, Agencia, Conta PIX.
)}
-
- {activeTab === 'fot'
- ? "FOT, Empresa, Curso, Observações, Instituição, Ano Formatura, Cidade, Estado, Gastos Captação, Pré Venda."
- : "FOT, Data, ..., Tipo Evento, Obs, Local, Endereço, Horário, Qtds (Formandos, Foto, Cine...), Faltantes, Logística."
- }
@@ -327,7 +512,7 @@ export const ImportData: React.FC = () => {
-
Pré-visualização ({activeTab === 'fot' ? 'FOT' : 'Agenda'})
+ Pré-visualização ({activeTab.toUpperCase()})
Total: {data.length}
-
+
+ {skippedCount > 0 && (
+
+
+ {skippedCount} registros ignorados (sem CPF/CNPJ)
+
+ )}
Total de Registros: {data.length}
diff --git a/frontend/pages/Team.tsx b/frontend/pages/Team.tsx
index 91341f8..bc34b33 100644
--- a/frontend/pages/Team.tsx
+++ b/frontend/pages/Team.tsx
@@ -1,46 +1,29 @@
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,
+ Star,
+ Camera,
+ Video,
+ UserCheck,
} 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 { Professional } from "../types";
import { ProfessionalDetailsModal } from "../components/ProfessionalDetailsModal";
+import { ProfessionalModal } from "../components/ProfessionalModal";
export const TeamPage: React.FC = () => {
- const { user, token: contextToken } = useAuth();
+ const { token: contextToken } = useAuth();
const token = contextToken || "";
// Lists
@@ -49,62 +32,34 @@ export const TeamPage: React.FC = () => {
// 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 [showModal, setShowModal] = 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: "",
+ // Helper renderers
+ const getRoleName = (id: string) => {
+ return roles.find((r) => r.id === id)?.nome || "Desconhecido";
};
- 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);
+ 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;
+ };
+
+ const GenericAvatar = "https://ui-avatars.com/api/?background=random";
+
// Fetch Data
useEffect(() => {
@@ -122,10 +77,6 @@ export const TeamPage: React.FC = () => {
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);
@@ -134,261 +85,6 @@ export const TeamPage: React.FC = () => {
}
};
- // 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 {
@@ -402,56 +98,44 @@ export const TeamPage: React.FC = () => {
}
};
- // Helper renderers
- const getRoleName = (id: string) => {
- return roles.find((r) => r.id === id)?.nome || "Desconhecido";
+ const handleEditClick = (professional: Professional) => {
+ setSelectedProfessional(professional);
+ setShowModal(true);
};
- 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;
+ const handleViewClick = (professional: Professional) => {
+ setViewProfessional(professional);
};
- // Filter Logic
- const filteredProfessionals = professionals.filter((p) => {
- const matchesSearch =
- p.nome.toLowerCase().includes(searchTerm.toLowerCase()) ||
- (p.email && p.email.toLowerCase().includes(searchTerm.toLowerCase()));
+ // Optimized Filter
+ const filteredProfessionals = React.useMemo(() => {
+ return 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;
+ 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;
- }
- })();
+ 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;
+ 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 matchesSearch && matchesRole && matchesRating;
+ });
+ }, [professionals, searchTerm, roleFilter, ratingFilter, roles]);
return (
@@ -468,17 +152,23 @@ export const TeamPage: React.FC = () => {
{/* Stats */}
+
+
+
+
Total de Profissionais
+
{professionals.length}
+
+
+
+
+
{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 (
@@ -496,7 +186,6 @@ export const TeamPage: React.FC = () => {
{/* Filters and Search */}
- {/* Search and Add Button Row */}
@@ -510,15 +199,14 @@ export const TeamPage: React.FC = () => {
- {/* Filters Row */}
@@ -559,365 +247,120 @@ export const TeamPage: React.FC = () => {
) : (
-
-
-
- | Profissional |
- Função |
- Contato |
- Ações |
-
-
-
- {filteredProfessionals.map((p) => {
- const roleName = getRoleName(p.funcao_profissional_id);
- const RoleIcon = getRoleIcon(roleName);
- return (
- handleViewClick(p)}>
-
-
- 
-
- {p.nome}
-
-
- {p.media ? p.media.toFixed(1) : "N/A"}
+ {filteredProfessionals.length === 0 ? (
+ Nenhum profissional encontrado.
+ ) : (
+
+
+
+ | Profissional |
+ Função |
+ Contato |
+ Ações |
+
+
+
+ {filteredProfessionals.map((p) => {
+ return (
+ handleViewClick(p)}>
+
+
+ 
+
+ {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}
- |
-
-
-
- |
-
- );
- })}
-
-
+ |
+
+
+ {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"}
-
-
+ {/* Modals */}
+
{
+ setShowModal(false);
+ setSelectedProfessional(null);
+ }}
+ professional={selectedProfessional}
+ existingProfessionals={professionals}
+ onSwitchToEdit={(prof) => {
+ setSelectedProfessional(prof);
+ // Modal automatically updates because 'professional' prop changes
+ }}
+ roles={roles}
+ onSuccess={() => {
+ fetchData();
+ }}
+ />
-
-
-
- )}
-
- {/* 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);
- }}
/>
)}
+ {showDeleteModal && (
+
+
+
Confirmar Exclusão
+
+ Tem certeza que deseja excluir o profissional {professionalToDelete?.nome}? Esta ação não pode ser desfeita.
+
+
+
+
+
+
+
+ )}
);
};