Add class numberOfSessions and fix attendance completion detection.
Sessions with full student attendance are now excluded from the pending list reliably by loading class rosters from the database.
This commit is contained in:
@@ -73,6 +73,11 @@ const classSchema = new mongoose.Schema({
|
|||||||
trim: true,
|
trim: true,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
numberOfSessions: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const applyScheduleFields = (payload, body) => {
|
|||||||
return payload;
|
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 normalizePricingFields = (body = {}) => {
|
||||||
const tuitionFee = Math.max(0, Number(body.tuitionFee) || 0);
|
const tuitionFee = Math.max(0, Number(body.tuitionFee) || 0);
|
||||||
@@ -29,6 +29,13 @@ const normalizePricingFields = (body = {}) => {
|
|||||||
return { tuitionFee, hasDiscount, discount };
|
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 enrichClassForDisplay = (cls) => {
|
||||||
const tuitionFee = cls.tuitionFee || 0;
|
const tuitionFee = cls.tuitionFee || 0;
|
||||||
const discount = cls.hasDiscount ? (cls.discount || 0) : 0;
|
const discount = cls.hasDiscount ? (cls.discount || 0) : 0;
|
||||||
@@ -77,7 +84,11 @@ const getOne = async (id) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const create = async (body) => {
|
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) {
|
if (body.freeSpots === '' || body.freeSpots === null || body.freeSpots === undefined) {
|
||||||
payload.freeSpots = null;
|
payload.freeSpots = null;
|
||||||
} else {
|
} else {
|
||||||
@@ -88,7 +99,13 @@ const create = async (body) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const update = async (id, 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) {
|
if (body.freeSpots === '' || body.freeSpots === null) {
|
||||||
payload.freeSpots = null;
|
payload.freeSpots = null;
|
||||||
} else if (body.freeSpots !== undefined) {
|
} else if (body.freeSpots !== undefined) {
|
||||||
|
|||||||
@@ -111,6 +111,22 @@ const getSessionEndDateTime = (session) => {
|
|||||||
return end;
|
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) => {
|
const isSessionDue = (session) => {
|
||||||
if (session.status === 'cancelled') return false;
|
if (session.status === 'cancelled') return false;
|
||||||
const end = getSessionEndDateTime(session);
|
const end = getSessionEndDateTime(session);
|
||||||
@@ -118,38 +134,42 @@ const isSessionDue = (session) => {
|
|||||||
return end.getTime() <= Date.now();
|
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 || [];
|
const students = session.class?.students || [];
|
||||||
return students.map((student) => String(student._id || student)).filter(Boolean);
|
return students.map(normalizeId).filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRecordedUserIds = (session) =>
|
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 hasCompleteAttendance = (session, studentsByClassId = null) => {
|
||||||
const studentIds = getClassStudentIds(session);
|
const studentIds = getClassStudentIds(session, studentsByClassId);
|
||||||
if (!studentIds.length) return true;
|
if (!studentIds.length) return true;
|
||||||
const recorded = new Set(getRecordedUserIds(session));
|
const recorded = new Set(getRecordedUserIds(session));
|
||||||
return studentIds.every((id) => recorded.has(id));
|
return studentIds.every((id) => recorded.has(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAttendancePending = (session) =>
|
const isAttendancePending = (session, studentsByClassId = null) =>
|
||||||
session.status === 'held' && isSessionDue(session) && !hasCompleteAttendance(session);
|
isSessionDue(session) && !hasCompleteAttendance(session, studentsByClassId);
|
||||||
|
|
||||||
const enrichSessionAttendance = (session) => ({
|
const enrichSessionAttendance = (session, studentsByClassId = null) => ({
|
||||||
...session,
|
...session,
|
||||||
isDue: isSessionDue(session),
|
isDue: isSessionDue(session),
|
||||||
attendanceComplete: hasCompleteAttendance(session),
|
attendanceComplete: hasCompleteAttendance(session, studentsByClassId),
|
||||||
attendancePending: isAttendancePending(session)
|
attendancePending: isAttendancePending(session, studentsByClassId)
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterByAttendanceScope = (sessions, scope) => {
|
const filterByAttendanceScope = (sessions, scope, studentsByClassId = null) => {
|
||||||
const dueSessions = sessions.filter(isSessionDue);
|
const dueSessions = sessions.filter(isSessionDue);
|
||||||
if (scope === 'pending') {
|
if (scope === 'pending') {
|
||||||
return dueSessions.filter((session) => !hasCompleteAttendance(session));
|
return dueSessions.filter((session) => !hasCompleteAttendance(session, studentsByClassId));
|
||||||
}
|
}
|
||||||
if (scope === 'recorded') {
|
if (scope === 'recorded') {
|
||||||
return dueSessions.filter((session) => hasCompleteAttendance(session));
|
return dueSessions.filter((session) => hasCompleteAttendance(session, studentsByClassId));
|
||||||
}
|
}
|
||||||
if (scope === 'due') {
|
if (scope === 'due') {
|
||||||
return dueSessions;
|
return dueSessions;
|
||||||
@@ -213,7 +233,9 @@ const getAllSessions = async (queryParams) => {
|
|||||||
|
|
||||||
let sessions = sortByClosestAttendance(matched);
|
let sessions = sortByClosestAttendance(matched);
|
||||||
if (attendanceScope) {
|
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;
|
const totalCount = sessions.length;
|
||||||
sessions = sessions.slice(skip, skip + limit);
|
sessions = sessions.slice(skip, skip + limit);
|
||||||
const meta = calculateMeta(totalCount, page, limit);
|
const meta = calculateMeta(totalCount, page, limit);
|
||||||
@@ -328,6 +350,12 @@ const updateSessionAttendance = async (sessionId, attendanceList, recordedBy = n
|
|||||||
|
|
||||||
await session.save();
|
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, {
|
eventEmitter.emit(EVENT_NAMES.ATTENDANCE_RECORDED, {
|
||||||
sessionId: session._id,
|
sessionId: session._id,
|
||||||
recordCount: list.length,
|
recordCount: list.length,
|
||||||
|
|||||||
Reference in New Issue
Block a user