Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
109 lines
3.6 KiB
JavaScript
109 lines
3.6 KiB
JavaScript
// /components/classes/classService.js
|
|
'use strict';
|
|
|
|
const Class = require('./classModel');
|
|
const User = require('../users/userModel');
|
|
const AppError = require('../../utils/AppError');
|
|
const { calculateMeta } = require('../../utils/pagination');
|
|
const { sendClassRegisteredSms } = require('../../utils/senders/smsMessages');
|
|
const logger = require('../../utils/logger');
|
|
|
|
const getAll = async (query) => {
|
|
const page = parseInt(query.page) || 1;
|
|
const limit = Math.min(parseInt(query.limit) || 20, 200);
|
|
const skip = (page - 1) * limit;
|
|
|
|
const filter = {};
|
|
if (query.courseId) filter.course = query.courseId;
|
|
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
|
|
|
|
const [items, total] = await Promise.all([
|
|
Class.find(filter)
|
|
.populate({ path: 'course', select: 'title type price' })
|
|
.populate({ path: 'professor', select: 'name surname' })
|
|
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
|
|
Class.countDocuments(filter)
|
|
]);
|
|
|
|
return { data: items, meta: calculateMeta(total, page, limit) };
|
|
};
|
|
|
|
const getOne = async (id) => {
|
|
const cls = await Class.findById(id)
|
|
.populate({ path: 'course', select: 'title type price' })
|
|
.populate({ path: 'professor', select: 'name surname phoneNumber' })
|
|
.populate({ path: 'students', select: 'name surname phoneNumber' })
|
|
.lean();
|
|
if (!cls) throw new AppError('CLASS_NOT_FOUND');
|
|
return cls;
|
|
};
|
|
|
|
const create = async (body) => {
|
|
const cls = await Class.create(body);
|
|
return getOne(cls._id);
|
|
};
|
|
|
|
const update = async (id, body) => {
|
|
const cls = await Class.findByIdAndUpdate(id, body, { new: true, runValidators: true })
|
|
.populate({ path: 'course', select: 'title' })
|
|
.lean();
|
|
if (!cls) throw new AppError('CLASS_NOT_FOUND');
|
|
return cls;
|
|
};
|
|
|
|
const remove = async (id) => {
|
|
const cls = await Class.findByIdAndDelete(id);
|
|
if (!cls) throw new AppError('CLASS_NOT_FOUND');
|
|
};
|
|
|
|
const registerUsers = async (classId, userIds) => {
|
|
const cls = await Class.findById(classId).populate({ path: 'course', select: 'title' });
|
|
if (!cls) throw new AppError('CLASS_NOT_FOUND');
|
|
|
|
const toAdd = (userIds || []).filter(
|
|
(id) => !cls.students.map((s) => s.toString()).includes(id.toString())
|
|
);
|
|
if (toAdd.length === 0) {
|
|
return getOne(classId);
|
|
}
|
|
|
|
cls.students.push(...toAdd);
|
|
await cls.save();
|
|
|
|
const classLabel = cls.name || cls.course?.title || 'کلاس';
|
|
const users = await User.find({ _id: { $in: toAdd } }).select('phoneNumber').lean();
|
|
await Promise.all(
|
|
users.map(async (user) => {
|
|
if (!user.phoneNumber) return;
|
|
try {
|
|
await sendClassRegisteredSms(user.phoneNumber, classLabel, user._id);
|
|
} catch (err) {
|
|
logger.error(`[registerUsers] SMS failed for ${user.phoneNumber}: ${err.message}`);
|
|
}
|
|
})
|
|
);
|
|
|
|
return getOne(classId);
|
|
};
|
|
|
|
const getMyClasses = async (userId, query = {}) => {
|
|
const page = parseInt(query.page) || 1;
|
|
const limit = Math.min(parseInt(query.limit) || 20, 200);
|
|
const skip = (page - 1) * limit;
|
|
|
|
const filter = { students: userId };
|
|
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
|
|
|
|
const [items, total] = await Promise.all([
|
|
Class.find(filter)
|
|
.populate({ path: 'course', select: 'title type price description' })
|
|
.populate({ path: 'professor', select: 'name surname' })
|
|
.skip(skip).limit(limit).sort({ startDate: -1, createdAt: -1 }).lean(),
|
|
Class.countDocuments(filter)
|
|
]);
|
|
|
|
return { data: items, meta: calculateMeta(total, page, limit) };
|
|
};
|
|
|
|
module.exports = { getAll, getOne, create, update, remove, registerUsers, getMyClasses };
|