route.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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. // Construir filtros
  35. const whereClause: any = {
  36. studentId: user.student.id,
  37. };
  38. if (sectionId && sectionId !== 'all') {
  39. whereClause.sectionId = sectionId;
  40. }
  41. if (status && status !== 'all') {
  42. whereClause.status = status;
  43. }
  44. if (startDate) {
  45. whereClause.date = {
  46. ...whereClause.date,
  47. gte: new Date(startDate)
  48. };
  49. }
  50. if (endDate) {
  51. whereClause.date = {
  52. ...whereClause.date,
  53. lte: new Date(endDate)
  54. };
  55. }
  56. // Si se especifica un período, filtrar por secciones de ese período
  57. if (periodId && periodId !== 'all') {
  58. whereClause.section = {
  59. class: {
  60. periodId: periodId
  61. }
  62. };
  63. }
  64. // Obtener registros de asistencia
  65. const attendanceRecords = await prisma.attendance.findMany({
  66. where: whereClause,
  67. include: {
  68. section: {
  69. include: {
  70. class: {
  71. include: {
  72. period: true
  73. }
  74. },
  75. teacherAssignments: {
  76. where: { isActive: true },
  77. include: {
  78. teacher: true
  79. }
  80. }
  81. }
  82. }
  83. },
  84. orderBy: {
  85. date: 'desc'
  86. }
  87. });
  88. // Obtener estadísticas generales
  89. const totalRecords = attendanceRecords.length;
  90. const presentCount = attendanceRecords.filter(r => r.status === 'PRESENT').length;
  91. const absentCount = attendanceRecords.filter(r => r.status === 'ABSENT').length;
  92. const justifiedCount = attendanceRecords.filter(r => r.status === 'JUSTIFIED').length;
  93. const attendanceRate = totalRecords > 0 ? Math.round((presentCount / totalRecords) * 100) : 0;
  94. // Estadísticas por sección
  95. const sectionStats = attendanceRecords.reduce((acc, record) => {
  96. const sectionId = record.sectionId;
  97. if (!acc[sectionId]) {
  98. acc[sectionId] = {
  99. sectionId,
  100. sectionName: record.section.name,
  101. className: record.section.class.name,
  102. classCode: record.section.class.code,
  103. periodName: record.section.class.period.name,
  104. total: 0,
  105. present: 0,
  106. absent: 0,
  107. justified: 0,
  108. attendanceRate: 0
  109. };
  110. }
  111. acc[sectionId].total++;
  112. if (record.status === 'PRESENT') acc[sectionId].present++;
  113. if (record.status === 'ABSENT') acc[sectionId].absent++;
  114. if (record.status === 'JUSTIFIED') acc[sectionId].justified++;
  115. acc[sectionId].attendanceRate = Math.round(
  116. (acc[sectionId].present / acc[sectionId].total) * 100
  117. );
  118. return acc;
  119. }, {} as Record<string, any>);
  120. // Estadísticas por período
  121. const periodStats = attendanceRecords.reduce((acc, record) => {
  122. const periodId = record.section.class.period.id;
  123. if (!acc[periodId]) {
  124. acc[periodId] = {
  125. periodId,
  126. periodName: record.section.class.period.name,
  127. isActive: record.section.class.period.isActive,
  128. total: 0,
  129. present: 0,
  130. absent: 0,
  131. justified: 0,
  132. attendanceRate: 0
  133. };
  134. }
  135. acc[periodId].total++;
  136. if (record.status === 'PRESENT') acc[periodId].present++;
  137. if (record.status === 'ABSENT') acc[periodId].absent++;
  138. if (record.status === 'JUSTIFIED') acc[periodId].justified++;
  139. acc[periodId].attendanceRate = Math.round(
  140. (acc[periodId].present / acc[periodId].total) * 100
  141. );
  142. return acc;
  143. }, {} as Record<string, any>);
  144. // Obtener todas las secciones matriculadas para filtros
  145. const enrolledSections = await prisma.studentEnrollment.findMany({
  146. where: {
  147. studentId: user.student.id,
  148. isActive: true
  149. },
  150. include: {
  151. section: {
  152. include: {
  153. class: {
  154. include: {
  155. period: true
  156. }
  157. }
  158. }
  159. }
  160. }
  161. });
  162. // Obtener períodos únicos
  163. const periods = Array.from(
  164. new Set(
  165. enrolledSections.map(e => e.section.class.period)
  166. )
  167. ).filter((period, index, self) =>
  168. self.findIndex(p => p.id === period.id) === index
  169. );
  170. return NextResponse.json({
  171. student: {
  172. id: user.student.id,
  173. firstName: user.student.firstName,
  174. lastName: user.student.lastName,
  175. admissionNumber: user.student.admissionNumber
  176. },
  177. attendanceRecords: attendanceRecords.map(record => ({
  178. id: record.id,
  179. date: record.date,
  180. status: record.status,
  181. reason: record.reason,
  182. section: {
  183. id: record.section.id,
  184. name: record.section.name,
  185. class: {
  186. id: record.section.class.id,
  187. name: record.section.class.name,
  188. code: record.section.class.code,
  189. period: {
  190. id: record.section.class.period.id,
  191. name: record.section.class.period.name,
  192. isActive: record.section.class.period.isActive
  193. }
  194. },
  195. teachers: record.section.teacherAssignments.map(ta => ({
  196. id: ta.teacher.id,
  197. firstName: ta.teacher.firstName,
  198. lastName: ta.teacher.lastName,
  199. email: ta.teacher.email
  200. }))
  201. }
  202. })),
  203. statistics: {
  204. overall: {
  205. totalRecords,
  206. present: presentCount,
  207. absent: absentCount,
  208. justified: justifiedCount,
  209. attendanceRate
  210. },
  211. bySections: Object.values(sectionStats),
  212. byPeriods: Object.values(periodStats)
  213. },
  214. filters: {
  215. sections: enrolledSections.map(e => ({
  216. id: e.section.id,
  217. name: e.section.name,
  218. className: e.section.class.name,
  219. classCode: e.section.class.code,
  220. periodName: e.section.class.period.name
  221. })),
  222. periods: periods.map(p => ({
  223. id: p.id,
  224. name: p.name,
  225. isActive: p.isActive
  226. }))
  227. }
  228. });
  229. } catch (error) {
  230. console.error('Error al obtener historial de asistencia:', error);
  231. return NextResponse.json(
  232. { error: 'Error interno del servidor' },
  233. { status: 500 }
  234. );
  235. }
  236. }