- Implementadas ações de Editar e Excluir na página de Gestão de FOT - Adicionado filtro de busca para FOTs - Corrigido desalinhamento de colunas na tabela de Gestão de FOT - Atualizado FotForm para suportar a edição de registros existentes - Corrigido erro de renderização do React no Dashboard mapeando corretamente os objetos de atribuição - Removidos dados de mock (INITIAL_EVENTS) e corrigido erro de referência nula no DataContext - Adicionados métodos de atualização/exclusão ao apiService
369 lines
17 KiB
TypeScript
369 lines
17 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { X, AlertTriangle, Save, Loader } from "lucide-react";
|
|
import { Button } from "./Button";
|
|
import { getCompanies, getAvailableCourses, getGraduationYears, createCadastroFot, updateCadastroFot } from "../services/apiService";
|
|
|
|
interface FotFormProps {
|
|
onCancel: () => void;
|
|
onSubmit: (success: boolean) => void;
|
|
token: string;
|
|
existingFots: number[]; // List of existing FOT numbers for validation
|
|
initialData?: any; // For editing
|
|
}
|
|
|
|
export const FotForm: React.FC<FotFormProps> = ({ onCancel, onSubmit, token, existingFots, initialData }) => {
|
|
const [formData, setFormData] = useState({
|
|
fot: "",
|
|
empresa_id: "",
|
|
curso_id: "",
|
|
ano_formatura_id: "",
|
|
instituicao: "",
|
|
cidade: "",
|
|
estado: "",
|
|
observacoes: "",
|
|
gastos_captacao: "",
|
|
pre_venda: false,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (initialData) {
|
|
setFormData({
|
|
fot: initialData.fot.toString(),
|
|
empresa_id: initialData.empresa_id,
|
|
curso_id: initialData.curso_id,
|
|
ano_formatura_id: initialData.ano_formatura_id,
|
|
instituicao: initialData.instituicao || "",
|
|
cidade: initialData.cidade || "",
|
|
estado: initialData.estado || "",
|
|
observacoes: initialData.observacoes || "",
|
|
gastos_captacao: initialData.gastos_captacao ? initialData.gastos_captacao.toString() : "",
|
|
pre_venda: initialData.pre_venda || false,
|
|
});
|
|
}
|
|
}, [initialData]);
|
|
|
|
const [companies, setCompanies] = useState<any[]>([]);
|
|
const [coursesList, setCoursesList] = useState<any[]>([]);
|
|
const [years, setYears] = useState<any[]>([]);
|
|
|
|
const [loadingDependencies, setLoadingDependencies] = useState(true);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [fotError, setFotError] = useState<string | null>(null);
|
|
|
|
// Fetch dependencies
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
try {
|
|
const [companiesRes, coursesRes, yearsRes] = await Promise.all([
|
|
getCompanies(),
|
|
getAvailableCourses(),
|
|
getGraduationYears()
|
|
]);
|
|
|
|
if (companiesRes.data) setCompanies(companiesRes.data);
|
|
if (coursesRes.data) setCoursesList(coursesRes.data);
|
|
if (yearsRes.data) setYears(yearsRes.data);
|
|
} catch (err) {
|
|
console.error("Failed to load dependency data", err);
|
|
setError("Falha ao carregar opções. Tente novamente.");
|
|
} finally {
|
|
setLoadingDependencies(false);
|
|
}
|
|
};
|
|
loadData();
|
|
}, []);
|
|
|
|
// Validate FOT uniqueness
|
|
const handleFotChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const val = e.target.value;
|
|
setFormData({ ...formData, fot: val });
|
|
|
|
if (!initialData && val && existingFots.includes(parseInt(val))) { // Check uniqueness only if new or changed (and not matching self? Logic simplied: only if not editing self, but passed list does not track who is who, assumed list of ALL. If editing, I AM in the list. OK, let's refine: existingFots should ideally exclude self if editing. We'll leave basic check but maybe warn only. Or just relax check for edit if value matches initial.)
|
|
if (initialData && parseInt(val) === initialData.fot) {
|
|
setFotError(null);
|
|
} else {
|
|
if (existingFots.includes(parseInt(val))) {
|
|
setFotError(`O FOT ${val} já existe!`);
|
|
} else {
|
|
setFotError(null);
|
|
}
|
|
}
|
|
} else if (val && existingFots.includes(parseInt(val))) {
|
|
setFotError(`O FOT ${val} já existe!`);
|
|
} else {
|
|
setFotError(null);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (fotError) return;
|
|
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const payload = {
|
|
fot: parseInt(formData.fot),
|
|
empresa_id: formData.empresa_id,
|
|
curso_id: formData.curso_id,
|
|
ano_formatura_id: formData.ano_formatura_id, // Assuming string UUID from dropdown
|
|
instituicao: formData.instituicao,
|
|
cidade: formData.cidade,
|
|
estado: formData.estado,
|
|
observacoes: formData.observacoes,
|
|
gastos_captacao: parseFloat(formData.gastos_captacao) || 0,
|
|
pre_venda: formData.pre_venda,
|
|
};
|
|
|
|
let result;
|
|
if (initialData) {
|
|
result = await updateCadastroFot(initialData.id, payload, token);
|
|
} else {
|
|
result = await createCadastroFot(payload, token);
|
|
}
|
|
|
|
if (result.error) {
|
|
throw new Error(result.error);
|
|
}
|
|
|
|
onSubmit(true);
|
|
} catch (err: any) {
|
|
setError(err.message || "Erro ao salvar FOT.");
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
|
|
if (loadingDependencies) {
|
|
return (
|
|
<div className="bg-white rounded-lg p-8 flex flex-col items-center justify-center min-h-[400px]">
|
|
<Loader className="w-8 h-8 text-brand-gold animate-spin mb-4" />
|
|
<p className="text-gray-500">Carregando opções...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg w-full max-w-4xl p-6 md:p-8 max-h-[90vh] overflow-y-auto">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-serif font-bold text-brand-black">Cadastro FOT</h2>
|
|
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600 transition-colors">
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* FOT number - First and prominent */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Número FOT <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
value={formData.fot}
|
|
onChange={handleFotChange}
|
|
className={`w-full px-4 py-3 border rounded-md focus:outline-none focus:ring-2 transition-colors
|
|
${fotError
|
|
? "border-red-300 focus:ring-red-200 focus:border-red-400 bg-red-50"
|
|
: "border-gray-300 focus:ring-brand-gold focus:border-brand-gold"
|
|
}`}
|
|
placeholder="Ex: 25193"
|
|
/>
|
|
{fotError && (
|
|
<div className="mt-2 text-red-600 text-sm flex items-center gap-1">
|
|
<AlertTriangle size={14} />
|
|
<span>{fotError}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Empresa */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Empresa <span className="text-red-500">*</span>
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.empresa_id}
|
|
onChange={(e) => setFormData({ ...formData, empresa_id: 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 bg-white"
|
|
>
|
|
<option value="">Selecione a empresa</option>
|
|
{companies.map((c) => (
|
|
<option key={c.id} value={c.id}>{c.nome}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Curso */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Curso <span className="text-red-500">*</span>
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.curso_id}
|
|
onChange={(e) => setFormData({ ...formData, curso_id: 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 bg-white"
|
|
>
|
|
<option value="">Selecione o curso</option>
|
|
{coursesList.map((c) => (
|
|
<option key={c.id} value={c.id}>{c.nome || c.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Ano Formatura */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Ano Formatura <span className="text-red-500">*</span>
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.ano_formatura_id}
|
|
onChange={(e) => setFormData({ ...formData, ano_formatura_id: 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 bg-white"
|
|
>
|
|
<option value="">Selecione o ano</option>
|
|
{years.map((y: any) => (
|
|
<option key={y.id} value={y.id}>{y.ano_semestre}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Instituição */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Instituição <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.instituicao}
|
|
onChange={(e) => setFormData({ ...formData, instituicao: 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="Nome da Instituição"
|
|
/>
|
|
</div>
|
|
|
|
{/* Cidade */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Cidade <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.cidade}
|
|
onChange={(e) => setFormData({ ...formData, cidade: 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="Cidade"
|
|
/>
|
|
</div>
|
|
|
|
{/* Estado */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Estado <span className="text-red-500">*</span>
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.estado}
|
|
onChange={(e) => setFormData({ ...formData, estado: 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 bg-white"
|
|
>
|
|
<option value="">UF</option>
|
|
<option value="SP">SP</option>
|
|
<option value="RJ">RJ</option>
|
|
<option value="MG">MG</option>
|
|
<option value="RS">RS</option>
|
|
<option value="PR">PR</option>
|
|
<option value="SC">SC</option>
|
|
{/* Add other states as needed */}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Gastos Captação */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Gastos Captação
|
|
</label>
|
|
<div className="relative">
|
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">R$</span>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
value={formData.gastos_captacao}
|
|
onChange={(e) => setFormData({ ...formData, gastos_captacao: e.target.value })}
|
|
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
placeholder="0,00"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Pre Venda Checkbox */}
|
|
<div className="flex items-center h-full pt-6">
|
|
<label className="flex items-center space-x-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.pre_venda}
|
|
onChange={(e) => setFormData({ ...formData, pre_venda: e.target.checked })}
|
|
className="w-5 h-5 text-brand-gold border-gray-300 rounded focus:ring-brand-gold"
|
|
/>
|
|
<span className="text-gray-900 font-medium">Pré Venda</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Observações */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Observações
|
|
</label>
|
|
<textarea
|
|
rows={3}
|
|
value={formData.observacoes}
|
|
onChange={(e) => setFormData({ ...formData, observacoes: 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="Observações adicionais..."
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-4 bg-red-50 text-red-700 rounded-md flex items-center gap-2">
|
|
<AlertTriangle size={20} />
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3 pt-6 border-t border-gray-100">
|
|
<Button type="button" variant="secondary" onClick={onCancel}>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting || !!fotError}
|
|
className="flex items-center gap-2"
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<Loader size={18} className="animate-spin" />
|
|
Salvando...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save size={18} />
|
|
Salvar Cadastro
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|