From 3096f07102f0e99a5fff54049e2c08ff7f56e255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Vitor?= Date: Mon, 8 Dec 2025 02:53:00 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20adicionar=20sistema=20completo=20de=20g?= =?UTF-8?q?est=C3=A3o=20de=20cursos=20e=20turmas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adicionada interface Course em types.ts - Criado CourseForm para cadastro/edição de turmas - Implementada página CourseManagement com tabelas Excel-like - Adicionadas funções CRUD de cursos no DataContext - Integrado dropdown de cursos no EventForm baseado na instituição - Adicionada rota 'courses' no App.tsx - Link 'Gestão de Cursos' inserido no menu principal após 'Equipe & Fotógrafos' - Removido 'Configurações' do menu principal (mantido apenas no dropdown do avatar) - Implementado comportamento de toggle para seleção de universidades - Sistema restrito a SUPERADMIN e BUSINESS_OWNER --- backend/package-lock.json | 6 + frontend/App.tsx | 4 + frontend/components/CourseForm.tsx | 205 +++++++++++++++++ frontend/components/EventForm.tsx | 79 ++++++- frontend/components/Navbar.tsx | 4 +- frontend/contexts/DataContext.tsx | 76 ++++++- frontend/pages/CourseManagement.tsx | 329 ++++++++++++++++++++++++++++ frontend/pages/Settings.tsx | 8 +- frontend/types.ts | 14 +- 9 files changed, 708 insertions(+), 17 deletions(-) create mode 100644 backend/package-lock.json create mode 100644 frontend/components/CourseForm.tsx create mode 100644 frontend/pages/CourseManagement.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..dfb18f1 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/frontend/App.tsx b/frontend/App.tsx index cd94801..0cc417c 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -7,6 +7,7 @@ import { Register } from "./pages/Register"; import { TeamPage } from "./pages/Team"; import { FinancePage } from "./pages/Finance"; import { SettingsPage } from "./pages/Settings"; +import { CourseManagement } from "./pages/CourseManagement"; import { InspirationPage } from "./pages/Inspiration"; import { PrivacyPolicy } from "./pages/PrivacyPolicy"; import { TermsOfUse } from "./pages/TermsOfUse"; @@ -62,6 +63,9 @@ const AppContent: React.FC = () => { case "settings": return ; + case "courses": + return ; + default: return ; } diff --git a/frontend/components/CourseForm.tsx b/frontend/components/CourseForm.tsx new file mode 100644 index 0000000..32d57b4 --- /dev/null +++ b/frontend/components/CourseForm.tsx @@ -0,0 +1,205 @@ +import React, { useState } from 'react'; +import { Course, Institution } from '../types'; +import { Input, Select } from './Input'; +import { Button } from './Button'; +import { GraduationCap, X, Check, AlertCircle } from 'lucide-react'; + +interface CourseFormProps { + onCancel: () => void; + onSubmit: (data: Partial) => void; + initialData?: Course; + userId: string; + institutions: Institution[]; +} + +const GRADUATION_TYPES = [ + 'Bacharelado', + 'Licenciatura', + 'Tecnológico', + 'Especialização', + 'Mestrado', + 'Doutorado' +]; + +export const CourseForm: React.FC = ({ + onCancel, + onSubmit, + initialData, + userId, + institutions +}) => { + const currentYear = new Date().getFullYear(); + const [formData, setFormData] = useState>(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 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 ( +
+ + {/* Success Toast */} + {showToast && ( +
+ +
+

Sucesso!

+

Curso cadastrado com sucesso.

+
+
+ )} + + {/* Form Header */} +
+
+ +
+

+ {initialData ? 'Editar Curso/Turma' : 'Cadastrar Curso/Turma'} +

+

+ Registre as turmas disponíveis para eventos fotográficos +

+
+
+ +
+ +
+ + {/* Erro global */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Informações do Curso */} +
+

+ Informações do Curso +

+ + handleChange('name', e.target.value)} + required + /> + +
+ handleChange('year', parseInt(e.target.value))} + min={currentYear - 1} + max={currentYear + 5} + required + /> + + ({ value: t, label: t }))} + value={formData.graduationType || ''} + onChange={(e) => handleChange('graduationType', e.target.value)} + required + /> +
+ + {/* Status Ativo/Inativo */} +
+ handleChange('isActive', e.target.checked)} + className="w-4 h-4 text-brand-gold border-gray-300 rounded focus:ring-brand-gold" + /> + +
+
+ + + {/* Actions */} +
+ + +
+
+
+ ); +}; diff --git a/frontend/components/EventForm.tsx b/frontend/components/EventForm.tsx index bd42d09..b3e6bb1 100644 --- a/frontend/components/EventForm.tsx +++ b/frontend/components/EventForm.tsx @@ -38,7 +38,12 @@ export const EventForm: React.FC = ({ initialData, }) => { const { user } = useAuth(); - const { institutions, getInstitutionsByUserId, addInstitution } = useData(); + const { + institutions, + getInstitutionsByUserId, + addInstitution, + getActiveCoursesByInstitutionId + } = useData(); const [activeTab, setActiveTab] = useState< "details" | "location" | "briefing" | "files" >("details"); @@ -48,6 +53,7 @@ export const EventForm: React.FC = ({ const [isGeocoding, setIsGeocoding] = useState(false); const [showToast, setShowToast] = useState(false); const [showInstitutionForm, setShowInstitutionForm] = useState(false); + const [availableCourses, setAvailableCourses] = useState([]); // Get institutions based on user role // Business owners and admins see all institutions, clients see only their own @@ -84,7 +90,7 @@ export const EventForm: React.FC = ({ "https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80", // Default institutionId: "", attendees: "", - course: "", + courseId: "", } ); @@ -100,6 +106,20 @@ export const EventForm: React.FC = ({ ? "Enviar Solicitação" : "Criar Evento"; + // Carregar cursos disponíveis quando instituição for selecionada + useEffect(() => { + if (formData.institutionId) { + const courses = getActiveCoursesByInstitutionId(formData.institutionId); + setAvailableCourses(courses); + } else { + setAvailableCourses([]); + // Limpa o curso selecionado se a instituição mudar + if (formData.courseId) { + setFormData((prev: any) => ({ ...prev, courseId: "" })); + } + } + }, [formData.institutionId, getActiveCoursesByInstitutionId]); + // Address Autocomplete Logic using Mapbox useEffect(() => { const timer = setTimeout(async () => { @@ -414,15 +434,6 @@ export const EventForm: React.FC = ({ } /> - - setFormData({ ...formData, course: e.target.value }) - } - /> - = ({ )} + {/* Course Selection - Condicional baseado na instituição */} + {formData.institutionId && ( +
+ + + {availableCourses.length === 0 ? ( +
+
+ +
+

+ Nenhum curso cadastrado +

+

+ Entre em contato com a administração para cadastrar os cursos/turmas disponíveis nesta universidade. +

+
+
+
+ ) : ( + + )} +
+ )} + {/* Cover Image Upload */}