- Sistema FOT (Formatura Operations Tracking): * Tela de Gestão FOT (/cursos) com tabela Excel-style * Modal CourseForm com 10 campos (FOT, Empresa, Instituição, etc) * Validação de FOT (5 dígitos numéricos) * Edição de turmas ao clicar na linha * Integração com API backend (empresas, níveis educacionais, universidades) - Dashboard renovado (/painel): * Tabela com 8 colunas (FOT, Data, Curso, Instituição, Ano, Empresa, Tipo, Status) * Filtros avançados: FOT (busca numérica), Data, Tipo de Evento * Removidos filtros de Estado e Cidade * Página de detalhes com tabela vertical (12 informações) * Botão Aprovar redireciona para modal de equipe - Sistema de Aprovação Dupla (/aprovacao): * 2 tabelas separadas por abas (Usuários Normais e Profissionais) * Coluna Universidade renomeada para Empresa * Coluna Função nos profissionais * Workflow de aprovação com atribuição de equipe - Cadastro Profissional (/cadastro-profissional): * Formulário específico para fotógrafos * Dropdown de Função Profissional da API * Tratamento de erro quando backend offline - Modal de Criar Evento: * Tipo de Evento como primeiro campo * Nome do Evento (Opcional) como segundo campo - Componentes novos: * EventTable.tsx - Tabela de eventos com ordenação * EventFiltersBar.tsx - Filtros avançados (3 filtros) * CourseForm.tsx - Formulário FOT completo * ProfessionalForm.tsx - Cadastro profissional - API Service: * Integração com backend Go * Endpoints: /api/empresas, /api/funcoes, /api/niveis-educacionais, /api/universidades, /graduation-years - Documentação: * README.md principal atualizado * frontend/README.md atualizado * Documentação completa de componentes e features
498 lines
14 KiB
TypeScript
498 lines
14 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;
|
|
funcaoId: string;
|
|
rua: string;
|
|
numero: string;
|
|
complemento: string;
|
|
bairro: string;
|
|
cidade: string;
|
|
uf: string;
|
|
whatsapp: string;
|
|
cpfCnpj: string;
|
|
banco: string;
|
|
agencia: string;
|
|
contaPix: string;
|
|
carroDisponivel: string;
|
|
possuiEstudio: string;
|
|
qtdEstudios: string;
|
|
tipoCartao: string;
|
|
observacao: 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 [formData, setFormData] = useState<ProfessionalData>({
|
|
nome: "",
|
|
email: "",
|
|
senha: "",
|
|
confirmarSenha: "",
|
|
funcaoId: "",
|
|
rua: "",
|
|
numero: "",
|
|
complemento: "",
|
|
bairro: "",
|
|
cidade: "",
|
|
uf: "",
|
|
whatsapp: "",
|
|
cpfCnpj: "",
|
|
banco: "",
|
|
agencia: "",
|
|
contaPix: "",
|
|
carroDisponivel: "nao",
|
|
possuiEstudio: "nao",
|
|
qtdEstudios: "0",
|
|
tipoCartao: "",
|
|
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 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;
|
|
}
|
|
|
|
onSubmit(formData);
|
|
};
|
|
|
|
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)}
|
|
/>
|
|
|
|
<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>
|
|
|
|
<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-3 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);
|
|
}}
|
|
/>
|
|
<Input
|
|
label="Conta / Pix *"
|
|
type="text"
|
|
required
|
|
value={formData.contaPix}
|
|
onChange={(e) => handleChange("contaPix", 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)}
|
|
/>
|
|
)}
|
|
|
|
<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">
|
|
Nome do Titular da Conta ou Observação
|
|
</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>
|
|
);
|
|
};
|