photum/frontend/components/CourseForm.tsx
João Vitor 3178207353 feat: Sistema completo de Gestão FOT e melhorias no dashboard
- 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
2025-12-11 16:02:39 -03:00

565 lines
17 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Course, Institution } from "../types";
import { Input, Select } from "./Input";
import { Button } from "./Button";
import {
GraduationCap,
X,
Check,
AlertCircle,
AlertTriangle,
} from "lucide-react";
import {
getInstitutions,
getGraduationYears,
getCompanies,
getEducationLevels,
getUniversities,
} from "../services/apiService";
interface CourseFormProps {
onCancel: () => void;
onSubmit: (data: Partial<Course>) => void;
initialData?: Course;
userId: string;
institutions: Institution[];
}
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",
];
export const CourseForm: React.FC<CourseFormProps> = ({
onCancel,
onSubmit,
initialData,
userId,
institutions,
}) => {
const currentYear = new Date().getFullYear();
const [formData, setFormData] = useState<Partial<Course>>(
initialData || {
name: "",
institutionId: "",
year: currentYear,
semester: 1,
graduationType: "",
createdAt: new Date().toISOString(),
createdBy: userId,
isActive: true,
}
);
// Novos campos
const [fotId, setFotId] = useState("");
const [empresaId, setEmpresaId] = useState("");
const [educationLevel, setEducationLevel] = useState("");
const [observacoes, setObservacoes] = useState("");
const [instituicao, setInstituicao] = useState("");
const [anoFormatura, setAnoFormatura] = useState(currentYear.toString());
const [cidade, setCidade] = useState("");
const [estado, setEstado] = useState("");
const [gastosCaptacao, setGastosCaptacao] = useState("");
const [preVenda, setPreVenda] = useState("nao");
const [showToast, setShowToast] = useState(false);
const [error, setError] = useState("");
const [companies, setCompanies] = useState<
Array<{ id: string; nome: string }>
>([]);
const [educationLevels, setEducationLevels] = useState<
Array<{ id: string; nome: string }>
>([]);
const [universities, setUniversities] = useState<
Array<{ id: string; nome: string }>
>([]);
const [graduationYears, setGraduationYears] = useState<number[]>([]);
const [isBackendDown, setIsBackendDown] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(true);
// Buscar dados do backend
useEffect(() => {
const fetchBackendData = async () => {
setIsLoadingData(true);
const [
companiesResponse,
yearsResponse,
levelsResponse,
universitiesResponse,
] = await Promise.all([
getCompanies(),
getGraduationYears(),
getEducationLevels(),
getUniversities(),
]);
if (
companiesResponse.isBackendDown ||
yearsResponse.isBackendDown ||
levelsResponse.isBackendDown ||
universitiesResponse.isBackendDown
) {
setIsBackendDown(true);
} else {
setIsBackendDown(false);
if (companiesResponse.data) {
setCompanies(companiesResponse.data);
}
if (yearsResponse.data) {
setGraduationYears(yearsResponse.data);
}
if (levelsResponse.data) {
setEducationLevels(levelsResponse.data);
}
if (universitiesResponse.data) {
setUniversities(universitiesResponse.data);
}
}
setIsLoadingData(false);
};
fetchBackendData();
}, []);
// Popular campos quando estiver editando
useEffect(() => {
if (initialData) {
setFotId((initialData as any).fotId || "");
setEmpresaId((initialData as any).empresaId || "");
setEducationLevel((initialData as any).educationLevel || "");
setObservacoes((initialData as any).observacoes || "");
setInstituicao((initialData as any).instituicaoId || "");
setAnoFormatura(
(initialData as any).anoFormatura?.toString() || currentYear.toString()
);
setCidade((initialData as any).cidade || "");
setEstado((initialData as any).estado || "");
setGastosCaptacao((initialData as any).gastosCaptacao || "");
setPreVenda((initialData as any).preVenda || "nao");
}
}, [initialData]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validações
if (!fotId || fotId.trim().length < 1) {
setError("FOT é obrigatório");
return;
}
if (!empresaId) {
setError("Selecione uma empresa");
return;
}
if (!educationLevel) {
setError("Selecione o nível (EF I / EF II)");
return;
}
if (!instituicao) {
setError("Selecione uma instituição");
return;
}
if (!anoFormatura) {
setError("Selecione o ano de formatura");
return;
}
if (!cidade || cidade.trim().length < 2) {
setError("Cidade é obrigatória");
return;
}
if (!estado) {
setError("Selecione um estado");
return;
}
// Montar objeto de dados com os novos campos
const submitData = {
...formData,
fotId,
empresaId,
educationLevel,
observacoes,
instituicao,
anoFormatura: parseInt(anoFormatura),
cidade,
estado,
gastosCaptacao,
preVenda,
};
setShowToast(true);
setTimeout(() => {
onSubmit(submitData);
}, 1000);
};
const formatCurrency = (value: string) => {
// Remove tudo que não for número
const numbers = value.replace(/\D/g, "");
// Converte para número e formata
const amount = parseFloat(numbers) / 100;
return amount.toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
});
};
const handleGastosCaptacaoChange = (value: string) => {
if (value === "") {
setGastosCaptacao("");
return;
}
setGastosCaptacao(formatCurrency(value));
};
return (
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-2xl max-h-[85vh] overflow-y-auto mx-auto border border-gray-100 slide-up relative">
{/* Success Toast */}
{showToast && (
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in">
<Check className="text-brand-gold h-6 w-6" />
<div>
<h4 className="font-bold text-sm">Sucesso!</h4>
<p className="text-xs text-gray-300">
Curso cadastrado com sucesso.
</p>
</div>
</div>
)}
{/* Form Header */}
<div className="bg-gray-50 border-b px-8 py-6 flex justify-between items-center">
<div className="flex items-center space-x-3">
<GraduationCap className="text-brand-gold h-8 w-8" />
<div>
<h2 className="text-2xl font-serif text-brand-black">
{initialData ? "Editar Turma FOT" : "Cadastrar Turma FOT"}
</h2>
<p className="text-sm text-gray-500 mt-1">
Registre as informações da turma FOT
</p>
</div>
</div>
<button
onClick={onCancel}
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
>
<X size={20} className="text-gray-600" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6">
{/* Erro global */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 flex items-start">
<AlertCircle
size={18}
className="text-red-500 mr-2 flex-shrink-0 mt-0.5"
/>
<p className="text-sm text-red-700">{error}</p>
</div>
)}
{/* Informações da Turma FOT */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
Informações da Turma FOT
</h3>
{/* FOT ID */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
FOT*
</label>
<input
type="text"
required
value={fotId}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, "").slice(0, 5);
setFotId(value);
setError("");
}}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
placeholder="Digite o identificador FOT"
maxLength={5}
/>
</div>
{/* Empresa */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Empresa*
</label>
<select
required
value={empresaId}
onChange={(e) => {
setEmpresaId(e.target.value);
setError("");
}}
disabled={isLoadingData || isBackendDown}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
>
<option value="">Selecione uma empresa</option>
{companies.map((company) => (
<option key={company.id} value={company.id}>
{company.nome}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>
Backend não está rodando. Não é possível carregar empresas.
</span>
</div>
)}
{isLoadingData && !isBackendDown && (
<div className="mt-2 text-sm text-gray-500">
Carregando empresas...
</div>
)}
</div>
{/* EF I / EF II */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
EF I / EF II*
</label>
<select
required
value={educationLevel}
onChange={(e) => {
setEducationLevel(e.target.value);
setError("");
}}
disabled={isLoadingData || isBackendDown}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
>
<option value="">Selecione o nível</option>
{educationLevels.map((level) => (
<option key={level.id} value={level.id}>
{level.nome}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>
Backend não está rodando. Não é possível carregar níveis.
</span>
</div>
)}
{isLoadingData && !isBackendDown && (
<div className="mt-2 text-sm text-gray-500">
Carregando níveis...
</div>
)}
</div>
{/* Observações */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Observações
</label>
<textarea
value={observacoes}
onChange={(e) => setObservacoes(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
rows={3}
placeholder="Adicione observações sobre a turma..."
/>
</div>
{/* Instituição */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Instituição (Universidade)*
</label>
<select
required
value={instituicao}
onChange={(e) => {
setInstituicao(e.target.value);
setError("");
}}
disabled={isLoadingData || isBackendDown}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
>
<option value="">Selecione uma universidade</option>
{universities.map((university) => (
<option key={university.id} value={university.id}>
{university.nome}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>
Backend não está rodando. Não é possível carregar
universidades.
</span>
</div>
)}
{isLoadingData && !isBackendDown && (
<div className="mt-2 text-sm text-gray-500">
Carregando universidades...
</div>
)}
</div>
{/* Ano Formatura */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Ano de Formatura*
</label>
<select
required
value={anoFormatura}
onChange={(e) => {
setAnoFormatura(e.target.value);
setError("");
}}
disabled={isLoadingData || isBackendDown}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
>
<option value="">Selecione o ano</option>
{graduationYears.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>Backend offline</span>
</div>
)}
</div>
{/* Cidade e Estado */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Cidade*
</label>
<input
type="text"
required
value={cidade}
onChange={(e) => {
setCidade(e.target.value);
setError("");
}}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
placeholder="Cidade"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Estado*
</label>
<select
required
value={estado}
onChange={(e) => {
setEstado(e.target.value);
setError("");
}}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="">UF</option>
{UFS.map((uf) => (
<option key={uf} value={uf}>
{uf}
</option>
))}
</select>
</div>
</div>
{/* Gastos Captação */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Gastos Captação
</label>
<input
type="text"
value={gastosCaptacao}
onChange={(e) => handleGastosCaptacaoChange(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
placeholder="R$ 0,00"
/>
</div>
{/* Pré Venda */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Pré Venda*
</label>
<select
required
value={preVenda}
onChange={(e) => setPreVenda(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="sim">Sim</option>
<option value="nao">Não</option>
</select>
</div>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3 pt-6 border-t">
<Button variant="outline" onClick={onCancel} type="button">
Cancelar
</Button>
<Button type="submit" variant="secondary">
{initialData ? "Salvar Alterações" : "Cadastrar Turma"}
</Button>
</div>
</form>
</div>
);
};