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 }); } };