server / graphql /resolvers.js
Mark-Lasfar
Add GraphQL
775acdd
Raw
History Blame Contribute Delete
281 kB
// 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
} = 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: `
<div class="contact-info">
<p><strong>Email:</strong> ${storeSettings?.contactEmail || user.email}</p>
<p><strong>Store:</strong> ${storeSettings?.storeName || user.profile?.nickname || user.username}</p>
</div>
`,
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',
`<div style="font-family: Arial, sans-serif; max-width: 600px;">
<h2>Welcome to MGZon! 🎉</h2>
<p>Your verification code is:</p>
<div style="background: #f3f4f6; padding: 20px; text-align: center; font-size: 32px; letter-spacing: 8px; font-weight: bold; border-radius: 10px;">
${otp}
</div>
<p>Valid for 10 minutes.</p>
</div>`,
`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',
`<div><h2>Verification Code</h2>
<div style="background: #f3f4f6; padding: 20px; font-size: 32px; letter-spacing: 8px;">${otp}</div>
<p>Valid for 10 minutes.</p></div>`,
`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',
`<div><h2>Verification Code</h2>
<div style="background: #f3f4f6; padding: 20px; font-size: 32px;">${otp}</div>
<p>Valid for 10 minutes.</p></div>`,
`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',
`<div><h2>Password Reset</h2>
<div style="background: #f3f4f6; padding: 20px; font-size: 32px;">${otp}</div>
<p>Valid for 10 minutes.</p></div>`,
`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',
`<div><h2>Welcome!</h2>
<a href="${magicLink}" style="display:inline-block;padding:12px 24px;background:#667eea;color:white;border-radius:8px;">Sign In</a>
<p>Valid for 15 minutes.</p></div>`,
`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}`,
`<div>
<h2>New Contact Message</h2>
<p><strong>Name:</strong> ${input.name}</p>
<p><strong>Email:</strong> ${input.email}</p>
<p><strong>Message:</strong></p>
<p>${input.message}</p>
</div>`,
`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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${page.title} - ${storeSettings?.storeName || user?.username}</title>
<meta name="description" content="${page.seo?.description || ''}">
<meta name="keywords" content="${page.seo?.keywords || ''}">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: #f9fafb; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
${page.css || ''}
</style>
</head>
<body>
<div class="container">
${page.content || ''}
</div>
<script>${page.js || ''}</script>
</body>
</html>`;
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 {
const views = await ProfileView.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 {
const analytics = await ProfileAnalytics.findOne({ userId });
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const weeklyViews = await ProfileView.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
};