bebo231312312321

Untitled

Jan 21st, 2026
102
0
Never
3
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1.  
  2. const academyController = require('express').Router();
  3. const { Op } = require('sequelize');
  4.  
  5. const { mentor_application, mentor, mentor_course, user_account, admin_notification, sequelize, user_notification, student, user_details } = require('../sequelize/models/index');
  6. const { getFirebaseDb } = require('../firebase/firebaseAdmin');
  7. const isAuth = require('../middlewares/isAuth.js');
  8. const rbac = require('../middlewares/rbac.js');
  9. const { statisticsCache } = require('../middlewares/statisticsCache');
  10. const { mentorApplicationSchema } = require('../schemas/mentorApplication.schema');
  11. const { initializeFirebaseAdmin } = require('../firebase/firebaseAdmin');
  12. const {
  13.   getMentorCombinedStats,
  14.   getAllMentorsCombinedStats,
  15.   updateMentorCachedStats
  16. } = require('../services/mentorActivityService');
  17.  
  18. initializeFirebaseAdmin();
  19. // Директно създаване на ментор (БЕЗ кандидатура)
  20. // ===============================
  21. academyController.post('/mentors', isAuth, rbac.checkPermission('mentor', 'create'), async (req, res, next) => {
  22.  
  23.   try {
  24.     const {
  25.       userId,
  26.       name,
  27.       email,
  28.       phone,
  29.       age,
  30.       country,
  31.       photoUrl,
  32.       specialization,
  33.       education,
  34.       experience,
  35.       motivation,
  36.       availability,
  37.       languages,
  38.       viber,
  39.       facebook,
  40.       linkedin,
  41.       otherContact,
  42.       priorityContact,
  43.       cvUrl,
  44.       cvOriginalName,
  45.       adminNotes,
  46.     } = req.body;
  47.  
  48.     // Валидация
  49.     if (!userId || !name || !email || !phone || !age) {
  50.       return res.status(400).json({
  51.         message: 'Missing required fields: userId, name, email, phone, age'
  52.       });
  53.     }
  54.  
  55.     const user = await user_account.findByPk(userId);
  56.     if (!user) {
  57.       return res.status(404).json({ message: 'User not found.' });
  58.     }
  59.  
  60.     const existingMentor = await mentor.findOne({
  61.       where: { userId }
  62.     });
  63.  
  64.     if (existingMentor) {
  65.       return res.status(400).json({
  66.         message: 'User is already a mentor.'
  67.       });
  68.     }
  69.  
  70.     const mentorData = {
  71.       userId,
  72.       applicationId: null,
  73.       name,
  74.       email,
  75.       phone,
  76.       age,
  77.       country: country || 'BG',
  78.       photoUrl: photoUrl || null,
  79.       specialization: specialization || null,
  80.       education: education || null,
  81.       experience: experience || null,
  82.       motivation: motivation || null,
  83.       availability: availability || null,
  84.       languages: languages || [],
  85.       viber: viber || null,
  86.       facebook: facebook || null,
  87.       linkedin: linkedin || null,
  88.       otherContact: otherContact || null,
  89.       priorityContact: priorityContact || 'email',
  90.       cvUrl: cvUrl || null,
  91.       cvOriginalName: cvOriginalName || null,
  92.       adminNotes: adminNotes || null,
  93.       approvedAt: new Date(),
  94.       status: 'active',
  95.     };
  96.  
  97.     const newMentor = await mentor.create(mentorData);
  98.  
  99.     if (user.role === 'user' || user.role === 'guest') {
  100.       await user_account.update(
  101.         {
  102.           role: 'mentor',
  103.           isMentor: true
  104.         },
  105.         { where: { id: userId } }
  106.       );
  107.     }
  108.  
  109.     res.status(201).json({
  110.       success: true,
  111.       message: 'Mentor created successfully!',
  112.       mentor: newMentor,
  113.     });
  114.  
  115.   } catch (err) {
  116.     console.error('❌ [CREATE MENTOR] Error:', err);
  117.     next(err);
  118.   }
  119. });
  120.  
  121. // ===============================
  122. // GET /api/academy/mentors
  123. // Преглед на всички ментори с filtering
  124. // ===============================
  125. academyController.get('/mentors', async (req, res, next) => {
  126.  
  127.   try {
  128.     const {
  129.       page = 1,
  130.       limit = 12,
  131.       search = '',
  132.       specialization = '',
  133.       status = 'all',
  134.       sortBy = 'newest',
  135.     } = req.query;
  136.  
  137.     const pageNum = parseInt(page);
  138.     const limitNum = parseInt(limit);
  139.     const offset = (pageNum - 1) * limitNum;
  140.  
  141.     // Build WHERE clause
  142.     const where = {};
  143.  
  144.     // Filter by status
  145.     if (status && status !== 'all') {
  146.       where.status = status;
  147.     }
  148.  
  149.     // Filter by specialization
  150.     if (specialization && specialization !== 'all') {
  151.       where.specialization = specialization;
  152.     }
  153.  
  154.     // Search by name or email
  155.     if (search) {
  156.       where[Op.or] = [
  157.         { name: { [Op.iLike]: `%${search}%` } },
  158.         { email: { [Op.iLike]: `%${search}%` } },
  159.       ];
  160.     }
  161.  
  162.     // Sort order
  163.     let order = [['createdAt', 'DESC']];
  164.  
  165.     switch (sortBy) {
  166.       case 'oldest':
  167.         order = [['createdAt', 'ASC']];
  168.         break;
  169.       case 'name':
  170.         order = [['name', 'ASC']];
  171.         break;
  172.       case 'rating':
  173.         order = [['rating', 'DESC']];
  174.         break;
  175.       case 'students':
  176.         order = [['studentsCount', 'DESC']];
  177.         break;
  178.       default:
  179.         order = [['createdAt', 'DESC']];
  180.     }
  181.  
  182.     const { count, rows: mentors } = await mentor.findAndCountAll({
  183.       where,
  184.       include: [
  185.         {
  186.           model: user_account,
  187.           as: 'user',
  188.           attributes: ['id', 'email', 'role'],
  189.         },
  190.         {
  191.           model: mentor_course,
  192.           as: 'courses',
  193.           attributes: ['id', 'courseName', 'courseCategory', 'enrolledStudents', 'status'],
  194.         },
  195.       ],
  196.       limit: limitNum,
  197.       offset,
  198.       order,
  199.       distinct: true,
  200.     });
  201.  
  202.     const totalPages = Math.ceil(count / limitNum);
  203.  
  204.     res.status(200).json({
  205.       success: true,
  206.       mentors,
  207.       pagination: {
  208.         page: pageNum,
  209.         limit: limitNum,
  210.         total: count,
  211.         totalPages,
  212.       },
  213.     });
  214.  
  215.   } catch (err) {
  216.     console.error('❌ [GET MENTORS] Error:', err);
  217.     next(err);
  218.   }
  219. });
  220.  
  221. // ===============================
  222. // PATCH /api/academy/mentors/:id
  223. // Редактиране на ментор
  224. // ===============================
  225. academyController.patch('/mentors/:id', isAuth, rbac.checkPermission('mentor', 'update'), async (req, res, next) => {
  226.  
  227.   try {
  228.     const mentorId = parseInt(req.params.id);
  229.     const updates = req.body;
  230.  
  231.     const mentorData = await mentor.findByPk(mentorId);
  232.  
  233.     if (!mentorData) {
  234.       return res.status(404).json({ message: 'Mentor not found.' });
  235.     }
  236.  
  237.     // Allowed fields to update
  238.     const allowedFields = [
  239.       'name',
  240.       'email',
  241.       'phone',
  242.       'age',
  243.       'country',
  244.       'photoUrl',
  245.       'specialization',
  246.       'education',
  247.       'experience',
  248.       'motivation',
  249.       'availability',
  250.       'languages',
  251.       'viber',
  252.       'facebook',
  253.       'linkedin',
  254.       'otherContact',
  255.       'priorityContact',
  256.       'cvUrl',
  257.       'cvOriginalName',
  258.       'isOnline',
  259.       'adminNotes',
  260.       'status',
  261.     ];
  262.  
  263.     // Filter only allowed fields
  264.     const filteredUpdates = {};
  265.     allowedFields.forEach(field => {
  266.       if (updates[field] !== undefined) {
  267.         filteredUpdates[field] = updates[field];
  268.       }
  269.     });
  270.  
  271.     // Update mentor
  272.     await mentorData.update(filteredUpdates);
  273.  
  274.     res.status(200).json({
  275.       success: true,
  276.       message: 'Mentor updated successfully!',
  277.       mentor: mentorData,
  278.     });
  279.  
  280.   } catch (err) {
  281.     console.error('❌ [UPDATE MENTOR] Error:', err);
  282.     next(err);
  283.   }
  284. });
  285.  
  286. // ===============================
  287. // DELETE /api/academy/mentors/:id
  288. // Изтриване на ментор
  289. // ===============================
  290. academyController.delete('/mentors/:id', isAuth, rbac.checkPermission('mentor', 'delete'), async (req, res, next) => {
  291.  
  292.   try {
  293.     const mentorId = parseInt(req.params.id);
  294.  
  295.     const mentorData = await mentor.findByPk(mentorId);
  296.  
  297.     if (!mentorData) {
  298.       return res.status(404).json({ message: 'Mentor not found.' });
  299.     }
  300.  
  301.     const userId = mentorData.userId;
  302.  
  303.     await mentorData.destroy();
  304.  
  305.     // Обновявам user role обратно на 'user' (ако не е admin/moderator)
  306.     const user = await user_account.findByPk(userId);
  307.     if (user && user.role === 'mentor') {
  308.       await user_account.update(
  309.         {
  310.           role: 'user',
  311.           isMentor: false
  312.         },
  313.         { where: { id: userId } }
  314.       );
  315.     }
  316.  
  317.     res.status(200).json({
  318.       success: true,
  319.       message: 'Mentor deleted successfully!',
  320.     });
  321.  
  322.   } catch (err) {
  323.     console.error('❌ [DELETE MENTOR] Error:', err);
  324.     next(err);
  325.   }
  326. });
  327. // ===============================
  328. // POST /api/academy/mentors/apply
  329. // Кандидатстване за ментор
  330. // ===============================
  331. // server/controllers/academyController.js
  332.  
  333. academyController.post('/mentors/apply', isAuth, async (req, res, next) => {
  334.   try {
  335.     const validationResult = mentorApplicationSchema.safeParse(req.body);
  336.  
  337.     if (!validationResult.success) {
  338.       throw validationResult.error;
  339.     }
  340.  
  341.     const userId = req.user.userId;
  342.  
  343.     const existingApplication = await mentor_application.findOne({
  344.       where: {
  345.         userId,
  346.         status: ['pending', 'approved']
  347.       }
  348.     });
  349.  
  350.     if (existingApplication) {
  351.       if (existingApplication.status === 'approved') {
  352.         return res.status(400).json({
  353.           message: 'You are already an approved mentor.'
  354.         });
  355.       }
  356.       return res.status(400).json({
  357.         message: 'You already have a pending mentor application.'
  358.       });
  359.     }
  360.  
  361.     const applicationData = {
  362.       userId,
  363.       ...validationResult.data,
  364.       country: validationResult.data.country || 'BG', // ✅ ДОБАВЕНО
  365.     };
  366.  
  367.     const application = await mentor_application.create(applicationData);
  368.  
  369.     res.status(201).json({
  370.       message: 'Mentor application submitted successfully!',
  371.       applicationId: application.id,
  372.       status: 'pending'
  373.     });
  374.  
  375.   } catch (err) {
  376.     next(err);
  377.   }
  378. });
  379.  
  380. // ===============================
  381. // GET /api/academy/mentors/applications/pending
  382. // Admin: Вземи всички pending кандидатури
  383. // ===============================
  384. academyController.get('/mentors/applications/pending',
  385.   isAuth,
  386.   rbac.checkPermission('mentorApplication', 'read'),
  387.   async (req, res, next) => {
  388.     try {
  389.       const applications = await mentor_application.findAll({
  390.         where: { status: 'pending' },
  391.         include: [
  392.           {
  393.             model: user_account,
  394.             as: 'user',
  395.             attributes: ['id', 'email', 'role', 'createdAt'],
  396.           }
  397.         ],
  398.         order: [['createdAt', 'DESC']],
  399.       });
  400.  
  401.       res.status(200).json({
  402.         success: true,
  403.         applications,
  404.         total: applications.length,
  405.       });
  406.  
  407.     } catch (err) {
  408.       next(err);
  409.     }
  410.   }
  411. );
  412. // ===============================
  413. // GET /api/academy/mentors/applications/rejected
  414. // Вземане на отхвърлени кандидатури
  415. // ===============================
  416. academyController.get('/mentors/applications/rejected',
  417.   isAuth,
  418.   rbac.checkPermission('mentorApplication', 'read'),
  419.   async (req, res, next) => {
  420.  
  421.     try {
  422.       const applications = await mentor_application.findAll({
  423.         where: { status: 'rejected' },
  424.         include: [
  425.           {
  426.             model: user_account,
  427.             as: 'user',
  428.             attributes: ['id', 'email', 'role'],
  429.           },
  430.         ],
  431.         order: [['rejectedAt', 'DESC']],
  432.       });
  433.  
  434.       res.status(200).json({
  435.         success: true,
  436.         applications,
  437.         total: applications.length,
  438.       });
  439.  
  440.     } catch (err) {
  441.       console.error('❌ [GET REJECTED APPLICATIONS] Error:', err);
  442.       next(err);
  443.     }
  444.   }
  445. );
  446. // ===============================
  447. // POST /api/academy/mentors/applications/:id/approve
  448. // Admin: Одобри кандидатура
  449. // ===============================
  450. academyController.post('/mentors/applications/:applicationId/approve',
  451.   isAuth,
  452.   rbac.checkPermission('mentorApplication', 'approve'),
  453.   async (req, res, next) => {
  454.  
  455.     try {
  456.       const applicationId = parseInt(req.params.applicationId);
  457.  
  458.       const application = await mentor_application.findByPk(applicationId, {
  459.         include: [
  460.           {
  461.             model: user_account,
  462.             as: 'user',
  463.           },
  464.         ],
  465.       });
  466.  
  467.       if (!application) {
  468.         return res.status(404).json({ message: 'Application not found.' });
  469.       }
  470.  
  471.       if (application.status === 'approved') {
  472.         return res.status(400).json({ message: 'Application is already approved.' });
  473.       }
  474.  
  475.       // Провери дали user вече е ментор
  476.       const existingMentor = await mentor.findOne({
  477.         where: { userId: application.userId }
  478.       });
  479.  
  480.       if (existingMentor) {
  481.         return res.status(400).json({
  482.           message: 'User is already a mentor.'
  483.         });
  484.       }
  485.  
  486.       await application.update({
  487.         status: 'approved',
  488.         approvedAt: new Date(),
  489.         rejectionReason: null,
  490.         rejectedAt: null,
  491.       });
  492.  
  493.       // Създай ментор запис
  494.       const mentorData = {
  495.         userId: application.userId,
  496.         applicationId: application.id,
  497.         name: application.name,
  498.         email: application.email,
  499.         phone: application.phone,
  500.         age: application.age,
  501.         country: application.country || 'BG',
  502.         photoUrl: application.photoUrl,
  503.         specialization: application.specialization,
  504.         education: application.education,
  505.         experience: application.experience,
  506.         motivation: application.motivation,
  507.         availability: application.availability,
  508.         languages: application.languages || [],
  509.         viber: application.viber,
  510.         facebook: application.facebook,
  511.         linkedin: application.linkedin,
  512.         otherContact: application.otherContact,
  513.         priorityContact: application.priorityContact || 'email',
  514.         cvUrl: application.cvUrl,
  515.         cvOriginalName: application.cvOriginalName,
  516.         status: 'active',
  517.         approvedAt: new Date(),
  518.       };
  519.  
  520.       const newMentor = await mentor.create(mentorData);
  521.  
  522.       // Обнови user role
  523.       const user = application.user;
  524.       if (user && (user.role === 'user' || user.role === 'guest')) {
  525.         await user_account.update(
  526.           {
  527.             role: 'mentor',
  528.             isMentor: true  //
  529.           },
  530.           { where: { id: application.userId } }
  531.         );
  532.       }
  533.       // const { user_notification } = require('../sequelize/models/index');
  534.  
  535.       await user_notification.create({
  536.         userId: application.userId,
  537.         type: 'mentor_application_approved',
  538.         title: 'Поздравления! Вашата кандидатура за ментор беше одобрена! 🎉',
  539.         message: 'Добре дошли в екипа на менторите на DigiBridge Academy! Вече можете да започнете да помагате на ученици и да споделяте знанията си.',
  540.         data: {
  541.           applicationId: application.id,
  542.           mentorId: newMentor.id,
  543.           specialization: application.specialization
  544.         },
  545.         read: false
  546.       });
  547.       res.status(201).json({
  548.         success: true,
  549.         message: 'Mentor application approved successfully!',
  550.         mentor: newMentor,
  551.       });
  552.  
  553.     } catch (err) {
  554.       console.error('❌ [APPROVE MENTOR APPLICATION] Error:', err);
  555.       next(err);
  556.     }
  557.   }
  558. );
  559.  
  560. // ===============================
  561. // PATCH /api/academy/mentors/:id/activate
  562. // Активиране на деактивиран ментор
  563. // ===============================
  564. academyController.patch(
  565.   '/mentors/:id/activate',
  566.   isAuth,
  567.   rbac.checkPermission('mentor', 'update'),
  568.   async (req, res, next) => {
  569.  
  570.     try {
  571.       const mentorId = parseInt(req.params.id);
  572.  
  573.       const mentorData = await mentor.findByPk(mentorId);
  574.  
  575.       if (!mentorData) {
  576.         return res.status(404).json({ message: 'Mentor not found.' });
  577.       }
  578.  
  579.       // Активирай ментора
  580.       await mentorData.update({ status: 'active' });
  581.  
  582.  
  583.       res.status(200).json({
  584.         success: true,
  585.         message: 'Mentor activated successfully!',
  586.         mentor: mentorData,
  587.       });
  588.  
  589.     } catch (err) {
  590.       console.error('❌ [ACTIVATE MENTOR] Error:', err);
  591.       next(err);
  592.     }
  593.   }
  594. );
  595.  
  596.  
  597. // ===============================
  598. // PATCH /api/academy/mentors/:id/deactivate
  599. // Деактивиране на ментор
  600. // ===============================
  601. academyController.patch(
  602.   '/mentors/:id/deactivate',
  603.   isAuth,
  604.   rbac.checkPermission('mentor', 'update'),
  605.   async (req, res, next) => {
  606.  
  607.     try {
  608.       const mentorId = parseInt(req.params.id);
  609.  
  610.       const mentorData = await mentor.findByPk(mentorId);
  611.  
  612.       if (!mentorData) {
  613.         return res.status(404).json({ message: 'Mentor not found.' });
  614.       }
  615.  
  616.       // Деактивирай ментора
  617.       await mentorData.update({ status: 'inactive' });
  618.  
  619.       res.status(200).json({
  620.         success: true,
  621.         message: 'Mentor deactivated successfully!',
  622.         mentor: mentorData,
  623.       });
  624.  
  625.     } catch (err) {
  626.       console.error('❌ [DEACTIVATE MENTOR] Error:', err);
  627.       next(err);
  628.     }
  629.   }
  630. );
  631. // ===============================
  632. // POST /api/academy/mentors/applications/:id/reject
  633. // Admin: Отхвърли кандидатура
  634. // ===============================
  635. academyController.post(
  636.   '/mentors/applications/:id/reject',
  637.   isAuth,
  638.   rbac.checkPermission('mentorApplication', 'update'),
  639.   async (req, res, next) => {
  640.     try {
  641.       const applicationId = parseInt(req.params.id);
  642.       const { rejectionReason } = req.body;
  643.  
  644.       if (!rejectionReason) {
  645.         return res.status(400).json({
  646.           message: 'Rejection reason is required.'
  647.         });
  648.       }
  649.  
  650.       const application = await mentor_application.findByPk(applicationId);
  651.  
  652.       if (!application) {
  653.         return res.status(404).json({ message: 'Application not found.' });
  654.       }
  655.  
  656.       if (application.status !== 'pending') {
  657.         return res.status(400).json({
  658.           message: `Application is already ${application.status}.`
  659.         });
  660.       }
  661.  
  662.       application.status = 'rejected';
  663.       application.rejectionReason = rejectionReason;
  664.       application.rejectedAt = new Date();
  665.       await application.save();
  666.  
  667.       // const { user_notification } = require('../sequelize/models/index');
  668.  
  669.       await user_notification.create({
  670.         userId: application.userId,
  671.         type: 'mentor_application_rejected',
  672.         title: 'Вашата кандидатура за ментор не беше одобрена',
  673.         message: 'За съжаление, вашата кандидатура за ментор не отговаря на нашите изисквания в момента.',
  674.         data: {
  675.           applicationId: application.id,
  676.           rejectionReason: rejectionReason,
  677.           specialization: application.specialization
  678.         },
  679.         read: false
  680.       });
  681.       res.status(200).json({
  682.         success: true,
  683.         message: 'Mentor application rejected successfully.',
  684.         applicationId: application.id,
  685.       });
  686.  
  687.     } catch (err) {
  688.       next(err);
  689.     }
  690.   }
  691. );
  692. // ===============================
  693. // POST /api/academy/admin/notifications
  694. // Създаване на нова нотификация
  695. // ===============================
  696. academyController.post(
  697.   '/admin/notifications',
  698.   isAuth,
  699.   rbac.checkPermission('notification', 'create'),
  700.   async (req, res, next) => {
  701.  
  702.     try {
  703.       const { type, title, message, data } = req.body;
  704.  
  705.       // Validation
  706.       if (!type || !title || !message) {
  707.         return res.status(400).json({
  708.           message: 'Type, title, and message are required.'
  709.         });
  710.       }
  711.  
  712.       const notification = await admin_notification.create({
  713.         type,
  714.         title,
  715.         message,
  716.         data: data || {},
  717.         read: false,
  718.       });
  719.  
  720.       res.status(201).json({
  721.         success: true,
  722.         message: 'Notification created successfully!',
  723.         notification,
  724.       });
  725.  
  726.     } catch (err) {
  727.       console.error('❌ [CREATE NOTIFICATION] Error:', err);
  728.       next(err);
  729.     }
  730.   }
  731. );
  732.  
  733. // ===============================
  734. // GET /api/academy/admin/notifications
  735. // Вземане на нотификации с филтриране
  736. // ===============================
  737. academyController.get(
  738.   '/admin/notifications',
  739.   isAuth,
  740.   rbac.checkPermission('notification', 'read'),
  741.   async (req, res, next) => {
  742.  
  743.     try {
  744.       const {
  745.         page = 1,
  746.         limit = 20,
  747.         read,
  748.         type,
  749.       } = req.query;
  750.  
  751.       const pageNum = parseInt(page);
  752.       const limitNum = parseInt(limit);
  753.       const offset = (pageNum - 1) * limitNum;
  754.  
  755.       // Build WHERE clause
  756.       const where = {};
  757.  
  758.       if (read !== undefined) {
  759.         where.read = read === 'true';
  760.       }
  761.  
  762.       if (type) {
  763.         where.type = type;
  764.       }
  765.  
  766.       // Fetch notifications
  767.       const { count, rows: notifications } = await admin_notification.findAndCountAll({
  768.         where,
  769.         limit: limitNum,
  770.         offset,
  771.         order: [['createdAt', 'DESC']],
  772.       });
  773.  
  774.       // Count unread
  775.       const unreadCount = await admin_notification.count({
  776.         where: { read: false },
  777.       });
  778.  
  779.       const totalPages = Math.ceil(count / limitNum);
  780.  
  781.       res.status(200).json({
  782.         success: true,
  783.         notifications,
  784.         unreadCount,
  785.         pagination: {
  786.           page: pageNum,
  787.           limit: limitNum,
  788.           total: count,
  789.           totalPages,
  790.         },
  791.       });
  792.  
  793.     } catch (err) {
  794.       console.error('❌ [GET NOTIFICATIONS] Error:', err);
  795.       next(err);
  796.     }
  797.   }
  798. );
  799.  
  800. // ===============================
  801. // PUT /api/academy/admin/notifications/:id/read
  802. // Маркиране на нотификация като прочетена
  803. // ===============================
  804. academyController.put(
  805.   '/admin/notifications/:id/read',
  806.   isAuth,
  807.   rbac.checkPermission('notification', 'update'),
  808.   async (req, res, next) => {
  809.  
  810.     try {
  811.       const notificationId = parseInt(req.params.id);
  812.  
  813.       const notification = await admin_notification.findByPk(notificationId);
  814.  
  815.       if (!notification) {
  816.         return res.status(404).json({ message: 'Notification not found.' });
  817.       }
  818.  
  819.       await notification.update({
  820.         read: true,
  821.         readAt: new Date(),
  822.       });
  823.  
  824.       res.status(200).json({
  825.         success: true,
  826.         message: 'Notification marked as read.',
  827.         notification,
  828.       });
  829.  
  830.     } catch (err) {
  831.       console.error('❌ [MARK AS READ] Error:', err);
  832.       next(err);
  833.     }
  834.   }
  835. );
  836.  
  837. // ===============================
  838. // PUT /api/academy/admin/notifications/mark-all-read
  839. // Маркиране на всички нотификации като прочетени
  840. // ===============================
  841. academyController.put(
  842.   '/admin/notifications/mark-all-read',
  843.   isAuth,
  844.   rbac.checkPermission('notification', 'update'),
  845.   async (req, res, next) => {
  846.  
  847.     try {
  848.       const [updatedCount] = await admin_notification.update(
  849.         {
  850.           read: true,
  851.           readAt: new Date(),
  852.         },
  853.         {
  854.           where: { read: false },
  855.         }
  856.       );
  857.  
  858.       res.status(200).json({
  859.         success: true,
  860.         message: `${updatedCount} notifications marked as read.`,
  861.         markedCount: updatedCount,
  862.       });
  863.  
  864.     } catch (err) {
  865.       console.error('❌ [MARK ALL AS READ] Error:', err);
  866.       next(err);
  867.     }
  868.   }
  869. );
  870.  
  871. // ===============================
  872. // DELETE /api/academy/admin/notifications/:id
  873. // Изтриване на нотификация
  874. // ===============================
  875. academyController.delete(
  876.   '/admin/notifications/:id',
  877.   isAuth,
  878.   rbac.checkPermission('notification', 'delete'),
  879.   async (req, res, next) => {
  880.  
  881.     try {
  882.       const notificationId = parseInt(req.params.id);
  883.  
  884.       const notification = await admin_notification.findByPk(notificationId);
  885.  
  886.       if (!notification) {
  887.         return res.status(404).json({ message: 'Notification not found.' });
  888.       }
  889.  
  890.       await notification.destroy();
  891.  
  892.       res.status(200).json({
  893.         success: true,
  894.         message: 'Notification deleted successfully.',
  895.       });
  896.  
  897.     } catch (err) {
  898.       console.error('❌ [DELETE NOTIFICATION] Error:', err);
  899.       next(err);
  900.     }
  901.   }
  902. );
  903. academyController.post(
  904.   '/mentors/bulk-delete',
  905.   isAuth,
  906.   rbac.checkPermission('mentor', 'delete'),
  907.   async (req, res, next) => {
  908.     try {
  909.       const { mentorIds } = req.body;
  910.  
  911.       if (!mentorIds || !Array.isArray(mentorIds) || mentorIds.length === 0) {
  912.         return res.status(400).json({
  913.           message: 'mentorIds array is required and must not be empty.'
  914.         });
  915.       }
  916.  
  917.       const deletedCount = await mentor.destroy({
  918.         where: {
  919.           id: mentorIds
  920.         }
  921.       });
  922.  
  923.       res.status(200).json({
  924.         success: true,
  925.         message: `Successfully deleted ${deletedCount} mentors.`,
  926.         deletedCount
  927.       });
  928.  
  929.     } catch (err) {
  930.       next(err);
  931.     }
  932.   }
  933. );
  934. academyController.get('/stats', async (req, res, next) => {
  935.   try {
  936.     const totalMentors = await mentor.count();
  937.     const activeMentors = await mentor.count({
  938.       where: { status: 'active' }
  939.     });
  940.  
  941.     const mentorsData = await mentor.findAll({
  942.       attributes: [[sequelize.fn('SUM', sequelize.col('students_count')), 'totalStudents']]
  943.     });
  944.     const totalStudents = parseInt(mentorsData[0].dataValues.totalStudents) || 0;
  945.  
  946.     const totalCourses = await mentor_course.count();
  947.  
  948.     const ratingData = await mentor.findAll({
  949.       attributes: [[sequelize.fn('AVG', sequelize.col('rating')), 'avgRating']]
  950.     });
  951.     const averageRating = parseFloat(ratingData[0].dataValues.avgRating) || 0;
  952.  
  953.     // ✅ РЕАЛНО ИЗЧИСЛЕНИЕ НА ДЪРЖАВИ
  954.     const countriesData = await mentor.findAll({
  955.       attributes: ['country'],
  956.       where: {
  957.         country: {
  958.           [Op.ne]: null
  959.         }
  960.       },
  961.       group: ['country'],
  962.       raw: true
  963.     });
  964.     const countries = countriesData.length;
  965.  
  966.     res.status(200).json({
  967.       success: true,
  968.       stats: {
  969.         totalMentors,
  970.         activeMentors,
  971.         totalStudents,
  972.         totalCourses,
  973.         averageRating: parseFloat(averageRating.toFixed(1)),
  974.         countries, // ✅ РЕАЛНО ОТ БАЗАТА
  975.         satisfaction: 100
  976.       }
  977.     });
  978.  
  979.   } catch (err) {
  980.     next(err);
  981.   }
  982. });
  983. academyController.get(
  984.   '/mentors/statistics/overview',
  985.   isAuth,
  986.   rbac.checkPermission('statistics', 'read'),
  987.   statisticsCache(300),
  988.   async (req, res, next) => {
  989.     try {
  990.       const db = getFirebaseDb();
  991.  
  992.       // ✅ 1. ОСНОВНИ COUNTS
  993.       const activeMentors = await mentor.count({
  994.         where: { status: 'active' }
  995.       });
  996.  
  997.       const totalMentors = await mentor.count();
  998.  
  999.       // ✅ 2. ОБЩ БРОЙ СТУДЕНТИ
  1000.       const mentorsData = await mentor.findAll({
  1001.         attributes: [[sequelize.fn('SUM', sequelize.col('students_count')), 'totalStudents']]
  1002.       });
  1003.       const totalStudents = parseInt(mentorsData[0].dataValues.totalStudents) || 0;
  1004.  
  1005.       // ✅ 3. СРЕДНА ОЦЕНКА
  1006.       const ratingData = await mentor.findAll({
  1007.         attributes: [[sequelize.fn('AVG', sequelize.col('rating')), 'avgRating']]
  1008.       });
  1009.       const averageRating = parseFloat(ratingData[0].dataValues.avgRating) || 0;
  1010.  
  1011.       // ✅ 4. ЗАВЪРШЕНИ КУРСОВЕ
  1012.       const coursesData = await mentor_course.findAll({
  1013.         attributes: [[sequelize.fn('SUM', sequelize.col('completed_count')), 'totalCompleted']]
  1014.       });
  1015.       const totalCoursesCompleted = parseInt(coursesData[0].dataValues.totalCompleted) || 0;
  1016.  
  1017.       // ✅ 5. ОБЩ БРОЙ СЕСИИ
  1018.       const sessionsData = await mentor.findAll({
  1019.         attributes: [[sequelize.fn('SUM', sequelize.col('sessions_count')), 'totalSessions']]
  1020.       });
  1021.       const totalSessionsThisMonth = parseInt(sessionsData[0].dataValues.totalSessions) || 0;
  1022.  
  1023.       // ✅ 6. ONLINE HOURS - КОМБИНИРАНИ (PostgreSQL + Firebase)
  1024.       // 6.1. ACCUMULATED ОТ PostgreSQL
  1025.       const onlineMinutesData = await mentor.findAll({
  1026.         attributes: [[sequelize.fn('SUM', sequelize.col('accumulated_online_minutes')), 'totalAccumulatedMinutes']]
  1027.       });
  1028.       const totalAccumulatedMinutes = parseInt(onlineMinutesData[0].dataValues.totalAccumulatedMinutes) || 0;
  1029.  
  1030.       // 6.2. CURRENT ОТ Firebase (active/completed sessions)
  1031.       let currentFirebaseMinutes = 0;
  1032.  
  1033.       const allMentors = await mentor.findAll({
  1034.         where: { status: 'active' },
  1035.         include: [
  1036.           {
  1037.             model: user_account,
  1038.             as: 'user',
  1039.             attributes: ['email']
  1040.           }
  1041.         ]
  1042.       });
  1043.  
  1044.       for (const mentorData of allMentors) {
  1045.         const firebaseMentorId = mentorData.user.email
  1046.           .replace(/\./g, '_dot_')
  1047.           .replace(/@/g, '_at_');
  1048.  
  1049.         const sessionsRef = db.ref(`mentor_sessions/${firebaseMentorId}`);
  1050.         const sessionsSnapshot = await sessionsRef.once('value');
  1051.         const sessions = sessionsSnapshot.val() || {};
  1052.  
  1053.         Object.values(sessions).forEach(session => {
  1054.           if (session.endTime && session.startTime) {
  1055.             const durationMs = session.endTime - session.startTime;
  1056.             const durationMinutes = Math.floor(durationMs / 60000);
  1057.             currentFirebaseMinutes += durationMinutes;
  1058.           }
  1059.         });
  1060.       }
  1061.  
  1062.       // 6.3. КОМБИНИРАЙ
  1063.       const totalOnlineMinutes = totalAccumulatedMinutes + currentFirebaseMinutes;
  1064.       const totalOnlineHours = Math.round((totalOnlineMinutes / 60) * 100) / 100;
  1065.  
  1066.       // ✅ 7. COMPLETION RATE (засега средна стойност)
  1067.       const averageCompletionRate = 95;
  1068.  
  1069.       // ✅ 8. ОБЩ БРОЙ ОТЗИВИ (засега 0)
  1070.       const totalReviews = 0;
  1071.  
  1072.       res.status(200).json({
  1073.         success: true,
  1074.         stats: {
  1075.           activeMentors,
  1076.           totalMentors,
  1077.           totalStudents,
  1078.           averageRating: parseFloat(averageRating.toFixed(1)),
  1079.           totalCoursesCompleted,
  1080.           totalSessionsThisMonth,
  1081.           totalOnlineHours, // ✅ FIXED!
  1082.           averageCompletionRate,
  1083.           totalReviews
  1084.         }
  1085.       });
  1086.  
  1087.     } catch (err) {
  1088.       console.error('❌ Error fetching overview stats:', err);
  1089.       next(err);
  1090.     }
  1091.   }
  1092. );
  1093.  
  1094. // ===============================
  1095. // GET /api/academy/mentors/statistics/by-specialization
  1096. // Ментори по специализация
  1097. // ===============================
  1098. academyController.get(
  1099.   '/mentors/statistics/by-specialization',
  1100.   isAuth,
  1101.   rbac.checkPermission('statistics', 'read'),
  1102.   statisticsCache(600),
  1103.   async (req, res, next) => {
  1104.     try {
  1105.       const specializations = await mentor.findAll({
  1106.         attributes: [
  1107.           'specialization',
  1108.           [sequelize.fn('COUNT', sequelize.col('id')), 'count'],
  1109.           [sequelize.fn('SUM', sequelize.col('students_count')), 'totalStudents'],
  1110.           [sequelize.fn('AVG', sequelize.col('rating')), 'averageRating']
  1111.         ],
  1112.         where: {
  1113.           specialization: {
  1114.             [Op.ne]: null
  1115.           }
  1116.         },
  1117.         group: ['specialization'],
  1118.         order: [[sequelize.literal('count'), 'DESC']],
  1119.         raw: true
  1120.       });
  1121.  
  1122.       const formattedData = specializations.map(spec => ({
  1123.         specialization: spec.specialization,
  1124.         count: parseInt(spec.count),
  1125.         totalStudents: parseInt(spec.totalStudents) || 0,
  1126.         averageRating: parseFloat(parseFloat(spec.averageRating).toFixed(1))
  1127.       }));
  1128.  
  1129.       res.status(200).json({
  1130.         success: true,
  1131.         specializations: formattedData
  1132.       });
  1133.  
  1134.     } catch (err) {
  1135.       console.error('❌ [BY-SPECIALIZATION] Error:', err);
  1136.       next(err);
  1137.     }
  1138.   }
  1139. );
  1140. // ===============================
  1141. // GET /api/academy/mentors/all-with-stats
  1142. // Всички ментори с Firebase статистики (за DetailedMentorsTable)
  1143. // ===============================
  1144. academyController.get(
  1145.   '/mentors/all-with-stats',
  1146.   statisticsCache(120),
  1147.   isAuth,
  1148.   rbac.checkPermission('statistics', 'read'),
  1149.   async (req, res, next) => {
  1150.     try {
  1151.  
  1152.       const mentors = await getAllMentorsCombinedStats();
  1153.  
  1154.       res.status(200).json({
  1155.         success: true,
  1156.         mentors,
  1157.         total: mentors.length
  1158.       });
  1159.  
  1160.     } catch (err) {
  1161.       console.error('❌ [ALL-WITH-STATS] Error:', err);
  1162.       next(err);
  1163.     }
  1164.   }
  1165. );
  1166.  
  1167. academyController.get(
  1168.   '/mentors/:id/firebase-stats',
  1169.   isAuth,
  1170.   rbac.checkPermission('statistics', 'readOwn'),
  1171.   statisticsCache(120),
  1172.   async (req, res, next) => {
  1173.     try {
  1174.       const mentorId = parseInt(req.params.id);
  1175.       const requestingUserId = req.user.userId;
  1176.       const requestingUserRole = req.user.role;
  1177.  
  1178.       // ✅ CHECK: Mentor може да вижда САМО СВОИТЕ stats
  1179.       if (requestingUserRole !== 'admin') {
  1180.         const mentorRecord = await mentor.findOne({
  1181.           where: { userId: requestingUserId }
  1182.         });
  1183.  
  1184.         if (!mentorRecord || mentorRecord.id !== mentorId) {
  1185.           return res.status(403).json({
  1186.             success: false,
  1187.             message: 'You can only view your own statistics'
  1188.           });
  1189.         }
  1190.       }
  1191.  
  1192.       // ✅ Admin или собственикът стигат до тук
  1193.       const stats = await getMentorCombinedStats(mentorId);
  1194.  
  1195.       res.status(200).json({
  1196.         success: true,
  1197.         mentor: stats
  1198.       });
  1199.  
  1200.     } catch (err) {
  1201.       console.error('❌ [FIREBASE-STATS] Error:', err);
  1202.  
  1203.       if (err.message === 'Mentor not found') {
  1204.         return res.status(404).json({
  1205.           success: false,
  1206.           message: 'Mentor not found'
  1207.         });
  1208.       }
  1209.  
  1210.       next(err);
  1211.     }
  1212.   }
  1213. );
  1214. // ===============================
  1215. // POST /api/academy/mentors/:id/refresh-stats
  1216. // Обнови кеширани статистики от Firebase
  1217. // ===============================
  1218. academyController.post(
  1219.   '/mentors/:id/refresh-stats',
  1220.   isAuth,
  1221.   rbac.checkPermission('statistics', 'readOwn'), // ✅ Admin или mentor
  1222.   async (req, res, next) => {
  1223.     try {
  1224.       const mentorId = parseInt(req.params.id);
  1225.       const requestingUserId = req.user.userId;
  1226.       const requestingUserRole = req.user.role;
  1227.  
  1228.       // ✅ CHECK: Mentor може да refresh-ва САМО СВОИТЕ stats
  1229.       if (requestingUserRole !== 'admin') {
  1230.         const mentorRecord = await mentor.findOne({
  1231.           where: { userId: requestingUserId }
  1232.         });
  1233.  
  1234.         if (!mentorRecord || mentorRecord.id !== mentorId) {
  1235.           return res.status(403).json({
  1236.             success: false,
  1237.             message: 'You can only refresh your own statistics'
  1238.           });
  1239.         }
  1240.       }
  1241.  
  1242.       // ✅ Admin или собственикът стигат до тук
  1243.       const mentorData = await mentor.findByPk(mentorId, {
  1244.         include: [
  1245.           {
  1246.             model: user_account,
  1247.             as: 'user',
  1248.             attributes: ['email']
  1249.           }
  1250.         ]
  1251.       });
  1252.  
  1253.       if (!mentorData) {
  1254.         return res.status(404).json({
  1255.           success: false,
  1256.           message: 'Mentor not found'
  1257.         });
  1258.       }
  1259.  
  1260.       const email = mentorData.user.email;
  1261.       const firebaseMentorId = email
  1262.         .replace(/\./g, '_dot_')
  1263.         .replace(/@/g, '_at_');
  1264.  
  1265.       // Обнови статистиките
  1266.       const result = await updateMentorCachedStats(mentorId, firebaseMentorId);
  1267.  
  1268.       res.status(200).json({
  1269.         success: true,
  1270.         message: 'Stats refreshed successfully',
  1271.         stats: result.stats
  1272.       });
  1273.  
  1274.     } catch (err) {
  1275.       console.error('❌ [REFRESH-STATS] Error:', err);
  1276.       next(err);
  1277.     }
  1278.   }
  1279. );
  1280.  
  1281. // ===============================
  1282. // GET /api/academy/mentors/statistics/firebase-overview
  1283. // Общи Firebase статистики (за Dashboard)
  1284. // ===============================
  1285.  
  1286. academyController.get(
  1287.   '/mentors/statistics/firebase-overview',
  1288.   isAuth,
  1289.   rbac.checkPermission('statistics', 'read'),
  1290.   statisticsCache(180),
  1291.   async (req, res, next) => {
  1292.     try {
  1293.       const mentors = await getAllMentorsCombinedStats();
  1294.  
  1295.       // Изчисли агрегирани статистики
  1296.       let totalSessions = 0;
  1297.       let totalOnlineHours = 0;
  1298.       let totalMessages = 0;
  1299.       let totalActiveSessions = 0;
  1300.       let responseTimes = [];
  1301.  
  1302.       mentors.forEach(mentor => {
  1303.         const stats = mentor.firebaseStats;
  1304.         totalSessions += stats.totalSessions || 0;
  1305.         totalOnlineHours += stats.totalOnlineHours || 0;
  1306.         totalMessages += stats.totalMessages || 0;
  1307.         totalActiveSessions += stats.activeSessions || 0;
  1308.  
  1309.         if (stats.averageResponseTime > 0) {
  1310.           responseTimes.push(stats.averageResponseTime);
  1311.         }
  1312.       });
  1313.  
  1314.       const averageResponseTime = responseTimes.length > 0
  1315.         ? Math.round(responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length)
  1316.         : 0;
  1317.  
  1318.       res.status(200).json({
  1319.         success: true,
  1320.         stats: {
  1321.           totalMentors: mentors.length,
  1322.           totalSessions,
  1323.           totalActiveSessions,
  1324.           totalOnlineHours,
  1325.           averageResponseTime,
  1326.           totalMessages
  1327.         }
  1328.       });
  1329.  
  1330.     } catch (err) {
  1331.       console.error('❌ [FIREBASE-OVERVIEW] Error:', err);
  1332.       next(err);
  1333.     }
  1334.   }
  1335. );
  1336. // ===============================
  1337. // GET /api/academy/mentors/statistics/top-by-online-time
  1338. // Топ ментори по онлайн време
  1339. // ===============================
  1340. academyController.get(
  1341.   '/mentors/statistics/top-by-online-time',
  1342.   isAuth,
  1343.   rbac.checkPermission('statistics', 'read'),
  1344.   statisticsCache(300),
  1345.   async (req, res, next) => {
  1346.     try {
  1347.       const { limit = 10 } = req.query;
  1348.  
  1349.       const mentors = await getAllMentorsCombinedStats();
  1350.  
  1351.       const topMentors = mentors
  1352.         .sort((a, b) => {
  1353.           const aHours = a.firebaseStats.totalOnlineHours || 0;
  1354.           const bHours = b.firebaseStats.totalOnlineHours || 0;
  1355.           return bHours - aHours;
  1356.         })
  1357.         .slice(0, parseInt(limit))
  1358.         .map(mentor => ({
  1359.           id: mentor.id,
  1360.           name: mentor.name,
  1361.           photoUrl: mentor.photoUrl,
  1362.           specialization: mentor.specialization,
  1363.           // ✅ ФОРМАТ КАКЪВТО ОЧАКВА КОМПОНЕНТА
  1364.           onlineTime: {
  1365.             thisMonth: mentor.firebaseStats.totalOnlineHours || 0,  // TODO: Разделяне на месеци идва от historical tracking
  1366.             total: mentor.firebaseStats.totalOnlineHours || 0
  1367.           }
  1368.         }));
  1369.  
  1370.       res.status(200).json({
  1371.         success: true,
  1372.         mentors: topMentors,
  1373.         total: topMentors.length
  1374.       });
  1375.  
  1376.     } catch (err) {
  1377.       console.error('❌ [TOP-BY-ONLINE-TIME] Error:', err);
  1378.       next(err);
  1379.     }
  1380.   }
  1381. );
  1382.  
  1383. // ===============================
  1384. // GET /api/academy/mentors/statistics/response-times
  1385. // Response time статистики
  1386. // ===============================
  1387. academyController.get(
  1388.   '/mentors/statistics/response-times',
  1389.   isAuth,
  1390.   rbac.checkPermission('statistics', 'read'),
  1391.   statisticsCache(300),
  1392.   async (req, res, next) => {
  1393.     try {
  1394.  
  1395.       const mentors = await getAllMentorsCombinedStats();
  1396.  
  1397.       // Групирай по response time ranges
  1398.       const ranges = {
  1399.         excellent: [], // <= 10 min
  1400.         good: [],      // 11-15 min
  1401.         average: [],   // 16-20 min
  1402.         slow: []       // > 20 min
  1403.       };
  1404.  
  1405.       mentors.forEach(mentor => {
  1406.         const responseTime = mentor.firebaseStats.averageResponseTime || 0;
  1407.  
  1408.         if (responseTime === 0) return; // Skip ако няма данни
  1409.  
  1410.         const mentorData = {
  1411.           id: mentor.id,
  1412.           name: mentor.name,
  1413.           responseTime,
  1414.           totalMessages: mentor.firebaseStats.totalMessages
  1415.         };
  1416.  
  1417.         if (responseTime <= 10) {
  1418.           ranges.excellent.push(mentorData);
  1419.         } else if (responseTime <= 15) {
  1420.           ranges.good.push(mentorData);
  1421.         } else if (responseTime <= 20) {
  1422.           ranges.average.push(mentorData);
  1423.         } else {
  1424.           ranges.slow.push(mentorData);
  1425.         }
  1426.       });
  1427.  
  1428.       const stats = {
  1429.         excellent: ranges.excellent.length,
  1430.         good: ranges.good.length,
  1431.         average: ranges.average.length,
  1432.         slow: ranges.slow.length,
  1433.         totalMentorsWithData: mentors.filter(m => m.firebaseStats.averageResponseTime > 0).length,
  1434.         mentorsByRange: ranges
  1435.       };
  1436.  
  1437.       res.status(200).json({
  1438.         success: true,
  1439.         stats
  1440.       });
  1441.  
  1442.     } catch (err) {
  1443.       console.error('❌ [RESPONSE-TIMES] Error:', err);
  1444.       next(err);
  1445.     }
  1446.   }
  1447. );
  1448.  
  1449. // ===============================
  1450. // GET /api/academy/mentors/statistics/activity-trend
  1451. // Activity trend (реални исторически данни от snapshots)
  1452. // ===============================
  1453. academyController.get(
  1454.   '/mentors/statistics/activity-trend',
  1455.   isAuth,
  1456.   rbac.checkPermission('statistics', 'read'),
  1457.   statisticsCache(600),
  1458.   async (req, res, next) => {
  1459.     try {
  1460.       const { months = 6 } = req.query;
  1461.  
  1462.       const { getActivityTrendData } = require('../services/mentorActivitySnapshotService');
  1463.       const trend = await getActivityTrendData(parseInt(months));
  1464.  
  1465.       // ✅ Ако няма snapshots, върни текущите данни като fallback
  1466.       if (trend.length === 0) {
  1467.  
  1468.         const mentors = await getAllMentorsCombinedStats();
  1469.         const totalSessions = mentors.reduce((sum, m) => sum + (m.sessionsCount || 0), 0);
  1470.         const totalOnlineHours = mentors.reduce((sum, m) => sum + (m.firebaseStats.totalOnlineHours || 0), 0);
  1471.         const totalMessages = mentors.reduce((sum, m) => sum + (m.firebaseStats.totalMessages || 0), 0);
  1472.         const currentMonth = new Date().toISOString().slice(0, 7);
  1473.  
  1474.         return res.status(200).json({
  1475.           success: true,
  1476.           trend: [
  1477.             {
  1478.               month: currentMonth,
  1479.               sessions: totalSessions,
  1480.               onlineHours: Math.round(totalOnlineHours),
  1481.               messages: totalMessages,
  1482.               activeMentors: mentors.length
  1483.             }
  1484.           ],
  1485.           note: 'No historical data yet. First snapshot will be created at 00:05 tonight. You can create a manual snapshot using POST /mentors/statistics/create-snapshot'
  1486.         });
  1487.       }
  1488.  
  1489.       res.status(200).json({
  1490.         success: true,
  1491.         trend
  1492.       });
  1493.  
  1494.     } catch (err) {
  1495.       console.error('❌ [ACTIVITY-TREND] Error:', err);
  1496.       next(err);
  1497.     }
  1498.   }
  1499. );
  1500.  
  1501. // ===============================
  1502. // GET /api/academy/mentors/statistics/session-quality
  1503. // Session quality metrics
  1504. // ===============================
  1505. academyController.get(
  1506.   '/mentors/statistics/session-quality',
  1507.   isAuth,
  1508.   rbac.checkPermission('statistics', 'read'),
  1509.   statisticsCache(300),
  1510.   async (req, res, next) => {
  1511.     try {
  1512.  
  1513.       const mentors = await getAllMentorsCombinedStats();
  1514.  
  1515.       let totalSessions = 0;
  1516.       let completedSessions = 0;
  1517.       let activeSessions = 0;
  1518.       let totalRatings = 0;
  1519.       let ratedMentors = 0;
  1520.  
  1521.       mentors.forEach(mentor => {
  1522.         totalSessions += mentor.sessionsCount || 0;
  1523.         completedSessions += mentor.firebaseStats.completedSessions || 0;
  1524.         activeSessions += mentor.firebaseStats.activeSessions || 0;
  1525.  
  1526.         if (mentor.rating > 0) {
  1527.           totalRatings += mentor.rating;
  1528.           ratedMentors++;
  1529.         }
  1530.       });
  1531.  
  1532.       const completionRate = totalSessions > 0
  1533.         ? Math.round((completedSessions / totalSessions) * 100)
  1534.         : 0;
  1535.  
  1536.       const averageRating = ratedMentors > 0
  1537.         ? (totalRatings / ratedMentors).toFixed(1)
  1538.         : 0;
  1539.  
  1540.       res.status(200).json({
  1541.         success: true,
  1542.         quality: {
  1543.           totalSessions,
  1544.           completedSessions,
  1545.           activeSessions,
  1546.           completionRate,
  1547.           averageRating,
  1548.           totalMentors: mentors.length,
  1549.           ratedMentors
  1550.         }
  1551.       });
  1552.  
  1553.     } catch (err) {
  1554.       console.error('❌ [SESSION-QUALITY] Error:', err);
  1555.       next(err);
  1556.     }
  1557.   }
  1558. );
  1559. // ===============================
  1560. // POST /api/academy/mentors/statistics/create-snapshot
  1561. // Manual snapshot creation (за testing и пропуснати дни)
  1562. // ===============================
  1563. academyController.post(
  1564.   '/mentors/statistics/create-snapshot',
  1565.   isAuth,
  1566.   rbac.checkPermission('mentor', 'update'),
  1567.   async (req, res, next) => {
  1568.     try {
  1569.  
  1570.       const { createDailySnapshots } = require('../services/mentorActivitySnapshotService');
  1571.       const result = await createDailySnapshots();
  1572.  
  1573.       res.status(200).json({
  1574.         success: true,
  1575.         message: 'Snapshot created successfully',
  1576.         count: result.count,
  1577.         date: new Date().toISOString().split('T')[0]
  1578.       });
  1579.     } catch (err) {
  1580.       console.error('❌ [CREATE-SNAPSHOT] Error:', err);
  1581.       next(err);
  1582.     }
  1583.   }
  1584. );
  1585. // ===============================
  1586. // POST /api/academy/sync-session
  1587. // SYNC SESSION STATS - извиква се от frontend след session end
  1588. // ===============================
  1589. academyController.post('/sync-session', async (req, res, next) => {
  1590.  
  1591.   try {
  1592.     const { sessionId, mentorEmail } = req.body;
  1593.  
  1594.     if (!sessionId || !mentorEmail) {
  1595.       return res.status(400).json({
  1596.         success: false,
  1597.         error: 'Missing sessionId or mentorEmail'
  1598.       });
  1599.     }
  1600.  
  1601.     const { syncCompletedSessionStats } = require('../services/sessionSyncService');
  1602.  
  1603.     // Вземи PostgreSQL mentor ID
  1604.     const userAccount = await user_account.findOne({
  1605.       where: { email: mentorEmail }
  1606.     });
  1607.  
  1608.     if (!userAccount) {
  1609.       return res.status(404).json({
  1610.         success: false,
  1611.         error: 'User not found'
  1612.       });
  1613.     }
  1614.  
  1615.     const mentorRecord = await mentor.findOne({
  1616.       where: { userId: userAccount.id }
  1617.     });
  1618.  
  1619.     if (!mentorRecord) {
  1620.       return res.status(404).json({
  1621.         success: false,
  1622.         error: 'Mentor not found'
  1623.       });
  1624.     }
  1625.  
  1626.     // Firebase mentor ID
  1627.     const mentorFirebaseId = mentorEmail
  1628.       .replace(/\./g, '_dot_')
  1629.       .replace(/@/g, '_at_');
  1630.  
  1631.     // Sync stats
  1632.     const result = await syncCompletedSessionStats(
  1633.       sessionId,
  1634.       mentorFirebaseId,
  1635.       mentorRecord.id
  1636.     );
  1637.  
  1638.     return res.json(result);
  1639.  
  1640.   } catch (error) {
  1641.     console.error('❌ Error syncing session stats:', error);
  1642.     return res.status(500).json({
  1643.       success: false,
  1644.       error: error.message
  1645.     });
  1646.   }
  1647. });
  1648. // ===============================
  1649. // GET /api/academy/mentors/all-with-stats-filtered
  1650. // Ментори с филтрирани статистики по период
  1651. // ===============================
  1652. academyController.get(
  1653.   '/mentors/all-with-stats-filtered',
  1654.   isAuth,
  1655.   rbac.checkPermission('statistics', 'read'),
  1656.   statisticsCache(120),
  1657.   async (req, res, next) => {
  1658.     try {
  1659.       const { timeFilter = 'thisMonth' } = req.query;
  1660.  
  1661.       // Изчисли date range
  1662.       const now = new Date();
  1663.       let startDate;
  1664.  
  1665.       switch (timeFilter) {
  1666.         case 'thisMonth':
  1667.           startDate = new Date(now.getFullYear(), now.getMonth(), 1);
  1668.           break;
  1669.         case 'lastMonth':
  1670.           startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
  1671.           break;
  1672.         case 'last3Months':
  1673.           startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1);
  1674.           break;
  1675.         case 'allTime':
  1676.           startDate = new Date(2020, 0, 1); // От началото на проекта
  1677.           break;
  1678.         default:
  1679.           startDate = new Date(now.getFullYear(), now.getMonth(), 1);
  1680.       }
  1681.  
  1682.       const { getFilteredMentorStats } = require('../services/mentorActivitySnapshotService');
  1683.       const mentors = await getFilteredMentorStats(startDate, now);
  1684.  
  1685.       res.status(200).json({
  1686.         success: true,
  1687.         mentors,
  1688.         total: mentors.length,
  1689.         timeFilter,
  1690.         startDate: startDate.toISOString(),
  1691.         endDate: now.toISOString()
  1692.       });
  1693.  
  1694.     } catch (err) {
  1695.       console.error('❌ [FILTERED-STATS] Error:', err);
  1696.       next(err);
  1697.     }
  1698.   }
  1699. );
  1700.  
  1701. // ===============================
  1702. // POST /api/academy/mentors/:mentorId/apply-student
  1703. // Student applies to a mentor
  1704. // ===============================
  1705. academyController.post(
  1706.   '/mentors/:mentorId/apply-student',
  1707.   isAuth,
  1708.   async (req, res, next) => {
  1709.     try {
  1710.       const mentorId = parseInt(req.params.mentorId);
  1711.       const userId = req.user.userId;
  1712.       const userRole = req.user.role;
  1713.  
  1714.       const {
  1715.         student_mentor_application,
  1716.         mentor,
  1717.         user_account,
  1718.         student
  1719.       } = require('../sequelize/models/index');
  1720.  
  1721.       // ✅ 1. ПРОВЕРКА: Admin и Mentor НЕ могат да кандидатстват
  1722.       if (userRole === 'admin' || userRole === 'mentor') {
  1723.         return res.status(403).json({
  1724.           success: false,
  1725.           message: 'Administrators and mentors cannot apply for mentors'
  1726.         });
  1727.       }
  1728.  
  1729.       // ✅ 2. ПРОВЕРКА: Менторът съществува и е активен
  1730.       const mentorData = await mentor.findByPk(mentorId);
  1731.  
  1732.       if (!mentorData) {
  1733.         return res.status(404).json({
  1734.           success: false,
  1735.           message: 'Mentor not found'
  1736.         });
  1737.       }
  1738.  
  1739.       if (mentorData.status !== 'active') {
  1740.         return res.status(400).json({
  1741.           success: false,
  1742.           message: 'This mentor is not currently accepting students'
  1743.         });
  1744.       }
  1745.  
  1746.       // ✅ 3. ПРОВЕРКА: Дали вече има pending заявка към ТОЗИ ментор
  1747.       const existingApplication = await student_mentor_application.findOne({
  1748.         where: {
  1749.           userId: userId,
  1750.           mentorId: mentorId,
  1751.           status: 'pending'
  1752.         }
  1753.       });
  1754.  
  1755.       if (existingApplication) {
  1756.         return res.status(400).json({
  1757.           success: false,
  1758.           message: 'You already have a pending application to this mentor'
  1759.         });
  1760.       }
  1761.       // ✅ 3.5. ПРОВЕРКА: Дали има APPROVED заявка към ТОЗИ ментор
  1762.       const approvedApplication = await student_mentor_application.findOne({
  1763.         where: {
  1764.           userId: userId,
  1765.           mentorId: mentorId,
  1766.           status: 'approved'
  1767.         }
  1768.       });
  1769.  
  1770.       if (approvedApplication) {
  1771.         return res.status(400).json({
  1772.           success: false,
  1773.           message: 'You are already assigned to this mentor'
  1774.         });
  1775.       }
  1776.  
  1777.       // ✅ 4. СЪЗДАЙ APPLICATION
  1778.       const newApplication = await student_mentor_application.create({
  1779.         userId: userId,
  1780.         mentorId: mentorId,
  1781.         status: 'pending'
  1782.       });
  1783.  
  1784.       // ✅ 5. СЪЗДАЙ ADMIN NOTIFICATION
  1785.       await require('../sequelize/models/index').admin_notification.create({
  1786.         type: 'student_application',
  1787.         title: 'New Student Application',
  1788.         message: `User applied to mentor ${mentorData.name}`,
  1789.         data: {
  1790.           applicationId: newApplication.id,
  1791.           userId: userId,
  1792.           mentorId: mentorId
  1793.         },
  1794.         isRead: false
  1795.       });
  1796.  
  1797.       res.status(201).json({
  1798.         success: true,
  1799.         message: 'Application sent successfully',
  1800.         application: newApplication
  1801.       });
  1802.  
  1803.     } catch (err) {
  1804.       console.error('❌ [APPLY FOR MENTOR] Error:', err);
  1805.       next(err);
  1806.     }
  1807.   }
  1808. );
  1809. // ===============================
  1810. // GET /api/academy/admin/student-applications
  1811. // Admin: Вземи всички student applications (всички ментори)
  1812. // ===============================
  1813. academyController.get(
  1814.   '/admin/student-applications',
  1815.   isAuth,
  1816.   rbac.checkPermission('studentApplication', 'readAll'),
  1817.   async (req, res, next) => {
  1818.     try {
  1819.       const { status } = req.query; // optional filter: pending, approved, rejected
  1820.  
  1821.       const {
  1822.         student_mentor_application,
  1823.         user_account,
  1824.         user_details,
  1825.         mentor
  1826.       } = require('../sequelize/models/index');
  1827.  
  1828.       // Build where clause
  1829.       const where = {};
  1830.       if (status && status !== 'all') {
  1831.         where.status = status;
  1832.       }
  1833.  
  1834.       // Вземи всички заявки
  1835.       const applications = await student_mentor_application.findAll({
  1836.         where,
  1837.         include: [
  1838.           {
  1839.             model: user_account,
  1840.             as: 'user',
  1841.             attributes: ['id', 'email', 'role'],
  1842.             include: [
  1843.               {
  1844.                 model: user_details,
  1845.                 as: 'details',
  1846.                 attributes: ['username', 'firstName', 'lastName', 'imageURL', 'phoneNumber']
  1847.               }
  1848.             ]
  1849.           },
  1850.           {
  1851.             model: mentor,
  1852.             as: 'mentor',
  1853.             attributes: ['id', 'name', 'email', 'photoUrl', 'specialization', 'phone']
  1854.           }
  1855.         ],
  1856.         order: [['createdAt', 'DESC']]
  1857.       });
  1858.  
  1859.       // Форматирай резултата
  1860.       const formattedApplications = applications.map(app => {
  1861.         const appData = app.get({ plain: true });
  1862.         const userName = appData.user?.details?.username ||
  1863.           `${appData.user?.details?.firstName || ''} ${appData.user?.details?.lastName || ''}`.trim() ||
  1864.           appData.user?.email?.split('@')[0] ||
  1865.           'Unknown';
  1866.  
  1867.         return {
  1868.           id: appData.id,
  1869.           // User data
  1870.           userId: appData.userId,
  1871.           userName: userName,
  1872.           userEmail: appData.user?.email,
  1873.           userAvatar: appData.user?.details?.imageURL,
  1874.           userPhone: appData.user?.details?.phoneNumber,
  1875.           userRole: appData.user?.role,
  1876.           // Mentor data
  1877.           mentorId: appData.mentorId,
  1878.           mentorName: appData.mentor?.name,
  1879.           mentorEmail: appData.mentor?.email,
  1880.           mentorPhoto: appData.mentor?.photoUrl,
  1881.           mentorSpecialization: appData.mentor?.specialization,
  1882.           mentorPhone: appData.mentor?.phone,
  1883.           // Application status
  1884.           status: appData.status,
  1885.           rejectionReason: appData.rejectionReason,
  1886.           approvedAt: appData.approvedAt,
  1887.           rejectedAt: appData.rejectedAt,
  1888.           createdAt: appData.createdAt,
  1889.           updatedAt: appData.updatedAt
  1890.         };
  1891.       });
  1892.  
  1893.       res.status(200).json({
  1894.         success: true,
  1895.         applications: formattedApplications,
  1896.         total: formattedApplications.length,
  1897.         statusCounts: {
  1898.           pending: formattedApplications.filter(a => a.status === 'pending').length,
  1899.           approved: formattedApplications.filter(a => a.status === 'approved').length,
  1900.           rejected: formattedApplications.filter(a => a.status === 'rejected').length
  1901.         }
  1902.       });
  1903.  
  1904.     } catch (err) {
  1905.       console.error('❌ [GET ALL STUDENT APPLICATIONS] Error:', err);
  1906.       next(err);
  1907.     }
  1908.   }
  1909. );
  1910.  
  1911. // ===============================
  1912. // POST /api/academy/admin/student-applications/:id/approve
  1913. // ===============================
  1914. academyController.post(
  1915.   '/admin/student-applications/:id/approve',
  1916.   isAuth,
  1917.   rbac.checkPermission('studentApplication', 'update'),
  1918.   async (req, res, next) => {
  1919.     try {
  1920.       const applicationId = parseInt(req.params.id);
  1921.  
  1922.       const {
  1923.         student_mentor_application,
  1924.         student,
  1925.         user_account,
  1926.         user_details,
  1927.         mentor,
  1928.         mentor_history,
  1929.         admin_notification,
  1930.         user_notification
  1931.       } = require('../sequelize/models/index');
  1932.       const { tokenGenerator } = require('../utils/jwt');
  1933.       const { refreshToken } = require('../sequelize/models/index');
  1934.  
  1935.       const application = await student_mentor_application.findByPk(applicationId);
  1936.  
  1937.       if (!application) {
  1938.         return res.status(404).json({
  1939.           success: false,
  1940.           message: 'Application not found'
  1941.         });
  1942.       }
  1943.  
  1944.       if (application.status !== 'pending') {
  1945.         return res.status(400).json({
  1946.           success: false,
  1947.           message: 'Application is already processed'
  1948.         });
  1949.       }
  1950.  
  1951.       const user = await user_account.findByPk(application.userId);
  1952.  
  1953.       if (!user) {
  1954.         return res.status(404).json({
  1955.           success: false,
  1956.           message: 'User not found'
  1957.         });
  1958.       }
  1959.  
  1960.       let userDetailsData = await user_details.findOne({
  1961.         where: { userAccountsId: application.userId }
  1962.       });
  1963.  
  1964.       if (!userDetailsData) {
  1965.         userDetailsData = await user_details.create({
  1966.           userAccountsId: application.userId,
  1967.           username: user.email.split('@')[0],
  1968.           workOptions: [],
  1969.           skills: [],
  1970.           interestOptions: []
  1971.         });
  1972.       }
  1973.  
  1974.       if (user.role !== 'student') {
  1975.         await user.update({ role: 'student' });
  1976.  
  1977.         const { token } = tokenGenerator('access', user.dataValues);
  1978.         const { token: refreshJwtToken, refreshTokenId, expiryDate } = tokenGenerator('refresh', user.dataValues);
  1979.  
  1980.         await refreshToken.destroy({ where: { userId: user.id } });
  1981.         await refreshToken.create({
  1982.           userId: user.id,
  1983.           token: refreshTokenId,
  1984.           expiryDate
  1985.         });
  1986.       }
  1987.  
  1988.       let studentData = await student.findOne({
  1989.         where: { userId: application.userId }
  1990.       });
  1991.  
  1992.       const isNewStudent = !studentData;
  1993.       const oldMentorId = studentData?.currentMentorId || null;
  1994.  
  1995.       if (studentData) {
  1996.         if (studentData.currentMentorId) {
  1997.           const oldMentor = await mentor.findByPk(studentData.currentMentorId);
  1998.           if (oldMentor) {
  1999.             await oldMentor.update({
  2000.               studentsCount: Math.max(0, oldMentor.studentsCount - 1)
  2001.             });
  2002.           }
  2003.         }
  2004.  
  2005.         await studentData.update({
  2006.           currentMentorId: application.mentorId,
  2007.           mentorAssignedDate: new Date(),
  2008.           status: 'active'
  2009.         });
  2010.  
  2011.       } else {
  2012.         studentData = await student.create({
  2013.           userId: application.userId,
  2014.           currentMentorId: application.mentorId,
  2015.           mentorAssignedDate: new Date(),
  2016.           status: 'active',
  2017.           country: 'BG'
  2018.         });
  2019.       }
  2020.  
  2021.       const mentorData = await mentor.findByPk(application.mentorId);
  2022.       await mentorData.update({
  2023.         studentsCount: mentorData.studentsCount + 1
  2024.       });
  2025.  
  2026.       if (!isNewStudent && oldMentorId && oldMentorId !== application.mentorId) {
  2027.         const oldHistory = await mentor_history.findOne({
  2028.           where: {
  2029.             studentId: studentData.id,
  2030.             periodEnd: null
  2031.           },
  2032.           order: [['periodStart', 'DESC']]
  2033.         });
  2034.  
  2035.         if (oldHistory) {
  2036.           await oldHistory.update({
  2037.             periodEnd: new Date(),
  2038.             reason: 'Reassigned to new mentor'
  2039.           });
  2040.         }
  2041.       }
  2042.  
  2043.       await mentor_history.create({
  2044.         studentId: studentData.id,
  2045.         mentorId: application.mentorId,
  2046.         mentorName: mentorData.name,
  2047.         periodStart: new Date(),
  2048.         periodEnd: null,
  2049.         reason: isNewStudent ? 'Initial assignment' : 'Reassigned from another mentor'
  2050.       });
  2051.  
  2052.       await application.update({
  2053.         status: 'approved',
  2054.         approvedAt: new Date()
  2055.       });
  2056.  
  2057.       await admin_notification.create({
  2058.         type: 'student_application_approved_by_admin',
  2059.         title: 'Заявка за студент одобрена от админ',
  2060.         message: `Администраторът одобри заявка за студент към ментор ${mentorData.name}`,
  2061.         data: {
  2062.           applicationId: application.id,
  2063.           userId: application.userId,
  2064.           mentorId: application.mentorId,
  2065.           studentId: studentData.id,
  2066.           approvedBy: 'admin'
  2067.         },
  2068.         isRead: false
  2069.       });
  2070.  
  2071.       // ✅ USER NOTIFICATION
  2072.       await user_notification.create({
  2073.         userId: application.userId,
  2074.         type: 'student_application_approved',
  2075.         title: 'Вашата заявка е одобрена! 🎉',
  2076.         message: `Администраторът одобри вашата заявка към ментор ${mentorData.name}. Вече можете да започнете обучението си!`,
  2077.         data: {
  2078.           applicationId: application.id,
  2079.           mentorId: application.mentorId,
  2080.           mentorName: mentorData.name,
  2081.           mentorEmail: mentorData.email,
  2082.           mentorPhoto: mentorData.photoUrl,
  2083.           studentId: studentData.id,
  2084.           approvedBy: 'admin'
  2085.         },
  2086.         read: false
  2087.       });
  2088.  
  2089.       res.status(200).json({
  2090.         success: true,
  2091.         message: 'Application approved successfully',
  2092.         application: application,
  2093.         student: studentData,
  2094.         userEmail: user.email
  2095.       });
  2096.  
  2097.     } catch (err) {
  2098.       console.error('❌ [ADMIN APPROVE APPLICATION] Error:', err);
  2099.       next(err);
  2100.     }
  2101.   }
  2102. );
  2103.  
  2104. // ===============================
  2105. // POST /api/academy/admin/student-applications/:id/reject
  2106. // ===============================
  2107. academyController.post(
  2108.   '/admin/student-applications/:id/reject',
  2109.   isAuth,
  2110.   rbac.checkPermission('studentApplication', 'update'),
  2111.   async (req, res, next) => {
  2112.     try {
  2113.       const applicationId = parseInt(req.params.id);
  2114.  
  2115.       const { rejectApplicationSchema } = require('../schemas/studentMentorApplication.schema');
  2116.       const { student_mentor_application, mentor, admin_notification, user_notification } = require('../sequelize/models/index');
  2117.  
  2118.       const validationResult = rejectApplicationSchema.safeParse(req.body);
  2119.  
  2120.       if (!validationResult.success) {
  2121.         return res.status(400).json({
  2122.           success: false,
  2123.           message: 'Validation failed',
  2124.           errors: validationResult.error.errors
  2125.         });
  2126.       }
  2127.  
  2128.       const application = await student_mentor_application.findByPk(applicationId);
  2129.  
  2130.       if (!application) {
  2131.         return res.status(404).json({
  2132.           success: false,
  2133.           message: 'Application not found'
  2134.         });
  2135.       }
  2136.  
  2137.       if (application.status !== 'pending') {
  2138.         return res.status(400).json({
  2139.           success: false,
  2140.           message: 'Application is already processed'
  2141.         });
  2142.       }
  2143.  
  2144.       await application.update({
  2145.         status: 'rejected',
  2146.         rejectionReason: validationResult.data.rejectionReason,
  2147.         rejectedAt: new Date()
  2148.       });
  2149.  
  2150.       const mentorData = await mentor.findByPk(application.mentorId);
  2151.  
  2152.       await admin_notification.create({
  2153.         type: 'student_application_rejected_by_admin',
  2154.         title: 'Заявка за студент отхвърлена от админ',
  2155.         message: `Администраторът отхвърли заявка за студент към ментор ${mentorData.name}`,
  2156.         data: {
  2157.           applicationId: application.id,
  2158.           userId: application.userId,
  2159.           mentorId: application.mentorId,
  2160.           rejectionReason: validationResult.data.rejectionReason,
  2161.           rejectedBy: 'admin'
  2162.         },
  2163.         isRead: false
  2164.       });
  2165.  
  2166.       // ✅ USER NOTIFICATION
  2167.       await user_notification.create({
  2168.         userId: application.userId,
  2169.         type: 'student_application_rejected',
  2170.         title: 'Вашата заявка не беше одобрена',
  2171.         message: `За съжаление, вашата заявка към ментор ${mentorData.name} не беше одобрена.`,
  2172.         data: {
  2173.           applicationId: application.id,
  2174.           mentorId: application.mentorId,
  2175.           mentorName: mentorData.name,
  2176.           mentorPhoto: mentorData.photoUrl,
  2177.           rejectionReason: validationResult.data.rejectionReason,
  2178.           rejectedBy: 'admin'
  2179.         },
  2180.         read: false
  2181.       });
  2182.  
  2183.       res.status(200).json({
  2184.         success: true,
  2185.         message: 'Application rejected successfully',
  2186.         application: application,
  2187.         userEmail: user.email
  2188.       });
  2189.  
  2190.     } catch (err) {
  2191.       console.error('❌ [ADMIN REJECT APPLICATION] Error:', err);
  2192.       next(err);
  2193.     }
  2194.   }
  2195. );
  2196.  
  2197. // ===============================
  2198. // POST /api/academy/admin/student-applications/:id/reapprove
  2199. // Admin: Повторно одобри отхвърлена заявка
  2200. // ===============================
  2201. academyController.post(
  2202.   '/admin/student-applications/:id/reapprove',
  2203.   isAuth,
  2204.   rbac.checkPermission('studentApplication', 'update'),
  2205.   async (req, res, next) => {
  2206.     try {
  2207.       const applicationId = parseInt(req.params.id);
  2208.  
  2209.       const {
  2210.         student_mentor_application,
  2211.         student,
  2212.         user_account,
  2213.         mentor,
  2214.         mentor_history,
  2215.         user_notification,
  2216.         admin_notification
  2217.       } = require('../sequelize/models/index');
  2218.       const { tokenGenerator, refreshToken } = require('../utils/jwt');
  2219.  
  2220.       // ✅ 1. ВЗЕМИ APPLICATION
  2221.       const application = await student_mentor_application.findByPk(applicationId);
  2222.  
  2223.       if (!application) {
  2224.         return res.status(404).json({
  2225.           success: false,
  2226.           message: 'Application not found'
  2227.         });
  2228.       }
  2229.  
  2230.       // ✅ 2. ПРОВЕРИ ДАЛИ Е REJECTED
  2231.       if (application.status !== 'rejected') {
  2232.         return res.status(400).json({
  2233.           success: false,
  2234.           message: `Can only reapprove rejected applications. Current status: ${application.status}`
  2235.         });
  2236.       }
  2237.  
  2238.       // ✅ 3. ВЗЕМИ USER DATA
  2239.       const user = await user_account.findByPk(application.userId);
  2240.  
  2241.       if (!user) {
  2242.         return res.status(404).json({
  2243.           success: false,
  2244.           message: 'User not found'
  2245.         });
  2246.       }
  2247.  
  2248.       // ✅ 4. ПРОВЕРИ ДАЛИ МЕНТОРЪТ Е АКТИВЕН
  2249.       const mentorData = await mentor.findByPk(application.mentorId);
  2250.  
  2251.       if (!mentorData) {
  2252.         return res.status(404).json({
  2253.           success: false,
  2254.           message: 'Mentor not found'
  2255.         });
  2256.       }
  2257.  
  2258.       if (mentorData.status !== 'active') {
  2259.         return res.status(400).json({
  2260.           success: false,
  2261.           message: 'Cannot assign student to inactive mentor'
  2262.         });
  2263.       }
  2264.  
  2265.       // ✅ 5. ПРОМЕНИ ROLE НА 'student' (ако не е вече)
  2266.       if (user.role !== 'student' && user.role !== 'admin' && user.role !== 'mentor') {
  2267.         await user.update({ role: 'student' });
  2268.       }
  2269.  
  2270.       // ✅ 6. ПРОВЕРИ ДАЛИ СТУДЕНТЪТ ВЕЧЕ СЪЩЕСТВУВА
  2271.       let studentData = await student.findOne({
  2272.         where: { userId: application.userId }
  2273.       });
  2274.  
  2275.       const isNewStudent = !studentData;
  2276.       const oldMentorId = studentData?.currentMentorId || null;
  2277.  
  2278.       if (studentData) {
  2279.         // ✅ СТУДЕНТЪТ СЪЩЕСТВУВА - ЗАМЕНИ МЕНТОРА
  2280.  
  2281.         // Намали studentsCount на стария ментор
  2282.         if (studentData.currentMentorId && studentData.currentMentorId !== application.mentorId) {
  2283.           const oldMentor = await mentor.findByPk(studentData.currentMentorId);
  2284.           if (oldMentor) {
  2285.             await oldMentor.update({
  2286.               studentsCount: Math.max(0, oldMentor.studentsCount - 1)
  2287.             });
  2288.           }
  2289.         }
  2290.  
  2291.         // Обнови студента с новия ментор
  2292.         await studentData.update({
  2293.           currentMentorId: application.mentorId,
  2294.           mentorAssignedDate: new Date(),
  2295.           status: 'active'
  2296.         });
  2297.  
  2298.       } else {
  2299.         // ✅ СТУДЕНТЪТ НЕ СЪЩЕСТВУВА - СЪЗДАЙ ГО
  2300.         studentData = await student.create({
  2301.           userId: application.userId,
  2302.           currentMentorId: application.mentorId,
  2303.           mentorAssignedDate: new Date(),
  2304.           status: 'active',
  2305.           country: 'BG'
  2306.         });
  2307.       }
  2308.  
  2309.       // ✅ 7. УВЕЛИЧИ studentsCount НА НОВИЯ МЕНТОР (само ако е различен)
  2310.       if (oldMentorId !== application.mentorId) {
  2311.         await mentorData.update({
  2312.           studentsCount: mentorData.studentsCount + 1
  2313.         });
  2314.       }
  2315.  
  2316.       // ✅ 8. СЪЗДАЙ MENTOR HISTORY ЗАПИС
  2317.       if (!isNewStudent && oldMentorId && oldMentorId !== application.mentorId) {
  2318.         // Завърши стария период
  2319.         const oldHistory = await mentor_history.findOne({
  2320.           where: {
  2321.             studentId: studentData.id,
  2322.             periodEnd: null
  2323.           },
  2324.           order: [['periodStart', 'DESC']]
  2325.         });
  2326.  
  2327.         if (oldHistory) {
  2328.           await oldHistory.update({
  2329.             periodEnd: new Date(),
  2330.             reason: 'Reassigned via reapproval'
  2331.           });
  2332.         }
  2333.       }
  2334.  
  2335.       // Създай нов history запис
  2336.       await mentor_history.create({
  2337.         studentId: studentData.id,
  2338.         mentorId: application.mentorId,
  2339.         mentorName: mentorData.name,
  2340.         periodStart: new Date(),
  2341.         periodEnd: null,
  2342.         reason: isNewStudent ? 'Initial assignment (reapproved)' : 'Reapproved by admin'
  2343.       });
  2344.  
  2345.       // ✅ 9. ОБНОВИ APPLICATION STATUS
  2346.       await application.update({
  2347.         status: 'approved',
  2348.         approvedAt: new Date(),
  2349.         rejectionReason: null,
  2350.         rejectedAt: null
  2351.       });
  2352.  
  2353.       await admin_notification.create({
  2354.         type: 'student_application_reapproved',
  2355.         title: 'Заявка за студент одобрена отново',
  2356.         message: `Предишно отхвърлена заявка беше одобрена за ментор ${mentorData.name}`,
  2357.         data: {
  2358.           applicationId: application.id,
  2359.           userId: application.userId,
  2360.           mentorId: application.mentorId,
  2361.           studentId: studentData.id,
  2362.           reapprovedBy: 'admin'
  2363.         },
  2364.         read: false
  2365.       });
  2366.  
  2367.       await user_notification.create({
  2368.         userId: application.userId,
  2369.         type: 'application_reapproved',
  2370.         title: 'Вашата кандидатура е одобрена! 🎉',
  2371.         message: `Радваме се да ви съобщим, че вашата кандидатура към ментор ${mentorData.name} беше одобрена!`,
  2372.         data: {
  2373.           mentorId: application.mentorId,
  2374.           mentorName: mentorData.name,
  2375.           mentorEmail: mentorData.email,
  2376.           mentorPhoto: mentorData.photoUrl
  2377.         },
  2378.         read: false
  2379.       });
  2380.  
  2381.       res.status(200).json({
  2382.         success: true,
  2383.         message: 'Application reapproved successfully',
  2384.         application: application,
  2385.         student: studentData,
  2386.         userEmail: user.email
  2387.       });
  2388.  
  2389.     } catch (err) {
  2390.       console.error('❌ [ADMIN REAPPROVE APPLICATION] Error:', err);
  2391.       next(err);
  2392.     }
  2393.   }
  2394. );
  2395.  
  2396. // ===============================
  2397. // DELETE /api/academy/admin/student-applications/:id
  2398. // Admin: Изтрий заявка
  2399. // ===============================
  2400. academyController.delete(
  2401.   '/admin/student-applications/:id',
  2402.   isAuth,
  2403.   rbac.checkPermission('studentApplication', 'delete'),
  2404.   async (req, res, next) => {
  2405.     try {
  2406.       const applicationId = parseInt(req.params.id);
  2407.  
  2408.       const { student_mentor_application } = require('../sequelize/models/index');
  2409.  
  2410.       // Намери application
  2411.       const application = await student_mentor_application.findByPk(applicationId);
  2412.  
  2413.       if (!application) {
  2414.         return res.status(404).json({
  2415.           success: false,
  2416.           message: 'Application not found'
  2417.         });
  2418.       }
  2419.  
  2420.       // Изтрий application
  2421.       await application.destroy();
  2422.  
  2423.       res.status(200).json({
  2424.         success: true,
  2425.         message: 'Application deleted successfully',
  2426.         userEmail: user?.email || null
  2427.       });
  2428.  
  2429.     } catch (err) {
  2430.       console.error('❌ [ADMIN DELETE APPLICATION] Error:', err);
  2431.       next(err);
  2432.     }
  2433.   }
  2434. );
  2435. // ===============================
  2436. // ============ ADMIN STUDENTS MANAGEMENT ============
  2437. // ===============================
  2438.  
  2439. // ===============================
  2440. // GET /api/academy/admin/students
  2441. // Вземи всички студенти с филтри и пагинация
  2442. // ===============================
  2443. academyController.get(
  2444.   '/admin/students',
  2445.   isAuth,
  2446.   rbac.checkPermission('student', 'read'),
  2447.   async (req, res, next) => {
  2448.     try {
  2449.       const {
  2450.         page = 1,
  2451.         limit = 12,
  2452.         search = '',
  2453.         status = 'all',
  2454.         mentorId = null,
  2455.         sortBy = 'newest',
  2456.       } = req.query;
  2457.  
  2458.       const pageNum = parseInt(page);
  2459.       const limitNum = parseInt(limit);
  2460.       const offset = (pageNum - 1) * limitNum;
  2461.  
  2462.       // Build WHERE clause
  2463.       const where = {};
  2464.  
  2465.       // Filter by status - ✅ ДОБАВЕНА ПРОВЕРКА
  2466.       if (status && status !== 'all' && status !== 'undefined') {
  2467.         where.status = status;
  2468.       }
  2469.  
  2470.       // Filter by mentor - ✅ ДОБАВЕНА ПРОВЕРКА
  2471.       if (mentorId && mentorId !== 'all' && mentorId !== 'undefined' && mentorId !== 'null') {
  2472.         where.currentMentorId = parseInt(mentorId);
  2473.       }
  2474.  
  2475.       // Search by name or email - ✅ ПОПРАВЕНО: snake_case колони
  2476.       if (search && search !== 'undefined' && search.trim() !== '') {
  2477.         where[Op.or] = [
  2478.           { '$user.details.username$': { [Op.iLike]: `%${search}%` } },
  2479.           { '$user.details.first_name$': { [Op.iLike]: `%${search}%` } },
  2480.           { '$user.details.last_name$': { [Op.iLike]: `%${search}%` } },
  2481.           { '$user.email$': { [Op.iLike]: `%${search}%` } },
  2482.         ];
  2483.       }
  2484.  
  2485.       // Sort order
  2486.       let order = [['createdAt', 'DESC']];
  2487.  
  2488.       switch (sortBy) {
  2489.         case 'oldest':
  2490.           order = [['createdAt', 'ASC']];
  2491.           break;
  2492.         case 'name':
  2493.           order = [[{ model: user_account, as: 'user' }, { model: user_details, as: 'details' }, 'username', 'ASC']];
  2494.           break;
  2495.         case 'credits':
  2496.           order = [['totalCreditsEarned', 'DESC']];
  2497.           break;
  2498.         case 'attendance':
  2499.           order = [['attendedSessions', 'DESC']];
  2500.           break;
  2501.         default:
  2502.           order = [['createdAt', 'DESC']];
  2503.       }
  2504.  
  2505.       const { count, rows: students } = await student.findAndCountAll({
  2506.         where,
  2507.         include: [
  2508.           {
  2509.             model: user_account,
  2510.             as: 'user',
  2511.             attributes: ['id', 'email', 'role'],
  2512.             required: true,
  2513.             include: [
  2514.               {
  2515.                 model: user_details,
  2516.                 as: 'details',
  2517.                 required: false,
  2518.                 attributes: ['username', 'firstName', 'lastName', 'imageURL', 'phoneNumber']
  2519.               }
  2520.             ]
  2521.           },
  2522.           {
  2523.             model: mentor,
  2524.             as: 'currentMentor',
  2525.             required: false,
  2526.             attributes: ['id', 'name', 'email', 'photoUrl']
  2527.           }
  2528.         ],
  2529.         limit: limitNum,
  2530.         offset,
  2531.         order,
  2532.         distinct: true,
  2533.         subQuery: false
  2534.       });
  2535.  
  2536.       // Форматирай резултата
  2537.       const formattedStudents = students.map(s => {
  2538.         const studentData = s.get({ plain: true });
  2539.         const name = studentData.user?.details?.username ||
  2540.           `${studentData.user?.details?.firstName || ''} ${studentData.user?.details?.lastName || ''}`.trim() ||
  2541.           studentData.user?.email?.split('@')[0] ||
  2542.           'Unknown';
  2543.  
  2544.         const attendanceRate = studentData.totalScheduledSessions > 0
  2545.           ? Math.round((studentData.attendedSessions / studentData.totalScheduledSessions) * 100)
  2546.           : 0;
  2547.  
  2548.         return {
  2549.           id: studentData.id,
  2550.           name: name,
  2551.           email: studentData.user?.email,
  2552.           avatar: studentData.avatar || studentData.user?.details?.imageURL || null,
  2553.           phone: studentData.phone || studentData.user?.details?.phoneNumber || null,
  2554.           status: studentData.status,
  2555.           totalCreditsEarned: studentData.totalCreditsEarned,
  2556.           attendanceRate: attendanceRate,
  2557.           currentMentor: studentData.currentMentor ? {
  2558.             id: studentData.currentMentor.id,
  2559.             name: studentData.currentMentor.name,
  2560.             photoUrl: studentData.currentMentor.photoUrl
  2561.           } : null,
  2562.           registrationDate: studentData.registrationDate,
  2563.           lastActiveAt: studentData.lastActiveAt
  2564.         };
  2565.       });
  2566.  
  2567.       const totalPages = Math.ceil(count / limitNum);
  2568.  
  2569.       res.status(200).json({
  2570.         success: true,
  2571.         students: formattedStudents,
  2572.         pagination: {
  2573.           page: pageNum,
  2574.           limit: limitNum,
  2575.           total: count,
  2576.           totalPages,
  2577.         },
  2578.       });
  2579.  
  2580.     } catch (err) {
  2581.       console.error('❌ [GET ADMIN STUDENTS] Error:', err);
  2582.       next(err);
  2583.     }
  2584.   }
  2585. );
  2586.  
  2587. // ===============================
  2588. // GET /api/academy/admin/students/:id
  2589. // Детайли за студент (ПЪЛНИ)
  2590. // ===============================
  2591. academyController.get(
  2592.   '/admin/students/:id',
  2593.   isAuth,
  2594.   rbac.checkPermission('student', 'read'),
  2595.   async (req, res, next) => {
  2596.     try {
  2597.       const studentId = parseInt(req.params.id);
  2598.  
  2599.       const {
  2600.         student_course,
  2601.         course,
  2602.         student_lecture,
  2603.         lecture,
  2604.         student_seminar,
  2605.         seminar,
  2606.         student_presentation,
  2607.         presentation,
  2608.         mentor_history
  2609.       } = require('../sequelize/models/index');
  2610.  
  2611.       const studentData = await student.findByPk(studentId, {
  2612.         include: [
  2613.           {
  2614.             model: user_account,
  2615.             as: 'user',
  2616.             attributes: ['id', 'email', 'role'],
  2617.             required: true,
  2618.             include: [
  2619.               {
  2620.                 model: user_details,
  2621.                 as: 'details',
  2622.                 required: false,
  2623.                 attributes: ['username', 'firstName', 'lastName', 'phoneNumber', 'imageURL', 'birthDate', 'region']
  2624.               }
  2625.             ]
  2626.           },
  2627.           {
  2628.             model: mentor,
  2629.             as: 'currentMentor',
  2630.             required: false,
  2631.             attributes: ['id', 'name', 'email', 'photoUrl', 'specialization']
  2632.           },
  2633.           {
  2634.             model: student_course,
  2635.             as: 'courses',
  2636.             required: false,
  2637.             include: [
  2638.               {
  2639.                 model: course,
  2640.                 as: 'course',
  2641.                 required: false,
  2642.                 attributes: ['id', 'name', 'category', 'thumbnailUrl']
  2643.               }
  2644.             ]
  2645.           },
  2646.           {
  2647.             model: student_lecture,
  2648.             as: 'lectures',
  2649.             required: false,
  2650.             include: [
  2651.               {
  2652.                 model: lecture,
  2653.                 as: 'lecture',
  2654.                 required: false,
  2655.                 attributes: ['id', 'title', 'scheduledDate', 'durationMinutes']
  2656.               }
  2657.             ]
  2658.           },
  2659.           {
  2660.             model: student_seminar,
  2661.             as: 'seminars',
  2662.             required: false,
  2663.             include: [
  2664.               {
  2665.                 model: seminar,
  2666.                 as: 'seminar',
  2667.                 required: false,
  2668.                 attributes: ['id', 'title', 'scheduledDate', 'durationMinutes']
  2669.               }
  2670.             ]
  2671.           },
  2672.           {
  2673.             model: student_presentation,
  2674.             as: 'presentations',
  2675.             required: false,
  2676.             include: [
  2677.               {
  2678.                 model: presentation,
  2679.                 as: 'presentation',
  2680.                 required: false,
  2681.                 attributes: ['id', 'title', 'dueDate', 'maxCredits']
  2682.               }
  2683.             ]
  2684.           },
  2685.           {
  2686.             model: mentor_history,
  2687.             as: 'mentorHistory',
  2688.             required: false,
  2689.             include: [
  2690.               {
  2691.                 model: mentor,
  2692.                 as: 'mentor',
  2693.                 required: false,
  2694.                 attributes: ['id', 'name', 'photoUrl']
  2695.               }
  2696.             ]
  2697.           }
  2698.         ]
  2699.       });
  2700.  
  2701.       if (!studentData) {
  2702.         return res.status(404).json({
  2703.           success: false,
  2704.           message: 'Student not found'
  2705.         });
  2706.       }
  2707.  
  2708.       const studentObj = studentData.get({ plain: true });
  2709.  
  2710.       const attendanceRate = studentObj.totalScheduledSessions > 0
  2711.         ? Math.round((studentObj.attendedSessions / studentObj.totalScheduledSessions) * 100)
  2712.         : 0;
  2713.  
  2714.       const studentName = studentObj.user?.details?.username ||
  2715.         `${studentObj.user?.details?.firstName || ''} ${studentObj.user?.details?.lastName || ''}`.trim() ||
  2716.         studentObj.user?.email?.split('@')[0] ||
  2717.         'Unknown';
  2718.  
  2719.       const formattedCourses = (studentObj.courses || []).map(sc => ({
  2720.         id: sc.id,
  2721.         courseId: sc.courseId,
  2722.         courseName: sc.course?.name || 'Unknown Course',
  2723.         category: sc.course?.category || null,
  2724.         thumbnailUrl: sc.course?.thumbnailUrl || null,
  2725.         status: sc.status,
  2726.         progress: sc.progress,
  2727.         completedLessons: sc.completedLessons,
  2728.         totalLessons: sc.totalLessons,
  2729.         earnedCredits: sc.earnedCredits,
  2730.         maxCredits: sc.maxCredits,
  2731.         startDate: sc.startDate,
  2732.         endDate: sc.endDate
  2733.       }));
  2734.  
  2735.       const formattedLectures = (studentObj.lectures || []).map(sl => ({
  2736.         id: sl.id,
  2737.         lectureId: sl.lectureId,
  2738.         title: sl.lecture?.title || 'Unknown Lecture',
  2739.         date: sl.lecture?.scheduledDate || null,
  2740.         duration: sl.lecture?.durationMinutes || 0,
  2741.         attended: sl.attended,
  2742.         attendedAt: sl.attendedAt,
  2743.         earnedCredits: sl.earnedCredits
  2744.       }));
  2745.  
  2746.       const formattedSeminars = (studentObj.seminars || []).map(ss => ({
  2747.         id: ss.id,
  2748.         seminarId: ss.seminarId,
  2749.         title: ss.seminar?.title || 'Unknown Seminar',
  2750.         date: ss.seminar?.scheduledDate || null,
  2751.         duration: ss.seminar?.durationMinutes || 0,
  2752.         attended: ss.attended,
  2753.         attendedAt: ss.attendedAt,
  2754.         earnedCredits: ss.earnedCredits
  2755.       }));
  2756.  
  2757.       const formattedPresentations = (studentObj.presentations || []).map(sp => ({
  2758.         id: sp.id,
  2759.         presentationId: sp.presentationId,
  2760.         title: sp.presentation?.title || 'Unknown Presentation',
  2761.         dueDate: sp.presentation?.dueDate || null,
  2762.         status: sp.status,
  2763.         submittedAt: sp.submittedAt,
  2764.         gradedAt: sp.gradedAt,
  2765.         earnedCredits: sp.earnedCredits,
  2766.         maxCredits: sp.presentation?.maxCredits || 0
  2767.       }));
  2768.  
  2769.       const formattedMentorHistory = (studentObj.mentorHistory || []).map(mh => ({
  2770.         id: mh.id,
  2771.         mentorId: mh.mentorId,
  2772.         mentorName: mh.mentorName,
  2773.         mentorPhoto: mh.mentor?.photoUrl || null,
  2774.         periodStart: mh.periodStart,
  2775.         periodEnd: mh.periodEnd,
  2776.         reason: mh.reason
  2777.       }));
  2778.  
  2779.       const formattedStudent = {
  2780.         id: studentObj.id,
  2781.         name: studentName,
  2782.         avatar: studentObj.avatar || studentObj.user?.details?.imageURL || null,
  2783.         status: studentObj.status,
  2784.         registrationDate: studentObj.registrationDate,
  2785.         user: {
  2786.           id: studentObj.user.id,
  2787.           email: studentObj.user.email,
  2788.           phone: studentObj.phone || studentObj.user?.details?.phoneNumber || null,
  2789.           details: studentObj.user.details ? {
  2790.             username: studentObj.user.details.username,
  2791.             firstName: studentObj.user.details.firstName,
  2792.             lastName: studentObj.user.details.lastName,
  2793.             birthDate: studentObj.user.details.birthDate,
  2794.             region: studentObj.user.details.region
  2795.           } : null
  2796.         },
  2797.         credits: {
  2798.           totalEarned: studentObj.totalCreditsEarned,
  2799.           totalPossible: studentObj.totalCreditsPossible,
  2800.           breakdown: {
  2801.             fromCourses: studentObj.creditsFromCourses,
  2802.             fromLectures: studentObj.creditsFromLectures,
  2803.             fromSeminars: studentObj.creditsFromSeminars,
  2804.             fromPresentations: studentObj.creditsFromPresentations
  2805.           }
  2806.         },
  2807.         currentMentor: studentObj.currentMentor ? {
  2808.           id: studentObj.currentMentor.id,
  2809.           name: studentObj.currentMentor.name,
  2810.           email: studentObj.currentMentor.email,
  2811.           photoUrl: studentObj.currentMentor.photoUrl,
  2812.           specialization: studentObj.currentMentor.specialization,
  2813.           assignedDate: studentObj.mentorAssignedDate
  2814.         } : null,
  2815.         attendance: {
  2816.           totalScheduledSessions: studentObj.totalScheduledSessions,
  2817.           attendedSessions: studentObj.attendedSessions,
  2818.           missedSessions: studentObj.missedSessions,
  2819.           attendanceRate: attendanceRate
  2820.         },
  2821.         mentorHelp: {
  2822.           totalChatSessions: studentObj.totalChatSessions,
  2823.           totalChatHours: parseFloat(studentObj.totalChatHours),
  2824.           lastChatDate: studentObj.lastChatDate,
  2825.           scheduledMeetings: studentObj.scheduledMeetings,
  2826.           completedMeetings: studentObj.completedMeetings
  2827.         },
  2828.         adminNotes: studentObj.adminNotes,
  2829.         specialNeeds: studentObj.specialNeeds,
  2830.         courses: formattedCourses,
  2831.         lectures: formattedLectures,
  2832.         seminars: formattedSeminars,
  2833.         presentations: formattedPresentations,
  2834.         mentorHistory: formattedMentorHistory
  2835.       };
  2836.  
  2837.       res.status(200).json({
  2838.         success: true,
  2839.         student: formattedStudent
  2840.       });
  2841.  
  2842.     } catch (err) {
  2843.       console.error('❌ [GET ADMIN STUDENT DETAILS] Error:', err);
  2844.       next(err);
  2845.     }
  2846.   }
  2847. );
  2848.  
  2849. // ===============================
  2850. // PATCH /api/academy/admin/students/:id
  2851. // Редактирай студент
  2852. // ===============================
  2853. academyController.patch(
  2854.   '/admin/students/:id',
  2855.   isAuth,
  2856.   rbac.checkPermission('student', 'update'),
  2857.   async (req, res, next) => {
  2858.     try {
  2859.       const studentId = parseInt(req.params.id);
  2860.       const updates = req.body;
  2861.  
  2862.  
  2863.       const studentData = await student.findByPk(studentId);
  2864.  
  2865.       if (!studentData) {
  2866.         return res.status(404).json({
  2867.           success: false,
  2868.           message: 'Student not found'
  2869.         });
  2870.       }
  2871.  
  2872.       const allowedStudentFields = [
  2873.         'phone', 'avatar', 'address', 'city', 'country',
  2874.         'status', 'specialNeeds', 'adminNotes',
  2875.         'emergencyContactName', 'emergencyContactPhone',
  2876.         'preferredLanguage', 'preferredContactMethod', 'availabilityNotes'
  2877.       ];
  2878.  
  2879.       const filteredUpdates = {};
  2880.       allowedStudentFields.forEach(field => {
  2881.         if (updates[field] !== undefined) {
  2882.           filteredUpdates[field] = updates[field];
  2883.         }
  2884.       });
  2885.  
  2886.       if (updates.name !== undefined && updates.name.trim().length > 0) {
  2887.  
  2888.         let userDetailsData = await user_details.findOne({
  2889.           where: { userAccountsId: studentData.userId }
  2890.         });
  2891.  
  2892.  
  2893.         if (!userDetailsData) {
  2894.           // ✅ СЪЗДАЙ user_details ако не съществува
  2895.  
  2896.           userDetailsData = await user_details.create({
  2897.             userAccountsId: studentData.userId,
  2898.             username: updates.name.trim(),
  2899.             workOptions: [],
  2900.             skills: [],
  2901.             interestOptions: []
  2902.           });
  2903.  
  2904.         } else {
  2905.           // ✅ UPDATE съществуващ user_details
  2906.           await userDetailsData.update({ username: updates.name.trim() });
  2907.  
  2908.           // ✅ Reload to verify
  2909.           await userDetailsData.reload();
  2910.         }
  2911.       }
  2912.  
  2913.       // Check if any updates exist
  2914.       if (Object.keys(filteredUpdates).length === 0 && updates.name === undefined) {
  2915.         return res.status(400).json({
  2916.           success: false,
  2917.           message: 'No valid fields to update'
  2918.         });
  2919.       }
  2920.  
  2921.       // Update student fields
  2922.       if (Object.keys(filteredUpdates).length > 0) {
  2923.         await studentData.update(filteredUpdates);
  2924.       }
  2925.  
  2926.       // ✅ Create admin notification
  2927.       const updatedFields = Object.keys(filteredUpdates);
  2928.       if (updates.name !== undefined) {
  2929.         updatedFields.push('name');
  2930.       }
  2931.  
  2932.       await admin_notification.create({
  2933.         type: 'student_updated',
  2934.         title: 'Student Updated',
  2935.         message: `Student profile was updated by admin`,
  2936.         data: {
  2937.           studentId: studentData.id,
  2938.           updatedFields: updatedFields
  2939.         },
  2940.         read: false
  2941.       });
  2942.  
  2943.       res.status(200).json({
  2944.         success: true,
  2945.         message: 'Student updated successfully',
  2946.         student: studentData
  2947.       });
  2948.  
  2949.     } catch (err) {
  2950.       console.error('❌ [UPDATE STUDENT] Error:', err);
  2951.       next(err);
  2952.     }
  2953.   }
  2954. );
  2955.  
  2956. // ===============================
  2957. // DELETE /api/academy/admin/students/:id
  2958. // Изтрий студент
  2959. // ===============================
  2960. academyController.delete(
  2961.   '/admin/students/:id',
  2962.   isAuth,
  2963.   rbac.checkPermission('student', 'delete'),
  2964.   async (req, res, next) => {
  2965.     try {
  2966.       const studentId = parseInt(req.params.id);
  2967.  
  2968.       const studentData = await student.findByPk(studentId, {
  2969.         include: [
  2970.           {
  2971.             model: user_account,
  2972.             as: 'user',
  2973.             attributes: ['email']
  2974.           }
  2975.         ]
  2976.       });
  2977.  
  2978.       if (!studentData) {
  2979.         return res.status(404).json({
  2980.           success: false,
  2981.           message: 'Student not found'
  2982.         });
  2983.       }
  2984.  
  2985.       const studentEmail = studentData.user.email;
  2986.       const mentorId = studentData.currentMentorId;
  2987.  
  2988.       if (mentorId) {
  2989.         const mentorData = await mentor.findByPk(mentorId);
  2990.         if (mentorData) {
  2991.           await mentorData.update({
  2992.             studentsCount: Math.max(0, mentorData.studentsCount - 1)
  2993.           });
  2994.         }
  2995.       }
  2996.  
  2997.       await studentData.destroy();
  2998.  
  2999.       await admin_notification.create({
  3000.         type: 'student_deleted',
  3001.         title: 'Student Deleted',
  3002.         message: `Student ${studentEmail} was deleted by admin`,
  3003.         data: {
  3004.           studentId: studentId,
  3005.           studentEmail: studentEmail,
  3006.           mentorId: mentorId
  3007.         },
  3008.         read: false
  3009.       });
  3010.  
  3011.       res.status(200).json({
  3012.         success: true,
  3013.         message: 'Student deleted successfully'
  3014.       });
  3015.  
  3016.     } catch (err) {
  3017.       console.error('❌ [DELETE STUDENT] Error:', err);
  3018.       next(err);
  3019.     }
  3020.   }
  3021. );
  3022.  
  3023. // ===============================
  3024. // PATCH /api/academy/admin/students/:id/status
  3025. // Смени статус
  3026. // ===============================
  3027. academyController.patch(
  3028.   '/admin/students/:id/status',
  3029.   isAuth,
  3030.   rbac.checkPermission('student', 'update'),
  3031.   async (req, res, next) => {
  3032.     try {
  3033.       const studentId = parseInt(req.params.id);
  3034.       const { status } = req.body;
  3035.  
  3036.       const validStatuses = ['active', 'inactive', 'graduated', 'suspended'];
  3037.       if (!status || !validStatuses.includes(status)) {
  3038.         return res.status(400).json({
  3039.           success: false,
  3040.           message: `Status must be one of: ${validStatuses.join(', ')}`
  3041.         });
  3042.       }
  3043.  
  3044.       const studentData = await student.findByPk(studentId, {
  3045.         include: [
  3046.           {
  3047.             model: user_account,
  3048.             as: 'user',
  3049.             attributes: ['id', 'email'],
  3050.             include: [
  3051.               {
  3052.                 model: user_details,
  3053.                 as: 'details',
  3054.                 attributes: ['username', 'firstName', 'lastName']
  3055.               }
  3056.             ]
  3057.           }
  3058.         ]
  3059.       });
  3060.  
  3061.       if (!studentData) {
  3062.         return res.status(404).json({
  3063.           success: false,
  3064.           message: 'Student not found'
  3065.         });
  3066.       }
  3067.  
  3068.       const oldStatus = studentData.status;
  3069.       await studentData.update({ status });
  3070.  
  3071.       const studentName = studentData.user?.details?.username ||
  3072.         `${studentData.user?.details?.firstName || ''} ${studentData.user?.details?.lastName || ''}`.trim() ||
  3073.         studentData.user?.email?.split('@')[0] ||
  3074.         'Unknown';
  3075.  
  3076.       await admin_notification.create({
  3077.         type: 'student_status_changed',
  3078.         title: 'Student Status Changed',
  3079.         message: `${studentName}'s status changed from ${oldStatus} to ${status}`,
  3080.        data: {
  3081.          studentId: studentData.id,
  3082.          oldStatus: oldStatus,
  3083.          newStatus: status
  3084.        },
  3085.        read: false
  3086.      });
  3087.  
  3088.      await user_notification.create({
  3089.        userId: studentData.userId,
  3090.        type: 'status_changed',
  3091.        title: 'Вашият статус е променен',
  3092.        message: `Вашият статус е променен на "${status}"`,
  3093.        data: {
  3094.          oldStatus: oldStatus,
  3095.          newStatus: status
  3096.        },
  3097.        read: false
  3098.      });
  3099.  
  3100.      res.status(200).json({
  3101.        success: true,
  3102.        message: 'Student status updated successfully',
  3103.        student: studentData
  3104.      });
  3105.  
  3106.    } catch (err) {
  3107.      console.error('[CHANGE STUDENT STATUS] Error:', err);
  3108.      next(err);
  3109.    }
  3110.  }
  3111. );
  3112. // ===============================
  3113. // POST /api/academy/admin/students/:id/assign-mentor
  3114. // Присвои/смени ментор на студент
  3115. // ===============================
  3116. academyController.post(
  3117.  '/admin/students/:id/assign-mentor',
  3118.  isAuth,
  3119.  rbac.checkPermission('student', 'update'),
  3120.  async (req, res, next) => {
  3121.    try {
  3122.      const studentId = parseInt(req.params.id);
  3123.      const { newMentorId } = req.body;
  3124.  
  3125.      if (!newMentorId) {
  3126.        return res.status(400).json({
  3127.          success: false,
  3128.          message: 'newMentorId is required'
  3129.        });
  3130.      }
  3131.  
  3132.      const { mentor_history, user_details } = require('../sequelize/models/index');
  3133.  
  3134.      const studentData = await student.findByPk(studentId, {
  3135.        include: [
  3136.          {
  3137.            model: user_account,
  3138.            as: 'user',
  3139.            attributes: ['id', 'email'],
  3140.            include: [
  3141.              {
  3142.                model: user_details,
  3143.                as: 'details',
  3144.                attributes: ['username', 'firstName', 'lastName']
  3145.              }
  3146.            ]
  3147.          }
  3148.        ]
  3149.      });
  3150.  
  3151.      if (!studentData) {
  3152.        return res.status(404).json({
  3153.          success: false,
  3154.          message: 'Student not found'
  3155.        });
  3156.      }
  3157.  
  3158.      const newMentor = await mentor.findByPk(newMentorId);
  3159.  
  3160.      if (!newMentor) {
  3161.        return res.status(404).json({
  3162.          success: false,
  3163.          message: 'Mentor not found'
  3164.        });
  3165.      }
  3166.  
  3167.      const oldMentorId = studentData.currentMentorId;
  3168.  
  3169.      // Намали studentsCount на стария ментор
  3170.      if (oldMentorId) {
  3171.        const oldMentor = await mentor.findByPk(oldMentorId);
  3172.        if (oldMentor) {
  3173.          await oldMentor.update({
  3174.            studentsCount: Math.max(0, oldMentor.studentsCount - 1)
  3175.          });
  3176.  
  3177.          // Завърши стария history запис
  3178.          const oldHistory = await mentor_history.findOne({
  3179.            where: {
  3180.              studentId: studentData.id,
  3181.              periodEnd: null
  3182.            },
  3183.            order: [['periodStart', 'DESC']]
  3184.          });
  3185.  
  3186.          if (oldHistory) {
  3187.            await oldHistory.update({
  3188.              periodEnd: new Date(),
  3189.              reason: 'Reassigned to new mentor by admin'
  3190.            });
  3191.          }
  3192.        }
  3193.      }
  3194.  
  3195.      // Увеличи studentsCount на новия ментор
  3196.      await newMentor.update({
  3197.        studentsCount: newMentor.studentsCount + 1
  3198.      });
  3199.  
  3200.      // Обнови студента
  3201.      await studentData.update({
  3202.        currentMentorId: newMentorId,
  3203.        mentorAssignedDate: new Date()
  3204.      });
  3205.  
  3206.      // Създай нов history запис
  3207.      await mentor_history.create({
  3208.        studentId: studentData.id,
  3209.        mentorId: newMentorId,
  3210.        mentorName: newMentor.name,
  3211.        periodStart: new Date(),
  3212.        periodEnd: null,
  3213.        reason: oldMentorId ? 'Reassigned by admin' : 'Initial assignment by admin'
  3214.      });
  3215.  
  3216.      const studentName = studentData.user?.details?.username ||
  3217.        `${studentData.user?.details?.firstName || ''} ${studentData.user?.details?.lastName || ''}`.trim() ||
  3218.        studentData.user?.email?.split('@')[0] ||
  3219.        'Unknown';
  3220.  
  3221.      // Admin notification
  3222.      await admin_notification.create({
  3223.        type: 'student_mentor_assigned',
  3224.        title: 'Mentor Assigned to Student',
  3225.        message: `${studentName} was assigned to mentor ${newMentor.name}`,
  3226.        data: {
  3227.          studentId: studentData.id,
  3228.          oldMentorId: oldMentorId,
  3229.          newMentorId: newMentorId,
  3230.          mentorName: newMentor.name
  3231.        },
  3232.        read: false
  3233.      });
  3234.  
  3235.      // User notification (за студента)
  3236.      await user_notification.create({
  3237.        userId: studentData.userId,
  3238.        type: 'mentor_assigned',
  3239.        title: 'Вашият ментор е назначен',
  3240.        message: `Вашият нов ментор е ${newMentor.name}`,
  3241.        data: {
  3242.          mentorId: newMentorId,
  3243.          mentorName: newMentor.name,
  3244.          mentorEmail: newMentor.email,
  3245.          mentorPhoto: newMentor.photoUrl
  3246.        },
  3247.        read: false
  3248.      });
  3249.  
  3250.      res.status(200).json({
  3251.        success: true,
  3252.        message: 'Mentor assigned successfully',
  3253.        student: studentData
  3254.      });
  3255.  
  3256.    } catch (err) {
  3257.      console.error('[ASSIGN MENTOR] Error:', err);
  3258.      next(err);
  3259.    }
  3260.  }
  3261. );
  3262. // ===============================
  3263. // POST /api/academy/admin/students/:id/assign-mentor
  3264. // Присвои ментор
  3265. // ===============================
  3266. academyController.post(
  3267.  '/admin/students/:id/assign-mentor',
  3268.  isAuth,
  3269.  rbac.checkPermission('student', 'assignMentor'),
  3270.  async (req, res, next) => {
  3271.    try {
  3272.      const studentId = parseInt(req.params.id);
  3273.      const { newMentorId } = req.body;
  3274.  
  3275.      if (!newMentorId) {
  3276.        return res.status(400).json({
  3277.          success: false,
  3278.          message: 'newMentorId is required'
  3279.        });
  3280.      }
  3281.  
  3282.      const { mentor_history } = require('../sequelize/models/index');
  3283.  
  3284.      const studentData = await student.findByPk(studentId, {
  3285.        include: [
  3286.          {
  3287.            model: user_account,
  3288.            as: 'user',
  3289.            attributes: ['id', 'email'],
  3290.            include: [
  3291.              {
  3292.                model: user_details,
  3293.                as: 'details',
  3294.                attributes: ['username', 'firstName', 'lastName']
  3295.              }
  3296.            ]
  3297.          }
  3298.        ]
  3299.      });
  3300.  
  3301.      if (!studentData) {
  3302.        return res.status(404).json({
  3303.          success: false,
  3304.          message: 'Student not found'
  3305.        });
  3306.      }
  3307.  
  3308.      const newMentor = await mentor.findByPk(newMentorId);
  3309.  
  3310.      if (!newMentor) {
  3311.        return res.status(404).json({
  3312.          success: false,
  3313.          message: 'Mentor not found'
  3314.        });
  3315.      }
  3316.  
  3317.      const oldMentorId = studentData.currentMentorId;
  3318.  
  3319.      if (oldMentorId) {
  3320.        const oldMentor = await mentor.findByPk(oldMentorId);
  3321.        if (oldMentor) {
  3322.          await oldMentor.update({
  3323.            studentsCount: Math.max(0, oldMentor.studentsCount - 1)
  3324.          });
  3325.  
  3326.          const oldHistory = await mentor_history.findOne({
  3327.            where: {
  3328.              studentId: studentData.id,
  3329.              periodEnd: null
  3330.            },
  3331.            order: [['periodStart', 'DESC']]
  3332.          });
  3333.  
  3334.          if (oldHistory) {
  3335.            await oldHistory.update({
  3336.              periodEnd: new Date(),
  3337.              reason: 'Reassigned to new mentor by admin'
  3338.            });
  3339.          }
  3340.        }
  3341.      }
  3342.  
  3343.      await newMentor.update({
  3344.        studentsCount: newMentor.studentsCount + 1
  3345.      });
  3346.  
  3347.      await studentData.update({
  3348.        currentMentorId: newMentorId,
  3349.        mentorAssignedDate: new Date()
  3350.      });
  3351.  
  3352.      await mentor_history.create({
  3353.        studentId: studentData.id,
  3354.        mentorId: newMentorId,
  3355.        mentorName: newMentor.name,
  3356.        periodStart: new Date(),
  3357.        periodEnd: null,
  3358.        reason: oldMentorId ? 'Reassigned by admin' : 'Initial assignment by admin'
  3359.      });
  3360.  
  3361.      const studentName = studentData.user?.details?.username ||
  3362.        `${studentData.user?.details?.firstName || ''} ${studentData.user?.details?.lastName || ''}`.trim() ||
  3363.        studentData.user?.email?.split('@')[0] ||
  3364.        'Unknown';
  3365.  
  3366.      await admin_notification.create({
  3367.        type: 'student_mentor_assigned',
  3368.        title: 'Mentor Assigned to Student',
  3369.        message: `${studentName} was assigned to mentor ${newMentor.name}`,
  3370.        data: {
  3371.          studentId: studentData.id,
  3372.          oldMentorId: oldMentorId,
  3373.          newMentorId: newMentorId,
  3374.          mentorName: newMentor.name
  3375.        },
  3376.        read: false
  3377.      });
  3378.  
  3379.      await user_notification.create({
  3380.        userId: studentData.userId,
  3381.        type: 'mentor_assigned',
  3382.        title: 'Вашият ментор е назначен',
  3383.        message: `Вашият нов ментор е ${newMentor.name}`,
  3384.        data: {
  3385.          mentorId: newMentorId,
  3386.          mentorName: newMentor.name,
  3387.          mentorEmail: newMentor.email,
  3388.          mentorPhoto: newMentor.photoUrl
  3389.        },
  3390.        read: false
  3391.      });
  3392.  
  3393.      res.status(200).json({
  3394.        success: true,
  3395.        message: 'Mentor assigned successfully',
  3396.        student: studentData
  3397.      });
  3398.  
  3399.    } catch (err) {
  3400.      console.error('[ASSIGN MENTOR] Error:', err);
  3401.      next(err);
  3402.    }
  3403.  }
  3404. );
  3405.  
  3406. // ===============================
  3407. // POST /api/academy/admin/students/:id/send-email
  3408. // Изпрати имейл
  3409. // ===============================
  3410. academyController.post(
  3411.  '/admin/students/:id/send-email',
  3412.  isAuth,
  3413.  rbac.checkPermission('student', 'sendEmail'),
  3414.  async (req, res, next) => {
  3415.    try {
  3416.      const studentId = parseInt(req.params.id);
  3417.      const { subject, message } = req.body;
  3418.  
  3419.      if (!subject || !message) {
  3420.        return res.status(400).json({
  3421.          success: false,
  3422.          message: 'Subject and message are required'
  3423.        });
  3424.      }
  3425.  
  3426.      if (subject.length > 200) {
  3427.        return res.status(400).json({
  3428.          success: false,
  3429.          message: 'Subject must not exceed 200 characters'
  3430.        });
  3431.      }
  3432.  
  3433.      if (message.length > 10000) {
  3434.        return res.status(400).json({
  3435.          success: false,
  3436.          message: 'Message must not exceed 10000 characters'
  3437.        });
  3438.      }
  3439.  
  3440.      const studentData = await student.findByPk(studentId, {
  3441.        include: [
  3442.          {
  3443.            model: user_account,
  3444.            as: 'user',
  3445.            attributes: ['id', 'email'],
  3446.            include: [
  3447.              {
  3448.                model: user_details,
  3449.                as: 'details',
  3450.                attributes: ['username', 'firstName', 'lastName']
  3451.              }
  3452.            ]
  3453.          }
  3454.        ]
  3455.      });
  3456.  
  3457.      if (!studentData) {
  3458.        return res.status(404).json({
  3459.          success: false,
  3460.          message: 'Student not found'
  3461.        });
  3462.      }
  3463.  
  3464.      const studentEmail = studentData.user.email;
  3465.      const studentName = studentData.user?.details?.username ||
  3466.        `${studentData.user?.details?.firstName || ''} ${studentData.user?.details?.lastName || ''}`.trim() ||
  3467.        'Student';
  3468.  
  3469.      const emailData = {
  3470.        from: 'info@pensa.club',
  3471.        to: studentEmail,
  3472.        subject: subject,
  3473.        message: `Здравейте ${studentName},
  3474.  
  3475. ${message}
  3476.  
  3477. ---
  3478. С уважение,
  3479. DigiBridge Academy Team`
  3480.      };
  3481.  
  3482.      await admin_notification.create({
  3483.        type: 'student_email_sent',
  3484.        title: 'Email Sent to Student',
  3485.        message: `Email sent to ${studentName}`,
  3486.        data: {
  3487.          studentId: studentData.id,
  3488.          studentEmail: studentEmail,
  3489.          subject: subject,
  3490.          emailData: emailData
  3491.        },
  3492.        read: false
  3493.      });
  3494.  
  3495.      res.status(200).json({
  3496.        success: true,
  3497.        message: 'Email prepared successfully',
  3498.        emailData: emailData
  3499.      });
  3500.  
  3501.    } catch (err) {
  3502.      console.error('[SEND EMAIL TO STUDENT] Error:', err);
  3503.      next(err);
  3504.    }
  3505.  }
  3506. );
  3507. // ===============================
  3508. // ============ ADMIN STUDENT NOTES ============
  3509. // ===============================
  3510.  
  3511. // ===============================
  3512. // GET /api/academy/admin/students/:id/notes
  3513. // Вземи админ бележки за студент
  3514. // ===============================
  3515. academyController.get(
  3516.  '/admin/students/:id/notes',
  3517.  isAuth,
  3518.  rbac.checkPermission('student', 'read'),
  3519.  async (req, res, next) => {
  3520.    try {
  3521.      const studentId = parseInt(req.params.id);
  3522.  
  3523.      const { admin_student_note } = require('../sequelize/models/index');
  3524.  
  3525.      const studentData = await student.findByPk(studentId);
  3526.  
  3527.      if (!studentData) {
  3528.        return res.status(404).json({
  3529.          success: false,
  3530.          message: 'Student not found'
  3531.        });
  3532.      }
  3533.  
  3534.      const notes = await admin_student_note.findAll({
  3535.        where: {
  3536.          studentId: studentId
  3537.        },
  3538.        order: [['createdAt', 'DESC']]
  3539.      });
  3540.  
  3541.      res.status(200).json({
  3542.        success: true,
  3543.        notes: notes,
  3544.        total: notes.length
  3545.      });
  3546.  
  3547.    } catch (err) {
  3548.      console.error('[GET ADMIN STUDENT NOTES] Error:', err);
  3549.      next(err);
  3550.    }
  3551.  }
  3552. );
  3553.  
  3554. // ===============================
  3555. // POST /api/academy/admin/students/:id/notes
  3556. // Създай админ бележка
  3557. // ===============================
  3558. academyController.post(
  3559.  '/admin/students/:id/notes',
  3560.  isAuth,
  3561.  rbac.checkPermission('student', 'update'),
  3562.  async (req, res, next) => {
  3563.    try {
  3564.      const studentId = parseInt(req.params.id);
  3565.      const { text, category } = req.body;  // ← ДОБАВЕНО category
  3566.  
  3567.      const { createAdminNoteSchema } = require('../schemas/adminStudentNotes.schema');
  3568.      const { admin_student_note } = require('../sequelize/models/index');
  3569.  
  3570.      const validationResult = createAdminNoteSchema.safeParse({ text, category });
  3571.  
  3572.      if (!validationResult.success) {
  3573.        return res.status(400).json({
  3574.          success: false,
  3575.          message: 'Validation failed',
  3576.          errors: validationResult.error.errors
  3577.        });
  3578.      }
  3579.  
  3580.      const studentData = await student.findByPk(studentId);
  3581.  
  3582.      if (!studentData) {
  3583.        return res.status(404).json({
  3584.          success: false,
  3585.          message: 'Student not found'
  3586.        });
  3587.      }
  3588.  
  3589.      const newNote = await admin_student_note.create({
  3590.        studentId: studentId,
  3591.        text: validationResult.data.text,
  3592.        category: validationResult.data.category  // ← ДОБАВЕНО
  3593.      });
  3594.  
  3595.      await admin_notification.create({
  3596.        type: 'admin_note_created',
  3597.        title: 'Admin Note Created',
  3598.        message: `Admin note added for student`,
  3599.        data: {
  3600.          studentId: studentId,
  3601.          noteId: newNote.id,
  3602.          category: newNote.category
  3603.        },
  3604.        read: false
  3605.      });
  3606.  
  3607.      res.status(201).json({
  3608.        success: true,
  3609.        message: 'Admin note created successfully',
  3610.        note: newNote
  3611.      });
  3612.  
  3613.    } catch (err) {
  3614.      console.error('[CREATE ADMIN NOTE] Error:', err);
  3615.      next(err);
  3616.    }
  3617.  }
  3618. );
  3619.  
  3620.  
  3621. // ===============================
  3622. // PATCH /api/academy/admin/students/:id/notes/:noteId
  3623. // Редактирай админ бележка
  3624. // ===============================
  3625. academyController.patch(
  3626.  '/admin/students/:id/notes/:noteId',
  3627.  isAuth,
  3628.  rbac.checkPermission('student', 'update'),
  3629.  async (req, res, next) => {
  3630.    try {
  3631.      const studentId = parseInt(req.params.id);
  3632.      const noteId = parseInt(req.params.noteId);
  3633.      const { text, category } = req.body;  // ← ДОБАВЕНО category
  3634.  
  3635.      const { updateAdminNoteSchema } = require('../schemas/adminStudentNotes.schema');
  3636.      const { admin_student_note } = require('../sequelize/models/index');
  3637.  
  3638.      const validationResult = updateAdminNoteSchema.safeParse({ text, category });
  3639.  
  3640.      if (!validationResult.success) {
  3641.        return res.status(400).json({
  3642.          success: false,
  3643.          message: 'Validation failed',
  3644.          errors: validationResult.error.errors
  3645.        });
  3646.      }
  3647.  
  3648.      const note = await admin_student_note.findOne({
  3649.        where: {
  3650.          id: noteId,
  3651.          studentId: studentId
  3652.        }
  3653.      });
  3654.  
  3655.      if (!note) {
  3656.        return res.status(404).json({
  3657.          success: false,
  3658.          message: 'Admin note not found'
  3659.        });
  3660.      }
  3661.  
  3662.      // Build update object
  3663.      const updateData = {};
  3664.      if (validationResult.data.text) updateData.text = validationResult.data.text;
  3665.      if (validationResult.data.category) updateData.category = validationResult.data.category;
  3666.  
  3667.      await note.update(updateData);
  3668.  
  3669.      res.status(200).json({
  3670.        success: true,
  3671.        message: 'Admin note updated successfully',
  3672.        note: note
  3673.      });
  3674.  
  3675.    } catch (err) {
  3676.      console.error('[UPDATE ADMIN NOTE] Error:', err);
  3677.      next(err);
  3678.    }
  3679.  }
  3680. );
  3681.  
  3682. // ===============================
  3683. // DELETE /api/academy/admin/students/:id/notes/:noteId
  3684. // Изтрий админ бележка
  3685. // ===============================
  3686. academyController.delete(
  3687.  '/admin/students/:id/notes/:noteId',
  3688.  isAuth,
  3689.  rbac.checkPermission('student', 'update'),
  3690.  async (req, res, next) => {
  3691.    try {
  3692.      const studentId = parseInt(req.params.id);
  3693.      const noteId = parseInt(req.params.noteId);
  3694.  
  3695.      const { admin_student_note } = require('../sequelize/models/index');
  3696.  
  3697.      const note = await admin_student_note.findOne({
  3698.        where: {
  3699.          id: noteId,
  3700.          studentId: studentId
  3701.        }
  3702.      });
  3703.  
  3704.      if (!note) {
  3705.        return res.status(404).json({
  3706.          success: false,
  3707.          message: 'Admin note not found'
  3708.        });
  3709.      }
  3710.  
  3711.      await note.destroy();
  3712.  
  3713.      res.status(200).json({
  3714.        success: true,
  3715.        message: 'Admin note deleted successfully'
  3716.      });
  3717.  
  3718.    } catch (err) {
  3719.      console.error('[DELETE ADMIN NOTE] Error:', err);
  3720.      next(err);
  3721.    }
  3722.  }
  3723. );
  3724. // ===============================
  3725. // ============ STUDENT STATISTICS ============
  3726. // ===============================
  3727.  
  3728. // ===============================
  3729. // GET /api/academy/admin/students/statistics/overview
  3730. // Overview статистики за студенти
  3731. // ===============================
  3732. academyController.get(
  3733.  '/admin/students/statistics/overview',
  3734.  isAuth,
  3735.  rbac.checkPermission('student', 'read'),
  3736.  async (req, res, next) => {
  3737.    try {
  3738.      const { student_course, student_lecture, student_seminar, student_presentation } = require('../sequelize/models/index');
  3739.  
  3740.      // 1. Общ брой студенти
  3741.      const totalStudents = await student.count();
  3742.  
  3743.      // 2. Активни студенти
  3744.      const activeStudents = await student.count({
  3745.        where: { status: 'active' }
  3746.      });
  3747.  
  3748.      // 3. Студенти с ментор
  3749.      const studentsWithMentor = await student.count({
  3750.        where: {
  3751.          currentMentorId: {
  3752.            [Op.ne]: null
  3753.          }
  3754.        }
  3755.      });
  3756.  
  3757.      // 4. Общи кредити
  3758.      const creditsData = await student.findAll({
  3759.        attributes: [
  3760.          [sequelize.fn('SUM', sequelize.col('total_credits_earned')), 'totalCredits'],
  3761.          [sequelize.fn('AVG', sequelize.col('total_credits_earned')), 'avgCredits']
  3762.        ],
  3763.        raw: true
  3764.      });
  3765.  
  3766.      const totalCreditsEarned = parseInt(creditsData[0].totalCredits) || 0;
  3767.      const averageCreditsPerStudent = parseFloat(creditsData[0].avgCredits) || 0;
  3768.  
  3769.      // 5. Общо посещения
  3770.      const attendanceData = await student.findAll({
  3771.        attributes: [
  3772.          [sequelize.fn('SUM', sequelize.col('attended_sessions')), 'totalAttended'],
  3773.          [sequelize.fn('SUM', sequelize.col('total_scheduled_sessions')), 'totalScheduled']
  3774.        ],
  3775.        raw: true
  3776.      });
  3777.  
  3778.      const totalAttendedSessions = parseInt(attendanceData[0].totalAttended) || 0;
  3779.      const totalScheduledSessions = parseInt(attendanceData[0].totalScheduled) || 0;
  3780.  
  3781.      const averageAttendanceRate = totalScheduledSessions > 0
  3782.        ? Math.round((totalAttendedSessions / totalScheduledSessions) * 100)
  3783.        : 0;
  3784.  
  3785.      // 6. Активни курсове
  3786.      const activeCourses = await student_course.count({
  3787.        where: {
  3788.          status: 'in_progress'
  3789.        }
  3790.      });
  3791.  
  3792.      // 7. Завършени курсове
  3793.      const completedCourses = await student_course.count({
  3794.        where: {
  3795.          status: 'completed'
  3796.        }
  3797.      });
  3798.  
  3799.      // 8. Среден прогрес
  3800.      const progressData = await student_course.findAll({
  3801.        attributes: [
  3802.          [sequelize.fn('AVG', sequelize.col('progress')), 'avgProgress']
  3803.        ],
  3804.        where: {
  3805.          status: 'in_progress'
  3806.        },
  3807.        raw: true
  3808.      });
  3809.  
  3810.      const averageProgress = Math.round(parseFloat(progressData[0].avgProgress) || 0);
  3811.  
  3812.      res.status(200).json({
  3813.        success: true,
  3814.        stats: {
  3815.          totalStudents,
  3816.          activeStudents,
  3817.          studentsWithMentor,
  3818.          totalCreditsEarned,
  3819.          averageCreditsPerStudent: Math.round(averageCreditsPerStudent),
  3820.          totalAttendedSessions,
  3821.          totalScheduledSessions,
  3822.          averageAttendanceRate,
  3823.          activeCourses,
  3824.          completedCourses,
  3825.          averageProgress
  3826.        }
  3827.      });
  3828.  
  3829.    } catch (err) {
  3830.      console.error('[STUDENT STATISTICS OVERVIEW] Error:', err);
  3831.      next(err);
  3832.    }
  3833.  }
  3834. );
  3835.  
  3836. // ===============================
  3837. // GET /api/academy/admin/students/statistics/by-status
  3838. // Студенти по статус
  3839. // ===============================
  3840. academyController.get(
  3841.  '/admin/students/statistics/by-status',
  3842.  isAuth,
  3843.  rbac.checkPermission('student', 'read'),
  3844.  async (req, res, next) => {
  3845.    try {
  3846.      const statistics = await student.findAll({
  3847.        attributes: [
  3848.          'status',
  3849.          [sequelize.fn('COUNT', sequelize.col('id')), 'count']
  3850.        ],
  3851.        group: ['status'],
  3852.        raw: true
  3853.      });
  3854.  
  3855.      const formattedStats = statistics.map(stat => ({
  3856.        status: stat.status,
  3857.        count: parseInt(stat.count)
  3858.      }));
  3859.  
  3860.      res.status(200).json({
  3861.        success: true,
  3862.        statistics: formattedStats
  3863.      });
  3864.  
  3865.    } catch (err) {
  3866.      console.error('[STUDENTS BY STATUS] Error:', err);
  3867.      next(err);
  3868.    }
  3869.  }
  3870. );
  3871.  
  3872. // ===============================
  3873. // GET /api/academy/admin/students/statistics/by-mentor
  3874. // Студенти по ментор
  3875. // ===============================
  3876. academyController.get(
  3877.  '/admin/students/statistics/by-mentor',
  3878.  isAuth,
  3879.  rbac.checkPermission('student', 'read'),
  3880.  async (req, res, next) => {
  3881.    try {
  3882.      const statistics = await student.findAll({
  3883.        attributes: [
  3884.          'currentMentorId',
  3885.          [sequelize.fn('COUNT', sequelize.col('student.id')), 'count']
  3886.        ],
  3887.        include: [
  3888.          {
  3889.            model: mentor,
  3890.            as: 'currentMentor',
  3891.            attributes: ['id', 'name', 'photoUrl'],
  3892.            required: false
  3893.          }
  3894.        ],
  3895.        where: {
  3896.          currentMentorId: {
  3897.            [Op.ne]: null
  3898.          }
  3899.        },
  3900.        group: ['currentMentorId', 'currentMentor.id', 'currentMentor.name', 'currentMentor.photoUrl'],
  3901.        raw: false
  3902.      });
  3903.  
  3904.      const formattedStats = statistics.map(stat => {
  3905.        const statData = stat.get({ plain: true });
  3906.        return {
  3907.          mentorId: statData.currentMentorId,
  3908.          mentorName: statData.currentMentor?.name || 'Unknown',
  3909.          mentorPhoto: statData.currentMentor?.photoUrl || null,
  3910.          studentCount: parseInt(statData.count)
  3911.        };
  3912.      });
  3913.  
  3914.      res.status(200).json({
  3915.        success: true,
  3916.        statistics: formattedStats
  3917.      });
  3918.  
  3919.    } catch (err) {
  3920.      console.error('[STUDENTS BY MENTOR] Error:', err);
  3921.      next(err);
  3922.    }
  3923.  }
  3924. );
  3925.  
  3926. // ===============================
  3927. // GET /api/academy/admin/students/statistics/credits-distribution
  3928. // Разпределение на кредити
  3929. // ===============================
  3930. academyController.get(
  3931.  '/admin/students/statistics/credits-distribution',
  3932.  isAuth,
  3933.  rbac.checkPermission('student', 'read'),
  3934.  async (req, res, next) => {
  3935.    try {
  3936.      const students = await student.findAll({
  3937.        attributes: ['totalCreditsEarned'],
  3938.        raw: true
  3939.      });
  3940.  
  3941.      const ranges = {
  3942.        '0-50': 0,
  3943.        '51-100': 0,
  3944.        '101-200': 0,
  3945.        '201-300': 0,
  3946.        '300+': 0
  3947.      };
  3948.  
  3949.      students.forEach(s => {
  3950.        const credits = s.totalCreditsEarned || 0;
  3951.        if (credits <= 50) ranges['0-50']++;
  3952.        else if (credits <= 100) ranges['51-100']++;
  3953.        else if (credits <= 200) ranges['101-200']++;
  3954.        else if (credits <= 300) ranges['201-300']++;
  3955.        else ranges['300+']++;
  3956.      });
  3957.  
  3958.      res.status(200).json({
  3959.        success: true,
  3960.        distribution: ranges
  3961.      });
  3962.  
  3963.    } catch (err) {
  3964.      console.error('[CREDITS DISTRIBUTION] Error:', err);
  3965.      next(err);
  3966.    }
  3967.  }
  3968. );
  3969.  
  3970. // ===============================
  3971. // GET /api/academy/admin/students/statistics/attendance-trends
  3972. // Attendance trends (последните 6 месеца)
  3973. // ===============================
  3974. academyController.get(
  3975.  '/admin/students/statistics/attendance-trends',
  3976.  isAuth,
  3977.  rbac.checkPermission('student', 'read'),
  3978.  async (req, res, next) => {
  3979.    try {
  3980.      // TODO: Implement historical tracking
  3981.      // За сега връщаме текущите данни
  3982.      const currentData = await student.findAll({
  3983.        attributes: [
  3984.          [sequelize.fn('SUM', sequelize.col('attended_sessions')), 'totalAttended'],
  3985.          [sequelize.fn('SUM', sequelize.col('total_scheduled_sessions')), 'totalScheduled']
  3986.        ],
  3987.        raw: true
  3988.      });
  3989.  
  3990.      const totalAttended = parseInt(currentData[0].totalAttended) || 0;
  3991.      const totalScheduled = parseInt(currentData[0].totalScheduled) || 0;
  3992.      const rate = totalScheduled > 0 ? Math.round((totalAttended / totalScheduled) * 100) : 0;
  3993.  
  3994.      const currentMonth = new Date().toISOString().slice(0, 7);
  3995.  
  3996.      res.status(200).json({
  3997.        success: true,
  3998.        trends: [
  3999.          {
  4000.            month: currentMonth,
  4001.            attendanceRate: rate,
  4002.            totalAttended: totalAttended,
  4003.            totalScheduled: totalScheduled
  4004.          }
  4005.        ]
  4006.      });
  4007.  
  4008.    } catch (err) {
  4009.      console.error('[ATTENDANCE TRENDS] Error:', err);
  4010.      next(err);
  4011.    }
  4012.  }
  4013. );
  4014.  
  4015. // ===============================
  4016. // GET /api/academy/admin/students/statistics/top-performers
  4017. // Топ студенти
  4018. // ===============================
  4019. academyController.get(
  4020.  '/admin/students/statistics/top-performers',
  4021.  isAuth,
  4022.  rbac.checkPermission('student', 'read'),
  4023.  async (req, res, next) => {
  4024.    try {
  4025.      const { limit = 10 } = req.query;
  4026.  
  4027.      const topStudents = await student.findAll({
  4028.        attributes: [
  4029.          'id',
  4030.          'userId',
  4031.          'avatar',
  4032.          'totalCreditsEarned',
  4033.          'attendedSessions',
  4034.          'totalScheduledSessions'
  4035.        ],
  4036.        include: [
  4037.          {
  4038.            model: user_account,
  4039.            as: 'user',
  4040.            attributes: ['email'],
  4041.            include: [
  4042.              {
  4043.                model: user_details,
  4044.                as: 'details',
  4045.                attributes: ['username', 'firstName', 'lastName', 'imageURL']
  4046.              }
  4047.            ]
  4048.          },
  4049.          {
  4050.            model: mentor,
  4051.            as: 'currentMentor',
  4052.            attributes: ['id', 'name']
  4053.          }
  4054.        ],
  4055.        order: [['totalCreditsEarned', 'DESC']],
  4056.        limit: parseInt(limit),
  4057.        raw: false
  4058.      });
  4059.  
  4060.      const formattedStudents = topStudents.map(s => {
  4061.        const studentData = s.get({ plain: true });
  4062.        const name = studentData.user?.details?.username ||
  4063.          `${studentData.user?.details?.firstName || ''} ${studentData.user?.details?.lastName || ''}`.trim() ||
  4064.          studentData.user?.email?.split('@')[0] ||
  4065.          'Unknown';
  4066.  
  4067.        const attendanceRate = studentData.totalScheduledSessions > 0
  4068.          ? Math.round((studentData.attendedSessions / studentData.totalScheduledSessions) * 100)
  4069.          : 0;
  4070.  
  4071.        return {
  4072.          id: studentData.id,
  4073.          name: name,
  4074.          avatar: studentData.avatar || studentData.user?.details?.imageURL || null,
  4075.          totalCredits: studentData.totalCreditsEarned || 0,
  4076.          attendanceRate: attendanceRate,
  4077.          mentorName: studentData.currentMentor?.name || 'No mentor'
  4078.        };
  4079.      });
  4080.  
  4081.      res.status(200).json({
  4082.        success: true,
  4083.        topPerformers: formattedStudents
  4084.      });
  4085.  
  4086.    } catch (err) {
  4087.      console.error('[TOP PERFORMERS] Error:', err);
  4088.      next(err);
  4089.    }
  4090.  }
  4091. );
  4092.  
  4093. // ===============================
  4094. // GET /api/academy/admin/students/statistics/engagement
  4095. // Student engagement метрики
  4096. // ===============================
  4097. academyController.get(
  4098.  '/admin/students/statistics/engagement',
  4099.  isAuth,
  4100.  rbac.checkPermission('student', 'read'),
  4101.  async (req, res, next) => {
  4102.    try {
  4103.      const totalStudents = await student.count();
  4104.  
  4105.      const activeLastWeek = await student.count({
  4106.        where: {
  4107.          lastActiveAt: {
  4108.            [Op.gte]: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
  4109.          }
  4110.        }
  4111.      });
  4112.  
  4113.      const activeLastMonth = await student.count({
  4114.        where: {
  4115.          lastActiveAt: {
  4116.            [Op.gte]: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
  4117.          }
  4118.        }
  4119.      });
  4120.  
  4121.      const chatSessionsData = await student.findAll({
  4122.        attributes: [
  4123.          [sequelize.fn('SUM', sequelize.col('total_chat_sessions')), 'totalChats'],
  4124.          [sequelize.fn('AVG', sequelize.col('total_chat_sessions')), 'avgChats']
  4125.        ],
  4126.        raw: true
  4127.      });
  4128.  
  4129.      const totalChatSessions = parseInt(chatSessionsData[0].totalChats) || 0;
  4130.      const averageChatsPerStudent = parseFloat(chatSessionsData[0].avgChats) || 0;
  4131.  
  4132.      res.status(200).json({
  4133.        success: true,
  4134.        engagement: {
  4135.          totalStudents,
  4136.          activeLastWeek,
  4137.          activeLastMonth,
  4138.          weeklyEngagementRate: totalStudents > 0 ? Math.round((activeLastWeek / totalStudents) * 100) : 0,
  4139.          monthlyEngagementRate: totalStudents > 0 ? Math.round((activeLastMonth / totalStudents) * 100) : 0,
  4140.          totalChatSessions,
  4141.          averageChatsPerStudent: Math.round(averageChatsPerStudent)
  4142.        }
  4143.      });
  4144.  
  4145.    } catch (err) {
  4146.      console.error('[ENGAGEMENT] Error:', err);
  4147.      next(err);
  4148.    }
  4149.  }
  4150. );
  4151. module.exports = academyController;
Advertisement
Comments
  • User was banned
  • User was banned
  • User was banned
Add Comment
Please, Sign In to add comment