diff --git a/components/classes/classController.js b/components/classes/classController.js index 55786a9..0a56bd0 100644 --- a/components/classes/classController.js +++ b/components/classes/classController.js @@ -44,3 +44,8 @@ exports.getMyClasses = catchAsync(async (req, res) => { const { data, meta } = await classService.getMyClasses(req.user._id, req.query); return listResponse(res, 200, data, meta); }); + +exports.getPublicClasses = catchAsync(async (req, res) => { + const { data, meta } = await classService.getPublicClasses(req.query); + return listResponse(res, 200, data, meta); +}); diff --git a/components/classes/classModel.js b/components/classes/classModel.js index 2c406be..36493fb 100644 --- a/components/classes/classModel.js +++ b/components/classes/classModel.js @@ -30,6 +30,25 @@ const classSchema = new mongoose.Schema({ type: Number, default: 0 }, + hasDiscount: { + type: Boolean, + default: false + }, + discount: { + type: Number, + default: 0, + min: 0 + }, + freeSpots: { + type: Number, + min: 0, + default: null + }, + showOnFrontend: { + type: Boolean, + default: true, + index: true + }, startDate: { type: Date }, @@ -63,7 +82,15 @@ const classSchema = new mongoose.Schema({ default: [] } }, { - timestamps: true + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}); + +classSchema.virtual('finalTuitionFee').get(function () { + const tuition = this.tuitionFee || 0; + if (!this.hasDiscount) return tuition; + return Math.max(0, tuition - (this.discount || 0)); }); module.exports = mongoose.model('Class', classSchema); diff --git a/components/classes/classRoutes.js b/components/classes/classRoutes.js index dd1f758..7d7b617 100644 --- a/components/classes/classRoutes.js +++ b/components/classes/classRoutes.js @@ -9,6 +9,9 @@ const { PERMISSIONS } = require('../../constants/permissions'); const router = express.Router(); +// Public Scope +router.get('/user/get-all', classController.getPublicClasses); + router.use(authMiddleware); // User Scope diff --git a/components/classes/classService.js b/components/classes/classService.js index cd92177..7b04b24 100644 --- a/components/classes/classService.js +++ b/components/classes/classService.js @@ -18,6 +18,28 @@ const applyScheduleFields = (payload, body) => { 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 normalizePricingFields = (body = {}) => { + const tuitionFee = Math.max(0, Number(body.tuitionFee) || 0); + const hasDiscount = Boolean(body.hasDiscount); + const discount = hasDiscount + ? Math.min(Math.max(0, Number(body.discount) || 0), tuitionFee) + : 0; + return { tuitionFee, hasDiscount, discount }; +}; + +const enrichClassForDisplay = (cls) => { + const tuitionFee = cls.tuitionFee || 0; + const discount = cls.hasDiscount ? (cls.discount || 0) : 0; + const finalTuitionFee = Math.max(0, tuitionFee - discount); + let daysUntilStart = null; + if (cls.startDate) { + daysUntilStart = Math.ceil((new Date(cls.startDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)); + } + return { ...cls, finalTuitionFee, daysUntilStart }; +}; + const getAll = async (query) => { const page = parseInt(query.page) || 1; const limit = Math.min(parseInt(query.limit) || 20, 200); @@ -34,14 +56,14 @@ const getAll = async (query) => { const [items, total] = await Promise.all([ Class.find(filter) - .select('name course professor students capacity tuitionFee startDate endDate days startTime endTime isActive adminNotes createdAt updatedAt') + .select(CLASS_LIST_FIELDS) .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) }; + return { data: items.map(enrichClassForDisplay), meta: calculateMeta(total, page, limit) }; }; const getOne = async (id) => { @@ -51,22 +73,32 @@ const getOne = async (id) => { .populate({ path: 'students', select: 'name phoneNumber gender' }) .lean(); if (!cls) throw new AppError('CLASS_NOT_FOUND'); - return cls; + return enrichClassForDisplay(cls); }; const create = async (body) => { - const payload = applyScheduleFields({ ...body }, body); + const payload = applyScheduleFields({ ...body, ...normalizePricingFields(body) }, body); + if (body.freeSpots === '' || body.freeSpots === null || body.freeSpots === undefined) { + payload.freeSpots = null; + } else { + payload.freeSpots = Math.max(0, Number(body.freeSpots) || 0); + } const cls = await Class.create(payload); return getOne(cls._id); }; const update = async (id, body) => { - const payload = applyScheduleFields({ ...body }, body); + const payload = applyScheduleFields({ ...body, ...normalizePricingFields(body) }, body); + if (body.freeSpots === '' || body.freeSpots === null) { + payload.freeSpots = null; + } else if (body.freeSpots !== undefined) { + payload.freeSpots = Math.max(0, Number(body.freeSpots) || 0); + } const cls = await Class.findByIdAndUpdate(id, payload, { new: true, runValidators: true }) .populate({ path: 'course', select: 'title' }) .lean(); if (!cls) throw new AppError('CLASS_NOT_FOUND'); - return cls; + return enrichClassForDisplay(cls); }; const remove = async (id) => { @@ -144,7 +176,30 @@ const getMyClasses = async (userId, query = {}) => { Class.countDocuments(filter) ]); - return { data: items, meta: calculateMeta(total, page, limit) }; + return { data: items.map(enrichClassForDisplay), meta: calculateMeta(total, page, limit) }; }; -module.exports = { getAll, getOne, create, update, remove, registerUsers, removeUser, getMyClasses }; +const getPublicClasses = async (query = {}) => { + const page = parseInt(query.page) || 1; + const limit = Math.min(parseInt(query.limit) || 50, 200); + const skip = (page - 1) * limit; + + const filter = { + isActive: { $ne: false }, + showOnFrontend: { $ne: false } + }; + if (query.courseId) filter.course = query.courseId; + + const [items, total] = await Promise.all([ + Class.find(filter) + .select('name course professor capacity tuitionFee hasDiscount discount freeSpots startDate endDate days startTime endTime') + .populate({ path: 'course', select: 'title type description' }) + .populate({ path: 'professor', select: 'name surname' }) + .skip(skip).limit(limit).sort({ startDate: 1, createdAt: -1 }).lean(), + Class.countDocuments(filter) + ]); + + return { data: items.map(enrichClassForDisplay), meta: calculateMeta(total, page, limit) }; +}; + +module.exports = { getAll, getOne, create, update, remove, registerUsers, removeUser, getMyClasses, getPublicClasses }; diff --git a/components/notifications/notificationService.js b/components/notifications/notificationService.js index f3198bf..893938f 100644 --- a/components/notifications/notificationService.js +++ b/components/notifications/notificationService.js @@ -49,7 +49,7 @@ const getAllNotifications = async (queryParams) => { const filter = buildFilterQuery(queryParams, ['subject', 'body']); const [notifications, totalCount] = await Promise.all([ - Notification.find(filter).populate('user', 'name username').sort(sort).skip(skip).limit(limit), + Notification.find(filter).populate('user', 'name username phoneNumber').sort(sort).skip(skip).limit(limit), Notification.countDocuments(filter) ]);