From fd734a5c26665fc1f72d8fe57fef0322627362a2 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Fri, 20 Dec 2024 10:41:02 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20implementa=20dashboard=20com=20estat?= =?UTF-8?q?=C3=ADsticas=20em=20tempo=20real?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/components/auth/LoginForm.tsx | 170 ++++++++++------ src/pages/dashboard/DashboardHome.tsx | 210 ++++++++++++++------ src/pages/dashboard/classes/ClassesPage.tsx | 140 +++++++------ 3 files changed, 325 insertions(+), 195 deletions(-) diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index 0edc6f9..a628857 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -1,5 +1,5 @@ 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 { useNavigate } from 'react-router-dom'; @@ -9,16 +9,31 @@ interface LoginFormProps { onRegisterClick?: () => void; } +const userTypeIcons = { + school: , + teacher: , + student: +}; + +const userTypeLabels = { + school: 'Escola', + teacher: 'Professor', + student: 'Aluno' +}; + export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); const { signIn } = useAuth(); const navigate = useNavigate(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); + setLoading(true); try { const { user } = await signIn(email, password); @@ -31,77 +46,106 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps } } catch (err) { setError('Erro ao fazer login. Verifique suas credenciais.'); + } finally { + setLoading(false); } }; return ( -
-
-

- Bem-vindo de volta! -

-

- Continue sua jornada de histórias mágicas -

-
- - {error && ( -
- {error} -
- )} - -
-
-
- - 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" - /> -
- -
- - 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" - /> +
+
+
+
+ {userTypeIcons[userType]}
+

+ Bem-vindo de volta! +

+

+ Faça login como {userTypeLabels[userType]} +

- - - {onRegisterClick && ( -
- + {error && ( +
+ {error}
)} - + +
+
+
+ + 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" + /> +
+ +
+ +
+ 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" + /> + +
+
+ + +
+ + {onRegisterClick && ( +
+

+ Ainda não tem uma conta?{' '} + +

+
+ )} +
+
); } \ No newline at end of file diff --git a/src/pages/dashboard/DashboardHome.tsx b/src/pages/dashboard/DashboardHome.tsx index 1115fdf..19f031c 100644 --- a/src/pages/dashboard/DashboardHome.tsx +++ b/src/pages/dashboard/DashboardHome.tsx @@ -1,87 +1,177 @@ -import React from 'react'; +import React, { useEffect, useState } from '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() { - const { loading, error } = useDatabase(); - const [stats, setStats] = React.useState({ + const [stats, setStats] = useState({ totalClasses: 0, totalTeachers: 0, totalStudents: 0, - totalStories: 0 + recentClasses: [], + recentStories: [] }); + const [loading, setLoading] = useState(true); - React.useEffect(() => { - // Implementar busca de estatísticas + useEffect(() => { + 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 ( -
-

Dashboard

+
+

Dashboard

- {error && ( -
- {error} -
- )} - -
-
-
-
- -
-
-

Total de Turmas

-

- {loading ? '...' : stats.totalClasses} -

-
+
+
+
+ +
+
+

Total de Turmas

+

{stats.totalClasses}

-
-
-
- -
-
-

Total de Professores

-

- {loading ? '...' : stats.totalTeachers} -

-
+
+
+ +
+
+

Total de Professores

+

{stats.totalTeachers}

-
-
-
- -
-
-

Total de Alunos

-

- {loading ? '...' : stats.totalStudents} -

-
+
+
+ +
+
+

Total de Alunos

+

{stats.totalStudents}

-
-
-

- Últimas Turmas -

- {/* Lista de últimas turmas */} +
+
+

Últimas Turmas

+ {loading ? ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ) : stats.recentClasses.length === 0 ? ( +

Nenhuma turma cadastrada

+ ) : ( +
+ {stats.recentClasses.map((classItem) => ( +
+
+

{classItem.name}

+

+ {classItem.grade} • {classItem.students.count} alunos +

+
+
+ ))} +
+ )}
-
-

- Histórias Recentes -

- {/* Lista de histórias recentes */} +
+

Histórias Recentes

+ {loading ? ( +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ ) : stats.recentStories.length === 0 ? ( +

Nenhuma história registrada

+ ) : ( +
+ {stats.recentStories.map((story) => ( +
+
+

{story.title}

+

+ por {story.students.name} +

+
+
+ ))} +
+ )}
diff --git a/src/pages/dashboard/classes/ClassesPage.tsx b/src/pages/dashboard/classes/ClassesPage.tsx index 30af112..9d6e127 100644 --- a/src/pages/dashboard/classes/ClassesPage.tsx +++ b/src/pages/dashboard/classes/ClassesPage.tsx @@ -1,113 +1,109 @@ 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 { useDatabase } from '../../../hooks/useDatabase'; - -interface Class { - id: string; - name: string; - grade: string; - year: number; - teacher_count: number; - student_count: number; -} +import { supabase } from '../../../lib/supabase'; +import type { Class } from '../../../types/database'; export function ClassesPage() { const navigate = useNavigate(); - const { loading, error } = useDatabase(); const [classes, setClasses] = React.useState([]); const [searchTerm, setSearchTerm] = React.useState(''); + const [loading, setLoading] = React.useState(true); 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 = () => { - navigate('/dashboard/turmas/nova'); - }; - - 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()) + const filteredClasses = classes.filter(classItem => + classItem.name.toLowerCase().includes(searchTerm.toLowerCase()) || + classItem.grade.toLowerCase().includes(searchTerm.toLowerCase()) ); return ( -
-
-

Turmas

+
+
+

Turmas

- {error && ( -
- {error} -
- )} - -
-
-
- - 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" - /> -
+
+
+ + setSearchTerm(e.target.value)} + 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" + />
{loading ? ( -
Carregando...
+
+
+
) : filteredClasses.length === 0 ? ( -
- Nenhuma turma encontrada +
+ {searchTerm ? 'Nenhuma turma encontrada' : 'Nenhuma turma cadastrada'}
) : ( -
+
{filteredClasses.map((classItem) => (
handleClassClick(classItem.id)} + className="flex items-center justify-between py-4 hover:bg-gray-50 transition-colors" > -
+
+
+ +

{classItem.name}

- {classItem.grade} - {classItem.year} + {classItem.grade} • {(classItem as any).students?.count || 0} alunos

-
-
- - {classItem.teacher_count} - {' '} - professores -
-
- - {classItem.student_count} - {' '} - alunos -
- -
+
+ +
+ + Ativo + +
))}