route.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import { NextRequest, NextResponse } from 'next/server';
  2. import { getServerSession } from 'next-auth';
  3. import { authOptions } from '@/lib/auth';
  4. import { prisma } from '@/lib/prisma';
  5. import { AttendanceStatus } from '@prisma/client';
  6. export async function GET(request: NextRequest) {
  7. try {
  8. // Verificar sesión
  9. const session = await getServerSession(authOptions);
  10. if (!session?.user?.id) {
  11. return NextResponse.json(
  12. { error: 'No autorizado' },
  13. { status: 401 }
  14. );
  15. }
  16. // Verificar que el usuario sea estudiante
  17. const user = await prisma.user.findUnique({
  18. where: { id: session.user.id },
  19. include: { student: true }
  20. });
  21. if (!user || user.role !== 'STUDENT' || !user.student) {
  22. return NextResponse.json(
  23. { error: 'Acceso denegado. Solo estudiantes pueden acceder.' },
  24. { status: 403 }
  25. );
  26. }
  27. // Obtener parámetros de consulta
  28. const { searchParams } = new URL(request.url);
  29. const sectionId = searchParams.get('sectionId');
  30. const status = searchParams.get('status') as AttendanceStatus | 'all' | null;
  31. const startDate = searchParams.get('startDate');
  32. const endDate = searchParams.get('endDate');
  33. const periodId = searchParams.get('periodId');
  34. /* eslint-disable @typescript-eslint/no-explicit-any */
  35. // Construir filtros
  36. const whereClause: any = {
  37. studentId: user.student.id,
  38. };
  39. if (sectionId && sectionId !== 'all') {
  40. whereClause.sectionId = sectionId;
  41. }
  42. if (status && status !== 'all') {
  43. whereClause.status = status;
  44. }
  45. if (startDate) {
  46. whereClause.date = {
  47. ...whereClause.date,
  48. gte: new Date(startDate)
  49. };
  50. }
  51. if (endDate) {
  52. whereClause.date = {
  53. ...whereClause.date,
  54. lte: new Date(endDate)
  55. };
  56. }
  57. // Si se especifica un período, filtrar por secciones de ese período
  58. if (periodId && periodId !== 'all') {
  59. whereClause.section = {
  60. class: {
  61. periodId: periodId
  62. }
  63. };
  64. }
  65. // Obtener registros de asistencia
  66. const attendanceRecords = await prisma.attendance.findMany({
  67. where: whereClause,
  68. include: {
  69. section: {
  70. include: {
  71. class: {
  72. include: {
  73. period: true
  74. }
  75. },
  76. teacherAssignments: {
  77. where: { isActive: true },
  78. include: {
  79. teacher: true
  80. }
  81. }
  82. }
  83. }
  84. },
  85. orderBy: {
  86. date: 'desc'
  87. }
  88. });
  89. // Obtener estadísticas generales
  90. const totalRecords = attendanceRecords.length;
  91. const presentCount = attendanceRecords.filter(r => r.status === 'PRESENT').length;
  92. const absentCount = attendanceRecords.filter(r => r.status === 'ABSENT').length;
  93. const justifiedCount = attendanceRecords.filter(r => r.status === 'JUSTIFIED').length;
  94. const attendanceRate = totalRecords > 0 ? Math.round((presentCount / totalRecords) * 100) : 0;
  95. /* eslint-disable @typescript-eslint/no-explicit-any */
  96. // Estadísticas por sección
  97. const sectionStats = attendanceRecords.reduce((acc, record) => {
  98. const sectionId = record.sectionId;
  99. if (!acc[sectionId]) {
  100. acc[sectionId] = {
  101. sectionId,
  102. sectionName: record.section.name,
  103. className: record.section.class.name,
  104. classCode: record.section.class.code,
  105. periodName: record.section.class.period.name,
  106. total: 0,
  107. present: 0,
  108. absent: 0,
  109. justified: 0,
  110. attendanceRate: 0
  111. };
  112. }
  113. acc[sectionId].total++;
  114. if (record.status === 'PRESENT') acc[sectionId].present++;
  115. if (record.status === 'ABSENT') acc[sectionId].absent++;
  116. if (record.status === 'JUSTIFIED') acc[sectionId].justified++;
  117. acc[sectionId].attendanceRate = Math.round(
  118. (acc[sectionId].present / acc[sectionId].total) * 100
  119. );
  120. return acc;
  121. }, {} as Record<string, any>);
  122. // Estadísticas por período
  123. const periodStats = attendanceRecords.reduce((acc, record) => {
  124. const periodId = record.section.class.period.id;
  125. if (!acc[periodId]) {
  126. acc[periodId] = {
  127. periodId,
  128. periodName: record.section.class.period.name,
  129. isActive: record.section.class.period.isActive,
  130. total: 0,
  131. present: 0,
  132. absent: 0,
  133. justified: 0,
  134. attendanceRate: 0
  135. };
  136. }
  137. acc[periodId].total++;
  138. if (record.status === 'PRESENT') acc[periodId].present++;
  139. if (record.status === 'ABSENT') acc[periodId].absent++;
  140. if (record.status === 'JUSTIFIED') acc[periodId].justified++;
  141. acc[periodId].attendanceRate = Math.round(
  142. (acc[periodId].present / acc[periodId].total) * 100
  143. );
  144. return acc;
  145. }, {} as Record<string, any>);
  146. // Obtener todas las secciones matriculadas para filtros
  147. const enrolledSections = await prisma.studentEnrollment.findMany({
  148. where: {
  149. studentId: user.student.id,
  150. isActive: true
  151. },
  152. include: {
  153. section: {
  154. include: {
  155. class: {
  156. include: {
  157. period: true
  158. }
  159. }
  160. }
  161. }
  162. }
  163. });
  164. // Obtener períodos únicos
  165. const periods = Array.from(
  166. new Set(
  167. enrolledSections.map(e => e.section.class.period)
  168. )
  169. ).filter((period, index, self) =>
  170. self.findIndex(p => p.id === period.id) === index
  171. );
  172. return NextResponse.json({
  173. student: {
  174. id: user.student.id,
  175. firstName: user.student.firstName,
  176. lastName: user.student.lastName,
  177. admissionNumber: user.student.admissionNumber
  178. },
  179. attendanceRecords: attendanceRecords.map(record => ({
  180. id: record.id,
  181. date: record.date,
  182. status: record.status,
  183. reason: record.reason,
  184. section: {
  185. id: record.section.id,
  186. name: record.section.name,
  187. class: {
  188. id: record.section.class.id,
  189. name: record.section.class.name,
  190. code: record.section.class.code,
  191. period: {
  192. id: record.section.class.period.id,
  193. name: record.section.class.period.name,
  194. isActive: record.section.class.period.isActive
  195. }
  196. },
  197. teachers: record.section.teacherAssignments.map(ta => ({
  198. id: ta.teacher.id,
  199. firstName: ta.teacher.firstName,
  200. lastName: ta.teacher.lastName,
  201. email: ta.teacher.email
  202. }))
  203. }
  204. })),
  205. statistics: {
  206. overall: {
  207. totalRecords,
  208. present: presentCount,
  209. absent: absentCount,
  210. justified: justifiedCount,
  211. attendanceRate
  212. },
  213. bySections: Object.values(sectionStats),
  214. byPeriods: Object.values(periodStats)
  215. },
  216. filters: {
  217. sections: enrolledSections.map(e => ({
  218. id: e.section.id,
  219. name: e.section.name,
  220. className: e.section.class.name,
  221. classCode: e.section.class.code,
  222. periodName: e.section.class.period.name
  223. })),
  224. periods: periods.map(p => ({
  225. id: p.id,
  226. name: p.name,
  227. isActive: p.isActive
  228. }))
  229. }
  230. });
  231. } catch (error) {
  232. console.error('Error al obtener historial de asistencia:', error);
  233. return NextResponse.json(
  234. { error: 'Error interno del servidor' },
  235. { status: 500 }
  236. );
  237. }
  238. }