666 lines
32 KiB
TypeScript
666 lines
32 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
X,
|
|
User,
|
|
Camera,
|
|
Eye,
|
|
EyeOff,
|
|
} from "lucide-react";
|
|
import { Button } from "./Button";
|
|
import {
|
|
createProfessional,
|
|
updateProfessional,
|
|
getUploadURL,
|
|
uploadFileToSignedUrl,
|
|
} from "../services/apiService";
|
|
import { useAuth } from "../contexts/AuthContext";
|
|
import { Professional, CreateProfessionalDTO } from "../types";
|
|
|
|
interface ProfessionalModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
professional: Professional | null; // If null, it's Add Mode
|
|
existingProfessionals?: Professional[];
|
|
onSwitchToEdit?: (prof: Professional) => void;
|
|
roles: { id: string; nome: string }[];
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
export const ProfessionalModal: React.FC<ProfessionalModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
professional,
|
|
existingProfessionals = [],
|
|
onSwitchToEdit,
|
|
roles,
|
|
onSuccess,
|
|
}) => {
|
|
const { token: contextToken, user } = useAuth(); // Get User
|
|
const token = contextToken || "";
|
|
|
|
const initialFormState: CreateProfessionalDTO & { senha?: string; confirmarSenha?: string } = {
|
|
nome: "",
|
|
regiao: "",
|
|
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>("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isLoadingCep, setIsLoadingCep] = useState(false);
|
|
|
|
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"];
|
|
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
if (professional) {
|
|
// Edit Mode
|
|
setFormData({
|
|
nome: professional.nome,
|
|
regiao: professional.regiao || "",
|
|
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: "",
|
|
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));
|
|
} else {
|
|
// Add Mode
|
|
const defaultRegion = user?.allowedRegions && user.allowedRegions.length > 0 ? user.allowedRegions[0] : "SP";
|
|
setFormData({ ...initialFormState, regiao: defaultRegion });
|
|
setAvatarPreview("");
|
|
}
|
|
setAvatarFile(null);
|
|
}
|
|
}, [isOpen, professional]); // user dependency intentionally omitted to avoid reset loop, but safe to add if needed
|
|
|
|
// Helpers
|
|
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");
|
|
} 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")
|
|
.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;
|
|
};
|
|
|
|
// Listen for rating changes
|
|
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,
|
|
});
|
|
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 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 handleCpfBlur = async () => {
|
|
const cpf = formData.cpf_cnpj_titular?.replace(/\D/g, "") || "";
|
|
if (cpf.length < 11) return;
|
|
|
|
// 1. Check Local Existence (Admin / TeamPage)
|
|
if (existingProfessionals.length > 0 && !professional) {
|
|
const found = existingProfessionals.find(p => {
|
|
const pCpf = p.cpf_cnpj_titular?.replace(/\D/g, "");
|
|
return pCpf === cpf;
|
|
});
|
|
|
|
if (found) {
|
|
if (confirm(`O profissional "${found.nome}" já está cadastrado com este CPF.\nDeseja abrir o cadastro existente para edição?`)) {
|
|
if (onSwitchToEdit) {
|
|
onSwitchToEdit(found);
|
|
return;
|
|
}
|
|
} else {
|
|
// User said No to edit. They probably want to create NEW but with same CPF?
|
|
// That's impossible due to unique constraint.
|
|
// Warn them.
|
|
alert("Atenção: Você não conseguirá salvar um novo profissional com o mesmo CPF.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Check API (for safety or if list is incomplete)
|
|
try {
|
|
const response = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/check?cpf=${cpf}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.exists) {
|
|
// If we are here, matched in API but maybe NOT in local list (if pagination existed)
|
|
// Or maybe user clicked "No" above.
|
|
if (!professional && data.name) {
|
|
// If we didn't switch to edit (e.g. user refused or logic above didn't catch it)
|
|
// We show the warning.
|
|
if (!existingProfessionals.some(p => p.cpf_cnpj_titular?.replace(/\D/g,"") === cpf)) {
|
|
alert(`ATENÇÃO: Este CPF já está cadastrado para o profissional "${data.name}" no banco de dados.`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao verificar CPF:", error);
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
if (!professional && (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;
|
|
|
|
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 };
|
|
delete payload.senha;
|
|
delete payload.confirmarSenha;
|
|
|
|
if (professional) {
|
|
// Update
|
|
await updateProfessional(professional.id, payload, token);
|
|
alert("Profissional atualizado com sucesso!");
|
|
} else {
|
|
// Create
|
|
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",
|
|
tipo_profissional: roles.find(r => r.id === formData.funcao_profissional_id)?.nome || "",
|
|
ativo: true,
|
|
}, token);
|
|
|
|
if (createRes.error) {
|
|
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!");
|
|
}
|
|
|
|
onSuccess();
|
|
onClose();
|
|
} catch (error: any) {
|
|
console.error("Error submitting form:", error);
|
|
alert(error.message || "Erro ao salvar profissional. Verifique o console.");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<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">{professional ? "Editar Profissional" : "Novo Profissional"}</h2>
|
|
<button onClick={onClose}><X size={24} /></button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Avatar */}
|
|
<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>
|
|
|
|
{/* Region Selection (Only for Multi-Region Admins) */}
|
|
{(user?.allowedRegions && user.allowedRegions.length > 1) || user?.role === "SUPERADMIN" ? (
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Região de Atuação *</label>
|
|
<select
|
|
value={formData.regiao || ""}
|
|
onChange={e => setFormData({ ...formData, regiao: e.target.value })}
|
|
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"
|
|
>
|
|
<option value="SP">São Paulo (SP)</option>
|
|
<option value="MG">Minas Gerais (MG)</option>
|
|
</select>
|
|
<p className="text-xs text-gray-500 mt-1">Selecione onde este profissional irá atuar.</p>
|
|
</div>
|
|
) : null}
|
|
|
|
{/* Seleção de Função (Movida para o topo) */}
|
|
<div className="col-span-1 md:col-span-2 mb-4">
|
|
<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>
|
|
|
|
{/* Name & CPF (Top) */}
|
|
<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>
|
|
{(!formData.funcoes_ids?.every(id => roles.find(r => r.id === id)?.nome.toLowerCase().includes("pesquisa")) || (formData.funcoes_ids?.length === 0)) && (
|
|
<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) })}
|
|
onBlur={handleCpfBlur}
|
|
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>
|
|
|
|
{/* Functions removed from here */}
|
|
|
|
{/* Email & Pass */}
|
|
<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>
|
|
|
|
{!professional && (
|
|
<>
|
|
<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>
|
|
|
|
|
|
{/* Endereço */}
|
|
{(!formData.funcoes_ids?.every(id => roles.find(r => r.id === id)?.nome.toLowerCase().includes("pesquisa")) || (formData.funcoes_ids?.length === 0)) && (
|
|
<>
|
|
<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 || false} 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 || false} 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 || 0} 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 || 0} 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 || 0} 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 || 0} 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 || 0} 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(String(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">Valor 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 focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" placeholder="Ex: R$ 200,00" />
|
|
</div>
|
|
<div className="flex items-center pt-6">
|
|
<label className="flex items-center gap-2">
|
|
<input type="checkbox" checked={formData.extra_por_equipamento || false} 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">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 focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" placeholder="Liste os equipamentos..." />
|
|
</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 focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
|
|
<div className="flex justify-end gap-4 pt-4 border-t sticky bottom-0 bg-white">
|
|
<Button type="button" variant="secondary" onClick={onClose}>Cancelar</Button>
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
{isSubmitting ? "Salvando..." : professional ? "Salvar Alterações" : "Criar Profissional"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|