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:
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user