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;
|
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. */
|
/** Upcoming soonest first, then most recent past — nearest attendance date. */
|
||||||
const sortByClosestAttendance = (sessions) => {
|
const sortByClosestAttendance = (sessions) => {
|
||||||
const today = startOfToday().getTime();
|
const today = startOfToday().getTime();
|
||||||
@@ -115,14 +171,17 @@ const sortByClosestAttendance = (sessions) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const populateSessionList = (query) =>
|
const populateSessionList = (query, { includeClassStudents = false } = {}) => {
|
||||||
query
|
let chain = query
|
||||||
.populate('course', 'title type')
|
.populate('course', 'title type')
|
||||||
.populate('class', 'name')
|
.populate('class', includeClassStudents ? 'name students' : 'name')
|
||||||
.populate('professor', 'name surname');
|
.populate('professor', 'name surname');
|
||||||
|
return chain;
|
||||||
|
};
|
||||||
|
|
||||||
const getAllSessions = async (queryParams) => {
|
const getAllSessions = async (queryParams) => {
|
||||||
const { page, limit, skip } = parsePaginationAndSort(queryParams, 'day', 'asc');
|
const { page, limit, skip } = parsePaginationAndSort(queryParams, 'day', 'asc');
|
||||||
|
const attendanceScope = queryParams.attendanceScope;
|
||||||
const filter = buildFilterQuery(queryParams, ['topic', 'place', 'note'], [
|
const filter = buildFilterQuery(queryParams, ['topic', 'place', 'note'], [
|
||||||
'page',
|
'page',
|
||||||
'limit',
|
'limit',
|
||||||
@@ -133,7 +192,8 @@ const getAllSessions = async (queryParams) => {
|
|||||||
'courseId',
|
'courseId',
|
||||||
'classId',
|
'classId',
|
||||||
'professorId',
|
'professorId',
|
||||||
'class' // handled below so we always cast consistently
|
'class',
|
||||||
|
'attendanceScope'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (queryParams.courseId) filter.course = queryParams.courseId;
|
if (queryParams.courseId) filter.course = queryParams.courseId;
|
||||||
@@ -147,12 +207,21 @@ const getAllSessions = async (queryParams) => {
|
|||||||
filter.status = STATUS_MAP[filter.status] || filter.status;
|
filter.status = STATUS_MAP[filter.status] || filter.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [matched, totalCount] = await Promise.all([
|
const includeClassStudents = Boolean(attendanceScope);
|
||||||
populateSessionList(Session.find(filter)).lean(),
|
|
||||||
Session.countDocuments(filter)
|
|
||||||
]);
|
|
||||||
|
|
||||||
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);
|
const meta = calculateMeta(totalCount, page, limit);
|
||||||
return { data: sessions, meta };
|
return { data: sessions, meta };
|
||||||
};
|
};
|
||||||
@@ -295,5 +364,8 @@ module.exports = {
|
|||||||
bulkUpdateSessionStatus,
|
bulkUpdateSessionStatus,
|
||||||
searchSessions,
|
searchSessions,
|
||||||
updateSessionAttendance,
|
updateSessionAttendance,
|
||||||
getMySessions
|
getMySessions,
|
||||||
|
isSessionDue,
|
||||||
|
hasCompleteAttendance,
|
||||||
|
isAttendancePending
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user