route.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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. export async function GET(request: NextRequest) {
  6. try {
  7. const session = await getServerSession(authOptions);
  8. if (!session || session.user.role !== 'ADMIN') {
  9. return NextResponse.json(
  10. { message: 'No tienes permisos para acceder a los reportes' },
  11. { status: 403 }
  12. );
  13. }
  14. const { searchParams } = new URL(request.url);
  15. const reportType = searchParams.get('type');
  16. const periodId = searchParams.get('periodId');
  17. const startDate = searchParams.get('startDate');
  18. const endDate = searchParams.get('endDate');
  19. switch (reportType) {
  20. case 'overview':
  21. return await getOverviewReport();
  22. case 'students':
  23. return await getStudentsReport(periodId);
  24. case 'teachers':
  25. return await getTeachersReport(periodId);
  26. case 'attendance':
  27. return await getAttendanceReport(periodId, startDate, endDate);
  28. case 'enrollments':
  29. return await getEnrollmentsReport(periodId);
  30. case 'classes':
  31. return await getClassesReport(periodId);
  32. default:
  33. return await getOverviewReport();
  34. }
  35. } catch (error) {
  36. console.error('Error generating report:', error);
  37. return NextResponse.json(
  38. { message: 'Error interno del servidor' },
  39. { status: 500 }
  40. );
  41. }
  42. }
  43. // Reporte general del sistema
  44. async function getOverviewReport() {
  45. try {
  46. const [totalUsers, totalStudents, totalTeachers, totalClasses, totalSections, activePeriods] = await Promise.all([
  47. prisma.user.count(),
  48. prisma.student.count({ where: { isActive: true } }),
  49. prisma.teacher.count({ where: { isActive: true } }),
  50. prisma.class.count({ where: { isActive: true } }),
  51. prisma.section.count({ where: { isActive: true } }),
  52. prisma.period.count({ where: { isActive: true } })
  53. ]);
  54. const usersByRole = await prisma.user.groupBy({
  55. by: ['role'],
  56. _count: {
  57. id: true
  58. }
  59. });
  60. const recentEnrollments = await prisma.studentEnrollment.count({
  61. where: {
  62. createdAt: {
  63. gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // Últimos 30 días
  64. }
  65. }
  66. });
  67. const recentAttendance = await prisma.attendance.count({
  68. where: {
  69. createdAt: {
  70. gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Últimos 7 días
  71. }
  72. }
  73. });
  74. return NextResponse.json({
  75. overview: {
  76. totalUsers,
  77. totalStudents,
  78. totalTeachers,
  79. totalClasses,
  80. totalSections,
  81. activePeriods,
  82. recentEnrollments,
  83. recentAttendance
  84. },
  85. usersByRole: usersByRole.map(item => ({
  86. role: item.role,
  87. count: item._count.id
  88. }))
  89. });
  90. } catch (error) {
  91. console.error('Error in overview report:', error);
  92. throw error;
  93. }
  94. }
  95. // Reporte de estudiantes
  96. async function getStudentsReport(periodId: string | null) {
  97. try {
  98. const whereClause = periodId ? {
  99. enrollments: {
  100. some: {
  101. section: {
  102. class: {
  103. periodId: periodId
  104. }
  105. }
  106. }
  107. }
  108. } : {};
  109. const students = await prisma.student.findMany({
  110. where: {
  111. isActive: true,
  112. ...whereClause
  113. },
  114. include: {
  115. enrollments: {
  116. where: {
  117. isActive: true,
  118. ...(periodId ? {
  119. section: {
  120. class: {
  121. periodId: periodId
  122. }
  123. }
  124. } : {})
  125. },
  126. include: {
  127. section: {
  128. include: {
  129. class: {
  130. include: {
  131. period: true
  132. }
  133. }
  134. }
  135. }
  136. }
  137. },
  138. attendances: {
  139. where: {
  140. ...(periodId ? {
  141. section: {
  142. class: {
  143. periodId: periodId
  144. }
  145. }
  146. } : {})
  147. }
  148. }
  149. },
  150. orderBy: {
  151. lastName: 'asc'
  152. }
  153. });
  154. const studentsWithStats = students.map(student => {
  155. const totalAttendances = student.attendances.length;
  156. const presentAttendances = student.attendances.filter(a => a.status === 'PRESENT').length;
  157. const attendanceRate = totalAttendances > 0 ? (presentAttendances / totalAttendances) * 100 : 0;
  158. return {
  159. id: student.id,
  160. firstName: student.firstName,
  161. lastName: student.lastName,
  162. cedula: student.cedula,
  163. email: student.email,
  164. admissionNumber: student.admissionNumber,
  165. enrollmentsCount: student.enrollments.length,
  166. attendanceRate: Math.round(attendanceRate * 100) / 100,
  167. totalAttendances,
  168. presentAttendances,
  169. enrollments: student.enrollments.map(enrollment => ({
  170. sectionName: enrollment.section.name,
  171. className: enrollment.section.class.name,
  172. classCode: enrollment.section.class.code,
  173. periodName: enrollment.section.class.period.name
  174. }))
  175. };
  176. });
  177. return NextResponse.json({
  178. students: studentsWithStats,
  179. summary: {
  180. totalStudents: students.length,
  181. averageEnrollments: students.length > 0 ? students.reduce((sum, s) => sum + s.enrollments.length, 0) / students.length : 0,
  182. averageAttendanceRate: studentsWithStats.length > 0 ? studentsWithStats.reduce((sum, s) => sum + s.attendanceRate, 0) / studentsWithStats.length : 0
  183. }
  184. });
  185. } catch (error) {
  186. console.error('Error in students report:', error);
  187. throw error;
  188. }
  189. }
  190. // Reporte de profesores
  191. async function getTeachersReport(periodId: string | null) {
  192. try {
  193. const whereClause = periodId ? {
  194. assignments: {
  195. some: {
  196. section: {
  197. class: {
  198. periodId: periodId
  199. }
  200. }
  201. }
  202. }
  203. } : {};
  204. const teachers = await prisma.teacher.findMany({
  205. where: {
  206. isActive: true,
  207. ...whereClause
  208. },
  209. include: {
  210. assignments: {
  211. where: {
  212. isActive: true,
  213. ...(periodId ? {
  214. section: {
  215. class: {
  216. periodId: periodId
  217. }
  218. }
  219. } : {})
  220. },
  221. include: {
  222. section: {
  223. include: {
  224. class: {
  225. include: {
  226. period: true
  227. }
  228. },
  229. studentEnrollments: {
  230. where: {
  231. isActive: true
  232. }
  233. }
  234. }
  235. }
  236. }
  237. }
  238. },
  239. orderBy: {
  240. lastName: 'asc'
  241. }
  242. });
  243. const teachersWithStats = teachers.map(teacher => {
  244. const totalStudents = teacher.assignments.reduce((sum, assignment) =>
  245. sum + assignment.section.studentEnrollments.length, 0
  246. );
  247. return {
  248. id: teacher.id,
  249. firstName: teacher.firstName,
  250. lastName: teacher.lastName,
  251. cedula: teacher.cedula,
  252. email: teacher.email,
  253. phone: teacher.phone,
  254. assignmentsCount: teacher.assignments.length,
  255. totalStudents,
  256. assignments: teacher.assignments.map(assignment => ({
  257. sectionName: assignment.section.name,
  258. className: assignment.section.class.name,
  259. classCode: assignment.section.class.code,
  260. periodName: assignment.section.class.period.name,
  261. studentsCount: assignment.section.studentEnrollments.length
  262. }))
  263. };
  264. });
  265. return NextResponse.json({
  266. teachers: teachersWithStats,
  267. summary: {
  268. totalTeachers: teachers.length,
  269. averageAssignments: teachers.length > 0 ? teachers.reduce((sum, t) => sum + t.assignments.length, 0) / teachers.length : 0,
  270. totalStudentsManaged: teachersWithStats.reduce((sum, t) => sum + t.totalStudents, 0)
  271. }
  272. });
  273. } catch (error) {
  274. console.error('Error in teachers report:', error);
  275. throw error;
  276. }
  277. }
  278. /* eslint-disable @typescript-eslint/no-explicit-any */
  279. // Reporte de asistencia
  280. async function getAttendanceReport(periodId: string | null, startDate: string | null, endDate: string | null) {
  281. try {
  282. const dateFilter: any = {};
  283. if (startDate) dateFilter.gte = new Date(startDate);
  284. if (endDate) dateFilter.lte = new Date(endDate);
  285. const whereClause: any = {
  286. ...(Object.keys(dateFilter).length > 0 && { date: dateFilter }),
  287. ...(periodId && {
  288. section: {
  289. class: {
  290. periodId: periodId
  291. }
  292. }
  293. })
  294. };
  295. const attendances = await prisma.attendance.findMany({
  296. where: whereClause,
  297. include: {
  298. student: true,
  299. section: {
  300. include: {
  301. class: {
  302. include: {
  303. period: true
  304. }
  305. }
  306. }
  307. }
  308. },
  309. orderBy: {
  310. date: 'desc'
  311. }
  312. });
  313. const attendanceByStatus = await prisma.attendance.groupBy({
  314. by: ['status'],
  315. where: whereClause,
  316. _count: {
  317. id: true
  318. }
  319. });
  320. const attendanceByDate = await prisma.attendance.groupBy({
  321. by: ['date'],
  322. where: whereClause,
  323. _count: {
  324. id: true
  325. },
  326. orderBy: {
  327. date: 'asc'
  328. }
  329. });
  330. return NextResponse.json({
  331. attendances: attendances.map(attendance => ({
  332. id: attendance.id,
  333. date: attendance.date,
  334. status: attendance.status,
  335. reason: attendance.reason,
  336. student: {
  337. firstName: attendance.student.firstName,
  338. lastName: attendance.student.lastName,
  339. cedula: attendance.student.cedula,
  340. admissionNumber: attendance.student.admissionNumber
  341. },
  342. section: {
  343. name: attendance.section.name,
  344. className: attendance.section.class.name,
  345. classCode: attendance.section.class.code,
  346. periodName: attendance.section.class.period.name
  347. }
  348. })),
  349. summary: {
  350. totalRecords: attendances.length,
  351. byStatus: attendanceByStatus.map(item => ({
  352. status: item.status,
  353. count: item._count.id
  354. })),
  355. byDate: attendanceByDate.map(item => ({
  356. date: item.date,
  357. count: item._count.id
  358. }))
  359. }
  360. });
  361. } catch (error) {
  362. console.error('Error in attendance report:', error);
  363. throw error;
  364. }
  365. }
  366. // Reporte de inscripciones
  367. async function getEnrollmentsReport(periodId: string | null) {
  368. try {
  369. const whereClause = periodId ? {
  370. section: {
  371. class: {
  372. periodId: periodId
  373. }
  374. }
  375. } : {};
  376. const enrollments = await prisma.studentEnrollment.findMany({
  377. where: whereClause,
  378. include: {
  379. student: true,
  380. section: {
  381. include: {
  382. class: {
  383. include: {
  384. period: true
  385. }
  386. }
  387. }
  388. }
  389. },
  390. orderBy: {
  391. createdAt: 'desc'
  392. }
  393. });
  394. const enrollmentsByStatus = await prisma.studentEnrollment.groupBy({
  395. by: ['isActive'],
  396. where: whereClause,
  397. _count: {
  398. id: true
  399. }
  400. });
  401. const enrollmentsBySection = await prisma.studentEnrollment.groupBy({
  402. by: ['sectionId'],
  403. where: whereClause,
  404. _count: {
  405. id: true
  406. }
  407. });
  408. return NextResponse.json({
  409. enrollments: enrollments.map(enrollment => ({
  410. id: enrollment.id,
  411. isActive: enrollment.isActive,
  412. createdAt: enrollment.createdAt,
  413. student: {
  414. firstName: enrollment.student.firstName,
  415. lastName: enrollment.student.lastName,
  416. cedula: enrollment.student.cedula,
  417. admissionNumber: enrollment.student.admissionNumber
  418. },
  419. section: {
  420. name: enrollment.section.name,
  421. className: enrollment.section.class.name,
  422. classCode: enrollment.section.class.code,
  423. periodName: enrollment.section.class.period.name
  424. }
  425. })),
  426. summary: {
  427. totalEnrollments: enrollments.length,
  428. byStatus: enrollmentsByStatus.map(item => ({
  429. isActive: item.isActive,
  430. count: item._count.id
  431. })),
  432. bySectionCount: enrollmentsBySection.length
  433. }
  434. });
  435. } catch (error) {
  436. console.error('Error in enrollments report:', error);
  437. throw error;
  438. }
  439. }
  440. // Reporte de clases
  441. async function getClassesReport(periodId: string | null) {
  442. try {
  443. const whereClause = periodId ? { periodId } : { isActive: true };
  444. const classes = await prisma.class.findMany({
  445. where: whereClause,
  446. include: {
  447. period: true,
  448. sections: {
  449. include: {
  450. studentEnrollments: {
  451. where: {
  452. isActive: true
  453. }
  454. },
  455. teacherAssignments: {
  456. where: {
  457. isActive: true
  458. },
  459. include: {
  460. teacher: true
  461. }
  462. }
  463. }
  464. }
  465. },
  466. orderBy: {
  467. name: 'asc'
  468. }
  469. });
  470. const classesWithStats = classes.map(classItem => {
  471. const totalSections = classItem.sections.length;
  472. const totalStudents = classItem.sections.reduce((sum, section) =>
  473. sum + section.studentEnrollments.length, 0
  474. );
  475. const totalTeachers = classItem.sections.reduce((sum, section) =>
  476. sum + section.teacherAssignments.length, 0
  477. );
  478. return {
  479. id: classItem.id,
  480. name: classItem.name,
  481. code: classItem.code,
  482. description: classItem.description,
  483. isActive: classItem.isActive,
  484. period: {
  485. name: classItem.period.name,
  486. isActive: classItem.period.isActive
  487. },
  488. totalSections,
  489. totalStudents,
  490. totalTeachers,
  491. sections: classItem.sections.map(section => ({
  492. name: section.name,
  493. studentsCount: section.studentEnrollments.length,
  494. teachersCount: section.teacherAssignments.length,
  495. teachers: section.teacherAssignments.map(assignment => ({
  496. firstName: assignment.teacher.firstName,
  497. lastName: assignment.teacher.lastName
  498. }))
  499. }))
  500. };
  501. });
  502. return NextResponse.json({
  503. classes: classesWithStats,
  504. summary: {
  505. totalClasses: classes.length,
  506. totalSections: classesWithStats.reduce((sum, c) => sum + c.totalSections, 0),
  507. totalStudents: classesWithStats.reduce((sum, c) => sum + c.totalStudents, 0),
  508. averageStudentsPerClass: classes.length > 0 ? classesWithStats.reduce((sum, c) => sum + c.totalStudents, 0) / classes.length : 0
  509. }
  510. });
  511. } catch (error) {
  512. console.error('Error in classes report:', error);
  513. throw error;
  514. }
  515. }