Spaces:
Running
Running
| const Notification = require('../models/notificationModel'); | |
| const Newsletter = require('../models/newsletterModel'); | |
| const { getIO } = require('../utils/socket'); | |
| // Helper to emit and get unread count | |
| const getAndEmitUnreadCount = async (userId) => { | |
| try { | |
| const unreadCount = await Notification.countDocuments({ | |
| user: userId, | |
| isRead: false, | |
| }); | |
| getIO().to(userId.toString()).emit('unreadCountUpdate', unreadCount); | |
| return unreadCount; | |
| } catch (err) { | |
| console.error('Error emitting unread count:', err); | |
| return 0; | |
| } | |
| }; | |
| // Get all notifications for the authenticated user | |
| exports.getMyNotifications = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| status: 'fail', | |
| message: 'You must be logged in to view notifications', | |
| }); | |
| } | |
| const page = parseInt(req.query.page, 10) || 1; | |
| const limit = parseInt(req.query.limit, 10) || 20; | |
| const skip = (page - 1) * limit; | |
| // Filter options | |
| const filter = { user: req.user._id }; | |
| if (req.query.isRead !== undefined) { | |
| filter.isRead = req.query.isRead === 'true'; | |
| } | |
| if (req.query.type) { | |
| filter.type = req.query.type; | |
| } | |
| const notifications = await Notification.find(filter) | |
| .sort({ createdAt: -1 }) | |
| .skip(skip) | |
| .limit(limit) | |
| .populate('relatedId'); | |
| const total = await Notification.countDocuments(filter); | |
| const unreadCount = await Notification.countDocuments({ | |
| user: req.user._id, | |
| isRead: false, | |
| }); | |
| res.status(200).json({ | |
| status: 'success', | |
| results: notifications.length, | |
| data: { | |
| notifications, | |
| pagination: { | |
| page, | |
| limit, | |
| total, | |
| pages: Math.ceil(total / limit), | |
| }, | |
| unreadCount, | |
| }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Get unread notification count | |
| exports.getUnreadCount = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| status: 'fail', | |
| message: 'You must be logged in', | |
| }); | |
| } | |
| const count = await Notification.countDocuments({ | |
| user: req.user._id, | |
| isRead: false, | |
| }); | |
| res.status(200).json({ | |
| status: 'success', | |
| data: { unreadCount: count }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Mark a notification as read | |
| exports.markAsRead = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| status: 'fail', | |
| message: 'You must be logged in', | |
| }); | |
| } | |
| const notification = await Notification.findOneAndUpdate( | |
| { _id: req.params.id, user: req.user._id }, | |
| { isRead: true, readAt: new Date() }, | |
| { new: true }, | |
| ); | |
| if (!notification) { | |
| return res.status(404).json({ | |
| status: 'fail', | |
| message: 'Notification not found', | |
| }); | |
| } | |
| const unreadCount = await getAndEmitUnreadCount(req.user._id); | |
| // Sync admin dashboard: Notify admins that this notification was read | |
| getIO().emit('notificationReadAdmin', { | |
| notificationId: notification._id, | |
| userId: req.user._id, | |
| isRead: true, | |
| }); | |
| res.status(200).json({ | |
| status: 'success', | |
| data: { notification, unreadCount }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Mark all notifications as read | |
| exports.markAllAsRead = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| status: 'fail', | |
| message: 'You must be logged in', | |
| }); | |
| } | |
| await Notification.updateMany( | |
| { user: req.user._id, isRead: false }, | |
| { isRead: true, readAt: new Date() }, | |
| ); | |
| await getAndEmitUnreadCount(req.user._id); | |
| // Sync admin dashboard: Notify admins that ALL user notifications were read | |
| getIO().emit('notificationAllReadAdmin', { | |
| userId: req.user._id, | |
| }); | |
| res.status(200).json({ | |
| status: 'success', | |
| message: 'All notifications marked as read', | |
| data: { unreadCount: 0 }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Delete a notification | |
| exports.deleteNotification = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| status: 'fail', | |
| message: 'You must be logged in', | |
| }); | |
| } | |
| const notification = await Notification.findOneAndDelete({ | |
| _id: req.params.id, | |
| user: req.user._id, | |
| }); | |
| if (!notification) { | |
| return res.status(404).json({ | |
| status: 'fail', | |
| message: 'Notification not found', | |
| }); | |
| } | |
| const unreadCount = await getAndEmitUnreadCount(req.user._id); | |
| // Sync admin dashboard: Notify admins that this notification was deleted | |
| getIO().emit('notificationDeletedAdmin', notification._id); | |
| res.status(200).json({ | |
| status: 'success', | |
| data: { unreadCount }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Delete all read notifications | |
| exports.deleteAllRead = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| status: 'fail', | |
| message: 'You must be logged in', | |
| }); | |
| } | |
| await Notification.deleteMany({ | |
| user: req.user._id, | |
| isRead: true, | |
| }); | |
| // Sync admin dashboard: Notify admins that ALL user read notifications were deleted | |
| getIO().emit('notificationAllDeletedReadAdmin', { | |
| userId: req.user._id, | |
| }); | |
| res.status(200).json({ | |
| status: 'success', | |
| message: 'All read notifications deleted', | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Admin: Get all notifications | |
| exports.getAllNotifications = async (req, res) => { | |
| try { | |
| const page = parseInt(req.query.page, 10) || 1; | |
| const limit = parseInt(req.query.limit, 10) || 50; | |
| const skip = (page - 1) * limit; | |
| const notifications = await Notification.find() | |
| .populate('user', 'name email') | |
| .sort({ createdAt: -1 }) | |
| .skip(skip) | |
| .limit(limit); | |
| const total = await Notification.countDocuments(); | |
| res.status(200).json({ | |
| status: 'success', | |
| results: notifications.length, | |
| data: { | |
| notifications, | |
| pagination: { | |
| page, | |
| limit, | |
| total, | |
| pages: Math.ceil(total / limit), | |
| }, | |
| }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Admin: Create notification for specific user(s) | |
| exports.createNotification = async (req, res) => { | |
| try { | |
| const { | |
| userId, | |
| title, | |
| message, | |
| type, | |
| priority, | |
| metadata, | |
| relatedId, | |
| relatedModel, | |
| } = req.body; | |
| if (!userId || !title || !message) { | |
| return res.status(400).json({ | |
| status: 'fail', | |
| message: 'Please provide userId, title, and message', | |
| }); | |
| } | |
| const { | |
| createNotification: createNotifService, | |
| } = require('../utils/notificationService'); | |
| const notification = await createNotifService({ | |
| userId, | |
| title, | |
| message, | |
| type: type || 'general', | |
| priority: priority || 'medium', | |
| metadata, | |
| relatedId: relatedId || undefined, | |
| relatedModel: relatedModel || undefined, | |
| }); | |
| if (!notification) { | |
| return res.status(200).json({ | |
| status: 'success', | |
| message: 'Notification skipped due to user preferences or error', | |
| data: null, | |
| }); | |
| } | |
| res.status(201).json({ | |
| status: 'success', | |
| data: { notification }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Admin: Broadcast notification to all users | |
| exports.broadcastNotification = async (req, res) => { | |
| try { | |
| const { | |
| title, | |
| message, | |
| type, | |
| priority, | |
| metadata, | |
| userRole, | |
| relatedId, | |
| relatedModel, | |
| } = req.body; | |
| if (!title || !message) { | |
| return res.status(400).json({ | |
| status: 'fail', | |
| message: 'Please provide title and message', | |
| }); | |
| } | |
| const User = require('../models/userModel'); | |
| let users; | |
| if (userRole === 'subscriber') { | |
| // Find all active newsletter emails | |
| const subscribers = await Newsletter.find({ isActive: true }).select( | |
| 'email', | |
| ); | |
| const emails = subscribers.map((s) => s.email); | |
| // Find users whose email is in the subscribers list | |
| users = await User.find({ email: { $in: emails } }).select('_id'); | |
| } else { | |
| // Filter users by role if specified | |
| const filter = userRole && userRole !== 'all' ? { role: userRole } : {}; | |
| users = await User.find(filter).select('_id'); | |
| } | |
| const recipients = users.map((u) => u._id); | |
| const { createBulkNotifications } = require('../utils/notificationService'); | |
| const results = await createBulkNotifications(recipients, { | |
| title, | |
| message, | |
| type: type || 'general', | |
| priority: priority || 'medium', | |
| metadata, | |
| relatedId: relatedId || undefined, | |
| relatedModel: relatedModel || undefined, | |
| }); | |
| res.status(201).json({ | |
| status: 'success', | |
| message: `Notification sent to ${results.length} recipients who opted in`, | |
| data: { count: results.length }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Admin: Delete any notification (no user ownership check) | |
| exports.adminDeleteNotification = async (req, res) => { | |
| try { | |
| const notification = await Notification.findByIdAndDelete(req.params.id); | |
| if (!notification) { | |
| return res.status(404).json({ | |
| status: 'fail', | |
| message: 'Notification not found', | |
| }); | |
| } | |
| const io = getIO(); | |
| // Notify the specific user that their notification was deleted | |
| io.to(notification.user.toString()).emit( | |
| 'notificationDeleted', | |
| notification._id, | |
| ); | |
| // Update the user's unread count after deletion | |
| await getAndEmitUnreadCount(notification.user); | |
| // Sync other admins | |
| io.emit('notificationDeletedAdmin', notification._id); | |
| res.status(200).json({ | |
| status: 'success', | |
| message: 'Notification deleted successfully', | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Get user's notification preferences | |
| exports.getMyPreferences = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| status: 'fail', | |
| message: 'You must be logged in', | |
| }); | |
| } | |
| res.status(200).json({ | |
| status: 'success', | |
| data: { | |
| preferences: req.user.notificationPreferences, | |
| }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Update user's notification preferences | |
| exports.updateMyPreferences = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| status: 'fail', | |
| message: 'You must be logged in', | |
| }); | |
| } | |
| const { preferences } = req.body; | |
| if (!preferences) { | |
| return res.status(400).json({ | |
| status: 'fail', | |
| message: 'Please provide preferences to update', | |
| }); | |
| } | |
| // Update user document | |
| const User = require('../models/userModel'); | |
| const existingUser = await User.findById(req.user._id); | |
| // Guard: Force-enable notifications only for categories the user has permission for. | |
| // Admins always receive everything. Employees only receive what matches their permissions. | |
| if (req.user.role === 'admin') { | |
| if (preferences.orderUpdates) { | |
| preferences.orderUpdates.app = true; | |
| preferences.orderUpdates.email = true; | |
| } | |
| if (preferences.productUpdates) { | |
| preferences.productUpdates.app = true; | |
| preferences.productUpdates.email = true; | |
| } | |
| delete preferences.vendorOrderVisibility; | |
| } else if (req.user.role === 'employee') { | |
| const empPerms = req.user.permissions || []; | |
| // Only force-enable orderUpdates if the employee has manage_orders permission | |
| if (preferences.orderUpdates && empPerms.includes('manage_orders')) { | |
| preferences.orderUpdates.app = true; | |
| preferences.orderUpdates.email = true; | |
| } | |
| // Only force-enable productUpdates if the employee has manage_products permission | |
| if (preferences.productUpdates && empPerms.includes('manage_products')) { | |
| preferences.productUpdates.app = true; | |
| preferences.productUpdates.email = true; | |
| } | |
| delete preferences.vendorOrderVisibility; | |
| } | |
| // Logic to handle disabledAt and blackoutPeriods for vendorOrderVisibility | |
| if (preferences.vendorOrderVisibility) { | |
| const oldPref = | |
| (existingUser.notificationPreferences && | |
| existingUser.notificationPreferences.vendorOrderVisibility) || | |
| {}; | |
| const wasEnabled = oldPref.app !== false; | |
| const isEnabled = preferences.vendorOrderVisibility.app !== false; | |
| const periods = oldPref.blackoutPeriods || []; | |
| if (wasEnabled && !isEnabled) { | |
| // Just turned off: set disabledAt (start of new blackout) | |
| preferences.vendorOrderVisibility.disabledAt = new Date(); | |
| preferences.vendorOrderVisibility.blackoutPeriods = periods; | |
| } else if (!wasEnabled && isEnabled) { | |
| // Just turned on: close the current blackout period and clear disabledAt | |
| if (oldPref.disabledAt) { | |
| periods.push({ | |
| start: oldPref.disabledAt, | |
| end: new Date(), | |
| }); | |
| } | |
| preferences.vendorOrderVisibility.blackoutPeriods = periods; | |
| preferences.vendorOrderVisibility.disabledAt = null; | |
| } else if (!wasEnabled && !isEnabled) { | |
| // Stayed off: carry over state | |
| preferences.vendorOrderVisibility.disabledAt = oldPref.disabledAt; | |
| preferences.vendorOrderVisibility.blackoutPeriods = periods; | |
| } else { | |
| // Stayed on: carry over state | |
| preferences.vendorOrderVisibility.disabledAt = null; | |
| preferences.vendorOrderVisibility.blackoutPeriods = periods; | |
| } | |
| } | |
| const user = await User.findByIdAndUpdate( | |
| req.user._id, | |
| { notificationPreferences: preferences }, | |
| { new: true, runValidators: true }, | |
| ); | |
| res.status(200).json({ | |
| status: 'success', | |
| data: { | |
| preferences: user.notificationPreferences, | |
| }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| status: 'error', | |
| message: err.message, | |
| }); | |
| } | |
| }; | |
| // Admin: Get current vendor preference state (reads first active vendor as representative) | |
| exports.getVendorGlobalPreferences = async (req, res) => { | |
| try { | |
| const User = require('../models/userModel'); | |
| const vendor = await User.findOne({ role: 'vendor', isActive: true }) | |
| .select('notificationPreferences') | |
| .lean(); | |
| const defaults = { | |
| orderUpdates: { app: true, email: true }, | |
| productUpdates: { app: true, email: true }, | |
| vendorOrderVisibility: { app: true, email: true }, | |
| }; | |
| if (!vendor || !vendor.notificationPreferences) { | |
| return res | |
| .status(200) | |
| .json({ status: 'success', data: { preferences: defaults } }); | |
| } | |
| const p = vendor.notificationPreferences; | |
| return res.status(200).json({ | |
| status: 'success', | |
| data: { | |
| preferences: { | |
| orderUpdates: { | |
| app: p.orderUpdates?.app !== false, | |
| email: p.orderUpdates?.email !== false, | |
| }, | |
| productUpdates: { | |
| app: p.productUpdates?.app !== false, | |
| email: p.productUpdates?.email !== false, | |
| }, | |
| vendorOrderVisibility: { | |
| app: p.vendorOrderVisibility?.app !== false, | |
| email: p.vendorOrderVisibility?.email !== false, | |
| }, | |
| }, | |
| }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ status: 'error', message: err.message }); | |
| } | |
| }; | |
| // Admin: Bulk-update preferences for ALL active vendor users | |
| // This lets admins globally control whether vendors receive notifications / see orders | |
| exports.updateAllVendorPreferences = async (req, res) => { | |
| try { | |
| if (!req.user) { | |
| return res | |
| .status(401) | |
| .json({ status: 'fail', message: 'You must be logged in' }); | |
| } | |
| const { preferences } = req.body; | |
| if (!preferences) { | |
| return res | |
| .status(400) | |
| .json({ status: 'fail', message: 'Please provide preferences' }); | |
| } | |
| const User = require('../models/userModel'); | |
| const now = new Date(); | |
| // Find all active vendor users | |
| const vendors = await User.find({ role: 'vendor', isActive: true }).select( | |
| '_id notificationPreferences', | |
| ); | |
| if (vendors.length === 0) { | |
| return res.status(200).json({ | |
| status: 'success', | |
| message: 'No active vendors found', | |
| data: { updatedCount: 0 }, | |
| }); | |
| } | |
| const bulkOps = vendors.map((vendor) => { | |
| const existingPrefs = vendor.notificationPreferences || {}; | |
| const updatedPrefs = { ...existingPrefs }; | |
| // Apply orderUpdates preference β app and email are always kept in sync | |
| if (preferences.orderUpdates !== undefined) { | |
| const val = | |
| typeof preferences.orderUpdates.app === 'boolean' | |
| ? preferences.orderUpdates.app | |
| : true; | |
| updatedPrefs.orderUpdates = { | |
| ...(existingPrefs.orderUpdates || {}), | |
| app: val, | |
| email: val, | |
| }; | |
| } | |
| // Apply productUpdates preference β app and email are always kept in sync | |
| if (preferences.productUpdates !== undefined) { | |
| const val = | |
| typeof preferences.productUpdates.app === 'boolean' | |
| ? preferences.productUpdates.app | |
| : true; | |
| updatedPrefs.productUpdates = { | |
| ...(existingPrefs.productUpdates || {}), | |
| app: val, | |
| email: val, | |
| }; | |
| } | |
| // Apply vendorOrderVisibility preference with blackout tracking | |
| if (preferences.vendorOrderVisibility !== undefined) { | |
| const oldVis = existingPrefs.vendorOrderVisibility || {}; | |
| const wasEnabled = oldVis.app !== false; | |
| const isEnabled = preferences.vendorOrderVisibility.app !== false; | |
| const periods = oldVis.blackoutPeriods || []; | |
| let disabledAt = oldVis.disabledAt || null; | |
| if (wasEnabled && !isEnabled) { | |
| // Turning OFF: record when it was disabled | |
| disabledAt = now; | |
| } else if (!wasEnabled && isEnabled) { | |
| // Turning ON: close the current blackout period | |
| if (oldVis.disabledAt) { | |
| periods.push({ start: oldVis.disabledAt, end: now }); | |
| } | |
| disabledAt = null; | |
| } else if (!wasEnabled && !isEnabled) { | |
| // Stayed off: keep previous disabledAt | |
| } else { | |
| // Stayed on | |
| disabledAt = null; | |
| } | |
| // vendorOrderVisibility: app and email always in sync | |
| updatedPrefs.vendorOrderVisibility = { | |
| app: isEnabled, | |
| email: isEnabled, | |
| disabledAt, | |
| blackoutPeriods: periods, | |
| }; | |
| } | |
| return { | |
| updateOne: { | |
| filter: { _id: vendor._id }, | |
| update: { $set: { notificationPreferences: updatedPrefs } }, | |
| }, | |
| }; | |
| }); | |
| const result = await User.bulkWrite(bulkOps); | |
| // Emit a socket event so online vendors refresh their preferences | |
| const { getIO } = require('../utils/socket'); | |
| try { | |
| const io = getIO(); | |
| vendors.forEach((vendor) => { | |
| io.to(vendor._id.toString()).emit('preferencesUpdated', { | |
| preferences: bulkOps.find( | |
| (op) => | |
| op.updateOne.filter._id.toString() === vendor._id.toString(), | |
| )?.updateOne?.update?.$set?.notificationPreferences, | |
| }); | |
| }); | |
| } catch (socketErr) { | |
| // Non-critical β socket might not be available | |
| console.error( | |
| 'Socket emit error in updateAllVendorPreferences:', | |
| socketErr, | |
| ); | |
| } | |
| res.status(200).json({ | |
| status: 'success', | |
| message: `Updated preferences for ${result.modifiedCount} vendor(s)`, | |
| data: { updatedCount: result.modifiedCount }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ status: 'error', message: err.message }); | |
| } | |
| }; | |
| // βββ Stock Subscription Handlers βββββββββββββββββββββββββββββββββββββββββββββ | |
| const StockSubscription = require('../models/stockSubscriptionModel'); | |
| // POST /notifications/stock-subscribe/:productId | |
| exports.subscribeToStock = async (req, res) => { | |
| try { | |
| const { productId } = req.params; | |
| const userId = req.user._id; | |
| // Upsert: create if not exists, reset notifiedAt so they get notified again on next restock | |
| await StockSubscription.findOneAndUpdate( | |
| { user: userId, product: productId }, | |
| { user: userId, product: productId, notifiedAt: null }, | |
| { upsert: true, new: true, setDefaultsOnInsert: true }, | |
| ); | |
| res.status(200).json({ | |
| status: 'success', | |
| data: { subscribed: true }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ status: 'error', message: err.message }); | |
| } | |
| }; | |
| // DELETE /notifications/stock-subscribe/:productId | |
| exports.unsubscribeFromStock = async (req, res) => { | |
| try { | |
| const { productId } = req.params; | |
| const userId = req.user._id; | |
| await StockSubscription.findOneAndDelete({ | |
| user: userId, | |
| product: productId, | |
| }); | |
| res.status(200).json({ | |
| status: 'success', | |
| data: { subscribed: false }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ status: 'error', message: err.message }); | |
| } | |
| }; | |
| // GET /notifications/stock-subscribe/:productId | |
| exports.checkStockSubscription = async (req, res) => { | |
| try { | |
| const { productId } = req.params; | |
| const userId = req.user._id; | |
| const sub = await StockSubscription.findOne({ | |
| user: userId, | |
| product: productId, | |
| }); | |
| res.status(200).json({ | |
| status: 'success', | |
| data: { subscribed: !!sub }, | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ status: 'error', message: err.message }); | |
| } | |
| }; | |