mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
feat: implementa dashboard com estatísticas em tempo real
- Adiciona busca de totais de turmas, professores e alunos - Implementa listagem de turmas recentes com contagem de alunos - Adiciona seção de histórias recentes com nome dos alunos - Melhora feedback visual com estados de loading - Usa queries otimizadas do Supabase com contagem e joins
This commit is contained in:
parent
70953ab57a
commit
fd734a5c26
@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { LogIn } from 'lucide-react';
|
import { LogIn, Eye, EyeOff, School, GraduationCap, User } from 'lucide-react';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
@ -9,16 +9,31 @@ interface LoginFormProps {
|
|||||||
onRegisterClick?: () => void;
|
onRegisterClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userTypeIcons = {
|
||||||
|
school: <School className="h-8 w-8 text-purple-600" />,
|
||||||
|
teacher: <GraduationCap className="h-8 w-8 text-purple-600" />,
|
||||||
|
student: <User className="h-8 w-8 text-purple-600" />
|
||||||
|
};
|
||||||
|
|
||||||
|
const userTypeLabels = {
|
||||||
|
school: 'Escola',
|
||||||
|
teacher: 'Professor',
|
||||||
|
student: 'Aluno'
|
||||||
|
};
|
||||||
|
|
||||||
export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps) {
|
export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps) {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const { signIn } = useAuth();
|
const { signIn } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { user } = await signIn(email, password);
|
const { user } = await signIn(email, password);
|
||||||
@ -31,77 +46,106 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Erro ao fazer login. Verifique suas credenciais.');
|
setError('Erro ao fazer login. Verifique suas credenciais.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md w-full space-y-8">
|
<div className="min-h-screen bg-gradient-to-b from-purple-50 to-white py-12">
|
||||||
<div>
|
<div className="max-w-md mx-auto px-4">
|
||||||
<h2 className="text-3xl font-bold text-center text-purple-600">
|
<div className="text-center mb-8">
|
||||||
Bem-vindo de volta!
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-purple-100 mb-4">
|
||||||
</h2>
|
{userTypeIcons[userType]}
|
||||||
<p className="mt-2 text-center text-gray-600">
|
|
||||||
Continue sua jornada de histórias mágicas
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
|
||||||
Senha
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
Bem-vindo de volta!
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Faça login como {userTypeLabels[userType]}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
{error && (
|
||||||
type="submit"
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-600">
|
||||||
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent rounded-lg shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
|
{error}
|
||||||
>
|
|
||||||
<LogIn className="w-5 h-5" />
|
|
||||||
Entrar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{onRegisterClick && (
|
|
||||||
<div className="text-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRegisterClick}
|
|
||||||
className="text-purple-600 hover:text-purple-500"
|
|
||||||
>
|
|
||||||
Criar uma nova conta
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
|
||||||
|
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||||
|
Senha
|
||||||
|
</label>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500 pr-10"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-5 w-5 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-5 w-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent rounded-lg shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
'Entrando...'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LogIn className="h-5 w-5" />
|
||||||
|
Entrar
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{onRegisterClick && (
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Ainda não tem uma conta?{' '}
|
||||||
|
<button
|
||||||
|
onClick={onRegisterClick}
|
||||||
|
className="text-purple-600 hover:text-purple-500 font-medium"
|
||||||
|
>
|
||||||
|
Cadastre-se
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,87 +1,177 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Users, GraduationCap, BookOpen } from 'lucide-react';
|
import { Users, GraduationCap, BookOpen } from 'lucide-react';
|
||||||
import { useDatabase } from '../../hooks/useDatabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
|
|
||||||
|
interface DashboardStats {
|
||||||
|
totalClasses: number;
|
||||||
|
totalTeachers: number;
|
||||||
|
totalStudents: number;
|
||||||
|
recentClasses: any[];
|
||||||
|
recentStories: any[];
|
||||||
|
}
|
||||||
|
|
||||||
export function DashboardHome() {
|
export function DashboardHome() {
|
||||||
const { loading, error } = useDatabase();
|
const [stats, setStats] = useState<DashboardStats>({
|
||||||
const [stats, setStats] = React.useState({
|
|
||||||
totalClasses: 0,
|
totalClasses: 0,
|
||||||
totalTeachers: 0,
|
totalTeachers: 0,
|
||||||
totalStudents: 0,
|
totalStudents: 0,
|
||||||
totalStories: 0
|
recentClasses: [],
|
||||||
|
recentStories: []
|
||||||
});
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
// Implementar busca de estatísticas
|
const fetchDashboardStats = async () => {
|
||||||
|
try {
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
if (!session?.user?.id) return;
|
||||||
|
|
||||||
|
const schoolId = session.user.id;
|
||||||
|
|
||||||
|
// Buscar total de turmas
|
||||||
|
const { count: classesCount } = await supabase
|
||||||
|
.from('classes')
|
||||||
|
.select('*', { count: 'exact', head: true })
|
||||||
|
.eq('school_id', schoolId);
|
||||||
|
|
||||||
|
// Buscar total de professores
|
||||||
|
const { count: teachersCount } = await supabase
|
||||||
|
.from('teacher_classes')
|
||||||
|
.select('teacher_id', { count: 'exact', head: true })
|
||||||
|
.eq('school_id', schoolId);
|
||||||
|
|
||||||
|
// Buscar total de alunos
|
||||||
|
const { count: studentsCount } = await supabase
|
||||||
|
.from('students')
|
||||||
|
.select('*', { count: 'exact', head: true })
|
||||||
|
.eq('school_id', schoolId);
|
||||||
|
|
||||||
|
// Buscar turmas recentes
|
||||||
|
const { data: recentClasses } = await supabase
|
||||||
|
.from('classes')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
students:students(count)
|
||||||
|
`)
|
||||||
|
.eq('school_id', schoolId)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(5);
|
||||||
|
|
||||||
|
// Buscar histórias recentes
|
||||||
|
const { data: recentStories } = await supabase
|
||||||
|
.from('stories')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
students:students(name)
|
||||||
|
`)
|
||||||
|
.eq('school_id', schoolId)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(5);
|
||||||
|
|
||||||
|
setStats({
|
||||||
|
totalClasses: classesCount || 0,
|
||||||
|
totalTeachers: teachersCount || 0,
|
||||||
|
totalStudents: studentsCount || 0,
|
||||||
|
recentClasses: recentClasses || [],
|
||||||
|
recentStories: recentStories || []
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar estatísticas:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDashboardStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="p-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
<h1 className="text-2xl font-bold mb-8">Dashboard</h1>
|
||||||
|
|
||||||
{error && (
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-8">
|
||||||
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
|
<div className="bg-white rounded-2xl p-6 flex items-center gap-4">
|
||||||
{error}
|
<div className="w-12 h-12 rounded-xl bg-purple-100 flex items-center justify-center">
|
||||||
</div>
|
<Users className="h-6 w-6 text-purple-600" />
|
||||||
)}
|
</div>
|
||||||
|
<div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
<p className="text-gray-600">Total de Turmas</p>
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
<p className="text-3xl font-bold">{stats.totalClasses}</p>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="p-3 bg-purple-100 rounded-lg">
|
|
||||||
<Users className="h-6 w-6 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Total de Turmas</p>
|
|
||||||
<p className="text-2xl font-semibold text-gray-900">
|
|
||||||
{loading ? '...' : stats.totalClasses}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
<div className="bg-white rounded-2xl p-6 flex items-center gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center">
|
||||||
<div className="p-3 bg-blue-100 rounded-lg">
|
<GraduationCap className="h-6 w-6 text-blue-600" />
|
||||||
<GraduationCap className="h-6 w-6 text-blue-600" />
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<p className="text-gray-600">Total de Professores</p>
|
||||||
<p className="text-sm text-gray-600">Total de Professores</p>
|
<p className="text-3xl font-bold">{stats.totalTeachers}</p>
|
||||||
<p className="text-2xl font-semibold text-gray-900">
|
|
||||||
{loading ? '...' : stats.totalTeachers}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
<div className="bg-white rounded-2xl p-6 flex items-center gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
|
||||||
<div className="p-3 bg-green-100 rounded-lg">
|
<BookOpen className="h-6 w-6 text-green-600" />
|
||||||
<Users className="h-6 w-6 text-green-600" />
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<p className="text-gray-600">Total de Alunos</p>
|
||||||
<p className="text-sm text-gray-600">Total de Alunos</p>
|
<p className="text-3xl font-bold">{stats.totalStudents}</p>
|
||||||
<p className="text-2xl font-semibold text-gray-900">
|
|
||||||
{loading ? '...' : stats.totalStudents}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
<div className="bg-white rounded-2xl p-6">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
<h2 className="text-xl font-semibold mb-4">Últimas Turmas</h2>
|
||||||
Últimas Turmas
|
{loading ? (
|
||||||
</h2>
|
<div className="animate-pulse space-y-4">
|
||||||
{/* Lista de últimas turmas */}
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div key={i} className="h-12 bg-gray-100 rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : stats.recentClasses.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-4">Nenhuma turma cadastrada</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{stats.recentClasses.map((classItem) => (
|
||||||
|
<div key={classItem.id} className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">{classItem.name}</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{classItem.grade} • {classItem.students.count} alunos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
<div className="bg-white rounded-2xl p-6">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
<h2 className="text-xl font-semibold mb-4">Histórias Recentes</h2>
|
||||||
Histórias Recentes
|
{loading ? (
|
||||||
</h2>
|
<div className="animate-pulse space-y-4">
|
||||||
{/* Lista de histórias recentes */}
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div key={i} className="h-12 bg-gray-100 rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : stats.recentStories.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-4">Nenhuma história registrada</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{stats.recentStories.map((story) => (
|
||||||
|
<div key={story.id} className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">{story.title}</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
por {story.students.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,113 +1,109 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Plus, Search, MoreVertical } from 'lucide-react';
|
import { Plus, Search, MoreVertical, GraduationCap } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useDatabase } from '../../../hooks/useDatabase';
|
import { useDatabase } from '../../../hooks/useDatabase';
|
||||||
|
import { supabase } from '../../../lib/supabase';
|
||||||
interface Class {
|
import type { Class } from '../../../types/database';
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
grade: string;
|
|
||||||
year: number;
|
|
||||||
teacher_count: number;
|
|
||||||
student_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ClassesPage() {
|
export function ClassesPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { loading, error } = useDatabase();
|
|
||||||
const [classes, setClasses] = React.useState<Class[]>([]);
|
const [classes, setClasses] = React.useState<Class[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = React.useState('');
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Implementar busca de turmas
|
const fetchClasses = async () => {
|
||||||
|
try {
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
if (!session?.user?.id) return;
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('classes')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
students:students(count),
|
||||||
|
teachers:teacher_classes(count)
|
||||||
|
`)
|
||||||
|
.eq('school_id', session.user.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setClasses(data || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao buscar turmas:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchClasses();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateClass = () => {
|
const filteredClasses = classes.filter(classItem =>
|
||||||
navigate('/dashboard/turmas/nova');
|
classItem.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
};
|
classItem.grade.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
|
||||||
const handleClassClick = (classId: string) => {
|
|
||||||
navigate(`/dashboard/turmas/${classId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredClasses = classes.filter(c =>
|
|
||||||
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
c.grade.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Turmas</h1>
|
<h1 className="text-2xl font-bold">Turmas</h1>
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateClass}
|
onClick={() => navigate('nova')}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
|
className="inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-xl hover:bg-purple-700 font-medium text-lg"
|
||||||
>
|
>
|
||||||
<Plus className="h-5 w-5" />
|
<Plus className="h-6 w-6" />
|
||||||
Nova Turma
|
Nova Turma
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
<div className="bg-white rounded-2xl shadow-sm p-6">
|
||||||
<div className="mb-4 p-4 bg-red-50 text-red-600 rounded-lg">
|
<div className="relative mb-6">
|
||||||
{error}
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
</div>
|
<input
|
||||||
)}
|
type="text"
|
||||||
|
placeholder="Buscar turmas..."
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
value={searchTerm}
|
||||||
<div className="p-4 border-b border-gray-200">
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
<div className="relative">
|
className="w-full pl-12 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:ring-purple-500 focus:border-purple-500"
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
/>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Buscar turmas..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-8 text-center text-gray-500">Carregando...</div>
|
<div className="text-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto"></div>
|
||||||
|
</div>
|
||||||
) : filteredClasses.length === 0 ? (
|
) : filteredClasses.length === 0 ? (
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="text-center py-12 text-gray-500">
|
||||||
Nenhuma turma encontrada
|
{searchTerm ? 'Nenhuma turma encontrada' : 'Nenhuma turma cadastrada'}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-100">
|
||||||
{filteredClasses.map((classItem) => (
|
{filteredClasses.map((classItem) => (
|
||||||
<div
|
<div
|
||||||
key={classItem.id}
|
key={classItem.id}
|
||||||
className="p-4 hover:bg-gray-50 cursor-pointer"
|
className="flex items-center justify-between py-4 hover:bg-gray-50 transition-colors"
|
||||||
onClick={() => handleClassClick(classItem.id)}
|
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center">
|
||||||
|
<GraduationCap className="h-5 w-5 text-purple-600" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-gray-900">
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
{classItem.name}
|
{classItem.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{classItem.grade} - {classItem.year}
|
{classItem.grade} • {(classItem as any).students?.count || 0} alunos
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6">
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
<span className="font-medium text-gray-900">
|
<div className="flex items-center gap-4">
|
||||||
{classItem.teacher_count}
|
<span className="px-3 py-1 text-sm bg-green-50 text-green-700 rounded-full">
|
||||||
</span>{' '}
|
Ativo
|
||||||
professores
|
</span>
|
||||||
</div>
|
<button className="p-2 hover:bg-gray-100 rounded-full">
|
||||||
<div className="text-sm text-gray-500">
|
<MoreVertical className="h-5 w-5 text-gray-400" />
|
||||||
<span className="font-medium text-gray-900">
|
</button>
|
||||||
{classItem.student_count}
|
|
||||||
</span>{' '}
|
|
||||||
alunos
|
|
||||||
</div>
|
|
||||||
<button className="p-2 hover:bg-gray-100 rounded-full">
|
|
||||||
<MoreVertical className="h-5 w-5 text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user