- Adiciona role 'agenda_viewer' para profissionais visualizarem apenas suas agendas - Implementa middleware de autorização baseado em roles - Adiciona validação de permissões nos endpoints de agenda - Melhora exibição de dados financeiros e logísticos - Atualiza componentes frontend para melhor UX - Adiciona documentação sobre o papel de visualização de agenda
300 lines
11 KiB
TypeScript
300 lines
11 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
ProfessionalForm,
|
|
ProfessionalData,
|
|
} from "../components/ProfessionalForm";
|
|
import { useAuth } from "../contexts/AuthContext";
|
|
import { X } from "lucide-react";
|
|
import { verifyAccessCode } from "../services/apiService";
|
|
|
|
interface ProfessionalRegisterProps {
|
|
onNavigate: (page: string) => void;
|
|
}
|
|
|
|
export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
|
onNavigate,
|
|
}) => {
|
|
const { register } = useAuth();
|
|
const [isSuccess, setIsSuccess] = useState(false);
|
|
const [showAccessCodeModal, setShowAccessCodeModal] = useState(true);
|
|
const [accessCode, setAccessCode] = useState("");
|
|
const [codeError, setCodeError] = useState("");
|
|
const [isAccessValidated, setIsAccessValidated] = useState(false);
|
|
|
|
// Verificar se já tem validação ativa na sessão
|
|
useEffect(() => {
|
|
const validated = sessionStorage.getItem('professionalAccessValidated');
|
|
if (validated === 'true') {
|
|
setIsAccessValidated(true);
|
|
setShowAccessCodeModal(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleVerifyCode = async () => {
|
|
if (accessCode.trim() === "") {
|
|
setCodeError("Por favor, digite o código de acesso");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await verifyAccessCode(accessCode.toUpperCase());
|
|
if (res.data && res.data.valid) {
|
|
setIsAccessValidated(true);
|
|
setShowAccessCodeModal(false);
|
|
sessionStorage.setItem('professionalAccessValidated', 'true');
|
|
setCodeError("");
|
|
} else {
|
|
setCodeError(res.data?.error || "Código de acesso inválido ou expirado");
|
|
}
|
|
} catch (e) {
|
|
setCodeError("Erro ao verificar código");
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (professionalData: ProfessionalData) => {
|
|
try {
|
|
// 1. Cadastrar Usuário (Auth) e Logar Automaticamente
|
|
const authResult = await register({
|
|
nome: professionalData.nome,
|
|
email: professionalData.email,
|
|
senha: professionalData.senha,
|
|
telefone: professionalData.whatsapp,
|
|
role: "PHOTOGRAPHER", // Role fixa para profissionais
|
|
tipo_profissional: professionalData.funcaoLabel || "", // Envia o nome da função (ex: Cinegrafista)
|
|
});
|
|
|
|
if (!authResult.success) {
|
|
throw new Error("Falha no cadastro de usuário.");
|
|
}
|
|
|
|
// 2. Criar Perfil Profissional (autenticado)
|
|
const { createProfessional, getUploadURL, uploadFileToSignedUrl } = await import("../services/apiService");
|
|
|
|
let avatarUrl = "";
|
|
// Upload de Avatar (obrigatório)
|
|
if (!professionalData.avatar) {
|
|
throw new Error("A foto de perfil é obrigatória.");
|
|
}
|
|
|
|
try {
|
|
console.log("Iniciando upload do avatar...");
|
|
const uploadRes = await getUploadURL(professionalData.avatar.name, professionalData.avatar.type);
|
|
|
|
if (uploadRes.error || !uploadRes.data) {
|
|
throw new Error(uploadRes.error || "Erro ao obter URL de upload");
|
|
}
|
|
|
|
await uploadFileToSignedUrl(uploadRes.data.upload_url, professionalData.avatar);
|
|
avatarUrl = uploadRes.data.public_url;
|
|
console.log("Upload concluído. URL:", avatarUrl);
|
|
} catch (err) {
|
|
console.error("Erro no upload do avatar:", err);
|
|
throw new Error("Falha ao enviar foto de perfil: " + (err instanceof Error ? err.message : "Erro desconhecido"));
|
|
}
|
|
|
|
// Mapear dados do formulário para o payload esperado pelo backend
|
|
|
|
// Mapear dados do formulário para o payload esperado pelo backend
|
|
// O curl fornecido pelo usuário mostra campos underscore (snake_case)
|
|
const payload: any = {
|
|
nome: professionalData.nome,
|
|
agencia: professionalData.agencia,
|
|
banco: professionalData.banco,
|
|
carro_disponivel: professionalData.carroDisponivel === "sim",
|
|
cidade: professionalData.cidade,
|
|
conta_pix: professionalData.pix, // Usando o campo PIX separado
|
|
cpf_cnpj_titular: professionalData.cpfCnpj,
|
|
endereco: `${professionalData.cep}, ${professionalData.rua}, ${professionalData.numero} - ${professionalData.bairro}`,
|
|
equipamentos: professionalData.equipamentos,
|
|
extra_por_equipamento: false, // Default
|
|
funcao_profissional_id: professionalData.funcaoId,
|
|
funcoes_ids: professionalData.funcoesIds, // Pass multi-select IDs
|
|
observacao: professionalData.observacao,
|
|
qtd_estudio: parseInt(professionalData.qtdEstudios) || 0,
|
|
tem_estudio: professionalData.possuiEstudio === "sim",
|
|
tipo_cartao: professionalData.tipoCartao,
|
|
uf: professionalData.uf,
|
|
whatsapp: professionalData.whatsapp,
|
|
|
|
// Campos numéricos default
|
|
desempenho_evento: 0,
|
|
disp_horario: 0,
|
|
educacao_simpatia: 0,
|
|
qual_tec: 0,
|
|
tabela_free: "",
|
|
avatar_url: avatarUrl,
|
|
};
|
|
|
|
const profResult = await createProfessional(payload, authResult.token);
|
|
|
|
if (profResult.error) {
|
|
// Se falhar o perfil, o usuário foi criado :/
|
|
// Idealmente limparíamos ou avisaríamos para completar perfil depois
|
|
throw new Error("Usuário criado, mas erro ao salvar dados profissionais: " + profResult.error);
|
|
}
|
|
|
|
console.log("Profissional cadastrado com sucesso!");
|
|
// Limpar dados de sessão após cadastro bem-sucedido
|
|
sessionStorage.removeItem('professionalAccessValidated');
|
|
sessionStorage.removeItem('professionalAccessData');
|
|
setIsSuccess(true);
|
|
} catch (error: any) {
|
|
console.error("Erro ao cadastrar profissional:", error);
|
|
alert(error.message || "Erro ao cadastrar profissional. Tente novamente.");
|
|
}
|
|
};
|
|
|
|
if (isSuccess) {
|
|
return (
|
|
<div
|
|
className="min-h-screen flex items-center justify-center"
|
|
style={{ backgroundColor: "#B9CF33" }}
|
|
>
|
|
<div className="text-center fade-in max-w-md px-4 py-8 bg-white rounded-2xl shadow-xl">
|
|
<div className="w-16 h-16 bg-yellow-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<svg
|
|
className="w-8 h-8 text-white"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
|
Cadastro Pendente de Aprovação
|
|
</h2>
|
|
<p className="text-gray-600 mb-4">
|
|
Seu cadastro foi realizado com sucesso e está aguardando aprovação.
|
|
</p>
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
|
<p className="text-sm text-yellow-800">
|
|
<strong>Atenção:</strong> Enquanto seu cadastro não for aprovado,
|
|
você não terá acesso ao sistema.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
window.location.href = "/";
|
|
}}
|
|
className="w-full px-6 py-2 text-white font-semibold rounded-lg hover:bg-opacity-90 transition-colors"
|
|
style={{ backgroundColor: "#B9CF33" }}
|
|
>
|
|
OK
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-white p-4 py-8 pt-24">
|
|
{/* Modal de Código de Acesso Obrigatório */}
|
|
{showAccessCodeModal && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
|
style={{ backgroundColor: "rgba(0, 0, 0, 0.5)" }}
|
|
>
|
|
<div
|
|
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8"
|
|
onClick={(e) => e.stopPropagation()}
|
|
style={{
|
|
animation: "fadeInScale 0.3s ease-out forwards"
|
|
}}
|
|
>
|
|
<style>{`
|
|
@keyframes fadeInScale {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.9);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
`}</style>
|
|
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h2 className="text-2xl font-bold text-gray-900">
|
|
Código de Acesso
|
|
</h2>
|
|
<button
|
|
onClick={() => {
|
|
setShowAccessCodeModal(false);
|
|
window.location.href = "/";
|
|
}}
|
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-gray-600 mb-6">
|
|
Digite o código de acesso fornecido pela empresa para continuar com o cadastro.
|
|
</p>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Código de Acesso *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={accessCode}
|
|
onChange={(e) => {
|
|
setAccessCode(e.target.value.toUpperCase());
|
|
setCodeError("");
|
|
}}
|
|
onKeyPress={(e) => e.key === "Enter" && handleVerifyCode()}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent uppercase"
|
|
placeholder="DIGITE O CÓDIGO"
|
|
autoFocus
|
|
/>
|
|
{codeError && (
|
|
<p className="text-red-500 text-sm mt-2">{codeError}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
|
<p className="text-sm text-yellow-800">
|
|
<strong>Atenção:</strong> O código de acesso é fornecido pela
|
|
empresa e tem validade temporária.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={() => {
|
|
setShowAccessCodeModal(false);
|
|
window.location.href = "/";
|
|
}}
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 font-semibold hover:bg-gray-50 transition-colors"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
onClick={handleVerifyCode}
|
|
className="flex-1 px-4 py-2 rounded-lg text-white font-semibold transition-colors"
|
|
style={{ backgroundColor: "#6B21A8" }}
|
|
>
|
|
Verificar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isAccessValidated && (
|
|
<ProfessionalForm
|
|
onSubmit={handleSubmit}
|
|
onCancel={() => onNavigate("cadastro")}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|