- Tradução de rotas para português (entrar, cadastro, configuracoes, etc) - Ajuste de responsividade na página Financeiro (mobile) - Correção navegação Configurações para usuário CEO/Business Owner - Modal de gerenciamento de equipe com lista de profissionais - Exibição de fotógrafos, cinegrafistas e recepcionistas disponíveis por data - Ajuste de layout da logo nas telas de login e cadastro - Correção de z-index do header - Melhoria de espaçamento e padding em cards
282 lines
9.3 KiB
TypeScript
282 lines
9.3 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 } from "../services/apiService";
|
|
|
|
interface CourseFormProps {
|
|
onCancel: () => void;
|
|
onSubmit: (data: Partial<Course>) => void;
|
|
initialData?: Course;
|
|
userId: string;
|
|
institutions: Institution[];
|
|
}
|
|
|
|
const GRADUATION_TYPES = [
|
|
"Bacharelado",
|
|
"Licenciatura",
|
|
"Tecnológico",
|
|
"Especialização",
|
|
"Mestrado",
|
|
"Doutorado",
|
|
];
|
|
|
|
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,
|
|
}
|
|
);
|
|
|
|
const [showToast, setShowToast] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [backendInstitutions, setBackendInstitutions] = useState<Array<{ id: string; name: 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 [institutionsResponse, yearsResponse] = await Promise.all([
|
|
getInstitutions(),
|
|
getGraduationYears()
|
|
]);
|
|
|
|
if (institutionsResponse.isBackendDown || yearsResponse.isBackendDown) {
|
|
setIsBackendDown(true);
|
|
} else {
|
|
setIsBackendDown(false);
|
|
if (institutionsResponse.data) {
|
|
setBackendInstitutions(institutionsResponse.data);
|
|
}
|
|
if (yearsResponse.data) {
|
|
setGraduationYears(yearsResponse.data);
|
|
}
|
|
}
|
|
|
|
setIsLoadingData(false);
|
|
};
|
|
|
|
fetchBackendData();
|
|
}, []);
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Validações
|
|
if (!formData.name || formData.name.trim().length < 3) {
|
|
setError("Nome do curso deve ter pelo menos 3 caracteres");
|
|
return;
|
|
}
|
|
|
|
if (!formData.institutionId) {
|
|
setError("Selecione uma universidade");
|
|
return;
|
|
}
|
|
|
|
if (!formData.graduationType) {
|
|
setError("Selecione o tipo de graduação");
|
|
return;
|
|
}
|
|
|
|
setShowToast(true);
|
|
setTimeout(() => {
|
|
onSubmit(formData);
|
|
}, 1000);
|
|
};
|
|
|
|
const handleChange = (field: keyof Course, value: any) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
setError(""); // Limpa erro ao editar
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-2xl 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 Curso/Turma" : "Cadastrar Curso/Turma"}
|
|
</h2>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Registre as turmas disponíveis para eventos fotográficos
|
|
</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 do Curso */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
|
|
Informações do Curso
|
|
</h3>
|
|
|
|
<Select
|
|
label="Universidade*"
|
|
options={institutions.map((inst) => ({
|
|
value: inst.id,
|
|
label: `${inst.name} - ${inst.type}`,
|
|
}))}
|
|
value={formData.institutionId || ""}
|
|
onChange={(e) => handleChange("institutionId", e.target.value)}
|
|
required
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Nome do Curso/Turma*
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.name || ""}
|
|
onChange={(e) => handleChange("name", e.target.value)}
|
|
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 um curso</option>
|
|
{backendInstitutions.map((inst) => (
|
|
<option key={inst.id} value={inst.name}>
|
|
{inst.name}
|
|
</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 os cursos.</span>
|
|
</div>
|
|
)}
|
|
{isLoadingData && !isBackendDown && (
|
|
<div className="mt-2 text-sm text-gray-500">
|
|
Carregando cursos...
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Ano*
|
|
</label>
|
|
<select
|
|
required
|
|
value={formData.year || currentYear}
|
|
onChange={(e) => handleChange("year", parseInt(e.target.value))}
|
|
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>
|
|
|
|
<Select
|
|
label="Semestre"
|
|
options={[
|
|
{ value: "1", label: "1º Semestre" },
|
|
{ value: "2", label: "2º Semestre" },
|
|
]}
|
|
value={formData.semester?.toString() || "1"}
|
|
onChange={(e) =>
|
|
handleChange("semester", parseInt(e.target.value))
|
|
}
|
|
/>
|
|
|
|
<Select
|
|
label="Tipo*"
|
|
options={GRADUATION_TYPES.map((t) => ({ value: t, label: t }))}
|
|
value={formData.graduationType || ""}
|
|
onChange={(e) => handleChange("graduationType", e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Status Ativo/Inativo */}
|
|
<div className="flex items-center space-x-3 pt-2">
|
|
<input
|
|
type="checkbox"
|
|
id="isActive"
|
|
checked={formData.isActive !== false}
|
|
onChange={(e) => handleChange("isActive", e.target.checked)}
|
|
className="w-4 h-4 text-brand-gold border-gray-300 rounded focus:ring-brand-gold"
|
|
/>
|
|
<label htmlFor="isActive" className="text-sm text-gray-700">
|
|
Curso ativo (disponível para seleção em eventos)
|
|
</label>
|
|
</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 Curso"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|