// graphql/resolvers.js const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const { GraphQLError } = require('graphql'); const mongoose = require('mongoose'); // استيراد الموديلات (من الكود الموجود) const { User, Comment, ProfileAnalytics, Coupon, Rating, Report, Post, Story, Job, JobApplication, JobSave, JobCategory, Product, Order, Page, Cart, StoreFollow, Follow, Notification, Conversation, Project, MessageReport, Message, SiteSettings, AIConversation, SubscriptionEarnings, UserSubscription, PlatformPaymentMethod, SubscriptionPlan, StoreSettings, StoreTheme, AlsoViewed } = require('../server'); // دوال مساعدة const ShortcodeService = require('../services/ShortcodeService'); const { sendEmailWithBrevo, sendStoreEmailWithBrevo } = require('../server'); const TemplateStore = require('../models/TemplateStore'); // ============================================ // 🔹 HELPER FUNCTIONS // ============================================ function detectLanguage(text) { const arabicPattern = /[\u0600-\u06FF]/; if (arabicPattern.test(text)) return 'ar'; const englishPattern = /[a-zA-Z]/; if (englishPattern.test(text)) return 'en'; return 'ar'; } async function saveAIConversation(userId, question, answer, provider, language, responseTime, visitorIp) { try { await AIConversation.create({ userId, visitorIp: visitorIp || '', question: question.substring(0, 500), answer: answer.substring(0, 1000), provider, language, responseTime: responseTime || 0 }); } catch (error) { console.error('Error saving AI conversation:', error); } } async function updateUserPermissions(userId, plan) { const user = await User.findById(userId); if (!user) return; if (!plan) { user.profile.storeEnabled = false; } else { user.profile.storeEnabled = plan.features?.storeEnabled || false; } await user.save(); } // ============================================ // 🔹 RESOLVERS OBJECT // ============================================ const resolvers = { // ============================================ // 🔹 SCALAR RESOLVERS // ============================================ Date: { serialize: (value) => value ? new Date(value).toISOString() : null, parseValue: (value) => value ? new Date(value) : null, parseLiteral: (ast) => ast.value ? new Date(ast.value) : null }, JSON: { serialize: (value) => value, parseValue: (value) => value, parseLiteral: (ast) => ast.value }, Upload: { serialize: () => null, parseValue: () => null, parseLiteral: () => null }, // ============================================ // 🔹 USER RESOLVERS - ✅ مع تحويل Object إلى Array // ============================================ User: { profile: (parent) => { const p = parent.profile || {}; // ✅ تحويل جميع الحقول من Object إلى Array return { ...p, education: Array.isArray(p.education) ? p.education : Object.values(p.education || {}), experience: Array.isArray(p.experience) ? p.experience : Object.values(p.experience || {}), certificates: Array.isArray(p.certificates) ? p.certificates : Object.values(p.certificates || {}), skills: Array.isArray(p.skills) ? p.skills : Object.values(p.skills || {}), projects: Array.isArray(p.projects) ? p.projects : Object.values(p.projects || {}), interests: Array.isArray(p.interests) ? p.interests : Object.values(p.interests || {}), customSections: Array.isArray(p.customSections) ? p.customSections : Object.values(p.customSections || {}), sectionOrder: Array.isArray(p.sectionOrder) ? p.sectionOrder : Object.values(p.sectionOrder || {}), }; }, followersCount: async (parent, args, context) => { if (context.loaders?.followersCountLoader) { return await context.loaders.followersCountLoader.load(parent._id.toString()); } try { const count = await Follow.countDocuments({ followingId: parent._id, status: 'accepted' }); return count || 0; } catch (error) { return 0; } }, followingCount: async (parent, args, context) => { if (context.loaders?.followingCountLoader) { return await context.loaders.followingCountLoader.load(parent._id.toString()); } try { const count = await Follow.countDocuments({ followerId: parent._id, status: 'accepted' }); return count || 0; } catch (error) { return 0; } }, postsCount: async (parent, args, context) => { if (context.loaders?.postsCountLoader) { return await context.loaders.postsCountLoader.load(parent._id.toString()); } try { const count = await Post.countDocuments({ userId: parent._id }); return count || 0; } catch (error) { return 0; } }, projectsCount: async (parent, args, context) => { try { const count = await Project.countDocuments({ userId: parent._id }); return count || 0; } catch (error) { return 0; } }, jobsCount: async (parent, args, context) => { try { const count = await Job.countDocuments({ employerId: parent._id }); return count || 0; } catch (error) { return 0; } }, totalLikes: async (parent, args, context) => { try { const result = await Post.aggregate([ { $match: { userId: parent._id } }, { $project: { likesCount: { $size: '$likes' } } }, { $group: { _id: null, total: { $sum: '$likesCount' } } } ]); return result[0]?.total || 0; } catch (error) { return 0; } }, store: async (parent, args, context) => { try { const storeSettings = await StoreSettings.findOne({ userId: parent._id }); if (!storeSettings) return null; const productsCount = await Product.countDocuments({ userId: parent._id, isActive: true }); const followersCount = await StoreFollow.countDocuments({ storeId: parent._id }); return { _id: parent._id, userId: parent._id, storeName: storeSettings.storeName || parent.profile?.nickname || parent.username, storeLogo: storeSettings.storeLogo, storeBanner: storeSettings.storeBanner, storeDescription: storeSettings.storeDescription, contactEmail: storeSettings.contactEmail || parent.email, enabled: storeSettings.enabled || false, currency: storeSettings.currency || 'USD', currencySymbol: storeSettings.currencySymbol || '$', productsCount: productsCount, followersCount: followersCount, averageRating: storeSettings.averageRating || 0, totalSales: 0, totalOrders: 0, createdAt: storeSettings.createdAt, updatedAt: storeSettings.updatedAt }; } catch (error) { return null; } }, subscription: async (parent, args, context) => { try { const subscription = await UserSubscription.findOne({ userId: parent._id, status: 'active' }).populate('planId'); return subscription || null; } catch (error) { return null; } }, permissions: async (parent, args, context) => { try { const storeSettings = await StoreSettings.findOne({ userId: parent._id }); const subscription = await UserSubscription.findOne({ userId: parent._id, status: 'active' }).populate('planId'); return { canCreateStore: !!(storeSettings?.enabled), canCreateProducts: !!(subscription?.planId?.features?.storeEnabled) }; } catch (error) { return { canCreateStore: false, canCreateProducts: false }; } }, hasStory: async (parent) => { try { const now = new Date(); const storyCount = await Story.countDocuments({ userId: parent._id, expiresAt: { $gt: now } }); return storyCount > 0; } catch (error) { console.error('hasStory error:', error); return false; } } }, // ============================================ // 🔹 COMMENT RESOLVERS - ✅ الحل النهائي // ============================================ Comment: { projectId: async (parent) => { return await Project.findById(parent.projectId); }, projectOwnerId: async (parent) => { return await User.findById(parent.projectOwnerId); }, userId: async (parent) => { return await User.findById(parent.userId); }, likes: async (parent) => { if (!parent || !parent.likes || !Array.isArray(parent.likes) || parent.likes.length === 0) { return []; } return await User.find({ _id: { $in: parent.likes } }); }, parentCommentId: async (parent) => { if (!parent.parentCommentId) return null; return await Comment.findById(parent.parentCommentId); }, replies: async (parent) => { if (!parent || !parent.replies || !Array.isArray(parent.replies) || parent.replies.length === 0) { return []; } return await Comment.find({ _id: { $in: parent.replies } }) .populate('userId', 'username profile.nickname profile.avatar') .sort({ timestamp: 1 }); }, reports: (parent) => parent.reports || [], repliesCount: (parent) => { if (!parent || !parent.replies || !Array.isArray(parent.replies)) return 0; return parent.replies.length; }, likesCount: (parent) => { if (!parent || !parent.likes || !Array.isArray(parent.likes)) return 0; return parent.likes.length; }, isLiked: (parent, args, context) => { if (!context.user) return false; if (!parent || !parent.likes || !Array.isArray(parent.likes)) return false; return parent.likes.some(id => id.toString() === context.user.userId); } }, CommentReport: { userId: async (parent) => { return await User.findById(parent.userId); }, resolvedBy: async (parent) => { if (!parent.resolvedBy) return null; return await User.findById(parent.resolvedBy); } }, // ============================================ // 🔹 PROFILE RESOLVERS - ✅ مع تحويل Object إلى Array // ============================================ Profile: { socialLinks: (parent) => parent.socialLinks || {}, storeEnabled: (parent) => { return parent.storeEnabled || false; }, averageRating: (parent) => { return parent.averageRating || 0; }, totalRatings: (parent) => { return parent.totalRatings || 0; }, // ✅ تحويل Object إلى Array لجميع الحقول التي قد تكون Object education: (parent) => { if (!parent.education) return []; if (Array.isArray(parent.education)) return parent.education; if (typeof parent.education === 'object') return Object.values(parent.education); return []; }, experience: (parent) => { if (!parent.experience) return []; if (Array.isArray(parent.experience)) return parent.experience; if (typeof parent.experience === 'object') return Object.values(parent.experience); return []; }, certificates: (parent) => { if (!parent.certificates) return []; if (Array.isArray(parent.certificates)) return parent.certificates; if (typeof parent.certificates === 'object') return Object.values(parent.certificates); return []; }, skills: (parent) => { if (!parent.skills) return []; if (Array.isArray(parent.skills)) return parent.skills; if (typeof parent.skills === 'object') return Object.values(parent.skills); return []; }, projects: (parent) => { if (!parent.projects) return []; if (Array.isArray(parent.projects)) return parent.projects; if (typeof parent.projects === 'object') return Object.values(parent.projects); return []; }, interests: (parent) => { if (!parent.interests) return []; if (Array.isArray(parent.interests)) return parent.interests; if (typeof parent.interests === 'object') return Object.values(parent.interests); return []; }, customSections: (parent) => { if (!parent.customSections) return []; if (Array.isArray(parent.customSections)) return parent.customSections; if (typeof parent.customSections === 'object') return Object.values(parent.customSections); return []; }, sectionOrder: (parent) => { if (!parent.sectionOrder) return []; if (Array.isArray(parent.sectionOrder)) return parent.sectionOrder; if (typeof parent.sectionOrder === 'object') return Object.values(parent.sectionOrder); return []; }, hasStory: async (parent, args, context) => { // لو الـ Profile جاي من User، استخدم الـ userId const userId = parent._id || parent.userId; if (!userId) return false; try { const count = await Story.countDocuments({ userId: userId, expiresAt: { $gt: new Date() } }); return count > 0; } catch (error) { return false; } }, // ✅ الحقول اللي مفروض تكون Objects sectionNames: (parent) => parent.sectionNames || {}, sectionVisibility: (parent) => parent.sectionVisibility || {}, sectionStyleSettings: (parent) => parent.sectionStyleSettings || {}, designSettings: (parent) => parent.designSettings || {}, aiBot: (parent) => parent.aiBot || { enabled: false, provider: 'mgzon' }, audio: (parent) => parent.audio || {}, theme: (parent) => parent.theme || {}, layout: (parent) => parent.layout || {}, header: (parent) => parent.header || {}, footer: (parent) => parent.footer || {}, seo: (parent) => parent.seo || {}, schema: (parent) => parent.schema || {}, }, Project: { userId: async (parent) => { return await User.findById(parent.userId); } }, // ============================================ // 🔹 POST RESOLVERS - ✅ الحل النهائي // ============================================ Post: { userId: async (parent) => { return await User.findById(parent.userId); }, likesCount: (parent) => { // ✅ التأكد من وجود likes وإرجاع 0 دائماً if (!parent || !parent.likes || !Array.isArray(parent.likes)) { return 0; } return parent.likes.length; }, commentsCount: (parent) => { if (!parent || !parent.comments || !Array.isArray(parent.comments)) { return 0; } return parent.comments.length; }, sharesCount: (parent) => { if (!parent || !parent.shares || !Array.isArray(parent.shares)) { return 0; } return parent.shares.length; }, savesCount: (parent) => { if (!parent || !parent.saves || !Array.isArray(parent.saves)) { return 0; } return parent.saves.length; }, isLiked: (parent, args, context) => { if (!context.user) return false; if (!parent || !parent.likes || !Array.isArray(parent.likes)) return false; return parent.likes.some(l => l.userId?.toString() === context.user.userId); }, isSaved: (parent, args, context) => { if (!context.user) return false; if (!parent || !parent.saves || !Array.isArray(parent.saves)) return false; return parent.saves.some(s => s.userId?.toString() === context.user.userId); } }, // ============================================ // 🔹 STORY RESOLVERS // ============================================ Story: { userId: async (parent) => { return await User.findById(parent.userId); }, viewsCount: (parent) => { if (!parent || !parent.views || !Array.isArray(parent.views)) { return 0; } return parent.views.length; }, reactionsCount: (parent) => { if (!parent || !parent.reactions || !Array.isArray(parent.reactions)) { return 0; } return parent.reactions.length; }, hasViewed: (parent, args, context) => { if (!context.user) return false; if (!parent || !parent.views || !Array.isArray(parent.views)) return false; return parent.views.some(v => v.userId?.toString() === context.user.userId); } }, StoryGroup: { user: (parent) => parent.user, stories: (parent) => parent.stories }, // ============================================ // 🔹 JOB RESOLVERS // ============================================ Job: { employerId: async (parent) => { return await User.findById(parent.employerId); }, hasApplied: async (parent, args, context) => { if (!context.user) return false; const application = await JobApplication.findOne({ jobId: parent._id, applicantId: context.user.userId }); return !!application; }, applicationStatus: async (parent, args, context) => { if (!context.user) return null; const application = await JobApplication.findOne({ jobId: parent._id, applicantId: context.user.userId }); return application?.status || null; }, isSaved: async (parent, args, context) => { if (!context.user) return false; const saved = await JobSave.findOne({ jobId: parent._id, userId: context.user.userId }); return !!saved; } }, JobApplication: { jobId: async (parent) => { return await Job.findById(parent.jobId); }, applicantId: async (parent) => { return await User.findById(parent.applicantId); } }, JobCategory: { displayName: (parent, args, context) => { // استخدام lang من الـ arguments const lang = args?.lang || 'en'; return lang === 'ar' ? (parent.nameAr || parent.name) : parent.name; }, count: async (parent) => { return await Job.countDocuments({ category: parent.name, status: 'active', deadline: { $gt: new Date() } }); } }, // ============================================ // 🔹 STORE RESOLVERS // ============================================ Store: { userId: async (parent) => { return await User.findById(parent.userId || parent._id); }, productsCount: async (parent) => { return await Product.countDocuments({ userId: parent.userId || parent._id, isActive: true }); }, followersCount: async (parent) => { return await StoreFollow.countDocuments({ storeId: parent.userId || parent._id }); }, totalSales: async (parent) => { const orders = await Order.find({ sellerId: parent.userId || parent._id, status: 'completed' }); return orders.reduce((sum, o) => sum + o.totalAmount, 0); }, totalOrders: async (parent) => { return await Order.countDocuments({ sellerId: parent.userId || parent._id }); }, averageRating: async (parent) => { const settings = await StoreSettings.findOne({ userId: parent.userId || parent._id }); return settings?.averageRating || 0; } }, StoreProduct: { userId: async (parent) => { return await User.findById(parent.userId); }, averageRating: (parent) => { if (!parent.reviews || parent.reviews.length === 0) return 0; const sum = parent.reviews.reduce((acc, r) => acc + r.rating, 0); return sum / parent.reviews.length; }, reviews: (parent) => parent.reviews || [] }, ProductReview: { userId: async (parent) => { return await User.findById(parent.userId); } }, Order: { buyerId: async (parent) => { return await User.findById(parent.buyerId); }, sellerId: async (parent) => { return await User.findById(parent.sellerId); } }, Cart: { userId: async (parent) => { return await User.findById(parent.userId); }, stores: (parent) => { if (!parent.stores) return []; const stores = []; for (let [storeId, storeData] of parent.stores) { stores.push({ ...storeData, storeId: storeId }); } return stores; } }, CartStore: { storeId: (parent) => parent.storeId, items: (parent) => parent.items || [] }, // ============================================ // 🔹 PAGE RESOLVERS // ============================================ Page: { storeId: async (parent) => { return await User.findById(parent.storeId); }, lockedBy: async (parent) => { if (!parent.lockedBy) return null; return await User.findById(parent.lockedBy); }, createdBy: async (parent) => { if (!parent.createdBy) return null; return await User.findById(parent.createdBy); }, updatedBy: async (parent) => { if (!parent.updatedBy) return null; return await User.findById(parent.updatedBy); }, url: (parent) => { return `/shop/${parent.storeId}/page/${parent.slug}`; }, isLocked: (parent) => { if (!parent.lockedAt) return false; return (Date.now() - new Date(parent.lockedAt).getTime()) < 30 * 60 * 1000; } }, // ============================================ // 🔹 STORE THEME RESOLVERS // ============================================ StoreTheme: { userId: async (parent) => { return await User.findById(parent.userId); }, isCustomTheme: (parent) => !!parent.uploadedTheme?.fileUrl, themeUrl: (parent) => { return `/api/store/theme/${parent.userId}`; } }, // ============================================ // 🔹 SUBSCRIPTION RESOLVERS // ============================================ UserSubscription: { userId: async (parent) => { return await User.findById(parent.userId); }, planId: async (parent) => { return await SubscriptionPlan.findById(parent.planId); }, approvedBy: async (parent) => { if (!parent.approvedBy) return null; return await User.findById(parent.approvedBy); } }, // ============================================ // 🔹 NOTIFICATION RESOLVERS // ============================================ Notification: { userId: async (parent) => { return await User.findById(parent.userId); }, actorId: async (parent) => { if (!parent.actorId) return null; return await User.findById(parent.actorId); } }, // ============================================ // 🔹 REPORT RESOLVERS // ============================================ Report: { reporterId: async (parent) => { return await User.findById(parent.reporterId); }, resolvedBy: async (parent) => { if (!parent.resolvedBy) return null; return await User.findById(parent.resolvedBy); } }, MessageReport: { messageId: async (parent) => { return await Message.findById(parent.messageId); }, reporterId: async (parent) => { return await User.findById(parent.reporterId); }, reportedUserId: async (parent) => { return await User.findById(parent.reportedUserId); }, conversationId: async (parent) => { if (!parent.conversationId) return null; return await Conversation.findById(parent.conversationId); }, resolvedBy: async (parent) => { if (!parent.resolvedBy) return null; return await User.findById(parent.resolvedBy); } }, Message: { conversationId: async (parent) => { return await Conversation.findById(parent.conversationId); }, senderId: async (parent) => { return await User.findById(parent.senderId); }, readBy: async (parent) => { const users = []; for (const item of parent.readBy || []) { const user = await User.findById(item.userId); if (user) users.push({ userId: user, readAt: item.readAt }); } return users; }, replyTo: async (parent) => { if (!parent.replyTo) return null; return await Message.findById(parent.replyTo); }, originalMessageId: async (parent) => { if (!parent.originalMessageId) return null; return await Message.findById(parent.originalMessageId); }, deletedFor: async (parent) => { return await User.find({ _id: { $in: parent.deletedFor || [] } }); } }, Conversation: { participants: async (parent) => { return await User.find({ _id: { $in: parent.participants } }); }, lastMessage: (parent) => parent.lastMessage || null, unreadCount: (parent, args, context) => { if (!context.user) return 0; return parent.unreadCount?.get(context.user.userId) || 0; } }, MessageRead: { userId: async (parent) => { return await User.findById(parent.userId); } }, // ============================================ // 🔹 FOLLOW RESOLVERS // ============================================ Follow: { followerId: async (parent) => { return await User.findById(parent.followerId); }, followingId: async (parent) => { return await User.findById(parent.followingId); } }, FollowRequest: { requester: async (parent) => { return await User.findById(parent.followerId); }, requestedAt: (parent) => parent.createdAt }, // ============================================ // 🔹 RATING RESOLVERS // ============================================ Rating: { targetUserId: async (parent) => { return await User.findById(parent.targetUserId); }, raterUserId: async (parent) => { return await User.findById(parent.raterUserId); } }, // ============================================ // 🔹 AICONVERSATION RESOLVERS // ============================================ AIConversation: { userId: async (parent) => { return await User.findById(parent.userId); } }, // ============================================ // 🔹 TEMPLATE RESOLVERS // ============================================ Template: { templateUrl: (parent) => { if (parent.themeData?.templateUrl) return parent.themeData.templateUrl; if (parent.themeData?.uploadedFiles?.htmlFiles?.length) { const indexFile = parent.themeData.uploadedFiles.htmlFiles.find(f => f.name === 'index.html' || f.name.endsWith('/index.html') ); return indexFile?.url || null; } return null; }, installedBy: async (parent) => { const users = []; for (const install of parent.installedBy || []) { const user = await User.findById(install.userId); if (user) users.push({ userId: user, installedAt: install.installedAt }); } return users; } }, TemplateInstall: { userId: async (parent) => { return await User.findById(parent.userId); } }, // ============================================ // 🔹 SITE SETTINGS RESOLVERS // ============================================ SiteSettings: { navbarLinks: (parent) => parent.navbarLinks || [] } }; // ============================================ // 🔹 QUERY RESOLVERS // ============================================ const Query = { // ===== Profile ===== me: async (parent, args, context) => { if (!context.user) { return null; } const user = await User.findById(context.user.userId).lean(); // ✅ استخدام lean() if (!user) return null; // ✅ تطبيق التحويلات يدوياً على profile if (user.profile) { const p = user.profile; user.profile.education = Array.isArray(p.education) ? p.education : Object.values(p.education || {}); user.profile.experience = Array.isArray(p.experience) ? p.experience : Object.values(p.experience || {}); user.profile.certificates = Array.isArray(p.certificates) ? p.certificates : Object.values(p.certificates || {}); user.profile.skills = Array.isArray(p.skills) ? p.skills : Object.values(p.skills || {}); user.profile.projects = Array.isArray(p.projects) ? p.projects : Object.values(p.projects || {}); user.profile.interests = Array.isArray(p.interests) ? p.interests : Object.values(p.interests || {}); user.profile.customSections = Array.isArray(p.customSections) ? p.customSections : Object.values(p.customSections || {}); user.profile.sectionOrder = Array.isArray(p.sectionOrder) ? p.sectionOrder : Object.values(p.sectionOrder || {}); user.profile.sectionNames = p.sectionNames || {}; user.profile.sectionVisibility = p.sectionVisibility || {}; user.profile.sectionStyleSettings = p.sectionStyleSettings || {}; user.profile.designSettings = p.designSettings || {}; user.profile.aiBot = p.aiBot || { enabled: false, provider: 'mgzon' }; user.profile.audio = p.audio || {}; user.profile.theme = p.theme || {}; user.profile.layout = p.layout || {}; user.profile.header = p.header || {}; user.profile.footer = p.footer || {}; user.profile.seo = p.seo || {}; user.profile.schema = p.schema || {}; user.profile.socialLinks = p.socialLinks || {}; } // ✅ جلب الحسابات const [followersCount, followingCount, postsCount, projectsCount, jobsCount, storeSettings, subscription, totalLikesResult] = await Promise.all([ Follow.countDocuments({ followingId: user._id, status: 'accepted' }), Follow.countDocuments({ followerId: user._id, status: 'accepted' }), Post.countDocuments({ userId: user._id }), Project.countDocuments({ userId: user._id }), Job.countDocuments({ employerId: user._id }), StoreSettings.findOne({ userId: user._id }), UserSubscription.findOne({ userId: user._id, status: 'active' }).populate('planId'), Post.aggregate([ { $match: { userId: user._id } }, { $project: { likesCount: { $size: '$likes' } } }, { $group: { _id: null, total: { $sum: '$likesCount' } } } ]) ]); user.followersCount = followersCount || 0; user.followingCount = followingCount || 0; user.postsCount = postsCount || 0; user.projectsCount = projectsCount || 0; user.jobsCount = jobsCount || 0; user.totalLikes = totalLikesResult[0]?.total || 0; // ✅ إضافة store if (storeSettings) { const productsCount = await Product.countDocuments({ userId: user._id, isActive: true }); const storeFollowersCount = await StoreFollow.countDocuments({ storeId: user._id }); user.store = { _id: user._id, userId: user._id, storeName: storeSettings.storeName || user.profile?.nickname || user.username, storeLogo: storeSettings.storeLogo, storeBanner: storeSettings.storeBanner, storeDescription: storeSettings.storeDescription, contactEmail: storeSettings.contactEmail || user.email, enabled: storeSettings.enabled || false, currency: storeSettings.currency || 'USD', currencySymbol: storeSettings.currencySymbol || '$', productsCount: productsCount, followersCount: storeFollowersCount, averageRating: storeSettings.averageRating || 0, totalSales: 0, totalOrders: 0, createdAt: storeSettings.createdAt, updatedAt: storeSettings.updatedAt }; } else { user.store = null; } user.subscription = subscription || null; user.permissions = { canCreateStore: !!(storeSettings?.enabled), canCreateProducts: !!(subscription?.planId?.features?.storeEnabled) }; if (user.profile) { user.profile.storeEnabled = storeSettings?.enabled || false; user.profile.averageRating = storeSettings?.averageRating || 0; user.profile.totalRatings = storeSettings?.totalRatings || 0; } // ✅ حساب hasStory const now = new Date(); const storyCount = await Story.countDocuments({ userId: user._id, expiresAt: { $gt: now } }); user.hasStory = storyCount > 0; return user; }, user: async (parent, { id }) => { const user = await User.findById(id).lean(); if (!user) return null; // ✅ تطبيق التحويلات على profile if (user.profile) { const p = user.profile; user.profile.education = Array.isArray(p.education) ? p.education : Object.values(p.education || {}); user.profile.experience = Array.isArray(p.experience) ? p.experience : Object.values(p.experience || {}); user.profile.certificates = Array.isArray(p.certificates) ? p.certificates : Object.values(p.certificates || {}); user.profile.skills = Array.isArray(p.skills) ? p.skills : Object.values(p.skills || {}); user.profile.projects = Array.isArray(p.projects) ? p.projects : Object.values(p.projects || {}); user.profile.interests = Array.isArray(p.interests) ? p.interests : Object.values(p.interests || {}); user.profile.customSections = Array.isArray(p.customSections) ? p.customSections : Object.values(p.customSections || {}); user.profile.sectionOrder = Array.isArray(p.sectionOrder) ? p.sectionOrder : Object.values(p.sectionOrder || {}); user.profile.sectionNames = p.sectionNames || {}; user.profile.sectionVisibility = p.sectionVisibility || {}; user.profile.sectionStyleSettings = p.sectionStyleSettings || {}; user.profile.designSettings = p.designSettings || {}; user.profile.aiBot = p.aiBot || { enabled: false, provider: 'mgzon' }; user.profile.audio = p.audio || {}; user.profile.theme = p.theme || {}; user.profile.layout = p.layout || {}; user.profile.header = p.header || {}; user.profile.footer = p.footer || {}; user.profile.seo = p.seo || {}; user.profile.schema = p.schema || {}; user.profile.socialLinks = p.socialLinks || {}; } const [followersCount, followingCount, postsCount] = await Promise.all([ Follow.countDocuments({ followingId: user._id, status: 'accepted' }), Follow.countDocuments({ followerId: user._id, status: 'accepted' }), Post.countDocuments({ userId: user._id }) ]); user.followersCount = followersCount || 0; user.followingCount = followingCount || 0; user.postsCount = postsCount || 0; // ✅ حساب hasStory const now = new Date(); const storyCount = await Story.countDocuments({ userId: user._id, expiresAt: { $gt: now } }); user.hasStory = storyCount > 0; return user; }, userByNickname: async (parent, { nickname }) => { return await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${nickname}$`, $options: 'i' } }, { username: { $regex: `^${nickname}$`, $options: 'i' } } ] }); }, profile: async (parent, { nickname }, context) => { const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${nickname}$`, $options: 'i' } }, { username: { $regex: `^${nickname}$`, $options: 'i' } } ] }).lean(); // ✅ استخدام lean() للحصول على Object عادي if (!user) throw new GraphQLError('User not found'); const isOwner = context.user && context.user.userId === user._id.toString(); if (!user.profile?.isPublic && !isOwner) { throw new GraphQLError('Profile is private'); } // ✅ تطبيق التحويلات يدوياً على profile if (user.profile) { const p = user.profile; // تحويل Object إلى Array user.profile.education = Array.isArray(p.education) ? p.education : Object.values(p.education || {}); user.profile.experience = Array.isArray(p.experience) ? p.experience : Object.values(p.experience || {}); user.profile.certificates = Array.isArray(p.certificates) ? p.certificates : Object.values(p.certificates || {}); user.profile.skills = Array.isArray(p.skills) ? p.skills : Object.values(p.skills || {}); user.profile.projects = Array.isArray(p.projects) ? p.projects : Object.values(p.projects || {}); user.profile.interests = Array.isArray(p.interests) ? p.interests : Object.values(p.interests || {}); user.profile.customSections = Array.isArray(p.customSections) ? p.customSections : Object.values(p.customSections || {}); user.profile.sectionOrder = Array.isArray(p.sectionOrder) ? p.sectionOrder : Object.values(p.sectionOrder || {}); // التأكد من وجود الحقول الافتراضية user.profile.sectionNames = p.sectionNames || {}; user.profile.sectionVisibility = p.sectionVisibility || {}; user.profile.sectionStyleSettings = p.sectionStyleSettings || {}; user.profile.designSettings = p.designSettings || {}; user.profile.aiBot = p.aiBot || { enabled: false, provider: 'mgzon' }; user.profile.audio = p.audio || {}; user.profile.theme = p.theme || {}; user.profile.layout = p.layout || {}; user.profile.header = p.header || {}; user.profile.footer = p.footer || {}; user.profile.seo = p.seo || {}; user.profile.schema = p.schema || {}; user.profile.socialLinks = p.socialLinks || {}; } // ✅ جلب الحسابات const [followersCount, followingCount, postsCount] = await Promise.all([ Follow.countDocuments({ followingId: user._id, status: 'accepted' }), Follow.countDocuments({ followerId: user._id, status: 'accepted' }), Post.countDocuments({ userId: user._id }) ]); user.followersCount = followersCount || 0; user.followingCount = followingCount || 0; user.postsCount = postsCount || 0; // ✅ حساب hasStory const now = new Date(); const storyCount = await Story.countDocuments({ userId: user._id, expiresAt: { $gt: now } }); user.hasStory = storyCount > 0; return user; }, myProfile: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const user = await User.findById(context.user.userId).lean(); if (!user) return null; // ✅ تطبيق التحويلات على profile if (user.profile) { const p = user.profile; user.profile.education = Array.isArray(p.education) ? p.education : Object.values(p.education || {}); user.profile.experience = Array.isArray(p.experience) ? p.experience : Object.values(p.experience || {}); user.profile.certificates = Array.isArray(p.certificates) ? p.certificates : Object.values(p.certificates || {}); user.profile.skills = Array.isArray(p.skills) ? p.skills : Object.values(p.skills || {}); user.profile.projects = Array.isArray(p.projects) ? p.projects : Object.values(p.projects || {}); user.profile.interests = Array.isArray(p.interests) ? p.interests : Object.values(p.interests || {}); user.profile.customSections = Array.isArray(p.customSections) ? p.customSections : Object.values(p.customSections || {}); user.profile.sectionOrder = Array.isArray(p.sectionOrder) ? p.sectionOrder : Object.values(p.sectionOrder || {}); user.profile.sectionNames = p.sectionNames || {}; user.profile.sectionVisibility = p.sectionVisibility || {}; user.profile.sectionStyleSettings = p.sectionStyleSettings || {}; user.profile.designSettings = p.designSettings || {}; user.profile.aiBot = p.aiBot || { enabled: false, provider: 'mgzon' }; user.profile.audio = p.audio || {}; user.profile.theme = p.theme || {}; user.profile.layout = p.layout || {}; user.profile.header = p.header || {}; user.profile.footer = p.footer || {}; user.profile.seo = p.seo || {}; user.profile.schema = p.schema || {}; user.profile.socialLinks = p.socialLinks || {}; } const [followersCount, followingCount, postsCount] = await Promise.all([ Follow.countDocuments({ followingId: user._id, status: 'accepted' }), Follow.countDocuments({ followerId: user._id, status: 'accepted' }), Post.countDocuments({ userId: user._id }) ]); user.followersCount = followersCount || 0; user.followingCount = followingCount || 0; user.postsCount = postsCount || 0; return user; }, userStats: async (parent, { userId }) => { const [posts, projects, jobs, followers, following, applications, savedPosts, savedJobs] = await Promise.all([ Post.countDocuments({ userId }), Project.countDocuments({ userId }), Job.countDocuments({ employerId: userId }), Follow.countDocuments({ followingId: userId, status: 'accepted' }), Follow.countDocuments({ followerId: userId, status: 'accepted' }), JobApplication.countDocuments({ applicantId: userId }), Post.countDocuments({ 'saves.userId': userId }), JobSave.countDocuments({ userId }) ]); const likesResult = await Post.aggregate([ { $match: { userId: new mongoose.Types.ObjectId(userId) } }, { $project: { likesCount: { $size: '$likes' } } }, { $group: { _id: null, total: { $sum: '$likesCount' } } } ]); return { posts, projects, jobs, followers, following, applications, savedPosts, savedJobs, totalLikes: likesResult[0]?.total || 0 }; }, // ============================================ // 🔹 PROFILE PAGE RESOLVERS // ============================================ // ===== 1. profilePage (مجمعة) ===== profilePage: async (parent, { nickname }, context) => { const userId = context.user?.userId; // ✅ 1. جلب بيانات البروفايل let profileResult = null; try { profileResult = await Query.profile(parent, { nickname }, context); } catch (e) { console.error('Error loading profile:', e); throw new GraphQLError('User not found'); } if (!profileResult) throw new GraphQLError('User not found'); const profileUserId = profileResult._id.toString(); const isOwner = userId === profileUserId; // ✅ 2. جلب الإحصائيات let stats = { posts: 0, projects: 0, jobs: 0, followers: 0, following: 0, applications: 0, savedPosts: 0, savedJobs: 0, totalLikes: 0 }; try { stats = await Query.userStats(parent, { userId: profileUserId }); } catch (e) { console.error('Error loading stats:', e); } // ✅ 3. جلب التقييمات let ratings = { data: [], pagination: { page: 1, limit: 1, total: 0, pages: 0, hasNext: false, hasPrev: false }, averageRating: 0, totalRatings: 0 }; try { ratings = await Query.userRatings(parent, { userId: profileUserId, page: 1, limit: 1 }); } catch (e) { console.error('Error loading ratings:', e); } // ✅ 4. جلب التفاعلات (Recent Activity) let interactions = []; try { interactions = await getProfileInteractions(profileUserId, 1, 4); } catch (e) { console.error('Error loading interactions:', e); } // ✅ 5. جلب طلبات التوظيف (للمالك فقط) let jobApplications = { data: [], pagination: { page: 1, limit: 5, total: 0, pages: 0, hasNext: false, hasPrev: false } }; if (isOwner) { try { jobApplications = await Query.myJobApplications(parent, { page: 1, limit: 5 }, context); } catch (e) { console.error('Error loading job applications:', e); } } // ✅ 6. جلب Contribution Graph let contributionGraph = { total: 0, weeks: [], year: 2026, months: [], weekDays: [] }; try { contributionGraph = await getContributionGraph(profileUserId, 2026); } catch (e) { console.error('Error loading contribution graph:', e); } // ✅ 7. جلب الإشعارات (للمالك فقط) let notifications = { data: [], pagination: { page: 1, limit: 10, total: 0, pages: 0, hasNext: false, hasPrev: false }, unreadCount: 0 }; if (isOwner) { try { notifications = await Query.notifications(parent, { page: 1, limit: 10 }, context); } catch (e) { console.error('Error loading notifications:', e); } } // ✅ 8. جلب التحليلات (للمالك فقط) let analytics = null; if (isOwner) { try { analytics = await getProfileAnalytics(profileUserId); } catch (e) { console.error('Error loading analytics:', e); } } // ✅ 9. جلب حالة الـ AI let aiStatus = { enabled: false }; try { aiStatus = await Query.aiStatus(parent, { nickname }); } catch (e) { console.error('Error loading AI status:', e); } // ✅ 10. جلب حالة المتابعة (للمستخدمين المسجلين فقط) let followStatus = null; if (userId && !isOwner) { try { followStatus = await Query.followStatus(parent, { userId: profileUserId }, context); } catch (e) { console.error('Error loading follow status:', e); } } // ✅ 11. جلب "شاهد أيضاً" let alsoViewed = []; try { alsoViewed = await getAlsoViewed(profileUserId); } catch (e) { console.error('Error loading also viewed:', e); } // ✅ 12. جلب اقتراحات الصفحات let pageSuggestions = []; try { pageSuggestions = await getPageSuggestions(2); } catch (e) { console.error('Error loading page suggestions:', e); } // ✅ 13. جلب عدد الإشعارات غير المقروءة (للمالك فقط) let unreadCount = 0; if (isOwner) { try { unreadCount = await Query.unreadNotificationsCount(parent, {}, context); } catch (e) { console.error('Error loading unread count:', e); } } // ✅ 14. جلب إعدادات المتجر والاشتراك (للمالك فقط) let storeSettings = null; let subscription = null; if (isOwner) { try { storeSettings = await Query.storeSettings(parent, {}, context); subscription = await UserSubscription.findOne({ userId: profileUserId, status: 'active' }).populate('planId'); } catch (e) { console.error('Error loading store settings:', e); } } // ✅ أضف القيم للـ profile if (profileResult.profile) { profileResult.profile.storeEnabled = storeSettings?.settings?.enabled || false; profileResult.profile.averageRating = storeSettings?.settings?.averageRating || 0; profileResult.profile.totalRatings = storeSettings?.settings?.totalRatings || 0; } // ✅ أضف الـ permissions للـ profileResult profileResult.permissions = { canCreateStore: !!(storeSettings?.settings?.enabled), canCreateProducts: !!(subscription?.planId?.features?.storeEnabled) }; // ✅ 15. جلب حالة الاشتراك let subscriptionStatus = { hasActiveSubscription: false, canCreateStore: false, message: 'No active subscription', planName: null, daysLeft: 0 }; try { subscriptionStatus = await getSubscriptionStatus(profileUserId); } catch (e) { console.error('Error loading subscription status:', e); } return { profile: profileResult, stats, ratings, interactions, jobApplications, contributionGraph, notifications, analytics, aiStatus, followStatus, alsoViewed, pageSuggestions, unreadCount, storeSettings, subscriptionStatus }; }, // ===== 2. contributionGraph (جديد) ===== contributionGraph: async (parent, { userId, year }) => { return await getContributionGraph(userId, year || 2026); }, // ===== 3. userInteractions (جديد) ===== userInteractions: async (parent, { nickname, page = 1, limit = 4 }) => { const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${nickname}$`, $options: 'i' } }, { username: { $regex: `^${nickname}$`, $options: 'i' } } ] }); if (!user) return []; return await getProfileInteractions(user._id, page, limit); }, // ===== 4. alsoViewed (جديد) ===== alsoViewed: async (parent, { userId }) => { return await getAlsoViewed(userId); }, // ===== 5. pageSuggestions (جديد) ===== pageSuggestions: async (parent, { limit = 2 }) => { return await getPageSuggestions(limit); }, // ===== 6. profileAnalytics (جديد) ===== profileAnalytics: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); return await getProfileAnalytics(context.user.userId); }, // ============================================ // 🔹 QUERY RESOLVERS - إضافة userSuggestions // ============================================ userSuggestions: async (parent, { limit = 3 }, context) => { // ✅ إذا مش مسجل، أرجع array فاضي if (!context.user) { return []; } try { // ✅ جلب المتابعين الحاليين const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }).select('followingId'); const followingIds = following.map(f => f.followingId); followingIds.push(context.user.userId); // ✅ استبعاد نفسه // ✅ استخدام MongoDB Aggregation لجلب الاقتراحات (عشوائية) const suggestions = await User.aggregate([ { $match: { _id: { $nin: followingIds }, 'profile.isPublic': true } }, { $lookup: { from: 'follows', localField: '_id', foreignField: 'followingId', as: 'followers' } }, { $addFields: { followersCount: { $size: '$followers' } } }, { $sample: { size: parseInt(limit) || 3 } // ✅ عشوائي مع كل ريلود }, { $project: { _id: 1, username: 1, followersCount: 1, 'profile.nickname': 1, 'profile.avatar': 1, 'profile.jobTitle': 1, 'profile.bio': 1 } } ]); return suggestions; } catch (error) { console.error('Error fetching user suggestions:', error); return []; } }, storeStatus: async (parent, args, context) => { // ✅ إذا مش مسجل، أرجع false if (!context.user) { return { enabled: false }; } try { // ✅ جلب الحقل enabled فقط من قاعدة البيانات const store = await StoreSettings.findOne( { userId: context.user.userId } ).select('enabled'); // ✅ فقط الحقل المطلوب return { enabled: store?.enabled || false }; } catch (error) { console.error('Error fetching store status:', error); return { enabled: false }; } }, // ===== Search ===== searchUsers: async (parent, { query, page = 1, limit = 20 }) => { if (query.length < 2) { return { data: [], pagination: { page: 1, limit: 20, total: 0, pages: 0, hasNext: false, hasPrev: false } }; } const skip = (page - 1) * limit; const searchQuery = { $or: [ { 'profile.nickname': { $regex: query, $options: 'i' } }, { username: { $regex: query, $options: 'i' } }, { 'profile.jobTitle': { $regex: query, $options: 'i' } }, { 'profile.bio': { $regex: query, $options: 'i' } } ] }; const [users, total] = await Promise.all([ User.find(searchQuery) .select('username profile email createdAt') .skip(skip) .limit(limit) .lean(), User.countDocuments(searchQuery) ]); return { data: users, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, searchGlobal: async (parent, { query, limit = 5 }) => { if (query.length < 2) { return { products: [], stores: [], users: [], jobs: [], posts: [], total: { products: 0, stores: 0, users: 0, jobs: 0, posts: 0 } }; } const regex = new RegExp(query, 'i'); const lim = Math.min(limit, 20); const [products, stores, users, jobs, posts] = await Promise.all([ Product.find({ isActive: true, $or: [{ title: regex }, { description: regex }, { tags: regex }] }) .limit(lim) .populate('userId', 'username profile.nickname') .lean(), StoreSettings.find({ enabled: true, $or: [{ storeName: regex }, { storeDescription: regex }] }) .limit(lim) .populate('userId', 'username profile.nickname profile.avatar') .lean(), User.find({ $or: [ { username: regex }, { 'profile.nickname': regex }, { 'profile.jobTitle': regex }, { 'profile.skills.name': regex } ] }) .limit(lim) .select('username profile.nickname profile.avatar profile.jobTitle profile.bio') .lean(), Job.find({ status: 'active', $or: [{ title: regex }, { description: regex }, { companyName: regex }] }) .limit(lim) .select('title companyName location jobType salaryMin salaryMax isRemote') .lean(), Post.find({ visibility: 'public', $or: [{ content: regex }, { tags: regex }] }) .limit(lim) .populate('userId', 'username profile.nickname profile.avatar') .sort({ createdAt: -1 }) .lean() ]); return { products: products.map(p => ({ _id: p._id, title: p.title, price: p.price, type: p.type, image: p.images?.[0] || null, storeNickname: p.userId?.profile?.nickname || p.userId?.username, url: `/product.html?id=${p._id}&owner=${p.userId?.profile?.nickname || p.userId?.username}` })), stores: stores.map(s => ({ _id: s._id, storeName: s.storeName || s.userId?.profile?.nickname || s.userId?.username, storeLogo: s.storeLogo || null, nickname: s.userId?.profile?.nickname || s.userId?.username, productsCount: s.productsCount || 0, followersCount: s.followersCount || 0, averageRating: s.averageRating || 0, url: `/shop/${s.userId?.profile?.nickname || s.userId?.username}` })), users: users.map(u => ({ _id: u._id, name: u.profile?.nickname || u.username, avatar: u.profile?.avatar || '/assets/img/default-avatar.png', jobTitle: u.profile?.jobTitle || 'Professional', bio: u.profile?.bio?.substring(0, 100) || '', url: `/profile/${u.profile?.nickname || u.username}` })), jobs: jobs.map(j => ({ _id: j._id, title: j.title, company: j.companyName, location: j.location, jobType: j.jobType, isRemote: j.isRemote || false, salary: j.salaryMin ? `${j.salaryMin} - ${j.salaryMax}` : null, url: `/job-details.html?id=${j._id}` })), posts: posts.map(p => ({ _id: p._id, content: p.content?.slice(0, 150) + (p.content?.length > 150 ? '...' : ''), image: p.images?.[0]?.url || null, author: { name: p.userId?.profile?.nickname || p.userId?.username, avatar: p.userId?.profile?.avatar || '/assets/img/default-avatar.png' }, likesCount: p.likes?.length || 0, createdAt: p.createdAt, url: `/post.html?id=${p._id}` })), total: { products: products.length, stores: stores.length, users: users.length, jobs: jobs.length, posts: posts.length } }; }, // ============================================ // 🔹 POSTS QUERIES - النسخة المحدثة // ============================================ postsFeed: async (parent, { page = 1, limit = 20, type }, context) => { // ✅ إذا لم يكن المستخدم موثّقاً، أرجع بيانات فارغة if (!context.user) { return { data: [], pagination: { page: 1, limit: 20, total: 0, pages: 0, hasNext: false, hasPrev: false } }; } const skip = (page - 1) * limit; const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }).select('followingId'); const followingIds = following.map(f => f.followingId); followingIds.push(context.user.userId); let query = { userId: { $in: followingIds }, visibility: { $in: ['public', 'followers'] } }; if (type === 'popular') { query = { ...query, 'likes.1': { $exists: true } }; } const [posts, total] = await Promise.all([ Post.find(query) .sort({ pinned: -1, createdAt: -1 }) .skip(skip) .limit(limit) .populate('userId', 'username profile.nickname profile.avatar') .lean(), Post.countDocuments(query) ]); // ✅ جلب الـ hasStory لكل مستخدم const userIds = posts.map(p => p.userId._id); const usersWithStories = await Story.aggregate([ { $match: { userId: { $in: userIds }, expiresAt: { $gt: new Date() } } }, { $group: { _id: '$userId', count: { $sum: 1 } } } ]); const userHasStoryMap = {}; usersWithStories.forEach(u => { userHasStoryMap[u._id.toString()] = true; }); // ✅ جلب البوستات الأصلية للمشاركات const postsWithSharedData = await Promise.all(posts.map(async (post) => { let sharedFrom = post.sharedFrom || null; if (sharedFrom && sharedFrom.originalPostId) { const originalPost = await Post.findById(sharedFrom.originalPostId) .populate('userId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(); if (originalPost) { sharedFrom = { originalPostId: originalPost._id, originalAuthorId: originalPost.userId?._id || null, originalAuthorName: originalPost.userId?.profile?.nickname || originalPost.userId?.username || 'Unknown', originalAuthorAvatar: originalPost.userId?.profile?.avatar || null, originalAuthorJobTitle: originalPost.userId?.profile?.jobTitle || null, originalContent: originalPost.content || '', originalImages: originalPost.images || [], originalVideo: originalPost.video || null, originalCreatedAt: originalPost.createdAt || null, originalLikesCount: originalPost.likes?.length || 0, originalCommentsCount: originalPost.comments?.length || 0, originalSharesCount: originalPost.shares?.length || 0, sharedAt: post.sharedFrom?.sharedAt || null }; } } return { ...post, sharedFrom }; })); // ✅ إرجاع كل الحقول مع الحسابات return { data: postsWithSharedData.map(post => ({ ...post, sharedFrom: post.sharedFrom || null, likesCount: (post.likes || []).length, commentsCount: (post.comments || []).length, sharesCount: (post.shares || []).length, savesCount: (post.saves || []).length, isLiked: post.likes?.some(l => l.userId?.toString() === context.user.userId) || false, isSaved: post.saves?.some(s => s.userId?.toString() === context.user.userId) || false, userId: { ...post.userId, hasStory: userHasStoryMap[post.userId._id.toString()] || false } })), pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, post: async (parent, { id }, context) => { // ✅ جلب البوست مع التعليقات والردود باستخدام aggregation const result = await Post.aggregate([ { $match: { _id: new mongoose.Types.ObjectId(id) } }, { $lookup: { from: 'users', localField: 'userId', foreignField: '_id', as: 'userId' } }, { $unwind: { path: '$userId', preserveNullAndEmptyArrays: true } }, { $lookup: { from: 'users', localField: 'comments.userId', foreignField: '_id', as: 'commentsUser' } }, { $lookup: { from: 'users', localField: 'comments.replies.userId', foreignField: '_id', as: 'repliesUser' } } ]); if (!result || result.length === 0) { throw new GraphQLError('Post not found'); } const post = result[0]; // ✅ معالجة التعليقات والردود if (post.comments && post.comments.length > 0) { post.comments = post.comments.map(c => { // ✅ جلب المستخدم من commentsUser const commentUser = post.commentsUser?.find(u => u._id.toString() === c.userId?.toString()); // ✅ معالجة الردود const replies = (c.replies || []).map(r => { const replyUser = post.repliesUser?.find(u => u._id.toString() === r.userId?.toString()); return { ...r, userId: replyUser || { _id: r.userId || 'unknown', username: 'Unknown', profile: { nickname: 'Unknown', avatar: '/assets/img/default-avatar.png', jobTitle: '' } }, likesCount: r.likes?.length || 0, isLiked: r.likes?.some(l => l.userId?.toString() === context.user?.userId) || false, isEdited: r.isEdited || false, edited: r.edited || false }; }); return { ...c, userId: commentUser || { _id: c.userId || 'unknown', username: 'Unknown', profile: { nickname: 'Unknown', avatar: '/assets/img/default-avatar.png', jobTitle: '' } }, likesCount: c.likes?.length || 0, isLiked: c.likes?.some(l => l.userId?.toString() === context.user?.userId) || false, replies: replies, isEdited: c.isEdited || false, edited: c.edited || false }; }); } // ✅ حذف الحقول المؤقتة delete post.commentsUser; delete post.repliesUser; // ============================================================ // ✅ جلب البوست الأصلي إذا كان هذا البوست مشاركة // ============================================================ let sharedFrom = post.sharedFrom || null; if (sharedFrom && sharedFrom.originalPostId) { const originalPost = await Post.findById(sharedFrom.originalPostId) .populate('userId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(); if (originalPost) { sharedFrom = { originalPostId: originalPost._id, originalAuthorId: originalPost.userId?._id || null, originalAuthorName: originalPost.userId?.profile?.nickname || originalPost.userId?.username || 'Unknown', originalAuthorAvatar: originalPost.userId?.profile?.avatar || null, originalAuthorJobTitle: originalPost.userId?.profile?.jobTitle || null, originalContent: originalPost.content || '', originalImages: originalPost.images || [], originalVideo: originalPost.video || null, originalCreatedAt: originalPost.createdAt || null, originalLikesCount: originalPost.likes?.length || 0, originalCommentsCount: originalPost.comments?.length || 0, originalSharesCount: originalPost.shares?.length || 0, sharedAt: post.sharedFrom?.sharedAt || null }; } } // ✅ حساب القيم مباشرة return { ...post, userId: post.userId || { _id: post.userId, username: 'Unknown', profile: {} }, sharedFrom, likesCount: post.likes?.length || 0, commentsCount: post.comments?.length || 0, sharesCount: post.shares?.length || 0, savesCount: post.saves?.length || 0, isLiked: context.user ? post.likes?.some(l => l.userId?.toString() === context.user.userId) : false, isSaved: context.user ? post.saves?.some(s => s.userId?.toString() === context.user.userId) : false }; }, userPosts: async (parent, { userId, page = 1, limit = 20 }, context) => { const skip = (page - 1) * limit; const isOwner = context.user && context.user.userId === userId; let visibilityFilter = {}; if (!isOwner) { visibilityFilter = { visibility: { $in: ['public', 'followers'] } }; } const [posts, total] = await Promise.all([ Post.find({ userId, ...visibilityFilter }) .sort({ pinned: -1, createdAt: -1 }) .skip(skip) .limit(limit) .populate('userId', 'username profile.nickname profile.avatar') .lean(), Post.countDocuments({ userId, ...visibilityFilter }) ]); // ✅ جلب البوستات الأصلية للمشاركات const sharedPostIds = posts .filter(p => p.sharedFrom?.originalPostId) .map(p => p.sharedFrom.originalPostId); let originalPostsMap = new Map(); if (sharedPostIds.length > 0) { const originalPosts = await Post.find({ _id: { $in: sharedPostIds } }) .populate('userId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(); originalPosts.forEach(op => { originalPostsMap.set(op._id.toString(), { _id: op._id, content: op.content || '', images: op.images || [], video: op.video || null, userId: op.userId, createdAt: op.createdAt, likesCount: op.likes?.length || 0, commentsCount: op.comments?.length || 0, sharesCount: op.shares?.length || 0 }); }); } return { data: posts.map(post => { // ✅ نبدأ بـ sharedFrom كـ null let sharedFrom = null; // ✅ لو في مشاركة، نبني الكائن كامل if (post.sharedFrom && post.sharedFrom.originalPostId) { const originalPost = originalPostsMap.get(post.sharedFrom.originalPostId.toString()); if (originalPost) { sharedFrom = { originalPostId: post.sharedFrom.originalPostId, originalAuthorId: originalPost.userId?._id || null, originalAuthorName: post.sharedFrom.originalAuthorName || 'Unknown', originalAuthorAvatar: originalPost.userId?.profile?.avatar || null, originalAuthorJobTitle: originalPost.userId?.profile?.jobTitle || null, originalContent: originalPost.content || '', originalImages: originalPost.images || [], // ✅ دائماً array وليس null originalVideo: originalPost.video || null, originalCreatedAt: originalPost.createdAt || null, originalLikesCount: originalPost.likesCount || 0, originalCommentsCount: originalPost.commentsCount || 0, originalSharesCount: originalPost.sharesCount || 0, sharedAt: post.sharedFrom.sharedAt || null }; } } return { ...post, sharedFrom, // ✅ null للبوستات العادية، أو كائن كامل للمشاركات likesCount: post.likes?.length || 0, commentsCount: post.comments?.length || 0, sharesCount: post.shares?.length || 0, savesCount: post.saves?.length || 0, isLiked: post.likes?.some(l => l.userId?.toString() === context.user?.userId) || false, isSaved: post.saves?.some(s => s.userId?.toString() === context.user?.userId) || false }; }), pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, savedPosts: async (parent, { page = 1, limit = 20 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; const [posts, total] = await Promise.all([ Post.find({ 'saves.userId': context.user.userId }) .sort({ 'saves.savedAt': -1 }) .skip(skip) .limit(limit) .populate('userId', 'username profile.nickname profile.avatar') .lean(), Post.countDocuments({ 'saves.userId': context.user.userId }) ]); // ✅ حساب القيم مباشرة return { data: posts.map(post => ({ ...post, sharedFrom: post.sharedFrom || null, likesCount: (post.likes || []).length, commentsCount: (post.comments || []).length, sharesCount: (post.shares || []).length, savesCount: (post.saves || []).length, isSaved: true, isLiked: post.likes?.some(l => l.userId?.toString() === context.user.userId) || false })), pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, suggestedPosts: async (parent, { limit = 5 }, context) => { if (!context.user) return []; const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }).select('followingId'); const followingIds = following.map(f => f.followingId); followingIds.push(context.user.userId); const posts = await Post.find({ userId: { $nin: followingIds }, visibility: { $in: ['public', 'followers'] } }) .sort({ createdAt: -1 }) .limit(limit) .populate('userId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(); // ✅ حساب القيم مباشرة return posts.map(post => ({ ...post, sharedFrom: post.sharedFrom || null, likesCount: (post.likes || []).length, commentsCount: (post.comments || []).length, sharesCount: (post.shares || []).length, savesCount: (post.saves || []).length, isLiked: post.likes?.some(l => l.userId?.toString() === context.user.userId) || false, isSaved: post.saves?.some(s => s.userId?.toString() === context.user.userId) || false })); }, searchPosts: async (parent, { query, page = 1, limit = 20 }, context) => { if (query.length < 2) { return { data: [], pagination: { page: 1, limit: 20, total: 0, pages: 0, hasNext: false, hasPrev: false } }; } const skip = (page - 1) * limit; const searchQuery = { $or: [ { content: { $regex: query, $options: 'i' } }, { tags: { $in: [new RegExp(query, 'i')] } } ], visibility: { $in: ['public', 'followers'] } }; const [posts, total] = await Promise.all([ Post.find(searchQuery) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .populate('userId', 'username profile.nickname profile.avatar') .lean(), Post.countDocuments(searchQuery) ]); // ✅ حساب القيم مباشرة return { data: posts.map(post => ({ ...post, sharedFrom: post.sharedFrom || null, likesCount: (post.likes || []).length, commentsCount: (post.comments || []).length, sharesCount: (post.shares || []).length, savesCount: (post.saves || []).length, isLiked: post.likes?.some(l => l.userId?.toString() === context.user.userId) || false, isSaved: post.saves?.some(s => s.userId?.toString() === context.user.userId) || false })), pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, // ===== Comments ===== comments: async (parent, { projectId, page = 1, limit = 20 }, context) => { const skip = (page - 1) * limit; const mainComments = await Comment.find({ projectId: new mongoose.Types.ObjectId(projectId), parentCommentId: null, hidden: false }) .populate('userId', 'username profile.nickname profile.avatar') .sort({ timestamp: -1 }) .skip(skip) .limit(limit); const commentsWithReplies = await Promise.all(mainComments.map(async (comment) => { const replies = await Comment.find({ parentCommentId: comment._id, hidden: false }) .populate('userId', 'username profile.nickname profile.avatar') .sort({ timestamp: 1 }); return { ...comment.toObject(), replies, repliesCount: replies.length, likesCount: comment.likes?.length || 0, isLiked: context.user ? comment.likes?.some(id => id.toString() === context.user.userId) : false }; })); const total = await Comment.countDocuments({ projectId: new mongoose.Types.ObjectId(projectId), parentCommentId: null, hidden: false }); return { data: commentsWithReplies, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, comment: async (parent, { id }, context) => { const comment = await Comment.findById(id) .populate('userId', 'username profile.nickname profile.avatar') .populate('projectId') .populate('projectOwnerId', 'username profile.nickname'); if (!comment) throw new GraphQLError('Comment not found'); if (comment.hidden && !context.isAdmin) { throw new GraphQLError('This comment is hidden'); } const replies = await Comment.find({ parentCommentId: comment._id, hidden: false }) .populate('userId', 'username profile.nickname profile.avatar') .sort({ timestamp: 1 }); return { ...comment.toObject(), replies, repliesCount: replies.length, likesCount: comment.likes?.length || 0, isLiked: context.user ? comment.likes?.some(id => id.toString() === context.user.userId) : false }; }, userComments: async (parent, { userId, page = 1, limit = 20 }, context) => { const skip = (page - 1) * limit; const isOwner = context.user && context.user.userId === userId; let query = { userId, hidden: false }; if (!isOwner) { query.visibility = 'public'; } const comments = await Comment.find(query) .populate('userId', 'username profile.nickname profile.avatar') .populate('projectId') .sort({ timestamp: -1 }) .skip(skip) .limit(limit); const total = await Comment.countDocuments(query); const enrichedComments = comments.map(comment => ({ ...comment.toObject(), likesCount: comment.likes?.length || 0, isLiked: context.user ? comment.likes?.some(id => id.toString() === context.user.userId) : false })); return { data: enrichedComments, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, reportedComments: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const reportedComments = await Comment.find({ reportCount: { $gt: 0 }, hidden: { $ne: true } }) .sort({ reportCount: -1, timestamp: -1 }) .populate('userId', 'username email profile.nickname') .populate('projectId'); return reportedComments; }, // ✅ getPostComments - جلب تعليقات منشور getPostComments: async (parent, { postId, page = 1, limit = 50 }, context) => { const post = await Post.findById(postId); if (!post) throw new GraphQLError('Post not found'); const skip = (page - 1) * limit; const comments = post.comments || []; // جلب تعليقات الصفحة const paginatedComments = comments.slice(skip, skip + limit); // جلب بيانات المستخدمين const commentData = await Promise.all(paginatedComments.map(async (comment) => { const user = await User.findById(comment.userId).select('username profile.nickname profile.avatar'); const likesCount = comment.likes?.length || 0; const isLiked = comment.likes?.some(id => id.toString() === context.user?.userId) || false; return { _id: comment._id, userId: user || { _id: comment.userId, username: 'Unknown', profile: {} }, text: comment.text, createdAt: comment.createdAt, likesCount, isLiked, edited: comment.edited || false, editedAt: comment.editedAt || null, hidden: comment.hidden || false }; })); return { data: commentData, pagination: { page, limit, total: comments.length, pages: Math.ceil(comments.length / limit), hasNext: skip + limit < comments.length, hasPrev: page > 1 } }; }, // ============================================ // 🔹 STORIES RESOLVERS - النسخة النهائية // ============================================ // ===== Stories ===== // ✅ storiesFeed - قصص المتابعين فقط storiesFeed: async (parent, { limit = 20 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }).select('followingId'); const followingIds = following.map(f => f.followingId); followingIds.push(context.user.userId); const stories = await Story.find({ userId: { $in: followingIds }, expiresAt: { $gt: new Date() }, $or: [ { visibility: 'public' }, { visibility: 'followers' }, { visibility: 'only-me', userId: context.user.userId } ] }) .sort({ createdAt: -1 }) .limit(limit * 2) .populate('userId') .lean(); const storiesByUser = {}; stories.forEach(story => { const userId = story.userId._id.toString(); if (!storiesByUser[userId]) { storiesByUser[userId] = { user: story.userId, stories: [] }; } const hasViewed = story.views?.some(v => v.userId?.toString() === context.user.userId) || false; storiesByUser[userId].stories.push({ ...story, viewsCount: (story.views || []).length, reactionsCount: (story.reactions || []).length, hasViewed: hasViewed, textColor: story.textColor || '#ffffff', userId: story.userId, allowDownload: story.allowDownload || false, filter: story.filter || 'none', // ✅ أضف هذا filterIntensity: story.filterIntensity || 0.5, // ✅ أضف هذا filterApplied: story.filterApplied || false, // ✅ أضف هذا audio: story.audio || { url: null, publicId: null, duration: 0 }, // ✅ أضف هذا textStyle: story.textStyle || { // ✅ أضف هذا fontSize: 32, fontFamily: 'Inter', fontWeight: '700', textAlign: 'center', position: { x: 50, y: 50 }, rotation: 0, opacity: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)', background: 'transparent', padding: '0', borderRadius: '0', maxWidth: 90 } }); }); return Object.values(storiesByUser); }, // ✅ storiesAll - كل القصص (للاكتشاف + المتابعين) storiesAll: async (parent, { page = 1, limit = 20 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }).select('followingId'); const followingIds = following.map(f => f.followingId.toString()); const allStories = await Story.find({ expiresAt: { $gt: new Date() }, $or: [ { visibility: 'public' }, { userId: { $in: followingIds }, visibility: { $in: ['public', 'followers'] } }, { userId: context.user.userId } ] }) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .populate('userId') .lean(); const storiesMap = {}; const userIds = new Set(); allStories.forEach(story => { const userId = story.userId._id.toString(); userIds.add(userId); if (!storiesMap[userId]) { storiesMap[userId] = { user: story.userId, stories: [], isFollowing: followingIds.includes(userId), isCurrentUser: userId === context.user.userId }; } const hasViewed = story.views?.some(v => v.userId?.toString() === context.user.userId) || false; storiesMap[userId].stories.push({ ...story, viewsCount: (story.views || []).length, reactionsCount: (story.reactions || []).length, hasViewed: hasViewed, textColor: story.textColor || '#ffffff', userId: story.userId, allowDownload: story.allowDownload || false, filter: story.filter || 'none', // ✅ أضف هذا filterIntensity: story.filterIntensity || 0.5, // ✅ أضف هذا filterApplied: story.filterApplied || false, // ✅ أضف هذا audio: story.audio || { url: null, publicId: null, duration: 0 }, // ✅ أضف هذا textStyle: story.textStyle || { // ✅ أضف هذا fontSize: 32, fontFamily: 'Inter', fontWeight: '700', textAlign: 'center', position: { x: 50, y: 50 }, rotation: 0, opacity: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)', background: 'transparent', padding: '0', borderRadius: '0', maxWidth: 90 } }); }); const sortedGroups = Object.values(storiesMap).sort((a, b) => { if (a.isFollowing && !b.isFollowing) return -1; if (!a.isFollowing && b.isFollowing) return 1; if (a.isCurrentUser && !b.isCurrentUser) return -1; if (!a.isCurrentUser && b.isCurrentUser) return 1; return b.stories.length - a.stories.length; }); const paginatedData = sortedGroups.slice(0, limit); const total = sortedGroups.length; const totalStories = allStories.length; const followingStories = allStories.filter(s => followingIds.includes(s.userId._id.toString()) ).length; return { data: paginatedData, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 }, stats: { totalStories, myStories: allStories.filter(s => s.userId._id.toString() === context.user.userId).length, followingStories, discoverStories: totalStories - followingStories, followingCount: followingIds.length } }; }, // ✅ storiesDiscover - قصص جديدة للاكتشاف storiesDiscover: async (parent, { limit = 10 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }).select('followingId'); const followingIds = following.map(f => f.followingId); followingIds.push(context.user.userId); const discoverStories = await Story.find({ userId: { $nin: followingIds }, visibility: 'public', expiresAt: { $gt: new Date() } }) .sort({ createdAt: -1 }) .limit(parseInt(limit)) .populate('userId') .lean(); const storiesByUser = {}; discoverStories.forEach(story => { const userId = story.userId._id.toString(); if (!storiesByUser[userId]) { storiesByUser[userId] = { user: story.userId, stories: [], isFollowing: false }; } const hasViewed = story.views?.some(v => v.userId?.toString() === context.user.userId) || false; storiesByUser[userId].stories.push({ ...story, viewsCount: (story.views || []).length, reactionsCount: (story.reactions || []).length, hasViewed: hasViewed, textColor: story.textColor || '#ffffff', userId: story.userId, allowDownload: story.allowDownload || false, filter: story.filter || 'none', // ✅ أضف هذا filterIntensity: story.filterIntensity || 0.5, // ✅ أضف هذا filterApplied: story.filterApplied || false, // ✅ أضف هذا audio: story.audio || { url: null, publicId: null, duration: 0 }, // ✅ أضف هذا textStyle: story.textStyle || { // ✅ أضف هذا fontSize: 32, fontFamily: 'Inter', fontWeight: '700', textAlign: 'center', position: { x: 50, y: 50 }, rotation: 0, opacity: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)', background: 'transparent', padding: '0', borderRadius: '0', maxWidth: 90 } }); }); return Object.values(storiesByUser); }, // ✅ storyStats - إحصائيات القصص storyStats: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }).select('followingId'); const followingIds = following.map(f => f.followingId); const [totalStories, myStories, followingStories, discoverStories] = await Promise.all([ Story.countDocuments({ expiresAt: { $gt: new Date() } }), Story.countDocuments({ userId: context.user.userId, expiresAt: { $gt: new Date() } }), Story.countDocuments({ userId: { $in: followingIds }, expiresAt: { $gt: new Date() }, visibility: { $in: ['public', 'followers'] } }), Story.countDocuments({ userId: { $nin: [...followingIds, context.user.userId] }, expiresAt: { $gt: new Date() }, visibility: 'public' }) ]); return { totalStories, myStories, followingStories, discoverStories, followingCount: followingIds.length }; }, // ✅ userStories - قصص مستخدم معين userStories: async (parent, { userId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const isOwner = userId === context.user.userId; const isFollowing = await Follow.exists({ followerId: context.user.userId, followingId: userId, status: 'accepted' }); let visibilityQuery = { userId, expiresAt: { $gt: new Date() } }; if (!isOwner) { visibilityQuery.$or = [ { visibility: 'public' }, { visibility: 'followers' } ]; if (!isFollowing) { visibilityQuery.visibility = 'public'; } } const stories = await Story.find(visibilityQuery) .sort({ createdAt: -1 }) .populate('userId') .lean(); return stories.map(story => ({ ...story, viewsCount: (story.views || []).length, reactionsCount: (story.reactions || []).length, hasViewed: story.views?.some(v => v.userId?.toString() === context.user.userId) || false, userId: story.userId, textColor: story.textColor || '#ffffff', allowDownload: story.allowDownload || false, filter: story.filter || 'none', // ✅ أضف هذا filterIntensity: story.filterIntensity || 0.5, // ✅ أضف هذا filterApplied: story.filterApplied || false, // ✅ أضف هذا audio: story.audio || { url: null, publicId: null, duration: 0 }, // ✅ أضف هذا textStyle: story.textStyle || { // ✅ أضف هذا fontSize: 32, fontFamily: 'Inter', fontWeight: '700', textAlign: 'center', position: { x: 50, y: 50 }, rotation: 0, opacity: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)', background: 'transparent', padding: '0', borderRadius: '0', maxWidth: 90 } })); }, // ✅ storyViewers - جلب مشاهدين القصة (للمالك فقط) storyViewers: async (parent, { storyId, page = 1, limit = 20 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const story = await Story.findById(storyId); if (!story) throw new GraphQLError('Story not found'); if (story.userId.toString() !== context.user.userId) { throw new GraphQLError('You are not authorized to view viewers of this story'); } const skip = (page - 1) * limit; const views = story.views || []; const total = views.length; const viewerIds = views.map(v => v.userId); const paginatedIds = viewerIds.slice(skip, skip + limit); const viewers = await User.find({ _id: { $in: paginatedIds } }); const viewersWithTime = paginatedIds.map((id) => { const user = viewers.find(u => u._id.toString() === id.toString()); const view = views.find(v => v.userId.toString() === id.toString()); return { userId: user || { _id: id, username: 'Unknown', profile: {} }, viewedAt: view?.viewedAt || new Date() }; }); return { data: viewersWithTime, total, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, // ✅ story - قصة واحدة story: async (parent, { id }, context) => { const story = await Story.findById(id) .populate('userId') .lean(); if (!story) throw new GraphQLError('Story not found'); if (story.expiresAt && new Date(story.expiresAt) < new Date()) { throw new GraphQLError('Story has expired'); } const isOwner = context.user && context.user.userId === story.userId._id.toString(); const isFollowing = context.user ? await Follow.exists({ followerId: context.user.userId, followingId: story.userId._id, status: 'accepted' }) : false; if (!isOwner && story.visibility === 'only-me') { throw new GraphQLError('This story is private'); } if (story.visibility === 'followers' && !isOwner && !isFollowing) { throw new GraphQLError('This story is only visible to followers'); } return { ...story, viewsCount: (story.views || []).length, reactionsCount: (story.reactions || []).length, hasViewed: story.views?.some(v => v.userId?.toString() === context.user.userId) || false, userId: story.userId, textColor: story.textColor || '#ffffff', allowDownload: story.allowDownload || false, filter: story.filter || 'none', // ✅ أضف هذا filterIntensity: story.filterIntensity || 0.5, // ✅ أضف هذا filterApplied: story.filterApplied || false, // ✅ أضف هذا audio: story.audio || { url: null, publicId: null, duration: 0 }, // ✅ أضف هذا textStyle: story.textStyle || { // ✅ أضف هذا fontSize: 32, fontFamily: 'Inter', fontWeight: '700', textAlign: 'center', position: { x: 50, y: 50 }, rotation: 0, opacity: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)', background: 'transparent', padding: '0', borderRadius: '0', maxWidth: 90 } }; }, // ============================================ // 🔹 VIDEOS QUERIES // ============================================ // ✅ videosFeed - جلب كل الفيديوهات videosFeed: async (parent, { page = 1, limit = 20, type = 'latest' }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; // ✅ شرط وجود فيديو let query = { 'video.url': { $exists: true, $ne: null }, visibility: { $in: ['public', 'followers'] } }; let sort = { createdAt: -1 }; if (type === 'trending') { // ✅ الترند: الأكثر مشاهدة + إعجابات sort = { viewsCount: -1, likesCount: -1 }; } else if (type === 'following') { // ✅ فقط من المتابعين const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }).select('followingId'); const followingIds = following.map(f => f.followingId); followingIds.push(context.user.userId); query.userId = { $in: followingIds }; } // ✅ جلب الفيديوهات const videos = await Post.find(query) .sort(sort) .skip(skip) .limit(limit) .populate('userId', 'username profile.nickname profile.avatar profile.jobTitle profile.bio') .lean(); // ✅ حساب الإحصائيات لكل فيديو const videosWithStats = await Promise.all(videos.map(async (video) => { const viewsCount = video.views?.length || 0; const likesCount = video.likes?.length || 0; const commentsCount = video.comments?.length || 0; const sharesCount = video.shares?.length || 0; const savesCount = video.saves?.length || 0; const isLiked = video.likes?.some(l => l.userId?.toString() === context.user.userId) || false; const isSaved = video.saves?.some(s => s.userId?.toString() === context.user.userId) || false; // ✅ التحقق من المتابعة let isFollowing = false; if (video.userId && context.user) { isFollowing = await Follow.exists({ followerId: context.user.userId, followingId: video.userId._id, status: 'accepted' }); } return { ...video, viewsCount, likesCount, commentsCount, sharesCount, savesCount, isLiked, isSaved, isFollowing: !!isFollowing }; })); // ✅ جلب الفيديوهات الترند (للسايدبار) const trending = await Post.find({ 'video.url': { $exists: true, $ne: null }, visibility: 'public' }) .sort({ viewsCount: -1, likesCount: -1 }) .limit(5) .populate('userId', 'username profile.nickname profile.avatar') .lean() .then(videos => videos.map(v => ({ ...v, viewsCount: v.views?.length || 0, likesCount: v.likes?.length || 0 }))); const total = await Post.countDocuments(query); return { data: videosWithStats, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 }, trending }; }, // ✅ video - فيديو واحد video: async (parent, { id }, context) => { const video = await Post.findOne({ _id: id, 'video.url': { $exists: true, $ne: null } }) .populate('userId', 'username profile.nickname profile.avatar profile.jobTitle profile.bio') .lean(); if (!video) throw new GraphQLError('Video not found'); // ✅ زيادة المشاهدات (غير متزامن) if (context.user) { Post.findByIdAndUpdate(id, { $push: { views: { userId: context.user.userId, viewedAt: new Date() } } }).catch(() => {}); } const viewsCount = video.views?.length || 0; const likesCount = video.likes?.length || 0; const commentsCount = video.comments?.length || 0; const sharesCount = video.shares?.length || 0; const savesCount = video.saves?.length || 0; const isLiked = video.likes?.some(l => l.userId?.toString() === context.user?.userId) || false; const isSaved = video.saves?.some(s => s.userId?.toString() === context.user?.userId) || false; let isFollowing = false; if (video.userId && context.user) { isFollowing = await Follow.exists({ followerId: context.user.userId, followingId: video.userId._id, status: 'accepted' }); } return { ...video, viewsCount, likesCount, commentsCount, sharesCount, savesCount, isLiked, isSaved, isFollowing: !!isFollowing }; }, // ===== Jobs ===== jobs: async (parent, { page = 1, limit = 20, search, category, jobType, location }) => { const skip = (page - 1) * limit; let query = { status: 'active', deadline: { $gt: new Date() } }; if (search) query.$text = { $search: search }; if (category) query.category = category; if (jobType) query.jobType = jobType; if (location) query.location = { $regex: location, $options: 'i' }; const [jobs, total, featured] = await Promise.all([ Job.find(query) .sort({ postedAt: -1 }) .skip(skip) .limit(limit) .populate('employerId', 'username profile.nickname profile.avatar') .lean(), Job.countDocuments(query), Job.find({ ...query, isFeatured: true, featuredUntil: { $gt: new Date() } }) .sort({ postedAt: -1 }) .limit(3) .populate('employerId', 'username profile.nickname profile.avatar') .lean() ]); return { data: jobs, featured, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, job: async (parent, { id }, context) => { const job = await Job.findById(id) .populate('employerId', 'username profile.nickname profile.avatar profile.bio profile.phone') .lean(); if (!job) throw new GraphQLError('Job not found'); // زيادة عدد المشاهدات (غير متزامن) Job.findByIdAndUpdate(id, { $inc: { views: 1 } }).catch(() => {}); let hasApplied = false; let applicationStatus = null; let isSaved = false; if (context.user) { const application = await JobApplication.findOne({ jobId: id, applicantId: context.user.userId }); if (application) { hasApplied = true; applicationStatus = application.status; } const saved = await JobSave.findOne({ jobId: id, userId: context.user.userId }); isSaved = !!saved; } return { ...job, hasApplied, applicationStatus, isSaved }; }, myJobs: async (parent, { page = 1, limit = 20, status }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; let query = { employerId: context.user.userId }; if (status) query.status = status; const [jobs, total] = await Promise.all([ Job.find(query) .sort({ postedAt: -1 }) .skip(skip) .limit(limit) .lean(), Job.countDocuments(query) ]); const jobsWithStats = await Promise.all(jobs.map(async (job) => { const applicationsCount = await JobApplication.countDocuments({ jobId: job._id }); const pendingCount = await JobApplication.countDocuments({ jobId: job._id, status: 'pending' }); return { ...job, applicationsCount, pendingCount }; })); return { data: jobsWithStats, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, myJobApplications: async (parent, { page = 1, limit = 20, status }, context) => { // ✅ لو مش مسجل، أرجع بيانات فارغة if (!context.user) { return { data: [], pagination: { page: 1, limit: 20, total: 0, pages: 0, hasNext: false, hasPrev: false } }; } const skip = (page - 1) * limit; let query = { applicantId: context.user.userId }; if (status) query.status = status; const [applications, total] = await Promise.all([ JobApplication.find(query) .sort({ appliedAt: -1 }) .skip(skip) .limit(limit) .populate('jobId', 'title companyName location jobType salaryMin salaryMax deadline status') .lean(), JobApplication.countDocuments(query) ]); // ✅ التأكد من أن pagination مش null return { data: applications || [], pagination: { page: page || 1, limit: limit || 20, total: total || 0, pages: Math.ceil((total || 0) / (limit || 20)), hasNext: skip + (limit || 20) < (total || 0), hasPrev: page > 1 } }; }, jobApplications: async (parent, { jobId, page = 1, limit = 20, status }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const job = await Job.findById(jobId); if (!job) throw new GraphQLError('Job not found'); if (job.employerId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to view applications for this job'); } const skip = (page - 1) * limit; let query = { jobId }; if (status) query.status = status; const [applications, total] = await Promise.all([ JobApplication.find(query) .sort({ appliedAt: -1 }) .skip(skip) .limit(limit) .populate('applicantId', 'username profile.nickname profile.avatar profile.bio profile.skills') .lean(), JobApplication.countDocuments(query) ]); return { data: applications, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, jobApplication: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const application = await JobApplication.findById(id) .populate('jobId') .populate('applicantId', 'username profile.nickname profile.avatar profile.bio profile.skills profile.education profile.experience') .lean(); if (!application) throw new GraphQLError('Application not found'); const isOwner = application.applicantId._id.toString() === context.user.userId; const isEmployer = application.jobId.employerId.toString() === context.user.userId; if (!isOwner && !isEmployer && !context.isAdmin) { throw new GraphQLError('Unauthorized to view this application'); } return application; }, savedJobs: async (parent, { page = 1, limit = 20 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; const savedJobs = await JobSave.find({ userId: context.user.userId }) .sort({ savedAt: -1 }) .skip(skip) .limit(limit) .populate('jobId') .lean(); const jobs = savedJobs.map(s => s.jobId).filter(j => j); const total = await JobSave.countDocuments({ userId: context.user.userId }); return { data: jobs, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, jobCategories: async (parent, { lang = 'en' }, context) => { const categories = await JobCategory.find({ isActive: true }).sort({ order: 1 }); const categoriesWithStats = await Promise.all(categories.map(async (cat) => { const count = await Job.countDocuments({ category: cat.name, status: 'active', deadline: { $gt: new Date() } }); return { ...cat.toObject(), displayName: lang === 'ar' ? (cat.nameAr || cat.name) : cat.name, count }; })); return categoriesWithStats; }, jobStats: async () => { const [totalJobs, totalCompanies, remoteJobsCount] = await Promise.all([ Job.countDocuments({ status: 'active', deadline: { $gt: new Date() } }), Job.distinct('employerId').then(arr => arr.length), Job.countDocuments({ isRemote: true, status: 'active', deadline: { $gt: new Date() } }) ]); const topCategories = await Job.aggregate([ { $match: { status: 'active', deadline: { $gt: new Date() } } }, { $group: { _id: '$category', count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $limit: 5 } ]); return { totalJobs, totalCompanies, remoteJobs: remoteJobsCount, topCategories: topCategories.map(c => ({ category: c._id, count: c.count })) }; }, // ===== Store ===== myStore: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const storeSettings = await StoreSettings.findOne({ userId: context.user.userId }); if (!storeSettings) { return { _id: context.user.userId, userId: context.user.userId, enabled: false, currency: 'USD', currencySymbol: '$', productsCount: 0, followersCount: 0, averageRating: 0, totalSales: 0, totalOrders: 0, createdAt: new Date(), updatedAt: new Date() }; } const user = await User.findById(context.user.userId); return { _id: context.user.userId, userId: context.user.userId, storeName: storeSettings.storeName || user?.profile?.nickname || user?.username, storeLogo: storeSettings.storeLogo, storeBanner: storeSettings.storeBanner, storeDescription: storeSettings.storeDescription, contactEmail: storeSettings.contactEmail || user?.email, enabled: storeSettings.enabled || false, currency: storeSettings.currency || 'USD', currencySymbol: storeSettings.currencySymbol || '$', productsCount: await Product.countDocuments({ userId: context.user.userId, isActive: true }), followersCount: await StoreFollow.countDocuments({ storeId: context.user.userId }), averageRating: storeSettings.averageRating || 0, totalSales: 0, totalOrders: 0, createdAt: storeSettings.createdAt, updatedAt: storeSettings.updatedAt }; }, store: async (parent, { username }) => { const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) throw new GraphQLError('Store not found'); const storeSettings = await StoreSettings.findOne({ userId: user._id }); if (!storeSettings || !storeSettings.enabled) { throw new GraphQLError('Store is not active'); } return { _id: user._id, userId: user._id, storeName: storeSettings.storeName || user.profile?.nickname || user.username, storeLogo: storeSettings.storeLogo, storeBanner: storeSettings.storeBanner, storeDescription: storeSettings.storeDescription, contactEmail: storeSettings.contactEmail || user.email, enabled: storeSettings.enabled, currency: storeSettings.currency || 'USD', currencySymbol: storeSettings.currencySymbol || '$', productsCount: await Product.countDocuments({ userId: user._id, isActive: true }), followersCount: await StoreFollow.countDocuments({ storeId: user._id }), averageRating: storeSettings.averageRating || 0, totalSales: 0, totalOrders: 0, createdAt: storeSettings.createdAt, updatedAt: storeSettings.updatedAt }; }, storeProducts: async (parent, { username, tags, limit = 50 }) => { const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) throw new GraphQLError('Store not found'); const storeSettings = await StoreSettings.findOne({ userId: user._id }); if (!storeSettings || !storeSettings.enabled) { throw new GraphQLError('Store is not active'); } let query = { userId: user._id, isActive: true }; if (tags && tags.length > 0) { query.tags = { $in: tags }; } const products = await Product.find(query) .select('title description price type images tags salesCount averageRating reviews deliveryTime') .sort({ createdAt: -1 }) .limit(limit); return products; }, storeProduct: async (parent, { id }) => { const product = await Product.findById(id) .populate('userId', 'username profile.nickname profile.avatar') .lean(); if (!product) throw new GraphQLError('Product not found'); if (!product.isActive) throw new GraphQLError('Product is not active'); return product; }, myProducts: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); return await Product.find({ userId: context.user.userId }) .sort({ createdAt: -1 }); }, storePages: async (parent, { username }) => { const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) throw new GraphQLError('Store not found'); const defaultPages = [ { slug: 'products', title: 'Products', type: 'default', isEnabled: true, order: 1 }, { slug: 'about', title: 'About Us', type: 'default', isEnabled: true, order: 2 }, { slug: 'contact', title: 'Contact', type: 'default', isEnabled: true, order: 3 } ]; const customPages = await Page.find({ storeId: user._id, isEnabled: true }) .sort({ order: 1 }) .select('slug title type order'); return [...defaultPages, ...customPages]; }, storePage: async (parent, { username, slug }) => { const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) throw new GraphQLError('Store not found'); if (slug === 'products') { return { slug: 'products', title: 'Products', type: 'default', isEnabled: true, isDefault: true, content: '', css: '', js: '' }; } if (slug === 'about') { const storeSettings = await StoreSettings.findOne({ userId: user._id }); return { slug: 'about', title: 'About Us', type: 'default', isEnabled: true, isDefault: true, content: storeSettings?.storeDescription || 'Welcome to our store!', css: '', js: '' }; } if (slug === 'contact') { const storeSettings = await StoreSettings.findOne({ userId: user._id }); return { slug: 'contact', title: 'Contact Us', type: 'default', isEnabled: true, isDefault: true, content: `

Email: ${storeSettings?.contactEmail || user.email}

Store: ${storeSettings?.storeName || user.profile?.nickname || user.username}

`, css: '', js: '' }; } const page = await Page.findOne({ storeId: user._id, slug, isEnabled: true }); if (!page) throw new GraphQLError('Page not found'); return page; }, myPages: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); return await Page.find({ storeId: context.user.userId }) .sort({ order: 1 }); }, page: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const page = await Page.findOne({ _id: id, storeId: context.user.userId }); if (!page) throw new GraphQLError('Page not found'); return page; }, storeSettings: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); let settings = await StoreSettings.findOne({ userId: context.user.userId }); if (!settings) { settings = new StoreSettings({ userId: context.user.userId }); await settings.save(); } let theme = await StoreTheme.findOne({ userId: context.user.userId }); if (!theme) { theme = new StoreTheme({ userId: context.user.userId }); await theme.save(); } return { settings, theme }; }, storeAnalytics: async (parent, { username }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${username}$`, $options: 'i' } }, { username: { $regex: `^${username}$`, $options: 'i' } } ] }); if (!user) throw new GraphQLError('Store not found'); if (user._id.toString() !== context.user.userId) { throw new GraphQLError('Unauthorized'); } let analytics = await ProfileAnalytics.findOne({ userId: user._id }); if (!analytics) { analytics = { totalViews: 0, uniqueViews: 0, profileViews: [], dailyStats: {}, monthlyStats: {}, referralSources: {} }; } const ordersStats = await Order.aggregate([ { $match: { sellerId: user._id } }, { $group: { _id: null, totalOrders: { $sum: 1 }, completedOrders: { $sum: { $cond: [{ $eq: ['$status', 'completed'] }, 1, 0] } }, totalRevenue: { $sum: { $cond: [{ $eq: ['$status', 'completed'] }, '$totalAmount', 0] } }, pendingOrders: { $sum: { $cond: [{ $eq: ['$status', 'pending'] }, 1, 0] } } }} ]); const productsStats = await Product.aggregate([ { $match: { userId: user._id } }, { $group: { _id: '$type', count: { $sum: 1 }, totalSales: { $sum: '$salesCount' } }} ]); const followersCount = await StoreFollow.countDocuments({ storeId: user._id }); const topProducts = await Product.find({ userId: user._id, isActive: true, salesCount: { $gt: 0 } }) .sort({ salesCount: -1 }) .limit(5) .select('title salesCount price images'); // آخر 7 أيام const last7Days = []; for (let i = 6; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; last7Days.push({ date: dateStr, views: analytics.dailyStats?.[dateStr]?.views || 0, uniqueViews: analytics.dailyStats?.[dateStr]?.uniqueViews || 0, orders: analytics.dailyStats?.[dateStr]?.orders || 0, revenue: analytics.dailyStats?.[dateStr]?.revenue || 0 }); } // آخر 6 أشهر const last6Months = []; for (let i = 5; i >= 0; i--) { const date = new Date(); date.setMonth(date.getMonth() - i); const monthStr = date.toISOString().slice(0, 7); last6Months.push({ month: monthStr, views: analytics.monthlyStats?.[monthStr]?.views || 0, uniqueViews: analytics.monthlyStats?.[monthStr]?.uniqueViews || 0, orders: analytics.monthlyStats?.[monthStr]?.orders || 0, revenue: analytics.monthlyStats?.[monthStr]?.revenue || 0 }); } return { totalViews: analytics.totalViews || 0, uniqueViews: analytics.uniqueViews || 0, followersCount, orders: { total: ordersStats[0]?.totalOrders || 0, completed: ordersStats[0]?.completedOrders || 0, pending: ordersStats[0]?.pendingOrders || 0, totalRevenue: ordersStats[0]?.totalRevenue || 0 }, products: { total: await Product.countDocuments({ userId: user._id, isActive: true }), digital: productsStats.find(p => p._id === 'digital')?.count || 0, projects: productsStats.find(p => p._id === 'project')?.count || 0, services: productsStats.find(p => p._id === 'service')?.count || 0, totalSales: productsStats.reduce((sum, p) => sum + p.totalSales, 0) }, topProducts: topProducts.map(p => ({ _id: p._id, title: p.title, salesCount: p.salesCount, price: p.price, image: p.images?.[0] || null })), last7Days, last6Months, referralSources: analytics.referralSources || { direct: 0, google: 0, facebook: 0, twitter: 0, linkedin: 0, instagram: 0, github: 0, mgzon: 0, search: 0, other: 0 }, recentViews: (analytics.profileViews || []).slice(-10).reverse().map(view => ({ viewerName: view.viewerName || 'Guest', deviceType: view.deviceType || 'unknown', browser: view.browser || 'unknown', timestamp: view.timestamp, referer: view.referer || 'direct' })) }; }, // ===== Cart ===== cart: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); let cart = await Cart.findOne({ userId: context.user.userId }); if (!cart) { cart = new Cart({ userId: context.user.userId, stores: new Map() }); await cart.save(); } return cart; }, cartSummary: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const cart = await Cart.findOne({ userId: context.user.userId }); if (!cart) { return { totalItems: 0, totalStores: 0, stores: [] }; } const stores = []; for (let [storeId, storeData] of cart.stores) { stores.push({ storeId, storeName: storeData.storeName, itemCount: storeData.itemCount, subtotal: storeData.subtotal }); } return { totalItems: cart.totalItems, totalStores: cart.totalStores, stores }; }, // ===== Orders ===== myOrders: async (parent, { as, page = 1, limit = 20 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; let query = {}; if (as === 'buyer') { query.buyerId = context.user.userId; } else if (as === 'seller') { query.sellerId = context.user.userId; } else { query = { $or: [ { buyerId: context.user.userId }, { sellerId: context.user.userId } ] }; } const [orders, total] = await Promise.all([ Order.find(query) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .populate('buyerId', 'username profile.nickname profile.avatar') .populate('sellerId', 'username profile.nickname profile.avatar') .lean(), Order.countDocuments(query) ]); return { data: orders, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, order: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const order = await Order.findById(id) .populate('buyerId', 'username email profile.nickname') .populate('sellerId', 'username profile.nickname') .lean(); if (!order) throw new GraphQLError('Order not found'); if (order.buyerId._id.toString() !== context.user.userId && order.sellerId._id.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized'); } return order; }, // ============================================ // 🔹 QUERY RESOLVERS - Follow (النسخة النهائية) // ============================================ // ✅ يفضل كما هو (مش هتغيره) followers: async (parent, { userId, page = 1, limit = 20 }, context) => { const skip = (page - 1) * limit; const followers = await Follow.find({ followingId: userId, status: 'accepted' }) .skip(skip) .limit(limit) .populate('followerId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(); const followersUsers = await Promise.all(followers.map(async (f) => ({ _id: f.followerId._id, username: f.followerId.username, profile: f.followerId.profile, isFollowing: context.user ? await Follow.exists({ followerId: context.user.userId, followingId: f.followerId._id, status: 'accepted' }) : false }))); const total = await Follow.countDocuments({ followingId: userId, status: 'accepted' }); return { data: followersUsers, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, // ✅ يفضل كما هو (مش هتغيره) following: async (parent, { userId, page = 1, limit = 20 }, context) => { const skip = (page - 1) * limit; const following = await Follow.find({ followerId: userId, status: 'accepted' }) .skip(skip) .limit(limit) .populate('followingId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(); const followingUsers = await Promise.all(following.map(async (f) => ({ _id: f.followingId._id, username: f.followingId.username, profile: f.followingId.profile, isFollowing: context.user ? await Follow.exists({ followerId: context.user.userId, followingId: f.followingId._id, status: 'accepted' }) : false }))); const total = await Follow.countDocuments({ followerId: userId, status: 'accepted' }); return { data: followingUsers, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, // ✅ 🔥 هذا هو اللي هيتغير (استبدله بالكود الجديد) myFollowers: async (parent, { page = 1, limit = 20 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; const followers = await Follow.find({ followingId: context.user.userId, status: 'accepted' }) .skip(skip) .limit(limit) .populate('followerId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(); const followersUsers = await Promise.all(followers.map(async (f) => ({ _id: f.followerId._id, username: f.followerId.username, profile: f.followerId.profile, isFollowing: await Follow.exists({ followerId: context.user.userId, followingId: f.followerId._id, status: 'accepted' }) }))); const total = await Follow.countDocuments({ followingId: context.user.userId, status: 'accepted' }); return { data: followersUsers, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, // ✅ 🔥 هذا هو اللي هيتغير (استبدله بالكود الجديد) myFollowing: async (parent, { page = 1, limit = 20 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; const following = await Follow.find({ followerId: context.user.userId, status: 'accepted' }) .skip(skip) .limit(limit) .populate('followingId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(); const followingUsers = await Promise.all(following.map(async (f) => ({ _id: f.followingId._id, username: f.followingId.username, profile: f.followingId.profile, isFollowing: await Follow.exists({ followerId: context.user.userId, followingId: f.followingId._id, status: 'accepted' }) }))); const total = await Follow.countDocuments({ followerId: context.user.userId, status: 'accepted' }); return { data: followingUsers, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, // ✅ يفضل كما هو (مش هتغيره) followRequests: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const requests = await Follow.find({ followingId: context.user.userId, status: 'pending' }).populate('followerId', 'username profile.nickname profile.avatar profile.jobTitle'); return requests.map(req => ({ _id: req._id, requester: req.followerId, requestedAt: req.createdAt })); }, // ✅ يفضل كما هو (مش هتغيره) followStatus: async (parent, { userId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const follow = await Follow.findOne({ followerId: context.user.userId, followingId: userId, status: 'accepted' }); return { isFollowing: !!follow }; }, // ===== Notifications ===== notifications: async (parent, { page = 1, limit = 30 }, context) => { // ✅ لو مش مسجل، أرجع بيانات فارغة بدل ما ترمي إيرور if (!context.user) { return { data: [], pagination: { page: 1, limit: 30, total: 0, pages: 0, hasNext: false, hasPrev: false }, unreadCount: 0 }; } const skip = (page - 1) * limit; const [notifications, total] = await Promise.all([ Notification.find({ userId: context.user.userId }) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .populate('actorId', 'username profile.nickname profile.avatar') .lean(), Notification.countDocuments({ userId: context.user.userId }) ]); const unreadCount = await Notification.countDocuments({ userId: context.user.userId, read: false }); // ✅ التأكد من أن pagination مش null return { data: notifications || [], pagination: { page: page || 1, limit: limit || 30, total: total || 0, pages: Math.ceil((total || 0) / (limit || 30)), hasNext: skip + (limit || 30) < (total || 0), hasPrev: page > 1 }, unreadCount: unreadCount || 0 }; }, unreadNotificationsCount: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); return await Notification.countDocuments({ userId: context.user.userId, read: false }); }, // ===== Ratings ===== userRatings: async (parent, { userId, page = 1, limit = 10 }) => { // ✅ لو مش موجود userId، أرجع بيانات فارغة if (!userId) { return { data: [], pagination: { page: 1, limit: 10, total: 0, pages: 0, hasNext: false, hasPrev: false }, averageRating: 0, totalRatings: 0 }; } const skip = (page - 1) * limit; const [ratings, total, allRatings] = await Promise.all([ Rating.find({ targetUserId: userId }) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .populate('raterUserId', 'username profile.nickname profile.avatar profile.jobTitle') .lean(), Rating.countDocuments({ targetUserId: userId }), Rating.find({ targetUserId: userId }) ]); const averageRating = allRatings.length > 0 ? allRatings.reduce((sum, r) => sum + r.rating, 0) / allRatings.length : 0; // ✅ التأكد من أن pagination مش null return { data: ratings || [], pagination: { page: page || 1, limit: limit || 10, total: total || 0, pages: Math.ceil((total || 0) / (limit || 10)), hasNext: skip + (limit || 10) < (total || 0), hasPrev: page > 1 }, averageRating: averageRating || 0, totalRatings: total || 0 }; }, myRating: async (parent, { targetUserId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const rating = await Rating.findOne({ targetUserId, raterUserId: context.user.userId }); return rating; }, // ===== AI ===== aiConversations: async (parent, { page = 1, limit = 50 }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const skip = (page - 1) * limit; const [data, total] = await Promise.all([ AIConversation.find({ userId: context.user.userId }) .sort({ timestamp: -1 }) .skip(skip) .limit(limit), AIConversation.countDocuments({ userId: context.user.userId }) ]); return { data, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, aiStats: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const [total, last7Days, providerStats, avgResponseTime] = await Promise.all([ AIConversation.countDocuments({ userId: context.user.userId }), AIConversation.aggregate([ { $match: { userId: context.user.userId, timestamp: { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } } }, { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } }, count: { $sum: 1 } } }, { $sort: { _id: 1 } } ]), AIConversation.aggregate([ { $match: { userId: context.user.userId } }, { $group: { _id: '$provider', count: { $sum: 1 } } } ]), AIConversation.aggregate([ { $match: { userId: context.user.userId, responseTime: { $gt: 0 } } }, { $group: { _id: null, avg: { $avg: '$responseTime' } } } ]) ]); const user = await User.findById(context.user.userId).select('profile.aiBot'); const aiConfig = user?.profile?.aiBot || {}; return { total, last7Days: last7Days.map(d => ({ date: d._id, count: d.count })), providerStats: providerStats.map(p => ({ provider: p._id, count: p.count })), avgResponseTime: Math.round(avgResponseTime[0]?.avg || 0), enabled: aiConfig.enabled || false, provider: aiConfig.provider || 'mgzon', totalQueries: aiConfig.totalQueries || 0, lastQueryAt: aiConfig.lastQueryAt || null }; }, aiStatus: async (parent, { nickname }) => { const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${nickname}$`, $options: 'i' } }, { username: { $regex: `^${nickname}$`, $options: 'i' } } ] }).select('profile.aiBot'); if (!user) throw new GraphQLError('User not found'); return { enabled: user.profile?.aiBot?.enabled || false }; }, // ===== Admin ===== adminUsers: async (parent, { page = 1, limit = 20, role }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const skip = (page - 1) * limit; const query = {}; if (role === 'admin') query.isAdmin = true; else if (role === 'user') query.isAdmin = false; const users = await User.find(query) .select('username email isAdmin createdAt profile.nickname profile.avatar profile.storeEnabled') .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(); const usersWithStats = await Promise.all(users.map(async (user) => ({ ...user, stats: { productsCount: await Product.countDocuments({ userId: user._id }), ordersCount: await Order.countDocuments({ buyerId: user._id }), salesCount: await Order.countDocuments({ sellerId: user._id, status: 'completed' }) } }))); const total = await User.countDocuments(query); return { data: usersWithStats, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, adminStores: async (parent, { page = 1, limit = 20 }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const skip = (page - 1) * limit; const stores = await StoreSettings.find({}) .populate('userId', 'username email profile.nickname profile.avatar profile.storeEnabled') .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(); const storesWithStats = await Promise.all(stores.map(async (store) => { const productsCount = await Product.countDocuments({ userId: store.userId._id }); const ordersCount = await Order.countDocuments({ sellerId: store.userId._id }); const completedOrders = await Order.find({ sellerId: store.userId._id, status: 'completed' }); const totalSales = completedOrders.reduce((sum, order) => sum + order.totalAmount, 0); return { _id: store._id, userId: store.userId, storeName: store.storeName || store.userId.profile?.nickname || store.userId.username, storeLogo: store.storeLogo, enabled: store.enabled, productsCount, ordersCount, totalSales, currencySymbol: store.currencySymbol || '$', createdAt: store.createdAt, updatedAt: store.updatedAt }; })); const total = await StoreSettings.countDocuments(); return { data: storesWithStats, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, adminOrders: async (parent, { page = 1, limit = 20, status }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const skip = (page - 1) * limit; const query = {}; if (status && status !== 'all') query.status = status; const [orders, total] = await Promise.all([ Order.find(query) .populate('buyerId', 'username email profile.nickname profile.avatar') .populate('sellerId', 'username email profile.nickname profile.avatar') .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(), Order.countDocuments(query) ]); return { data: orders, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, adminProducts: async (parent, { page = 1, limit = 20, type }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const skip = (page - 1) * limit; const query = {}; if (type && type !== 'all') query.type = type; const [products, total] = await Promise.all([ Product.find(query) .populate('userId', 'username email profile.nickname profile.avatar') .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(), Product.countDocuments(query) ]); return { data: products, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, adminReports: async (parent, { page = 1, limit = 20, status }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const skip = (page - 1) * limit; const query = {}; if (status && status !== 'all') query.status = status; const [reports, total] = await Promise.all([ Report.find(query) .populate('reporterId', 'username email') .populate('resolvedBy', 'username') .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(), Report.countDocuments(query) ]); return { data: reports, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, adminMessageReports: async (parent, { page = 1, limit = 20, status }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const skip = (page - 1) * limit; const query = {}; if (status && status !== 'all') query.status = status; const [reports, total] = await Promise.all([ MessageReport.find(query) .populate('reporterId', 'username profile.nickname profile.avatar') .populate('reportedUserId', 'username profile.nickname profile.avatar') .sort({ reportedAt: -1 }) .skip(skip) .limit(limit) .lean(), MessageReport.countDocuments(query) ]); return { data: reports, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, adminSubscriptions: async (parent, { page = 1, limit = 20, status }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const skip = (page - 1) * limit; const query = {}; if (status && status !== 'all') query.status = status; const [subscriptions, total] = await Promise.all([ UserSubscription.find(query) .populate('userId', 'username email profile.nickname profile.avatar') .populate('planId') .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(), UserSubscription.countDocuments(query) ]); return { data: subscriptions, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, adminSubscriptionStats: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const [total, active, pending, expired, cancelled, earnings] = await Promise.all([ UserSubscription.countDocuments(), UserSubscription.countDocuments({ status: 'active' }), UserSubscription.countDocuments({ status: 'pending' }), UserSubscription.countDocuments({ status: 'expired' }), UserSubscription.countDocuments({ status: 'cancelled' }), SubscriptionEarnings.aggregate([ { $match: { status: 'completed' } }, { $group: { _id: null, total: { $sum: '$amount' } } } ]) ]); const monthlyEarnings = await SubscriptionEarnings.aggregate([ { $match: { status: 'completed' } }, { $group: { _id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } }, total: { $sum: '$amount' }, count: { $sum: 1 } }}, { $sort: { _id: -1 } }, { $limit: 12 } ]); return { total, active, pending, expired, cancelled, totalRevenue: earnings[0]?.total || 0, monthlyEarnings: monthlyEarnings.reverse().map(m => ({ month: m._id, total: m.total, count: m.count })) }; }, adminLogs: async (parent, { page = 1, limit = 50, type }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const skip = (page - 1) * limit; const query = {}; if (type === 'admin') query.type = { $in: ['admin_action', 'admin_report', 'admin_info'] }; else if (type === 'user') query.type = { $nin: ['admin_action', 'admin_report', 'admin_info'] }; const [logs, total] = await Promise.all([ Notification.find(query) .populate('userId', 'username email') .populate('actorId', 'username') .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(), Notification.countDocuments(query) ]); return { data: logs, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: skip + limit < total, hasPrev: page > 1 } }; }, adminPlans: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); return await SubscriptionPlan.find().sort({ order: 1, price: 1 }); }, adminPaymentMethods: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); return await PlatformPaymentMethod.find().sort({ order: 1 }); }, // ============================================ // 🔹 QUERY RESOLVERS - Templates // ============================================ templates: async (parent, { category, sort, limit = 20, offset = 0 }) => { // ✅ تأكد من وجود TemplateStore if (!TemplateStore) { console.warn('⚠️ TemplateStore model not loaded'); return { data: [], pagination: { page: 1, limit, total: 0, pages: 0, hasNext: false, hasPrev: false } }; } const query = { isActive: true }; if (category && category !== 'all') query.category = category; let sortOption = {}; switch (sort) { case 'popular': sortOption = { downloadsCount: -1 }; break; case 'rating': sortOption = { rating: -1 }; break; case 'newest': sortOption = { createdAt: -1 }; break; default: sortOption = { downloadsCount: -1 }; } try { const [templates, total] = await Promise.all([ TemplateStore.find(query) .sort(sortOption) .limit(limit) .skip(offset), TemplateStore.countDocuments(query) ]); return { data: templates, pagination: { page: Math.floor(offset / limit) + 1, limit, total, pages: Math.ceil(total / limit), hasNext: offset + limit < total, hasPrev: offset > 0 } }; } catch (error) { console.error('Templates error:', error); return { data: [], pagination: { page: 1, limit, total: 0, pages: 0, hasNext: false, hasPrev: false } }; } }, featuredTemplates: async (parent, { limit = 6 }) => { // ✅ إضافة isFeatured: true return await TemplateStore.find({ isActive: true, isFeatured: true }) .sort({ downloadsCount: -1 }) .limit(limit); }, popularTemplates: async (parent, { limit = 12 }) => { return await TemplateStore.find({ isActive: true }) .sort({ downloadsCount: -1 }) .limit(limit); }, template: async (parent, { id }) => { let template; if (id.match(/^[0-9a-fA-F]{24}$/)) { template = await TemplateStore.findById(id); } else { template = await TemplateStore.findOne({ slug: id }); } if (!template) throw new GraphQLError('Template not found'); // زيادة عدد المشاهدات (غير متزامن) TemplateStore.incrementViews(template._id).catch(() => {}); return template; }, templateCategories: async () => { return await TemplateStore.aggregate([ { $match: { isActive: true } }, { $group: { _id: '$category', count: { $sum: 1 } } }, { $sort: { count: -1 } } ]); }, myInstalledTemplates: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const templates = await TemplateStore.find({ 'installedBy.userId': context.user.userId, isActive: true }).select('name slug previewImage installedBy'); return templates.map(t => ({ ...t.toObject(), installedAt: t.installedBy.find(i => i.userId.toString() === context.user.userId)?.installedAt })); }, // ===== Site Settings ===== siteSettings: async () => { let settings = await SiteSettings.findOne(); if (!settings) { settings = new SiteSettings(); await settings.save(); } return settings; } }; // ============================================ // 🔹 MUTATION RESOLVERS // ============================================ const Mutation = { // ===== Auth ===== register: async (parent, { username, email, password }) => { try { const existingUser = await User.findOne({ email }); if (existingUser) { throw new GraphQLError('Email already exists'); } const hashedPassword = await bcrypt.hash(password, 10); const user = new User({ username, email, password: hashedPassword, emailVerified: false }); await user.save(); const otp = Math.floor(100000 + Math.random() * 900000).toString(); user.otp = otp; user.otpExpires = Date.now() + 10 * 60 * 1000; await user.save(); try { await sendEmailWithBrevo( email, 'Verify Your Email - MGZon', `

Welcome to MGZon! 🎉

Your verification code is:

${otp}

Valid for 10 minutes.

`, `Your verification code is: ${otp}` ); } catch (e) { console.log('Email error:', e.message); } return { success: true, token: null, refreshToken: null, user: null, requiresVerification: true, email: email, message: 'Registration successful! Please verify your email.' }; } catch (error) { console.error('Register error:', error); if (error instanceof GraphQLError) { throw error; } throw new GraphQLError('Registration failed: ' + error.message); } }, login: async (parent, { email, password }) => { try { const user = await User.findOne({ email }); if (!user) { throw new GraphQLError('Invalid credentials'); } const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { throw new GraphQLError('Invalid credentials'); } // أدمن → توكن مباشر if (user.isAdmin) { const token = jwt.sign( { userId: user._id, isAdmin: user.isAdmin }, process.env.JWT_SECRET, { expiresIn: '30d' } ); const refreshToken = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '90d' } ); user.refreshTokens = user.refreshTokens || []; user.refreshTokens.push({ token: refreshToken, createdAt: new Date() }); await user.save(); return { success: true, token: token, refreshToken: refreshToken, user: user, requiresVerification: false, email: user.email, message: 'Login successful' }; } // إيميل غير مفعل → OTP if (!user.emailVerified) { const otp = Math.floor(100000 + Math.random() * 900000).toString(); user.otp = otp; user.otpExpires = Date.now() + 10 * 60 * 1000; await user.save(); try { await sendEmailWithBrevo( email, 'Verify Your Email - MGZon', `

Verification Code

${otp}

Valid for 10 minutes.

`, `Your verification code is: ${otp}` ); } catch (e) { console.log('Email error:', e.message); } return { success: false, token: null, refreshToken: null, user: null, requiresVerification: true, email: email, message: 'Email not verified. OTP sent to your email.' }; } // ✅ إيميل مفعل → توكن const token = jwt.sign( { userId: user._id, isAdmin: user.isAdmin }, process.env.JWT_SECRET, { expiresIn: '30d' } ); const refreshToken = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '90d' } ); user.refreshTokens = user.refreshTokens || []; user.refreshTokens.push({ token: refreshToken, createdAt: new Date() }); await user.save(); return { success: true, token: token, refreshToken: refreshToken, user: user, requiresVerification: false, email: user.email, message: 'Login successful' }; } catch (error) { console.error('Login error:', error); if (error instanceof GraphQLError) { throw error; } throw new GraphQLError('Login failed: ' + error.message); } }, verifyEmail: async (parent, { email, otp }) => { try { const user = await User.findOne({ email, otp, otpExpires: { $gt: Date.now() } }); if (!user) { throw new GraphQLError('Invalid or expired verification code'); } user.emailVerified = true; user.otp = null; user.otpExpires = null; await user.save(); const token = jwt.sign( { userId: user._id, isAdmin: user.isAdmin }, process.env.JWT_SECRET, { expiresIn: '30d' } ); const refreshToken = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '90d' } ); user.refreshTokens = user.refreshTokens || []; user.refreshTokens.push({ token: refreshToken, createdAt: new Date() }); await user.save(); return { success: true, token: token, refreshToken: refreshToken, user: user, requiresVerification: false, email: user.email, message: 'Email verified successfully!' }; } catch (error) { console.error('Verify email error:', error); if (error instanceof GraphQLError) { throw error; } throw new GraphQLError('Verification failed: ' + error.message); } }, resendVerification: async (parent, { email }) => { try { const user = await User.findOne({ email }); if (!user) throw new GraphQLError('User not found'); if (user.emailVerified) throw new GraphQLError('Email already verified'); const otp = Math.floor(100000 + Math.random() * 900000).toString(); user.otp = otp; user.otpExpires = Date.now() + 10 * 60 * 1000; await user.save(); try { await sendEmailWithBrevo( email, 'Your Verification Code - MGZon', `

Verification Code

${otp}

Valid for 10 minutes.

`, `Your verification code is: ${otp}` ); } catch (e) {} return true; } catch (error) { console.error('Resend verification error:', error); if (error instanceof GraphQLError) { throw error; } throw new GraphQLError('Failed to resend code: ' + error.message); } }, forgotPassword: async (parent, { email }) => { try { const user = await User.findOne({ email }); if (!user) throw new GraphQLError('User not found'); const otp = Math.floor(100000 + Math.random() * 900000).toString(); user.otp = otp; user.otpExpires = Date.now() + 10 * 60 * 1000; await user.save(); try { await sendEmailWithBrevo( email, 'Password Reset Code - MGZon', `

Password Reset

${otp}

Valid for 10 minutes.

`, `Your password reset code is: ${otp}` ); } catch (e) {} return true; } catch (error) { console.error('Forgot password error:', error); if (error instanceof GraphQLError) { throw error; } throw new GraphQLError('Failed to send reset code: ' + error.message); } }, resetPassword: async (parent, { email, otp, newPassword }) => { try { const user = await User.findOne({ email, otp, otpExpires: { $gt: Date.now() } }); if (!user) throw new GraphQLError('Invalid or expired OTP'); user.password = await bcrypt.hash(newPassword, 10); user.otp = null; user.otpExpires = null; await user.save(); return true; } catch (error) { console.error('Reset password error:', error); if (error instanceof GraphQLError) { throw error; } throw new GraphQLError('Failed to reset password: ' + error.message); } }, refreshToken: async (parent, { refreshToken }) => { try { const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET); const user = await User.findOne({ _id: decoded.userId, 'refreshTokens.token': refreshToken }); if (!user) throw new GraphQLError('Invalid refresh token'); const token = jwt.sign( { userId: user._id, isAdmin: user.isAdmin }, process.env.JWT_SECRET, { expiresIn: '30d' } ); const newRefreshToken = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '90d' } ); user.refreshTokens = user.refreshTokens.filter(t => t.token !== refreshToken); user.refreshTokens.push({ token: newRefreshToken }); await user.save(); return { success: true, token: token, refreshToken: newRefreshToken, user: user }; } catch (error) { console.error('Refresh token error:', error); throw new GraphQLError('Invalid or expired refresh token'); } }, logout: async (parent, { refreshToken }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const user = await User.findById(context.user.userId); if (!user) throw new GraphQLError('User not found'); user.refreshTokens = user.refreshTokens.filter(t => t.token !== refreshToken); await user.save(); return true; }, magicLink: async (parent, { email }) => { try { let user = await User.findOne({ email }); if (!user) { user = new User({ email, username: email.split('@')[0], emailVerified: false, profile: { nickname: email.split('@')[0] } }); await user.save(); } const magicToken = jwt.sign( { userId: user._id, email, purpose: 'magic-link' }, process.env.JWT_SECRET, { expiresIn: '15m' } ); const frontendUrl = process.env.FRONTEND_URL_XCV || 'https://xccv.vercel.app'; const magicLink = `${frontendUrl}/auth/callback?token=${magicToken}&provider=magic-link`; try { await sendEmailWithBrevo( email, '✨ Sign in to XCV with Magic Link', `

Welcome!

Sign In

Valid for 15 minutes.

`, `Sign in with this link: ${magicLink}` ); } catch (e) {} return true; } catch (error) { console.error('Magic link error:', error); throw new GraphQLError('Failed to send magic link: ' + error.message); } }, verifyMagicLink: async (parent, { token }) => { try { const decoded = jwt.verify(token, process.env.JWT_SECRET); if (decoded.purpose !== 'magic-link') { throw new GraphQLError('Invalid token purpose'); } const user = await User.findById(decoded.userId); if (!user) throw new GraphQLError('User not found'); user.emailVerified = true; const accessToken = jwt.sign( { userId: user._id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '30d' } ); const refreshToken = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '90d' } ); user.refreshTokens = user.refreshTokens || []; user.refreshTokens.push({ token: refreshToken }); await user.save(); return { success: true, token: accessToken, refreshToken: refreshToken, user: user }; } catch (error) { console.error('Verify magic link error:', error); if (error.name === 'TokenExpiredError') { throw new GraphQLError('Magic link has expired. Please request a new one.'); } throw new GraphQLError('Invalid magic link: ' + error.message); } }, // ===== Comments ===== addComment: async (parent, { projectId, projectOwnerId, rating, text }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); if (!mongoose.Types.ObjectId.isValid(projectId)) { throw new GraphQLError('Invalid project ID'); } if (!mongoose.Types.ObjectId.isValid(projectOwnerId)) { throw new GraphQLError('Invalid project owner ID'); } const project = await Project.findById(projectId); if (!project) throw new GraphQLError('Project not found'); const comment = new Comment({ projectId: new mongoose.Types.ObjectId(projectId), projectOwnerId: new mongoose.Types.ObjectId(projectOwnerId), userId: context.user.userId, rating: rating, text: text, timestamp: new Date(), likes: [], reports: [], reportCount: 0, hidden: false, visibility: 'public' }); await comment.save(); // إشعار لصاحب المشروع if (projectOwnerId !== context.user.userId) { const user = await User.findById(context.user.userId); const notification = new Notification({ userId: projectOwnerId, type: 'comment', actorId: context.user.userId, actorName: user.profile?.nickname || user.username, actorAvatar: user.profile?.avatar, targetId: projectId, targetType: 'project', content: `commented on your project: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}" (${rating}⭐)`, read: false }); await notification.save(); } return comment; }, addCommentReply: async (parent, { commentId, text, rating }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const parentComment = await Comment.findById(commentId).populate('userId', 'email username profile.nickname'); if (!parentComment) throw new GraphQLError('Comment not found'); if (parentComment.hidden) throw new GraphQLError('Cannot reply to a hidden comment'); let finalRating = rating; if (!finalRating || finalRating < 1) { finalRating = parentComment.rating >= 1 ? parentComment.rating : 1; } const reply = new Comment({ projectId: parentComment.projectId, projectOwnerId: parentComment.projectOwnerId, userId: context.user.userId, rating: finalRating, text: text, parentCommentId: commentId, timestamp: new Date(), likes: [], reports: [], reportCount: 0, hidden: false, visibility: parentComment.visibility }); await reply.save(); parentComment.replies.push(reply._id); await parentComment.save(); // إشعار لصاحب التعليق الأصلي if (parentComment.userId._id.toString() !== context.user.userId) { const replyAuthor = await User.findById(context.user.userId); const notification = new Notification({ userId: parentComment.userId._id, type: 'reply', actorId: context.user.userId, actorName: replyAuthor?.profile?.nickname || replyAuthor?.username || 'Someone', targetId: parentComment.projectId, targetType: 'comment', content: `replied to your comment: "${text.substring(0, 60)}${text.length > 60 ? '...' : ''}"`, read: false }); await notification.save(); } return reply; }, updateComment: async (parent, { id, text }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const comment = await Comment.findById(id); if (!comment) throw new GraphQLError('Comment not found'); if (comment.userId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to edit this comment'); } comment.text = text; comment.isEdited = true; comment.editedAt = new Date(); await comment.save(); return comment; }, deleteComment: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const comment = await Comment.findById(id); if (!comment) throw new GraphQLError('Comment not found'); let isProjectOwner = false; if (comment.projectOwnerId) { isProjectOwner = comment.projectOwnerId.toString() === context.user.userId; } else { const project = await Project.findById(comment.projectId); isProjectOwner = project && project.userId.toString() === context.user.userId; } const isCommentOwner = comment.userId.toString() === context.user.userId; if (!isCommentOwner && !context.isAdmin && !isProjectOwner) { throw new GraphQLError('Unauthorized to delete this comment'); } // حذف الردود if (comment.replies && comment.replies.length > 0) { await Comment.deleteMany({ _id: { $in: comment.replies } }); } if (comment.parentCommentId) { await Comment.updateOne( { _id: comment.parentCommentId }, { $pull: { replies: comment._id } } ); } await Comment.findByIdAndDelete(id); return true; }, likeComment: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const comment = await Comment.findById(id); if (!comment) throw new GraphQLError('Comment not found'); if (comment.hidden) throw new GraphQLError('Cannot like a hidden comment'); const userId = context.user.userId; const hasLiked = comment.likes && comment.likes.includes(userId); if (hasLiked) { comment.likes = comment.likes.filter(id => id.toString() !== userId); } else { if (!comment.likes) comment.likes = []; comment.likes.push(userId); } await comment.save(); return { success: true, likesCount: comment.likes.length, isLiked: !hasLiked }; }, reportComment: async (parent, { commentId, reason }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const comment = await Comment.findById(commentId).populate('userId', 'username email profile.nickname'); if (!comment) throw new GraphQLError('Comment not found'); if (comment.userId._id.toString() === context.user.userId) { throw new GraphQLError('You cannot report your own comment'); } if (comment.reports?.some(r => r.userId.toString() === context.user.userId)) { throw new GraphQLError('You already reported this comment'); } comment.reports = comment.reports || []; comment.reports.push({ userId: context.user.userId, reason: reason || 'No reason provided', reportedAt: new Date(), reporterUsername: context.user.username, resolved: false }); comment.reportCount = (comment.reportCount || 0) + 1; let hiddenMessage = ''; if (comment.reportCount >= 3) { comment.hidden = true; hiddenMessage = ' Comment has been hidden.'; } await comment.save(); // إشعار للمشرفين const adminUsers = await User.find({ isAdmin: true }); for (const admin of adminUsers) { const notification = new Notification({ userId: admin._id, type: 'admin_report', actorId: context.user.userId, actorName: context.user.username, targetId: comment._id, targetType: 'comment', content: `🚨 New report on comment by ${comment.userId.username || comment.userId.profile?.nickname}: "${comment.text.substring(0, 80)}${comment.text.length > 80 ? '...' : ''}" (${comment.reportCount}/3)`, link: `/admin/comments?highlight=${comment._id}`, read: false }); await notification.save(); } return { success: true, reportCount: comment.reportCount, hidden: comment.hidden || false, message: comment.reportCount >= 3 ? 'Comment has been hidden due to multiple reports' : 'Report submitted successfully' }; }, resolveCommentReport: async (parent, { commentId, action, adminNote }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const comment = await Comment.findById(commentId).populate('userId', 'username email profile.nickname'); if (!comment) throw new GraphQLError('Comment not found'); if (comment.reports && comment.reports.length > 0) { comment.reports.forEach(r => { r.resolved = true; r.resolvedAt = new Date(); r.resolvedBy = context.user.userId; r.adminNote = adminNote || ''; }); } let actionMessage = ''; if (action === 'hide') { comment.hidden = true; actionMessage = 'Comment has been hidden'; } else if (action === 'delete') { await Comment.findByIdAndDelete(commentId); return comment; } else { comment.hidden = false; actionMessage = 'Comment remains visible'; } await comment.save(); // إشعار لصاحب التعليق if (comment.userId) { const notification = new Notification({ userId: comment.userId._id, type: 'report_resolved', actorId: context.user.userId, actorName: context.user.username, targetId: comment._id, targetType: 'comment', content: `Report on your comment resolved: ${actionMessage}. ${adminNote ? `Note: ${adminNote}` : ''}`, read: false }); await notification.save(); } return comment; }, updateCommentPrivacy: async (parent, { id, visibility }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const comment = await Comment.findById(id); if (!comment) throw new GraphQLError('Comment not found'); if (comment.userId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized'); } comment.visibility = visibility; await comment.save(); return comment; }, // ===== Posts ===== createPost: async (parent, { input, files }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = new Post({ userId: context.user.userId, content: input.content, visibility: input.visibility || 'public', tags: input.tags || [], mentions: input.mentions || [] }); await post.save(); return post; }, updatePost: async (parent, { id, input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findById(id); if (!post) throw new GraphQLError('Post not found'); if (post.userId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to edit this post'); } if (input.content) post.content = input.content; if (input.visibility) post.visibility = input.visibility; if (input.tags) post.tags = input.tags; post.isEdited = true; post.editedAt = new Date(); await post.save(); return post; }, deletePost: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findById(id); if (!post) throw new GraphQLError('Post not found'); if (post.userId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to delete this post'); } await post.deleteOne(); return true; }, likePost: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findById(id); if (!post) throw new GraphQLError('Post not found'); const hasLiked = post.likes.some(l => l.userId.toString() === context.user.userId); if (hasLiked) { post.likes = post.likes.filter(l => l.userId.toString() !== context.user.userId); } else { post.likes.push({ userId: context.user.userId, likedAt: new Date() }); if (post.userId.toString() !== context.user.userId) { const notification = new Notification({ userId: post.userId, type: 'like', actorId: context.user.userId, targetId: post._id, targetType: 'post', content: `liked your post`, read: false }); await notification.save(); } } await post.save(); return { success: true, likesCount: post.likes.length, isLiked: !hasLiked }; }, savePost: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findById(id); if (!post) throw new GraphQLError('Post not found'); const hasSaved = post.saves.some(s => s.userId.toString() === context.user.userId); if (hasSaved) { post.saves = post.saves.filter(s => s.userId.toString() !== context.user.userId); } else { post.saves.push({ userId: context.user.userId, savedAt: new Date() }); } await post.save(); return { success: true, count: post.saves.length, isSaved: !hasSaved }; }, pinPost: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findOne({ _id: id, userId: context.user.userId }); if (!post) throw new GraphQLError('Post not found'); if (!post.pinned) { await Post.updateMany( { userId: context.user.userId, pinned: true }, { $set: { pinned: false, pinnedAt: null } } ); } post.pinned = !post.pinned; post.pinnedAt = post.pinned ? new Date() : null; await post.save(); return post; }, sharePost: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findById(id); if (!post) throw new GraphQLError('Post not found'); const hasShared = post.shares.some(s => s.userId.toString() === context.user.userId); if (!hasShared) { post.shares.push({ userId: context.user.userId, sharedAt: new Date() }); if (post.userId.toString() !== context.user.userId) { const notification = new Notification({ userId: post.userId, type: 'share', actorId: context.user.userId, targetId: post._id, targetType: 'post', content: `shared your post`, read: false }); await notification.save(); } } await post.save(); return { success: true, sharesCount: post.shares.length }; }, sharePostToProfile: async (parent, { originalPostId, content }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const originalPost = await Post.findById(originalPostId) .populate('userId', 'username profile.nickname profile.avatar'); if (!originalPost) throw new GraphQLError('Original post not found'); const sharedContent = content || `Shared a post by ${originalPost.userId.profile?.nickname || originalPost.userId.username}: ${originalPost.content.substring(0, 200)}${originalPost.content.length > 200 ? '...' : ''}`; const sharedPost = new Post({ userId: context.user.userId, content: sharedContent, visibility: 'public', sharedFrom: { originalPostId: originalPost._id, originalAuthorId: originalPost.userId._id, originalAuthorName: originalPost.userId.profile?.nickname || originalPost.userId.username, sharedAt: new Date() }, createdAt: new Date() }); await sharedPost.save(); if (originalPost.userId._id.toString() !== context.user.userId) { const notification = new Notification({ userId: originalPost.userId._id, type: 'share', actorId: context.user.userId, targetId: originalPost._id, targetType: 'post', content: `shared your post on their profile`, read: false }); await notification.save(); } return sharedPost; }, addPostComment: async (parent, { postId, text }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findById(postId); if (!post) throw new GraphQLError('Post not found'); const comment = { userId: context.user.userId, text, createdAt: new Date(), likes: [] }; post.comments.push(comment); await post.save(); if (post.userId.toString() !== context.user.userId) { const notification = new Notification({ userId: post.userId, type: 'comment', actorId: context.user.userId, targetId: post._id, targetType: 'post', content: `commented on your post: "${text.substring(0, 50)}"`, read: false }); await notification.save(); } return comment; }, updatePostComment: async (parent, { postId, commentId, text }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findById(postId); if (!post) throw new GraphQLError('Post not found'); const comment = post.comments.id(commentId); if (!comment) throw new GraphQLError('Comment not found'); if (comment.userId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to edit this comment'); } comment.text = text; comment.edited = true; comment.editedAt = new Date(); await post.save(); return comment; }, deletePostComment: async (parent, { postId, commentId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findById(postId); if (!post) throw new GraphQLError('Post not found'); const comment = post.comments.id(commentId); if (!comment) throw new GraphQLError('Comment not found'); const isCommentOwner = comment.userId.toString() === context.user.userId; const isPostOwner = post.userId.toString() === context.user.userId; if (!isCommentOwner && !isPostOwner && !context.isAdmin) { throw new GraphQLError('Unauthorized to delete this comment'); } post.comments.pull({ _id: commentId }); await post.save(); return true; }, likePostComment: async (parent, { commentId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findOne({ 'comments._id': commentId }); if (!post) throw new GraphQLError('Comment not found'); const comment = post.comments.id(commentId); if (!comment) throw new GraphQLError('Comment not found'); const hasLiked = comment.likes && comment.likes.includes(context.user.userId); if (hasLiked) { comment.likes = comment.likes.filter(id => id.toString() !== context.user.userId); } else { if (!comment.likes) comment.likes = []; comment.likes.push(context.user.userId); } await post.save(); return { success: true, likesCount: comment.likes.length, isLiked: !hasLiked }; }, reportPostComment: async (parent, { commentId, reason }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const post = await Post.findOne({ 'comments._id': commentId }); if (!post) throw new GraphQLError('Comment not found'); const comment = post.comments.id(commentId); if (!comment) throw new GraphQLError('Comment not found'); if (comment.userId.toString() === context.user.userId) { throw new GraphQLError('You cannot report your own comment'); } if (!comment.reports) comment.reports = []; if (comment.reports.some(r => r.userId.toString() === context.user.userId)) { throw new GraphQLError('You already reported this comment'); } comment.reports.push({ userId: context.user.userId, reason: reason || 'No reason provided', reportedAt: new Date() }); comment.reportCount = (comment.reportCount || 0) + 1; if (comment.reportCount >= 3) { comment.hidden = true; } await post.save(); return true; }, // ===== Stories ===== createStory: async (parent, { input, file }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); // ✅ إعدادات النص الافتراضية const defaultTextStyle = { fontSize: 32, fontFamily: 'Inter', fontWeight: '700', textAlign: 'center', position: { x: 50, y: 50 }, rotation: 0, opacity: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)', background: 'transparent', padding: '0', borderRadius: '0', maxWidth: 90 }; const story = new Story({ userId: context.user.userId, text: input.text || '', backgroundColor: input.backgroundColor || '#000000', textColor: input.textColor || '#ffffff', textStyle: input.textStyle ? { ...defaultTextStyle, ...input.textStyle } : defaultTextStyle, visibility: input.visibility || 'followers' }); await story.save(); return story; }, deleteStory: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const story = await Story.findById(id); if (!story) throw new GraphQLError('Story not found'); if (story.userId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to delete this story'); } await story.deleteOne(); return true; }, viewStory: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const story = await Story.findById(id); if (!story) throw new GraphQLError('Story not found'); if (story.isExpired()) throw new GraphQLError('Story has expired'); if (story.userId.toString() === context.user.userId) { return story; } const hasViewed = story.views.some(v => v.userId.toString() === context.user.userId); if (!hasViewed) { story.views.push({ userId: context.user.userId, viewedAt: new Date() }); await story.save(); } return story; }, reactToStory: async (parent, { id, type }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const story = await Story.findById(id); if (!story) throw new GraphQLError('Story not found'); if (story.isExpired()) throw new GraphQLError('Story has expired'); const existingReaction = story.reactions.find( r => r.userId.toString() === context.user.userId ); if (existingReaction) { existingReaction.type = type; existingReaction.createdAt = new Date(); } else { story.reactions.push({ userId: context.user.userId, type: type, createdAt: new Date() }); } await story.save(); return story; }, deleteExpiredStories: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const result = await Story.deleteMany({ expiresAt: { $lt: new Date() } }); return { deletedCount: result.deletedCount }; }, // ===== Jobs ===== createJob: async (parent, { input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const user = await User.findById(context.user.userId); const job = new Job({ ...input, employerId: context.user.userId, companyName: input.companyName || user.profile?.portfolioName || user.username, companyLogo: input.companyLogo || user.profile?.avatar }); await job.save(); return job; }, updateJob: async (parent, { id, input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const job = await Job.findById(id); if (!job) throw new GraphQLError('Job not found'); if (job.employerId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to update this job'); } const allowedFields = [ 'title', 'description', 'requirements', 'responsibilities', 'category', 'jobType', 'experienceLevel', 'location', 'isRemote', 'salaryMin', 'salaryMax', 'salaryCurrency', 'isSalaryNegotiable', 'requiredSkills', 'educationLevel', 'vacancies', 'deadline', 'status' ]; for (const field of allowedFields) { if (input[field] !== undefined) { job[field] = input[field]; } } await job.save(); return job; }, deleteJob: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const job = await Job.findById(id); if (!job) throw new GraphQLError('Job not found'); if (job.employerId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to delete this job'); } await JobApplication.deleteMany({ jobId: job._id }); await JobSave.deleteMany({ jobId: job._id }); await job.deleteOne(); return true; }, duplicateJob: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const originalJob = await Job.findById(id); if (!originalJob) throw new GraphQLError('Job not found'); if (originalJob.employerId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to duplicate this job'); } const newJob = new Job({ ...originalJob.toObject(), _id: undefined, title: `${originalJob.title} (Copy)`, postedAt: new Date(), views: 0, applicationsCount: 0, status: 'draft' }); await newJob.save(); return newJob; }, applyToJob: async (parent, { jobId, input, resume }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const job = await Job.findById(jobId); if (!job) throw new GraphQLError('Job not found'); if (job.status !== 'active' || job.deadline < new Date()) { throw new GraphQLError('This job is no longer accepting applications'); } const existingApplication = await JobApplication.findOne({ jobId: job._id, applicantId: context.user.userId }); if (existingApplication) { throw new GraphQLError('You have already applied for this job'); } const application = new JobApplication({ jobId: job._id, applicantId: context.user.userId, fullName: input.fullName, email: input.email, phone: input.phone, coverLetter: input.coverLetter, portfolio: input.portfolio, linkedin: input.linkedin, github: input.github, answers: input.answers || [] }); await application.save(); await Job.findByIdAndUpdate(job._id, { $inc: { applicationsCount: 1 } }); const applicant = await User.findById(context.user.userId); const notification = new Notification({ userId: job.employerId, type: 'application', content: `New application from ${applicant.profile?.nickname || applicant.username} for "${job.title}"`, targetId: application._id, targetType: 'application', read: false }); await notification.save(); return application; }, withdrawApplication: async (parent, { applicationId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const application = await JobApplication.findById(applicationId); if (!application) throw new GraphQLError('Application not found'); if (application.applicantId.toString() !== context.user.userId) { throw new GraphQLError('Unauthorized to withdraw this application'); } if (application.status !== 'pending') { throw new GraphQLError(`Cannot withdraw application with status: ${application.status}`); } application.status = 'withdrawn'; application.updatedAt = new Date(); await application.save(); return application; }, updateApplicationStatus: async (parent, { applicationId, status, employerNotes }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const application = await JobApplication.findById(applicationId).populate('jobId'); if (!application) throw new GraphQLError('Application not found'); if (application.jobId.employerId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized to update this application'); } application.status = status; if (employerNotes) application.employerNotes = employerNotes; application.updatedAt = new Date(); await application.save(); const notification = new Notification({ userId: application.applicantId, type: 'system', content: `Your application for "${application.jobId.title}" has been updated to: ${status}`, targetId: application._id, targetType: 'application', read: false }); await notification.save(); return application; }, scheduleInterview: async (parent, { applicationId, scheduledAt, type, meetingLink }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const application = await JobApplication.findById(applicationId).populate('jobId'); if (!application) throw new GraphQLError('Application not found'); if (application.jobId.employerId.toString() !== context.user.userId && !context.isAdmin) { throw new GraphQLError('Unauthorized'); } const interview = { scheduledAt: new Date(scheduledAt), type, meetingLink: meetingLink || null, status: 'scheduled' }; application.interviews.push(interview); application.status = 'interview'; await application.save(); const notification = new Notification({ userId: application.applicantId, type: 'system', content: `Interview scheduled for ${application.jobId.title} on ${new Date(scheduledAt).toLocaleString()}`, targetId: application._id, targetType: 'interview', read: false }); await notification.save(); return application; }, saveJob: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const job = await Job.findById(id); if (!job) throw new GraphQLError('Job not found'); const existingSave = await JobSave.findOne({ jobId: job._id, userId: context.user.userId }); if (existingSave) { await existingSave.deleteOne(); return { success: true, count: 0, isSaved: false }; } else { await JobSave.create({ jobId: job._id, userId: context.user.userId }); return { success: true, count: 1, isSaved: true }; } }, adminCreateJobCategory: async (parent, { name, nameAr, icon, color, order }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const category = new JobCategory({ name, nameAr: nameAr || '', icon: icon || 'bx-briefcase', color: color || '#6b7280', order: order || 0, isActive: true }); await category.save(); return category; }, adminUpdateJobCategory: async (parent, { id, name, nameAr, icon, color, isActive, order }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const category = await JobCategory.findById(id); if (!category) throw new GraphQLError('Category not found'); if (name) category.name = name; if (nameAr !== undefined) category.nameAr = nameAr; if (icon) category.icon = icon; if (color) category.color = color; if (isActive !== undefined) category.isActive = isActive; if (order !== undefined) category.order = order; await category.save(); return category; }, adminDeleteJobCategory: async (parent, { id }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const category = await JobCategory.findById(id); if (!category) throw new GraphQLError('Category not found'); const jobsCount = await Job.countDocuments({ category: category.name }); if (jobsCount > 0) { throw new GraphQLError(`Cannot delete category with ${jobsCount} jobs`); } await category.deleteOne(); return true; }, // ===== Store ===== toggleStore: async (parent, { enabled }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); if (enabled) { const subscription = await UserSubscription.findOne({ userId: context.user.userId, status: 'active' }).populate('planId'); if (!subscription) { throw new GraphQLError('You need an active subscription to enable your store'); } const plan = subscription.planId; if (!plan || !plan.features || !plan.features.storeEnabled) { throw new GraphQLError('Your current plan does not include store functionality'); } } await User.findByIdAndUpdate(context.user.userId, { 'profile.storeEnabled': enabled }); await StoreSettings.findOneAndUpdate( { userId: context.user.userId }, { enabled, updatedAt: Date.now() }, { upsert: true } ); return enabled; }, updateStoreSettings: async (parent, { input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); let settings = await StoreSettings.findOne({ userId: context.user.userId }); if (!settings) { settings = new StoreSettings({ userId: context.user.userId }); } const updateData = {}; if (input.storeName !== undefined) updateData.storeName = input.storeName; if (input.storeLogo !== undefined) updateData.storeLogo = input.storeLogo; if (input.storeBanner !== undefined) updateData.storeBanner = input.storeBanner; if (input.storeDescription !== undefined) updateData.storeDescription = input.storeDescription; if (input.contactEmail !== undefined) updateData.contactEmail = input.contactEmail; if (input.currency !== undefined) { updateData.currency = input.currency; const symbols = { 'USD': '$', 'EUR': '€', 'GBP': '£', 'EGP': 'ج.م', 'SAR': '﷼', 'AED': 'د.إ' }; updateData.currencySymbol = symbols[input.currency] || '$'; } if (input.primaryColor !== undefined) updateData.primaryColor = input.primaryColor; if (input.secondaryColor !== undefined) updateData.secondaryColor = input.secondaryColor; if (input.fontFamily !== undefined) updateData.fontFamily = input.fontFamily; if (input.borderRadius !== undefined) updateData.borderRadius = input.borderRadius; if (input.shadowType !== undefined) updateData.shadowType = input.shadowType; if (input.animationType !== undefined) updateData.animationType = input.animationType; if (input.glassEffect !== undefined) updateData.glassEffect = input.glassEffect; if (input.backgroundType !== undefined) updateData.backgroundType = input.backgroundType; if (input.backgroundValue !== undefined) updateData.backgroundValue = input.backgroundValue; if (input.customCss !== undefined) updateData.customCss = input.customCss; if (input.productViewType !== undefined) updateData.productViewType = input.productViewType; if (input.socialLinks !== undefined) updateData.socialLinks = input.socialLinks; if (input.pageBuilder !== undefined) updateData['pageBuilder.sections'] = input.pageBuilder.sections; if (input.paymentMethods !== undefined) updateData.paymentMethods = input.paymentMethods; updateData.updatedAt = Date.now(); Object.assign(settings, updateData); await settings.save(); return settings; }, createProduct: async (parent, { input, file }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const subscription = await UserSubscription.findOne({ userId: context.user.userId, status: 'active' }).populate('planId'); if (!subscription) { throw new GraphQLError('You need an active subscription to add products'); } const plan = subscription.planId; if (!plan || !plan.features || !plan.features.storeEnabled) { throw new GraphQLError('Your current plan does not allow adding products'); } const currentProductsCount = await Product.countDocuments({ userId: context.user.userId, isActive: true }); const maxProducts = plan.features.maxProducts || 0; if (maxProducts > 0 && currentProductsCount >= maxProducts) { throw new GraphQLError(`Your plan allows up to ${maxProducts} products`); } const product = new Product({ userId: context.user.userId, type: input.type, title: input.title, description: input.description, price: input.price, currency: input.currency || 'USD', deliveryTime: input.deliveryTime || null, tags: input.tags || [], images: input.images || [], isActive: input.isActive !== false }); await product.save(); await StoreSettings.findOneAndUpdate( { userId: context.user.userId }, { $inc: { productsCount: 1 }, updatedAt: Date.now() } ); return product; }, updateProduct: async (parent, { id, input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const product = await Product.findOne({ _id: id, userId: context.user.userId }); if (!product) throw new GraphQLError('Product not found'); const subscription = await UserSubscription.findOne({ userId: context.user.userId, status: 'active' }).populate('planId'); if (!subscription) { throw new GraphQLError('You need an active subscription to manage products'); } const plan = subscription.planId; if (!plan || !plan.features || !plan.features.storeEnabled) { throw new GraphQLError('Your current plan does not allow managing products'); } if (input.title !== undefined) product.title = input.title; if (input.description !== undefined) product.description = input.description; if (input.price !== undefined) product.price = input.price; if (input.type !== undefined) product.type = input.type; if (input.currency !== undefined) product.currency = input.currency; if (input.deliveryTime !== undefined) product.deliveryTime = input.deliveryTime; if (input.tags !== undefined) product.tags = input.tags; if (input.images !== undefined) product.images = input.images; if (input.isActive !== undefined) product.isActive = input.isActive; product.updatedAt = Date.now(); await product.save(); return product; }, deleteProduct: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const product = await Product.findOne({ _id: id, userId: context.user.userId }); if (!product) throw new GraphQLError('Product not found'); await product.deleteOne(); await StoreSettings.findOneAndUpdate( { userId: context.user.userId }, { $inc: { productsCount: -1 }, updatedAt: Date.now() } ); return true; }, toggleProduct: async (parent, { id, isActive }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const product = await Product.findOneAndUpdate( { _id: id, userId: context.user.userId }, { isActive, updatedAt: Date.now() }, { new: true } ); if (!product) throw new GraphQLError('Product not found'); return product; }, addProductReview: async (parent, { productId, rating, comment }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const product = await Product.findById(productId); if (!product) throw new GraphQLError('Product not found'); const hasPurchased = await Order.findOne({ buyerId: context.user.userId, 'items.productId': productId, status: 'completed' }); if (!hasPurchased) { throw new GraphQLError('You can only review products you have purchased'); } const existingReview = product.reviews.find(r => r.userId.toString() === context.user.userId); if (existingReview) { throw new GraphQLError('You have already reviewed this product'); } product.reviews.push({ userId: context.user.userId, rating, comment: comment || '', createdAt: new Date() }); const totalRatings = product.reviews.length; const sumRatings = product.reviews.reduce((sum, r) => sum + r.rating, 0); product.averageRating = sumRatings / totalRatings; await product.save(); const allProducts = await Product.find({ userId: product.userId, isActive: true, 'reviews.0': { $exists: true } }); let totalStoreRating = 0; let totalStoreReviews = 0; for (const p of allProducts) { for (const review of p.reviews) { totalStoreRating += review.rating; totalStoreReviews++; } } const storeAvgRating = totalStoreReviews > 0 ? totalStoreRating / totalStoreReviews : 0; await StoreSettings.findOneAndUpdate( { userId: product.userId }, { averageRating: storeAvgRating, updatedAt: Date.now() } ); return product.reviews[product.reviews.length - 1]; }, followStore: async (parent, { storeId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const store = await User.findById(storeId); if (!store || !store.profile.storeEnabled) { throw new GraphQLError('Store not found'); } if (storeId === context.user.userId) { throw new GraphQLError('You cannot follow your own store'); } const existingFollow = await StoreFollow.findOne({ userId: context.user.userId, storeId }); let isFollowing; let followersCount; if (existingFollow) { await existingFollow.deleteOne(); isFollowing = false; await StoreSettings.findOneAndUpdate( { userId: storeId }, { $inc: { followersCount: -1 }, updatedAt: Date.now() } ); } else { await StoreFollow.create({ userId: context.user.userId, storeId }); isFollowing = true; await StoreSettings.findOneAndUpdate( { userId: storeId }, { $inc: { followersCount: 1 }, updatedAt: Date.now() } ); } followersCount = await StoreFollow.countDocuments({ storeId }); return { success: true, isFollowing, followersCount }; }, createCoupon: async (parent, { input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const existingCoupon = await Coupon.findOne({ code: input.code.toUpperCase() }); if (existingCoupon) { throw new GraphQLError('Coupon code already exists'); } const coupon = new Coupon({ storeId: context.user.userId, code: input.code.toUpperCase(), discountType: input.discountType, discountValue: input.discountValue, minPurchase: input.minPurchase || 0, maxDiscount: input.maxDiscount || null, validUntil: input.validUntil ? new Date(input.validUntil) : null, usageLimit: input.usageLimit || null, applicableProducts: input.applicableProducts || [] }); await coupon.save(); return coupon; }, updateCoupon: async (parent, { id, input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const coupon = await Coupon.findOne({ _id: id, storeId: context.user.userId }); if (!coupon) throw new GraphQLError('Coupon not found'); if (input.code) coupon.code = input.code.toUpperCase(); if (input.discountType) coupon.discountType = input.discountType; if (input.discountValue !== undefined) coupon.discountValue = input.discountValue; if (input.minPurchase !== undefined) coupon.minPurchase = input.minPurchase; if (input.maxDiscount !== undefined) coupon.maxDiscount = input.maxDiscount; if (input.validUntil !== undefined) coupon.validUntil = input.validUntil ? new Date(input.validUntil) : null; if (input.usageLimit !== undefined) coupon.usageLimit = input.usageLimit; if (input.applicableProducts !== undefined) coupon.applicableProducts = input.applicableProducts; await coupon.save(); return coupon; }, deleteCoupon: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const coupon = await Coupon.findOne({ _id: id, storeId: context.user.userId }); if (!coupon) throw new GraphQLError('Coupon not found'); await coupon.deleteOne(); return true; }, validateCoupon: async (parent, { code, cartTotal, storeId }) => { const coupon = await Coupon.findOne({ code: code.toUpperCase(), storeId, isActive: true }); if (!coupon) { return { valid: false, discount: 0, finalTotal: cartTotal, message: 'Invalid coupon code' }; } const now = new Date(); if (coupon.validFrom && now < coupon.validFrom) { return { valid: false, discount: 0, finalTotal: cartTotal, message: 'Coupon not yet active' }; } if (coupon.validUntil && now > coupon.validUntil) { return { valid: false, discount: 0, finalTotal: cartTotal, message: 'Coupon has expired' }; } if (coupon.usageLimit && coupon.usageCount >= coupon.usageLimit) { return { valid: false, discount: 0, finalTotal: cartTotal, message: 'Coupon usage limit reached' }; } if (cartTotal < coupon.minPurchase) { return { valid: false, discount: 0, finalTotal: cartTotal, message: `Minimum purchase of ${coupon.minPurchase} required` }; } let discountAmount = 0; if (coupon.discountType === 'percentage') { discountAmount = (cartTotal * coupon.discountValue) / 100; if (coupon.maxDiscount && discountAmount > coupon.maxDiscount) { discountAmount = coupon.maxDiscount; } } else { discountAmount = Math.min(coupon.discountValue, cartTotal); } return { valid: true, discount: discountAmount, finalTotal: cartTotal - discountAmount, coupon }; }, contactStore: async (parent, { input, storeId }, context) => { const seller = await User.findById(storeId); if (!seller) throw new GraphQLError('Store not found'); const storeSettings = await StoreSettings.findOne({ userId: storeId }); if (!storeSettings || !storeSettings.enabled) { throw new GraphQLError('Store is not active'); } const storeContactEmail = storeSettings.contactEmail || seller.email; const notification = new Notification({ userId: storeId, type: 'contact_message', actorId: context?.user?.userId || null, actorName: input.name, content: `New contact message from ${input.name}: "${input.message.substring(0, 80)}${input.message.length > 80 ? '...' : ''}"`, targetId: `/shop/${seller.profile?.nickname || seller.username}`, targetType: 'store', read: false, metadata: { email: input.email, message: input.message, name: input.name } }); await notification.save(); try { await sendStoreEmailWithBrevo( storeContactEmail, `📬 New Contact Message from ${input.name}`, `

New Contact Message

Name: ${input.name}

Email: ${input.email}

Message:

${input.message}

`, `New message from ${input.name}: ${input.message}` ); } catch (e) {} return true; }, updatePagesOrder: async (parent, { pages }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); for (const page of pages) { if (page.id && page.id !== 'products' && page.id !== 'about' && page.id !== 'contact') { await Page.findOneAndUpdate( { _id: page.id, storeId: context.user.userId }, { order: page.order, updatedAt: Date.now() } ); } else if (page.id === 'about' || page.id === 'contact') { const existingPage = await Page.findOne({ storeId: context.user.userId, slug: page.id }); if (existingPage) { await Page.findOneAndUpdate( { _id: existingPage._id }, { order: page.order } ); } else { await Page.create({ storeId: context.user.userId, slug: page.id, title: page.id === 'about' ? 'About Us' : 'Contact Us', type: 'custom', isDefault: true, order: page.order, isEnabled: true }); } } } const productsOrder = pages.find(p => p.id === 'products'); if (productsOrder) { await StoreSettings.findOneAndUpdate( { userId: context.user.userId }, { 'pagesOrder.products': productsOrder.order } ); } return true; }, uploadPage: async (parent, { file, slug, title }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); return new Page({ storeId: context.user.userId, slug: slug || 'page', title: title || 'Uploaded Page', type: 'uploaded', isEnabled: true }); }, uploadPageZip: async (parent, { file }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); return []; }, createPage: async (parent, { input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const existingPage = await Page.findOne({ storeId: context.user.userId, slug: input.slug }); if (existingPage) { throw new GraphQLError('Page with this slug already exists'); } const page = new Page({ storeId: context.user.userId, slug: input.slug.toLowerCase().replace(/[^a-z0-9-]/g, '-'), title: input.title, content: input.content || '', css: input.css || '', js: input.js || '', type: 'custom', isEnabled: true, order: input.order || 0, seo: input.seo || {}, visibility: input.visibility || 'public', settings: input.settings || {} }); await page.save(); return page; }, updatePage: async (parent, { id, input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const page = await Page.findOne({ _id: id, storeId: context.user.userId }); if (!page) throw new GraphQLError('Page not found'); const updateData = { title: input.title || page.title, content: input.content !== undefined ? input.content : page.content, css: input.css !== undefined ? input.css : page.css, js: input.js !== undefined ? input.js : page.js, isEnabled: input.isEnabled !== undefined ? input.isEnabled : page.isEnabled, visibility: input.visibility || page.visibility, settings: input.settings || page.settings, updatedAt: Date.now() }; if (input.seo) updateData.seo = { ...page.seo, ...input.seo }; if (input.order !== undefined) updateData.order = input.order; if (input.slug && !page.isDefault) { updateData.slug = input.slug.toLowerCase().replace(/[^a-z0-9-]/g, '-'); } Object.assign(page, updateData); await page.save(); return page; }, deletePage: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const page = await Page.findOne({ _id: id, storeId: context.user.userId }); if (!page) throw new GraphQLError('Page not found'); await page.deleteOne(); return true; }, duplicatePage: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const originalPage = await Page.findOne({ _id: id, storeId: context.user.userId }); if (!originalPage) throw new GraphQLError('Page not found'); let finalSlug = `${originalPage.slug}-copy`; let counter = 1; while (await Page.findOne({ storeId: context.user.userId, slug: finalSlug })) { counter++; finalSlug = `${originalPage.slug}-copy-${counter}`; } const duplicatedPage = new Page({ storeId: context.user.userId, slug: finalSlug, title: `${originalPage.title} (Copy)`, content: originalPage.content, css: originalPage.css, js: originalPage.js, type: 'custom', isEnabled: false, order: await Page.countDocuments({ storeId: context.user.userId }), seo: originalPage.seo, visibility: originalPage.visibility, settings: originalPage.settings }); await duplicatedPage.save(); return duplicatedPage; }, exportPage: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const page = await Page.findOne({ _id: id, storeId: context.user.userId }); if (!page) throw new GraphQLError('Page not found'); const user = await User.findById(context.user.userId); const storeSettings = await StoreSettings.findOne({ userId: context.user.userId }); const html = ` ${page.title} - ${storeSettings?.storeName || user?.username}
${page.content || ''}
`; return html; }, resetTemplate: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); await StoreSettings.findOneAndUpdate( { userId: context.user.userId }, { $unset: { appliedTemplate: 1 }, $set: { updatedAt: Date.now() } } ); await StoreTheme.findOneAndUpdate( { userId: context.user.userId }, { isActive: false, updatedAt: Date.now() } ); const defaultTheme = new StoreTheme({ userId: context.user.userId }); await defaultTheme.save(); return true; }, applyTemplate: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const template = await TemplateStore.findById(id); if (!template) throw new GraphQLError('Template not found'); const themeData = template.themeData || {}; const colors = themeData.colors || {}; const typography = themeData.typography || {}; const layout = themeData.layout || {}; let templateUrl = null; let hasFullTemplate = false; if (themeData.templateUrl) { templateUrl = themeData.templateUrl; hasFullTemplate = true; } else if (themeData.uploadedFiles?.htmlFiles) { const indexFile = themeData.uploadedFiles.htmlFiles.find(f => f.name === 'index.html' || f.name.endsWith('/index.html') ); if (indexFile && indexFile.url) { templateUrl = indexFile.url; hasFullTemplate = true; } } await StoreSettings.findOneAndUpdate( { userId: context.user.userId }, { primaryColor: colors.primary || '#3b82f6', secondaryColor: colors.secondary || '#8b5cf6', fontFamily: typography.fontFamily || 'Inter, sans-serif', borderRadius: themeData.borderRadius || 16, shadowType: themeData.shadowType || layout.shadowType || 'md', animationType: themeData.animationType || 'lift', glassEffect: themeData.glassEffect || false, backgroundType: themeData.backgroundType || 'default', backgroundValue: themeData.backgroundValue || '', customCss: themeData.customCss || '', productViewType: themeData.productsSettings?.productViewType || 'modal', appliedTemplate: { slug: template.slug, name: template.name, templateUrl, appliedAt: new Date(), hasFullTemplate }, updatedAt: Date.now() }, { upsert: true } ); await User.findByIdAndUpdate(context.user.userId, { 'profile.storeEnabled': true }); await TemplateStore.recordDownload(template._id, context.user.userId, context.user.userId); return { success: true, message: `Template "${template.name}" applied successfully!`, appliedSettings: { primaryColor: colors.primary || '#3b82f6', secondaryColor: colors.secondary || '#8b5cf6', fontFamily: typography.fontFamily || 'Inter, sans-serif', borderRadius: themeData.borderRadius || 16, shadowType: themeData.shadowType || layout.shadowType || 'md', animationType: themeData.animationType || 'lift', glassEffect: themeData.glassEffect || false, backgroundType: themeData.backgroundType || 'default', sectionsCount: themeData.sections?.length || 0, layout: layout.type || 'default', productsPerRow: layout.columns || themeData.productsSettings?.productsPerRow || 4, hasCustomCss: !!themeData.customCss, hasCustomHeader: !!themeData.customHeaderFooter?.header, hasFullTemplate, templateUrl }, needsRefresh: true, redirectUrl: `/shop/${context.user.username || context.user.userId}` }; }, uploadTheme: async (parent, { file }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); return new StoreTheme({ userId: context.user.userId, name: 'Uploaded Theme' }); }, updateTheme: async (parent, { input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); let theme = await StoreTheme.findOne({ userId: context.user.userId }); if (!theme) { theme = new StoreTheme({ userId: context.user.userId }); } if (input.name) theme.name = input.name; if (input.isActive !== undefined) theme.isActive = input.isActive; if (input.version) theme.version = input.version; if (input.layout) theme.layout = input.layout; if (input.customTemplates) { theme.customTemplates = { ...theme.customTemplates, ...input.customTemplates }; } if (input.settings) { theme.settings = { ...theme.settings, ...input.settings }; } if (input.colors) { theme.colors = { ...theme.colors, ...input.colors }; } if (input.typography) { theme.typography = { ...theme.typography, ...input.typography }; } if (input.customAssets) { theme.customAssets = { ...theme.customAssets, ...input.customAssets }; } if (input.seo) { theme.seo = { ...theme.seo, ...input.seo }; } await theme.recordChange(); await theme.save(); return theme; }, resetTheme: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const theme = await StoreTheme.resetToDefault(context.user.userId); return theme; }, updateLayoutTemplate: async (parent, { layout, input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const validLayouts = ['default', 'sidebar-left', 'sidebar-right', 'product-focused', 'minimal', 'magazine', 'compact', 'dark-store']; if (!validLayouts.includes(layout)) { throw new GraphQLError('Invalid layout template'); } const updateData = { 'layoutSettings.layoutTemplate': layout, updatedAt: Date.now() }; if (input) { if (input.headerLayout) updateData['layoutSettings.headerLayout'] = input.headerLayout; if (input.footerLayout) updateData['layoutSettings.footerLayout'] = input.footerLayout; if (input.sidebarPosition) updateData['layoutSettings.sidebarPosition'] = input.sidebarPosition; if (input.productsPerRowDesktop) updateData['layoutSettings.productsPerRowDesktop'] = input.productsPerRowDesktop; if (input.productsPerRowTablet) updateData['layoutSettings.productsPerRowTablet'] = input.productsPerRowTablet; if (input.productsPerRowMobile) updateData['layoutSettings.productsPerRowMobile'] = input.productsPerRowMobile; if (input.enableStickyAddToCart !== undefined) { updateData['layoutSettings.enableStickyAddToCart'] = input.enableStickyAddToCart; } if (input.enableQuickView !== undefined) { updateData['layoutSettings.enableQuickView'] = input.enableQuickView; } if (input.enableCompareProducts !== undefined) { updateData['layoutSettings.enableCompareProducts'] = input.enableCompareProducts; } if (input.enableWishlist !== undefined) { updateData['layoutSettings.enableWishlist'] = input.enableWishlist; } if (input.enableRecentlyViewed !== undefined) { updateData['layoutSettings.enableRecentlyViewed'] = input.enableRecentlyViewed; } if (input.enableProductReviews !== undefined) { updateData['layoutSettings.enableProductReviews'] = input.enableProductReviews; } if (input.cardStyle) updateData['layoutSettings.cardStyle'] = input.cardStyle; if (input.heroStyle) updateData['layoutSettings.heroStyle'] = input.heroStyle; if (input.showStats !== undefined) updateData['layoutSettings.showStats'] = input.showStats; if (input.showAnalytics !== undefined) updateData['layoutSettings.showAnalytics'] = input.showAnalytics; if (input.showHero !== undefined) updateData['layoutSettings.showHero'] = input.showHero; if (input.showFeatured !== undefined) updateData['layoutSettings.showFeatured'] = input.showFeatured; if (input.showCategories !== undefined) updateData['layoutSettings.showCategories'] = input.showCategories; if (input.theme) updateData['layoutSettings.theme'] = input.theme; if (input.productViewType) updateData['layoutSettings.productViewType'] = input.productViewType; if (input.compactMode) updateData['layoutSettings.compactMode'] = input.compactMode; if (input.magazineMode) updateData['layoutSettings.magazineMode'] = input.magazineMode; if (input.darkStoreMode) updateData['layoutSettings.darkStoreMode'] = input.darkStoreMode; if (input.sectionsOrder) updateData['layoutSettings.sectionsOrder'] = input.sectionsOrder; if (input.advanced) updateData['layoutSettings.advanced'] = input.advanced; } const settings = await StoreSettings.findOneAndUpdate( { userId: context.user.userId }, { $set: updateData }, { upsert: true, new: true } ); return settings; }, resetLayoutTemplate: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const defaultSettings = { 'layoutSettings.layoutTemplate': 'default', 'layoutSettings.sidebarPosition': 'none', 'layoutSettings.productsPerRowDesktop': 4, 'layoutSettings.productsPerRowTablet': 3, 'layoutSettings.productsPerRowMobile': 2, 'layoutSettings.headerLayout': 'default', 'layoutSettings.footerLayout': 'default', 'layoutSettings.enableStickyAddToCart': true, 'layoutSettings.enableQuickView': true, 'layoutSettings.enableCompareProducts': false, 'layoutSettings.enableWishlist': false, 'layoutSettings.enableRecentlyViewed': true, 'layoutSettings.enableProductReviews': true, 'productViewType': 'modal', 'primaryColor': '#3b82f6', 'secondaryColor': '#8b5cf6', 'fontFamily': 'Inter, sans-serif', 'borderRadius': 16, 'shadowType': 'md', 'animationType': 'lift', 'glassEffect': false, 'backgroundType': 'default', 'backgroundValue': '', 'updatedAt': Date.now() }; const settings = await StoreSettings.findOneAndUpdate( { userId: context.user.userId }, { $set: defaultSettings }, { upsert: true, new: true } ); return settings; }, // ===== Cart ===== addToCart: async (parent, { productId, storeId, quantity }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); if (quantity < 1 || quantity > 99) { throw new GraphQLError('Quantity must be between 1 and 99'); } let cart = await Cart.findOne({ userId: context.user.userId }); if (!cart) { cart = new Cart({ userId: context.user.userId, stores: new Map() }); } const product = await Product.findById(productId); if (!product) throw new GraphQLError('Product not found'); let storeCart = cart.stores.get(storeId); if (!storeCart) { storeCart = { storeId, storeName: '', storeLogo: '', items: [], subtotal: 0, itemCount: 0, createdAt: new Date(), updatedAt: new Date() }; } const existingItemIndex = storeCart.items.findIndex( item => item.productId?.toString() === productId?.toString() ); if (existingItemIndex !== -1) { const newQuantity = storeCart.items[existingItemIndex].quantity + quantity; storeCart.items[existingItemIndex].quantity = Math.min(newQuantity, 99); } else { storeCart.items.push({ productId, title: product.title, price: product.price, quantity: Math.min(quantity, 99), image: product.images?.[0] || '/assets/img/default-product.png', productType: product.type || 'digital' }); } let subtotal = 0; let itemCount = 0; for (const item of storeCart.items) { subtotal += item.price * item.quantity; itemCount += item.quantity; } storeCart.subtotal = subtotal; storeCart.itemCount = itemCount; storeCart.updatedAt = new Date(); cart.stores.set(storeId, storeCart); await cart.calculateTotals(); await cart.save(); return cart; }, updateCartItem: async (parent, { storeId, productId, quantity }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); if (quantity < 1 || quantity > 99) { throw new GraphQLError('Quantity must be between 1 and 99'); } const cart = await Cart.findOne({ userId: context.user.userId }); if (!cart) throw new GraphQLError('Cart not found'); const storeCart = cart.stores.get(storeId); if (!storeCart) throw new GraphQLError('Store not found in cart'); const itemIndex = storeCart.items.findIndex( item => item.productId?.toString() === productId?.toString() ); if (itemIndex === -1) throw new GraphQLError('Product not found in cart'); storeCart.items[itemIndex].quantity = quantity; let subtotal = 0; let itemCount = 0; for (const item of storeCart.items) { subtotal += item.price * item.quantity; itemCount += item.quantity; } storeCart.subtotal = subtotal; storeCart.itemCount = itemCount; storeCart.updatedAt = new Date(); cart.stores.set(storeId, storeCart); await cart.calculateTotals(); await cart.save(); return cart; }, removeFromCart: async (parent, { storeId, productId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const cart = await Cart.findOne({ userId: context.user.userId }); if (!cart) throw new GraphQLError('Cart not found'); const storeCart = cart.stores.get(storeId); if (!storeCart) throw new GraphQLError('Store not found in cart'); storeCart.items = storeCart.items.filter( item => item.productId?.toString() !== productId?.toString() ); if (storeCart.items.length === 0) { cart.stores.delete(storeId); } else { let subtotal = 0; let itemCount = 0; for (const item of storeCart.items) { subtotal += item.price * item.quantity; itemCount += item.quantity; } storeCart.subtotal = subtotal; storeCart.itemCount = itemCount; storeCart.updatedAt = new Date(); cart.stores.set(storeId, storeCart); } await cart.calculateTotals(); await cart.save(); return cart; }, clearStoreCart: async (parent, { storeId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const cart = await Cart.findOne({ userId: context.user.userId }); if (!cart) throw new GraphQLError('Cart not found'); cart.stores.delete(storeId); await cart.calculateTotals(); await cart.save(); return cart; }, clearCart: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); await Cart.findOneAndDelete({ userId: context.user.userId }); return true; }, // ===== Orders ===== createOrder: async (parent, { input }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const { storeId, items, buyerNotes, paymentMethod } = input; const cart = await Cart.findOne({ userId: context.user.userId }); if (!cart) throw new GraphQLError('Cart is empty'); const storeCart = cart.stores.get(storeId); if (!storeCart || storeCart.items.length === 0) { throw new GraphQLError('Store cart is empty'); } const storeSettings = await StoreSettings.findOne({ userId: storeId }); if (!storeSettings || !storeSettings.enabled) { throw new GraphQLError('Store is not active'); } const paymentMethodData = storeSettings.paymentMethods?.find( m => m.type === paymentMethod ); if (!paymentMethodData) { throw new GraphQLError('Payment method not available'); } const order = new Order({ orderNumber: `ORD-${Date.now()}-${Math.floor(Math.random() * 10000)}`, buyerId: context.user.userId, sellerId: storeId, items: storeCart.items.map(item => ({ productId: item.productId, productType: item.productType, title: item.title, price: item.price, quantity: item.quantity })), subtotal: storeCart.subtotal, totalAmount: storeCart.subtotal, currency: storeSettings.currency || 'USD', status: 'pending', paymentMethod: paymentMethodData.type, paymentDetails: paymentMethodData.details || {}, buyerNotes: buyerNotes || '', createdAt: new Date() }); await order.save(); for (const item of storeCart.items) { await Product.findByIdAndUpdate(item.productId, { $inc: { salesCount: item.quantity } }); } cart.stores.delete(storeId); await cart.calculateTotals(); await cart.save(); return order; }, updateOrderStatus: async (parent, { id, status, sellerNotes }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const order = await Order.findOne({ _id: id, sellerId: context.user.userId }); if (!order) throw new GraphQLError('Order not found'); order.status = status; if (sellerNotes) order.sellerNotes = sellerNotes; if (status === 'completed') order.deliveredAt = new Date(); order.updatedAt = new Date(); await order.save(); return order; }, cancelOrder: async (parent, { id, reason }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const order = await Order.findOne({ _id: id, buyerId: context.user.userId }); if (!order) throw new GraphQLError('Order not found'); if (order.status !== 'pending') { throw new GraphQLError('Cannot cancel order in current status'); } order.status = 'cancelled'; order.cancelledAt = new Date(); order.cancellationReason = reason || 'Cancelled by user'; await order.save(); return order; }, uploadOrderFile: async (parent, { orderId, productId, file }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const order = await Order.findOne({ _id: orderId, sellerId: context.user.userId }); if (!order) throw new GraphQLError('Order not found or unauthorized'); const item = order.items.find(i => i.productId.toString() === productId); if (!item) throw new GraphQLError('Product not found in this order'); return item; }, addOrderFileUrl: async (parent, { orderId, productId, fileUrl, fileName }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const order = await Order.findOne({ _id: orderId, sellerId: context.user.userId }); if (!order) throw new GraphQLError('Order not found or unauthorized'); const item = order.items.find(i => i.productId.toString() === productId); if (!item) throw new GraphQLError('Product not found in this order'); item.fileUrl = fileUrl; item.fileName = fileName || item.title + '.file'; item.fileType = 'link'; item.uploadedAt = new Date(); await order.save(); return item; }, downloadOrderFile: async (parent, { orderId, productId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const order = await Order.findById(orderId); if (!order) throw new GraphQLError('Order not found'); if (order.buyerId.toString() !== context.user.userId || order.status !== 'completed') { throw new GraphQLError('Unauthorized or order not completed'); } const item = order.items.find(i => i.productId.toString() === productId); if (!item || !item.fileUrl) { throw new GraphQLError('File not found'); } await Order.findByIdAndUpdate(orderId, { $inc: { 'items.$[item].downloadCount': 1 } }, { arrayFilters: [{ 'item.productId': productId }] }); return item.fileUrl; }, // ===== Follow ===== followUser: async (parent, { userId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); if (userId === context.user.userId) { throw new GraphQLError('Cannot follow yourself'); } const existingFollow = await Follow.findOne({ followerId: context.user.userId, followingId: userId }); if (existingFollow) { throw new GraphQLError('Already following this user'); } const follow = new Follow({ followerId: context.user.userId, followingId: userId, status: 'accepted' }); await follow.save(); const user = await User.findById(context.user.userId); const notification = new Notification({ userId, type: 'follow', actorId: context.user.userId, actorName: user.profile?.nickname || user.username, actorAvatar: user.profile?.avatar, targetId: context.user.userId, targetType: 'user', content: `started following you`, read: false }); await notification.save(); return { success: true, isFollowing: true }; }, unfollowUser: async (parent, { userId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const follow = await Follow.findOneAndDelete({ followerId: context.user.userId, followingId: userId }); if (!follow) throw new GraphQLError('Not following this user'); await Notification.deleteMany({ userId, type: 'follow', actorId: context.user.userId }); return { success: true, isFollowing: false }; }, acceptFollowRequest: async (parent, { requestId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const follow = await Follow.findById(requestId); if (!follow) throw new GraphQLError('Request not found'); if (follow.followingId.toString() !== context.user.userId) { throw new GraphQLError('Unauthorized'); } follow.status = 'accepted'; await follow.save(); const notification = new Notification({ userId: follow.followerId, type: 'follow', actorId: context.user.userId, content: `accepted your follow request`, read: false }); await notification.save(); return true; }, declineFollowRequest: async (parent, { requestId }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const follow = await Follow.findById(requestId); if (!follow) throw new GraphQLError('Request not found'); if (follow.followingId.toString() !== context.user.userId) { throw new GraphQLError('Unauthorized'); } await follow.deleteOne(); return true; }, // ===== Notifications ===== markNotificationRead: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const notification = await Notification.findOneAndUpdate( { _id: id, userId: context.user.userId }, { read: true }, { new: true } ); if (!notification) throw new GraphQLError('Notification not found'); return notification; }, markAllNotificationsRead: async (parent, args, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); await Notification.updateMany( { userId: context.user.userId, read: false }, { $set: { read: true } } ); return true; }, deleteNotification: async (parent, { id }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const notification = await Notification.findOneAndDelete({ _id: id, userId: context.user.userId }); if (!notification) throw new GraphQLError('Notification not found'); return true; }, // ===== Ratings ===== rateUser: async (parent, { targetUserId, rating, review }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); if (targetUserId === context.user.userId) { throw new GraphQLError('You cannot rate yourself'); } const targetUser = await User.findById(targetUserId); if (!targetUser) throw new GraphQLError('User not found'); const existingRating = await Rating.findOne({ targetUserId, raterUserId: context.user.userId }); if (existingRating) { existingRating.rating = rating; existingRating.review = review || existingRating.review; existingRating.updatedAt = new Date(); await existingRating.save(); } else { const newRating = new Rating({ targetUserId, raterUserId: context.user.userId, rating, review: review || '' }); await newRating.save(); } const allRatings = await Rating.find({ targetUserId }); const averageRating = allRatings.reduce((sum, r) => sum + r.rating, 0) / allRatings.length; await User.findByIdAndUpdate(targetUserId, { 'profile.averageRating': averageRating, 'profile.totalRatings': allRatings.length }); return { success: true, averageRating, totalRatings: allRatings.length }; }, // ===== AI ===== askAI: async (parent, { nickname, question, language }, context) => { const startTime = Date.now(); if (question.trim().length < 2) { throw new GraphQLError('Question is too short'); } if (question.trim().length > 1000) { throw new GraphQLError('Question is too long'); } const user = await User.findOne({ $or: [ { 'profile.nickname': { $regex: `^${nickname}$`, $options: 'i' } }, { username: { $regex: `^${nickname}$`, $options: 'i' } } ] }); if (!user) throw new GraphQLError('User not found'); const aiConfig = user.profile?.aiBot || { enabled: false }; if (!aiConfig.enabled) { throw new GraphQLError('AI Bot is not enabled for this profile'); } const isOwner = context.user && context.user.userId === user._id.toString(); const targetLanguage = language || detectLanguage(question); if (isOwner) { await User.updateOne( { _id: user._id }, { $inc: { 'profile.aiBot.totalQueries': 1 }, $set: { 'profile.aiBot.lastQueryAt': new Date() } } ); return { answer: 'AI response would be generated here', language: targetLanguage, provider: 'mgzon', responseTime: Date.now() - startTime, isCommand: false }; } const visitorIp = context.req?.ip || context.req?.headers?.['x-forwarded-for'] || ''; await saveAIConversation( user._id, question, 'AI response', 'mgzon', targetLanguage, Date.now() - startTime, visitorIp ); return { answer: 'AI response for visitor', language: targetLanguage, provider: 'mgzon', responseTime: Date.now() - startTime, isCommand: false }; }, clearAICache: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); return true; }, // ===== Subscriptions ===== subscribe: async (parent, { planId, paymentMethodId, paymentDetails, autoRenew, paymentProof }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const plan = await SubscriptionPlan.findById(planId); if (!plan || !plan.isActive) throw new GraphQLError('Plan not found'); const paymentMethod = await PlatformPaymentMethod.findById(paymentMethodId); if (!paymentMethod || !paymentMethod.isActive) throw new GraphQLError('Payment method not found'); const existingSubscription = await UserSubscription.findOne({ userId: context.user.userId, status: { $in: ['active', 'pending'] } }); let isTrial = false; let trialEndDate = null; if (plan.hasFreeTrial && !existingSubscription) { isTrial = true; trialEndDate = new Date(); trialEndDate.setDate(trialEndDate.getDate() + plan.freeTrialDays); } const startDate = isTrial ? new Date() : null; const endDate = isTrial ? trialEndDate : null; const subscription = new UserSubscription({ userId: context.user.userId, planId, status: isTrial ? 'active' : 'pending', startDate, endDate, isTrial, trialEndDate, paymentMethod: paymentMethod.name, paymentDetails: paymentDetails || {}, amount: isTrial ? 0 : plan.price, currency: plan.currency, autoRenew: autoRenew || false, createdAt: new Date() }); await subscription.save(); if (isTrial) { await updateUserPermissions(context.user.userId, plan); } return subscription; }, renewSubscription: async (parent, { planId, paymentMethodId, paymentDetails, paymentProof }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const plan = await SubscriptionPlan.findById(planId); if (!plan || !plan.isActive) throw new GraphQLError('Plan not found'); const paymentMethod = await PlatformPaymentMethod.findById(paymentMethodId); if (!paymentMethod || !paymentMethod.isActive) throw new GraphQLError('Payment method not found'); const startDate = new Date(); const endDate = new Date(); endDate.setDate(endDate.getDate() + plan.durationDays); const subscription = new UserSubscription({ userId: context.user.userId, planId, status: plan.price === 0 ? 'active' : 'pending', startDate, endDate, isTrial: false, paymentMethod: paymentMethod.name, paymentDetails: paymentDetails || {}, amount: plan.price, currency: plan.currency, autoRenew: false, createdAt: new Date() }); await subscription.save(); if (plan.price === 0) { await updateUserPermissions(context.user.userId, plan); } return subscription; }, cancelSubscription: async (parent, { reason }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const subscription = await UserSubscription.findOne({ userId: context.user.userId, status: 'active' }).populate('planId'); if (!subscription) throw new GraphQLError('No active subscription found'); subscription.status = 'cancelled'; subscription.cancelledAt = new Date(); subscription.cancellationReason = reason || 'Cancelled by user'; subscription.autoRenew = false; subscription.updatedAt = new Date(); await subscription.save(); await User.findByIdAndUpdate(context.user.userId, { 'profile.storeEnabled': false }); return true; }, adminApproveSubscription: async (parent, { id, adminNotes }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const subscription = await UserSubscription.findById(id).populate('planId'); if (!subscription) throw new GraphQLError('Subscription not found'); const startDate = new Date(); const endDate = new Date(); endDate.setDate(endDate.getDate() + subscription.planId.durationDays); subscription.status = 'active'; subscription.startDate = startDate; subscription.endDate = endDate; subscription.adminNotes = adminNotes || ''; subscription.approvedBy = context.user.userId; subscription.approvedAt = new Date(); subscription.updatedAt = new Date(); await subscription.save(); if (subscription.amount > 0) { const earning = new SubscriptionEarnings({ subscriptionId: subscription._id, userId: subscription.userId, planId: subscription.planId._id, amount: subscription.amount, currency: subscription.currency, status: 'completed', paymentMethod: subscription.paymentMethod, transactionId: subscription.transactionId, payoutStatus: 'pending' }); await earning.save(); } await updateUserPermissions(subscription.userId, subscription.planId); return subscription; }, adminRejectSubscription: async (parent, { id, rejectionReason }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const subscription = await UserSubscription.findById(id).populate('planId'); if (!subscription) throw new GraphQLError('Subscription not found'); subscription.status = 'cancelled'; subscription.cancelledAt = new Date(); subscription.cancellationReason = rejectionReason || 'Rejected by admin'; subscription.adminNotes = rejectionReason || ''; subscription.updatedAt = new Date(); await subscription.save(); return true; }, adminCancelSubscription: async (parent, { id, reason }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const subscription = await UserSubscription.findById(id).populate('planId'); if (!subscription) throw new GraphQLError('Subscription not found'); subscription.status = 'cancelled'; subscription.cancelledAt = new Date(); subscription.cancellationReason = reason || 'Cancelled by admin'; subscription.updatedAt = new Date(); await subscription.save(); await updateUserPermissions(subscription.userId, null); return true; }, adminCreatePlan: async (parent, { input }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const existingPlan = await SubscriptionPlan.findOne({ name: input.name }); if (existingPlan) throw new GraphQLError('Plan name already exists'); const plan = new SubscriptionPlan({ name: input.name, nameAr: input.nameAr || '', description: input.description || '', descriptionAr: input.descriptionAr || '', price: input.price, currency: input.currency || 'USD', currencySymbol: input.currency === 'USD' ? '$' : input.currency === 'EUR' ? '€' : 'ج.م', duration: input.duration || 'monthly', durationDays: input.duration === 'monthly' ? 30 : input.duration === 'yearly' ? 365 : 0, features: { storeEnabled: input.features.storeEnabled || false, maxProducts: input.features.maxProducts || 0, maxDigitalProducts: input.features.maxDigitalProducts || 0, maxProjects: input.features.maxProjects || 0, maxServices: input.features.maxServices || 0, pageBuilderEnabled: input.features.pageBuilderEnabled || false, customDomain: input.features.customDomain || false, analyticsEnabled: input.features.analyticsEnabled || false, prioritySupport: input.features.prioritySupport || false, removeBranding: input.features.removeBranding || false, teamMembers: input.features.teamMembers || 1, apiAccess: input.features.apiAccess || false, customCss: input.features.customCss || false, advancedAnalytics: input.features.advancedAnalytics || false, exportData: input.features.exportData || false }, hasFreeTrial: input.hasFreeTrial || false, freeTrialDays: input.freeTrialDays || 0, badge: input.badge || null, icon: input.icon || 'bx-star', isActive: input.isActive !== false, order: input.order || 0, customPermissions: input.customPermissions || {} }); await plan.save(); return plan; }, adminUpdatePlan: async (parent, { id, input }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const plan = await SubscriptionPlan.findById(id); if (!plan) throw new GraphQLError('Plan not found'); if (input.name) plan.name = input.name; if (input.nameAr !== undefined) plan.nameAr = input.nameAr; if (input.description !== undefined) plan.description = input.description; if (input.descriptionAr !== undefined) plan.descriptionAr = input.descriptionAr; if (input.price !== undefined) plan.price = input.price; if (input.currency) plan.currency = input.currency; if (input.duration) { plan.duration = input.duration; plan.durationDays = input.duration === 'monthly' ? 30 : input.duration === 'yearly' ? 365 : 0; } if (input.features) { plan.features = { ...plan.features, ...input.features }; } if (input.hasFreeTrial !== undefined) plan.hasFreeTrial = input.hasFreeTrial; if (input.freeTrialDays !== undefined) plan.freeTrialDays = input.freeTrialDays; if (input.badge !== undefined) plan.badge = input.badge; if (input.icon) plan.icon = input.icon; if (input.isActive !== undefined) plan.isActive = input.isActive; if (input.order !== undefined) plan.order = input.order; if (input.customPermissions !== undefined) plan.customPermissions = input.customPermissions; plan.updatedAt = Date.now(); await plan.save(); return plan; }, adminDeletePlan: async (parent, { id }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const activeSubscriptions = await UserSubscription.countDocuments({ planId: id, status: { $in: ['active', 'pending'] } }); if (activeSubscriptions > 0) { throw new GraphQLError(`Cannot delete plan with ${activeSubscriptions} active subscriptions`); } await SubscriptionPlan.findByIdAndDelete(id); return true; }, adminCreatePaymentMethod: async (parent, { input }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const method = new PlatformPaymentMethod({ name: input.name, nameAr: input.nameAr || '', type: input.type, icon: input.icon || (input.type === 'bank' ? 'bx-bank' : input.type === 'mobile_wallet' ? 'bx-mobile-alt' : 'bx-credit-card'), instructions: input.instructions || '', instructionsAr: input.instructionsAr || '', bankDetails: input.bankDetails || {}, mobileWalletDetails: input.mobileWalletDetails || {}, onlineDetails: input.onlineDetails || {}, isActive: input.isActive !== false, order: input.order || 0 }); await method.save(); return method; }, adminUpdatePaymentMethod: async (parent, { id, input }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const method = await PlatformPaymentMethod.findById(id); if (!method) throw new GraphQLError('Payment method not found'); if (input.name) method.name = input.name; if (input.nameAr !== undefined) method.nameAr = input.nameAr; if (input.instructions !== undefined) method.instructions = input.instructions; if (input.instructionsAr !== undefined) method.instructionsAr = input.instructionsAr; if (input.bankDetails) method.bankDetails = { ...method.bankDetails, ...input.bankDetails }; if (input.mobileWalletDetails) { method.mobileWalletDetails = { ...method.mobileWalletDetails, ...input.mobileWalletDetails }; } if (input.onlineDetails) { method.onlineDetails = { ...method.onlineDetails, ...input.onlineDetails }; } if (input.isActive !== undefined) method.isActive = input.isActive; if (input.order !== undefined) method.order = input.order; method.updatedAt = Date.now(); await method.save(); return method; }, adminDeletePaymentMethod: async (parent, { id }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); await PlatformPaymentMethod.findByIdAndDelete(id); return true; }, // ===== Admin ===== adminToggleStore: async (parent, { userId, enabled }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); await StoreSettings.findOneAndUpdate( { userId }, { enabled, updatedAt: Date.now() }, { upsert: true } ); await User.findByIdAndUpdate(userId, { 'profile.storeEnabled': enabled }); return true; }, adminDeleteStore: async (parent, { userId }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); await Product.deleteMany({ userId }); await Order.deleteMany({ sellerId: userId }); await StoreSettings.findOneAndDelete({ userId }); await User.findByIdAndUpdate(userId, { 'profile.storeEnabled': false }); return true; }, adminToggleProduct: async (parent, { productId, isActive }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const product = await Product.findByIdAndUpdate( productId, { isActive, updatedAt: Date.now() }, { new: true } ); if (!product) throw new GraphQLError('Product not found'); return product; }, adminDeleteProduct: async (parent, { productId }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const product = await Product.findByIdAndDelete(productId); if (!product) throw new GraphQLError('Product not found'); return true; }, adminUpdateOrderStatus: async (parent, { orderId, status, adminNotes }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const order = await Order.findByIdAndUpdate( orderId, { status, adminNotes, updatedAt: Date.now() }, { new: true } ); if (!order) throw new GraphQLError('Order not found'); return order; }, adminMakeUser: async (parent, { userId, isAdmin }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const user = await User.findByIdAndUpdate( userId, { isAdmin }, { new: true } ); if (!user) throw new GraphQLError('User not found'); return user; }, adminResolveReport: async (parent, { reportId, action, adminNote }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const report = await Report.findById(reportId); if (!report) throw new GraphQLError('Report not found'); report.status = 'resolved'; report.resolvedBy = context.user.userId; report.resolvedAt = new Date(); report.adminNotes = adminNote || ''; await report.save(); return report; }, adminResolveMessageReport: async (parent, { reportId, action, adminNote }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const report = await MessageReport.findById(reportId); if (!report) throw new GraphQLError('Report not found'); report.status = 'resolved'; report.resolvedAt = new Date(); report.resolvedBy = context.user.userId; report.adminNote = adminNote || ''; report.resolution = action; await report.save(); if (action === 'delete') { await Message.findByIdAndDelete(report.messageId); } else if (action === 'hide') { await Message.findByIdAndUpdate(report.messageId, { hidden: true }); } return true; }, adminCreatePlatformBank: async (parent, { input }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const bank = new PlatformBank({ bankName: input.bankName, accountName: input.accountName, accountNumber: input.accountNumber, iban: input.iban || '', swiftCode: input.swiftCode || '', isActive: input.isActive !== false, order: input.order || 0 }); await bank.save(); return bank; }, adminUpdatePlatformBank: async (parent, { id, input }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); const bank = await PlatformBank.findById(id); if (!bank) throw new GraphQLError('Bank not found'); if (input.bankName) bank.bankName = input.bankName; if (input.accountName) bank.accountName = input.accountName; if (input.accountNumber) bank.accountNumber = input.accountNumber; if (input.iban !== undefined) bank.iban = input.iban; if (input.swiftCode !== undefined) bank.swiftCode = input.swiftCode; if (input.isActive !== undefined) bank.isActive = input.isActive; if (input.order !== undefined) bank.order = input.order; bank.updatedAt = Date.now(); await bank.save(); return bank; }, adminDeletePlatformBank: async (parent, { id }, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); await PlatformBank.findByIdAndDelete(id); return true; }, adminRegenerateSitemaps: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); return true; }, adminExportUsers: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); return 'CSV data'; }, adminExportOrders: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); return 'CSV data'; }, adminCreateBackup: async (parent, args, context) => { if (!context.isAdmin) throw new GraphQLError('Admin access required'); return 'Backup created'; }, // ===== Reports ===== reportContent: async (parent, { targetType, targetId, reason, details }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const existingReport = await Report.findOne({ reporterId: context.user.userId, targetType, targetId, status: 'pending' }); if (existingReport) { throw new GraphQLError('You already reported this content'); } const report = new Report({ reporterId: context.user.userId, targetType, targetId, reason, details: details || '', status: 'pending' }); await report.save(); return report; }, reportMessage: async (parent, { messageId, reason }, context) => { if (!context.user) throw new GraphQLError('Not authenticated'); const message = await Message.findById(messageId).populate('senderId'); if (!message) throw new GraphQLError('Message not found'); if (message.senderId._id.toString() === context.user.userId) { throw new GraphQLError('You cannot report your own message'); } const existingReport = await MessageReport.findOne({ messageId, reporterId: context.user.userId }); if (existingReport) { throw new GraphQLError('You already reported this message'); } const report = new MessageReport({ messageId, reporterId: context.user.userId, reporterUsername: context.user.username, reportedUserId: message.senderId._id, reason, messageContent: message.text, messageAttachments: message.attachments, conversationId: message.conversationId, status: 'pending', reportedAt: new Date() }); await report.save(); return true; } }; // ============================================ // 🔹 HELPER FUNCTIONS FOR PROFILE PAGE // ============================================ // جلب التفاعلات (Recent Activity) async function getProfileInteractions(userId, page = 1, limit = 4) { if (!userId) return []; const skip = (page - 1) * limit; try { const comments = await Comment.find({ userId }) .populate('projectId', 'title image userId') .sort({ timestamp: -1 }) .skip(skip) .limit(limit) .lean(); if (!comments || comments.length === 0) return []; return comments.map(c => { const project = c.projectId || {}; // ✅ التأكد من وجود projectId حقيقي const projectId = project._id ? project._id.toString() : null; return { _id: c._id || 'unknown', commentId: c._id || 'unknown', projectId: projectId, // ✅ null بدلاً من 'unknown-project' projectTitle: project.title || null, // ✅ null بدلاً من 'Unknown Project' projectNickname: project.userId?.profile?.nickname || null, projectImage: project.image || null, rating: c.rating || 0, text: c.text || '', timestamp: c.timestamp || c.createdAt || new Date(), replies: [], likes: c.likes || [], isEdited: c.isEdited || false, parentCommentId: c.parentCommentId || null, visibility: c.visibility || 'public' }; }); } catch (error) { console.error('Error in getProfileInteractions:', error); return []; } } // ✅ جلب Contribution Graph async function getContributionGraph(userId, year) { if (!userId) { return { total: 0, weeks: [], year: year || new Date().getFullYear(), months: [], weekDays: [] }; } const targetYear = year || new Date().getFullYear(); const startDate = new Date(targetYear, 0, 1); const endDate = new Date(targetYear, 11, 31); try { // جلب الأنشطة const [posts, comments, likes, shares, jobApplications] = await Promise.all([ Post.find({ userId, createdAt: { $gte: startDate, $lte: endDate } }) .select('_id createdAt') .lean(), Comment.find({ userId, timestamp: { $gte: startDate, $lte: endDate } }) .select('_id projectId timestamp') .lean(), Post.aggregate([ { $match: { 'likes.userId': userId, createdAt: { $gte: startDate, $lte: endDate } } }, { $unwind: '$likes' }, { $match: { 'likes.userId': userId } }, { $project: { postId: '$_id', createdAt: '$likes.likedAt' } } ]), Post.aggregate([ { $match: { 'shares.userId': userId, createdAt: { $gte: startDate, $lte: endDate } } }, { $unwind: '$shares' }, { $match: { 'shares.userId': userId } }, { $project: { postId: '$_id', createdAt: '$shares.sharedAt' } } ]), JobApplication.find({ applicantId: userId, appliedAt: { $gte: startDate, $lte: endDate } }) .select('_id jobId appliedAt') .lean() ]); // تجميع الأنشطة const activitiesMap = new Map(); const addActivity = (date, type, id, extra = {}) => { if (!date) return; const dateStr = date.toISOString().split('T')[0]; if (!activitiesMap.has(dateStr)) { activitiesMap.set(dateStr, { count: 0, activities: [] }); } const entry = activitiesMap.get(dateStr); entry.count++; entry.activities.push({ type, id, ...extra }); }; posts.forEach(p => addActivity(p.createdAt, 'post', p._id)); comments.forEach(c => addActivity(c.timestamp, 'comment', c._id, { projectId: c.projectId })); likes.forEach(l => addActivity(l.createdAt, 'like', l.postId, { postId: l.postId })); shares.forEach(s => addActivity(s.createdAt, 'share', s.postId, { postId: s.postId })); jobApplications.forEach(j => addActivity(j.appliedAt, 'job_application', j._id, { jobId: j.jobId })); // بناء الأسبوعيات const weeks = []; let currentDate = new Date(startDate); currentDate.setDate(currentDate.getDate() - currentDate.getDay()); while (currentDate <= endDate) { const week = []; for (let i = 0; i < 7; i++) { const date = new Date(currentDate); const dateStr = date.toISOString().split('T')[0]; const data = activitiesMap.get(dateStr) || { count: 0, activities: [] }; let level = 0; if (data.count > 0) level = 1; if (data.count > 3) level = 2; if (data.count > 7) level = 3; if (data.count > 15) level = 4; week.push({ date: dateStr, count: data.count, level, activities: data.activities || [] }); currentDate.setDate(currentDate.getDate() + 1); } weeks.push(week); } let total = 0; for (const [_, data] of activitiesMap) { total += data.count; } const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return { total, weeks, year: targetYear, months, weekDays }; } catch (error) { console.error('Error in getContributionGraph:', error); return { total: 0, weeks: [], year: targetYear, months: [], weekDays: [] }; } } // ✅ جلب "شاهد أيضاً" async function getAlsoViewed(userId) { if (!userId) return []; try { // ✅ استخدم AlsoViewed بدلاً من ProfileView const views = await AlsoViewed.find({ targetUserId: userId }) .sort({ viewedAt: -1 }) .limit(10) .lean(); if (!views || views.length === 0) return []; const viewerIds = views.map(v => v.viewerId).filter(id => id); if (viewerIds.length === 0) return []; const users = await User.find({ _id: { $in: viewerIds } }) .limit(5) .lean(); return users.map(u => ({ _id: u._id, username: u.username, profile: u.profile || {}, followersCount: u.followersCount || 0 })); } catch (error) { console.error('Error in getAlsoViewed:', error); return []; } } // ✅ جلب اقتراحات الصفحات async function getPageSuggestions(limit = 2) { try { const pages = await Page.find({ isEnabled: true }) .sort({ followersCount: -1 }) .limit(limit) .lean(); if (!pages || pages.length === 0) return []; return pages.map(p => ({ _id: p._id, name: p.title || p.slug || 'Unknown', logo: p.thumbnail || null, followersCount: p.followersCount || 0, isFollowing: false, url: `/page/${p.slug || p._id}` })); } catch (error) { console.error('Error in getPageSuggestions:', error); return []; } } // ✅ جلب التحليلات (للمالك) async function getProfileAnalytics(userId) { if (!userId) { return { views: 0, uniqueViews: 0, weeklyViews: 0, impressions: 0, searches: 0, lastUpdated: new Date() }; } try { // ✅ استخدم AlsoViewed بدلاً من ProfileView const analytics = await ProfileAnalytics.findOne({ userId }); const weekAgo = new Date(); weekAgo.setDate(weekAgo.getDate() - 7); // ✅ استخدم AlsoViewed بدلاً من ProfileView const weeklyViews = await AlsoViewed.countDocuments({ targetUserId: userId, viewedAt: { $gte: weekAgo } }); return { views: analytics?.views || 0, uniqueViews: analytics?.uniqueViews || 0, weeklyViews: weeklyViews || 0, impressions: analytics?.impressions || 0, searches: analytics?.searches || 0, lastUpdated: analytics?.updatedAt || new Date() }; } catch (error) { console.error('Error in getProfileAnalytics:', error); return { views: 0, uniqueViews: 0, weeklyViews: 0, impressions: 0, searches: 0, lastUpdated: new Date() }; } } // ✅ جلب حالة الاشتراك async function getSubscriptionStatus(userId) { if (!userId) { return { hasActiveSubscription: false, canCreateStore: false, message: 'No user ID provided', planName: null, daysLeft: 0 }; } try { const subscription = await UserSubscription.findOne({ userId, status: 'active' }).populate('planId'); if (subscription) { const now = new Date(); const endDate = new Date(subscription.endDate); const daysLeft = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24)); // ✅ تحويل القيم إلى Boolean const canCreateStore = !!(subscription.planId?.features?.storeEnabled); return { hasActiveSubscription: true, canCreateStore: canCreateStore, message: 'Active subscription', planName: subscription.planId?.name || 'Pro', daysLeft: daysLeft > 0 ? daysLeft : 0 }; } return { hasActiveSubscription: false, canCreateStore: false, message: 'No active subscription', planName: null, daysLeft: 0 }; } catch (error) { console.error('Error in getSubscriptionStatus:', error); return { hasActiveSubscription: false, canCreateStore: false, message: 'Error checking subscription', planName: null, daysLeft: 0 }; } } // ============================================ // 🔹 EXPORT // ============================================ module.exports = { Query, Mutation };