Prechádzať zdrojové kódy

lmao i forgot about this component

Matthew Trejo 3 mesiacov pred
rodič
commit
7a5cb96ae3

+ 287 - 0
src/app/admin/actions.ts

@@ -0,0 +1,287 @@
+'use server'
+
+import { getServerSession } from 'next-auth'
+import { authOptions } from '@/lib/auth'
+import { prisma } from '@/lib/prisma'
+import { redirect } from 'next/navigation'
+
+export interface DashboardStats {
+  totalUsers: number
+  totalStudents: number
+  totalTeachers: number
+  totalClasses: number
+  totalSections: number
+  activePeriods: number
+  recentEnrollments: number
+  recentAttendance: number
+}
+
+export interface RecentActivity {
+  id: string
+  action: string
+  user: string
+  time: string
+  type: 'user' | 'class' | 'assignment' | 'enrollment'
+}
+
+export interface CurrentPeriod {
+  id: string
+  name: string
+  startDate: Date
+  endDate: Date
+  isActive: boolean
+  totalClasses: number
+  totalPartials: number
+}
+
+export async function getDashboardStats(): Promise<DashboardStats> {
+  const session = await getServerSession(authOptions)
+  
+  if (!session || session.user.role !== 'ADMIN') {
+    redirect('/login')
+  }
+
+  try {
+    const [totalUsers, totalStudents, totalTeachers, totalClasses, totalSections, activePeriods] = await Promise.all([
+      prisma.user.count(),
+      prisma.student.count({ where: { isActive: true } }),
+      prisma.teacher.count({ where: { isActive: true } }),
+      prisma.class.count({ where: { isActive: true } }),
+      prisma.section.count({ where: { isActive: true } }),
+      prisma.period.count({ where: { isActive: true } })
+    ])
+
+    const recentEnrollments = await prisma.studentEnrollment.count({
+      where: {
+        createdAt: {
+          gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // Últimos 30 días
+        }
+      }
+    })
+
+    const recentAttendance = await prisma.attendance.count({
+      where: {
+        createdAt: {
+          gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Últimos 7 días
+        }
+      }
+    })
+
+    return {
+      totalUsers,
+      totalStudents,
+      totalTeachers,
+      totalClasses,
+      totalSections,
+      activePeriods,
+      recentEnrollments,
+      recentAttendance
+    }
+  } catch (error) {
+    console.error('Error fetching dashboard stats:', error)
+    throw new Error('Error al obtener estadísticas del dashboard')
+  }
+}
+
+export async function getRecentActivities(): Promise<RecentActivity[]> {
+  const session = await getServerSession(authOptions)
+  
+  if (!session || session.user.role !== 'ADMIN') {
+    redirect('/login')
+  }
+
+  try {
+    // Obtener actividades recientes de diferentes fuentes
+    const [recentUsers, recentEnrollments, recentAssignments] = await Promise.all([
+      // Usuarios recientes
+      prisma.user.findMany({
+        where: {
+          createdAt: {
+            gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Últimos 7 días
+          }
+        },
+        include: {
+          student: true,
+          teacher: true
+        },
+        orderBy: { createdAt: 'desc' },
+        take: 5
+      }),
+      // Inscripciones recientes
+      prisma.studentEnrollment.findMany({
+        where: {
+          createdAt: {
+            gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
+          }
+        },
+        include: {
+          student: true,
+          section: {
+            include: {
+              class: true
+            }
+          }
+        },
+        orderBy: { createdAt: 'desc' },
+        take: 5
+      }),
+      // Asignaciones recientes de profesores
+      prisma.teacherAssignment.findMany({
+        where: {
+          createdAt: {
+            gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
+          }
+        },
+        include: {
+          teacher: true,
+          section: {
+            include: {
+              class: true
+            }
+          }
+        },
+        orderBy: { createdAt: 'desc' },
+        take: 5
+      })
+    ])
+
+    const activities: RecentActivity[] = []
+
+    // Procesar usuarios recientes
+    recentUsers.forEach(user => {
+      const userName = user.student 
+        ? `${user.student.firstName} ${user.student.lastName}`
+        : user.teacher 
+        ? `${user.teacher.firstName} ${user.teacher.lastName}`
+        : user.email
+      
+      const userType = user.role === 'STUDENT' ? 'estudiante' : user.role === 'TEACHER' ? 'profesor' : 'usuario'
+      
+      activities.push({
+        id: `user-${user.id}`,
+        action: `Nuevo ${userType} registrado`,
+        user: userName,
+        time: getRelativeTime(user.createdAt),
+        type: 'user'
+      })
+    })
+
+    // Procesar inscripciones recientes
+    recentEnrollments.forEach(enrollment => {
+      activities.push({
+        id: `enrollment-${enrollment.id}`,
+        action: `Estudiante inscrito en ${enrollment.section.class.name}`,
+        user: `${enrollment.student.firstName} ${enrollment.student.lastName}`,
+        time: getRelativeTime(enrollment.createdAt),
+        type: 'enrollment'
+      })
+    })
+
+    // Procesar asignaciones recientes
+    recentAssignments.forEach(assignment => {
+      activities.push({
+        id: `assignment-${assignment.id}`,
+        action: `Profesor asignado a ${assignment.section.class.name}`,
+        user: `${assignment.teacher.firstName} ${assignment.teacher.lastName}`,
+        time: getRelativeTime(assignment.createdAt),
+        type: 'assignment'
+      })
+    })
+
+    // Ordenar por fecha y tomar los más recientes
+    return activities
+      .sort((a, b) => {
+        // Convertir tiempo relativo a timestamp para ordenar
+        const timeA = parseRelativeTime(a.time)
+        const timeB = parseRelativeTime(b.time)
+        return timeA - timeB
+      })
+      .slice(0, 10)
+
+  } catch (error) {
+    console.error('Error fetching recent activities:', error)
+    return []
+  }
+}
+
+export async function getCurrentPeriod(): Promise<CurrentPeriod | null> {
+  const session = await getServerSession(authOptions)
+  
+  if (!session || session.user.role !== 'ADMIN') {
+    redirect('/login')
+  }
+
+  try {
+    const period = await prisma.period.findFirst({
+      where: {
+        isActive: true,
+        deletedAt: null
+      },
+      include: {
+        classes: {
+          where: { isActive: true }
+        },
+        partials: {
+          where: { isActive: true }
+        }
+      },
+      orderBy: { createdAt: 'desc' }
+    })
+
+    if (!period) return null
+
+    return {
+      id: period.id,
+      name: period.name,
+      startDate: period.startDate,
+      endDate: period.endDate,
+      isActive: period.isActive,
+      totalClasses: period.classes.length,
+      totalPartials: period.partials.length
+    }
+  } catch (error) {
+    console.error('Error fetching current period:', error)
+    return null
+  }
+}
+
+// Función auxiliar para obtener tiempo relativo
+function getRelativeTime(date: Date): string {
+  const now = new Date()
+  const diffInMs = now.getTime() - date.getTime()
+  const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60))
+  const diffInDays = Math.floor(diffInHours / 24)
+
+  if (diffInHours < 1) {
+    const diffInMinutes = Math.floor(diffInMs / (1000 * 60))
+    return `Hace ${diffInMinutes} minuto${diffInMinutes !== 1 ? 's' : ''}`
+  } else if (diffInHours < 24) {
+    return `Hace ${diffInHours} hora${diffInHours !== 1 ? 's' : ''}`
+  } else if (diffInDays < 7) {
+    return `Hace ${diffInDays} día${diffInDays !== 1 ? 's' : ''}`
+  } else {
+    const diffInWeeks = Math.floor(diffInDays / 7)
+    return `Hace ${diffInWeeks} semana${diffInWeeks !== 1 ? 's' : ''}`
+  }
+}
+
+// Función auxiliar para parsear tiempo relativo a timestamp (para ordenamiento)
+function parseRelativeTime(timeStr: string): number {
+  const now = Date.now()
+  
+  if (timeStr.includes('minuto')) {
+    const minutes = parseInt(timeStr.match(/\d+/)?.[0] || '0')
+    return now - (minutes * 60 * 1000)
+  } else if (timeStr.includes('hora')) {
+    const hours = parseInt(timeStr.match(/\d+/)?.[0] || '0')
+    return now - (hours * 60 * 60 * 1000)
+  } else if (timeStr.includes('día')) {
+    const days = parseInt(timeStr.match(/\d+/)?.[0] || '0')
+    return now - (days * 24 * 60 * 60 * 1000)
+  } else if (timeStr.includes('semana')) {
+    const weeks = parseInt(timeStr.match(/\d+/)?.[0] || '0')
+    return now - (weeks * 7 * 24 * 60 * 60 * 1000)
+  }
+  
+  return now
+}

+ 161 - 96
src/app/admin/page.tsx

@@ -1,69 +1,49 @@
-'use client'
-
 import { MainLayout } from '@/components/layout/main-layout'
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
-import { Users, BookOpen, GraduationCap, User, TrendingUp, Calendar } from 'lucide-react'
-
-const stats = [
-  {
-    title: 'Total Usuarios',
-    value: '3',
-    description: 'Usuarios registrados',
-    icon: Users,
-    color: 'text-blue-600',
-    bgColor: 'bg-blue-100'
-  },
-  {
-    title: 'Clases Activas',
-    value: '3',
-    description: 'Clases en el periodo actual',
-    icon: BookOpen,
-    color: 'text-green-600',
-    bgColor: 'bg-green-100'
-  },
-  {
-    title: 'Profesores',
-    value: '1',
-    description: 'Profesores activos',
-    icon: GraduationCap,
-    color: 'text-purple-600',
-    bgColor: 'bg-purple-100'
-  },
-  {
-    title: 'Estudiantes',
-    value: '1',
-    description: 'Estudiantes matriculados',
-    icon: User,
-    color: 'text-orange-600',
-    bgColor: 'bg-orange-100'
-  }
-]
+import { Users, BookOpen, GraduationCap, User, TrendingUp, Calendar, UserCheck, Clock } from 'lucide-react'
+import { getDashboardStats, getRecentActivities, getCurrentPeriod } from './actions'
 
-const recentActivities = [
-  {
-    id: 1,
-    action: 'Nuevo estudiante registrado',
-    user: 'María González',
-    time: 'Hace 2 horas',
-    type: 'user'
-  },
-  {
-    id: 2,
-    action: 'Clase creada',
-    user: 'Sistema',
-    time: 'Hace 3 horas',
-    type: 'class'
-  },
-  {
-    id: 3,
-    action: 'Profesor asignado',
-    user: 'Juan Pérez',
-    time: 'Hace 5 horas',
-    type: 'assignment'
-  }
-]
+export default async function AdminDashboard() {
+  const [stats, recentActivities, currentPeriod] = await Promise.all([
+    getDashboardStats(),
+    getRecentActivities(),
+    getCurrentPeriod()
+  ])
 
-export default function AdminDashboard() {
+  const statsCards = [
+    {
+      title: 'Total Usuarios',
+      value: stats.totalUsers.toString(),
+      description: 'Usuarios registrados',
+      icon: Users,
+      color: 'text-blue-600',
+      bgColor: 'bg-blue-100'
+    },
+    {
+      title: 'Clases Activas',
+      value: stats.totalClasses.toString(),
+      description: 'Clases en el periodo actual',
+      icon: BookOpen,
+      color: 'text-green-600',
+      bgColor: 'bg-green-100'
+    },
+    {
+      title: 'Profesores',
+      value: stats.totalTeachers.toString(),
+      description: 'Profesores activos',
+      icon: GraduationCap,
+      color: 'text-purple-600',
+      bgColor: 'bg-purple-100'
+    },
+    {
+      title: 'Estudiantes',
+      value: stats.totalStudents.toString(),
+      description: 'Estudiantes matriculados',
+      icon: User,
+      color: 'text-orange-600',
+      bgColor: 'bg-orange-100'
+    }
+  ]
   return (
     <MainLayout 
       title="Dashboard Administrativo" 
@@ -73,7 +53,7 @@ export default function AdminDashboard() {
       <div className="space-y-6">
         {/* Estadísticas */}
         <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
-          {stats.map((stat) => {
+          {statsCards.map((stat) => {
             const Icon = stat.icon
             return (
               <Card key={stat.title}>
@@ -110,57 +90,142 @@ export default function AdminDashboard() {
             </CardHeader>
             <CardContent>
               <div className="space-y-4">
-                {recentActivities.map((activity) => (
-                  <div key={activity.id} className="flex items-center space-x-4">
-                    <div className="w-2 h-2 bg-blue-600 rounded-full"></div>
-                    <div className="flex-1 space-y-1">
-                      <p className="text-sm font-medium leading-none">
-                        {activity.action}
-                      </p>
-                      <p className="text-sm text-muted-foreground">
-                        {activity.user} • {activity.time}
-                      </p>
-                    </div>
+                {recentActivities.length > 0 ? (
+                  recentActivities.map((activity) => {
+                    const getActivityColor = (type: string) => {
+                      switch (type) {
+                        case 'user': return 'bg-blue-600'
+                        case 'enrollment': return 'bg-green-600'
+                        case 'assignment': return 'bg-purple-600'
+                        default: return 'bg-gray-600'
+                      }
+                    }
+
+                    return (
+                      <div key={activity.id} className="flex items-center space-x-4">
+                        <div className={`w-2 h-2 rounded-full ${getActivityColor(activity.type)}`}></div>
+                        <div className="flex-1 space-y-1">
+                          <p className="text-sm font-medium leading-none">
+                            {activity.action}
+                          </p>
+                          <p className="text-sm text-muted-foreground">
+                            {activity.user} • {activity.time}
+                          </p>
+                        </div>
+                      </div>
+                    )
+                  })
+                ) : (
+                  <div className="text-center py-4">
+                    <Clock className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
+                    <p className="text-sm text-muted-foreground">
+                      No hay actividad reciente
+                    </p>
                   </div>
-                ))}
+                )}
               </div>
             </CardContent>
           </Card>
 
           {/* Resumen del Periodo */}
-          {/* <Card>
+          <Card>
             <CardHeader>
               <CardTitle className="flex items-center gap-2">
                 <Calendar className="h-5 w-5" />
                 Periodo Académico Actual
               </CardTitle>
               <CardDescription>
-                Información del periodo 2024
+                {currentPeriod ? `Información del ${currentPeriod.name}` : 'No hay periodo activo'}
               </CardDescription>
             </CardHeader>
             <CardContent>
-              <div className="space-y-4">
-                <div className="flex justify-between items-center">
-                  <span className="text-sm font-medium">Periodo:</span>
-                  <span className="text-sm text-muted-foreground">2024</span>
-                </div>
-                <div className="flex justify-between items-center">
-                  <span className="text-sm font-medium">Parciales:</span>
-                  <span className="text-sm text-muted-foreground">3 configurados</span>
-                </div>
-                <div className="flex justify-between items-center">
-                  <span className="text-sm font-medium">Estado:</span>
-                  <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
-                    Activo
-                  </span>
+              {currentPeriod ? (
+                <div className="space-y-4">
+                  <div className="flex justify-between items-center">
+                    <span className="text-sm font-medium">Periodo:</span>
+                    <span className="text-sm text-muted-foreground">{currentPeriod.name}</span>
+                  </div>
+                  <div className="flex justify-between items-center">
+                    <span className="text-sm font-medium">Parciales:</span>
+                    <span className="text-sm text-muted-foreground">{currentPeriod.totalPartials} configurados</span>
+                  </div>
+                  <div className="flex justify-between items-center">
+                    <span className="text-sm font-medium">Estado:</span>
+                    <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
+                      currentPeriod.isActive 
+                        ? 'bg-green-100 text-green-800' 
+                        : 'bg-red-100 text-red-800'
+                    }`}>
+                      {currentPeriod.isActive ? 'Activo' : 'Inactivo'}
+                    </span>
+                  </div>
+                  <div className="flex justify-between items-center">
+                    <span className="text-sm font-medium">Clases:</span>
+                    <span className="text-sm text-muted-foreground">{currentPeriod.totalClasses} activas</span>
+                  </div>
+                  <div className="flex justify-between items-center">
+                    <span className="text-sm font-medium">Secciones:</span>
+                    <span className="text-sm text-muted-foreground">{stats.totalSections} activas</span>
+                  </div>
                 </div>
-                <div className="flex justify-between items-center">
-                  <span className="text-sm font-medium">Clases:</span>
-                  <span className="text-sm text-muted-foreground">3 activas</span>
+              ) : (
+                <div className="text-center py-4">
+                  <Calendar className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
+                  <p className="text-sm text-muted-foreground">
+                    No hay periodo académico activo
+                  </p>
                 </div>
-              </div>
+              )}
             </CardContent>
-          </Card> */}
+          </Card>
+        </div>
+
+        {/* Estadísticas adicionales */}
+        <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">
+                Inscripciones Recientes
+              </CardTitle>
+              <UserCheck className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{stats.recentEnrollments}</div>
+              <p className="text-xs text-muted-foreground">
+                Últimos 30 días
+              </p>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">
+                Asistencias Registradas
+              </CardTitle>
+              <Clock className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{stats.recentAttendance}</div>
+              <p className="text-xs text-muted-foreground">
+                Últimos 7 días
+              </p>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+              <CardTitle className="text-sm font-medium">
+                Periodos Activos
+              </CardTitle>
+              <Calendar className="h-4 w-4 text-muted-foreground" />
+            </CardHeader>
+            <CardContent>
+              <div className="text-2xl font-bold">{stats.activePeriods}</div>
+              <p className="text-xs text-muted-foreground">
+                Periodos académicos
+              </p>
+            </CardContent>
+          </Card>
         </div>
       </div>
     </MainLayout>

+ 1 - 1
src/app/page.tsx

@@ -56,7 +56,7 @@ export default function Home() {
               <GraduationCap className="h-10 w-10 text-primary" />
             </div>
             <h1 className="text-5xl font-bold slate-900 bg-clip-text">
-              TAPIR
+              UCSG
             </h1>
           </div>
           <h2 className="text-xl text-muted-foreground mb-8 max-w-lg mx-auto">

+ 9 - 6
src/components/layout/sidebar.tsx

@@ -6,7 +6,7 @@ import Link from 'next/link'
 import { usePathname } from 'next/navigation'
 import { cn } from '@/lib/utils'
 import { Button } from '@/components/ui/button'
-import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
+import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle } from '@/components/ui/sheet'
 import { Separator } from '@/components/ui/separator'
 import {
   Menu,
@@ -85,9 +85,9 @@ function SidebarContent({ className }: SidebarProps) {
       {/* Header */}
       <div className="p-6">
         <div className="flex items-center gap-2">
-          <GraduationCap className="h-8 w-8 text-blue-600" />
+          <GraduationCap className="h-8 w-8 text-red-600" />
           <div>
-            <h1 className="text-xl font-bold text-gray-900">TAPIR</h1>
+            <h1 className="text-xl font-bold text-gray-900">UCSG</h1>
             <p className="text-sm text-gray-500">Sistema de Asistencia</p>
           </div>
         </div>
@@ -99,8 +99,8 @@ function SidebarContent({ className }: SidebarProps) {
       {session?.user && (
         <div className="p-4">
           <div className="flex items-center gap-3">
-            <div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
-              <User className="h-5 w-5 text-blue-600" />
+            <div className="h-10 w-10 rounded-full bg-red-100 flex items-center justify-center">
+              <User className="h-5 w-5 text-red-600" />
             </div>
             <div className="flex-1 min-w-0">
               <p className="text-sm font-medium text-gray-900 truncate">
@@ -129,7 +129,7 @@ function SidebarContent({ className }: SidebarProps) {
               className={cn(
                 'flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md transition-colors',
                 isActive
-                  ? 'bg-blue-100 text-blue-700'
+                  ? 'bg-red-100 text-red-700'
                   : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
               )}
             >
@@ -175,6 +175,9 @@ export function MobileSidebar() {
         </Button>
       </SheetTrigger>
       <SheetContent side="left" className="p-0 w-64">
+        <SheetHeader className="sr-only">
+          <SheetTitle>Menú de navegación</SheetTitle>
+        </SheetHeader>
         <SidebarContent />
       </SheetContent>
     </Sheet>