Matthew Trejo преди 4 месеца
родител
ревизия
411c6c6a18

+ 2 - 1
src/app/api/teacher/attendance-history/route.ts

@@ -133,8 +133,9 @@ export async function GET(request: NextRequest) {
     }, {} as Record<string, any>)
 
     // Convert to array and sort by date (most recent first)
+    // Usar comparación de strings directamente para evitar offset de timezone
     const historyByDate = Object.values(groupedByDate).sort((a: any, b: any) => 
-      new Date(b.date).getTime() - new Date(a.date).getTime()
+      b.date.localeCompare(a.date)
     )
 
     return NextResponse.json(historyByDate)

+ 119 - 32
src/app/api/teacher/export-attendance/route.ts

@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
 import { getServerSession } from 'next-auth'
 import { authOptions } from '@/lib/auth'
 import { db } from '@/lib/db'
-import { attendance, users, sections, classes, studentEnrollments } from '@/lib/db/schema'
+import { attendance, users, sections, classes, teacherAssignments, partials } from '@/lib/db/schema'
 import { eq, and, gte, lte, desc } from 'drizzle-orm'
 import { format } from 'date-fns'
 import { es } from 'date-fns/locale'
@@ -35,36 +35,120 @@ export async function GET(request: NextRequest) {
     // Convertir fecha a strings para comparación (el campo date es string en la DB)
     const dateStr = date // La fecha ya viene en formato YYYY-MM-DD
 
-    // Construir query base
-    let whereConditions = [
-      eq(attendance.date, dateStr)
-    ]
-
-    // Agregar filtro de sección si se proporciona
+    // Verificar acceso del profesor a las secciones
+    let teacherSections: string[] = []
+    
     if (sectionId && sectionId !== 'all') {
-      whereConditions.push(eq(attendance.sectionId, sectionId))
+      // Verificar acceso a la sección específica
+      const teacherAssignment = await db
+        .select()
+        .from(teacherAssignments)
+        .where(
+          and(
+            eq(teacherAssignments.teacherId, session.user.id),
+            eq(teacherAssignments.sectionId, sectionId)
+          )
+        )
+
+      if (teacherAssignment.length === 0) {
+        return NextResponse.json(
+          { error: 'No tienes acceso a esta sección' },
+          { status: 403 }
+        )
+      }
+      teacherSections = [sectionId]
+    } else {
+      // Obtener todas las secciones del profesor
+      const assignments = await db
+        .select({ sectionId: teacherAssignments.sectionId })
+        .from(teacherAssignments)
+        .where(eq(teacherAssignments.teacherId, session.user.id))
+      
+      teacherSections = assignments
+        .map(a => a.sectionId)
+        .filter((id): id is string => id !== null)
+      
+      if (teacherSections.length === 0) {
+        return NextResponse.json(
+          { error: 'No tienes secciones asignadas' },
+          { status: 403 }
+        )
+      }
     }
 
+    // Construir query base con verificación de acceso
+    let whereConditions = [
+      eq(attendance.date, dateStr),
+      // Solo incluir secciones a las que el profesor tiene acceso
+      teacherSections.length === 1 
+        ? eq(attendance.sectionId, teacherSections[0])
+        : attendance.sectionId // Si hay múltiples secciones, se filtrará en el query
+    ]
+
     // Obtener registros de asistencia con información relacionada
-    const attendanceData = await db
-      .select({
-        id: attendance.id,
-        date: attendance.date,
-        status: attendance.status,
-        reason: attendance.reason,
-        studentName: users.firstName,
-        studentLastName: users.lastName,
-        studentEmail: users.email,
-        sectionName: sections.name,
-        className: classes.name,
-        classCode: classes.code
-      })
-      .from(attendance)
-      .innerJoin(users, eq(attendance.studentId, users.id))
-      .innerJoin(sections, eq(attendance.sectionId, sections.id))
-      .innerJoin(classes, eq(sections.classId, classes.id))
-      .where(and(...whereConditions))
-      .orderBy(desc(attendance.date), users.firstName, users.lastName)
+    let attendanceData
+    
+    if (teacherSections.length === 1) {
+      // Caso simple: una sola sección
+      attendanceData = await db
+        .select({
+          id: attendance.id,
+          date: attendance.date,
+          status: attendance.status,
+          reason: attendance.reason,
+          studentName: users.firstName,
+          studentLastName: users.lastName,
+          studentEmail: users.email,
+          sectionName: sections.name,
+          className: classes.name,
+          classCode: classes.code,
+          partialName: partials.name,
+          sectionId: attendance.sectionId
+        })
+        .from(attendance)
+        .innerJoin(users, eq(attendance.studentId, users.id))
+        .innerJoin(sections, eq(attendance.sectionId, sections.id))
+        .innerJoin(classes, eq(sections.classId, classes.id))
+        .leftJoin(partials, eq(attendance.partialId, partials.id))
+        .where(
+          and(
+            eq(attendance.date, dateStr),
+            eq(attendance.sectionId, teacherSections[0])
+          )
+        )
+        .orderBy(desc(attendance.date), users.firstName, users.lastName)
+    } else {
+      // Caso múltiples secciones: obtener todos los registros y filtrar
+      const allData = await db
+        .select({
+          id: attendance.id,
+          date: attendance.date,
+          status: attendance.status,
+          reason: attendance.reason,
+          studentName: users.firstName,
+          studentLastName: users.lastName,
+          studentEmail: users.email,
+          sectionName: sections.name,
+          className: classes.name,
+          classCode: classes.code,
+          partialName: partials.name,
+          sectionId: attendance.sectionId
+        })
+        .from(attendance)
+        .innerJoin(users, eq(attendance.studentId, users.id))
+        .innerJoin(sections, eq(attendance.sectionId, sections.id))
+        .innerJoin(classes, eq(sections.classId, classes.id))
+        .leftJoin(partials, eq(attendance.partialId, partials.id))
+        .where(eq(attendance.date, dateStr))
+        .orderBy(desc(attendance.date), users.firstName, users.lastName)
+      
+      // Filtrar solo las secciones del profesor
+      attendanceData = allData.filter(record => 
+        record.sectionId !== null && teacherSections.includes(record.sectionId)
+      )
+    }
+    
+    const filteredData = attendanceData
 
     // Generar contenido CSV
     const csvHeaders = [
@@ -75,17 +159,20 @@ export async function GET(request: NextRequest) {
       'Código de Clase',
       'Sección',
       'Estado',
+      'Parcial',
       'Razón'
     ]
 
-    const csvRows = attendanceData.map(record => [
-      format(new Date(record.date), 'dd/MM/yyyy', { locale: es }),
+    const csvRows = filteredData.map(record => [
+      // Usar la fecha directamente para evitar offset de timezone
+      record.date.split('-').reverse().join('/'), // Convertir YYYY-MM-DD a DD/MM/YYYY
       `${record.studentName || ''} ${record.studentLastName || ''}`.trim(),
       record.studentEmail || '',
       record.className || '',
       record.classCode || '',
       record.sectionName || '',
       getStatusText(record.status),
+      record.partialName || '',
       record.reason || ''
     ])
 
@@ -95,10 +182,10 @@ export async function GET(request: NextRequest) {
       ...csvRows.map(row => row.map(field => `"${field.toString().replace(/"/g, '""')}"`).join(','))
     ].join('\n')
 
-    // Generar nombre de archivo
+    // Generar nombre de archivo usando la fecha directamente para evitar offset de timezone
     const fileName = sectionId && sectionId !== 'all' 
-      ? `asistencia_${format(new Date(date), 'yyyy-MM-dd', { locale: es })}_seccion_${sectionId}.csv`
-      : `asistencia_${format(new Date(date), 'yyyy-MM-dd', { locale: es })}.csv`
+      ? `asistencia_${date}_seccion_${sectionId}.csv`
+      : `asistencia_${date}.csv`
 
     // Retornar archivo CSV
     return new NextResponse(csvContent, {

+ 27 - 13
src/app/teacher/attendance-history/page.tsx

@@ -61,8 +61,17 @@ export default function AttendanceHistoryPage() {
     // Set default date range (last 30 days)
     const today = new Date()
     const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
-    setEndDate(today.toISOString().split('T')[0])
-    setStartDate(thirtyDaysAgo.toISOString().split('T')[0])
+    
+    // Format dates in local timezone to avoid offset issues
+    const formatLocalDate = (date: Date) => {
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      return `${year}-${month}-${day}`
+    }
+    
+    setEndDate(formatLocalDate(today))
+    setStartDate(formatLocalDate(thirtyDaysAgo))
   }, [])
 
   const fetchSections = async () => {
@@ -132,12 +141,21 @@ export default function AttendanceHistoryPage() {
   }
 
   const formatDate = (dateString: string) => {
-    return new Date(dateString).toLocaleDateString('es-ES', {
-      weekday: 'long',
-      year: 'numeric',
-      month: 'long',
-      day: 'numeric'
-    })
+    // Evitar offset de timezone usando la fecha directamente
+    const [year, month, day] = dateString.split('-')
+    const monthNames = [
+      'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
+      'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'
+    ]
+    const dayNames = [
+      'domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'
+    ]
+    
+    // Crear fecha en zona local para obtener el día de la semana
+    const localDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day))
+    const dayOfWeek = dayNames[localDate.getDay()]
+    
+    return `${dayOfWeek}, ${parseInt(day)} de ${monthNames[parseInt(month) - 1]} de ${year}`
   }
 
   const breadcrumbs = [
@@ -277,11 +295,7 @@ export default function AttendanceHistoryPage() {
                     day.records.map((record) => (
                       <TableRow key={record.id}>
                         <TableCell className="font-medium">
-                          {new Date(day.date).toLocaleDateString('es-ES', {
-                            day: '2-digit',
-                            month: '2-digit',
-                            year: 'numeric'
-                          })}
+                          {day.date.split('-').reverse().join('/')}
                         </TableCell>
                         <TableCell>
                           <div>

+ 7 - 1
src/app/teacher/attendance/page.tsx

@@ -45,7 +45,13 @@ export default function AttendancePage() {
   const [students, setStudents] = useState<Student[]>([])
   const [selectedSection, setSelectedSection] = useState<string>('')
   const [selectedPartial, setSelectedPartial] = useState<string>('')
-  const [selectedDate, setSelectedDate] = useState<string>(new Date().toISOString().split('T')[0])
+  const [selectedDate, setSelectedDate] = useState<string>(() => {
+    const today = new Date()
+    const year = today.getFullYear()
+    const month = String(today.getMonth() + 1).padStart(2, '0')
+    const day = String(today.getDate()).padStart(2, '0')
+    return `${year}-${month}-${day}`
+  })
   const [loading, setLoading] = useState(false)
   const [saving, setSaving] = useState(false)
 

+ 7 - 4
src/app/teacher/export-reports/page.tsx

@@ -15,7 +15,7 @@ import { toast } from 'sonner'
 import { Spinner } from '@/components/ui/spinner'
 
 interface Section {
-  id: number
+  id: string
   name: string
   className: string
   classCode: string
@@ -110,13 +110,16 @@ export default function ExportReportsPage() {
 
   const getSelectedSectionName = () => {
     if (selectedSection === 'all') return 'Todas las secciones'
-    const section = sections.find(s => s.id.toString() === selectedSection)
+    const section = sections.find(s => s.id === selectedSection)
     return section ? `${section.className} - ${section.name}` : 'Sección desconocida'
   }
 
   const formatSelectedDate = () => {
     if (!selectedDate) return ''
-    return format(new Date(selectedDate), 'dd \\de MMMM \\de yyyy', { locale: es })
+    // Crear fecha en zona horaria local para evitar offset
+    const [year, month, day] = selectedDate.split('-').map(Number)
+    const localDate = new Date(year, month - 1, day)
+    return format(localDate, 'dd \\de MMMM \\de yyyy', { locale: es })
   }
 
   if (loading) {
@@ -191,7 +194,7 @@ export default function ExportReportsPage() {
                 <SelectContent>
                   <SelectItem value="all">Todas las secciones</SelectItem>
                   {sections.map((section) => (
-                    <SelectItem key={section.id} value={section.id.toString()}>
+                    <SelectItem key={section.id} value={section.id}>
                       {section.className} - {section.name}
                     </SelectItem>
                   ))}

+ 88 - 61
src/app/teacher/students/page.tsx

@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
 import { Input } from '@/components/ui/input'
 import { Badge } from '@/components/ui/badge'
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
 import { Users, Search, Mail, User, BookOpen } from 'lucide-react'
 import { toast } from 'sonner'
 import { DashboardLayout } from '@/components/dashboard-layout'
@@ -97,10 +98,21 @@ export default function StudentsPage() {
     }
   }
 
-  const getAttendanceColor = (percentage: number) => {
-    if (percentage >= 80) return 'bg-green-100 text-green-800'
-    if (percentage >= 60) return 'bg-yellow-100 text-yellow-800'
-    return 'bg-red-100 text-red-800'
+  const getAttendanceBadgeVariant = (percentage: number): "default" | "secondary" | "destructive" | "outline" => {
+    if (percentage >= 80) return 'default'
+    if (percentage >= 60) return 'secondary'
+    return 'destructive'
+  }
+
+  const formatEnrollmentDate = (dateString: string) => {
+    // Evitar offset de timezone usando la fecha directamente
+    const [year, month, day] = dateString.split('-')
+    const monthNames = [
+      'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
+      'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'
+    ]
+    
+    return `${parseInt(day)} de ${monthNames[parseInt(month) - 1]} de ${year}`
   }
 
   const selectedSectionInfo = sections.find(s => s.id === selectedSection)
@@ -114,8 +126,8 @@ export default function StudentsPage() {
     <DashboardLayout breadcrumbs={breadcrumbs}>
       <div className="space-y-6">
       <div>
-        <h1 className="text-2xl font-bold text-gray-900">Mis Estudiantes</h1>
-        <p className="text-gray-600">Gestiona y revisa la información de tus estudiantes</p>
+        <h1 className="text-2xl font-bold">Mis Estudiantes</h1>
+        <p>Gestiona y revisa la información de tus estudiantes</p>
       </div>
 
       {/* Section Selection */}
@@ -146,7 +158,7 @@ export default function StudentsPage() {
 
             {selectedSectionInfo && (
               <div className="flex items-end">
-                <div className="text-sm text-gray-600">
+                <div className="text-sm">
                   <p><strong>Clase:</strong> {selectedSectionInfo.className}</p>
                   <p><strong>Período:</strong> {selectedSectionInfo.periodName}</p>
                   <p><strong>Estudiantes:</strong> {selectedSectionInfo.studentCount}</p>
@@ -193,64 +205,79 @@ export default function StudentsPage() {
                 </p>
               </div>
             ) : (
-              <div className="space-y-4">
-                {filteredStudents.map((student) => (
-                  <div
-                    key={student.id}
-                    className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50"
-                  >
-                    <div className="flex items-center space-x-4">
-                      <div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
-                        <User className="h-5 w-5 text-blue-600" />
-                      </div>
-                      <div>
-                        <h3 className="font-medium text-gray-900">{student.name}</h3>
-                        <div className="flex items-center space-x-2 text-sm text-gray-600">
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>Estudiante</TableHead>
+                    <TableHead>Email</TableHead>
+                    <TableHead>Fecha de Matrícula</TableHead>
+                    <TableHead>Asistencia</TableHead>
+                    <TableHead>Estadísticas</TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {filteredStudents.map((student) => (
+                    <TableRow key={student.id}>
+                      <TableCell>
+                        <div className="flex items-center space-x-3">
+                          <div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
+                            <User className="h-4 w-4" />
+                          </div>
+                          <span className="font-medium">{student.name}</span>
+                        </div>
+                      </TableCell>
+                      <TableCell>
+                        <div className="flex items-center space-x-2 text-sm">
                           <Mail className="h-4 w-4" />
                           <span>{student.email}</span>
                         </div>
-                        <p className="text-xs text-gray-500">
-                          Matriculado: {new Date(student.enrollmentDate).toLocaleDateString('es-ES')}
-                        </p>
-                      </div>
-                    </div>
-
-                    {student.attendanceStats && (
-                      <div className="text-right space-y-2">
-                        <Badge className={getAttendanceColor(student.attendanceStats.attendancePercentage)}>
-                          {student.attendanceStats.attendancePercentage.toFixed(1)}% Asistencia
-                        </Badge>
-                        <div className="text-xs text-gray-600 space-y-1">
-                          <div className="flex justify-between gap-4">
-                            <span>Presente:</span>
-                            <span className="font-medium text-green-600">
-                              {student.attendanceStats.presentCount}
-                            </span>
+                      </TableCell>
+                      <TableCell>
+                        <span className="text-sm">
+                          {formatEnrollmentDate(student.enrollmentDate)}
+                        </span>
+                      </TableCell>
+                      <TableCell>
+                        {student.attendanceStats && (
+                          <Badge variant={getAttendanceBadgeVariant(student.attendanceStats.attendancePercentage)}>
+                            {student.attendanceStats.attendancePercentage.toFixed(1)}% Asistencia
+                          </Badge>
+                        )}
+                      </TableCell>
+                      <TableCell>
+                        {student.attendanceStats && (
+                          <div className="space-y-1 text-xs">
+                            <div className="flex justify-between gap-4">
+                              <span>Presente:</span>
+                              <Badge variant="outline" className="text-green-600 border-green-200">
+                                {student.attendanceStats.presentCount}
+                              </Badge>
+                            </div>
+                            <div className="flex justify-between gap-4">
+                              <span>Tardanza:</span>
+                              <Badge variant="outline" className="text-yellow-600 border-yellow-200">
+                                {student.attendanceStats.lateCount}
+                              </Badge>
+                            </div>
+                            <div className="flex justify-between gap-4">
+                              <span>Ausente:</span>
+                              <Badge variant="outline" className="text-red-600 border-red-200">
+                                {student.attendanceStats.absentCount}
+                              </Badge>
+                            </div>
+                            <div className="flex justify-between gap-4 pt-1 border-t">
+                              <span>Total:</span>
+                              <Badge variant="secondary">
+                                {student.attendanceStats.totalClasses}
+                              </Badge>
+                            </div>
                           </div>
-                          <div className="flex justify-between gap-4">
-                            <span>Tardanza:</span>
-                            <span className="font-medium text-yellow-600">
-                              {student.attendanceStats.lateCount}
-                            </span>
-                          </div>
-                          <div className="flex justify-between gap-4">
-                            <span>Ausente:</span>
-                            <span className="font-medium text-red-600">
-                              {student.attendanceStats.absentCount}
-                            </span>
-                          </div>
-                          <div className="flex justify-between gap-4 pt-1 border-t">
-                            <span>Total clases:</span>
-                            <span className="font-medium">
-                              {student.attendanceStats.totalClasses}
-                            </span>
-                          </div>
-                        </div>
-                      </div>
-                    )}
-                  </div>
-                ))}
-              </div>
+                        )}
+                      </TableCell>
+                    </TableRow>
+                  ))}
+                </TableBody>
+              </Table>
             )}
           </CardContent>
         </Card>