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.
This commit is contained in:
2026-08-16 08:22:18 +03:30
parent f57d8bbdfa
commit fafc362689
+82 -10
View File
@@ -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
};