feat(fot): implementa cadastro e listagem de FOT
- Refatora CourseManagement para listar dados de /api/cadastro-fot - Cria componente FotForm para novo cadastro de turmas - Adiciona validação de unicidade para número FOT - Integra dropdowns com endpoints /api/cursos e /api/anos-formaturas - Corrige duplicidade no registro de profissionais no backend
This commit is contained in:
parent
85e07c2f26
commit
d84d6ff022
4 changed files with 631 additions and 191 deletions
|
|
@ -55,7 +55,9 @@ func (h *Handler) Register(c *gin.Context) {
|
||||||
// If role is Photographer, the Service code checks `profissionalData`.
|
// If role is Photographer, the Service code checks `profissionalData`.
|
||||||
// I should probably populate `profissionalData` if it's a professional.
|
// I should probably populate `profissionalData` if it's a professional.
|
||||||
|
|
||||||
if req.Role == "PHOTOGRAPHER" || req.Role == "BUSINESS_OWNER" {
|
// PHOTOGRAPHER role is handled by a separate flow (ProfessionalRegister) that calls CreateProfissional after Register.
|
||||||
|
// We skip creating the partial profile here to avoid duplicates.
|
||||||
|
if req.Role == "BUSINESS_OWNER" {
|
||||||
profData = &profissionais.CreateProfissionalInput{
|
profData = &profissionais.CreateProfissionalInput{
|
||||||
Nome: req.Nome,
|
Nome: req.Nome,
|
||||||
Whatsapp: &req.Telefone,
|
Whatsapp: &req.Telefone,
|
||||||
|
|
|
||||||
336
frontend/components/FotForm.tsx
Normal file
336
frontend/components/FotForm.tsx
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { X, AlertTriangle, Save, Loader } from "lucide-react";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { getCompanies, getAvailableCourses, getGraduationYears, createCadastroFot } from "../services/apiService";
|
||||||
|
|
||||||
|
interface FotFormProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (success: boolean) => void;
|
||||||
|
token: string;
|
||||||
|
existingFots: number[]; // List of existing FOT numbers for validation
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FotForm: React.FC<FotFormProps> = ({ onCancel, onSubmit, token, existingFots }) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
fot: "",
|
||||||
|
empresa_id: "",
|
||||||
|
curso_id: "",
|
||||||
|
ano_formatura_id: "",
|
||||||
|
instituicao: "",
|
||||||
|
cidade: "",
|
||||||
|
estado: "",
|
||||||
|
observacoes: "",
|
||||||
|
gastos_captacao: "",
|
||||||
|
pre_venda: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (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,
|
||||||
|
};
|
||||||
|
|
||||||
|
const 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.");
|
||||||
|
} finally {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,25 +1,72 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useData } from "../contexts/DataContext";
|
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { UserRole, Course } from "../types";
|
import { UserRole } from "../types";
|
||||||
import { CourseForm } from "../components/CourseForm";
|
|
||||||
import { Plus, Edit, Trash2 } from "lucide-react";
|
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
|
import { getCadastroFot } from "../services/apiService";
|
||||||
|
import { Briefcase, AlertTriangle, Plus, Edit } from "lucide-react";
|
||||||
|
import { FotForm } from "../components/FotForm";
|
||||||
|
|
||||||
|
interface FotData {
|
||||||
|
id: string;
|
||||||
|
fot: number;
|
||||||
|
empresa_nome: string;
|
||||||
|
curso_nome: string;
|
||||||
|
observacoes: string;
|
||||||
|
instituicao: string;
|
||||||
|
ano_formatura_label: string;
|
||||||
|
cidade: string;
|
||||||
|
estado: string;
|
||||||
|
gastos_captacao: number;
|
||||||
|
pre_venda: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const CourseManagement: React.FC = () => {
|
export const CourseManagement: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { institutions, courses, addCourse, updateCourse } = useData();
|
const [fotList, setFotList] = useState<FotData[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
|
||||||
const [showCourseForm, setShowCourseForm] = useState(false);
|
// Verificar se è admin
|
||||||
const [editingCourse, setEditingCourse] = useState<Course | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verificar se é admin
|
|
||||||
const isAdmin =
|
const isAdmin =
|
||||||
user?.role === UserRole.SUPERADMIN ||
|
user?.role === UserRole.SUPERADMIN ||
|
||||||
user?.role === UserRole.BUSINESS_OWNER;
|
user?.role === UserRole.BUSINESS_OWNER;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchFotData();
|
||||||
|
}, [isAdmin]);
|
||||||
|
|
||||||
|
const fetchFotData = async () => {
|
||||||
|
if (!isAdmin) return;
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await getCadastroFot(token);
|
||||||
|
if (response.data) {
|
||||||
|
setFotList(response.data);
|
||||||
|
setError(null);
|
||||||
|
} else if (response.error) {
|
||||||
|
setError(response.error);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("Erro ao carregar dados.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
fetchFotData(); // Refresh list after successful creation
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract existing FOT numbers for uniqueness validation
|
||||||
|
const existingFots = fotList.map(item => item.fot);
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 pt-32 pb-12">
|
<div className="min-h-screen bg-gray-50 pt-32 pb-12">
|
||||||
|
|
@ -33,38 +80,6 @@ export const CourseManagement: React.FC = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddCourse = () => {
|
|
||||||
setEditingCourse(undefined);
|
|
||||||
setShowCourseForm(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditCourse = (course: Course) => {
|
|
||||||
setEditingCourse(course);
|
|
||||||
setShowCourseForm(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmitCourse = (courseData: Partial<Course>) => {
|
|
||||||
if (editingCourse) {
|
|
||||||
updateCourse(editingCourse.id, courseData);
|
|
||||||
} else {
|
|
||||||
const newCourse: Course = {
|
|
||||||
id: `course-${Date.now()}`,
|
|
||||||
name: courseData.name || "",
|
|
||||||
institutionId: courseData.institutionId || "",
|
|
||||||
year: courseData.year || new Date().getFullYear(),
|
|
||||||
semester: courseData.semester,
|
|
||||||
graduationType: courseData.graduationType || "",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
createdBy: user?.id || "",
|
|
||||||
isActive: courseData.isActive !== false,
|
|
||||||
...courseData,
|
|
||||||
};
|
|
||||||
addCourse(newCourse);
|
|
||||||
}
|
|
||||||
setShowCourseForm(false);
|
|
||||||
setEditingCourse(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
|
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
@ -80,34 +95,45 @@ export const CourseManagement: React.FC = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAddCourse}
|
onClick={() => setShowForm(true)}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="flex items-center space-x-2"
|
className="flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
<span>Cadastrar Turma</span>
|
<span>Cadastro FOT</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Course Form Modal */}
|
|
||||||
{showCourseForm && (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto">
|
{/* Form Modal */}
|
||||||
<CourseForm
|
{showForm && (
|
||||||
onCancel={() => {
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
setShowCourseForm(false);
|
<FotForm
|
||||||
setEditingCourse(undefined);
|
onCancel={() => setShowForm(false)}
|
||||||
}}
|
onSubmit={handleFormSubmit}
|
||||||
onSubmit={handleSubmitCourse}
|
token={localStorage.getItem("token") || ""}
|
||||||
initialData={editingCourse}
|
existingFots={existingFots}
|
||||||
userId={user?.id || ""}
|
|
||||||
institutions={institutions}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-center gap-3 text-red-700">
|
||||||
|
<AlertTriangle size={20} />
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Courses Table */}
|
{/* Courses Table */}
|
||||||
<div className="bg-white rounded-lg shadow border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-lg shadow border border-gray-200 overflow-hidden">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-12 text-center text-gray-500">
|
||||||
|
Carregando dados...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
|
|
@ -148,90 +174,90 @@ export const CourseManagement: React.FC = () => {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{courses.length === 0 ? (
|
{fotList.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={11}
|
colSpan={11}
|
||||||
className="px-6 py-12 text-center text-gray-500"
|
className="px-6 py-12 text-center text-gray-500"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<Briefcase className="w-12 h-12 text-gray-300 mb-3" />
|
||||||
<p className="text-lg font-medium">
|
<p className="text-lg font-medium">
|
||||||
Nenhuma turma cadastrada
|
Nenhuma turma FOT encontrada
|
||||||
</p>
|
|
||||||
<p className="text-sm mt-1">
|
|
||||||
Clique em "Cadastrar Turma" para adicionar a primeira
|
|
||||||
turma
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
courses.map((course) => (
|
fotList.map((item) => (
|
||||||
<tr
|
<tr
|
||||||
key={course.id}
|
key={item.id}
|
||||||
className="hover:bg-gray-50 transition-colors"
|
className="hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">
|
||||||
{(course as any).fotId || "-"}
|
{item.fot || "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{(course as any).empresaNome || "-"}
|
{item.empresa_nome || "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{(course as any).educationLevel || "-"}
|
{item.curso_nome || "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="text-sm text-gray-600 max-w-xs truncate">
|
<div className="text-sm text-gray-600 max-w-xs truncate">
|
||||||
{(course as any).observacoes || "-"}
|
{item.observacoes || "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{(course as any).instituicao || "-"}
|
{item.instituicao || "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{(course as any).anoFormatura || course.year || "-"}
|
{item.ano_formatura_label || "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{(course as any).cidade || "-"}
|
{item.cidade || "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{(course as any).estado || "-"}
|
{item.estado || "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{(course as any).gastosCaptacao || "R$ 0,00"}
|
{item.gastos_captacao
|
||||||
|
? item.gastos_captacao.toLocaleString("pt-BR", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "BRL",
|
||||||
|
})
|
||||||
|
: "R$ 0,00"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${item.pre_venda
|
||||||
(course as any).preVenda === "sim"
|
|
||||||
? "bg-green-100 text-green-800"
|
? "bg-green-100 text-green-800"
|
||||||
: "bg-gray-100 text-gray-800"
|
: "bg-gray-100 text-gray-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{(course as any).preVenda === "sim" ? "Sim" : "Não"}
|
{item.pre_venda ? "Sim" : "Não"}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEditCourse(course)}
|
|
||||||
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||||
title="Editar turma"
|
title="Editar"
|
||||||
>
|
>
|
||||||
<Edit size={16} />
|
<Edit size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -242,8 +268,9 @@ export const CourseManagement: React.FC = () => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -163,8 +163,83 @@ export async function getInstitutions(): Promise<
|
||||||
/**
|
/**
|
||||||
* Busca os anos de formatura disponíveis
|
* Busca os anos de formatura disponíveis
|
||||||
*/
|
*/
|
||||||
export async function getGraduationYears(): Promise<ApiResponse<number[]>> {
|
export async function getGraduationYears(): Promise<ApiResponse<Array<{ id: string; ano_semestre: string }>>> {
|
||||||
return fetchFromBackend<number[]>("/graduation-years");
|
return fetchFromBackend<Array<{ id: string; ano_semestre: string }>>("/api/anos-formaturas");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca os cursos disponíveis
|
||||||
|
*/
|
||||||
|
export async function getAvailableCourses(): Promise<ApiResponse<Array<{ id: string; nome: string }>>> {
|
||||||
|
return fetchFromBackend<Array<{ id: string; nome: string }>>("/api/cursos");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca a listagem de Cadastro FOT
|
||||||
|
*/
|
||||||
|
export async function getCadastroFot(token: string): Promise<ApiResponse<any[]>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/cadastro-fot`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error: null,
|
||||||
|
isBackendDown: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching cadastro fot:", error);
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
isBackendDown: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria um novo cadastro FOT
|
||||||
|
*/
|
||||||
|
export async function createCadastroFot(data: any, token: string): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/cadastro-fot`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
return {
|
||||||
|
data: responseData,
|
||||||
|
error: null,
|
||||||
|
isBackendDown: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating cadastro fot:", error);
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
isBackendDown: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue