- Adicionar restrições de exclusão de FOT quando há eventos associados - Implementar tooltips para motivos de recusa de eventos por fotógrafos - Filtrar eventos recusados das listas de fotógrafos - Adicionar sistema de filtros avançados no modal de gerenciar equipe - Implementar campos completos de gestão de equipe (fotógrafos, recepcionistas, cinegrafistas, estúdios, pontos de foto, pontos decorados, pontos LED) - Adicionar colunas de gestão na tabela principal com cálculos automáticos de profissionais faltantes - Implementar controle de visibilidade da seção de gestão apenas para empresas - Adicionar status visual "Profissionais OK" com indicadores de completude - Implementar sistema de cálculo em tempo real de equipe necessária vs confirmada - Adicionar validações condicionais baseadas no tipo de usuário
646 lines
20 KiB
TypeScript
646 lines
20 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { Button } from "./Button";
|
|
import { Input } from "./Input";
|
|
import { getFunctions } from "../services/apiService";
|
|
|
|
interface ProfessionalFormProps {
|
|
onSubmit: (data: ProfessionalData) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export interface ProfessionalData {
|
|
nome: string;
|
|
email: string;
|
|
senha: string;
|
|
confirmarSenha: string;
|
|
avatar?: File | null;
|
|
funcaoId: string;
|
|
cep: string;
|
|
rua: string;
|
|
numero: string;
|
|
complemento: string;
|
|
bairro: string;
|
|
cidade: string;
|
|
uf: string;
|
|
whatsapp: string;
|
|
cpfCnpj: string;
|
|
banco: string;
|
|
agencia: string;
|
|
conta: string;
|
|
pix: string;
|
|
carroDisponivel: string;
|
|
possuiEstudio: string;
|
|
qtdEstudios: string;
|
|
tipoCartao: string;
|
|
equipamentos: string;
|
|
observacao: string;
|
|
funcaoLabel?: string;
|
|
}
|
|
|
|
export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
|
onSubmit,
|
|
onCancel,
|
|
}) => {
|
|
const [functions, setFunctions] = useState<
|
|
Array<{ id: string; nome: string }>
|
|
>([]);
|
|
const [isLoadingFunctions, setIsLoadingFunctions] = useState(false);
|
|
const [functionsError, setFunctionsError] = useState(false);
|
|
const [isLoadingCep, setIsLoadingCep] = useState(false);
|
|
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
|
const [formData, setFormData] = useState<ProfessionalData>({
|
|
nome: "",
|
|
email: "",
|
|
senha: "",
|
|
confirmarSenha: "",
|
|
avatar: null,
|
|
funcaoId: "",
|
|
cep: "",
|
|
rua: "",
|
|
numero: "",
|
|
complemento: "",
|
|
bairro: "",
|
|
cidade: "",
|
|
uf: "",
|
|
whatsapp: "",
|
|
cpfCnpj: "",
|
|
banco: "",
|
|
agencia: "",
|
|
conta: "",
|
|
pix: "",
|
|
carroDisponivel: "nao",
|
|
possuiEstudio: "nao",
|
|
qtdEstudios: "0",
|
|
tipoCartao: "",
|
|
equipamentos: "",
|
|
observacao: "",
|
|
});
|
|
|
|
useEffect(() => {
|
|
const loadFunctions = async () => {
|
|
setIsLoadingFunctions(true);
|
|
setFunctionsError(false);
|
|
try {
|
|
const result = await getFunctions();
|
|
if (result.data) {
|
|
setFunctions(result.data);
|
|
} else {
|
|
setFunctionsError(true);
|
|
}
|
|
} catch (error) {
|
|
setFunctionsError(true);
|
|
}
|
|
setIsLoadingFunctions(false);
|
|
};
|
|
loadFunctions();
|
|
}, []);
|
|
|
|
const handleChange = (field: keyof ProfessionalData, value: string) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
setFormData((prev) => ({ ...prev, avatar: file }));
|
|
// Create preview URL
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setAvatarPreview(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
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,
|
|
rua: data.address || prev.rua,
|
|
bairro: data.district || prev.bairro,
|
|
cidade: data.city || prev.cidade,
|
|
uf: data.state || prev.uf,
|
|
}));
|
|
} catch (error) {
|
|
console.error("Erro ao buscar CEP:", error);
|
|
// Opcional: mostrar erro para usuário
|
|
} finally {
|
|
setIsLoadingCep(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Validação de senha
|
|
if (formData.senha !== formData.confirmarSenha) {
|
|
alert("As senhas não coincidem!");
|
|
return;
|
|
}
|
|
|
|
if (formData.senha.length < 6) {
|
|
alert("A senha deve ter pelo menos 6 caracteres!");
|
|
return;
|
|
}
|
|
|
|
const selectedFunction = functions.find(f => f.id === formData.funcaoId);
|
|
onSubmit({
|
|
...formData,
|
|
funcaoLabel: selectedFunction?.nome
|
|
});
|
|
};
|
|
|
|
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",
|
|
];
|
|
|
|
return (
|
|
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-6 sm:p-8 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
{/* Logo dentro do card */}
|
|
<div className="flex justify-center mb-4">
|
|
<img
|
|
src="/logo.png"
|
|
alt="Photum Formaturas"
|
|
className="h-18 sm:h-30 w-auto max-w-[200px] object-contain"
|
|
/>
|
|
</div>
|
|
|
|
<div className="text-center mb-6">
|
|
<span
|
|
className="font-bold tracking-widest uppercase text-xs sm:text-sm"
|
|
style={{ color: "#B9CF33" }}
|
|
>
|
|
Cadastro de Profissional
|
|
</span>
|
|
<h2 className="mt-2 text-2xl sm:text-3xl font-serif font-bold text-gray-900">
|
|
Crie sua conta profissional
|
|
</h2>
|
|
<p className="mt-2 text-xs sm:text-sm text-gray-600">
|
|
Já tem uma conta?{" "}
|
|
<button
|
|
type="button"
|
|
onClick={() => (window.location.href = "/entrar")}
|
|
className="font-medium hover:opacity-80 transition-opacity"
|
|
style={{ color: "#B9CF33" }}
|
|
>
|
|
Voltar ao login
|
|
</button>
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Informações Pessoais */}
|
|
<div className="space-y-4">
|
|
<h3 className="font-semibold text-lg text-gray-700 border-b pb-2">
|
|
Informações Pessoais
|
|
</h3>
|
|
|
|
<Input
|
|
label="Nome Completo *"
|
|
type="text"
|
|
required
|
|
value={formData.nome}
|
|
onChange={(e) => handleChange("nome", e.target.value)}
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Foto de Perfil
|
|
</label>
|
|
<div className="flex items-center gap-4">
|
|
{avatarPreview ? (
|
|
<div className="relative w-24 h-24 rounded-full overflow-hidden border-4 border-[#B9CF33] shadow-lg">
|
|
<img
|
|
src={avatarPreview}
|
|
alt="Preview"
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAvatarPreview(null);
|
|
setFormData((prev) => ({ ...prev, avatar: null }));
|
|
}}
|
|
className="absolute top-0 right-0 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
|
|
title="Remover foto"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center border-2 border-dashed border-gray-300">
|
|
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
<div className="flex-1">
|
|
<label className="cursor-pointer inline-flex items-center px-4 py-2 bg-[#B9CF33] text-white rounded-lg hover:opacity-90 transition-opacity">
|
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
Escolher Foto
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleAvatarChange}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
<p className="text-xs text-gray-500 mt-1">PNG, JPG ou JPEG até 5MB</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Input
|
|
label="E-mail *"
|
|
type="email"
|
|
required
|
|
placeholder="nome@exemplo.com"
|
|
value={formData.email}
|
|
onChange={(e) => handleChange("email", e.target.value)}
|
|
/>
|
|
|
|
<Input
|
|
label="Senha *"
|
|
type="password"
|
|
required
|
|
minLength={6}
|
|
placeholder="••••••••"
|
|
value={formData.senha}
|
|
onChange={(e) => handleChange("senha", e.target.value)}
|
|
/>
|
|
|
|
<Input
|
|
label="Confirmar Senha *"
|
|
type="password"
|
|
required
|
|
minLength={6}
|
|
placeholder="••••••••"
|
|
value={formData.confirmarSenha}
|
|
onChange={(e) => handleChange("confirmarSenha", e.target.value)}
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Função Profissional *
|
|
</label>
|
|
{isLoadingFunctions ? (
|
|
<p className="text-sm text-gray-500">Carregando funções...</p>
|
|
) : functionsError ? (
|
|
<p className="text-sm text-red-500">
|
|
❌ Erro ao carregar funções. Verifique se o backend está
|
|
rodando.
|
|
</p>
|
|
) : (
|
|
<select
|
|
required
|
|
value={formData.funcaoId}
|
|
onChange={(e) => handleChange("funcaoId", e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
|
|
>
|
|
<option value="">Selecione uma função</option>
|
|
{functions.map((func) => (
|
|
<option key={func.id} value={func.id}>
|
|
{func.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Endereço */}
|
|
<div className="space-y-4">
|
|
<h3 className="font-semibold text-lg text-gray-700 border-b pb-2">
|
|
Endereço
|
|
</h3>
|
|
|
|
<Input
|
|
label="CEP *"
|
|
type="text"
|
|
required
|
|
placeholder="00000-000"
|
|
value={formData.cep}
|
|
onChange={(e) => {
|
|
const value = e.target.value.replace(/\D/g, "");
|
|
const formatted = value.replace(/^(\d{5})(\d)/, "$1-$2");
|
|
handleChange("cep", formatted);
|
|
}}
|
|
onBlur={handleCepBlur}
|
|
maxLength={9}
|
|
/>
|
|
{isLoadingCep && (
|
|
<p className="text-xs text-blue-500">Buscando endereço...</p>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<div className="sm:col-span-2">
|
|
<Input
|
|
label="Rua *"
|
|
type="text"
|
|
required
|
|
value={formData.rua}
|
|
onChange={(e) => handleChange("rua", e.target.value)}
|
|
/>
|
|
</div>
|
|
<Input
|
|
label="Número *"
|
|
type="text"
|
|
required
|
|
value={formData.numero}
|
|
onChange={(e) => {
|
|
const value = e.target.value.replace(/\D/g, "");
|
|
handleChange("numero", value);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<Input
|
|
label="Complemento"
|
|
type="text"
|
|
value={formData.complemento}
|
|
onChange={(e) => handleChange("complemento", e.target.value)}
|
|
/>
|
|
|
|
<Input
|
|
label="Bairro *"
|
|
type="text"
|
|
required
|
|
value={formData.bairro}
|
|
onChange={(e) => handleChange("bairro", e.target.value)}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<Input
|
|
label="Cidade *"
|
|
type="text"
|
|
required
|
|
value={formData.cidade}
|
|
onChange={(e) => handleChange("cidade", e.target.value)}
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
UF *
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.uf}
|
|
onChange={(e) => handleChange("uf", e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
|
|
>
|
|
<option value="">Selecione</option>
|
|
{ufs.map((uf) => (
|
|
<option key={uf} value={uf}>
|
|
{uf}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Contato */}
|
|
<div className="space-y-4">
|
|
<h3 className="font-semibold text-lg text-gray-700 border-b pb-2">
|
|
Contato
|
|
</h3>
|
|
|
|
<Input
|
|
label="WhatsApp *"
|
|
type="tel"
|
|
required
|
|
placeholder="(00) 00000-0000"
|
|
value={formData.whatsapp}
|
|
onChange={(e) => handleChange("whatsapp", e.target.value)}
|
|
mask="phone"
|
|
/>
|
|
</div>
|
|
|
|
{/* Dados Bancários */}
|
|
<div className="space-y-4">
|
|
<h3 className="font-semibold text-lg text-gray-700 border-b pb-2">
|
|
Dados Bancários
|
|
</h3>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
CPF ou CNPJ do Titular da Conta *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.cpfCnpj}
|
|
onChange={(e) => {
|
|
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);
|
|
}}
|
|
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"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<Input
|
|
label="Banco *"
|
|
type="text"
|
|
required
|
|
value={formData.banco}
|
|
onChange={(e) => handleChange("banco", e.target.value)}
|
|
/>
|
|
<Input
|
|
label="Agência *"
|
|
type="text"
|
|
required
|
|
value={formData.agencia}
|
|
onChange={(e) => {
|
|
const value = e.target.value.replace(/\D/g, "");
|
|
handleChange("agencia", value);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<Input
|
|
label="Conta *"
|
|
type="text"
|
|
required
|
|
value={formData.conta}
|
|
onChange={(e) => handleChange("conta", e.target.value)}
|
|
/>
|
|
<Input
|
|
label="PIX *"
|
|
type="text"
|
|
required
|
|
placeholder="E-mail, telefone, CPF ou chave aleatória"
|
|
value={formData.pix}
|
|
onChange={(e) => handleChange("pix", e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Informações Profissionais */}
|
|
<div className="space-y-4">
|
|
<h3 className="font-semibold text-lg text-gray-700 border-b pb-2">
|
|
Informações Profissionais
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Carro Disponível para uso? *
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.carroDisponivel}
|
|
onChange={(e) =>
|
|
handleChange("carroDisponivel", e.target.value)
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
|
|
>
|
|
<option value="sim">Sim</option>
|
|
<option value="nao">Não</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Possui estúdio? *
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.possuiEstudio}
|
|
onChange={(e) => {
|
|
handleChange("possuiEstudio", e.target.value);
|
|
if (e.target.value === "nao") {
|
|
handleChange("qtdEstudios", "0");
|
|
}
|
|
}}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
|
|
>
|
|
<option value="sim">Sim</option>
|
|
<option value="nao">Não</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{formData.possuiEstudio === "sim" && (
|
|
<Input
|
|
label="Quantidade de Estúdios *"
|
|
type="number"
|
|
required
|
|
min="1"
|
|
value={formData.qtdEstudios}
|
|
onChange={(e) => handleChange("qtdEstudios", e.target.value)}
|
|
/>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Equipamentos
|
|
</label>
|
|
<textarea
|
|
value={formData.equipamentos}
|
|
onChange={(e) => handleChange("equipamentos", e.target.value)}
|
|
rows={3}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent resize-none"
|
|
placeholder="Descreva os equipamentos que você possui (câmeras, lentes, flashes, etc.)"
|
|
/>
|
|
</div>
|
|
|
|
<Input
|
|
label="Tipo de Cartão (Cartão de Memória) *"
|
|
type="text"
|
|
required
|
|
placeholder="Ex: SD, CF, XQD"
|
|
value={formData.tipoCartao}
|
|
onChange={(e) => handleChange("tipoCartao", e.target.value)}
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Observações
|
|
</label>
|
|
<textarea
|
|
value={formData.observacao}
|
|
onChange={(e) => handleChange("observacao", e.target.value)}
|
|
rows={3}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent resize-none"
|
|
placeholder="Qualquer observação relevante..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botões */}
|
|
<div className="flex gap-4 pt-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={onCancel}
|
|
className="flex-1"
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button type="submit" className="flex-1">
|
|
Cadastrar Profissional
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|