feat: add soft delete for classes with cascade to sessions and linked records

This commit is contained in:
2026-08-21 13:19:50 +03:30
parent 5b56171328
commit e34fd25af7
15 changed files with 184 additions and 33 deletions
+5
View File
@@ -30,6 +30,11 @@ exports.delete = catchAsync(async (req, res) => {
return successResponse(res, 200, 'Class deleted successfully'); return successResponse(res, 200, 'Class deleted successfully');
}); });
exports.restore = catchAsync(async (req, res) => {
const cls = await classService.restore(req.params.id);
return successResponse(res, 200, 'Class restored successfully', cls);
});
exports.registerUsers = catchAsync(async (req, res) => { exports.registerUsers = catchAsync(async (req, res) => {
const cls = await classService.registerUsers(req.params.id, req.body.userIds || [], req.body); const cls = await classService.registerUsers(req.params.id, req.body.userIds || [], req.body);
return successResponse(res, 200, 'Users registered in class successfully', cls); return successResponse(res, 200, 'Users registered in class successfully', cls);
+9
View File
@@ -108,6 +108,15 @@ const classSchema = new mongoose.Schema({
type: Boolean, type: Boolean,
default: true default: true
}, },
isDeleted: {
type: Boolean,
default: false,
index: true
},
deletedAt: {
type: Date,
default: null
},
adminNotes: { adminNotes: {
type: [String], type: [String],
default: [] default: []
+1
View File
@@ -24,6 +24,7 @@ router.get('/admin/get-one/:id', perm.requires(PERMISSIONS.CLASSES_READ), classC
router.post('/admin/create', perm.requires(PERMISSIONS.CLASSES_CREATE), classController.create); router.post('/admin/create', perm.requires(PERMISSIONS.CLASSES_CREATE), classController.create);
router.put('/admin/update/:id', perm.requires(PERMISSIONS.CLASSES_UPDATE), classController.update); router.put('/admin/update/:id', perm.requires(PERMISSIONS.CLASSES_UPDATE), classController.update);
router.delete('/admin/delete/:id', perm.requires(PERMISSIONS.CLASSES_DELETE), classController.delete); router.delete('/admin/delete/:id', perm.requires(PERMISSIONS.CLASSES_DELETE), classController.delete);
router.post('/admin/restore/:id', perm.requires(PERMISSIONS.CLASSES_DELETE), classController.restore);
router.post('/admin/:id/register-users', perm.requires(PERMISSIONS.CLASSES_REGISTER_USERS), classController.registerUsers); router.post('/admin/:id/register-users', perm.requires(PERMISSIONS.CLASSES_REGISTER_USERS), classController.registerUsers);
router.delete('/admin/:id/students/:userId', perm.requires(PERMISSIONS.CLASSES_REGISTER_USERS), classController.removeUser); router.delete('/admin/:id/students/:userId', perm.requires(PERMISSIONS.CLASSES_REGISTER_USERS), classController.removeUser);
+40 -14
View File
@@ -4,6 +4,8 @@
const Class = require('./classModel'); const Class = require('./classModel');
const User = require('../users/userModel'); const User = require('../users/userModel');
const Session = require('../sessions/sessionModel'); const Session = require('../sessions/sessionModel');
const PendingStudent = require('../pendingStudents/pendingStudentModel');
const Payment = require('../payments/paymentModel');
const AppError = require('../../utils/AppError'); const AppError = require('../../utils/AppError');
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination'); const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages'); const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages');
@@ -19,7 +21,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 numberOfSessions payoutType payoutPercentage payoutHourlyRate extraExpensePerSession isActive adminNotes createdAt updatedAt'; const CLASS_LIST_FIELDS = 'name course professor students capacity tuitionFee hasDiscount discount freeSpots showOnFrontend startDate endDate days startTime endTime numberOfSessions payoutType payoutPercentage payoutHourlyRate extraExpensePerSession isActive isDeleted deletedAt 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);
@@ -56,12 +58,18 @@ const enrichClassForDisplay = (cls) => {
return { ...cls, finalTuitionFee, daysUntilStart }; return { ...cls, finalTuitionFee, daysUntilStart };
}; };
const getAll = async (query) => { const getAll = async (query = {}) => {
const page = parseInt(query.page) || 1; const page = parseInt(query.page, 10) || 1;
const limit = Math.min(parseInt(query.limit) || 20, 200); const limit = Math.min(parseInt(query.limit, 10) || 20, 200);
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const filter = {}; const filter = {};
if (query.trash === 'true' || query.isDeleted === 'true') {
filter.isDeleted = true;
} else {
filter.isDeleted = { $ne: true };
}
if (query.courseId) filter.course = query.courseId; if (query.courseId) filter.course = query.courseId;
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true'; if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
@@ -130,8 +138,24 @@ const update = async (id, body) => {
}; };
const remove = async (id) => { const remove = async (id) => {
const cls = await Class.findByIdAndDelete(id); const cls = await Class.findById(id);
if (!cls) throw new AppError('CLASS_NOT_FOUND'); if (!cls) throw new AppError('CLASS_NOT_FOUND');
const now = new Date();
await Class.findByIdAndUpdate(id, { $set: { isDeleted: true, deletedAt: now } });
await Session.updateMany({ class: id, isDeleted: { $ne: true } }, { $set: { isDeleted: true, deletedAt: now } });
await PendingStudent.updateMany({ class: id, isDeleted: { $ne: true } }, { $set: { isDeleted: true, deletedAt: now } });
await Payment.updateMany({ classes: id, isDeleted: { $ne: true } }, { $set: { isDeleted: true, deletedAt: now } });
return { success: true };
};
const restore = async (id) => {
const cls = await Class.findById(id);
if (!cls) throw new AppError('CLASS_NOT_FOUND');
await Class.findByIdAndUpdate(id, { $set: { isDeleted: false, deletedAt: null } });
await Session.updateMany({ class: id, isDeleted: true }, { $set: { isDeleted: false, deletedAt: null } });
await PendingStudent.updateMany({ class: id, isDeleted: true }, { $set: { isDeleted: false, deletedAt: null } });
await Payment.updateMany({ classes: id, isDeleted: true }, { $set: { isDeleted: false, deletedAt: null } });
return getOne(id);
}; };
const registerUsers = async (classId, userIds, notifyInput = {}) => { const registerUsers = async (classId, userIds, notifyInput = {}) => {
@@ -151,7 +175,7 @@ const registerUsers = async (classId, userIds, notifyInput = {}) => {
if (notify.sms || notify.email || notify.bot) { if (notify.sms || notify.email || notify.bot) {
const classLabel = cls.name || cls.course?.title || 'کلاس'; const classLabel = cls.name || cls.course?.title || 'کلاس';
const sessions = await Session.find({ class: classId }) const sessions = await Session.find({ class: classId, isDeleted: { $ne: true } })
.select('day startTime endTime') .select('day startTime endTime')
.sort({ day: 1 }) .sort({ day: 1 })
.lean(); .lean();
@@ -200,11 +224,11 @@ const removeUser = async (classId, userId) => {
}; };
const getMyClasses = async (userId, query = {}) => { const getMyClasses = async (userId, query = {}) => {
const page = parseInt(query.page) || 1; const page = parseInt(query.page, 10) || 1;
const limit = Math.min(parseInt(query.limit) || 20, 200); const limit = Math.min(parseInt(query.limit, 10) || 20, 200);
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const filter = { students: userId }; const filter = { students: userId, isDeleted: { $ne: true } };
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true'; if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
const [items, total] = await Promise.all([ const [items, total] = await Promise.all([
@@ -219,13 +243,14 @@ const getMyClasses = async (userId, query = {}) => {
}; };
const getPublicClasses = async (query = {}) => { const getPublicClasses = async (query = {}) => {
const page = parseInt(query.page) || 1; const page = parseInt(query.page, 10) || 1;
const limit = Math.min(parseInt(query.limit) || 50, 200); const limit = Math.min(parseInt(query.limit, 10) || 50, 200);
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const filter = { const filter = {
isActive: { $ne: false }, isActive: { $ne: false },
showOnFrontend: { $ne: false } showOnFrontend: { $ne: false },
isDeleted: { $ne: true }
}; };
if (query.courseId) filter.course = query.courseId; if (query.courseId) filter.course = query.courseId;
@@ -245,7 +270,8 @@ const getPublicOne = async (id) => {
const cls = await Class.findOne({ const cls = await Class.findOne({
_id: id, _id: id,
isActive: { $ne: false }, isActive: { $ne: false },
showOnFrontend: { $ne: false } showOnFrontend: { $ne: false },
isDeleted: { $ne: true }
}) })
.select('name course professor capacity tuitionFee hasDiscount discount freeSpots startDate endDate days startTime endTime isActive') .select('name course professor capacity tuitionFee hasDiscount discount freeSpots startDate endDate days startTime endTime isActive')
.populate({ path: 'course', select: 'title type description sectionCount hoursPerSection price' }) .populate({ path: 'course', select: 'title type description sectionCount hoursPerSection price' })
@@ -256,4 +282,4 @@ const getPublicOne = async (id) => {
return enrichClassForDisplay(cls); return enrichClassForDisplay(cls);
}; };
module.exports = { getAll, getOne, create, update, remove, registerUsers, removeUser, getMyClasses, getPublicClasses, getPublicOne }; module.exports = { getAll, getOne, create, update, remove, restore, registerUsers, removeUser, getMyClasses, getPublicClasses, getPublicOne };
@@ -0,0 +1,62 @@
'use strict';
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const Class = require('./classModel');
const Session = require('../sessions/sessionModel');
const PendingStudent = require('../pendingStudents/pendingStudentModel');
const Payment = require('../payments/paymentModel');
describe('Soft delete schema configuration', () => {
it('has isDeleted and deletedAt in Class schema', () => {
const isDeletedPath = Class.schema.path('isDeleted');
const deletedAtPath = Class.schema.path('deletedAt');
assert.ok(isDeletedPath, 'Class schema should have isDeleted path');
assert.equal(isDeletedPath.instance, 'Boolean');
assert.equal(isDeletedPath.defaultValue, false);
assert.ok(deletedAtPath, 'Class schema should have deletedAt path');
assert.equal(deletedAtPath.instance, 'Date');
assert.equal(deletedAtPath.defaultValue, null);
});
it('has isDeleted and deletedAt in Session schema', () => {
const isDeletedPath = Session.schema.path('isDeleted');
const deletedAtPath = Session.schema.path('deletedAt');
assert.ok(isDeletedPath, 'Session schema should have isDeleted path');
assert.equal(isDeletedPath.instance, 'Boolean');
assert.equal(isDeletedPath.defaultValue, false);
assert.ok(deletedAtPath, 'Session schema should have deletedAt path');
assert.equal(deletedAtPath.instance, 'Date');
assert.equal(deletedAtPath.defaultValue, null);
});
it('has isDeleted and deletedAt in PendingStudent schema', () => {
const isDeletedPath = PendingStudent.schema.path('isDeleted');
const deletedAtPath = PendingStudent.schema.path('deletedAt');
assert.ok(isDeletedPath, 'PendingStudent schema should have isDeleted path');
assert.equal(isDeletedPath.instance, 'Boolean');
assert.equal(isDeletedPath.defaultValue, false);
assert.ok(deletedAtPath, 'PendingStudent schema should have deletedAt path');
assert.equal(deletedAtPath.instance, 'Date');
assert.equal(deletedAtPath.defaultValue, null);
});
it('has isDeleted and deletedAt in Payment schema', () => {
const isDeletedPath = Payment.schema.path('isDeleted');
const deletedAtPath = Payment.schema.path('deletedAt');
assert.ok(isDeletedPath, 'Payment schema should have isDeleted path');
assert.equal(isDeletedPath.instance, 'Boolean');
assert.equal(isDeletedPath.defaultValue, false);
assert.ok(deletedAtPath, 'Payment schema should have deletedAt path');
assert.equal(deletedAtPath.instance, 'Date');
assert.equal(deletedAtPath.defaultValue, null);
});
});
+3 -3
View File
@@ -24,8 +24,8 @@ const getAdminStats = async () => {
Professor.countDocuments({}), Professor.countDocuments({}),
Professor.countDocuments({ isActive: true }), Professor.countDocuments({ isActive: true }),
Course.countDocuments({}), Course.countDocuments({}),
Session.countDocuments({}), Session.countDocuments({ isDeleted: { $ne: true } }),
Session.find({}) Session.find({ isDeleted: { $ne: true } })
.sort({ day: -1 }) .sort({ day: -1 })
.limit(8) .limit(8)
.populate('course', 'title type') .populate('course', 'title type')
@@ -75,7 +75,7 @@ const getAdminStats = async () => {
// Daily sessions trend (same window) // Daily sessions trend (same window)
const dailySessionsRaw = await Session.aggregate([ const dailySessionsRaw = await Session.aggregate([
{ $match: { createdAt: { $gte: rangeStart } } }, { $match: { isDeleted: { $ne: true }, createdAt: { $gte: rangeStart } } },
{ {
$group: { $group: {
_id: { _id: {
@@ -33,7 +33,7 @@ const normalizeId = (value) => {
/** Revenue collected/owed for a set of classIds, derived from Payment records (one payment ~ one class enrollment). */ /** Revenue collected/owed for a set of classIds, derived from Payment records (one payment ~ one class enrollment). */
const getRevenueByClass = async (classIds) => { const getRevenueByClass = async (classIds) => {
const payments = await Payment.find({ classes: { $in: classIds } }) const payments = await Payment.find({ classes: { $in: classIds }, isDeleted: { $ne: true } })
.select('classes amount discount paidAmount') .select('classes amount discount paidAmount')
.lean(); .lean();
@@ -53,7 +53,7 @@ const getRevenueByClass = async (classIds) => {
}; };
const getSessionCountsByClass = async (classIds) => { const getSessionCountsByClass = async (classIds) => {
const sessions = await Session.find({ class: { $in: classIds } }).select('class status').lean(); const sessions = await Session.find({ class: { $in: classIds }, isDeleted: { $ne: true } }).select('class status').lean();
const byClass = new Map(classIds.map((id) => [String(id), { total: 0, held: 0, scheduled: 0, cancelled: 0 }])); const byClass = new Map(classIds.map((id) => [String(id), { total: 0, held: 0, scheduled: 0, cancelled: 0 }]));
for (const session of sessions) { for (const session of sessions) {
const key = String(session.class); const key = String(session.class);
@@ -224,7 +224,7 @@ const getSessionReport = async (sessionId) => {
const getRangeReport = async (query = {}) => { const getRangeReport = async (query = {}) => {
const { start, end } = resolveDateRange(query); const { start, end } = resolveDateRange(query);
const classFilter = { isActive: { $ne: false } }; const classFilter = { isActive: { $ne: false }, isDeleted: { $ne: true } };
if (query.classId) classFilter._id = query.classId; if (query.classId) classFilter._id = query.classId;
const classes = await Class.find(classFilter) const classes = await Class.find(classFilter)
@@ -248,7 +248,7 @@ const getRangeReport = async (query = {}) => {
}; };
} }
const sessions = await Session.find({ class: { $in: classIds }, day: { $gte: start, $lte: end } }) const sessions = await Session.find({ class: { $in: classIds }, day: { $gte: start, $lte: end }, isDeleted: { $ne: true } })
.select('class status day') .select('class status day')
.lean(); .lean();
@@ -259,7 +259,7 @@ const getRangeReport = async (query = {}) => {
heldByClass.set(key, (heldByClass.get(key) || 0) + 1); heldByClass.set(key, (heldByClass.get(key) || 0) + 1);
} }
const payments = await Payment.find({ classes: { $in: classIds } }).select('_id classes').lean(); const payments = await Payment.find({ classes: { $in: classIds }, isDeleted: { $ne: true } }).select('_id classes').lean();
const paymentClassByPaymentId = new Map(); const paymentClassByPaymentId = new Map();
const paymentIds = []; const paymentIds = [];
for (const payment of payments) { for (const payment of payments) {
+10 -1
View File
@@ -45,7 +45,16 @@ const paymentSchema = new mongoose.Schema({
enum: ['pending', 'partial', 'paid', 'overdue'], enum: ['pending', 'partial', 'paid', 'overdue'],
default: 'pending' default: 'pending'
}, },
notes: { type: String, trim: true, maxlength: 5000 } notes: { type: String, trim: true, maxlength: 5000 },
isDeleted: {
type: Boolean,
default: false,
index: true
},
deletedAt: {
type: Date,
default: null
}
}, { }, {
timestamps: true, timestamps: true,
toJSON: { virtuals: true }, toJSON: { virtuals: true },
+5
View File
@@ -253,6 +253,11 @@ const getAllPayments = async (query) => {
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const filter = {}; const filter = {};
if (query.trash === 'true' || query.isDeleted === 'true') {
filter.isDeleted = true;
} else {
filter.isDeleted = { $ne: true };
}
if (query.userId) filter.user = query.userId; if (query.userId) filter.user = query.userId;
if (query.status) filter.status = query.status; if (query.status) filter.status = query.status;
@@ -51,7 +51,16 @@ const pendingStudentSchema = new mongoose.Schema({
paymentReference: { type: String, trim: true }, paymentReference: { type: String, trim: true },
adminNotes: { type: String, trim: true, maxlength: 2000 }, adminNotes: { type: String, trim: true, maxlength: 2000 },
reviewedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, reviewedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
reviewedAt: { type: Date } reviewedAt: { type: Date },
isDeleted: {
type: Boolean,
default: false,
index: true
},
deletedAt: {
type: Date,
default: null
}
}, { }, {
timestamps: true timestamps: true
}); });
@@ -256,6 +256,11 @@ const getAllPendingStudents = async (query = {}) => {
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const filter = {}; const filter = {};
if (query.trash === 'true' || query.isDeleted === 'true') {
filter.isDeleted = true;
} else {
filter.isDeleted = { $ne: true };
}
if (query.status) filter.status = query.status; if (query.status) filter.status = query.status;
if (query.type) filter.type = query.type; if (query.type) filter.type = query.type;
if (query.classId) filter.class = query.classId; if (query.classId) filter.class = query.classId;
+9
View File
@@ -84,6 +84,15 @@ const sessionSchema = new mongoose.Schema({
adminNotes: { adminNotes: {
type: [String], type: [String],
default: [] default: []
},
isDeleted: {
type: Boolean,
default: false,
index: true
},
deletedAt: {
type: Date,
default: null
} }
}, { }, {
timestamps: true timestamps: true
+15 -6
View File
@@ -124,7 +124,7 @@ const buildStudentsByClassId = async (sessions) => {
)]; )];
if (!classIds.length) return new Map(); if (!classIds.length) return new Map();
const classes = await Class.find({ _id: { $in: classIds } }).select('students').lean(); const classes = await Class.find({ _id: { $in: classIds }, isDeleted: { $ne: true } }).select('students').lean();
const allStudentIds = [...new Set( const allStudentIds = [...new Set(
classes.flatMap((cls) => (cls.students || []).map(normalizeId)).filter(Boolean) classes.flatMap((cls) => (cls.students || []).map(normalizeId)).filter(Boolean)
)]; )];
@@ -257,6 +257,12 @@ const getAllSessions = async (queryParams) => {
} }
if (queryParams.professorId) filter.professor = queryParams.professorId; if (queryParams.professorId) filter.professor = queryParams.professorId;
if (queryParams.trash === 'true' || queryParams.isDeleted === 'true') {
filter.isDeleted = true;
} else {
filter.isDeleted = { $ne: true };
}
if (filter.status) { if (filter.status) {
filter.status = STATUS_MAP[filter.status] || filter.status; filter.status = STATUS_MAP[filter.status] || filter.status;
} }
@@ -313,7 +319,7 @@ const deleteSession = async (id) => {
if (!session) { if (!session) {
throw new AppError('SESSION_NOT_FOUND'); throw new AppError('SESSION_NOT_FOUND');
} }
await Session.findByIdAndDelete(id); await Session.findByIdAndUpdate(id, { $set: { isDeleted: true, deletedAt: new Date() } });
return null; return null;
}; };
@@ -328,8 +334,11 @@ const bulkDeleteSessions = async (ids) => {
throw new AppError('VALIDATION_FAILED', { ids: 'Required' }, 'هیچ جلسه‌ای انتخاب نشده است.'); throw new AppError('VALIDATION_FAILED', { ids: 'Required' }, 'هیچ جلسه‌ای انتخاب نشده است.');
} }
const result = await Session.deleteMany({ _id: { $in: sessionIds } }); const result = await Session.updateMany(
return { deletedCount: result.deletedCount || 0 }; { _id: { $in: sessionIds } },
{ $set: { isDeleted: true, deletedAt: new Date() } }
);
return { deletedCount: result.modifiedCount || result.nModified || 0 };
}; };
const bulkUpdateSessionStatus = async (ids, status, actorId = null) => { const bulkUpdateSessionStatus = async (ids, status, actorId = null) => {
@@ -405,10 +414,10 @@ const updateSessionAttendance = async (sessionId, attendanceList, recordedBy = n
}; };
const getMySessions = async (userId, queryParams) => { const getMySessions = async (userId, queryParams) => {
const userClasses = await Class.find({ students: userId }).select('_id'); const userClasses = await Class.find({ students: userId, isDeleted: { $ne: true } }).select('_id');
const classIds = userClasses.map((c) => c._id); const classIds = userClasses.map((c) => c._id);
const filter = { class: { $in: classIds } }; const filter = { class: { $in: classIds }, isDeleted: { $ne: true } };
const { page, limit, skip } = parsePaginationAndSort(queryParams, 'day', 'asc'); const { page, limit, skip } = parsePaginationAndSort(queryParams, 'day', 'asc');
const [matched, totalCount] = await Promise.all([ const [matched, totalCount] = await Promise.all([
+4 -2
View File
@@ -38,8 +38,9 @@ const runClassReminderJob = async () => {
status: 'scheduled', status: 'scheduled',
reminderSentAt: null, reminderSentAt: null,
day: { $gte: dayFrom, $lte: dayTo }, day: { $gte: dayFrom, $lte: dayTo },
isDeleted: { $ne: true }
}) })
.populate({ path: 'class', select: 'name students startDate days startTime endTime uniqueCode' }) .populate({ path: 'class', select: 'name students startDate days startTime endTime uniqueCode isDeleted' })
.populate({ path: 'course', select: 'title' }) .populate({ path: 'course', select: 'title' })
.lean(); .lean();
@@ -48,6 +49,7 @@ const runClassReminderJob = async () => {
if (!startAt || startAt < windowStart || startAt > windowEnd) continue; if (!startAt || startAt < windowStart || startAt > windowEnd) continue;
const classDoc = session.class; const classDoc = session.class;
if (!classDoc || classDoc.isDeleted) continue;
const studentIds = classDoc?.students || []; const studentIds = classDoc?.students || [];
if (studentIds.length === 0) { if (studentIds.length === 0) {
await Session.updateOne({ _id: session._id }, { reminderSentAt: new Date() }); await Session.updateOne({ _id: session._id }, { reminderSentAt: new Date() });
@@ -58,7 +60,7 @@ const runClassReminderJob = async () => {
const timeLabel = session.startTime; const timeLabel = session.startTime;
const placeLabel = session.place || '-'; const placeLabel = session.place || '-';
const classSessions = classDoc?._id const classSessions = classDoc?._id
? await Session.find({ class: classDoc._id }).select('day startTime endTime').sort({ day: 1 }).lean() ? await Session.find({ class: classDoc._id, isDeleted: { $ne: true } }).select('day startTime endTime').sort({ day: 1 }).lean()
: []; : [];
const schedule = buildClassScheduleContext(classDoc, classSessions, session); const schedule = buildClassScheduleContext(classDoc, classSessions, session);
+1 -1
View File
@@ -7,7 +7,7 @@
"start": "node app.js", "start": "node app.js",
"dev": "nodemon app.js", "dev": "nodemon app.js",
"seed": "node seed.js", "seed": "node seed.js",
"test": "node --test components/settings/smsTemplates.test.js components/users/passwordReset.test.js components/dataImport/importHelpers.test.js utils/classSchedule.test.js utils/jalaliDate.test.js utils/messagingChannels.test.js utils/notifyFlags.test.js utils/passwordRules.test.js utils/paymentAmount.test.js utils/professorShare.test.js utils/financialRange.test.js" "test": "node --test components/classes/classSoftDelete.test.js components/settings/smsTemplates.test.js components/users/passwordReset.test.js components/dataImport/importHelpers.test.js utils/classSchedule.test.js utils/jalaliDate.test.js utils/messagingChannels.test.js utils/notifyFlags.test.js utils/passwordRules.test.js utils/paymentAmount.test.js utils/professorShare.test.js utils/financialRange.test.js"
}, },
"keywords": [ "keywords": [
"express", "express",