bebo231312312321

Untitled

Jan 21st, 2026
79
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // server/src/controllers/academyMyController.js
  2.  
  3. const academyMyController = require('express').Router();
  4. const { Op } = require('sequelize');
  5.  
  6. const {
  7.   course_enrollment,
  8.   student_lesson,
  9.   student_lecture,
  10.   student_seminar,
  11.   course,
  12.   lesson,
  13.   lecture,
  14.   seminar,
  15.   module: courseModule,
  16.   certificate,
  17.   test_attempt,
  18.   student,
  19.   mentor,
  20.   user_account,
  21.   user_details,
  22.   sequelize,
  23. } = require('../sequelize/models/index');
  24.  
  25. const { validateBody, validateQuery } = require('../middlewares/validateRequest');
  26. const {
  27.   paginationSchema,
  28.   lessonProgressUpdateSchema,
  29. } = require('../schemas/academySchemas');
  30.  
  31. const isAuth = require('../middlewares/isAuth.js');
  32.  
  33. // Local schemas
  34. const { z } = require('zod');
  35.  
  36. const myCoursesQuerySchema = paginationSchema.extend({
  37.   status: z.enum(['all', 'active', 'pending', 'completed', 'dropped', 'rejected']).default('all'),
  38.   sortBy: z.enum(['recent', 'newest', 'oldest', 'progress', 'name']).default('recent'),
  39. });
  40.  
  41. const limitOnlySchema = z.object({
  42.   limit: z.coerce.number().int().min(1).max(50).default(5),
  43. });
  44.  
  45. const myLecturesQuerySchema = paginationSchema.extend({
  46.   filter: z.enum(['all', 'upcoming', 'past', 'attended']).default('all'),
  47.   sortBy: z.enum(['date', 'title']).default('date'),
  48. });
  49.  
  50. const mySeminarsQuerySchema = paginationSchema.extend({
  51.   filter: z.enum(['all', 'upcoming', 'past', 'attended', 'pending']).default('all'),
  52.   sortBy: z.enum(['date', 'title']).default('date'),
  53. });
  54.  
  55. const scheduleQuerySchema = z.object({
  56.   days: z.coerce.number().int().min(1).max(365).default(30),
  57. });
  58.  
  59. // ===============================
  60. // HELPER: Get student by userId
  61. // ===============================
  62. const getStudentByUserId = async (userId) => {
  63.   return await student.findOne({
  64.     where: { userId },
  65.   });
  66. };
  67.  
  68. // ===============================
  69. // HELPER: Get or create student
  70. // ===============================
  71. const getOrCreateStudent = async (userId) => {
  72.   let studentData = await student.findOne({
  73.     where: { userId },
  74.   });
  75.  
  76.   if (!studentData) {
  77.     studentData = await student.create({
  78.       userId,
  79.       status: 'active',
  80.     });
  81.   }
  82.  
  83.   return studentData;
  84. };
  85.  
  86. // =========================================================
  87. //                    MY STUFF - OVERVIEW
  88. // =========================================================
  89.  
  90. // ===============================
  91. // GET /api/academy/my/dashboard
  92. // Dashboard с обобщена информация
  93. // ===============================
  94. academyMyController.get('/dashboard', isAuth, async (req, res, next) => {
  95.   try {
  96.     const userId = req.user.userId;
  97.     const studentData = await getStudentByUserId(userId);
  98.  
  99.     if (!studentData) {
  100.       return res.status(200).json({
  101.         success: true,
  102.         dashboard: {
  103.           courses: { enrolled: 0, inProgress: 0, completed: 0 },
  104.           lectures: { registered: 0, attended: 0, upcoming: 0 },
  105.           seminars: { registered: 0, attended: 0, upcoming: 0 },
  106.           certificates: 0,
  107.           totalCredits: 0,
  108.         },
  109.       });
  110.     }
  111.  
  112.     const studentId = studentData.id;
  113.  
  114.     // Courses stats
  115.     const enrolledCourses = await course_enrollment.count({
  116.       where: { studentId },
  117.     });
  118.  
  119.     const inProgressCourses = await course_enrollment.count({
  120.       where: { studentId, status: 'active' },
  121.     });
  122.  
  123.     const completedCourses = await course_enrollment.count({
  124.       where: { studentId, status: 'completed' },
  125.     });
  126.  
  127.     // Lectures stats
  128.     const registeredLectures = await student_lecture.count({
  129.       where: { studentId },
  130.     });
  131.  
  132.     const attendedLectures = await student_lecture.count({
  133.       where: { studentId, attended: true },
  134.     });
  135.  
  136.     const upcomingLectures = await student_lecture.count({
  137.       where: { studentId },
  138.       include: [
  139.         {
  140.           model: lecture,
  141.           as: 'lecture',
  142.           where: {
  143.             scheduledDate: { [Op.gte]: new Date() },
  144.             status: { [Op.in]: ['scheduled', 'live'] },
  145.           },
  146.           required: true,
  147.         },
  148.       ],
  149.     });
  150.  
  151.     // Seminars stats
  152.     const registeredSeminars = await student_seminar.count({
  153.       where: { studentId, status: 'approved' },
  154.     });
  155.  
  156.     const attendedSeminars = await student_seminar.count({
  157.       where: { studentId, attended: true },
  158.     });
  159.  
  160.     const upcomingSeminars = await student_seminar.count({
  161.       where: { studentId, status: 'approved' },
  162.       include: [
  163.         {
  164.           model: seminar,
  165.           as: 'seminar',
  166.           where: {
  167.             scheduledDate: { [Op.gte]: new Date() },
  168.             status: { [Op.in]: ['scheduled', 'live'] },
  169.           },
  170.           required: true,
  171.         },
  172.       ],
  173.     });
  174.  
  175.     // Certificates
  176.     const certificatesCount = await certificate.count({
  177.       where: { studentId, status: 'active' },
  178.     });
  179.  
  180.     // Total credits
  181.     const courseCredits = await course_enrollment.sum('totalCreditsEarned', {
  182.       where: { studentId },
  183.     });
  184.  
  185.     const lectureCredits = await student_lecture.sum('earnedCredits', {
  186.       where: { studentId },
  187.     });
  188.  
  189.     const seminarCredits = await student_seminar.sum('earnedCredits', {
  190.       where: { studentId },
  191.     });
  192.  
  193.     const totalCredits =
  194.       (courseCredits || 0) + (lectureCredits || 0) + (seminarCredits || 0);
  195.  
  196.     res.status(200).json({
  197.       success: true,
  198.       dashboard: {
  199.         courses: {
  200.           enrolled: enrolledCourses,
  201.           inProgress: inProgressCourses,
  202.           completed: completedCourses,
  203.         },
  204.         lectures: {
  205.           registered: registeredLectures,
  206.           attended: attendedLectures,
  207.           upcoming: upcomingLectures,
  208.         },
  209.         seminars: {
  210.           registered: registeredSeminars,
  211.           attended: attendedSeminars,
  212.           upcoming: upcomingSeminars,
  213.         },
  214.         certificates: certificatesCount,
  215.         totalCredits,
  216.       },
  217.     });
  218.   } catch (err) {
  219.     console.error('❌ [GET MY DASHBOARD] Error:', err);
  220.     next(err);
  221.   }
  222. });
  223.  
  224. // =========================================================
  225. //                    MY COURSES
  226. // =========================================================
  227.  
  228. // ===============================
  229. // GET /api/academy/my/courses
  230. // Моите курсове
  231. // ===============================
  232. academyMyController.get(
  233.   '/courses',
  234.   isAuth,
  235.   validateQuery(myCoursesQuerySchema),
  236.   async (req, res, next) => {
  237.     try {
  238.       const userId = req.user.userId;
  239.       const { page, limit, status, sortBy } = req.query;
  240.  
  241.       const offset = (page - 1) * limit;
  242.  
  243.       const studentData = await getStudentByUserId(userId);
  244.  
  245.       if (!studentData) {
  246.         return res.status(200).json({
  247.           success: true,
  248.           courses: [],
  249.           pagination: { page, limit, total: 0, totalPages: 0 },
  250.         });
  251.       }
  252.  
  253.       const where = { studentId: studentData.id };
  254.  
  255.       if (status && status !== 'all') {
  256.         where.status = status;
  257.       }
  258.  
  259.       let order = [['lastAccessedAt', 'DESC NULLS LAST']];
  260.       switch (sortBy) {
  261.         case 'newest':
  262.           order = [['enrolledAt', 'DESC']];
  263.           break;
  264.         case 'oldest':
  265.           order = [['enrolledAt', 'ASC']];
  266.           break;
  267.         case 'progress':
  268.           order = [['progressPercentage', 'DESC']];
  269.           break;
  270.         case 'name':
  271.           order = [[{ model: course, as: 'course' }, 'name', 'ASC']];
  272.           break;
  273.       }
  274.  
  275.       const { count, rows: enrollments } = await course_enrollment.findAndCountAll({
  276.         where,
  277.         include: [
  278.           {
  279.             model: course,
  280.             as: 'course',
  281.             attributes: [
  282.               'id',
  283.               'name',
  284.               'slug',
  285.               'shortDescription',
  286.               'thumbnailUrl',
  287.               'category',
  288.               'difficultyLevel',
  289.               'totalLessons',
  290.               'estimatedHours',
  291.               'durationWeeks',
  292.             ],
  293.             include: [
  294.               {
  295.                 model: mentor,
  296.                 as: 'mentorInstances',
  297.                 attributes: ['id', 'name', 'photoUrl'],
  298.                 through: { attributes: [] },
  299.               },
  300.             ],
  301.           },
  302.         ],
  303.         limit,
  304.         offset,
  305.         order,
  306.         distinct: true,
  307.       });
  308.  
  309.       const totalPages = Math.ceil(count / limit);
  310.  
  311.       const courses = enrollments.map((e) => ({
  312.         enrollmentId: e.id,
  313.         courseId: e.courseId,
  314.         course: e.course,
  315.         status: e.status,
  316.         progressPercentage: e.progressPercentage,
  317.         completedLessons: e.completedLessons,
  318.         totalCreditsEarned: e.totalCreditsEarned,
  319.         enrolledAt: e.enrolledAt,
  320.         completedAt: e.completedAt,
  321.         lastAccessedAt: e.lastAccessedAt,
  322.         currentLessonId: e.currentLessonId,
  323.       }));
  324.  
  325.       res.status(200).json({
  326.         success: true,
  327.         courses,
  328.         pagination: {
  329.           page,
  330.           limit,
  331.           total: count,
  332.           totalPages,
  333.         },
  334.       });
  335.     } catch (err) {
  336.       console.error('❌ [GET MY COURSES] Error:', err);
  337.       next(err);
  338.     }
  339.   }
  340. );
  341.  
  342. // ===============================
  343. // GET /api/academy/my/courses/continue
  344. // Курсове за продължаване (in progress)
  345. // ===============================
  346. academyMyController.get(
  347.   '/courses/continue',
  348.   isAuth,
  349.   validateQuery(limitOnlySchema),
  350.   async (req, res, next) => {
  351.     try {
  352.       const userId = req.user.userId;
  353.       const { limit } = req.query;
  354.  
  355.       const studentData = await getStudentByUserId(userId);
  356.  
  357.       if (!studentData) {
  358.         return res.status(200).json({
  359.           success: true,
  360.           courses: [],
  361.         });
  362.       }
  363.  
  364.       const enrollments = await course_enrollment.findAll({
  365.         where: {
  366.           studentId: studentData.id,
  367.           status: 'active',
  368.           progressPercentage: { [Op.gt]: 0, [Op.lt]: 100 },
  369.         },
  370.         include: [
  371.           {
  372.             model: course,
  373.             as: 'course',
  374.             attributes: ['id', 'name', 'slug', 'thumbnailUrl', 'totalLessons'],
  375.           },
  376.           {
  377.             model: lesson,
  378.             as: 'currentLesson',
  379.             attributes: ['id', 'title', 'slug'],
  380.           },
  381.         ],
  382.         order: [['lastAccessedAt', 'DESC NULLS LAST']],
  383.         limit,
  384.       });
  385.  
  386.       res.status(200).json({
  387.         success: true,
  388.         courses: enrollments,
  389.       });
  390.     } catch (err) {
  391.       console.error('❌ [GET CONTINUE COURSES] Error:', err);
  392.       next(err);
  393.     }
  394.   }
  395. );
  396.  
  397. // =========================================================
  398. //                    MY LECTURES
  399. // =========================================================
  400.  
  401. // ===============================
  402. // GET /api/academy/my/lectures
  403. // Моите лекции
  404. // ===============================
  405. academyMyController.get(
  406.   '/lectures',
  407.   isAuth,
  408.   validateQuery(myLecturesQuerySchema),
  409.   async (req, res, next) => {
  410.     try {
  411.       const userId = req.user.userId;
  412.       const { page, limit, filter, sortBy } = req.query;
  413.  
  414.       const offset = (page - 1) * limit;
  415.  
  416.       const studentData = await getStudentByUserId(userId);
  417.  
  418.       if (!studentData) {
  419.         return res.status(200).json({
  420.           success: true,
  421.           lectures: [],
  422.           pagination: { page, limit, total: 0, totalPages: 0 },
  423.         });
  424.       }
  425.  
  426.       const where = { studentId: studentData.id };
  427.       const lectureWhere = {};
  428.  
  429.       switch (filter) {
  430.         case 'upcoming':
  431.           lectureWhere.scheduledDate = { [Op.gte]: new Date() };
  432.           lectureWhere.status = { [Op.in]: ['scheduled', 'live'] };
  433.           break;
  434.         case 'past':
  435.           lectureWhere.scheduledDate = { [Op.lt]: new Date() };
  436.           break;
  437.         case 'attended':
  438.           where.attended = true;
  439.           break;
  440.       }
  441.  
  442.       let order = [[{ model: lecture, as: 'lecture' }, 'scheduledDate', 'DESC']];
  443.       if (filter === 'upcoming') {
  444.         order = [[{ model: lecture, as: 'lecture' }, 'scheduledDate', 'ASC']];
  445.       }
  446.  
  447.       const { count, rows: registrations } = await student_lecture.findAndCountAll({
  448.         where,
  449.         include: [
  450.           {
  451.             model: lecture,
  452.             as: 'lecture',
  453.             where: Object.keys(lectureWhere).length > 0 ? lectureWhere : undefined,
  454.             attributes: [
  455.               'id',
  456.               'title',
  457.               'slug',
  458.               'shortDescription',
  459.               'thumbnailUrl',
  460.               'scheduledDate',
  461.               'scheduledEndDate',
  462.               'durationMinutes',
  463.               'status',
  464.               'isOnline',
  465.               'location',
  466.               'meetingLink',
  467.               'category',
  468.             ],
  469.             include: [
  470.               {
  471.                 model: mentor,
  472.                 as: 'lecturer',
  473.                 attributes: ['id', 'name', 'photoUrl'],
  474.               },
  475.             ],
  476.           },
  477.         ],
  478.         limit,
  479.         offset,
  480.         order,
  481.         distinct: true,
  482.       });
  483.  
  484.       const totalPages = Math.ceil(count / limit);
  485.  
  486.       const lectures = registrations.map((r) => ({
  487.         registrationId: r.id,
  488.         lectureId: r.lectureId,
  489.         lecture: r.lecture,
  490.         attended: r.attended,
  491.         attendedAt: r.attendedAt,
  492.         earnedCredits: r.earnedCredits,
  493.         registeredAt: r.createdAt,
  494.       }));
  495.  
  496.       res.status(200).json({
  497.         success: true,
  498.         lectures,
  499.         pagination: {
  500.           page,
  501.           limit,
  502.           total: count,
  503.           totalPages,
  504.         },
  505.       });
  506.     } catch (err) {
  507.       console.error('❌ [GET MY LECTURES] Error:', err);
  508.       next(err);
  509.     }
  510.   }
  511. );
  512.  
  513. // ===============================
  514. // GET /api/academy/my/lectures/upcoming
  515. // Предстоящи лекции
  516. // ===============================
  517. academyMyController.get(
  518.   '/lectures/upcoming',
  519.   isAuth,
  520.   validateQuery(limitOnlySchema),
  521.   async (req, res, next) => {
  522.     try {
  523.       const userId = req.user.userId;
  524.       const { limit } = req.query;
  525.  
  526.       const studentData = await getStudentByUserId(userId);
  527.  
  528.       if (!studentData) {
  529.         return res.status(200).json({
  530.           success: true,
  531.           lectures: [],
  532.         });
  533.       }
  534.  
  535.       const registrations = await student_lecture.findAll({
  536.         where: { studentId: studentData.id },
  537.         include: [
  538.           {
  539.             model: lecture,
  540.             as: 'lecture',
  541.             where: {
  542.               scheduledDate: { [Op.gte]: new Date() },
  543.               status: { [Op.in]: ['scheduled', 'live'] },
  544.             },
  545.             attributes: [
  546.               'id',
  547.               'title',
  548.               'slug',
  549.               'thumbnailUrl',
  550.               'scheduledDate',
  551.               'durationMinutes',
  552.               'isOnline',
  553.               'meetingLink',
  554.               'status',
  555.             ],
  556.             include: [
  557.               {
  558.                 model: mentor,
  559.                 as: 'lecturer',
  560.                 attributes: ['id', 'name', 'photoUrl'],
  561.               },
  562.             ],
  563.           },
  564.         ],
  565.         order: [[{ model: lecture, as: 'lecture' }, 'scheduledDate', 'ASC']],
  566.         limit,
  567.       });
  568.  
  569.       res.status(200).json({
  570.         success: true,
  571.         lectures: registrations,
  572.       });
  573.     } catch (err) {
  574.       console.error('❌ [GET UPCOMING LECTURES] Error:', err);
  575.       next(err);
  576.     }
  577.   }
  578. );
  579.  
  580. // =========================================================
  581. //                    MY SEMINARS
  582. // =========================================================
  583.  
  584. // ===============================
  585. // GET /api/academy/my/seminars
  586. // Моите семинари
  587. // ===============================
  588. academyMyController.get(
  589.   '/seminars',
  590.   isAuth,
  591.   validateQuery(mySeminarsQuerySchema),
  592.   async (req, res, next) => {
  593.     try {
  594.       const userId = req.user.userId;
  595.       const { page, limit, filter, sortBy } = req.query;
  596.  
  597.       const offset = (page - 1) * limit;
  598.  
  599.       const studentData = await getStudentByUserId(userId);
  600.  
  601.       if (!studentData) {
  602.         return res.status(200).json({
  603.           success: true,
  604.           seminars: [],
  605.           pagination: { page, limit, total: 0, totalPages: 0 },
  606.         });
  607.       }
  608.  
  609.       const where = { studentId: studentData.id };
  610.       const seminarWhere = {};
  611.  
  612.       switch (filter) {
  613.         case 'upcoming':
  614.           seminarWhere.scheduledDate = { [Op.gte]: new Date() };
  615.           seminarWhere.status = { [Op.in]: ['scheduled', 'live'] };
  616.           where.status = 'approved';
  617.           break;
  618.         case 'past':
  619.           seminarWhere.scheduledDate = { [Op.lt]: new Date() };
  620.           break;
  621.         case 'attended':
  622.           where.attended = true;
  623.           break;
  624.         case 'pending':
  625.           where.status = 'pending';
  626.           break;
  627.       }
  628.  
  629.       let order = [[{ model: seminar, as: 'seminar' }, 'scheduledDate', 'DESC']];
  630.       if (filter === 'upcoming') {
  631.         order = [[{ model: seminar, as: 'seminar' }, 'scheduledDate', 'ASC']];
  632.       }
  633.  
  634.       const { count, rows: registrations } = await student_seminar.findAndCountAll({
  635.         where,
  636.         include: [
  637.           {
  638.             model: seminar,
  639.             as: 'seminar',
  640.             where: Object.keys(seminarWhere).length > 0 ? seminarWhere : undefined,
  641.             attributes: [
  642.               'id',
  643.               'title',
  644.               'slug',
  645.               'shortDescription',
  646.               'thumbnailUrl',
  647.               'scheduledDate',
  648.               'scheduledEndDate',
  649.               'durationMinutes',
  650.               'status',
  651.               'seminarType',
  652.               'isOnline',
  653.               'location',
  654.               'meetingLink',
  655.               'category',
  656.             ],
  657.             include: [
  658.               {
  659.                 model: mentor,
  660.                 as: 'facilitator',
  661.                 attributes: ['id', 'name', 'photoUrl'],
  662.               },
  663.             ],
  664.           },
  665.         ],
  666.         limit,
  667.         offset,
  668.         order,
  669.         distinct: true,
  670.       });
  671.  
  672.       const totalPages = Math.ceil(count / limit);
  673.  
  674.       const seminars = registrations.map((r) => ({
  675.         registrationId: r.id,
  676.         seminarId: r.seminarId,
  677.         seminar: r.seminar,
  678.         status: r.status,
  679.         attended: r.attended,
  680.         attendedAt: r.attendedAt,
  681.         participationLevel: r.participationLevel,
  682.         earnedCredits: r.earnedCredits,
  683.         registeredAt: r.createdAt,
  684.       }));
  685.  
  686.       res.status(200).json({
  687.         success: true,
  688.         seminars,
  689.         pagination: {
  690.           page,
  691.           limit,
  692.           total: count,
  693.           totalPages,
  694.         },
  695.       });
  696.     } catch (err) {
  697.       console.error('❌ [GET MY SEMINARS] Error:', err);
  698.       next(err);
  699.     }
  700.   }
  701. );
  702.  
  703. // ===============================
  704. // GET /api/academy/my/seminars/upcoming
  705. // Предстоящи семинари
  706. // ===============================
  707. academyMyController.get(
  708.   '/seminars/upcoming',
  709.   isAuth,
  710.   validateQuery(limitOnlySchema),
  711.   async (req, res, next) => {
  712.     try {
  713.       const userId = req.user.userId;
  714.       const { limit } = req.query;
  715.  
  716.       const studentData = await getStudentByUserId(userId);
  717.  
  718.       if (!studentData) {
  719.         return res.status(200).json({
  720.           success: true,
  721.           seminars: [],
  722.         });
  723.       }
  724.  
  725.       const registrations = await student_seminar.findAll({
  726.         where: {
  727.           studentId: studentData.id,
  728.           status: 'approved',
  729.         },
  730.         include: [
  731.           {
  732.             model: seminar,
  733.             as: 'seminar',
  734.             where: {
  735.               scheduledDate: { [Op.gte]: new Date() },
  736.               status: { [Op.in]: ['scheduled', 'live'] },
  737.             },
  738.             attributes: [
  739.               'id',
  740.               'title',
  741.               'slug',
  742.               'thumbnailUrl',
  743.               'scheduledDate',
  744.               'durationMinutes',
  745.               'seminarType',
  746.               'isOnline',
  747.               'location',
  748.               'meetingLink',
  749.               'status',
  750.             ],
  751.             include: [
  752.               {
  753.                 model: mentor,
  754.                 as: 'facilitator',
  755.                 attributes: ['id', 'name', 'photoUrl'],
  756.               },
  757.             ],
  758.           },
  759.         ],
  760.         order: [[{ model: seminar, as: 'seminar' }, 'scheduledDate', 'ASC']],
  761.         limit,
  762.       });
  763.  
  764.       res.status(200).json({
  765.         success: true,
  766.         seminars: registrations,
  767.       });
  768.     } catch (err) {
  769.       console.error('❌ [GET UPCOMING SEMINARS] Error:', err);
  770.       next(err);
  771.     }
  772.   }
  773. );
  774.  
  775. // =========================================================
  776. //                    MY CERTIFICATES
  777. // =========================================================
  778.  
  779. // ===============================
  780. // GET /api/academy/my/certificates
  781. // Моите сертификати
  782. // ===============================
  783. academyMyController.get(
  784.   '/certificates',
  785.   isAuth,
  786.   validateQuery(paginationSchema),
  787.   async (req, res, next) => {
  788.     try {
  789.       const userId = req.user.userId;
  790.       const { page, limit } = req.query;
  791.  
  792.       const offset = (page - 1) * limit;
  793.  
  794.       const studentData = await getStudentByUserId(userId);
  795.  
  796.       if (!studentData) {
  797.         return res.status(200).json({
  798.           success: true,
  799.           certificates: [],
  800.           pagination: { page, limit, total: 0, totalPages: 0 },
  801.         });
  802.       }
  803.  
  804.       const { count, rows: certificates } = await certificate.findAndCountAll({
  805.         where: {
  806.           studentId: studentData.id,
  807.           status: 'active',
  808.         },
  809.         include: [
  810.           {
  811.             model: course,
  812.             as: 'course',
  813.             attributes: ['id', 'name', 'slug', 'thumbnailUrl', 'category', 'difficultyLevel'],
  814.           },
  815.         ],
  816.         limit,
  817.         offset,
  818.         order: [['issuedAt', 'DESC']],
  819.         distinct: true,
  820.       });
  821.  
  822.       const totalPages = Math.ceil(count / limit);
  823.  
  824.       res.status(200).json({
  825.         success: true,
  826.         certificates,
  827.         pagination: {
  828.           page,
  829.           limit,
  830.           total: count,
  831.           totalPages,
  832.         },
  833.       });
  834.     } catch (err) {
  835.       console.error('❌ [GET MY CERTIFICATES] Error:', err);
  836.       next(err);
  837.     }
  838.   }
  839. );
  840.  
  841. // =========================================================
  842. //                    MY PROGRESS
  843. // =========================================================
  844.  
  845. // ===============================
  846. // GET /api/academy/my/progress
  847. // Общ прогрес
  848. // ===============================
  849. academyMyController.get('/progress', isAuth, async (req, res, next) => {
  850.   try {
  851.     const userId = req.user.userId;
  852.     const studentData = await getStudentByUserId(userId);
  853.  
  854.     if (!studentData) {
  855.       return res.status(200).json({
  856.         success: true,
  857.         progress: {
  858.           courses: [],
  859.           recentActivity: [],
  860.           stats: {
  861.             totalCoursesStarted: 0,
  862.             totalCoursesCompleted: 0,
  863.             totalLessonsCompleted: 0,
  864.             totalTimeSpent: 0,
  865.             totalCredits: 0,
  866.             averageScore: 0,
  867.           },
  868.         },
  869.       });
  870.     }
  871.  
  872.     const studentId = studentData.id;
  873.  
  874.     const enrollments = await course_enrollment.findAll({
  875.       where: { studentId },
  876.       include: [
  877.         {
  878.           model: course,
  879.           as: 'course',
  880.           attributes: ['id', 'name', 'slug', 'totalLessons'],
  881.         },
  882.       ],
  883.       order: [['lastAccessedAt', 'DESC NULLS LAST']],
  884.     });
  885.  
  886.     const recentLessons = await student_lesson.findAll({
  887.       where: { studentId },
  888.       include: [
  889.         {
  890.           model: lesson,
  891.           as: 'lesson',
  892.           attributes: ['id', 'title', 'slug', 'courseId'],
  893.           include: [
  894.             {
  895.               model: course,
  896.               as: 'course',
  897.               attributes: ['id', 'name', 'slug'],
  898.             },
  899.           ],
  900.         },
  901.       ],
  902.       order: [['lastAccessedAt', 'DESC']],
  903.       limit: 10,
  904.     });
  905.  
  906.     const totalCoursesStarted = enrollments.length;
  907.     const totalCoursesCompleted = enrollments.filter((e) => e.status === 'completed').length;
  908.  
  909.     const totalLessonsCompleted = await student_lesson.count({
  910.       where: { studentId, status: 'completed' },
  911.     });
  912.  
  913.     const totalTimeSpent = await student_lesson.sum('timeSpentMinutes', {
  914.       where: { studentId },
  915.     });
  916.  
  917.     const courseCredits = await course_enrollment.sum('totalCreditsEarned', {
  918.       where: { studentId },
  919.     });
  920.     const lectureCredits = await student_lecture.sum('earnedCredits', {
  921.       where: { studentId },
  922.     });
  923.     const seminarCredits = await student_seminar.sum('earnedCredits', {
  924.       where: { studentId },
  925.     });
  926.     const totalCredits = (courseCredits || 0) + (lectureCredits || 0) + (seminarCredits || 0);
  927.  
  928.     const testAttempts = await test_attempt.findAll({
  929.       where: { studentId, status: 'completed' },
  930.       attributes: [[sequelize.fn('AVG', sequelize.col('score_percentage')), 'avgScore']],
  931.       raw: true,
  932.     });
  933.     const averageScore = Math.round(parseFloat(testAttempts[0]?.avgScore) || 0);
  934.  
  935.     res.status(200).json({
  936.       success: true,
  937.       progress: {
  938.         courses: enrollments.map((e) => ({
  939.           courseId: e.courseId,
  940.           course: e.course,
  941.           status: e.status,
  942.           progressPercentage: e.progressPercentage,
  943.           completedLessons: e.completedLessons,
  944.           totalLessons: e.course?.totalLessons,
  945.           enrolledAt: e.enrolledAt,
  946.           completedAt: e.completedAt,
  947.           lastAccessedAt: e.lastAccessedAt,
  948.         })),
  949.         recentActivity: recentLessons.map((l) => ({
  950.           lessonId: l.lessonId,
  951.           lesson: l.lesson,
  952.           status: l.status,
  953.           progressPercentage: l.progressPercentage,
  954.           lastAccessedAt: l.lastAccessedAt,
  955.         })),
  956.         stats: {
  957.           totalCoursesStarted,
  958.           totalCoursesCompleted,
  959.           totalLessonsCompleted,
  960.           totalTimeSpent: totalTimeSpent || 0,
  961.           totalCredits,
  962.           averageScore,
  963.         },
  964.       },
  965.     });
  966.   } catch (err) {
  967.     console.error('❌ [GET MY PROGRESS] Error:', err);
  968.     next(err);
  969.   }
  970. });
  971.  
  972. // ===============================
  973. // GET /api/academy/my/progress/course/:courseId
  974. // Прогрес по курс
  975. // ===============================
  976. academyMyController.get('/progress/course/:courseId', isAuth, async (req, res, next) => {
  977.   try {
  978.     const userId = req.user.userId;
  979.     const courseId = parseInt(req.params.courseId);
  980.  
  981.     const studentData = await getStudentByUserId(userId);
  982.  
  983.     if (!studentData) {
  984.       return res.status(404).json({
  985.         success: false,
  986.         message: 'Student profile not found',
  987.       });
  988.     }
  989.  
  990.     const enrollment = await course_enrollment.findOne({
  991.       where: {
  992.         studentId: studentData.id,
  993.         courseId,
  994.       },
  995.       include: [
  996.         {
  997.           model: course,
  998.           as: 'course',
  999.           include: [
  1000.             {
  1001.               model: courseModule,
  1002.               as: 'modules',
  1003.               include: [
  1004.                 {
  1005.                   model: lesson,
  1006.                   as: 'lessons',
  1007.                   where: { isPublished: true },
  1008.                   required: false,
  1009.                   attributes: ['id', 'title', 'slug', 'durationMinutes', 'lessonType'],
  1010.                   order: [['sortOrder', 'ASC']],
  1011.                 },
  1012.               ],
  1013.               order: [['sortOrder', 'ASC']],
  1014.             },
  1015.             {
  1016.               model: lesson,
  1017.               as: 'lessons',
  1018.               where: { isPublished: true, moduleId: null },
  1019.               required: false,
  1020.               attributes: ['id', 'title', 'slug', 'durationMinutes', 'lessonType'],
  1021.               order: [['sortOrder', 'ASC']],
  1022.             },
  1023.           ],
  1024.         },
  1025.       ],
  1026.     });
  1027.  
  1028.     if (!enrollment) {
  1029.       return res.status(404).json({
  1030.         success: false,
  1031.         message: 'Enrollment not found',
  1032.       });
  1033.     }
  1034.  
  1035.     const lessonProgress = await student_lesson.findAll({
  1036.       where: {
  1037.         studentId: studentData.id,
  1038.         enrollmentId: enrollment.id,
  1039.       },
  1040.     });
  1041.  
  1042.     const progressMap = {};
  1043.     lessonProgress.forEach((lp) => {
  1044.       progressMap[lp.lessonId] = {
  1045.         status: lp.status,
  1046.         progressPercentage: lp.progressPercentage,
  1047.         completedAt: lp.completedAt,
  1048.         timeSpentMinutes: lp.timeSpentMinutes,
  1049.         testPassed: lp.testPassed,
  1050.         testScore: lp.testScore,
  1051.       };
  1052.     });
  1053.  
  1054.     const courseData = enrollment.course.get({ plain: true });
  1055.  
  1056.     if (courseData.modules) {
  1057.       courseData.modules = courseData.modules.map((mod) => ({
  1058.         ...mod,
  1059.         lessons: mod.lessons?.map((les) => ({
  1060.           ...les,
  1061.           progress: progressMap[les.id] || { status: 'not_started', progressPercentage: 0 },
  1062.         })),
  1063.       }));
  1064.     }
  1065.  
  1066.     if (courseData.lessons) {
  1067.       courseData.lessons = courseData.lessons.map((les) => ({
  1068.         ...les,
  1069.         progress: progressMap[les.id] || { status: 'not_started', progressPercentage: 0 },
  1070.       }));
  1071.     }
  1072.  
  1073.     res.status(200).json({
  1074.       success: true,
  1075.       enrollment: {
  1076.         id: enrollment.id,
  1077.         status: enrollment.status,
  1078.         progressPercentage: enrollment.progressPercentage,
  1079.         completedLessons: enrollment.completedLessons,
  1080.         totalCreditsEarned: enrollment.totalCreditsEarned,
  1081.         enrolledAt: enrollment.enrolledAt,
  1082.         completedAt: enrollment.completedAt,
  1083.         lastAccessedAt: enrollment.lastAccessedAt,
  1084.         currentLessonId: enrollment.currentLessonId,
  1085.       },
  1086.       course: courseData,
  1087.     });
  1088.   } catch (err) {
  1089.     console.error('❌ [GET COURSE PROGRESS] Error:', err);
  1090.     next(err);
  1091.   }
  1092. });
  1093.  
  1094. // ===============================
  1095. // POST /api/academy/my/progress/lesson/:lessonId
  1096. // Обновяване на прогрес по урок
  1097. // ===============================
  1098. academyMyController.post(
  1099.   '/progress/lesson/:lessonId',
  1100.   isAuth,
  1101.   validateBody(lessonProgressUpdateSchema),
  1102.   async (req, res, next) => {
  1103.     try {
  1104.       const userId = req.user.userId;
  1105.       const lessonId = parseInt(req.params.lessonId);
  1106.       const { watchedSeconds, progressPercentage, videoPosition } = req.body;
  1107.  
  1108.       const studentData = await getOrCreateStudent(userId);
  1109.  
  1110.       const lessonData = await lesson.findByPk(lessonId, {
  1111.         include: [{ model: course, as: 'course' }],
  1112.       });
  1113.  
  1114.       if (!lessonData) {
  1115.         return res.status(404).json({
  1116.           success: false,
  1117.           message: 'Lesson not found',
  1118.         });
  1119.       }
  1120.  
  1121.       let enrollment = null;
  1122.       if (!lessonData.isFree) {
  1123.         enrollment = await course_enrollment.findOne({
  1124.           where: {
  1125.             studentId: studentData.id,
  1126.             courseId: lessonData.courseId,
  1127.             status: 'active',
  1128.           },
  1129.         });
  1130.  
  1131.         if (!enrollment) {
  1132.           return res.status(403).json({
  1133.             success: false,
  1134.             message: 'Must be enrolled in course',
  1135.           });
  1136.         }
  1137.       }
  1138.  
  1139.       let progress = await student_lesson.findOne({
  1140.         where: {
  1141.           studentId: studentData.id,
  1142.           lessonId,
  1143.         },
  1144.       });
  1145.  
  1146.       if (!progress) {
  1147.         progress = await student_lesson.create({
  1148.           studentId: studentData.id,
  1149.           lessonId,
  1150.           enrollmentId: enrollment?.id || null,
  1151.           status: 'in_progress',
  1152.           startedAt: new Date(),
  1153.           progressPercentage: 0,
  1154.           timeSpentMinutes: 0,
  1155.         });
  1156.       }
  1157.  
  1158.       const updates = {
  1159.         lastAccessedAt: new Date(),
  1160.       };
  1161.  
  1162.       if (progressPercentage !== undefined) {
  1163.         updates.progressPercentage = Math.min(100, Math.max(progress.progressPercentage, progressPercentage));
  1164.       }
  1165.  
  1166.       if (watchedSeconds !== undefined) {
  1167.         const additionalMinutes = Math.floor(watchedSeconds / 60);
  1168.         updates.timeSpentMinutes = (progress.timeSpentMinutes || 0) + additionalMinutes;
  1169.       }
  1170.  
  1171.       if (videoPosition !== undefined) {
  1172.         updates.videoPosition = videoPosition;
  1173.       }
  1174.  
  1175.       if (progress.status === 'not_started') {
  1176.         updates.status = 'in_progress';
  1177.         updates.startedAt = new Date();
  1178.       }
  1179.  
  1180.       await progress.update(updates);
  1181.  
  1182.       if (enrollment) {
  1183.         await enrollment.update({
  1184.           lastAccessedAt: new Date(),
  1185.           currentLessonId: lessonId,
  1186.         });
  1187.       }
  1188.  
  1189.       res.status(200).json({
  1190.         success: true,
  1191.         message: 'Progress updated',
  1192.         progress,
  1193.       });
  1194.     } catch (err) {
  1195.       console.error('❌ [UPDATE LESSON PROGRESS] Error:', err);
  1196.       next(err);
  1197.     }
  1198.   }
  1199. );
  1200.  
  1201. // ===============================
  1202. // POST /api/academy/my/progress/lesson/:lessonId/complete
  1203. // Маркиране като завършен
  1204. // ===============================
  1205. academyMyController.post('/progress/lesson/:lessonId/complete', isAuth, async (req, res, next) => {
  1206.   try {
  1207.     const userId = req.user.userId;
  1208.     const lessonId = parseInt(req.params.lessonId);
  1209.  
  1210.     const studentData = await getStudentByUserId(userId);
  1211.  
  1212.     if (!studentData) {
  1213.       return res.status(404).json({
  1214.         success: false,
  1215.         message: 'Student profile not found',
  1216.       });
  1217.     }
  1218.  
  1219.     const lessonData = await lesson.findByPk(lessonId);
  1220.  
  1221.     if (!lessonData) {
  1222.       return res.status(404).json({
  1223.         success: false,
  1224.         message: 'Lesson not found',
  1225.       });
  1226.     }
  1227.  
  1228.     const progress = await student_lesson.findOne({
  1229.       where: {
  1230.         studentId: studentData.id,
  1231.         lessonId,
  1232.       },
  1233.     });
  1234.  
  1235.     if (!progress) {
  1236.       return res.status(404).json({
  1237.         success: false,
  1238.         message: 'Start the lesson first',
  1239.       });
  1240.     }
  1241.  
  1242.     if (progress.status === 'completed') {
  1243.       return res.status(200).json({
  1244.         success: true,
  1245.         message: 'Lesson already completed',
  1246.         progress,
  1247.       });
  1248.     }
  1249.  
  1250.     if (lessonData.hasTest && !progress.testPassed) {
  1251.       return res.status(400).json({
  1252.         success: false,
  1253.         message: 'Must pass the test first',
  1254.       });
  1255.     }
  1256.  
  1257.     let earnedCredits = lessonData.creditsForCompletion || 0;
  1258.     if (progress.testPassed && lessonData.creditsForTest) {
  1259.       earnedCredits += lessonData.creditsForTest;
  1260.     }
  1261.  
  1262.     await progress.update({
  1263.       status: 'completed',
  1264.       completedAt: new Date(),
  1265.       progressPercentage: 100,
  1266.       earnedCredits,
  1267.     });
  1268.  
  1269.     if (progress.enrollmentId) {
  1270.       const enrollment = await course_enrollment.findByPk(progress.enrollmentId);
  1271.  
  1272.       if (enrollment) {
  1273.         const completedCount = await student_lesson.count({
  1274.           where: {
  1275.             enrollmentId: enrollment.id,
  1276.             status: 'completed',
  1277.           },
  1278.         });
  1279.  
  1280.         const totalLessons = await lesson.count({
  1281.           where: { courseId: lessonData.courseId, isPublished: true },
  1282.         });
  1283.  
  1284.         const newProgress = totalLessons > 0 ? Math.round((completedCount / totalLessons) * 100) : 0;
  1285.  
  1286.         await enrollment.update({
  1287.           completedLessons: completedCount,
  1288.           progressPercentage: newProgress,
  1289.           totalCreditsEarned: (enrollment.totalCreditsEarned || 0) + earnedCredits,
  1290.         });
  1291.  
  1292.         if (newProgress >= 100) {
  1293.           await enrollment.update({
  1294.             status: 'completed',
  1295.             completedAt: new Date(),
  1296.           });
  1297.  
  1298.           await course.increment('completedCount', {
  1299.             where: { id: lessonData.courseId },
  1300.           });
  1301.         }
  1302.       }
  1303.     }
  1304.  
  1305.     res.status(200).json({
  1306.       success: true,
  1307.       message: 'Lesson completed',
  1308.       progress,
  1309.       earnedCredits,
  1310.     });
  1311.   } catch (err) {
  1312.     console.error('❌ [COMPLETE LESSON] Error:', err);
  1313.     next(err);
  1314.   }
  1315. });
  1316.  
  1317. // =========================================================
  1318. //                    CALENDAR / SCHEDULE
  1319. // =========================================================
  1320.  
  1321. // ===============================
  1322. // GET /api/academy/my/schedule
  1323. // Моят график (предстоящи събития)
  1324. // ===============================
  1325. academyMyController.get(
  1326.   '/schedule',
  1327.   isAuth,
  1328.   validateQuery(scheduleQuerySchema),
  1329.   async (req, res, next) => {
  1330.     try {
  1331.       const userId = req.user.userId;
  1332.       const { days } = req.query;
  1333.  
  1334.       const studentData = await getStudentByUserId(userId);
  1335.  
  1336.       if (!studentData) {
  1337.         return res.status(200).json({
  1338.           success: true,
  1339.           schedule: [],
  1340.         });
  1341.       }
  1342.  
  1343.       const endDate = new Date();
  1344.       endDate.setDate(endDate.getDate() + days);
  1345.  
  1346.       const upcomingLectures = await student_lecture.findAll({
  1347.         where: { studentId: studentData.id },
  1348.         include: [
  1349.           {
  1350.             model: lecture,
  1351.             as: 'lecture',
  1352.             where: {
  1353.               scheduledDate: {
  1354.                 [Op.gte]: new Date(),
  1355.                 [Op.lte]: endDate,
  1356.               },
  1357.               status: { [Op.in]: ['scheduled', 'live'] },
  1358.             },
  1359.             attributes: ['id', 'title', 'slug', 'scheduledDate', 'durationMinutes', 'isOnline', 'meetingLink'],
  1360.           },
  1361.         ],
  1362.       });
  1363.  
  1364.       const upcomingSeminars = await student_seminar.findAll({
  1365.         where: {
  1366.           studentId: studentData.id,
  1367.           status: 'approved',
  1368.         },
  1369.         include: [
  1370.           {
  1371.             model: seminar,
  1372.             as: 'seminar',
  1373.             where: {
  1374.               scheduledDate: {
  1375.                 [Op.gte]: new Date(),
  1376.                 [Op.lte]: endDate,
  1377.               },
  1378.               status: { [Op.in]: ['scheduled', 'live'] },
  1379.             },
  1380.             attributes: ['id', 'title', 'slug', 'scheduledDate', 'durationMinutes', 'isOnline', 'location', 'meetingLink'],
  1381.           },
  1382.         ],
  1383.       });
  1384.  
  1385.       const schedule = [
  1386.         ...upcomingLectures.map((l) => ({
  1387.           type: 'lecture',
  1388.           id: l.lecture.id,
  1389.           title: l.lecture.title,
  1390.           slug: l.lecture.slug,
  1391.           date: l.lecture.scheduledDate,
  1392.           durationMinutes: l.lecture.durationMinutes,
  1393.           isOnline: l.lecture.isOnline,
  1394.           meetingLink: l.lecture.meetingLink,
  1395.         })),
  1396.         ...upcomingSeminars.map((s) => ({
  1397.           type: 'seminar',
  1398.           id: s.seminar.id,
  1399.           title: s.seminar.title,
  1400.           slug: s.seminar.slug,
  1401.           date: s.seminar.scheduledDate,
  1402.           durationMinutes: s.seminar.durationMinutes,
  1403.           isOnline: s.seminar.isOnline,
  1404.           location: s.seminar.location,
  1405.           meetingLink: s.seminar.meetingLink,
  1406.         })),
  1407.       ].sort((a, b) => new Date(a.date) - new Date(b.date));
  1408.  
  1409.       res.status(200).json({
  1410.         success: true,
  1411.         schedule,
  1412.       });
  1413.     } catch (err) {
  1414.       console.error('❌ [GET MY SCHEDULE] Error:', err);
  1415.       next(err);
  1416.     }
  1417.   }
  1418. );
  1419.  
  1420. module.exports = academyMyController;
Advertisement
Add Comment
Please, Sign In to add comment