diff --git a/components/classes/classModel.js b/components/classes/classModel.js index 36493fb..a70a8d1 100644 --- a/components/classes/classModel.js +++ b/components/classes/classModel.js @@ -73,6 +73,11 @@ const classSchema = new mongoose.Schema({ trim: true, default: '' }, + numberOfSessions: { + type: Number, + min: 1, + default: null + }, isActive: { type: Boolean, default: true diff --git a/components/classes/classService.js b/components/classes/classService.js index abb178e..559c634 100644 --- a/components/classes/classService.js +++ b/components/classes/classService.js @@ -18,7 +18,7 @@ const applyScheduleFields = (payload, body) => { return payload; }; -const CLASS_LIST_FIELDS = 'name course professor students capacity tuitionFee hasDiscount discount freeSpots showOnFrontend startDate endDate days startTime endTime isActive adminNotes createdAt updatedAt'; +const CLASS_LIST_FIELDS = 'name course professor students capacity tuitionFee hasDiscount discount freeSpots showOnFrontend startDate endDate days startTime endTime numberOfSessions isActive adminNotes createdAt updatedAt'; const normalizePricingFields = (body = {}) => { const tuitionFee = Math.max(0, Number(body.tuitionFee) || 0); @@ -29,6 +29,13 @@ const normalizePricingFields = (body = {}) => { return { tuitionFee, hasDiscount, discount }; }; +const normalizeNumberOfSessions = (value) => { + if (value === '' || value === null || value === undefined) return null; + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 1) return null; + return Math.floor(parsed); +}; + const enrichClassForDisplay = (cls) => { const tuitionFee = cls.tuitionFee || 0; const discount = cls.hasDiscount ? (cls.discount || 0) : 0; @@ -77,7 +84,11 @@ const getOne = async (id) => { }; const create = async (body) => { - const payload = applyScheduleFields({ ...body, ...normalizePricingFields(body) }, body); + const payload = applyScheduleFields({ + ...body, + ...normalizePricingFields(body), + numberOfSessions: normalizeNumberOfSessions(body.numberOfSessions) + }, body); if (body.freeSpots === '' || body.freeSpots === null || body.freeSpots === undefined) { payload.freeSpots = null; } else { @@ -88,7 +99,13 @@ const create = async (body) => { }; const update = async (id, body) => { - const payload = applyScheduleFields({ ...body, ...normalizePricingFields(body) }, body); + const payload = applyScheduleFields({ + ...body, + ...normalizePricingFields(body), + ...(body.numberOfSessions !== undefined + ? { numberOfSessions: normalizeNumberOfSessions(body.numberOfSessions) } + : {}) + }, body); if (body.freeSpots === '' || body.freeSpots === null) { payload.freeSpots = null; } else if (body.freeSpots !== undefined) { diff --git a/components/sessions/sessionService.js b/components/sessions/sessionService.js index a1525a6..881f64d 100644 --- a/components/sessions/sessionService.js +++ b/components/sessions/sessionService.js @@ -111,6 +111,22 @@ const getSessionEndDateTime = (session) => { return end; }; +const normalizeId = (value) => { + if (value == null) return ''; + if (typeof value === 'object' && value._id != null) return String(value._id); + return String(value); +}; + +const buildStudentsByClassId = async (sessions) => { + const classIds = [...new Set( + sessions.map((session) => normalizeId(session.class?._id || session.class)).filter(Boolean) + )]; + if (!classIds.length) return new Map(); + + const classes = await Class.find({ _id: { $in: classIds } }).select('students').lean(); + return new Map(classes.map((cls) => [String(cls._id), cls.students || []])); +}; + const isSessionDue = (session) => { if (session.status === 'cancelled') return false; const end = getSessionEndDateTime(session); @@ -118,38 +134,42 @@ const isSessionDue = (session) => { return end.getTime() <= Date.now(); }; -const getClassStudentIds = (session) => { +const getClassStudentIds = (session, studentsByClassId = null) => { + const classId = normalizeId(session.class?._id || session.class); + if (studentsByClassId && classId && studentsByClassId.has(classId)) { + return (studentsByClassId.get(classId) || []).map(normalizeId).filter(Boolean); + } const students = session.class?.students || []; - return students.map((student) => String(student._id || student)).filter(Boolean); + return students.map(normalizeId).filter(Boolean); }; const getRecordedUserIds = (session) => - (session.attendanceList || []).map((record) => String(record.user?._id || record.user)).filter(Boolean); + (session.attendanceList || []).map((record) => normalizeId(record.user)).filter(Boolean); -const hasCompleteAttendance = (session) => { - const studentIds = getClassStudentIds(session); +const hasCompleteAttendance = (session, studentsByClassId = null) => { + const studentIds = getClassStudentIds(session, studentsByClassId); if (!studentIds.length) return true; const recorded = new Set(getRecordedUserIds(session)); return studentIds.every((id) => recorded.has(id)); }; -const isAttendancePending = (session) => - session.status === 'held' && isSessionDue(session) && !hasCompleteAttendance(session); +const isAttendancePending = (session, studentsByClassId = null) => + isSessionDue(session) && !hasCompleteAttendance(session, studentsByClassId); -const enrichSessionAttendance = (session) => ({ +const enrichSessionAttendance = (session, studentsByClassId = null) => ({ ...session, isDue: isSessionDue(session), - attendanceComplete: hasCompleteAttendance(session), - attendancePending: isAttendancePending(session) + attendanceComplete: hasCompleteAttendance(session, studentsByClassId), + attendancePending: isAttendancePending(session, studentsByClassId) }); -const filterByAttendanceScope = (sessions, scope) => { +const filterByAttendanceScope = (sessions, scope, studentsByClassId = null) => { const dueSessions = sessions.filter(isSessionDue); if (scope === 'pending') { - return dueSessions.filter((session) => !hasCompleteAttendance(session)); + return dueSessions.filter((session) => !hasCompleteAttendance(session, studentsByClassId)); } if (scope === 'recorded') { - return dueSessions.filter((session) => hasCompleteAttendance(session)); + return dueSessions.filter((session) => hasCompleteAttendance(session, studentsByClassId)); } if (scope === 'due') { return dueSessions; @@ -213,7 +233,9 @@ const getAllSessions = async (queryParams) => { let sessions = sortByClosestAttendance(matched); if (attendanceScope) { - sessions = filterByAttendanceScope(sessions, attendanceScope).map(enrichSessionAttendance); + const studentsByClassId = await buildStudentsByClassId(matched); + sessions = filterByAttendanceScope(sessions, attendanceScope, studentsByClassId) + .map((session) => enrichSessionAttendance(session, studentsByClassId)); const totalCount = sessions.length; sessions = sessions.slice(skip, skip + limit); const meta = calculateMeta(totalCount, page, limit); @@ -328,6 +350,12 @@ const updateSessionAttendance = async (sessionId, attendanceList, recordedBy = n await session.save(); + const classItem = await Class.findById(session.class).select('students').lean(); + if (classItem && hasCompleteAttendance({ ...session.toObject(), class: classItem }) && session.status !== 'cancelled') { + session.status = 'held'; + await session.save(); + } + eventEmitter.emit(EVENT_NAMES.ATTENDANCE_RECORDED, { sessionId: session._id, recordCount: list.length,