- 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
329 lines
14 KiB
TypeScript
329 lines
14 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useData } from '../contexts/DataContext';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { UserRole, Course } from '../types';
|
|
import { CourseForm } from '../components/CourseForm';
|
|
import { GraduationCap, Plus, Building2, ChevronRight, Edit, Trash2, CheckCircle, XCircle } from 'lucide-react';
|
|
import { Button } from '../components/Button';
|
|
|
|
export const CourseManagement: React.FC = () => {
|
|
const { user } = useAuth();
|
|
const {
|
|
institutions,
|
|
courses,
|
|
addCourse,
|
|
updateCourse,
|
|
getCoursesByInstitutionId
|
|
} = useData();
|
|
|
|
const [selectedInstitution, setSelectedInstitution] = useState<string | null>(null);
|
|
const [showCourseForm, setShowCourseForm] = useState(false);
|
|
const [editingCourse, setEditingCourse] = useState<Course | undefined>(undefined);
|
|
|
|
// Verificar se é admin
|
|
const isAdmin = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER;
|
|
|
|
if (!isAdmin) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 pt-32 pb-12">
|
|
<div className="max-w-7xl mx-auto px-4 text-center">
|
|
<h1 className="text-2xl font-bold text-red-600">Acesso Negado</h1>
|
|
<p className="text-gray-600 mt-2">Apenas administradores podem acessar esta página.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const selectedInstitutionData = institutions.find(inst => inst.id === selectedInstitution);
|
|
const institutionCourses = selectedInstitution ? getCoursesByInstitutionId(selectedInstitution) : [];
|
|
|
|
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: selectedInstitution || courseData.institutionId!,
|
|
year: courseData.year!,
|
|
semester: courseData.semester,
|
|
graduationType: courseData.graduationType!,
|
|
createdAt: new Date().toISOString(),
|
|
createdBy: user?.id || '',
|
|
isActive: courseData.isActive !== false,
|
|
};
|
|
addCourse(newCourse);
|
|
}
|
|
setShowCourseForm(false);
|
|
setEditingCourse(undefined);
|
|
};
|
|
|
|
const handleToggleActive = (course: Course) => {
|
|
updateCourse(course.id, { isActive: !course.isActive });
|
|
};
|
|
|
|
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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
{/* Header */}
|
|
<div className="mb-6 sm:mb-8">
|
|
<div className="flex items-center space-x-3 mb-2">
|
|
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black">
|
|
Gestão de Cursos e Turmas
|
|
</h1>
|
|
</div>
|
|
<p className="text-sm sm:text-base text-gray-600">
|
|
Cadastre e gerencie os cursos/turmas disponíveis em cada universidade
|
|
</p>
|
|
</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">
|
|
<CourseForm
|
|
onCancel={() => {
|
|
setShowCourseForm(false);
|
|
setEditingCourse(undefined);
|
|
}}
|
|
onSubmit={handleSubmitCourse}
|
|
initialData={editingCourse}
|
|
userId={user?.id || ''}
|
|
institutions={institutions}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Left Panel - Institutions Table */}
|
|
<div className="bg-white rounded-lg shadow border border-gray-200">
|
|
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Building2 className="text-brand-gold h-5 w-5" />
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
Universidades Cadastradas
|
|
</h2>
|
|
</div>
|
|
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
|
|
{institutions.length} {institutions.length === 1 ? 'instituição' : 'instituições'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Universidade
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Tipo
|
|
</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Cursos
|
|
</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Ação
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200">
|
|
{institutions.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={4} className="px-4 py-8 text-center text-gray-500">
|
|
Nenhuma universidade cadastrada
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
institutions.map((institution) => {
|
|
const coursesCount = getCoursesByInstitutionId(institution.id).length;
|
|
const isSelected = selectedInstitution === institution.id;
|
|
|
|
return (
|
|
<tr
|
|
key={institution.id}
|
|
className={`hover:bg-gray-50 transition-colors cursor-pointer ${
|
|
isSelected ? 'bg-blue-50 border-l-4 border-brand-gold' : ''
|
|
}`}
|
|
onClick={() => setSelectedInstitution(isSelected ? null : institution.id)}
|
|
>
|
|
<td className="px-4 py-4">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{institution.name}
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">
|
|
{institution.address?.city}, {institution.address?.state}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-4">
|
|
<span className="text-xs font-medium text-gray-600 bg-gray-100 px-2 py-1 rounded">
|
|
{institution.type}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-4 text-center">
|
|
<span className={`inline-flex items-center justify-center w-8 h-8 rounded-full text-sm font-semibold ${
|
|
coursesCount > 0
|
|
? 'bg-green-100 text-green-700'
|
|
: 'bg-gray-100 text-gray-500'
|
|
}`}>
|
|
{coursesCount}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-4 text-center">
|
|
<ChevronRight
|
|
className={`inline-block transition-transform ${
|
|
isSelected ? 'rotate-90 text-brand-gold' : 'text-gray-400'
|
|
}`}
|
|
size={20}
|
|
/>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Panel - Courses Table */}
|
|
<div className="bg-white rounded-lg shadow border border-gray-200">
|
|
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
{selectedInstitutionData ? (
|
|
<>Cursos - {selectedInstitutionData.name}</>
|
|
) : (
|
|
<>Cursos da Universidade</>
|
|
)}
|
|
</h2>
|
|
{selectedInstitutionData && (
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
{institutionCourses.length} {institutionCourses.length === 1 ? 'curso cadastrado' : 'cursos cadastrados'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{selectedInstitution && (
|
|
<Button
|
|
onClick={handleAddCourse}
|
|
variant="secondary"
|
|
className="flex items-center space-x-2"
|
|
>
|
|
<Plus size={16} />
|
|
<span>Cadastrar Turma</span>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
{!selectedInstitution ? (
|
|
<div className="px-6 py-12 text-center">
|
|
<Building2 className="mx-auto h-12 w-12 text-gray-300 mb-3" />
|
|
<p className="text-gray-500 text-sm">
|
|
Selecione uma universidade para ver seus cursos
|
|
</p>
|
|
</div>
|
|
) : institutionCourses.length === 0 ? (
|
|
<div className="px-6 py-12 text-center">
|
|
<GraduationCap className="mx-auto h-12 w-12 text-gray-300 mb-3" />
|
|
<p className="text-gray-500 text-sm mb-4">
|
|
Nenhum curso cadastrado nesta universidade
|
|
</p>
|
|
<Button onClick={handleAddCourse} variant="secondary">
|
|
<Plus size={16} className="mr-2" />
|
|
Cadastrar Primeiro Curso
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Curso/Turma
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Tipo
|
|
</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Período
|
|
</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Ações
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200">
|
|
{institutionCourses.map((course) => (
|
|
<tr key={course.id} className="hover:bg-gray-50">
|
|
<td className="px-4 py-4">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{course.name}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-4">
|
|
<span className="text-xs font-medium text-gray-600 bg-gray-100 px-2 py-1 rounded">
|
|
{course.graduationType}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-4 text-center">
|
|
<span className="text-xs text-gray-600">
|
|
{course.year}/{course.semester}º
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-4 text-center">
|
|
<button
|
|
onClick={() => handleToggleActive(course)}
|
|
className="inline-flex items-center space-x-1"
|
|
>
|
|
{course.isActive ? (
|
|
<>
|
|
<CheckCircle size={16} className="text-green-500" />
|
|
<span className="text-xs text-green-700 font-medium">Ativo</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<XCircle size={16} className="text-gray-400" />
|
|
<span className="text-xs text-gray-500 font-medium">Inativo</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</td>
|
|
<td className="px-4 py-4">
|
|
<div className="flex items-center justify-center space-x-2">
|
|
<button
|
|
onClick={() => handleEditCourse(course)}
|
|
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
|
title="Editar curso"
|
|
>
|
|
<Edit size={16} />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|