feat(users,professors): add promote student to professor and full student profile aggregation

This commit is contained in:
2026-08-24 22:52:38 +03:30
parent 282dd42422
commit dca7250be0
8 changed files with 401 additions and 2 deletions
+248 -1
View File
@@ -4,6 +4,15 @@
const User = require('./userModel');
const Role = require('../roles/roleModel');
const Class = require('../classes/classModel');
const Session = require('../sessions/sessionModel');
const Payment = require('../payments/paymentModel');
const Transaction = require('../payments/transactionModel');
const Certificate = require('../certificates/certificateModel');
const Document = require('../documents/documentModel');
const Waitlist = require('../waitlist/waitlistModel');
const Notification = require('../notifications/notificationModel');
const ActivityLog = require('../activityLogs/activityLogModel');
const professorService = require('../professors/professorService');
const bcrypt = require('bcryptjs');
const AppError = require('../../utils/AppError');
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
@@ -363,14 +372,252 @@ const enrollUserInCourse = async (userId, courseId) => {
return User.findById(userId).select(SAFE_FIELDS).populate('courses').lean();
};
const getUserFullProfile = async (id) => {
const user = await User.findById(id).select(SAFE_FIELDS).populate(POPULATE_ROLE).lean();
if (!user) throw new AppError('USER_NOT_FOUND');
// 1. Enrolled Classes
const classes = await Class.find({ students: id, isDeleted: { $ne: true } })
.populate('course', 'title type price hoursPerSection')
.populate('professor', 'name surname phoneNumber email')
.sort({ createdAt: -1 })
.lean();
const classIds = classes.map((c) => c._id);
// 2. All Sessions for user's classes or where user is in attendanceList
const sessions = await Session.find({
$or: [
{ class: { $in: classIds } },
{ 'attendanceList.user': id }
],
isDeleted: { $ne: true }
})
.populate('course', 'title')
.populate('class', 'name')
.populate('professor', 'name surname')
.populate('attendanceList.recordedBy', 'name username')
.sort({ day: -1, startTime: -1 })
.lean();
// Map attendance records specifically for this student
let presentCount = 0;
let absentCount = 0;
let lateCount = 0;
let excusedCount = 0;
const userAttendances = sessions.map((sess) => {
const record = (sess.attendanceList || []).find((r) => String(r.user) === String(id));
const status = record ? record.status : 'scheduled';
if (status === 'present') presentCount++;
else if (status === 'absent') absentCount++;
else if (status === 'late') lateCount++;
else if (status === 'excused') excusedCount++;
return {
_id: sess._id,
sessionId: sess._id,
day: sess.day,
startTime: sess.startTime,
endTime: sess.endTime,
place: sess.place,
topic: sess.topic,
sessionStatus: sess.status,
course: sess.course,
class: sess.class,
professor: sess.professor,
attendanceStatus: status,
attendanceNote: record?.note || '',
recordedBy: record?.recordedBy || null
};
});
const totalRecordedAttendance = presentCount + absentCount + lateCount + excusedCount;
const attendanceRate = totalRecordedAttendance > 0
? Math.round(((presentCount + lateCount) / totalRecordedAttendance) * 100)
: 0;
// 3. Payments & Invoices
const payments = await Payment.find({ user: id, isDeleted: { $ne: true } })
.populate('classes', 'name tuitionFee')
.populate('course', 'title')
.sort({ createdAt: -1 })
.lean();
const paymentIds = payments.map((p) => p._id);
// 4. Bank Transactions
const transactions = await Transaction.find({
$or: [
{ user: id },
{ payment: { $in: paymentIds } }
]
})
.populate('payment', 'uniqueCode amount status')
.populate('recordedBy', 'name username')
.sort({ date: -1, createdAt: -1 })
.lean();
// 5. Financial Summary Calculations
let totalTuition = 0;
let totalDiscount = 0;
let totalPayable = 0;
let totalPaid = 0;
let overdueAmount = 0;
let pendingAmount = 0;
const paymentStatusMap = {
paid: { count: 0, amount: 0 },
partial: { count: 0, amount: 0 },
pending: { count: 0, amount: 0 },
overdue: { count: 0, amount: 0 },
cancelled: { count: 0, amount: 0 },
reverted: { count: 0, amount: 0 }
};
payments.forEach((p) => {
const payable = Math.max(0, (p.amount || 0) - (p.discount || 0));
totalTuition += (p.amount || 0);
totalDiscount += (p.discount || 0);
totalPayable += payable;
totalPaid += (p.paidAmount || 0);
const st = p.status || 'pending';
if (paymentStatusMap[st]) {
paymentStatusMap[st].count++;
paymentStatusMap[st].amount += payable;
}
if (st === 'overdue') {
overdueAmount += Math.max(0, payable - (p.paidAmount || 0));
} else if (st === 'pending' || st === 'partial') {
pendingAmount += Math.max(0, payable - (p.paidAmount || 0));
}
});
const remainingDebt = Math.max(0, totalPayable - totalPaid);
const financialSummary = {
totalTuition,
totalDiscount,
totalPayable,
totalPaid,
remainingDebt,
overdueAmount,
pendingAmount,
totalInvoicesCount: payments.length,
totalTransactionsCount: transactions.length
};
// Monthly transaction timeline for charts
const monthlyTimelineMap = {};
transactions.forEach((t) => {
if (t.status !== 'cancelled' && t.status !== 'reverted') {
const d = t.date || t.createdAt;
if (d) {
const monthKey = new Date(d).toISOString().slice(0, 7); // YYYY-MM
if (!monthlyTimelineMap[monthKey]) {
monthlyTimelineMap[monthKey] = { month: monthKey, amount: 0, count: 0 };
}
monthlyTimelineMap[monthKey].amount += (t.amount || 0);
monthlyTimelineMap[monthKey].count++;
}
}
});
const monthlyTransactions = Object.values(monthlyTimelineMap).sort((a, b) => a.month.localeCompare(b.month));
// 6. Certificates & Documents
const certificates = await Certificate.find({ user: id })
.populate('course', 'title')
.sort({ issuedAt: -1 })
.lean();
const documents = await Document.find({ user: id })
.populate('uploadedBy', 'name username')
.sort({ createdAt: -1 })
.lean();
// 7. Waitlist Entries
const waitlist = await Waitlist.find({ user: id, isDeleted: { $ne: true } })
.populate('course', 'title type price')
.populate('class', 'name startDate')
.sort({ createdAt: -1 })
.lean();
// 8. Notifications
const notifications = await Notification.find({ user: id })
.sort({ createdAt: -1 })
.limit(50)
.lean();
// 9. Activity Logs
const activityLogs = await ActivityLog.find({
$or: [
{ actor: id },
{ resourceId: String(id) },
{ 'metadata.userId': String(id) }
]
})
.sort({ createdAt: -1 })
.limit(50)
.lean();
const chartsData = {
attendance: {
present: presentCount,
absent: absentCount,
late: lateCount,
excused: excusedCount,
total: totalRecordedAttendance,
attendanceRate
},
paymentsBreakdown: [
{ status: 'paid', count: paymentStatusMap.paid.count, amount: paymentStatusMap.paid.amount },
{ status: 'partial', count: paymentStatusMap.partial.count, amount: paymentStatusMap.partial.amount },
{ status: 'pending', count: paymentStatusMap.pending.count, amount: paymentStatusMap.pending.amount },
{ status: 'overdue', count: paymentStatusMap.overdue.count, amount: paymentStatusMap.overdue.amount }
],
financialSummary,
monthlyTransactions
};
return {
user: {
...user,
nationalId: user.nationalIdCode,
registeredClassesCount: classes.length
},
classes,
sessions,
attendances: userAttendances,
payments,
transactions,
financialSummary,
attendanceSummary: chartsData.attendance,
chartsData,
certificates,
documents,
waitlist,
notifications,
activityLogs
};
};
const promoteToProfessor = async (id, data = {}) => {
return professorService.createProfessorFromUser(id, data);
};
module.exports = {
signUp,
getUserById,
getUserFullProfile,
getAllUsers,
searchUsers,
createUserAdmin,
updateUser,
deleteUser,
resetPasswordAndSendSms,
enrollUserInCourse
enrollUserInCourse,
promoteToProfessor
};