From fafc362689f0d2627fea406c4f2035ec563f49a9 Mon Sep 17 00:00:00 2001 From: Kavehhn174 Date: Sun, 16 Aug 2026 08:22:18 +0330 Subject: [PATCH] fix: filter attendance sessions to past-due with incomplete records Only list sessions after their end time when at least one enrolled student lacks attendance, and expose attendanceScope on session listing. --- components/sessions/sessionService.js | 92 ++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/components/sessions/sessionService.js b/components/sessions/sessionService.js index e997016..a1525a6 100644 --- a/components/sessions/sessionService.js +++ b/components/sessions/sessionService.js @@ -101,6 +101,62 @@ const startOfToday = () => { return d; }; +const getSessionEndDateTime = (session) => { + const end = new Date(session.day || session.date || 0); + if (Number.isNaN(end.getTime())) return null; + const parts = String(session.endTime || '23:59').trim().split(':'); + const hours = Number(parts[0]); + const minutes = Number(parts[1]); + end.setHours(Number.isFinite(hours) ? hours : 23, Number.isFinite(minutes) ? minutes : 59, 59, 999); + return end; +}; + +const isSessionDue = (session) => { + if (session.status === 'cancelled') return false; + const end = getSessionEndDateTime(session); + if (!end) return false; + return end.getTime() <= Date.now(); +}; + +const getClassStudentIds = (session) => { + const students = session.class?.students || []; + return students.map((student) => String(student._id || student)).filter(Boolean); +}; + +const getRecordedUserIds = (session) => + (session.attendanceList || []).map((record) => String(record.user?._id || record.user)).filter(Boolean); + +const hasCompleteAttendance = (session) => { + const studentIds = getClassStudentIds(session); + 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 enrichSessionAttendance = (session) => ({ + ...session, + isDue: isSessionDue(session), + attendanceComplete: hasCompleteAttendance(session), + attendancePending: isAttendancePending(session) +}); + +const filterByAttendanceScope = (sessions, scope) => { + const dueSessions = sessions.filter(isSessionDue); + if (scope === 'pending') { + return dueSessions.filter((session) => !hasCompleteAttendance(session)); + } + if (scope === 'recorded') { + return dueSessions.filter((session) => hasCompleteAttendance(session)); + } + if (scope === 'due') { + return dueSessions; + } + return sessions; +}; + /** Upcoming soonest first, then most recent past — nearest attendance date. */ const sortByClosestAttendance = (sessions) => { const today = startOfToday().getTime(); @@ -115,14 +171,17 @@ const sortByClosestAttendance = (sessions) => { }); }; -const populateSessionList = (query) => - query +const populateSessionList = (query, { includeClassStudents = false } = {}) => { + let chain = query .populate('course', 'title type') - .populate('class', 'name') + .populate('class', includeClassStudents ? 'name students' : 'name') .populate('professor', 'name surname'); + return chain; +}; const getAllSessions = async (queryParams) => { const { page, limit, skip } = parsePaginationAndSort(queryParams, 'day', 'asc'); + const attendanceScope = queryParams.attendanceScope; const filter = buildFilterQuery(queryParams, ['topic', 'place', 'note'], [ 'page', 'limit', @@ -133,7 +192,8 @@ const getAllSessions = async (queryParams) => { 'courseId', 'classId', 'professorId', - 'class' // handled below so we always cast consistently + 'class', + 'attendanceScope' ]); if (queryParams.courseId) filter.course = queryParams.courseId; @@ -147,12 +207,21 @@ const getAllSessions = async (queryParams) => { filter.status = STATUS_MAP[filter.status] || filter.status; } - const [matched, totalCount] = await Promise.all([ - populateSessionList(Session.find(filter)).lean(), - Session.countDocuments(filter) - ]); + const includeClassStudents = Boolean(attendanceScope); - const sessions = sortByClosestAttendance(matched).slice(skip, skip + limit); + const matched = await populateSessionList(Session.find(filter), { includeClassStudents }).lean(); + + let sessions = sortByClosestAttendance(matched); + if (attendanceScope) { + sessions = filterByAttendanceScope(sessions, attendanceScope).map(enrichSessionAttendance); + const totalCount = sessions.length; + sessions = sessions.slice(skip, skip + limit); + const meta = calculateMeta(totalCount, page, limit); + return { data: sessions, meta }; + } + + const totalCount = await Session.countDocuments(filter); + sessions = sessions.slice(skip, skip + limit); const meta = calculateMeta(totalCount, page, limit); return { data: sessions, meta }; }; @@ -295,5 +364,8 @@ module.exports = { bulkUpdateSessionStatus, searchSessions, updateSessionAttendance, - getMySessions + getMySessions, + isSessionDue, + hasCompleteAttendance, + isAttendancePending };