const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const dotenv = require('dotenv'); const morgan = require('morgan'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const fal = require("@fal-ai/serverless-client"); const { createCanvas, loadImage } = require('canvas'); const sharp = require('sharp'); const PDFDocument = require('pdfkit'); const archiver = require('archiver'); const json = JSON; const axios = require('axios'); const mqtt = require('mqtt'); const net = require('net'); const { SerialPort } = require('serialport'); const cloudinary = require('cloudinary').v2; const { CloudinaryStorage } = require('multer-storage-cloudinary'); const multer = require('multer'); // Force load .env dotenv.config({ path: __dirname + '/.env' }); // Models const Store = require('./models/Store'); const PayoutMethod = require('./models/PayoutMethod'); const Transaction = require('./models/Transaction'); // Payment Gateways const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const paypal = require('@paypal/checkout-server-sdk'); console.log('STRIPE_SECRET_KEY exists:', !!process.env.STRIPE_SECRET_KEY); console.log('PAYPAL_CLIENT_ID exists:', !!process.env.PAYPAL_CLIENT_ID); const app = express(); const PORT = process.env.PORT || 7860; // Configure FAL.AI fal.config({ credentials: process.env.FAL_AI_KEY, }); const patternPrompts = { geometric: "Islamic geometric carpet design, intricate 8-pointed star patterns, arabesque motifs, rich colors, Persian rug style, high resolution, realistic texture, detailed weaving", floral: "Persian floral carpet design, elaborate flowers and vines, medallion center, rich jewel tones, traditional oriental rug, high resolution, realistic fabric texture, hand-knotted", abstract: "Modern abstract carpet design, contemporary art style, bold colors, flowing shapes, luxury interior design rug, high resolution, realistic wool texture, premium quality", traditional: "Antique traditional carpet design, classic oriental pattern, rich earth tones, detailed borders, hand-knotted appearance, high resolution, realistic weave, heritage style" }; const fs = require('fs'); const path = require('path'); // Canvas and SVG to PDF (للتصدير) let SVGtoPDF; try { SVGtoPDF = require('svg-to-pdfkit'); console.log('✅ SVG to PDF loaded successfully'); } catch (e) { console.log('⚠️ SVG to PDF not available:', e.message); } // PayPal Client let paypalClient; if (process.env.PAYPAL_MODE === 'sandbox') { paypalClient = new paypal.core.PayPalHttpClient( new paypal.core.SandboxEnvironment( process.env.PAYPAL_CLIENT_ID, process.env.PAYPAL_CLIENT_SECRET ) ); } else if (process.env.PAYPAL_CLIENT_ID && process.env.PAYPAL_CLIENT_SECRET) { paypalClient = new paypal.core.PayPalHttpClient( new paypal.core.LiveEnvironment( process.env.PAYPAL_CLIENT_ID, process.env.PAYPAL_CLIENT_SECRET ) ); } // Middleware // ================ CORS Configuration ================ const allowedOrigins = [ 'http://localhost:3000', 'http://localhost:5000', 'http://localhost:7860', 'http://127.0.0.1:3000', 'http://127.0.0.1:5000', 'http://127.0.0.1:7860', 'https://naseej-system.vercel.app', 'https://mgzon-naseej-backend.hf.space', 'https://naseej-socket-server.onrender.com', 'https://naseej-socket-server.railway.app', process.env.FRONTEND_URL, process.env.SOCKET_SERVER_URL ].filter(Boolean); // إزالة القيم undefined // دالة التحقق من origin const corsOptionsDelegate = function (req, callback) { const origin = req.header('Origin'); // السماح للطلبات بدون origin (مثل Postman, mobile apps) if (!origin) { return callback(null, { origin: false }); } // التحقق إذا كان الـ origin مسموحاً if (allowedOrigins.includes(origin)) { callback(null, { origin: true, credentials: true }); } else { console.log('❌ CORS blocked origin:', origin); // في التطوير، نسمح بكل الـ origins if (process.env.NODE_ENV === 'development') { console.log('⚠️ Development mode: allowing all origins'); callback(null, { origin: true, credentials: true }); } else { callback(new Error('Not allowed by CORS')); } } }; // تطبيق CORS app.use(cors(corsOptionsDelegate)); app.options('*', cors(corsOptionsDelegate)); // Pre-flight requests // باقي middleware app.use(express.json()); app.use(morgan('dev')); // ================ Dashboard & Static Files ================ // خدمة الملفات الثابتة (للوحة التحكم) app.use(express.static('public')); // متغير لتخزين آخر طلب (للوحة التحكم) let lastRequest = { path: '/', method: 'GET', timestamp: new Date() }; // Middleware لتسجيل آخر طلب app.use((req, res, next) => { // تجاهل طلبات التحقق من الصحة والموارد الثابتة لتجنب التضخم if (!req.path.startsWith('/api/dashboard') && req.path !== '/') { lastRequest = { path: req.path, method: req.method, timestamp: new Date() }; } next(); }); // ================ API Routes للوحة التحكم (Dashboard) ================ // جلب إحصائيات لوحة التحكم app.get('/api/dashboard/stats', async (req, res) => { try { const usersCount = await User.countDocuments(); const productsCount = await Product.countDocuments(); const ordersCount = await Order.countDocuments(); let dbStatus = 'disconnected'; if (mongoose.connection.readyState === 1) { dbStatus = 'connected'; } res.json({ usersCount, productsCount, ordersCount, dbStatus, lastRequest }); } catch (error) { console.error('Dashboard stats error:', error); res.status(500).json({ error: error.message }); } }); // جلب معلومات البيئة app.get('/api/dashboard/env', (req, res) => { const memoryUsage = process.memoryUsage(); res.json({ nodeVersion: process.version, platform: process.platform, memoryUsage: `${Math.round(memoryUsage.rss / 1024 / 1024)} MB`, uptime: `${Math.floor(process.uptime() / 60)} minutes` }); }); // ================ Welcome Page (Dashboard HTML) ================ app.get('/', (req, res) => { res.sendFile('index.html', { root: 'public' }); }); // Configure Cloudinary cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET }); // Configure Multer with Cloudinary storage const storage = new CloudinaryStorage({ cloudinary: cloudinary, params: async (req, file) => { const isImage = file.mimetype.startsWith('image/'); const isVideo = file.mimetype.startsWith('video/'); const isAudio = file.mimetype.startsWith('audio/'); let resourceType = 'auto'; let folder = 'naseej/products'; let transformation = []; if (isImage) { resourceType = 'image'; transformation = [ { quality: 'auto' }, { fetch_format: 'auto' }, { width: 1200, height: 1200, crop: 'limit' } ]; } else if (isVideo) { resourceType = 'video'; transformation = [{ quality: 'auto' }]; } else if (isAudio) { resourceType = 'video'; // Cloudinary يتعامل مع الصوت كـ video folder = 'naseej/audio'; transformation = []; } let allowedFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif']; if (isVideo) allowedFormats = ['mp4', 'webm', 'mov']; if (isAudio) allowedFormats = ['mp3', 'wav', 'ogg', 'm4a', 'webm']; return { folder: folder, allowed_formats: allowedFormats, resource_type: resourceType, transformation: transformation, public_id: `${Date.now()}-${Math.round(Math.random() * 1e9)}` }; } }); const upload = multer({ storage: storage, limits: { fileSize: 50 * 1024 * 1024 // 50MB limit for videos and audio }, fileFilter: (req, file, cb) => { const allowedTypes = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'video/mp4', 'video/webm', 'video/mov', 'audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/m4a' ]; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Invalid file type. Only images, videos, and audio files are allowed.')); } } }); // MongoDB Connection if (!process.env.MONGODB_URI) { console.error('❌ MONGODB_URI is not defined in environment variables'); process.exit(1); } mongoose.connect(process.env.MONGODB_URI) .then(() => console.log('✅ MongoDB connected')) .catch(err => console.error('❌ MongoDB connection error:', err)); // ================ Models ================ // User Model const userSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, role: { type: String, enum: ['admin', 'seller', 'customer'], default: 'customer' }, phone: { type: String, default: '' }, followingStores: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Store', default: [] }], // ← أضف هذا السطر address: { type: String, default: '' }, storeId: { type: mongoose.Schema.Types.ObjectId, ref: 'Store', default: null }, canSell: { type: Boolean, default: false }, wishlist: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Product', default: [] }], createdAt: { type: Date, default: Date.now }, lastSeen: { type: Date, default: Date.now }, isOnline: { type: Boolean, default: false } }); const User = mongoose.model('User', userSchema); // Product Model const productSchema = new mongoose.Schema({ name: { type: String, required: true }, storeId: { type: mongoose.Schema.Types.ObjectId, ref: 'Store', required: true }, ownerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, status: { type: String, enum: ['active', 'pending', 'rejected', 'inactive'], default: 'pending' }, slug: { type: String, required: true, unique: true }, category: { type: String, enum: ['carpet', 'textile'], required: true }, subcategory: { type: String, default: '' }, material: { type: String, default: '' }, size: { type: String, default: '' }, color: { type: String, default: '' }, price: { type: Number, required: true }, oldPrice: { type: Number, default: 0 }, quantity: { type: Number, default: 0 }, imageUrl: { type: String, default: '' }, images: [{ type: String, default: [] }], description: { type: String, default: '' }, features: [{ type: String, default: [] }], tags: [{ type: String, default: [] }], rating: { type: Number, default: 0 }, reviewCount: { type: Number, default: 0 }, views: { type: Number, default: 0 }, soldCount: { type: Number, default: 0 }, isFeatured: { type: Boolean, default: false }, isNew: { type: Boolean, default: false }, discount: { type: Number, default: 0 }, inStock: { type: Boolean, default: true }, costPrice: { type: Number, default: 0 }, // سعر الشراء profitPerItem: { type: Number, default: 0 }, // الربح لكل قطعة totalProfit: { type: Number, default: 0 }, // إجمالي الربح من المبيعات totalCost: { type: Number, default: 0 }, // إجمالي التكلفة createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); productSchema.pre('save', function (next) { if (this.isModified('name')) { const storePrefix = this.storeId ? this.storeId.toString().slice(-6) : ''; this.slug = `${storePrefix}-${this.name .toLowerCase() .replace(/[^a-z0-9\u0621-\u064A]+/g, '-') .replace(/^-|-$/g, '')}`; } this.updatedAt = Date.now(); this.inStock = this.quantity > 0; next(); }); const Product = mongoose.model('Product', productSchema); // Review Model const reviewSchema = new mongoose.Schema({ productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, rating: { type: Number, required: true, min: 1, max: 5 }, text: { type: String, required: true }, timestamp: { type: Date, default: Date.now } }); const Review = mongoose.model('Review', reviewSchema); // Customer Model const customerSchema = new mongoose.Schema({ name: { type: String, required: true }, phone: { type: String, required: true }, address: { type: String, default: '' }, email: { type: String, default: '' }, registeredAt: { type: Date, default: Date.now } }); const Customer = mongoose.model('Customer', customerSchema); // Invoice Model const invoiceItemSchema = new mongoose.Schema({ productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true }, quantity: { type: Number, required: true }, unitPrice: { type: Number, required: true }, subtotal: { type: Number, required: true } }); const invoiceSchema = new mongoose.Schema({ invoiceNumber: { type: String, required: true, unique: true }, sellerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, customerId: { type: mongoose.Schema.Types.ObjectId, ref: 'Customer', required: true }, items: [invoiceItemSchema], totalAmount: { type: Number, required: true }, status: { type: String, enum: ['paid', 'unpaid', 'cancelled'], default: 'unpaid' }, date: { type: Date, default: Date.now } }); const Invoice = mongoose.model('Invoice', invoiceSchema); // Coupon Model const couponSchema = new mongoose.Schema({ code: { type: String, required: true, unique: true }, discountType: { type: String, enum: ['percentage', 'fixed'], default: 'percentage' }, discountValue: { type: Number, required: true }, minOrderAmount: { type: Number, default: 0 }, maxDiscount: { type: Number, default: 0 }, validFrom: Date, validTo: Date, usageLimit: { type: Number, default: 1 }, usedCount: { type: Number, default: 0 }, isActive: { type: Boolean, default: true } }); const Coupon = mongoose.model('Coupon', couponSchema); // ShippingRate Model const shippingRateSchema = new mongoose.Schema({ city: { type: String, required: true }, district: String, cost: { type: Number, required: true }, estimatedDays: { type: Number, default: 3 }, isActive: { type: Boolean, default: true } }); const ShippingRate = mongoose.model('ShippingRate', shippingRateSchema); // Order Model const orderItemSchema = new mongoose.Schema({ productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' }, name: String, quantity: Number, unitPrice: Number, subtotal: Number, storeId: { type: mongoose.Schema.Types.ObjectId, ref: 'Store' }, paymentMethod: { type: String, default: 'cash' } }); const trackingHistorySchema = new mongoose.Schema({ status: String, location: String, timestamp: { type: Date, default: Date.now }, note: String }); const orderSchema = new mongoose.Schema({ orderNumber: { type: String, required: true, unique: true }, customerId: { type: mongoose.Schema.Types.ObjectId, ref: 'Customer', required: true }, items: [orderItemSchema], shippingAddress: { street: String, city: String, district: String, phone: String, notes: String, email: String }, integrationSyncResults: [{ storeId: { type: mongoose.Schema.Types.ObjectId, ref: 'Store' }, service: { type: String }, status: { type: String, enum: ['success', 'failed', 'pending'] }, trackingNumber: { type: String }, error: { type: String }, syncedAt: { type: Date, default: Date.now } }], shippingCost: { type: Number, default: 0 }, discount: { type: Number, default: 0 }, couponCode: { type: String, default: '' }, subtotal: { type: Number, required: true }, totalAmount: { type: Number, required: true }, paymentMethod: { type: String, enum: ['cash', 'paypal', 'card', 'bank', 'vodafone_cash', 'instapay', 'fawry'], default: 'cash' }, paymentDetails: { method: String, merchantPhone: String, bankDetails: Object, status: String, transactionId: String, requestedAt: Date, paidAt: Date }, paymentStatus: { type: String, enum: ['pending', 'paid', 'failed', 'refunded'], default: 'pending' }, orderStatus: { type: String, enum: ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'returned', 'refunded'], default: 'pending' }, trackingNumber: { type: String, default: '' }, trackingHistory: [trackingHistorySchema], paypalOrderId: { type: String, default: '' }, paypalCaptureId: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, deliveredAt: Date, cancelledAt: Date }); const Order = mongoose.model('Order', orderSchema); // ================ Production Partner API (Public) ================ // نموذج لطلب الإنتاج من مصنع شريك const productionOrderSchema = new mongoose.Schema({ orderId: { type: mongoose.Schema.Types.ObjectId, ref: 'Order', required: true }, designId: { type: mongoose.Schema.Types.ObjectId, ref: 'Design', required: true }, partnerId: { type: String, required: true }, // معرف المصنع الشريك status: { type: String, enum: ['pending', 'approved', 'in_progress', 'completed', 'shipped', 'delivered', 'cancelled'], default: 'pending' }, trackingHistory: [{ status: { type: String }, note: { type: String }, location: { type: String }, timestamp: { type: Date, default: Date.now } }], progress: { type: Number, default: 0, min: 0, max: 100 }, shippedAt: { type: Date }, gcode: { type: String, required: true }, // تعليمات الإنتاج estimatedCompletion: { type: Date }, actualCompletion: { type: Date }, trackingNumber: { type: String, default: '' }, shippingCompany: { type: String, default: '' }, cost: { type: Number, required: true }, // تكلفة الإنتاج الفعلية partnerNotes: { type: String, default: '' }, webhookUrl: { type: String, default: '' }, // عنوان إشعارات المصنع createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); const ProductionOrder = mongoose.model('ProductionOrder', productionOrderSchema); // نموذج لبيانات المصنع الشريك const partnerFactorySchema = new mongoose.Schema({ name: { type: String, required: true }, apiKey: { type: String, required: true, unique: true }, apiSecret: { type: String, required: true }, webhookUrl: { type: String, default: '' }, apiEndpoint: { type: String, default: '' }, // URL الخاص بالمصنع webhookUrl: { type: String, default: '' }, // URL الذي يستقبل إشعارات المصنع webhookSecret: { type: String, default: '' }, // مفتاح للتحقق من webhook capabilities: { maxWidth: { type: Number, default: 400 }, maxHeight: { type: Number, default: 400 }, materials: [{ type: String }], // المواد التي يدعمها patterns: [{ type: String }] // النقوش التي يدعمها }, pricing: { basePricePerSqm: { type: Number, default: 100 }, materialMultiplier: { type: Map, of: Number, default: {} }, complexityMultiplier: { type: Map, of: Number, default: {} } }, isActive: { type: Boolean, default: true }, contactInfo: { email: String, phone: String, address: String }, createdAt: { type: Date, default: Date.now } }); const PartnerFactory = mongoose.model('PartnerFactory', partnerFactorySchema); // ================ AI Design Models ================ // Design Model - تخزين التصاميم const designSchema = new mongoose.Schema({ name: { type: String, required: true }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, dimensions: { width: { type: Number, required: true }, // cm height: { type: Number, required: true }, // cm unit: { type: String, default: 'cm' } }, colors: { primary: { type: String, required: true }, secondary: [{ type: String }], accent: [{ type: String }] }, pattern: { type: { type: String, enum: ['geometric', 'floral', 'abstract', 'custom', 'traditional'], default: 'geometric' }, complexity: { type: Number, min: 1, max: 10, default: 5 }, customSvg: { type: String, default: '' } }, material: { type: { type: String, enum: ['wool', 'silk', 'cotton', 'polyester', 'blend'], required: true }, density: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' }, thickness: { type: Number, default: 1.5 }, weightPerSquareMeter: { type: Number, default: 2.5 } }, aiGenerated: { type: Boolean, default: false }, aiPrompt: { type: String, default: '' }, previewUrl: { type: String, default: '' }, preview3D: { type: { type: String, default: 'simple' }, data: { type: Object, default: {} } }, costEstimate: { materials: { type: Number, default: 0 }, labor: { type: Number, default: 0 }, total: { type: Number, default: 0 } }, productionTime: { type: Number, default: 0 }, gcode: { type: String, default: '' }, status: { type: String, enum: ['draft', 'approved', 'production', 'completed', 'cancelled'], default: 'draft' }, createdAt: { type: Date, default: Date.now }, pythonCode: { type: String, default: '' }, dstCode: { type: String, default: '' }, embCode: { type: String, default: '' }, updatedAt: { type: Date, default: Date.now } }); const Design = mongoose.model('Design', designSchema); // Material Library Model const materialSchema = new mongoose.Schema({ name: { type: String, required: true, unique: true }, category: { type: String, enum: ['wool', 'silk', 'cotton', 'polyester', 'blend'], required: true }, supplier: { type: String, required: true }, pricePerKg: { type: Number, required: true }, pricePerMeter: { type: Number, default: 0 }, availableColors: [{ type: String }], availableQuantities: { type: Number, default: 0 }, thickness: { type: Number, default: 1.5 }, weight: { type: Number, default: 2.5 }, durability: { type: Number, min: 1, max: 10, default: 5 }, softness: { type: Number, min: 1, max: 10, default: 5 }, imageUrl: { type: String, default: '' }, isActive: { type: Boolean, default: true }, createdAt: { type: Date, default: Date.now } }); const Material = mongoose.model('Material', materialSchema); // Pattern Library Model const patternSchema = new mongoose.Schema({ name: { type: String, required: true, unique: true }, category: { type: String, enum: ['geometric', 'floral', 'abstract', 'traditional', 'custom'], required: true }, complexity: { type: Number, min: 1, max: 10, default: 5 }, svgData: { type: String, required: true }, thumbnailUrl: { type: String, default: '' }, previewUrl: { type: String, default: '' }, tags: [{ type: String }], isPublic: { type: Boolean, default: true }, createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, usageCount: { type: Number, default: 0 }, createdAt: { type: Date, default: Date.now } }); const Pattern = mongoose.model('Pattern', patternSchema); // Machine Profile Model const machineSchema = new mongoose.Schema({ name: { type: String, required: true }, type: { type: String, enum: ['cnc_loom', 'jacquard', 'tufting', 'weaving'], required: true }, ipAddress: { type: String, required: true }, port: { type: Number, default: 502 }, protocol: { type: String, enum: ['TCP', 'Serial', 'MQTT'], default: 'TCP' }, status: { type: String, enum: ['online', 'offline', 'busy', 'maintenance'], default: 'offline' }, lastConnection: { type: Date }, capabilities: { maxWidth: { type: Number, default: 400 }, maxHeight: { type: Number, default: 400 }, supportedMaterials: [{ type: String }], supportedPatterns: [{ type: String }] }, isActive: { type: Boolean, default: true } }); const Machine = mongoose.model('Machine', machineSchema); // Production Log Model const productionLogSchema = new mongoose.Schema({ designId: { type: mongoose.Schema.Types.ObjectId, ref: 'Design', required: true }, machineId: { type: mongoose.Schema.Types.ObjectId, ref: 'Machine', required: true }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, status: { type: String, enum: ['started', 'in_progress', 'completed', 'failed'], default: 'started' }, progress: { type: Number, default: 0 }, details: { type: Object, default: {} }, startedAt: { type: Date, default: Date.now }, completedAt: { type: Date } }); const ProductionLog = mongoose.model('ProductionLog', productionLogSchema); // ================ Social Posts Models ================ // Post Model - المنشورات const postSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, storeId: { type: mongoose.Schema.Types.ObjectId, ref: 'Store', default: null }, content: { type: String, required: true }, media: [{ type: { type: String, enum: ['image', 'video', 'audio'], default: 'image' }, url: { type: String, required: true }, thumbnail: { type: String, default: '' } }], hashtags: [{ type: String }], mentions: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], // تفاعلات likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], likesCount: { type: Number, default: 0 }, commentsCount: { type: Number, default: 0 }, sharesCount: { type: Number, default: 0 }, viewsCount: { type: Number, default: 0 }, // إعدادات النشر visibility: { type: String, enum: ['public', 'followers', 'private', 'store_only'], default: 'public' }, isScheduled: { type: Boolean, default: false }, scheduledAt: { type: Date, default: null }, isPinned: { type: Boolean, default: false }, // حالة المنشور status: { type: String, enum: ['published', 'draft', 'archived', 'reported'], default: 'published' }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); const Post = mongoose.model('Post', postSchema); // Comment Model - التعليقات const commentSchema = new mongoose.Schema({ postId: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, parentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment', default: null }, // للردود content: { type: String, required: true }, media: { type: String, default: '' }, likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], likesCount: { type: Number, default: 0 }, status: { type: String, enum: ['published', 'hidden', 'reported'], default: 'published' }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); const Comment = mongoose.model('Comment', commentSchema); // Notification Model - الإشعارات const notificationSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, type: { type: String, enum: ['like', 'comment', 'share', 'follow', 'mention', 'post_approved', 'payment_received'], required: true }, actorId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, postId: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', default: null }, commentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Comment', default: null }, content: { type: String, required: true }, isRead: { type: Boolean, default: false }, createdAt: { type: Date, default: Date.now } }); const Notification = mongoose.model('Notification', notificationSchema); // Story Model - القصص (مثل ستوري انستجرام) const storySchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, storeId: { type: mongoose.Schema.Types.ObjectId, ref: 'Store', default: null }, media: { type: { type: String, enum: ['image', 'video'], required: true }, url: { type: String, required: true } }, duration: { type: Number, default: 24 }, // ساعات views: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], viewsCount: { type: Number, default: 0 }, reactions: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, emoji: { type: String, default: '❤️' } }], expiresAt: { type: Date, default: () => new Date(Date.now() + 24 * 60 * 60 * 1000) }, createdAt: { type: Date, default: Date.now } }); const Story = mongoose.model('Story', storySchema); // ================ Chat Models ================ // Conversation Model - المحادثة بين مستخدمين const conversationSchema = new mongoose.Schema({ participants: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }], participantsDetails: [{ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, lastReadAt: { type: Date, default: Date.now }, isTyping: { type: Boolean, default: false }, typingAt: { type: Date, default: null } }], lastMessage: { text: { type: String, default: '' }, senderId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, sentAt: { type: Date, default: Date.now }, isRead: { type: Boolean, default: false }, type: { type: String, enum: ['text', 'image', 'video', 'audio', 'file'], default: 'text' }, mediaUrl: { type: String, default: '' }, duration: { type: Number, default: null } // مدة الصوت }, settings: { isPinned: { type: Boolean, default: false }, isMuted: { type: Boolean, default: false } }, unreadCount: { type: Number, default: 0 }, isArchived: { type: Boolean, default: false }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); conversationSchema.index({ participants: 1 }); conversationSchema.index({ updatedAt: -1 }); const Conversation = mongoose.model('Conversation', conversationSchema); // Message Model - الرسائل الفردية const messageSchema = new mongoose.Schema({ conversationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Conversation', required: true }, senderId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, receiverId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, text: { type: String, default: '' }, isEdited: { type: Boolean, default: false }, reactions: { type: Map, of: String, default: {} }, replyTo: { type: mongoose.Schema.Types.ObjectId, ref: 'Message', default: null }, type: { type: String, enum: ['text', 'image', 'video', 'audio', 'file'], default: 'text' }, mediaUrl: { type: String, default: '' }, duration: { type: Number, default: null }, isRead: { type: Boolean, default: false }, readAt: { type: Date, default: null }, isDeleted: { type: Boolean, default: false }, type: { type: String, enum: ['text', 'image', 'video', 'audio', 'file'], default: 'text' }, deletedFor: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], createdAt: { type: Date, default: Date.now } }); messageSchema.index({ conversationId: 1, createdAt: -1 }); messageSchema.index({ conversationId: 1, isRead: 1 }); messageSchema.index({ senderId: 1, receiverId: 1 }); const Message = mongoose.model('Message', messageSchema); // ================ Integration Models ================ // User Integration Model - لتخزين بيانات تكامل البائع مع خدمات خارجية const userIntegrationSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, storeId: { type: mongoose.Schema.Types.ObjectId, ref: 'Store', required: true }, service: { type: String, enum: ['bosta', 'talabat', 'fatura', 'paymob', 'vodafone_cash', 'instapay', 'fawry'], required: true }, serviceName: { type: String, required: true }, apiKey: { type: String, default: '' }, apiSecret: { type: String, default: '' }, webhookUrl: { type: String, default: '' }, settings: { autoSyncProducts: { type: Boolean, default: false }, autoSyncOrders: { type: Boolean, default: false }, autoSyncInventory: { type: Boolean, default: false }, shippingRate: { type: Number, default: 0 }, freeShippingThreshold: { type: Number, default: 0 } }, status: { type: String, enum: ['active', 'inactive', 'pending', 'error'], default: 'pending' }, lastSyncAt: { type: Date, default: null }, syncErrors: [{ timestamp: Date, error: String }], createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); const UserIntegration = mongoose.model('UserIntegration', userIntegrationSchema); // Integration Log Model - لتسجيل عمليات التكامل const integrationLogSchema = new mongoose.Schema({ integrationId: { type: mongoose.Schema.Types.ObjectId, ref: 'UserIntegration' }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, action: { type: String, enum: ['sync_products', 'sync_order', 'sync_inventory', 'webhook_received', 'webhook_sent'] }, status: { type: String, enum: ['success', 'failed', 'pending'] }, requestData: { type: Object, default: {} }, responseData: { type: Object, default: {} }, errorMessage: { type: String, default: '' }, createdAt: { type: Date, default: Date.now } }); const IntegrationLog = mongoose.model('IntegrationLog', integrationLogSchema); // MQTT Client Setup let mqttClient = null; function initMQTT() { if (!process.env.MQTT_BROKER_URL) { console.log('⚠️ MQTT not configured, skipping...'); return; } try { mqttClient = mqtt.connect(process.env.MQTT_BROKER_URL, { username: process.env.MQTT_USERNAME, password: process.env.MQTT_PASSWORD, rejectUnauthorized: false }); mqttClient.on('connect', () => { console.log('✅ MQTT Connected to HiveMQ Cloud'); // Subscribe to machine status topics mqttClient.subscribe(`${process.env.MQTT_TOPIC_PREFIX}/+/status`); }); mqttClient.on('message', (topic, message) => { console.log(`📡 MQTT Message: ${topic}`, message.toString()); handleMQTTMessage(topic, message.toString()); }); mqttClient.on('error', (err) => { console.error('❌ MQTT Error:', err); }); } catch (error) { console.error('❌ MQTT Init Error:', error); } } async function handleMQTTMessage(topic, payload) { try { const data = JSON.parse(payload); const machineId = topic.split('/')[2]; if (data.status === 'completed') { const productionLog = await ProductionLog.findOne({ machineId, status: { $in: ['started', 'in_progress'] } }); if (productionLog) { productionLog.status = 'completed'; productionLog.completedAt = new Date(); productionLog.progress = 100; await productionLog.save(); await Design.findByIdAndUpdate(productionLog.designId, { status: 'completed', completedAt: new Date() }); } } else if (data.progress) { await ProductionLog.updateOne( { machineId, status: { $in: ['started', 'in_progress'] } }, { progress: data.progress } ); } } catch (error) { console.error('MQTT message handling error:', error); } } // دالة مزامنة المنتجات مع Bosta async function syncProductWithBosta(product, integration) { try { // هذا مجرد مثال - ستحتاج إلى API الحقيقي لـ Bosta const bostaApiUrl = 'https://api.bosta.co/v2/products'; const productData = { name: product.name, description: product.description, price: product.price, quantity: product.quantity, images: product.images, category: product.category, sku: product._id.toString().slice(-8) }; const response = await axios.post(bostaApiUrl, productData, { headers: { 'Authorization': `Bearer ${integration.apiKey}`, 'Content-Type': 'application/json' } }); // تسجيل العملية const log = new IntegrationLog({ integrationId: integration._id, userId: integration.userId, action: 'sync_products', status: 'success', requestData: productData, responseData: response.data }); await log.save(); return { success: true, data: response.data }; } catch (error) { // تسجيل الخطأ const log = new IntegrationLog({ integrationId: integration._id, userId: integration.userId, action: 'sync_products', status: 'failed', errorMessage: error.message }); await log.save(); return { success: false, error: error.message }; } } // دالة مزامنة الطلب مع Bosta async function syncOrderWithBosta(order, integration, storeOrder) { try { const bostaApiUrl = 'https://api.bosta.co/v2/shipments'; const shipmentData = { type: 'PICKUP', customer: { name: order.shippingAddress.name || order.customerId?.name, phone: order.shippingAddress.phone, email: order.shippingAddress.email }, address: { city: order.shippingAddress.city, district: order.shippingAddress.district, street: order.shippingAddress.street, notes: order.shippingAddress.notes }, items: storeOrder.items.map(item => ({ name: item.name, quantity: item.quantity, price: item.unitPrice })), codAmount: storeOrder.subtotal, orderNumber: order.orderNumber, notes: `Order from Naseej Marketplace - ${order.orderNumber} - Store: ${storeOrder.storeId}` }; const response = await axios.post(bostaApiUrl, shipmentData, { headers: { 'Authorization': `Bearer ${integration.apiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }); const log = new IntegrationLog({ integrationId: integration._id, userId: integration.userId, action: 'sync_order', status: 'success', requestData: shipmentData, responseData: response.data }); await log.save(); return { success: true, trackingNumber: response.data.trackingNumber, data: response.data }; } catch (error) { const log = new IntegrationLog({ integrationId: integration._id, userId: integration.userId, action: 'sync_order', status: 'failed', errorMessage: error.message, requestData: { orderNumber: order.orderNumber, storeId: storeOrder.storeId } }); await log.save(); return { success: false, error: error.message }; } } // Initialize MQTT on server start initMQTT(); // ================ Middleware ================ const authenticateToken = async (req, res, next) => { const token = req.headers['authorization']?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Access denied. No token provided.' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET || 'naseej_secret_key'); req.user = decoded; next(); } catch (error) { return res.status(403).json({ error: 'Invalid or expired token.' }); } }; const isAdmin = (req, res, next) => { if (req.user.role !== 'admin') { return res.status(403).json({ error: 'Admin access required.' }); } next(); }; // ================ Helper Functions ================ function getStatusLocation(status) { const locations = { pending: 'Order Placed', confirmed: 'Order Confirmed', processing: 'Warehouse', shipped: 'On Delivery', delivered: 'Delivered', cancelled: 'Cancelled' }; return locations[status] || status; } function getStatusNote(status) { const notes = { pending: 'Your order has been received', confirmed: 'Your order has been confirmed', processing: 'Your order is being prepared', shipped: 'Your order is on the way', delivered: 'Your order has been delivered' }; return notes[status] || ''; } function getEstimatedDelivery(order) { const created = new Date(order.createdAt); const estimated = new Date(created); estimated.setDate(created.getDate() + 5); return estimated; } function getPaymentMethodName(type) { const names = { bank: 'Bank Transfer', paypal: 'PayPal', vodafone_cash: 'Vodafone Cash', instapay: 'InstaPay', fawry: 'Fawry' }; return names[type] || type; } function getPaymentMethodDescription(method) { switch (method.type) { case 'bank': return `Transfer to ${method.bankDetails?.bankName || 'bank'} account`; case 'paypal': return `Pay with PayPal to ${method.paypalDetails?.email || 'seller'}`; case 'vodafone_cash': return `Pay via Vodafone Cash to ${method.mobileWalletDetails?.phoneNumber || 'seller'}`; case 'instapay': return `Pay via InstaPay to ${method.mobileWalletDetails?.phoneNumber || 'seller'}`; default: return 'Select this payment method'; } } // ================ PayPal Integration ================ async function createPayPalPayment(order) { try { if (!paypalClient) { console.error('PayPal client not initialized'); return null; } const request = new paypal.orders.OrdersCreateRequest(); request.requestBody({ intent: 'CAPTURE', purchase_units: [{ reference_id: order.orderNumber, amount: { currency_code: 'USD', value: ((order.totalAmount) / 50).toFixed(2), breakdown: { item_total: { currency_code: 'USD', value: (order.subtotal / 50).toFixed(2) }, shipping: { currency_code: 'USD', value: (order.shippingCost / 50).toFixed(2) } } }, items: order.items.map(item => ({ name: item.name.substring(0, 127), quantity: item.quantity, unit_amount: { currency_code: 'USD', value: (item.unitPrice / 50).toFixed(2) } })), shipping: { address: { address_line_1: order.shippingAddress.street, admin_area_2: order.shippingAddress.city, country_code: 'EG' } } }], application_context: { return_url: `${process.env.FRONTEND_URL}/order-tracking/${order.orderNumber}`, cancel_url: `${process.env.FRONTEND_URL}/cart`, brand_name: 'Naseej', locale: 'en-EG', shipping_preference: 'SET_PROVIDED_ADDRESS', user_action: 'PAY_NOW' } }); const response = await paypalClient.execute(request); const approvalUrl = response.result.links.find(link => link.rel === 'approve').href; order.paypalOrderId = response.result.id; await order.save(); return approvalUrl; } catch (error) { console.error('PayPal payment creation error:', error.message); return null; } } // ================ Stripe Integration ================ async function createStripePayment(order) { try { if (!process.env.STRIPE_SECRET_KEY) { console.error('Stripe secret key is missing'); return null; } const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: order.items.map(item => ({ price_data: { currency: 'egp', product_data: { name: item.name, images: item.productId?.imageUrl ? [item.productId.imageUrl] : [], }, unit_amount: Math.round(item.unitPrice * 100), }, quantity: item.quantity, })), shipping_options: [ { shipping_rate_data: { type: 'fixed_amount', fixed_amount: { amount: Math.round(order.shippingCost * 100), currency: 'egp', }, display_name: 'Standard Shipping', delivery_estimate: { minimum: { unit: 'business_day', value: 3 }, maximum: { unit: 'business_day', value: 5 }, }, }, }, ], discounts: order.discount > 0 ? [{ coupon: await createStripeCoupon(order.discount, order.subtotal) }] : [], mode: 'payment', success_url: `${process.env.FRONTEND_URL}/order-tracking/${order.orderNumber}?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.FRONTEND_URL}/cart`, customer_email: order.shippingAddress.email, metadata: { orderId: order._id.toString(), orderNumber: order.orderNumber } }); return session.url; } catch (error) { console.error('Stripe payment creation error:', error.message); return null; } } async function createStripeCoupon(discountAmount, subtotal) { try { const percentOff = Math.round((discountAmount / subtotal) * 100); const coupon = await stripe.coupons.create({ percent_off: percentOff, duration: 'once', name: `Order Discount ${percentOff}%` }); return coupon.id; } catch (error) { console.error('Stripe coupon error:', error); return null; } } // ================ Webhooks ================ app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { console.error('Webhook signature verification failed:', err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } switch (event.type) { case 'checkout.session.completed': const session = event.data.object; const order = await Order.findOne({ orderNumber: session.metadata.orderNumber }); if (order) { order.paymentStatus = 'paid'; order.orderStatus = 'confirmed'; await order.save(); console.log(`Order ${order.orderNumber} paid via Stripe`); } break; default: console.log(`Unhandled event type ${event.type}`); } res.json({ received: true }); }); app.post('/api/webhooks/paypal/capture', async (req, res) => { const { orderId, payerId, orderNumber } = req.body; try { const request = new paypal.orders.OrdersCaptureRequest(orderId); request.requestBody({}); const response = await paypalClient.execute(request); if (response.result.status === 'COMPLETED') { const order = await Order.findOne({ orderNumber }); if (order) { order.paymentStatus = 'paid'; order.orderStatus = 'confirmed'; order.paypalCaptureId = response.result.purchase_units[0].payments.captures[0].id; await order.save(); } } res.json({ success: true }); } catch (error) { console.error('PayPal capture error:', error); res.status(500).json({ error: error.message }); } }); // ================ Auth Routes ================ app.post('/api/auth/register', async (req, res) => { try { const { username, email, password, role } = req.body; const existingUser = await User.findOne({ $or: [{ username }, { email }] }); if (existingUser) { return res.status(400).json({ error: 'Username or email already exists.' }); } const hashedPassword = await bcrypt.hash(password, 10); const user = new User({ username, email, password: hashedPassword, role: role || 'seller' }); await user.save(); const token = jwt.sign({ userId: user._id, username: user.username, email: user.email, role: user.role }, process.env.JWT_SECRET || 'naseej_secret_key', { expiresIn: '7d' }); res.status(201).json({ token, user: { id: user._id, username: user.username, email: user.email, role: user.role } }); } catch (error) { console.error('Register error:', error); res.status(500).json({ error: error.message }); } }); // ================ Verify Token for Socket.IO ================ app.get('/api/auth/verify', authenticateToken, async (req, res) => { try { const user = await User.findById(req.user.userId).select('username email'); res.json({ userId: req.user.userId, username: user?.username || req.user.username, email: user?.email }); } catch (error) { console.error('Verify error:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/auth/login', async (req, res) => { try { const { username, password } = req.body; const user = await User.findOne({ username }); if (!user) { return res.status(401).json({ error: 'Invalid credentials.' }); } const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Invalid credentials.' }); } // ✅ تحديث آخر ظهور وحالة الاتصال user.lastSeen = new Date(); user.isOnline = true; await user.save(); const token = jwt.sign({ userId: user._id, username: user.username, email: user.email, role: user.role }, process.env.JWT_SECRET || 'naseej_secret_key', { expiresIn: '7d' }); res.json({ token, user: { id: user._id, username: user.username, email: user.email, role: user.role, lastSeen: user.lastSeen, isOnline: user.isOnline } }); } catch (error) { console.error('Login error:', error); res.status(500).json({ error: error.message }); } }); app.get('/api/auth/me', authenticateToken, async (req, res) => { try { const user = await User.findById(req.user.userId).select('-password'); res.json(user); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/auth/profile', authenticateToken, async (req, res) => { try { const { username, phone, address } = req.body; const user = await User.findByIdAndUpdate( req.user.userId, { username, phone, address }, { new: true } ).select('-password'); res.json(user); } catch (error) { res.status(500).json({ error: error.message }); } }); // تحديث آخر ظهور (Ping) app.post('/api/user/ping', authenticateToken, async (req, res) => { try { const userId = req.user.userId; await User.findByIdAndUpdate(userId, { lastSeen: new Date(), isOnline: true }); res.json({ success: true }); } catch (error) { console.error('Ping error:', error); res.status(500).json({ error: error.message }); } }); // تحديث حالة عدم الاتصال (عند تسجيل الخروج) app.post('/api/user/offline', authenticateToken, async (req, res) => { try { const userId = req.user.userId; await User.findByIdAndUpdate(userId, { isOnline: false, lastSeen: new Date() }); res.json({ success: true }); } catch (error) { console.error('Offline error:', error); res.status(500).json({ error: error.message }); } }); // ================ Product Routes ================ app.get('/api/products', async (req, res) => { try { const products = await Product.find().sort({ createdAt: -1 }); res.json(products); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/products/slug/:slug', async (req, res) => { try { const product = await Product.findOne({ slug: req.params.slug }); if (!product) { return res.status(404).json({ error: 'Product not found' }); } product.views += 1; await product.save(); res.json(product); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/products/:id', async (req, res) => { try { const product = await Product.findById(req.params.id); if (!product) return res.status(404).json({ error: 'Product not found.' }); res.json(product); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/products/:productId/related', async (req, res) => { try { const product = await Product.findById(req.params.productId); if (!product) { return res.status(404).json({ error: 'Product not found' }); } const relatedProducts = await Product.find({ _id: { $ne: product._id }, $or: [ { category: product.category }, { material: product.material }, { subcategory: product.subcategory }, { tags: { $in: product.tags } } ], inStock: true }) .limit(8) .sort({ soldCount: -1, views: -1 }); res.json(relatedProducts); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/products/featured', async (req, res) => { try { const products = await Product.find({ isFeatured: true, inStock: true }) .limit(6) .sort({ createdAt: -1 }); res.json(products); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/products/new-arrivals', async (req, res) => { try { const products = await Product.find({ isNew: true, inStock: true }) .limit(8) .sort({ createdAt: -1 }); res.json(products); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/products/search', async (req, res) => { try { const { q, category, minPrice, maxPrice, sort, page = 1, limit = 20 } = req.query; const query = { inStock: true }; if (q) { query.$or = [ { name: { $regex: q, $options: 'i' } }, { description: { $regex: q, $options: 'i' } }, { tags: { $in: [new RegExp(q, 'i')] } } ]; } if (category && category !== 'all') query.category = category; if (minPrice) query.price = { $gte: parseInt(minPrice) }; if (maxPrice) query.price = { ...query.price, $lte: parseInt(maxPrice) }; let sortOption = { createdAt: -1 }; if (sort === 'price_asc') sortOption = { price: 1 }; if (sort === 'price_desc') sortOption = { price: -1 }; if (sort === 'popular') sortOption = { soldCount: -1 }; if (sort === 'rating') sortOption = { rating: -1 }; const products = await Product.find(query) .sort(sortOption) .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await Product.countDocuments(query); res.json({ products, total, page: parseInt(page), pages: Math.ceil(total / limit) }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/products', authenticateToken, async (req, res) => { try { const user = await User.findById(req.user.userId); if (!user.canSell && user.role !== 'admin') { return res.status(403).json({ error: 'You need to create a store first' }); } const store = await Store.findOne({ ownerId: req.user.userId }); if (!store && user.role !== 'admin') { return res.status(403).json({ error: 'Store not found' }); } const { name, category, subcategory, material, size, color, price, oldPrice, quantity, imageUrl, images, description, features, tags, isFeatured, isNew, discount } = req.body; const storeId = user.role === 'admin' ? req.body.storeId : store._id; const ownerId = req.user.userId; const slug = `${storeId}-${name .toLowerCase() .replace(/[^a-z0-9\u0621-\u064A]+/g, '-') .replace(/^-|-$/g, '')}`; const existingProduct = await Product.findOne({ slug }); if (existingProduct) { return res.status(400).json({ error: 'Product with similar name already exists' }); } const product = new Product({ name, slug, storeId, ownerId, category, subcategory, material, size, color, price, oldPrice, quantity, imageUrl, images, description, features, tags, isFeatured, isNew, discount, status: 'active', // ✅ تغيير: جميع المنتجات تصبح active مباشرة inStock: quantity > 0 }); await product.save(); await Store.findByIdAndUpdate(storeId, { $inc: { 'stats.totalProducts': 1 } }); res.status(201).json(product); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/products/:id', authenticateToken, isAdmin, async (req, res) => { try { const updateData = req.body; if (updateData.name) { updateData.slug = updateData.name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); } updateData.inStock = updateData.quantity > 0; const product = await Product.findByIdAndUpdate(req.params.id, updateData, { new: true }); if (!product) return res.status(404).json({ error: 'Product not found.' }); res.json(product); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/products/:id', authenticateToken, isAdmin, async (req, res) => { try { const product = await Product.findByIdAndDelete(req.params.id); if (!product) return res.status(404).json({ error: 'Product not found.' }); res.json({ message: 'Product deleted successfully.' }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/products/:productId/rate', authenticateToken, async (req, res) => { try { const { rating } = req.body; const product = await Product.findById(req.params.productId); if (!product) { return res.status(404).json({ error: 'Product not found' }); } const newRating = (product.rating * product.reviewCount + rating) / (product.reviewCount + 1); product.rating = Math.round(newRating * 10) / 10; product.reviewCount += 1; await product.save(); res.json({ success: true, rating: product.rating, reviewCount: product.reviewCount }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Review Routes ================ app.get('/api/reviews/product/:slug', async (req, res) => { try { const product = await Product.findOne({ slug: req.params.slug }); if (!product) { return res.status(404).json({ error: 'Product not found' }); } const reviews = await Review.find({ productId: product._id }) .populate('userId', 'username') .sort({ timestamp: -1 }); res.json(reviews); } catch (error) { console.error('Error fetching reviews:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/reviews', authenticateToken, async (req, res) => { try { const { productId, rating, text } = req.body; if (!productId || !rating || !text) { return res.status(400).json({ error: 'Missing required fields' }); } if (rating < 1 || rating > 5) { return res.status(400).json({ error: 'Rating must be between 1 and 5' }); } const product = await Product.findById(productId); if (!product) { return res.status(404).json({ error: 'Product not found' }); } const existingReview = await Review.findOne({ productId, userId: req.user.userId }); if (existingReview) { return res.status(400).json({ error: 'You have already reviewed this product' }); } const review = new Review({ productId, userId: req.user.userId, rating, text }); await review.save(); const allReviews = await Review.find({ productId }); const avgRating = allReviews.reduce((sum, r) => sum + r.rating, 0) / allReviews.length; product.rating = Math.round(avgRating * 10) / 10; product.reviewCount = allReviews.length; await product.save(); res.status(201).json({ success: true, review }); } catch (error) { console.error('Error creating review:', error); res.status(500).json({ error: error.message }); } }); // ================ Customer Routes ================ app.get('/api/customers', authenticateToken, async (req, res) => { try { const customers = await Customer.find().sort({ registeredAt: -1 }); res.json(customers); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/customers', authenticateToken, async (req, res) => { try { const { name, phone, address, email } = req.body; const customer = new Customer({ name, phone, address, email }); await customer.save(); res.status(201).json(customer); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Invoice Routes ================ app.post('/api/invoices', authenticateToken, async (req, res) => { try { const { customerId, items } = req.body; let totalAmount = 0; const invoiceItems = []; for (const item of items) { const product = await Product.findById(item.productId); if (!product) { return res.status(404).json({ error: `Product ${item.productId} not found.` }); } if (product.quantity < item.quantity) { return res.status(400).json({ error: `Insufficient stock for ${product.name}. Available: ${product.quantity}` }); } const subtotal = product.price * item.quantity; totalAmount += subtotal; invoiceItems.push({ productId: product._id, quantity: item.quantity, unitPrice: product.price, subtotal }); product.quantity -= item.quantity; product.inStock = product.quantity > 0; await product.save(); } const invoiceNumber = `INV-${Date.now()}-${Math.floor(Math.random() * 1000)}`; const invoice = new Invoice({ invoiceNumber, sellerId: req.user.userId, customerId, items: invoiceItems, totalAmount }); await invoice.save(); const populatedInvoice = await Invoice.findById(invoice._id) .populate('sellerId', 'username') .populate('customerId', 'name phone') .populate('items.productId', 'name'); res.status(201).json(populatedInvoice); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/invoices', authenticateToken, async (req, res) => { try { const invoices = await Invoice.find() .populate('sellerId', 'username') .populate('customerId', 'name phone') .populate('items.productId', 'name') .sort({ date: -1 }); res.json(invoices); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/invoices/:id', authenticateToken, async (req, res) => { try { const invoice = await Invoice.findById(req.params.id) .populate('sellerId', 'username') .populate('customerId', 'name phone address') .populate('items.productId', 'name category'); if (!invoice) return res.status(404).json({ error: 'Invoice not found.' }); res.json(invoice); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Order Routes ================ app.post('/api/orders', authenticateToken, async (req, res) => { try { const { customerId, items, shippingAddress, paymentMethod, couponCode } = req.body; const customer = await Customer.findById(customerId); if (!customer) { return res.status(404).json({ error: 'Customer not found' }); } let subtotal = 0; const orderItems = []; const storeOrdersMap = new Map(); // لتجميع الطلبات حسب المتجر for (const item of items) { const product = await Product.findById(item.productId); if (!product) { return res.status(404).json({ error: `Product ${item.productId} not found` }); } if (product.quantity < item.quantity) { return res.status(400).json({ error: `Insufficient stock for ${product.name}. Available: ${product.quantity}` }); } const itemTotal = product.price * item.quantity; subtotal += itemTotal; const orderItem = { productId: product._id, name: product.name, quantity: item.quantity, unitPrice: product.price, subtotal: itemTotal, storeId: product.storeId }; orderItems.push(orderItem); // تجميع حسب المتجر if (!storeOrdersMap.has(product.storeId.toString())) { storeOrdersMap.set(product.storeId.toString(), { storeId: product.storeId, items: [], subtotal: 0 }); } const storeOrder = storeOrdersMap.get(product.storeId.toString()); storeOrder.items.push(orderItem); storeOrder.subtotal += itemTotal; product.quantity -= item.quantity; product.inStock = product.quantity > 0; product.soldCount += item.quantity; await product.save(); } let discount = 0; let coupon = null; if (couponCode) { coupon = await Coupon.findOne({ code: couponCode.toUpperCase(), isActive: true, validFrom: { $lte: new Date() }, validTo: { $gte: new Date() } }); if (coupon && coupon.usedCount < coupon.usageLimit && subtotal >= coupon.minOrderAmount) { if (coupon.discountType === 'percentage') { discount = (subtotal * coupon.discountValue) / 100; if (coupon.maxDiscount > 0 && discount > coupon.maxDiscount) { discount = coupon.maxDiscount; } } else { discount = coupon.discountValue; } coupon.usedCount += 1; await coupon.save(); } } const shippingRate = await ShippingRate.findOne({ city: shippingAddress.city, isActive: true }); const shippingCost = shippingRate ? shippingRate.cost : (subtotal >= 1000 ? 0 : 50); const totalAmount = subtotal - discount + shippingCost; const orderNumber = `ORD-${Date.now()}-${Math.floor(Math.random() * 1000)}`; const validPaymentMethods = ['cash', 'paypal', 'card', 'bank', 'vodafone_cash', 'instapay', 'fawry']; if (!validPaymentMethods.includes(paymentMethod)) { return res.status(400).json({ error: `Invalid payment method: ${paymentMethod}` }); } // جلب طريقة الدفع الخاصة بالمتجر let storePaymentMethod = null; if (paymentMethod !== 'cash' && paymentMethod !== 'paypal' && paymentMethod !== 'card') { const store = await Store.findOne({ ownerId: req.user.userId }); if (store) { storePaymentMethod = await PayoutMethod.findOne({ storeId: store._id, type: paymentMethod, status: 'active' }); } } const order = new Order({ orderNumber, customerId, items: orderItems, shippingAddress: { ...shippingAddress, phone: shippingAddress.phone || customer.phone }, shippingCost, discount, couponCode: couponCode || '', subtotal, totalAmount, paymentMethod, paymentStatus: paymentMethod === 'cash' ? 'pending' : 'pending', orderStatus: 'pending', trackingHistory: [{ status: 'pending', location: 'Order placed', note: 'Your order has been received and is pending confirmation' }] }); await order.save(); // ✅ ================ مزامنة الطلب مع التكاملات المفعلة ================ const syncResults = []; for (const [storeId, storeOrder] of storeOrdersMap) { // جلب التكاملات النشطة لهذا المتجر const integrations = await UserIntegration.find({ storeId: storeId, status: 'active', 'settings.autoSyncOrders': true }); for (const integration of integrations) { try { let result; switch (integration.service) { case 'bosta': result = await syncOrderWithBosta(order, integration, storeOrder); break; case 'talabat': // result = await syncOrderWithTalabat(order, integration, storeOrder); break; case 'fatura': // result = await syncOrderWithFatura(order, integration, storeOrder); break; default: console.log(`No handler for service: ${integration.service}`); } if (result?.success) { syncResults.push({ storeId, service: integration.service, status: 'success', trackingNumber: result.trackingNumber }); // تحديث رقم التتبع في الطلب الرئيسي if (result.trackingNumber && !order.trackingNumber) { order.trackingNumber = result.trackingNumber; await order.save(); } } else { syncResults.push({ storeId, service: integration.service, status: 'failed', error: result?.error }); } } catch (error) { console.error(`Sync error for store ${storeId}:`, error); syncResults.push({ storeId, service: integration.service, status: 'failed', error: error.message }); } } } // تسجيل نتائج المزامنة في سجل الطلب if (syncResults.length > 0) { order.integrationSyncResults = syncResults; await order.save(); } let paymentUrl = null; let paymentInstruction = null; // معالجة طرق الدفع المختلفة if (paymentMethod === 'paypal') { paymentUrl = await createPayPalPayment(order); } else if (paymentMethod === 'card') { paymentUrl = await createStripePayment(order); } else if (paymentMethod === 'vodafone_cash' && storePaymentMethod) { paymentInstruction = await createVodafoneCashPayment(order, storePaymentMethod); } else if (paymentMethod === 'instapay' && storePaymentMethod) { paymentInstruction = await createInstaPayPayment(order, storePaymentMethod); } else if (paymentMethod === 'bank' && storePaymentMethod) { paymentInstruction = await createBankTransferPayment(order, storePaymentMethod); } res.status(201).json({ success: true, order, paymentUrl, paymentInstruction, syncResults // إرسال نتائج المزامنة للفرونت }); } catch (error) { console.error('Order creation error:', error); res.status(500).json({ error: error.message }); } }); app.get('/api/orders', authenticateToken, isAdmin, async (req, res) => { try { const { status, fromDate, toDate, page = 1, limit = 20 } = req.query; const query = {}; if (status) query.orderStatus = status; if (fromDate || toDate) { query.createdAt = {}; if (fromDate) query.createdAt.$gte = new Date(fromDate); if (toDate) query.createdAt.$lte = new Date(toDate); } const orders = await Order.find(query) .populate('customerId', 'name phone') .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await Order.countDocuments(query); res.json({ orders, total, page: parseInt(page), pages: Math.ceil(total / limit) }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/orders/my-orders', authenticateToken, async (req, res) => { try { console.log('My orders requested - user:', req.user); let customer = await Customer.findOne({ email: req.user.email }); if (!customer) { const user = await User.findById(req.user.userId); if (user && user.email) { customer = await Customer.findOne({ email: user.email }); } } if (!customer) { console.log('No customer found for email:', req.user.email); return res.json([]); } const orders = await Order.find({ customerId: customer._id }) .populate('items.productId', 'name imageUrl') .sort({ createdAt: -1 }); const formattedOrders = orders.map(order => ({ ...order._doc, items: order.items.map(item => ({ name: item.productId?.name || item.name || 'Product', quantity: item.quantity, unitPrice: item.unitPrice, subtotal: item.subtotal })) })); res.json(formattedOrders); } catch (error) { console.error('Error fetching my orders:', error); res.status(500).json({ error: error.message }); } }); app.get('/api/orders/:orderId', authenticateToken, async (req, res) => { try { const order = await Order.findById(req.params.orderId) .populate('customerId', 'name phone email') .populate('items.productId', 'name imageUrl'); if (!order) { return res.status(404).json({ error: 'Order not found' }); } const customer = await Customer.findOne({ email: req.user.email }); if (req.user.role !== 'admin' && (!customer || order.customerId._id.toString() !== customer._id.toString())) { return res.status(403).json({ error: 'Unauthorized' }); } res.json(order); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/orders/:orderId/status', authenticateToken, isAdmin, async (req, res) => { try { const { status, trackingNumber, note } = req.body; const order = await Order.findById(req.params.orderId); if (!order) { return res.status(404).json({ error: 'Order not found' }); } order.orderStatus = status; if (trackingNumber) order.trackingNumber = trackingNumber; order.trackingHistory.push({ status, location: getStatusLocation(status), note: note || getStatusNote(status) }); if (status === 'delivered') { order.deliveredAt = new Date(); order.paymentStatus = 'paid'; } if (status === 'cancelled') { order.cancelledAt = new Date(); for (const item of order.items) { await Product.findByIdAndUpdate(item.productId, { $inc: { quantity: item.quantity } }); } } await order.save(); res.json({ success: true, order }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/orders/track/:orderNumber', async (req, res) => { try { const order = await Order.findOne({ orderNumber: req.params.orderNumber }) .populate('items.productId', 'name imageUrl price'); if (!order) { return res.status(404).json({ error: 'Order not found' }); } const formattedItems = order.items.map(item => ({ name: item.productId?.name || item.name || 'Product', quantity: item.quantity, unitPrice: item.unitPrice, subtotal: item.subtotal })); res.json({ orderNumber: order.orderNumber, orderStatus: order.orderStatus, trackingNumber: order.trackingNumber, trackingHistory: order.trackingHistory, items: formattedItems, subtotal: order.subtotal, discount: order.discount, shippingCost: order.shippingCost, totalAmount: order.totalAmount, shippingAddress: order.shippingAddress, estimatedDelivery: order.orderStatus === 'shipped' ? getEstimatedDelivery(order) : null, createdAt: order.createdAt }); } catch (error) { console.error('Track order error:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/orders/shipping-cost', async (req, res) => { try { const { city, district, subtotal } = req.body; let shippingRate = await ShippingRate.findOne({ city, isActive: true }); if (!shippingRate && district) { shippingRate = await ShippingRate.findOne({ city, district, isActive: true }); } let shippingCost = shippingRate ? shippingRate.cost : 50; if (subtotal >= 1000) { shippingCost = 0; } res.json({ shippingCost, estimatedDays: shippingRate?.estimatedDays || 3 }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/coupons/validate', async (req, res) => { try { const { code, subtotal } = req.body; const coupon = await Coupon.findOne({ code: code.toUpperCase(), isActive: true, validFrom: { $lte: new Date() }, validTo: { $gte: new Date() } }); if (!coupon) { return res.status(404).json({ error: 'Invalid or expired coupon' }); } if (coupon.usedCount >= coupon.usageLimit) { return res.status(400).json({ error: 'Coupon usage limit reached' }); } if (subtotal < coupon.minOrderAmount) { return res.status(400).json({ error: `Minimum order amount for this coupon is ${coupon.minOrderAmount} EGP` }); } let discount = 0; if (coupon.discountType === 'percentage') { discount = (subtotal * coupon.discountValue) / 100; if (coupon.maxDiscount > 0 && discount > coupon.maxDiscount) { discount = coupon.maxDiscount; } } else { discount = coupon.discountValue; } res.json({ discount, coupon }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Dashboard Stats ================ app.get('/api/stats', authenticateToken, isAdmin, async (req, res) => { try { console.log('Stats endpoint called by user:', req.user?.username); const totalProducts = await Product.countDocuments(); const totalCustomers = await Customer.countDocuments(); const totalInvoices = await Invoice.countDocuments(); const totalOrders = await Order.countDocuments(); const invoices = await Invoice.find(); const totalSales = invoices.reduce((sum, inv) => sum + inv.totalAmount, 0); const orders = await Order.find(); const totalOrderValue = orders.reduce((sum, ord) => sum + ord.totalAmount, 0); const lowStockProducts = await Product.find({ quantity: { $lt: 10 } }); const productSales = {}; for (const order of orders) { for (const item of order.items) { const productId = item.productId?.toString(); if (productId) { if (!productSales[productId]) { productSales[productId] = { quantity: 0, revenue: 0 }; } productSales[productId].quantity += item.quantity; productSales[productId].revenue += item.subtotal; } } } const topProducts = await Promise.all( Object.entries(productSales) .sort((a, b) => b[1].quantity - a[1].quantity) .slice(0, 5) .map(async ([id, data]) => { const product = await Product.findById(id); return { name: product?.name || 'Unknown', ...data }; }) ); res.json({ totalProducts, totalCustomers, totalInvoices, totalOrders, totalSales, totalOrderValue, lowStockCount: lowStockProducts.length, lowStockProducts, topProducts }); } catch (error) { console.error('Stats error:', error); res.status(500).json({ error: error.message }); } }); // ================ Store Routes ================ app.get('/api/stores', async (req, res) => { try { const stores = await Store.find({ 'settings.isActive': true }) .select('name slug logo description stats') .sort({ createdAt: -1 }); res.json(stores); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/stores/:slug', async (req, res) => { try { const store = await Store.findOne({ slug: req.params.slug, 'settings.isActive': true }) .populate('ownerId', 'username email'); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const products = await Product.find({ storeId: store._id, status: 'active', inStock: true }).sort({ createdAt: -1 }); res.json({ store, products }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/stores/:storeSlug/product/:productSlug', async (req, res) => { try { const { storeSlug, productSlug } = req.params; console.log('🔍 Searching for product:', { storeSlug, productSlug }); // جلب المتجر const store = await Store.findOne({ slug: storeSlug }); if (!store) { console.log('❌ Store not found:', storeSlug); return res.status(404).json({ error: 'Store not found' }); } console.log('✅ Store found:', store._id, store.name); // طرق متعددة للبحث عن المنتج let product = null; // 1. البحث بالـ slug الكامل (الذي يحتوي على معرف المتجر) const fullSlugPattern = new RegExp(`^${store._id}.*${productSlug}`, 'i'); product = await Product.findOne({ $or: [ { slug: productSlug }, // الرابط المباشر { slug: fullSlugPattern }, // slug يبدأ بمعرف المتجر { slug: { $regex: productSlug, $options: 'i' } }, // يحتوي على النص { name: { $regex: `^${productSlug.replace(/-/g, ' ')}`, $options: 'i' } } // يبدأ بالاسم ], storeId: store._id }); // 2. إذا لم يتم العثور، جرب البحث في كل المتاجر if (!product) { product = await Product.findOne({ slug: { $regex: productSlug, $options: 'i' } }); } // 3. إذا لم يتم العثور، جرب البحث بالاسم بالكامل if (!product) { const namePattern = productSlug.replace(/-/g, ' '); product = await Product.findOne({ name: { $regex: namePattern, $options: 'i' }, storeId: store._id }); } if (!product) { console.log('❌ Product not found for:', productSlug); return res.status(404).json({ error: 'Product not found in this store' }); } console.log('✅ Product found:', product._id, product.name, 'Slug:', product.slug); // زيادة عدد المشاهدات product.views += 1; await product.save(); res.json({ store, product }); } catch (error) { console.error('❌ Error fetching store product:', error); res.status(500).json({ error: error.message }); } }); // إنشاء متجر جديد (للمستخدم) app.post('/api/stores', authenticateToken, async (req, res) => { try { const { name, description, contact, socialLinks, logo, coverImage } = req.body; if (!name) { return res.status(400).json({ error: 'Store name is required' }); } const existingStore = await Store.findOne({ ownerId: req.user.userId }); if (existingStore) { return res.status(400).json({ error: 'You already have a store' }); } // إنشاء slug فريد const slug = name .toLowerCase() .replace(/[^a-z0-9\u0621-\u064A]+/g, '-') .replace(/^-|-$/g, '') + '-' + Date.now(); const store = new Store({ name, slug, ownerId: req.user.userId, description: description || '', logo: logo || '', coverImage: coverImage || '', contact: contact || { phone: '', email: '', address: '', city: '' }, socialLinks: socialLinks || {}, 'settings.isActive': true, stats: { totalProducts: 0, totalSales: 0, totalRevenue: 0, views: 0 } }); await store.save(); await User.findByIdAndUpdate(req.user.userId, { storeId: store._id, canSell: true, role: 'seller' }); res.status(201).json(store); } catch (error) { console.error('Error creating store:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/stores/:slug', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ slug: req.params.slug }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } if (store.ownerId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } Object.assign(store, req.body); await store.save(); res.json(store); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Payout Methods Routes ================ app.post('/api/payouts/methods', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const { type, isDefault, bankDetails, paypalDetails, mobileWalletDetails } = req.body; if (isDefault) { await PayoutMethod.updateMany( { storeId: store._id, isDefault: true }, { isDefault: false } ); } const payoutMethod = new PayoutMethod({ storeId: store._id, type, isDefault: isDefault || false, bankDetails: bankDetails || {}, paypalDetails: paypalDetails || {}, mobileWalletDetails: mobileWalletDetails || {}, status: 'active' }); await payoutMethod.save(); res.status(201).json(payoutMethod); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/payouts/methods', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const methods = await PayoutMethod.find({ storeId: store._id }); res.json(methods); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/payouts/methods/:methodId', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const method = await PayoutMethod.findById(req.params.methodId); if (!method) { return res.status(404).json({ error: 'Method not found' }); } if (method.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } const { type, isDefault, bankDetails, paypalDetails, mobileWalletDetails } = req.body; if (isDefault && !method.isDefault) { await PayoutMethod.updateMany( { storeId: store._id, isDefault: true }, { isDefault: false } ); } Object.assign(method, { type, isDefault, bankDetails, paypalDetails, mobileWalletDetails }); await method.save(); res.json(method); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/payouts/methods/:methodId/default', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const method = await PayoutMethod.findById(req.params.methodId); if (!method) { return res.status(404).json({ error: 'Method not found' }); } if (method.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } await PayoutMethod.updateMany( { storeId: store._id, isDefault: true }, { isDefault: false } ); method.isDefault = true; await method.save(); res.json({ success: true, method }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/payouts/methods/:methodId', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const method = await PayoutMethod.findById(req.params.methodId); if (!method) { return res.status(404).json({ error: 'Method not found' }); } if (method.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } await method.deleteOne(); res.json({ success: true, message: 'Payment method deleted' }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Store Payment Methods (Public) ================ app.get('/api/stores/:storeId/payment-methods', async (req, res) => { try { const store = await Store.findById(req.params.storeId); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const methods = await PayoutMethod.find({ storeId: store._id, status: 'active' }); const formattedMethods = methods.map(method => ({ type: method.type, name: getPaymentMethodName(method.type), isActive: true, description: getPaymentMethodDescription(method), bankDetails: method.bankDetails, paypalDetails: method.paypalDetails, mobileWalletDetails: method.mobileWalletDetails })); res.json(formattedMethods); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Seller Routes ================ app.get('/api/seller/stats', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const products = await Product.find({ storeId: store._id }); const orders = await Order.find({ 'items.storeId': store._id }); const totalRevenue = orders.reduce((sum, order) => { const storeItems = order.items.filter(item => item.storeId?.toString() === store._id.toString()); return sum + storeItems.reduce((s, item) => s + item.subtotal, 0); }, 0); res.json({ totalProducts: products.length, totalSales: orders.length, totalRevenue, storeViews: store.stats?.views || 0, averageRating: store.stats?.averageRating || 0 }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Seller Profit Analytics ================ // جلب إحصائيات الأرباح للبائع app.get('/api/seller/profit-stats', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const products = await Product.find({ storeId: store._id }); // حساب الإحصائيات let totalRevenue = 0; let totalCost = 0; let totalProfit = 0; let totalProductsSold = 0; const productsData = products.map(product => { const productRevenue = (product.price || 0) * (product.soldCount || 0); const productCost = (product.costPrice || 0) * (product.soldCount || 0); const productProfit = productRevenue - productCost; totalRevenue += productRevenue; totalCost += productCost; totalProfit += productProfit; totalProductsSold += product.soldCount || 0; return { productId: product._id, name: product.name, imageUrl: product.imageUrl, price: product.price, costPrice: product.costPrice, soldCount: product.soldCount || 0, remainingStock: product.quantity || 0, totalRevenue: productRevenue, totalCost: productCost, totalProfit: productProfit, profitMargin: productRevenue > 0 ? ((productProfit / productRevenue) * 100).toFixed(1) : '0' }; }); // ترتيب المنتجات حسب الربح const topProfitable = [...productsData] .sort((a, b) => b.totalProfit - a.totalProfit) .slice(0, 5); // المنتجات التي تباع بخسارة const lossMaking = productsData.filter(p => p.totalProfit < 0); // المنتجات منخفضة المخزون const lowStock = productsData.filter(p => p.remainingStock < 10 && p.remainingStock > 0); res.json({ summary: { totalRevenue, totalCost, totalProfit, totalProductsSold, avgProfitMargin: totalRevenue > 0 ? ((totalProfit / totalRevenue) * 100).toFixed(1) : '0', activeProducts: products.length }, products: productsData, topProfitable, lossMaking, lowStock }); } catch (error) { console.error('Profit stats error:', error); res.status(500).json({ error: error.message }); } }); // تحديث تكلفة المنتج app.put('/api/seller/products/:productId/cost', authenticateToken, async (req, res) => { try { const { costPrice } = req.body; const product = await Product.findById(req.params.productId); if (!product) return res.status(404).json({ error: 'Product not found' }); const store = await Store.findOne({ ownerId: req.user.userId }); if (!store || product.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } product.costPrice = costPrice; product.profitPerItem = (product.price - costPrice) > 0 ? (product.price - costPrice) : 0; await product.save(); res.json({ success: true, product }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/seller/products', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const products = await Product.find({ storeId: store._id }).sort({ createdAt: -1 }); res.json(products); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/seller/products', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found. Please create a store first.' }); } const { name, category, subcategory, material, size, color, price, oldPrice, costPrice, quantity, imageUrl, images, description, features, tags, discount } = req.body; const slug = `${store._id}-${name .toLowerCase() .replace(/[^a-z0-9\u0621-\u064A]+/g, '-') .replace(/^-|-$/g, '')}`; // حساب الربح المتوقع لكل قطعة const profitPerItem = (price - (costPrice || 0)); const product = new Product({ name, slug, storeId: store._id, ownerId: req.user.userId, category, subcategory, material, size, color, price, oldPrice, costPrice: costPrice || 0, quantity, imageUrl, images, description, features, tags, discount, profitPerItem: profitPerItem > 0 ? profitPerItem : 0, status: 'active', inStock: quantity > 0 }); await product.save(); await Store.findByIdAndUpdate(store._id, { $inc: { 'stats.totalProducts': 1 } }); res.status(201).json(product); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/seller/products/:productId', authenticateToken, async (req, res) => { try { const product = await Product.findById(req.params.productId); if (!product) return res.status(404).json({ error: 'Product not found' }); const store = await Store.findOne({ ownerId: req.user.userId }); if (!store || product.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } const { costPrice, price, ...otherFields } = req.body; // تحديث الحقول Object.assign(product, otherFields); // تحديث costPrice إذا وجد if (costPrice !== undefined) { product.costPrice = costPrice; } if (price !== undefined) { product.price = price; } // إعادة حساب الربح لكل قطعة product.profitPerItem = (product.price - product.costPrice) > 0 ? (product.price - product.costPrice) : 0; await product.save(); res.json(product); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/seller/products/:productId', authenticateToken, async (req, res) => { try { const product = await Product.findById(req.params.productId); if (!product) return res.status(404).json({ error: 'Product not found' }); const store = await Store.findOne({ ownerId: req.user.userId }); if (!store || product.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } await product.deleteOne(); await Store.findByIdAndUpdate(store._id, { $inc: { 'stats.totalProducts': -1 } }); res.json({ message: 'Product deleted successfully' }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/seller/orders', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) return res.status(404).json({ error: 'Store not found' }); const orders = await Order.find({ 'items.storeId': store._id }) .populate('customerId', 'name phone') .sort({ createdAt: -1 }); const formattedOrders = orders.map(order => ({ ...order._doc, items: order.items.filter(item => item.storeId?.toString() === store._id.toString()) })); res.json(formattedOrders); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/seller/orders/:orderId/status', authenticateToken, async (req, res) => { try { const { status, trackingNumber } = req.body; const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) return res.status(404).json({ error: 'Store not found' }); const order = await Order.findById(req.params.orderId); if (!order) return res.status(404).json({ error: 'Order not found' }); const hasStoreItem = order.items.some(item => item.storeId?.toString() === store._id.toString()); if (!hasStoreItem) return res.status(403).json({ error: 'Unauthorized' }); order.orderStatus = status; if (trackingNumber) order.trackingNumber = trackingNumber; await order.save(); res.json(order); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Seller Routes ================ // جلب معلومات المتجر (للبائع) app.get('/api/seller/store', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) return res.status(404).json({ error: 'Store not found' }); res.json(store); } catch (error) { res.status(500).json({ error: error.message }); } }); // إنشاء أو تحديث المتجر (للبائع) app.put('/api/seller/store', authenticateToken, async (req, res) => { try { console.log('Updating/creating store for user:', req.user.userId); let store = await Store.findOne({ ownerId: req.user.userId }); const { name, description, logo, coverImage, contact, socialLinks, paymentSettings } = req.body; if (!store) { // إنشاء متجر جديد إذا لم يكن موجوداً console.log('No store found, creating new store...'); // التحقق من وجود اسم للمتجر if (!name) { return res.status(400).json({ error: 'Store name is required' }); } // إنشاء slug فريد const slug = name .toLowerCase() .replace(/[^a-z0-9\u0621-\u064A]+/g, '-') .replace(/^-|-$/g, '') + '-' + Date.now(); store = new Store({ name, slug, ownerId: req.user.userId, description: description || '', logo: logo || '', coverImage: coverImage || '', contact: contact || { phone: '', email: '', address: '', city: '' }, socialLinks: socialLinks || { facebook: '', instagram: '', twitter: '', whatsapp: '' }, paymentSettings: paymentSettings || { minimumPayout: 500, autoReleaseDays: 14 }, 'settings.isActive': true, stats: { totalProducts: 0, totalSales: 0, totalRevenue: 0, views: 0 } }); await store.save(); // تحديث بيانات المستخدم await User.findByIdAndUpdate(req.user.userId, { storeId: store._id, canSell: true, role: 'seller' }); console.log('Store created successfully:', store._id); } else { // تحديث المتجر الموجود if (name) store.name = name; if (description !== undefined) store.description = description; if (logo !== undefined) store.logo = logo; if (coverImage !== undefined) store.coverImage = coverImage; if (contact) store.contact = { ...store.contact, ...contact }; if (socialLinks) store.socialLinks = { ...store.socialLinks, ...socialLinks }; if (paymentSettings) store.paymentSettings = { ...store.paymentSettings, ...paymentSettings }; await store.save(); console.log('Store updated successfully:', store._id); } res.json(store); } catch (error) { console.error('Error updating/creating store:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/seller/store', authenticateToken, async (req, res) => { try { console.log('Updating store for user:', req.user.userId); const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const { name, description, logo, coverImage, contact, socialLinks, paymentSettings } = req.body; if (name) store.name = name; if (description !== undefined) store.description = description; if (logo !== undefined) store.logo = logo; if (coverImage !== undefined) store.coverImage = coverImage; if (contact) store.contact = { ...store.contact, ...contact }; if (socialLinks) store.socialLinks = { ...store.socialLinks, ...socialLinks }; if (paymentSettings) store.paymentSettings = { ...store.paymentSettings, ...paymentSettings }; await store.save(); console.log('Store updated successfully:', store._id); res.json(store); } catch (error) { console.error('Error updating store:', error); res.status(500).json({ error: error.message }); } }); // ================ Store Follow Routes ================ // متابعة متجر app.post('/api/stores/:storeSlug/follow', authenticateToken, async (req, res) => { try { const { storeSlug } = req.params; const userId = req.user.userId; // جلب المتجر const store = await Store.findOne({ slug: storeSlug }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } // جلب المستخدم const user = await User.findById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } // التحقق إذا كان المستخدم يتابع المتجر بالفعل if (user.followingStores && user.followingStores.includes(store._id)) { return res.status(400).json({ error: 'Already following this store' }); } // إضافة المتجر إلى قائمة المتابعة if (!user.followingStores) { user.followingStores = []; } user.followingStores.push(store._id); await user.save(); // تحديث إحصائيات المتجر await Store.findByIdAndUpdate(store._id, { $inc: { 'stats.followers': 1 } }); res.json({ success: true, message: `Now following ${store.name}` }); } catch (error) { console.error('Follow store error:', error); res.status(500).json({ error: error.message }); } }); // إلغاء متابعة متجر app.delete('/api/stores/:storeSlug/follow', authenticateToken, async (req, res) => { try { const { storeSlug } = req.params; const userId = req.user.userId; // جلب المتجر const store = await Store.findOne({ slug: storeSlug }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } // جلب المستخدم const user = await User.findById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } // إزالة المتجر من قائمة المتابعة if (user.followingStores) { user.followingStores = user.followingStores.filter( id => id.toString() !== store._id.toString() ); await user.save(); } // تحديث إحصائيات المتجر await Store.findByIdAndUpdate(store._id, { $inc: { 'stats.followers': -1 } }); res.json({ success: true, message: `Unfollowed ${store.name}` }); } catch (error) { console.error('Unfollow store error:', error); res.status(500).json({ error: error.message }); } }); // التحقق من متابعة متجر app.get('/api/stores/:storeSlug/follow/check', authenticateToken, async (req, res) => { try { const { storeSlug } = req.params; const userId = req.user.userId; // جلب المتجر const store = await Store.findOne({ slug: storeSlug }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } // جلب المستخدم const user = await User.findById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } const isFollowing = user.followingStores && user.followingStores.some(id => id.toString() === store._id.toString()); res.json({ following: isFollowing }); } catch (error) { console.error('Check follow error:', error); res.status(500).json({ error: error.message }); } }); // جلب المتاجر التي يتابعها المستخدم app.get('/api/user/following-stores', authenticateToken, async (req, res) => { try { const user = await User.findById(req.user.userId).populate('followingStores'); res.json(user?.followingStores || []); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Payout Transactions Routes ================ app.get('/api/payouts/transactions', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const transactions = await Transaction.find({ storeId: store._id }) .populate('orderId', 'orderNumber createdAt') .sort({ createdAt: -1 }); const completedTransactions = transactions.filter(t => t.status === 'completed'); const pendingTransactions = transactions.filter(t => t.status === 'pending'); const heldTransactions = transactions.filter(t => t.status === 'held'); const stats = { totalEarnings: transactions.reduce((sum, t) => sum + (t.sellerAmount || 0), 0), availableBalance: completedTransactions.reduce((sum, t) => sum + (t.sellerAmount || 0), 0), pendingAmount: pendingTransactions.reduce((sum, t) => sum + (t.sellerAmount || 0), 0), heldAmount: heldTransactions.reduce((sum, t) => sum + (t.sellerAmount || 0), 0), totalTransactions: transactions.length }; res.json({ transactions, stats }); } catch (error) { console.error('Error fetching transactions:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/payouts/withdraw', authenticateToken, async (req, res) => { try { const { amount, methodId } = req.body; const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const method = await PayoutMethod.findById(methodId); if (!method) { return res.status(404).json({ error: 'Payout method not found' }); } if (method.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } const completedTransactions = await Transaction.find({ storeId: store._id, status: 'completed' }); const availableBalance = completedTransactions.reduce((sum, t) => sum + (t.sellerAmount || 0), 0); if (amount <= 0) { return res.status(400).json({ error: 'Amount must be greater than 0' }); } if (amount > availableBalance) { return res.status(400).json({ error: `Insufficient balance. Available: ${availableBalance.toLocaleString()} EGP` }); } const minPayout = store.paymentSettings?.minimumPayout || 500; if (amount < minPayout) { return res.status(400).json({ error: `Minimum payout amount is ${minPayout.toLocaleString()} EGP` }); } console.log(`💰 Withdrawal request: ${amount} EGP via ${method.type}`); res.json({ success: true, message: 'Withdrawal request submitted successfully', data: { amount, method: method.type, requestId: Date.now(), status: 'pending' } }); } catch (error) { console.error('Withdrawal error:', error); res.status(500).json({ error: error.message }); } }); // ================ Wishlist Routes ================ app.get('/api/wishlist', authenticateToken, async (req, res) => { try { const user = await User.findById(req.user.userId).populate('wishlist'); res.json(user?.wishlist || []); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/wishlist', authenticateToken, async (req, res) => { try { const { productId } = req.body; const user = await User.findById(req.user.userId); if (!user.wishlist) user.wishlist = []; if (!user.wishlist.includes(productId)) { user.wishlist.push(productId); await user.save(); } res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.delete('/api/wishlist/:productId', authenticateToken, async (req, res) => { try { const user = await User.findById(req.user.userId); user.wishlist = user.wishlist.filter(id => id.toString() !== req.params.productId); await user.save(); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/wishlist/check/:productId', authenticateToken, async (req, res) => { try { const user = await User.findById(req.user.userId); const isWishlisted = user.wishlist?.includes(req.params.productId) || false; res.json({ isWishlisted }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Payment Processing ================ // إنشاء طلب دفع لـ InstaPay async function createInstaPayPayment(order, storePaymentMethod) { try { order.paymentDetails = { method: 'instapay', merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, status: 'pending', requestedAt: new Date() }; await order.save(); return { requiresAction: true, instruction: `Please send ${order.totalAmount} EGP via InstaPay to: ${storePaymentMethod.mobileWalletDetails?.phoneNumber}`, merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, amount: order.totalAmount, reference: order.orderNumber }; } catch (error) { console.error('InstaPay payment error:', error); return null; } } // إنشاء طلب دفع لـ Bank Transfer async function createBankTransferPayment(order, storePaymentMethod) { try { order.paymentDetails = { method: 'bank', bankDetails: storePaymentMethod.bankDetails, status: 'pending', requestedAt: new Date() }; await order.save(); return { requiresAction: true, instruction: `Please transfer ${order.totalAmount} EGP to the following bank account:`, bankDetails: storePaymentMethod.bankDetails, amount: order.totalAmount, reference: order.orderNumber }; } catch (error) { console.error('Bank transfer payment error:', error); return null; } } // ================ Payment Processing (Production Ready) ================ // تكامل Vodafone Cash API الحقيقي async function createVodafoneCashPayment(order, storePaymentMethod) { try { const vodafoneApiUrl = process.env.VODAFONE_CASH_API_URL; const apiKey = process.env.VODAFONE_CASH_API_KEY; if (!vodafoneApiUrl || !apiKey) { console.log('⚠️ Vodafone Cash API not configured, using manual instructions'); // Fallback to manual instructions order.paymentDetails = { method: 'vodafone_cash', merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, status: 'pending', requestedAt: new Date() }; await order.save(); return { requiresAction: true, instruction: `Please send ${order.totalAmount} EGP to Vodafone Cash number: ${storePaymentMethod.mobileWalletDetails?.phoneNumber}`, merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, amount: order.totalAmount, reference: order.orderNumber }; } const paymentRequest = { merchantId: process.env.VODAFONE_MERCHANT_ID, orderId: order.orderNumber, amount: order.totalAmount, currency: 'EGP', customerPhone: order.shippingAddress.phone, merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, callbackUrl: `${process.env.BACKEND_URL}/api/webhooks/vodafone-cash`, redirectUrl: `${process.env.FRONTEND_URL}/order-tracking/${order.orderNumber}` }; const response = await axios.post(vodafoneApiUrl, paymentRequest, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 10000 }); order.paymentDetails = { method: 'vodafone_cash', merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, transactionId: response.data.transactionId, status: 'pending', requestedAt: new Date(), paymentUrl: response.data.paymentUrl }; await order.save(); return { requiresAction: true, paymentUrl: response.data.paymentUrl, transactionId: response.data.transactionId, instruction: `Please complete payment via Vodafone Cash`, merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, amount: order.totalAmount, reference: order.orderNumber }; } catch (error) { console.error('Vodafone Cash payment error:', error.message); // Fallback to manual instructions order.paymentDetails = { method: 'vodafone_cash', merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, status: 'pending', requestedAt: new Date() }; await order.save(); return { requiresAction: true, instruction: `Please send ${order.totalAmount} EGP to Vodafone Cash number: ${storePaymentMethod.mobileWalletDetails?.phoneNumber}`, merchantPhone: storePaymentMethod.mobileWalletDetails?.phoneNumber, amount: order.totalAmount, reference: order.orderNumber }; } } // Webhook لاستقبال تأكيد الدفع من Vodafone Cash app.post('/api/webhooks/vodafone-cash', express.raw({ type: 'application/json' }), async (req, res) => { try { const notification = req.body; const { orderNumber, transactionId, status, amount } = notification; const order = await Order.findOne({ orderNumber }); if (!order) { return res.status(404).json({ error: 'Order not found' }); } if (status === 'completed' || status === 'success') { order.paymentStatus = 'paid'; order.orderStatus = 'confirmed'; order.paymentDetails.status = 'completed'; order.paymentDetails.transactionId = transactionId; order.paymentDetails.paidAt = new Date(); await order.save(); // Create transaction for seller const storeItems = order.items.reduce((acc, item) => { if (!acc[item.storeId]) acc[item.storeId] = []; acc[item.storeId].push(item); return acc; }, {}); for (const [storeId, items] of Object.entries(storeItems)) { const storeSubtotal = items.reduce((sum, item) => sum + item.subtotal, 0); const platformCommission = storeSubtotal * 0.1; const sellerAmount = storeSubtotal - platformCommission; const transaction = new Transaction({ orderId: order._id, storeId, buyerId: order.customerId, amount: storeSubtotal, platformCommission, sellerAmount, status: 'pending', paymentMethod: 'wallet', releaseDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000) }); await transaction.save(); } console.log(`✅ Order ${order.orderNumber} paid via Vodafone Cash`); } res.json({ success: true }); } catch (error) { console.error('Webhook error:', error); res.status(500).json({ error: error.message }); } }); // ================ Machine Integration (Production Ready) ================ // Send to real machine via TCP async function sendToTCPMachine(machine, gcode) { return new Promise((resolve, reject) => { const client = new net.Socket(); const timeout = setTimeout(() => { client.destroy(); reject(new Error('TCP connection timeout')); }, 10000); client.connect(machine.port, machine.ipAddress, () => { clearTimeout(timeout); client.write(gcode); client.end(); resolve({ success: true, message: 'G-code sent via TCP' }); }); client.on('error', (err) => { clearTimeout(timeout); reject(err); }); }); } // Send to real machine via MQTT async function sendToMQTTMachine(machine, gcode, designId) { if (!mqttClient || !mqttClient.connected) { throw new Error('MQTT client not connected'); } const topic = `${process.env.MQTT_TOPIC_PREFIX}/${machine._id}/gcode`; const message = JSON.stringify({ designId, gcode, timestamp: new Date().toISOString(), machineId: machine._id.toString() }); return new Promise((resolve, reject) => { mqttClient.publish(topic, message, { qos: 1 }, (err) => { if (err) reject(err); else resolve({ success: true, message: 'G-code sent via MQTT' }); }); }); } // Main send to machine function async function sendToRealMachine(machine, gcode, designId) { try { let result; switch (machine.protocol) { case 'TCP': result = await sendToTCPMachine(machine, gcode); break; case 'MQTT': result = await sendToMQTTMachine(machine, gcode, designId); break; default: result = { success: false, message: `Unsupported protocol: ${machine.protocol}` }; } return result; } catch (error) { console.error('Machine communication error:', error); return { success: false, message: error.message }; } } // تحديث مسار إرسال التصميم للآلة app.post('/api/machines/:machineId/send-design', authenticateToken, async (req, res) => { try { const { designId } = req.body; const machine = await Machine.findById(req.params.machineId); const design = await Design.findById(designId); if (!machine || !design) { return res.status(404).json({ error: 'Machine or design not found' }); } if (design.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } // Generate advanced G-code const gcode = await generateAdvancedGCode(design, machine); // Send to real machine const sendResult = await sendToRealMachine(machine, gcode, design._id); if (sendResult.success) { design.status = 'production'; design.gcode = gcode; design.productionStartedAt = new Date(); await design.save(); const productionLog = new ProductionLog({ designId: design._id, machineId: machine._id, userId: req.user.userId, status: 'started', details: sendResult }); await productionLog.save(); // Publish status via MQTT if available if (mqttClient && mqttClient.connected) { const statusTopic = `${process.env.MQTT_TOPIC_PREFIX}/${machine._id}/status`; mqttClient.publish(statusTopic, JSON.stringify({ designId: design._id, status: 'started', timestamp: new Date().toISOString() })); } res.json({ success: true, message: 'Design sent to machine successfully', gcode: gcode.substring(0, 500) + '...', machineResponse: sendResult }); } else { throw new Error(sendResult.message); } } catch (error) { console.error('Send to machine error:', error); res.status(500).json({ error: error.message }); } }); // Generate advanced G-code // ================ Advanced G-Code Generation ================ async function generateAdvancedGCode(design, machine) { const area = (design.dimensions.width * design.dimensions.height) / 10000; const totalStitches = area * (design.pattern.complexity * 1000); const colors = [design.colors.primary, ...(design.colors.secondary || [])]; return ` ; ============================================ ; Naseej AI Generated G-Code - Advanced Version ; ============================================ ; Design ID: ${design._id} ; Design Name: ${design.name} ; Dimensions: ${design.dimensions.width}x${design.dimensions.height} cm ; Area: ${area.toFixed(2)} m² ; Total Stitches: ${Math.round(totalStitches).toLocaleString()} ; Machine: ${machine?.name || 'CNC Loom'} (${machine?.type || 'standard'}) ; Generated: ${new Date().toISOString()} ; ============================================ ; Initialize G21 ; Set units to mm G90 ; Absolute positioning G28 ; Home all axes G92 X0 Y0 Z0 ; Set current position ; Material Settings M104 S${getMaterialTemperature(design.material.type)} ; Set temperature M106 S255 ; Main motor on full speed ; Start weaving pattern ; Primary color: ${design.colors.primary} ; Secondary colors: ${design.colors.secondary?.join(', ') || 'None'} ${generateWeavingPattern(design, machine || {})} ; Finish M107 ; Main motor off M30 ; Program end ; ============================================ ; Production Stats ; Estimated Time: ${design.productionTime} hours ; Material Used: ${(area * (design.material.weightPerSquareMeter || 2.5)).toFixed(2)} kg ; ============================================ `; } // دالة توليد G-Code للآلات function generateGCodeForMachine(design) { const width = design.dimensions.width * 10; // mm const height = design.dimensions.height * 10; // mm const complexity = design.pattern.complexity; const area = (design.dimensions.width * design.dimensions.height) / 10000; const totalStitches = Math.round(area * (complexity * 800)); return `; ============================================ ; NASEEJ AI Generated G-Code for CNC Carpet Weaving ; ============================================ ; Design ID: ${design._id} ; Design Name: ${design.name} ; Dimensions: ${design.dimensions.width}x${design.dimensions.height} cm ; Area: ${area.toFixed(2)} m² ; Material: ${design.material.type} ; Pattern: ${design.pattern.type} ; Complexity: ${design.pattern.complexity}/10 ; Total Stitches: ${totalStitches.toLocaleString()} ; Generated: ${new Date().toISOString()} ; ============================================ ; Initialize Machine G21 ; Set units to mm G90 ; Absolute positioning G28 ; Home all axes G92 X0 Y0 Z0 ; Set current position ; Material Settings M104 S${getMaterialTemperature(design.material.type)} ; Set temperature M106 S255 ; Main motor on full speed M08 ; Coolant on ; Color Palette ; Primary: ${design.colors.primary} ; Secondary: ${design.colors.secondary?.join(', ') || 'None'} ; Accent: ${design.colors.accent?.join(', ') || 'None'} ; Start Weaving Sequence ${generateWeavingPattern(design, {})} ; Finish M05 ; Spindle stop M09 ; Coolant off M107 ; Main motor off M30 ; Program end ; ============================================ ; Production Statistics ; Estimated Production Time: ${Math.ceil(totalStitches / 5000)} hours ; Material Usage: ${(area * (design.material.weightPerSquareMeter || 2.5)).toFixed(2)} kg ; Recommended Speed: ${600 + complexity * 20} mm/min ; ============================================ `; } // دالة توليد كود Python للآلات function generatePythonCodeForMachine(design) { const area = (design.dimensions.width * design.dimensions.height) / 10000; const totalStitches = Math.round(area * (design.pattern.complexity * 800)); // ✅ استخدام JSON.stringify بدلاً من json.dumps const secondaryColorsJson = JSON.stringify(design.colors.secondary || []); const accentColorsJson = JSON.stringify(design.colors.accent || []); return `#!/usr/bin/env python3 # ============================================ # NASEEJ AI Generated Python Code for Carpet Weaving # ============================================ # Design ID: ${design._id} # Design Name: ${design.name} # Dimensions: ${design.dimensions.width}x${design.dimensions.height} cm # Material: ${design.material.type} # Pattern: ${design.pattern.type} # Complexity: ${design.pattern.complexity}/10 # Generated: ${new Date().toISOString()} # ============================================ import time import threading import json from datetime import datetime # Machine Configuration class CarpetWeavingMachine: def __init__(self): self.position_x = 0 self.position_y = 0 self.current_color = "${design.colors.primary}" self.is_running = False self.stitch_count = 0 self.total_stitches = ${totalStitches} # Color palette self.colors = { 'primary': "${design.colors.primary}", 'secondary': ${secondaryColorsJson}, 'accent': ${accentColorsJson} } # Material settings self.material = { 'type': "${design.material.type}", 'temperature': ${getMaterialTemperature(design.material.type)}, 'density': "${design.material.density}", 'weight_per_sqm': ${design.material.weightPerSquareMeter || 2.5} } # Pattern settings self.pattern = { 'type': "${design.pattern.type}", 'complexity': ${design.pattern.complexity}, 'width_cm': ${design.dimensions.width}, 'height_cm': ${design.dimensions.height} } def set_temperature(self): """Set weaving temperature based on material""" print(f"[HEATER] Setting temperature to {self.material['temperature']}°C") time.sleep(1) def move_to(self, x, y, speed=800): """Move weaving head to coordinates""" self.position_x = x self.position_y = y print(f"[MOTION] Moving to X:{x} Y:{y} at speed {speed}mm/min") time.sleep(0.01) def weave_line(self, y_pos, direction): """Weave a single line""" width_mm = self.pattern['width_cm'] * 10 speed = 600 + (self.pattern['complexity'] * 20) if direction == 'right': self.move_to(width_mm, y_pos, speed) else: self.move_to(0, y_pos, speed) self.stitch_count += 1 def change_color(self, color_name): """Change thread color""" print(f"[COLOR] Changing to {color_name}") self.current_color = color_name time.sleep(0.5) def run(self): """Execute the weaving process""" print("=" * 60) print("Starting Carpet Weaving Process") print("=" * 60) print(f"Design: ${design.name}") print(f"Dimensions: {self.pattern['width_cm']}x{self.pattern['height_cm']} cm") print(f"Material: {self.material['type']}") print(f"Pattern: {self.pattern['type']} (Complexity: {self.pattern['complexity']}/10)") print("-" * 60) self.is_running = True start_time = datetime.now() # Initialize machine self.set_temperature() print("[MOTOR] Main motor started") height_mm = self.pattern['height_cm'] * 10 step = max(2, int(50 / self.pattern['complexity'])) colors_list = [self.colors['primary']] + (self.colors['secondary'] or []) for y in range(0, height_mm + 1, step): direction = 'right' if (y // step) % 2 == 0 else 'left' self.weave_line(y, direction) # Change color periodically color_index = (y // step) % len(colors_list) if color_index == 0 and y > 0: self.change_color(colors_list[color_index]) # Report progress progress = (y / height_mm) * 100 if int(progress) % 10 == 0: print(f"[PROGRESS] {progress:.0f}% complete ({self.stitch_count}/{self.total_stitches} stitches)") # Finish elapsed = (datetime.now() - start_time).total_seconds() print("-" * 60) print(f"[COMPLETE] Weaving finished in {elapsed:.0f} seconds") print(f"[STATS] Total stitches: {self.stitch_count}") print(f"[STATS] Material used: {(self.pattern['width_cm'] * self.pattern['height_cm'] / 10000 * self.material['weight_per_sqm']):.2f} kg") print("=" * 60) self.is_running = False if __name__ == "__main__": machine = CarpetWeavingMachine() try: machine.run() except KeyboardInterrupt: print("\\n[MACHINE] Process interrupted by user") except Exception as e: print(f"[ERROR] {str(e)}") `; } async function enhanceImageQuality(imageUrl) { try { const response = await axios.get(imageUrl, { responseType: 'arraybuffer', timeout: 30000, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; NaseejBot/1.0)' } }); const enhancedBuffer = await sharp(response.data) .resize(2048, 2048, { fit: 'inside', withoutEnlargement: true, kernel: 'lanczos3' // أفضل خوارزمية للتحجيم }) .sharpen({ sigma: 1.2, m1: 1.0, m2: 0.5 // تحسين الحدة بشكل طبيعي }) .withMetadata() .png({ quality: 95, compressionLevel: 6, palette: false }) .toBuffer(); const base64Image = enhancedBuffer.toString('base64'); console.log('✅ Image enhanced successfully (2048x2048, PNG)'); return `data:image/png;base64,${base64Image}`; } catch (error) { console.error('Image enhancement failed:', error.message); return imageUrl; } } // Material descriptions for better realism const materialDescriptions = { wool: "luxurious wool carpet, soft texture, natural fibers, matte finish, premium quality", silk: "premium silk carpet, lustrous sheen, smooth texture, elegant shine, high-end", cotton: "soft cotton carpet, natural look, comfortable texture, matte finish, breathable", polyester: "durable polyester carpet, modern material, consistent texture, stain-resistant", acrylic: "acrylic carpet, vibrant colors, soft feel, stain-resistant, colorfast", jute: "natural jute carpet, eco-friendly, rustic texture, organic look, sustainable" }; async function generateRealisticCarpetImage(design) { const width = design.dimensions.width; const height = design.dimensions.height; const primaryColor = design.colors.primary; const secondaryColors = design.colors.secondary || []; const accentColors = design.colors.accent || []; const patternType = design.pattern.type; const complexity = design.pattern.complexity; const materialType = design.material.type; // تحويل الألوان إلى أسماء مفهومة const colorNames = { '#8B4513': 'saddle brown', '#D2691E': 'chocolate', '#F5DEB3': 'wheat', '#FFD700': 'gold', '#C0C0C0': 'silver', '#8B0000': 'dark red', '#1B4D3E': 'dark green', '#C9A87C': 'tan', '#D4AF37': 'metallic gold' }; const primaryColorName = colorNames[primaryColor.toUpperCase()] || primaryColor; const secondaryColorNames = secondaryColors.map(c => colorNames[c.toUpperCase()] || c).join(', '); // بناء الـ prompt المبسط const prompt = `${design.aiPrompt || `A beautiful ${primaryColorName} ${materialType} carpet with ${patternType} patterns`}. ${secondaryColorNames ? `Colors: ${secondaryColorNames}.` : ''} High quality, realistic texture, professional photography, 4K, sharp details, carpet only, no background.`; // قائمة بمحاولات API مختلفة const apis = [ // Pollinations.ai (مجاني، لا يحتاج مفتاح) { name: 'Pollinations.ai', url: `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=1024&height=1024&nologo=true`, type: 'direct' }, // Generative AI (بديل) { name: 'Generative AI', url: `https://generativeai.p.rapidapi.com/images/generate?prompt=${encodeURIComponent(prompt)}`, type: 'rapidapi', headers: { 'X-RapidAPI-Key': process.env.RAPIDAPI_KEY || '', 'X-RapidAPI-Host': 'generativeai.p.rapidapi.com' } } ]; // المحاولة الأولى: Pollinations.ai try { console.log('🎨 Trying Pollinations.ai (free, no API key required)...'); const pollinationsUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=1024&height=1024&nologo=true&seed=${Date.now()}`; // اختبار ما إذا كان الرابط يعمل const testResponse = await fetch(pollinationsUrl, { method: 'HEAD' }); if (testResponse.ok) { console.log('✅ Pollinations.ai is working!'); return pollinationsUrl; } } catch (error) { console.log('Pollinations.ai failed:', error.message); } // إذا فشلت Pollinations، استخدم SVG console.log('🔄 All APIs failed, using SVG fallback...'); return generateFallbackSVG(design); } // Fallback SVG generator (improved version with better colors) function generateFallbackSVG(design) { const width = design.dimensions.width; const height = design.dimensions.height; const primaryColor = design.colors.primary; const secondaryColors = design.colors.secondary || ['#8B4513', '#A0522D']; const accentColors = design.colors.accent || ['#FFD700']; const patternType = design.pattern.type; const complexity = design.pattern.complexity; // إضافة تأثيرات أكثر واقعية let svgContent = ` `; // إضافة الزخارف حسب النوع if (patternType === 'geometric') { const cellSize = Math.max(50, Math.min(100, 180 / complexity)); for (let x = 30; x < width - 30; x += cellSize) { for (let y = 30; y < height - 30; y += cellSize) { const colorIdx = Math.floor((x + y) / cellSize) % secondaryColors.length; svgContent += ``; svgContent += ``; } } } else if (patternType === 'traditional') { const centerX = width/2; const centerY = height/2; const radius = Math.min(width, height) * 0.2; svgContent += ``; svgContent += ``; svgContent += ``; } svgContent += `✦ AI Generated ✦`; svgContent += ``; return `data:image/svg+xml;utf8,${encodeURIComponent(svgContent)}`; } function generateWeavingGCode(width, height, complexity, design) { return generateWeavingPattern(design, {}); } // ================ Machine Format Generation Functions ================ // دالة الحصول على درجة حرارة المادة // دالة مساعدة للحصول على وصف النقشة function getPatternDescription(patternType, complexity) { const descriptions = { geometric: { 1: 'Simple geometric grid pattern', 5: 'Balanced geometric design with moderate complexity', 10: 'Intricate geometric masterpiece with complex overlapping patterns' }, floral: { 1: 'Simple floral motifs scattered across the surface', 5: 'Elegant floral patterns with detailed petal structures', 10: 'Luxurious floral paradise with rich botanical details' }, abstract: { 1: 'Minimalist abstract shapes and forms', 5: 'Dynamic abstract composition with flowing elements', 10: 'Complex abstract artwork with multi-layered visual effects' }, traditional: { 1: 'Classic traditional border patterns', 5: 'Authentic traditional motifs with cultural significance', 10: 'Heritage-inspired masterpiece with authentic regional patterns' } }; return descriptions[patternType]?.[complexity] || `${patternType} pattern with ${complexity}/10 complexity`; } function getMaterialTemperature(materialType) { const temps = { wool: 180, silk: 160, cotton: 200, polyester: 220, blend: 190, acrylic: 185, nylon: 210 }; return temps[materialType] || 180; } // دالة توليد نمط النسيج المتقدم function generateWeavingPattern(design, machine) { const width = design.dimensions.width * 10; // mm const height = design.dimensions.height * 10; // mm const complexity = design.pattern.complexity; const step = Math.max(2, Math.floor(50 / complexity)); const colors = [design.colors.primary, ...(design.colors.secondary || [])]; let gcode = ''; let stitchCount = 0; // إعدادات السرعة حسب التعقيد const baseSpeed = 600 + (complexity * 20); const detailSpeed = 400 + (complexity * 10); for (let y = 0; y <= height; y += step) { const direction = (Math.floor(y / step) % 2 === 0) ? 'right' : 'left'; const colorIndex = Math.floor(y / step) % colors.length; const currentColor = colors[colorIndex] || colors[0]; // حركة النسيج الأساسية if (direction === 'right') { gcode += `G01 X${width} Y${y} F${baseSpeed}\n`; } else { gcode += `G01 X0 Y${y} F${baseSpeed}\n`; } stitchCount++; // تغيير اللون عند الحاجة if (colorIndex === 0 && y > 0) { gcode += `; Color change to ${currentColor}\n`; gcode += `M104 S${getMaterialTemperature(design.material.type)}\n`; gcode += `G04 P0.5 ; Pause for color change\n`; } // أنماط معقدة إضافية للتعقيد العالي if (complexity >= 6) { // نمط متعرج داخلي للتفاصيل const midPoint = width / 2; gcode += `G01 X${midPoint} Y${y + step / 2} F${detailSpeed}\n`; gcode += `G01 X${midPoint} Y${y} F${detailSpeed}\n`; stitchCount += 2; if (complexity >= 8) { // نقوش دائرية const radius = 30; gcode += `G02 X${midPoint + radius} Y${y + radius} I${radius} J0 F${detailSpeed}\n`; gcode += `G03 X${midPoint} Y${y + radius * 2} I${-radius} J0 F${detailSpeed}\n`; stitchCount += 2; } } } // إضافة إحصائيات الإنتاج gcode += `; Production Statistics:\n`; gcode += `; Total Stitches: ${stitchCount.toLocaleString()}\n`; gcode += `; Estimated Time: ${Math.ceil(stitchCount / 1000)} minutes\n`; return gcode; } // Production tracking route app.get('/api/production/design/:designId', authenticateToken, async (req, res) => { try { const design = await Design.findById(req.params.designId); if (!design) { return res.status(404).json({ error: 'Design not found' }); } const productionLogs = await ProductionLog.find({ designId: design._id }) .populate('machineId', 'name type') .sort({ startedAt: -1 }); res.json({ design, status: design.status, productionLogs, estimatedCompletion: design.productionStartedAt ? new Date(design.productionStartedAt.getTime() + design.productionTime * 60 * 60 * 1000) : null }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Machine status webhook (from MQTT or direct API) app.post('/api/webhooks/machine-status', async (req, res) => { try { const { machineId, designId, status, progress, details } = req.body; const productionLog = await ProductionLog.findOne({ machineId, designId, status: { $in: ['started', 'in_progress'] } }); if (!productionLog) { return res.status(404).json({ error: 'Production log not found' }); } if (status === 'completed') { productionLog.status = 'completed'; productionLog.completedAt = new Date(); productionLog.progress = 100; await productionLog.save(); await Design.findByIdAndUpdate(designId, { status: 'completed', completedAt: new Date() }); } else if (status === 'in_progress' && progress) { productionLog.status = 'in_progress'; productionLog.progress = progress; productionLog.details = { ...productionLog.details, ...details }; await productionLog.save(); } else if (status === 'failed') { productionLog.status = 'failed'; productionLog.details = { ...productionLog.details, error: details }; await productionLog.save(); await Design.findByIdAndUpdate(designId, { status: 'cancelled' }); } res.json({ success: true }); } catch (error) { console.error('Machine webhook error:', error); res.status(500).json({ error: error.message }); } }); // ================ Social Posts Routes ================ // إنشاء منشور جديد app.post('/api/posts', authenticateToken, async (req, res) => { try { const { content, media, hashtags, mentions, visibility, isScheduled, scheduledAt } = req.body; // التحقق من صحة المحتوى if (!content && (!media || media.length === 0)) { return res.status(400).json({ error: 'Please add content or media' }); } // ✅ التحقق من صحة visibility const validVisibility = ['public', 'followers', 'private', 'store_only']; const finalVisibility = validVisibility.includes(visibility) ? visibility : 'public'; const post = new Post({ userId: req.user.userId, content: content || '', media: media || [], hashtags: hashtags || [], mentions: mentions || [], visibility: finalVisibility, isScheduled: isScheduled || false, scheduledAt: scheduledAt || null, status: isScheduled ? 'draft' : 'published' }); await post.save(); // Populate user info للمنشور الجديد مع معلومات المتجر const populatedPost = await Post.findById(post._id) .populate('userId', 'username email avatar role storeId') .populate({ path: 'userId', populate: { path: 'storeId', model: 'Store', select: 'name slug logo' } }); res.status(201).json({ success: true, post: populatedPost, message: isScheduled ? 'Post scheduled successfully' : 'Post created successfully' }); } catch (error) { console.error('Create post error:', error); res.status(500).json({ error: error.message }); } }); // جلب منشورات مستخدم معين (لصفحة المستخدم الشخصية) app.get('/api/posts/user/:userId', async (req, res) => { try { const { userId } = req.params; const { page = 1, limit = 20 } = req.query; // جلب المستخدم const targetUser = await User.findById(userId); if (!targetUser) { return res.status(404).json({ error: 'User not found' }); } const currentUserId = req.user?.userId; const isOwner = currentUserId === userId; // جلب منشورات المستخدم const query = { userId: userId, status: 'published', isScheduled: false }; // ✅ إصلاح: صاحب الحساب يرى كل منشوراته (بما فيها الخاصة والمتابعين) // الزائر يرى فقط المنشورات العامة if (!isOwner) { query.visibility = 'public'; } // إذا كان صاحب الحساب، لا نضيف شرط visibility - يرى كل شيء const posts = await Post.find(query) .populate('userId', 'username email avatar role storeId') .populate('likes', 'username') .populate({ path: 'userId', populate: { path: 'storeId', model: 'Store', select: 'name slug logo' } }) .sort({ isPinned: -1, createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); // إضافة معلومات الإعجاب للمستخدم الحالي if (currentUserId) { for (const post of posts) { post.liked = post.likes.some(like => like._id.toString() === currentUserId); } } res.json({ posts, page: parseInt(page), hasMore: posts.length === limit, total: posts.length, user: { id: targetUser._id, username: targetUser.username, email: targetUser.email, storeId: targetUser.storeId } }); } catch (error) { console.error('User posts error:', error); res.status(500).json({ error: error.message }); } }); // ================ Upload Routes (Enhanced) ================ // رفع صورة واحدة app.post('/api/upload', authenticateToken, upload.single('image'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } res.json({ success: true, url: req.file.path, publicId: req.file.filename, type: 'image' }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ error: error.message }); } }); // رفع فيديو app.post('/api/upload/video', authenticateToken, upload.single('video'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No video uploaded' }); } // التحقق من أن الملف فيديو if (!req.file.mimetype.startsWith('video/')) { return res.status(400).json({ error: 'File must be a video' }); } res.json({ success: true, url: req.file.path, publicId: req.file.filename, type: 'video', duration: req.file.duration || null }); } catch (error) { console.error('Video upload error:', error); res.status(500).json({ error: error.message }); } }); // رفع صور متعددة app.post('/api/upload/multiple', authenticateToken, upload.array('media', 10), async (req, res) => { try { if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'No files uploaded' }); } const uploadedFiles = req.files.map(file => ({ type: file.mimetype.startsWith('image/') ? 'image' : 'video', url: file.path, publicId: file.filename })); res.json({ success: true, files: uploadedFiles, count: uploadedFiles.length }); } catch (error) { console.error('Multiple upload error:', error); res.status(500).json({ error: error.message }); } }); // حذف ملف من Cloudinary app.delete('/api/upload/:publicId', authenticateToken, async (req, res) => { try { const { publicId } = req.params; if (!publicId) { return res.status(400).json({ error: 'Public ID is required' }); } const result = await cloudinary.uploader.destroy(publicId, { invalidate: true }); if (result.result === 'ok') { res.json({ success: true, message: 'File deleted successfully' }); } else { res.status(404).json({ error: 'File not found' }); } } catch (error) { console.error('Delete error:', error); res.status(500).json({ error: error.message }); } }); // ================ Audio Upload Routes ================ // رفع ملف صوتي (Voice Message) app.post('/api/upload/audio', authenticateToken, upload.single('audio'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'No audio file uploaded' }); } // التحقق من أن الملف صوتي const isAudio = req.file.mimetype.startsWith('audio/'); if (!isAudio) { return res.status(400).json({ error: 'File must be an audio file' }); } // الحصول على مدة الصوت (إذا كانت متوفرة) let duration = null; if (req.file.duration) { duration = req.file.duration; } // ✅ إذا كان الملف من Cloudinary، قد يكون duration موجود في metadata if (req.file.metadata && req.file.metadata.duration) { duration = parseFloat(req.file.metadata.duration); } res.json({ success: true, url: req.file.path, publicId: req.file.filename, type: 'audio', duration: duration, mimetype: req.file.mimetype, size: req.file.size }); } catch (error) { console.error('Audio upload error:', error); res.status(500).json({ error: error.message }); } }); // رفع ملفات صوتية متعددة app.post('/api/upload/audio/multiple', authenticateToken, upload.array('audio', 10), async (req, res) => { try { if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'No audio files uploaded' }); } const uploadedFiles = req.files.map(file => ({ type: 'audio', url: file.path, publicId: file.filename, duration: file.duration || null, size: file.size, mimetype: file.mimetype })); res.json({ success: true, files: uploadedFiles, count: uploadedFiles.length }); } catch (error) { console.error('Multiple audio upload error:', error); res.status(500).json({ error: error.message }); } }); // جلب جميع المنشورات (Feed) app.get('/api/posts/feed', async (req, res) => { try { const { page = 1, limit = 20, type = 'for_you' } = req.query; const query = { status: 'published', isScheduled: false }; const currentUserId = req.user?.userId; let currentUser = null; if (currentUserId) { currentUser = await User.findById(currentUserId); } // ================ For You Feed ================ if (type === 'for_you') { if (currentUser) { // للمستخدمين المسجلين: // 1. المنشورات العامة (public) من الجميع // 2. منشورات المستخدمين الذين يتابعهم (حتى لو كانت followers) // 3. منشورات المستخدم نفسه const followingIds = currentUser.followingStores || []; query.$or = [ { visibility: 'public' }, // عامة للجميع { userId: currentUserId }, // منشورات المستخدم نفسه { userId: { $in: followingIds }, // منشورات المتابعين visibility: { $in: ['public', 'followers'] } // عامة أو للمتابعين } ]; } else { // لغير المسجلين: اعرض فقط المنشورات العامة query.visibility = 'public'; } } // ================ Following Feed ================ else if (type === 'following') { if (!currentUser) { return res.json({ posts: [], page: 1, hasMore: false, total: 0 }); } const followingIds = currentUser.followingStores || []; if (followingIds.length === 0) { return res.json({ posts: [], page: 1, hasMore: false, total: 0 }); } query.userId = { $in: followingIds }; query.visibility = { $in: ['public', 'followers'] }; } // ================ Trending Feed ================ else if (type === 'trending') { const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); query.createdAt = { $gte: sevenDaysAgo }; query.visibility = 'public'; } // ================ Store Feed ================ else if (type === 'store' && req.query.storeId) { query.storeId = req.query.storeId; query.visibility = { $in: ['public', 'followers'] }; } // جلب المنشورات مع populate let posts = await Post.find(query) .populate('userId', 'username email avatar role storeId') .populate('likes', 'username') .populate({ path: 'userId', populate: { path: 'storeId', model: 'Store', select: 'name slug logo' } }) .sort({ isPinned: -1, createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); // ================ ترتيب المنشورات الرائجة حسب التفاعل ================ if (type === 'trending') { posts = posts.sort((a, b) => { const scoreA = (a.likesCount || 0) * 2 + (a.commentsCount || 0) * 3 + (a.sharesCount || 0) * 1.5; const scoreB = (b.likesCount || 0) * 2 + (b.commentsCount || 0) * 3 + (b.sharesCount || 0) * 1.5; return scoreB - scoreA; }); } // ================ تحديث عدد المشاهدات ================ for (const post of posts) { post.viewsCount = (post.viewsCount || 0) + 1; await post.save(); } // ================ إضافة معلومات إضافية للمستخدم ================ if (currentUserId) { for (const post of posts) { post.liked = post.likes.some(like => like._id.toString() === currentUserId); } } res.json({ posts, page: parseInt(page), hasMore: posts.length === limit, total: posts.length }); } catch (error) { console.error('Feed error:', error); res.status(500).json({ error: error.message }); } }); // جلب منشورات متجر معين app.get('/api/posts/store/:storeId', async (req, res) => { try { const { storeId } = req.params; const { page = 1, limit = 20 } = req.query; // جلب المتجر أولاً للتأكد من وجوده const store = await Store.findById(storeId); if (!store) { return res.status(404).json({ error: 'Store not found' }); } // جلب المستخدم صاحب المتجر const owner = await User.findById(store.ownerId); if (!owner) { return res.status(404).json({ error: 'Store owner not found' }); } // جلب منشورات المستخدم التي: // 1. مرتبطة بهذا المتجر (storeId موجود) // 2. أو منشورات عامة من المستخدم const query = { userId: owner._id, status: 'published', isScheduled: false }; // إضافة شرط visibility حسب المستخدم الزائر const currentUserId = req.user?.userId; if (!currentUserId) { // لغير المسجلين، اعرض فقط المنشورات العامة query.visibility = 'public'; } else { const currentUser = await User.findById(currentUserId); const isFollowing = currentUser?.followingStores?.includes(storeId); // للمستخدمين المسجلين: // - المنشورات العامة (public) يراها الجميع // - منشورات المتابعين (followers) يراها فقط من يتابع المتجر // - المنشورات الخاصة (private) يراها صاحب المتجر فقط // - منشورات المتجر فقط (store_only) يراها فقط زوار المتجر query.$or = [ { visibility: 'public' }, { visibility: 'store_only' } ]; if (isFollowing) { query.$or.push({ visibility: 'followers' }); } if (owner._id.toString() === currentUserId) { // صاحب المتجر يرى كل منشوراته delete query.$or; } } const posts = await Post.find(query) .populate('userId', 'username email avatar role storeId') .populate('likes', 'username') .populate({ path: 'userId', populate: { path: 'storeId', model: 'Store', select: 'name slug logo' } }) .sort({ isPinned: -1, createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); // إضافة معلومات الإعجاب للمستخدم الحالي if (currentUserId) { for (const post of posts) { post.liked = post.likes.some(like => like._id.toString() === currentUserId); } } res.json({ posts, page: parseInt(page), hasMore: posts.length === limit, total: posts.length, store: { id: store._id, name: store.name, slug: store.slug, logo: store.logo } }); } catch (error) { console.error('Store posts error:', error); res.status(500).json({ error: error.message }); } }); // تحديث visibility منشور app.put('/api/posts/:postId/visibility', authenticateToken, async (req, res) => { try { const { visibility } = req.body; const post = await Post.findById(req.params.postId); if (!post) { return res.status(404).json({ error: 'Post not found' }); } if (post.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } const validVisibility = ['public', 'followers', 'private', 'store_only']; if (!validVisibility.includes(visibility)) { return res.status(400).json({ error: 'Invalid visibility value' }); } post.visibility = visibility; await post.save(); res.json({ success: true, visibility: post.visibility }); } catch (error) { res.status(500).json({ error: error.message }); } }); // جلب إحصائيات المستخدم للـ Social Home app.get('/api/user/stats', authenticateToken, async (req, res) => { try { const userId = req.user.userId; // عدد الإعجابات التي أعطاها المستخدم const likedPosts = await Post.countDocuments({ likes: userId }); const likedComments = await Comment.countDocuments({ likes: userId }); const totalLikes = likedPosts + likedComments; // عدد التعليقات التي كتبها المستخدم const totalComments = await Comment.countDocuments({ userId }); // عدد المنشورات التي شاركها المستخدم const totalShares = await Post.countDocuments({ userId, sharesCount: { $gt: 0 } }); // عدد الأيام النشطة (منذ إنشاء الحساب) const user = await User.findById(userId); const daysSinceJoined = Math.floor((Date.now() - new Date(user.createdAt).getTime()) / (1000 * 60 * 60 * 24)); res.json({ totalLikes, totalComments, totalShares, activeDays: daysSinceJoined || 1 }); } catch (error) { console.error('Stats error:', error); res.status(500).json({ error: error.message }); } }); // جلب منشور واحد app.get('/api/posts/:postId', async (req, res) => { try { const post = await Post.findById(req.params.postId) .populate('userId', 'username email avatar role storeId') .populate('likes', 'username'); if (!post) { return res.status(404).json({ error: 'Post not found' }); } // زيادة المشاهدات post.viewsCount += 1; await post.save(); // جلب التعليقات const comments = await Comment.find({ postId: post._id, parentId: null }) .populate('userId', 'username email avatar') .populate('likes', 'username') .sort({ createdAt: -1 }); res.json({ post, comments }); } catch (error) { res.status(500).json({ error: error.message }); } }); // الإعجاب بمنشور app.post('/api/posts/:postId/like', authenticateToken, async (req, res) => { try { const post = await Post.findById(req.params.postId); if (!post) { return res.status(404).json({ error: 'Post not found' }); } const hasLiked = post.likes.includes(req.user.userId); if (hasLiked) { post.likes = post.likes.filter(id => id.toString() !== req.user.userId); post.likesCount -= 1; } else { post.likes.push(req.user.userId); post.likesCount += 1; // إنشاء إشعار if (post.userId.toString() !== req.user.userId) { const notification = new Notification({ userId: post.userId, type: 'like', actorId: req.user.userId, postId: post._id, content: `liked your post` }); await notification.save(); } } await post.save(); res.json({ success: true, likesCount: post.likesCount, hasLiked: !hasLiked }); } catch (error) { res.status(500).json({ error: error.message }); } }); // إضافة تعليق app.post('/api/posts/:postId/comment', authenticateToken, async (req, res) => { try { const { content, parentId } = req.body; const post = await Post.findById(req.params.postId); if (!post) { return res.status(404).json({ error: 'Post not found' }); } const comment = new Comment({ postId: post._id, userId: req.user.userId, content, parentId: parentId || null }); await comment.save(); post.commentsCount += 1; await post.save(); // إنشاء إشعار if (post.userId.toString() !== req.user.userId) { const notification = new Notification({ userId: post.userId, type: 'comment', actorId: req.user.userId, postId: post._id, commentId: comment._id, content: `commented on your post` }); await notification.save(); } const populatedComment = await Comment.findById(comment._id) .populate('userId', 'username email avatar'); res.status(201).json({ success: true, comment: populatedComment }); } catch (error) { res.status(500).json({ error: error.message }); } }); // مشاركة منشور app.post('/api/posts/:postId/share', authenticateToken, async (req, res) => { try { const post = await Post.findById(req.params.postId); if (!post) { return res.status(404).json({ error: 'Post not found' }); } post.sharesCount += 1; await post.save(); // إنشاء منشور مشاركة const sharedPost = new Post({ userId: req.user.userId, content: `Shared: ${post.content.substring(0, 100)}...`, media: post.media, hashtags: post.hashtags, visibility: 'public', status: 'published' }); await sharedPost.save(); res.json({ success: true, sharesCount: post.sharesCount }); } catch (error) { res.status(500).json({ error: error.message }); } }); // حذف منشور app.delete('/api/posts/:postId', authenticateToken, async (req, res) => { try { const post = await Post.findById(req.params.postId); if (!post) { return res.status(404).json({ error: 'Post not found' }); } if (post.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } post.status = 'archived'; await post.save(); res.json({ success: true, message: 'Post deleted' }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Stories Routes ================ // إنشاء قصة جديدة app.post('/api/stories', authenticateToken, async (req, res) => { try { const { media, duration } = req.body; if (!media || !media.url) { return res.status(400).json({ error: 'Media is required' }); } const story = new Story({ userId: req.user.userId, media: { type: media.type || 'image', url: media.url }, duration: duration || 24, expiresAt: new Date(Date.now() + (duration || 24) * 60 * 60 * 1000) }); await story.save(); const populatedStory = await Story.findById(story._id) .populate('userId', 'username email avatar'); res.status(201).json({ success: true, story: populatedStory }); } catch (error) { console.error('Create story error:', error); res.status(500).json({ error: error.message }); } }); // حذف قصة app.delete('/api/stories/:storyId', authenticateToken, async (req, res) => { try { const story = await Story.findById(req.params.storyId); if (!story) { return res.status(404).json({ error: 'Story not found' }); } if (story.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } await story.deleteOne(); res.json({ success: true, message: 'Story deleted' }); } catch (error) { console.error('Delete story error:', error); res.status(500).json({ error: error.message }); } }); // جلب المنشورات المجدولة app.get('/api/posts/scheduled', authenticateToken, async (req, res) => { try { const scheduledPosts = await Post.find({ userId: req.user.userId, isScheduled: true, scheduledAt: { $gt: new Date() }, status: 'draft' }) .populate('userId', 'username email avatar') .sort({ scheduledAt: 1 }); res.json(scheduledPosts); } catch (error) { console.error('Scheduled posts error:', error); res.status(500).json({ error: error.message }); } }); // جلب القصص app.get('/api/stories/feed', async (req, res) => { try { const stories = await Story.find({ expiresAt: { $gt: new Date() } }) .populate('userId', 'username email avatar') .sort({ createdAt: -1 }); // إضافة معلومات عما إذا كان المستخدم قد شاهد القصة if (req.user) { for (const story of stories) { story.viewed = story.views.includes(req.user.userId); } } // تجميع القصص حسب المستخدم const groupedStories = stories.reduce((acc, story) => { const userId = story.userId._id.toString(); if (!acc[userId]) { acc[userId] = { user: story.userId, stories: [], viewed: story.viewed || false }; } acc[userId].stories.push(story); return acc; }, {}); res.json(Object.values(groupedStories)); } catch (error) { console.error('Stories error:', error); res.status(500).json({ error: error.message }); } }); // مشاهدة قصة app.post('/api/stories/:storyId/view', authenticateToken, async (req, res) => { try { const story = await Story.findById(req.params.storyId); if (!story) { return res.status(404).json({ error: 'Story not found' }); } if (!story.views.includes(req.user.userId)) { story.views.push(req.user.userId); story.viewsCount += 1; await story.save(); } res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Notifications Routes ================ // جلب الإشعارات app.get('/api/notifications', authenticateToken, async (req, res) => { try { const notifications = await Notification.find({ userId: req.user.userId }) .populate('actorId', 'username email avatar') .populate('postId', 'content media') .sort({ createdAt: -1 }) .limit(50); const unreadCount = notifications.filter(n => !n.isRead).length; res.json({ notifications, unreadCount }); } catch (error) { res.status(500).json({ error: error.message }); } }); // تحديث حالة الإشعار app.put('/api/notifications/:notificationId/read', authenticateToken, async (req, res) => { try { const notification = await Notification.findById(req.params.notificationId); if (!notification) { return res.status(404).json({ error: 'Notification not found' }); } notification.isRead = true; await notification.save(); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // تحديث كل الإشعارات كمقروءة app.put('/api/notifications/read-all', authenticateToken, async (req, res) => { try { await Notification.updateMany( { userId: req.user.userId, isRead: false }, { isRead: true } ); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // تحرير رسالة app.put('/api/chat/messages/:messageId', authenticateToken, async (req, res) => { try { const { messageId } = req.params; const { text } = req.body; const currentUserId = req.user.userId; const message = await Message.findById(messageId); if (!message) { return res.status(404).json({ error: 'Message not found' }); } if (message.senderId.toString() !== currentUserId) { return res.status(403).json({ error: 'Unauthorized' }); } message.text = text; message.isEdited = true; await message.save(); res.json({ success: true, message }); } catch (error) { console.error('Edit message error:', error); res.status(500).json({ error: error.message }); } }); // الإبلاغ عن رسالة app.post('/api/chat/messages/:messageId/report', authenticateToken, async (req, res) => { try { const { messageId } = req.params; const { reason } = req.body; const currentUserId = req.user.userId; // تخزين التقرير (يمكن إضافة موديل Reports) console.log(`User ${currentUserId} reported message ${messageId}: ${reason || 'No reason'}`); res.json({ success: true }); } catch (error) { console.error('Report message error:', error); res.status(500).json({ error: error.message }); } }); // تثبيت محادثة app.put('/api/chat/conversations/:conversationId/pin', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const { isPinned } = req.body; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId); if (!conversation) { return res.status(404).json({ error: 'Conversation not found' }); } if (!conversation.participants.includes(currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } if (!conversation.settings) conversation.settings = {}; conversation.settings.isPinned = isPinned; await conversation.save(); res.json({ success: true }); } catch (error) { console.error('Pin conversation error:', error); res.status(500).json({ error: error.message }); } }); // كتم محادثة app.put('/api/chat/conversations/:conversationId/mute', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const { isMuted } = req.body; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId); if (!conversation) { return res.status(404).json({ error: 'Conversation not found' }); } if (!conversation.participants.includes(currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } if (!conversation.settings) conversation.settings = {}; conversation.settings.isMuted = isMuted; await conversation.save(); res.json({ success: true }); } catch (error) { console.error('Mute conversation error:', error); res.status(500).json({ error: error.message }); } }); // تصدير محادثة app.get('/api/chat/conversations/:conversationId/export', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId) .populate('participants', 'username email'); if (!conversation) { return res.status(404).json({ error: 'Conversation not found' }); } const messages = await Message.find({ conversationId }) .populate('senderId', 'username') .sort({ createdAt: 1 }); const exportData = { exportedAt: new Date(), participants: conversation.participants, messages: messages.map(msg => ({ from: msg.senderId.username, text: msg.text, timestamp: msg.createdAt, isEdited: msg.isEdited || false })) }; res.json(exportData); } catch (error) { console.error('Export conversation error:', error); res.status(500).json({ error: error.message }); } }); // حذف محادثة بالكامل app.delete('/api/chat/conversations/:conversationId', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId); if (!conversation) { return res.status(404).json({ error: 'Conversation not found' }); } if (!conversation.participants.includes(currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } await Message.deleteMany({ conversationId }); await Conversation.deleteOne({ _id: conversationId }); res.json({ success: true }); } catch (error) { console.error('Delete conversation error:', error); res.status(500).json({ error: error.message }); } }); // ================ Chat Routes ================ // إنشاء محادثة جديدة أو جلب الموجودة app.post('/api/chat/conversation', authenticateToken, async (req, res) => { try { const { otherUserId } = req.body; const currentUserId = req.user.userId; // البحث عن محادثة موجودة let conversation = await Conversation.findOne({ participants: { $all: [currentUserId, otherUserId] } }).populate('participants', 'username email avatar storeId'); if (!conversation) { // إنشاء محادثة جديدة conversation = new Conversation({ participants: [currentUserId, otherUserId], participantsDetails: [ { userId: currentUserId, lastReadAt: new Date() }, { userId: otherUserId, lastReadAt: new Date() } ] }); await conversation.save(); conversation = await Conversation.findById(conversation._id) .populate('participants', 'username email avatar storeId'); } res.json({ success: true, conversation }); } catch (error) { console.error('Conversation error:', error); res.status(500).json({ error: error.message }); } }); // جلب محادثة واحدة app.get('/api/chat/conversation/:conversationId', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId) .populate({ path: 'participants', select: 'username email avatar storeId lastSeen isOnline', populate: { path: 'storeId', model: 'Store', select: 'name slug logo' } }); if (!conversation) { return res.status(404).json({ error: 'Conversation not found' }); } if (!conversation.participants.some(p => p._id.toString() === currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } const otherParticipant = conversation.participants.find( p => p._id.toString() !== currentUserId ); // حساب حالة المستخدم const isOnline = otherParticipant?.isOnline || false; const lastSeen = otherParticipant?.lastSeen; let statusText = ''; let statusColor = ''; if (isOnline) { statusText = 'Online'; statusColor = 'text-green-500'; } else if (lastSeen) { const minutesAgo = Math.floor((Date.now() - new Date(lastSeen).getTime()) / (1000 * 60)); if (minutesAgo < 1) { statusText = 'Just now'; statusColor = 'text-green-500'; } else if (minutesAgo < 60) { statusText = `${minutesAgo} min ago`; statusColor = 'text-gray-400'; } else if (minutesAgo < 1440) { const hoursAgo = Math.floor(minutesAgo / 60); statusText = `${hoursAgo} hour${hoursAgo > 1 ? 's' : ''} ago`; statusColor = 'text-gray-400'; } else { const daysAgo = Math.floor(minutesAgo / 1440); statusText = `${daysAgo} day${daysAgo > 1 ? 's' : ''} ago`; statusColor = 'text-gray-400'; } } else { statusText = 'Offline'; statusColor = 'text-gray-400'; } const formattedConversation = { _id: conversation._id, participants: conversation.participants, otherUser: { _id: otherParticipant?._id, username: otherParticipant?.username, email: otherParticipant?.email, avatar: otherParticipant?.avatar, storeId: otherParticipant?.storeId, // ✅ الآن يحتوي على storeId مع populated status: { isOnline, lastSeen, text: statusText, color: statusColor } }, lastMessage: conversation.lastMessage, updatedAt: conversation.updatedAt, settings: conversation.settings || {} }; res.json({ conversation: formattedConversation }); } catch (error) { console.error('Get conversation error:', error); res.status(500).json({ error: error.message }); } }); // جلب جميع محادثات المستخدم app.get('/api/chat/conversations', authenticateToken, async (req, res) => { try { const currentUserId = req.user.userId; const conversations = await Conversation.find({ participants: currentUserId, isArchived: false }) .populate({ path: 'participants', select: 'username email avatar storeId lastSeen isOnline', populate: { path: 'storeId', model: 'Store', select: 'name slug logo' } }) .sort({ updatedAt: -1 }); const formattedConversations = await Promise.all(conversations.map(async (conv) => { const otherParticipant = conv.participants.find( p => p._id.toString() !== currentUserId ); const lastMessage = await Message.findOne({ conversationId: conv._id, isDeleted: false, deletedFor: { $ne: currentUserId } }) .sort({ createdAt: -1 }) .select('text senderId createdAt'); const unreadCount = await Message.countDocuments({ conversationId: conv._id, receiverId: currentUserId, isRead: false, isDeleted: false, deletedFor: { $ne: currentUserId } }); let lastMessageText = ''; if (lastMessage) { if (lastMessage.senderId.toString() === currentUserId) { lastMessageText = `You: ${lastMessage.text.substring(0, 50)}${lastMessage.text.length > 50 ? '...' : ''}`; } else { lastMessageText = `${lastMessage.text.substring(0, 50)}${lastMessage.text.length > 50 ? '...' : ''}`; } } else { lastMessageText = 'No messages yet'; } // ✅ حساب حالة المستخدم (نشط/غير نشط) const isUserOnline = otherParticipant?.isOnline || false; const lastSeen = otherParticipant?.lastSeen; let statusText = ''; let statusColor = ''; if (isUserOnline) { statusText = 'Online'; statusColor = 'text-green-500'; } else if (lastSeen) { const minutesAgo = Math.floor((Date.now() - new Date(lastSeen).getTime()) / (1000 * 60)); if (minutesAgo < 1) { statusText = 'Just now'; statusColor = 'text-green-500'; } else if (minutesAgo < 60) { statusText = `${minutesAgo} min ago`; statusColor = 'text-gray-400'; } else if (minutesAgo < 1440) { const hoursAgo = Math.floor(minutesAgo / 60); statusText = `${hoursAgo} hour${hoursAgo > 1 ? 's' : ''} ago`; statusColor = 'text-gray-400'; } else { const daysAgo = Math.floor(minutesAgo / 1440); statusText = `${daysAgo} day${daysAgo > 1 ? 's' : ''} ago`; statusColor = 'text-gray-400'; } } else { statusText = 'Offline'; statusColor = 'text-gray-400'; } return { _id: conv._id, otherUser: { _id: otherParticipant?._id, username: otherParticipant?.username, email: otherParticipant?.email, avatar: otherParticipant?.avatar, storeId: otherParticipant?.storeId, // ✅ الآن يحتوي على storeId populated status: { text: statusText, color: statusColor, isOnline: isUserOnline, lastSeen } }, lastMessage: lastMessageText, lastMessageTime: lastMessage?.createdAt || conv.updatedAt, unreadCount, isTyping: conv.participantsDetails?.find(p => p.userId.toString() === otherParticipant?._id?.toString())?.isTyping || false, isPinned: conv.settings?.isPinned || false }; })); formattedConversations.sort((a, b) => { if (a.isPinned && !b.isPinned) return -1; if (!a.isPinned && b.isPinned) return 1; return new Date(b.lastMessageTime) - new Date(a.lastMessageTime); }); res.json({ conversations: formattedConversations }); } catch (error) { console.error('Get conversations error:', error); res.status(500).json({ error: error.message }); } }); // جلب رسائل محادثة معينة app.get('/api/chat/messages/:conversationId', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const { page = 1, limit = 50 } = req.query; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId); if (!conversation) { return res.status(404).json({ error: 'Conversation not found' }); } if (!conversation.participants.includes(currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } // تحديث آخر قراءة const participantDetail = conversation.participantsDetails.find( p => p.userId.toString() === currentUserId ); if (participantDetail) { participantDetail.lastReadAt = new Date(); await conversation.save(); } // جلب الرسائل const messages = await Message.find({ conversationId, isDeleted: false, deletedFor: { $ne: currentUserId } }) .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); // تحديث حالة القراءة للرسائل await Message.updateMany( { conversationId, receiverId: currentUserId, isRead: false }, { isRead: true, readAt: new Date() } ); res.json({ messages: messages.reverse(), hasMore: messages.length === limit, page: parseInt(page) }); } catch (error) { console.error('Get messages error:', error); res.status(500).json({ error: error.message }); } }); // إرسال رسالة جديدة app.post('/api/chat/messages', authenticateToken, async (req, res) => { try { const { conversationId, receiverId, text, type = 'text', mediaUrl = '', replyTo, duration } = req.body; const senderId = req.user.userId; let convId = conversationId; if (!convId) { // البحث عن محادثة موجودة let conversation = await Conversation.findOne({ participants: { $all: [senderId, receiverId] } }); if (!conversation) { conversation = new Conversation({ participants: [senderId, receiverId], participantsDetails: [ { userId: senderId, lastReadAt: new Date() }, { userId: receiverId, lastReadAt: new Date() } ] }); await conversation.save(); } convId = conversation._id; } const message = new Message({ conversationId: convId, senderId, receiverId, text, type, mediaUrl, duration: duration || null, // ✅ أضف المدة isRead: false, replyTo: replyTo || null }); await message.save(); // تحديث آخر رسالة في المحادثة await Conversation.findByIdAndUpdate(convId, { lastMessage: { text, senderId, sentAt: new Date(), isRead: false, type, mediaUrl, duration: duration || null // ✅ أضف المدة هنا أيضاً }, updatedAt: new Date(), $inc: { unreadCount: 1 } }); const populatedMessage = await Message.findById(message._id) .populate('senderId', 'username email avatar storeId') .populate('receiverId', 'username email avatar storeId'); res.status(201).json({ success: true, message: populatedMessage }); } catch (error) { console.error('Send message error:', error); res.status(500).json({ error: error.message }); } }); // جلب عدد الرسائل غير المقروءة للمستخدم // ================ جلب عدد الرسائل غير المقروءة ================ app.get('/api/chat/unread-count', authenticateToken, async (req, res) => { try { const currentUserId = req.user.userId; // جلب جميع المحادثات التي يشارك فيها المستخدم const conversations = await Conversation.find({ participants: currentUserId, isArchived: false }); // حساب إجمالي الرسائل غير المقروءة let totalUnreadCount = 0; for (const conv of conversations) { const unreadCount = await Message.countDocuments({ conversationId: conv._id, receiverId: currentUserId, isRead: false, isDeleted: false, deletedFor: { $ne: currentUserId } }); totalUnreadCount += unreadCount; } res.json({ unreadCount: totalUnreadCount }); } catch (error) { console.error('Get unread count error:', error); res.status(500).json({ error: error.message }); } }); // تحديث حالة الكتابة app.post('/api/chat/typing', authenticateToken, async (req, res) => { try { const { conversationId, isTyping } = req.body; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId); if (!conversation) { return res.status(404).json({ error: 'Conversation not found' }); } const participantDetail = conversation.participantsDetails.find( p => p.userId.toString() === currentUserId ); if (participantDetail) { participantDetail.isTyping = isTyping; participantDetail.typingAt = isTyping ? new Date() : null; await conversation.save(); } res.json({ success: true }); } catch (error) { console.error('Typing error:', error); res.status(500).json({ error: error.message }); } }); // حذف رسالة (للمستخدم فقط) app.delete('/api/chat/messages/:messageId', authenticateToken, async (req, res) => { try { const { messageId } = req.params; const currentUserId = req.user.userId; const message = await Message.findById(messageId); if (!message) { return res.status(404).json({ error: 'Message not found' }); } if (message.senderId.toString() !== currentUserId) { return res.status(403).json({ error: 'Unauthorized' }); } message.isDeleted = true; message.deletedFor.push(currentUserId); await message.save(); res.json({ success: true }); } catch (error) { console.error('Delete message error:', error); res.status(500).json({ error: error.message }); } }); // أرشفة محادثة app.put('/api/chat/conversations/:conversationId/archive', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const { isArchived } = req.body; await Conversation.findByIdAndUpdate(conversationId, { isArchived }); res.json({ success: true }); } catch (error) { console.error('Archive error:', error); res.status(500).json({ error: error.message }); } }); // البحث عن مستخدمين للمحادثة app.get('/api/chat/search-users', authenticateToken, async (req, res) => { try { const { q } = req.query; const currentUserId = req.user.userId; const users = await User.find({ _id: { $ne: currentUserId }, $or: [ { username: { $regex: q, $options: 'i' } }, { email: { $regex: q, $options: 'i' } } ] }) .select('username email avatar storeId') .limit(20); // إضافة معلومات المحادثة الموجودة const usersWithConversation = await Promise.all(users.map(async (user) => { const conversation = await Conversation.findOne({ participants: { $all: [currentUserId, user._id] } }); return { ...user.toObject(), conversationId: conversation?._id || null }; })); res.json({ users: usersWithConversation }); } catch (error) { console.error('Search users error:', error); res.status(500).json({ error: error.message }); } }); // ================ Reaction API (أضف هذا) ================ app.post('/api/chat/messages/:messageId/reaction', authenticateToken, async (req, res) => { try { const { messageId } = req.params; const { reaction } = req.body; const currentUserId = req.user.userId; const message = await Message.findById(messageId); if (!message) { return res.status(404).json({ error: 'Message not found' }); } // التحقق من أن المستخدم مشارك في المحادثة const conversation = await Conversation.findById(message.conversationId); if (!conversation.participants.includes(currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } // إضافة أو إزالة الرد فعل if (!message.reactions) message.reactions = {}; if (message.reactions[currentUserId] === reaction) { // إزالة الرد فعل delete message.reactions[currentUserId]; } else { // إضافة أو تحديث الرد فعل message.reactions[currentUserId] = reaction; } await message.save(); res.json({ success: true, reactions: message.reactions }); } catch (error) { console.error('Reaction error:', error); res.status(500).json({ error: error.message }); } }); // جلب الوسائط المشتركة في المحادثة app.get('/api/chat/conversations/:conversationId/media', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId); if (!conversation || !conversation.participants.includes(currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } const messages = await Message.find({ conversationId, mediaUrl: { $ne: null, $ne: '' }, isDeleted: false }).select('mediaUrl type createdAt'); const media = messages.map(msg => ({ url: msg.mediaUrl, type: msg.type || 'image', createdAt: msg.createdAt })); res.json({ media }); } catch (error) { res.status(500).json({ error: error.message }); } }); // تحديث إعدادات المحادثة app.put('/api/chat/conversations/:conversationId/settings', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const currentUserId = req.user.userId; const settings = req.body; const conversation = await Conversation.findById(conversationId); if (!conversation || !conversation.participants.includes(currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } if (!conversation.settings) conversation.settings = {}; Object.assign(conversation.settings, settings); await conversation.save(); res.json({ success: true, settings: conversation.settings }); } catch (error) { res.status(500).json({ error: error.message }); } }); // جلب إعدادات المحادثة app.get('/api/chat/conversations/:conversationId/settings', authenticateToken, async (req, res) => { try { const { conversationId } = req.params; const currentUserId = req.user.userId; const conversation = await Conversation.findById(conversationId); if (!conversation || !conversation.participants.includes(currentUserId)) { return res.status(403).json({ error: 'Unauthorized' }); } res.json({ settings: conversation.settings || {} }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ================ Integration Routes ================ // جلب تكاملات البائع app.get('/api/integrations', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const integrations = await UserIntegration.find({ storeId: store._id, userId: req.user.userId }); res.json(integrations); } catch (error) { res.status(500).json({ error: error.message }); } }); // إنشاء تكامل جديد app.post('/api/integrations', authenticateToken, async (req, res) => { try { const { service, apiKey, apiSecret, settings } = req.body; const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } // التحقق من وجود تكامل سابق لنفس الخدمة const existingIntegration = await UserIntegration.findOne({ storeId: store._id, service: service }); if (existingIntegration) { return res.status(400).json({ error: `Integration with ${service} already exists` }); } const serviceNames = { bosta: 'Bosta (أوصل)', talabat: 'Talabat', fatura: 'Fatura (فاتورة)', paymob: 'Paymob', vodafone_cash: 'Vodafone Cash', instapay: 'InstaPay', fawry: 'Fawry' }; const integration = new UserIntegration({ userId: req.user.userId, storeId: store._id, service, serviceName: serviceNames[service] || service, apiKey, apiSecret, settings: settings || {}, status: 'pending' }); await integration.save(); res.status(201).json(integration); } catch (error) { res.status(500).json({ error: error.message }); } }); // تحديث تكامل app.put('/api/integrations/:integrationId', authenticateToken, async (req, res) => { try { const { integrationId } = req.params; const { apiKey, apiSecret, settings, status } = req.body; const integration = await UserIntegration.findById(integrationId); if (!integration) { return res.status(404).json({ error: 'Integration not found' }); } const store = await Store.findOne({ ownerId: req.user.userId }); if (!store || integration.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } if (apiKey !== undefined) integration.apiKey = apiKey; if (apiSecret !== undefined) integration.apiSecret = apiSecret; if (settings !== undefined) integration.settings = { ...integration.settings, ...settings }; if (status !== undefined) integration.status = status; integration.updatedAt = new Date(); await integration.save(); res.json(integration); } catch (error) { res.status(500).json({ error: error.message }); } }); // حذف تكامل app.delete('/api/integrations/:integrationId', authenticateToken, async (req, res) => { try { const { integrationId } = req.params; const integration = await UserIntegration.findById(integrationId); if (!integration) { return res.status(404).json({ error: 'Integration not found' }); } const store = await Store.findOne({ ownerId: req.user.userId }); if (!store || integration.storeId.toString() !== store._id.toString()) { return res.status(403).json({ error: 'Unauthorized' }); } await integration.deleteOne(); res.json({ success: true, message: 'Integration deleted' }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Webhook لاستقبال تحديثات الشحن من Bosta app.post('/api/webhooks/bosta', express.json(), async (req, res) => { try { const { orderNumber, trackingNumber, status, location, timestamp } = req.body; const order = await Order.findOne({ orderNumber }); if (!order) { return res.status(404).json({ error: 'Order not found' }); } // تحديث حالة الطلب let orderStatus = order.orderStatus; switch (status) { case 'PICKED_UP': orderStatus = 'processing'; break; case 'IN_TRANSIT': orderStatus = 'shipped'; break; case 'DELIVERED': orderStatus = 'delivered'; break; case 'CANCELLED': orderStatus = 'cancelled'; break; } order.orderStatus = orderStatus; order.trackingNumber = trackingNumber; order.trackingHistory.push({ status: orderStatus, location: location || '', note: `Bosta update: ${status}`, timestamp: new Date(timestamp) }); await order.save(); // تحديث المخزون في حالة التسليم if (status === 'DELIVERED') { order.paymentStatus = 'paid'; await order.save(); } res.json({ success: true }); } catch (error) { console.error('Bosta webhook error:', error); res.status(500).json({ error: error.message }); } }); // ================ Seller Customers Routes ================ // جلب عملاء البائع app.get('/api/seller/customers', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } // جلب جميع الطلبات الخاصة بالمتجر const orders = await Order.find({ 'items.storeId': store._id }) .populate('customerId', 'name phone email address') .sort({ createdAt: -1 }); // تجميع العملاء const customersMap = new Map(); for (const order of orders) { const customer = order.customerId; if (!customer) continue; if (!customersMap.has(customer._id.toString())) { customersMap.set(customer._id.toString(), { _id: customer._id, name: customer.name, email: customer.email, phone: customer.phone, address: customer.address, createdAt: customer.createdAt, totalOrders: 0, totalSpent: 0, lastOrderAt: order.createdAt, averageOrderValue: 0 }); } const customerData = customersMap.get(customer._id.toString()); customerData.totalOrders += 1; customerData.totalSpent += order.totalAmount; if (order.createdAt > customerData.lastOrderAt) { customerData.lastOrderAt = order.createdAt; } customerData.averageOrderValue = customerData.totalSpent / customerData.totalOrders; } res.json(Array.from(customersMap.values())); } catch (error) { console.error('Get customers error:', error); res.status(500).json({ error: error.message }); } }); // جلب طلبات عميل معين app.get('/api/seller/customers/:customerId/orders', authenticateToken, async (req, res) => { try { const store = await Store.findOne({ ownerId: req.user.userId }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const orders = await Order.find({ customerId: req.params.customerId, 'items.storeId': store._id }).sort({ createdAt: -1 }); res.json(orders); } catch (error) { console.error('Get customer orders error:', error); res.status(500).json({ error: error.message }); } }); // ================ AI Design Routes (Complete) ================ // توليد تصميم جديد باستخدام AI app.post('/api/ai/generate-design', authenticateToken, async (req, res) => { try { const { dimensions, colors, pattern, material, prompt } = req.body; if (!dimensions || !dimensions.width || !dimensions.height) { return res.status(400).json({ error: 'Dimensions are required' }); } const area = (dimensions.width * dimensions.height) / 10000; // حساب التكلفة المتقدم const materialCosts = { wool: 250, silk: 800, cotton: 150, polyester: 100, blend: 200, acrylic: 120, nylon: 180, jute: 90, linen: 220 }; const laborCosts = { low: 50, medium: 100, high: 180, premium: 280 }; const densityMultiplier = { low: 0.8, medium: 1.0, high: 1.3, premium: 1.6 }; const materialCost = area * (materialCosts[material.type] || 200); const laborCost = area * (laborCosts[material.density] || 100); const patternCost = (pattern.complexity / 10) * 150; const densityCost = area * 50 * (densityMultiplier[material.density] || 1); const totalCost = Math.round(materialCost + laborCost + patternCost + densityCost); // إنشاء التصميم مع تفاصيل إضافية const design = new Design({ name: `AI Design ${Date.now()}`, userId: req.user.userId, dimensions: { width: dimensions.width, height: dimensions.height, unit: 'cm', area: area.toFixed(2) }, colors: { primary: colors.primary, secondary: colors.secondary || [], accent: colors.accent || ['#FFD700', '#C0C0C0'] }, pattern: { type: pattern.type || 'geometric', complexity: pattern.complexity || 5, description: getPatternDescription(pattern.type, pattern.complexity) }, material: { type: material.type, density: material.density || 'medium', thickness: material.thickness || 1.5, weightPerSquareMeter: material.type === 'silk' ? 1.2 : material.type === 'wool' ? 2.5 : 2.0 }, aiGenerated: true, aiPrompt: prompt || '', costEstimate: { materials: Math.round(materialCost), labor: Math.round(laborCost), pattern: Math.round(patternCost), density: Math.round(densityCost), total: totalCost }, productionTime: Math.ceil(area * (pattern.complexity / 2) * (densityMultiplier[material.density] || 1)), status: 'draft', createdAt: new Date(), updatedAt: new Date() }); await design.save(); // إنشاء SVG فخم let svgPreview; try { console.log('🎨 Generating realistic carpet image with FAL.AI...'); svgPreview = await generateRealisticCarpetImage(design); console.log('✅ FAL.AI image generated successfully'); } catch (error) { console.error('❌ FAL.AI failed, using fallback SVG:', error.message); svgPreview = generateFallbackSVG(design); } design.previewUrl = svgPreview; // توليد جميع صيغ الإنتاج const gcode = generateGCodeForMachine(design); const pythonCode = generatePythonCodeForMachine(design); const dstCode = generateDSTForMachine(design); const embCode = generateEMBForMachine(design); design.gcode = gcode; design.pythonCode = pythonCode; design.dstCode = dstCode; design.embCode = embCode; await design.save(); // إرسال الاستجابة مع جميع البيانات res.json({ success: true, design: { _id: design._id, name: design.name, dimensions: design.dimensions, colors: design.colors, pattern: design.pattern, material: design.material, status: design.status, createdAt: design.createdAt }, preview3D: svgPreview, costEstimate: design.costEstimate, productionTime: design.productionTime, gcode: gcode.substring(0, 1500) + '\n\n... (full G-Code available for download)', pythonCode: pythonCode.substring(0, 1500) + '\n\n... (full Python code available for download)', machineFormats: { gcode: { available: true, length: gcode.length }, python: { available: true, length: pythonCode.length }, dst: { available: true, length: dstCode.length }, emb: { available: true, length: embCode.length } }, message: 'Design generated successfully! Ready for machine production.' }); } catch (error) { console.error('Generate design error:', error); res.status(500).json({ error: error.message }); } }); // جلب جميع تصاميم المستخدم app.get('/api/designs', authenticateToken, async (req, res) => { try { const { page = 1, limit = 20, status } = req.query; const query = { userId: req.user.userId }; if (status && status !== 'all') { query.status = status; } const designs = await Design.find(query) .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await Design.countDocuments(query); res.json({ designs, total, page: parseInt(page), pages: Math.ceil(total / limit) }); } catch (error) { console.error('Get designs error:', error); res.status(500).json({ error: error.message }); } }); // جلب تصميم واحد app.get('/api/designs/:designId', authenticateToken, async (req, res) => { try { const design = await Design.findById(req.params.designId); if (!design) { return res.status(404).json({ error: 'Design not found' }); } if (design.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } res.json(design); } catch (error) { console.error('Get design error:', error); res.status(500).json({ error: error.message }); } }); // تحديث حالة التصميم app.put('/api/designs/:designId/status', authenticateToken, async (req, res) => { try { const { status } = req.body; const design = await Design.findById(req.params.designId); if (!design) { return res.status(404).json({ error: 'Design not found' }); } if (design.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } const validStatuses = ['draft', 'approved', 'production', 'completed', 'cancelled']; if (!validStatuses.includes(status)) { return res.status(400).json({ error: 'Invalid status' }); } design.status = status; design.updatedAt = Date.now(); await design.save(); res.json({ success: true, design }); } catch (error) { console.error('Update design status error:', error); res.status(500).json({ error: error.message }); } }); // حذف تصميم app.delete('/api/designs/:designId', authenticateToken, async (req, res) => { try { const design = await Design.findById(req.params.designId); if (!design) { return res.status(404).json({ error: 'Design not found' }); } if (design.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } await design.deleteOne(); res.json({ success: true, message: 'Design deleted' }); } catch (error) { console.error('Delete design error:', error); res.status(500).json({ error: error.message }); } }); // حفظ التصميم كمنتج (تحويل التصميم إلى منتج حقيقي) app.post('/api/designs/:designId/save-to-products', authenticateToken, async (req, res) => { try { const design = await Design.findById(req.params.designId); if (!design) { return res.status(404).json({ error: 'Design not found' }); } if (design.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } // جلب المتجر الخاص بالمستخدم let store = await Store.findOne({ ownerId: req.user.userId }); // إذا لم يكن للمستخدم متجر، قم بإنشاء متجر افتراضي if (!store && req.user.role !== 'admin') { store = new Store({ name: `${req.user.username}'s Store`, slug: `${req.user.username}-store-${Date.now()}`, ownerId: req.user.userId, 'settings.isActive': true, stats: { totalProducts: 0, totalSales: 0, totalRevenue: 0, views: 0 } }); await store.save(); await User.findByIdAndUpdate(req.user.userId, { storeId: store._id, canSell: true, role: 'seller' }); } const storeId = store ? store._id : null; // إنشاء منتج من التصميم const slug = `${storeId || 'user'}-${design.name .toLowerCase() .replace(/[^a-z0-9\u0621-\u064A]+/g, '-') .replace(/^-|-$/g, '')}`; const product = new Product({ name: design.name, slug, storeId: storeId, ownerId: req.user.userId, category: 'carpet', subcategory: design.pattern.type, material: design.material.type, size: `${design.dimensions.width}x${design.dimensions.height} cm`, price: design.costEstimate?.total || 100, quantity: 10, description: `AI Generated Design\nDimensions: ${design.dimensions.width}x${design.dimensions.height} cm\nPattern: ${design.pattern.type}\nMaterial: ${design.material.type}\n\n${design.aiPrompt || ''}`, features: [ `AI Generated Design`, `${design.material.type.charAt(0).toUpperCase() + design.material.type.slice(1)} Material`, `${design.pattern.type.charAt(0).toUpperCase() + design.pattern.type.slice(1)} Pattern`, `Complexity Level: ${design.pattern.complexity}/10` ], tags: ['ai-generated', design.pattern.type, design.material.type], status: 'pending', inStock: true, images: design.previewUrl ? [design.previewUrl] : [] }); await product.save(); if (storeId) { await Store.findByIdAndUpdate(storeId, { $inc: { 'stats.totalProducts': 1 } }); } // تحديث حالة التصميم design.status = 'approved'; await design.save(); res.status(201).json({ success: true, product, message: 'Design saved as product successfully' }); } catch (error) { console.error('Save design to products error:', error); res.status(500).json({ error: error.message }); } }); // ================ Material Library Routes (Complete) ================ // جلب جميع المواد app.get('/api/materials', async (req, res) => { try { const { category, search, page = 1, limit = 50 } = req.query; const query = { isActive: true }; if (category && category !== 'all') { query.category = category; } if (search) { query.$or = [ { name: { $regex: search, $options: 'i' } }, { supplier: { $regex: search, $options: 'i' } } ]; } const materials = await Material.find(query) .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await Material.countDocuments(query); res.json({ materials, total, page: parseInt(page), pages: Math.ceil(total / limit) }); } catch (error) { console.error('Get materials error:', error); res.status(500).json({ error: error.message }); } }); // جلب مادة واحدة app.get('/api/materials/:materialId', async (req, res) => { try { const material = await Material.findById(req.params.materialId); if (!material) { return res.status(404).json({ error: 'Material not found' }); } res.json(material); } catch (error) { console.error('Get material error:', error); res.status(500).json({ error: error.message }); } }); // إنشاء مادة جديدة (للمسؤول فقط) app.post('/api/materials', authenticateToken, isAdmin, async (req, res) => { try { const { name, category, supplier, pricePerKg, pricePerMeter, availableColors, availableQuantities, thickness, weight, durability, softness, imageUrl } = req.body; // التحقق من وجود اسم فريد const existingMaterial = await Material.findOne({ name }); if (existingMaterial) { return res.status(400).json({ error: 'Material with this name already exists' }); } const material = new Material({ name, category, supplier, pricePerKg, pricePerMeter: pricePerMeter || 0, availableColors: availableColors || [], availableQuantities: availableQuantities || 0, thickness: thickness || 1.5, weight: weight || 2.5, durability: durability || 5, softness: softness || 5, imageUrl: imageUrl || '', isActive: true }); await material.save(); res.status(201).json(material); } catch (error) { console.error('Create material error:', error); res.status(500).json({ error: error.message }); } }); // تحديث مادة (للمسؤول فقط) app.put('/api/materials/:materialId', authenticateToken, isAdmin, async (req, res) => { try { const material = await Material.findById(req.params.materialId); if (!material) { return res.status(404).json({ error: 'Material not found' }); } const { name, category, supplier, pricePerKg, pricePerMeter, availableColors, availableQuantities, thickness, weight, durability, softness, imageUrl, isActive } = req.body; if (name && name !== material.name) { const existingMaterial = await Material.findOne({ name, _id: { $ne: material._id } }); if (existingMaterial) { return res.status(400).json({ error: 'Material with this name already exists' }); } material.name = name; } if (category) material.category = category; if (supplier) material.supplier = supplier; if (pricePerKg !== undefined) material.pricePerKg = pricePerKg; if (pricePerMeter !== undefined) material.pricePerMeter = pricePerMeter; if (availableColors) material.availableColors = availableColors; if (availableQuantities !== undefined) material.availableQuantities = availableQuantities; if (thickness !== undefined) material.thickness = thickness; if (weight !== undefined) material.weight = weight; if (durability !== undefined) material.durability = durability; if (softness !== undefined) material.softness = softness; if (imageUrl !== undefined) material.imageUrl = imageUrl; if (isActive !== undefined) material.isActive = isActive; await material.save(); res.json(material); } catch (error) { console.error('Update material error:', error); res.status(500).json({ error: error.message }); } }); // حذف مادة (للمسؤول فقط) app.delete('/api/materials/:materialId', authenticateToken, isAdmin, async (req, res) => { try { const material = await Material.findById(req.params.materialId); if (!material) { return res.status(404).json({ error: 'Material not found' }); } await material.deleteOne(); res.json({ success: true, message: 'Material deleted' }); } catch (error) { console.error('Delete material error:', error); res.status(500).json({ error: error.message }); } }); // إحصائيات المواد app.get('/api/materials/stats/summary', authenticateToken, isAdmin, async (req, res) => { try { const totalMaterials = await Material.countDocuments(); const activeMaterials = await Material.countDocuments({ isActive: true }); const lowStockMaterials = await Material.countDocuments({ availableQuantities: { $lt: 100 } }); const outOfStockMaterials = await Material.countDocuments({ availableQuantities: { $eq: 0 } }); const categoryStats = {}; const categories = ['wool', 'silk', 'cotton', 'polyester', 'blend']; for (const category of categories) { const count = await Material.countDocuments({ category }); categoryStats[category] = count; } const totalValue = await Material.aggregate([ { $group: { _id: null, total: { $sum: { $multiply: ['$pricePerKg', '$availableQuantities'] } } } } ]); res.json({ totalMaterials, activeMaterials, lowStockMaterials, outOfStockMaterials, categoryStats, totalInventoryValue: totalValue[0]?.total || 0 }); } catch (error) { console.error('Materials stats error:', error); res.status(500).json({ error: error.message }); } }); // ================ Helper Functions for Design Studio ================ // دالة توليد EMB (Wilcom) function generateEMBForMachine(design) { const area = (design.dimensions.width * design.dimensions.height) / 10000; const totalStitches = Math.round(area * (design.pattern.complexity * 800)); return `EMB:${design._id}|${design.name}|${design.dimensions.width}x${design.dimensions.height}|${design.pattern.type}|${design.pattern.complexity} STITCHES:${totalStitches} FORMAT:WILCOM_EMB_4.0 MATERIAL:${design.material.type} PALETTE:${design.colors.primary},${design.colors.secondary?.join(',') || ''} THREAD_COUNT:${(design.colors.secondary?.length || 0) + 1} PRODUCTION_TIME:${Math.ceil(totalStitches / 5000)} hours `; } // دالة تحويل التصميم إلى صيغ مختلفة function convertDesignToMachineFormat(design, format = 'gcode') { switch (format) { case 'gcode': return generateGCodeForMachine(design); case 'python': return generatePythonCodeForMachine(design); case 'dst': return generateDSTForMachine(design); case 'emb': return generateEMBForMachine(design); default: return generateGCodeForMachine(design); } } // ================ SVG Generation Functions ================ // ================ Realistic Carpet SVG Generation ================ // ================ Realistic Carpet SVG Generation (Enhanced) ================ function generateSimpleSVG(design) { const width = design.dimensions.width; const height = design.dimensions.height; const primaryColor = design.colors.primary; const secondaryColors = design.colors.secondary || ['#8B4513', '#A0522D', '#CD853F']; const accentColors = design.colors.accent || ['#FFD700', '#DAA520', '#B8860B']; const patternType = design.pattern.type; const complexity = design.pattern.complexity; const materialType = design.material.type; // استخدام بذرة عشوائية ثابتة للحصول على نتائج متسقة const randomSeed = design._id ? parseInt(design._id.toString().slice(-8), 16) : Math.floor(Math.random() * 10000); let random = mulberry32(randomSeed); function randRange(min, max) { return min + random() * (max - min); } // خصائص المادة مع قيم محسنة const materialProperties = { wool: { pileIntensity: 3, threadSize: 1.5, shineIntensity: 0.15, baseOpacity: 0.95, contrast: 1.1 }, silk: { pileIntensity: 2, threadSize: 1.0, shineIntensity: 0.35, baseOpacity: 0.98, contrast: 1.2 }, cotton: { pileIntensity: 4, threadSize: 2.0, shineIntensity: 0.1, baseOpacity: 0.92, contrast: 1.0 }, polyester: { pileIntensity: 3, threadSize: 1.2, shineIntensity: 0.25, baseOpacity: 0.95, contrast: 1.05 } }; const props = materialProperties[materialType] || materialProperties.wool; // دالة لتوليد لون أغمق أو أفتح function darkenColor(color, percent) { return adjustColor(color, -percent); } function lightenColor(color, percent) { return adjustColor(color, percent); } let svgContent = ` ${secondaryColors.map((c, i) => ` `).join('')} ${accentColors.map((c, i) => ` `).join('')} `; // ============ Geometric Pattern (Enhanced) ============ if (patternType === 'geometric') { const cellSize = Math.max(50, Math.min(120, Math.floor(180 / complexity))); const rows = Math.ceil(height / cellSize); const cols = Math.ceil(width / cellSize); for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const x = col * cellSize; const y = row * cellSize; const colorIdx = (row + col) % secondaryColors.length; const accentIdx = (row) % accentColors.length; const cellWidth = Math.min(cellSize, width - x); const cellHeight = Math.min(cellSize, height - y); const centerX = x + cellWidth / 2; const centerY = y + cellHeight / 2; // خلفية الخلية svgContent += ``; // إطار الخلية svgContent += ``; // نجمة ثمانية الأطراف const starSize = cellWidth * 0.32; const starPoints = []; for (let i = 0; i < 8; i++) { const angle = (i * 45) * Math.PI / 180; const radius = i % 2 === 0 ? starSize : starSize * 0.55; starPoints.push(`${centerX + Math.cos(angle) * radius},${centerY + Math.sin(angle) * radius}`); } svgContent += ``; // دائرة مركزية svgContent += ``; svgContent += ``; // زخارف الزوايا if (complexity >= 5) { const cornerSize = cellWidth * 0.12; svgContent += ``; svgContent += ``; svgContent += ``; svgContent += ``; } } } // الإطار الخارجي الفاخر svgContent += ``; svgContent += ``; // شرائط زخرفية أفقية for (let i = 0; i < 3; i++) { const yPos = 28 + i * 14; svgContent += ``; svgContent += ``; } } // ============ Floral Pattern (Enhanced) ============ else if (patternType === 'floral') { const numLargeFlowers = Math.max(3, Math.floor(complexity / 2.5)); const numMedFlowers = Math.max(5, complexity); const numSmallFlowers = Math.max(10, complexity + 3); // زهور كبيرة for (let f = 0; f < numLargeFlowers; f++) { const flowerX = 50 + random() * (width - 100); const flowerY = 50 + random() * (height - 100); const flowerSize = 40 + (complexity * 5); const colorIdx = f % secondaryColors.length; const accentIdx = f % accentColors.length; // بتلات الزهرة for (let p = 0; p < 12; p++) { const angle = (p * 30) * Math.PI / 180; const petalX = flowerX + Math.cos(angle) * flowerSize * 0.75; const petalY = flowerY + Math.sin(angle) * flowerSize * 0.75; const petalW = flowerSize * 0.3; const petalH = flowerSize * 0.5; svgContent += ``; } // مركز الزهرة svgContent += ``; svgContent += ``; // أوراق svgContent += ``; svgContent += ``; } // زهور متوسطة for (let f = 0; f < numMedFlowers; f++) { const flowerX = 30 + random() * (width - 60); const flowerY = 30 + random() * (height - 60); const flowerSize = 20 + random() * 18; const colorIdx = f % secondaryColors.length; for (let p = 0; p < 8; p++) { const angle = (p * 45) * Math.PI / 180; const petalX = flowerX + Math.cos(angle) * flowerSize * 0.65; const petalY = flowerY + Math.sin(angle) * flowerSize * 0.65; svgContent += ``; } svgContent += ``; } // زهور صغيرة for (let f = 0; f < numSmallFlowers; f++) { const flowerX = 15 + random() * (width - 30); const flowerY = 15 + random() * (height - 30); const flowerSize = 8 + random() * 10; const colorIdx = Math.floor(random() * secondaryColors.length); for (let p = 0; p < 5; p++) { const angle = (p * 72) * Math.PI / 180; const petalX = flowerX + Math.cos(angle) * flowerSize; const petalY = flowerY + Math.sin(angle) * flowerSize; svgContent += ``; } svgContent += ``; } // أغصان متعرجة for (let s = 0; s < complexity * 2; s++) { const startX = 20 + random() * (width - 40); const startY = height - 30; const endX = 20 + random() * (width - 40); const endY = 40 + random() * (height / 2.5); svgContent += ``; } } // ============ Traditional Pattern (Enhanced Persian) ============ else if (patternType === 'traditional') { const medallionCenterX = width / 2; const medallionCenterY = height / 2; const medallionRadius = Math.min(width, height) * 0.22; // مدالية مركزية متعددة الطبقات for (let i = 0; i < 4; i++) { const radius = medallionRadius - i * 10; svgContent += ``; } // حشوة المدالية svgContent += ``; // نجمة مركزية 16 رأس const starPoints = []; for (let i = 0; i < 16; i++) { const angle = (i * 22.5) * Math.PI / 180; const radius = i % 2 === 0 ? medallionRadius * 0.52 : medallionRadius * 0.32; starPoints.push(`${medallionCenterX + Math.cos(angle) * radius},${medallionCenterY + Math.sin(angle) * radius}`); } svgContent += ``; // مركز ذهبي svgContent += ``; svgContent += ``; // زوايا فاخرة const cornerSize = Math.min(width, height) * 0.16; const corners = [ { x: 18, y: 18 }, { x: width - cornerSize - 18, y: 18 }, { x: 18, y: height - cornerSize - 18 }, { x: width - cornerSize - 18, y: height - cornerSize - 18 } ]; corners.forEach((corner, idx) => { svgContent += ``; svgContent += ``; svgContent += ``; }); // حزام حدودي const borderStep = 38; for (let x = 45; x < width - 45; x += borderStep) { svgContent += ``; svgContent += ``; } } // ============ Abstract Pattern (Enhanced) ============ else if (patternType === 'abstract') { const numElements = 35 + complexity * 6; for (let i = 0; i < numElements; i++) { const elementType = Math.floor(random() * 5); const x = 20 + random() * (width - 40); const y = 20 + random() * (height - 40); const size = 25 + random() * 60; const colorIdx = Math.floor(random() * secondaryColors.length); const accentIdx = Math.floor(random() * accentColors.length); const rotation = random() * 360; switch (elementType) { case 0: svgContent += ``; svgContent += ``; break; case 1: svgContent += ``; break; case 2: svgContent += ``; break; case 3: for (let w = 0; w < 3; w++) { svgContent += ``; } break; case 4: svgContent += ``; break; } } // خطوط ديناميكية for (let l = 0; l < complexity * 2.5; l++) { const startX = random() * width; const startY = random() * height; const endX = random() * width; const endY = random() * height; svgContent += ``; } } svgContent += ` ✦ Naseej AI Artisan ✦ `; return `data:image/svg+xml;utf8,${encodeURIComponent(svgContent)}`; } // دالة البذرة العشوائية function mulberry32(seed) { return function () { let t = seed += 0x6D2B79F5; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; }; } function adjustColor(color, percent) { let r, g, b; if (color.startsWith('#')) { r = parseInt(color.slice(1, 3), 16); g = parseInt(color.slice(3, 5), 16); b = parseInt(color.slice(5, 7), 16); } else { return color; } r = Math.min(255, Math.max(0, r + percent)); g = Math.min(255, Math.max(0, g + percent)); b = Math.min(255, Math.max(0, b + percent)); return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; } // دالة توليد DST (Tajima) function generateDSTForMachine(design) { const area = (design.dimensions.width * design.dimensions.height) / 10000; const totalStitches = Math.round(area * (design.pattern.complexity * 800)); const colors = [design.colors.primary, ...(design.colors.secondary || [])]; return `DST:${design._id}|${design.name}|${design.dimensions.width}x${design.dimensions.height}|${design.pattern.type}|${design.pattern.complexity} STITCH_COUNT:${totalStitches} COLORS:${colors.join(',')} MATERIAL:${design.material.type} DESCRIPTION:AI Generated Design - ${design.name} NOTES:Created with Naseej AI Design Studio `; } // مسار لتحميل G-Code كملف app.get('/api/designs/:designId/download/:format', authenticateToken, async (req, res) => { try { const { designId, format } = req.params; const design = await Design.findById(designId); if (!design) { return res.status(404).json({ error: 'Design not found' }); } if (design.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } let content = ''; let mimeType = 'text/plain'; let filename = `${design.name}_${designId}`; switch (format) { case 'gcode': content = design.gcode || generateGCodeForMachine(design); filename += '.gcode'; break; case 'python': content = design.pythonCode || generatePythonCodeForMachine(design); filename += '.py'; break; case 'dst': content = design.dstCode || generateDSTForMachine(design); filename += '.dst'; break; case 'emb': content = design.embCode || generateEMBForMachine(design); filename += '.emb'; break; default: return res.status(400).json({ error: 'Invalid format' }); } res.setHeader('Content-Type', mimeType); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.send(content); } catch (error) { console.error('Download error:', error); res.status(500).json({ error: error.message }); } }); // ================ Pattern Library Routes ================ // جلب جميع النقوش app.get('/api/patterns', async (req, res) => { try { const { category, search, page = 1, limit = 50 } = req.query; const query = { isPublic: true }; if (category && category !== 'all') { query.category = category; } if (search) { query.$or = [ { name: { $regex: search, $options: 'i' } }, { tags: { $in: [new RegExp(search, 'i')] } } ]; } const patterns = await Pattern.find(query) .sort({ usageCount: -1, createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await Pattern.countDocuments(query); res.json({ patterns, total, page: parseInt(page), pages: Math.ceil(total / limit) }); } catch (error) { console.error('Get patterns error:', error); res.status(500).json({ error: error.message }); } }); // جلب نقش واحد app.get('/api/patterns/:patternId', async (req, res) => { try { const pattern = await Pattern.findById(req.params.patternId); if (!pattern) { return res.status(404).json({ error: 'Pattern not found' }); } // زيادة عدد الاستخدامات pattern.usageCount += 1; await pattern.save(); res.json(pattern); } catch (error) { console.error('Get pattern error:', error); res.status(500).json({ error: error.message }); } }); // إنشاء نقش جديد app.post('/api/patterns', authenticateToken, async (req, res) => { try { const { name, category, svgData, tags, isPublic } = req.body; const existingPattern = await Pattern.findOne({ name }); if (existingPattern) { return res.status(400).json({ error: 'Pattern with this name already exists' }); } const pattern = new Pattern({ name, category, svgData, tags: tags || [], isPublic: isPublic || false, createdBy: req.user.userId, usageCount: 0 }); await pattern.save(); res.status(201).json(pattern); } catch (error) { console.error('Create pattern error:', error); res.status(500).json({ error: error.message }); } }); // تحديث نقش app.put('/api/patterns/:patternId', authenticateToken, async (req, res) => { try { const pattern = await Pattern.findById(req.params.patternId); if (!pattern) { return res.status(404).json({ error: 'Pattern not found' }); } if (pattern.createdBy.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } const { name, category, svgData, tags, isPublic } = req.body; if (name) pattern.name = name; if (category) pattern.category = category; if (svgData) pattern.svgData = svgData; if (tags) pattern.tags = tags; if (isPublic !== undefined) pattern.isPublic = isPublic; await pattern.save(); res.json(pattern); } catch (error) { console.error('Update pattern error:', error); res.status(500).json({ error: error.message }); } }); // حذف نقش app.delete('/api/patterns/:patternId', authenticateToken, async (req, res) => { try { const pattern = await Pattern.findById(req.params.patternId); if (!pattern) { return res.status(404).json({ error: 'Pattern not found' }); } if (pattern.createdBy.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } await pattern.deleteOne(); res.json({ success: true, message: 'Pattern deleted' }); } catch (error) { console.error('Delete pattern error:', error); res.status(500).json({ error: error.message }); } }); // ================ Partner API Endpoints ================ // جلب قائمة المصانع المتاحة (للمستخدمين) app.get('/api/partners/factories', async (req, res) => { try { const factories = await PartnerFactory.find({ isActive: true }) .select('name partnerId capabilities pricing contactInfo'); res.json(factories); } catch (error) { res.status(500).json({ error: error.message }); } }); app.post('/api/admin/factories', authenticateToken, isAdmin, async (req, res) => { try { const { name, partnerId, capabilities, pricing, contactInfo, webhookUrl } = req.body; // التحقق من وجود partnerId فريد const existingFactory = await PartnerFactory.findOne({ partnerId }); if (existingFactory) { return res.status(400).json({ error: 'Partner ID already exists' }); } // إنشاء API Key و Secret فريدين const apiKey = `naseej_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; const apiSecret = Math.random().toString(36).substring(2, 20) + Math.random().toString(36).substring(2, 10); const factory = new PartnerFactory({ name, partnerId, apiKey, apiSecret, capabilities: capabilities || { maxWidth: 400, maxHeight: 400, materials: [], patterns: [] }, pricing: pricing || { basePricePerSqm: 100, materialMultiplier: new Map(), complexityMultiplier: new Map() }, contactInfo: contactInfo || {}, webhookUrl: webhookUrl || '', isActive: true }); await factory.save(); res.status(201).json({ success: true, factory: { _id: factory._id, name: factory.name, partnerId: factory.partnerId, apiKey: factory.apiKey, apiSecret: factory.apiSecret, isActive: factory.isActive } }); } catch (error) { console.error('Create factory error:', error); res.status(500).json({ error: error.message }); } }); app.put('/api/admin/factories/:factoryId', authenticateToken, isAdmin, async (req, res) => { try { const factory = await PartnerFactory.findById(req.params.factoryId); if (!factory) { return res.status(404).json({ error: 'Factory not found' }); } const { name, capabilities, pricing, contactInfo, webhookUrl, isActive } = req.body; if (name) factory.name = name; if (capabilities) factory.capabilities = capabilities; if (pricing) factory.pricing = pricing; if (contactInfo) factory.contactInfo = contactInfo; if (webhookUrl !== undefined) factory.webhookUrl = webhookUrl; if (isActive !== undefined) factory.isActive = isActive; await factory.save(); res.json({ success: true, factory }); } catch (error) { console.error('Update factory error:', error); res.status(500).json({ error: error.message }); } }); app.delete('/api/admin/factories/:factoryId', authenticateToken, isAdmin, async (req, res) => { try { const factory = await PartnerFactory.findById(req.params.factoryId); if (!factory) { return res.status(404).json({ error: 'Factory not found' }); } await factory.deleteOne(); res.json({ success: true, message: 'Factory deleted' }); } catch (error) { console.error('Delete factory error:', error); res.status(500).json({ error: error.message }); } }); app.get('/api/admin/production-orders', authenticateToken, isAdmin, async (req, res) => { try { const { status, page = 1, limit = 50 } = req.query; const query = {}; if (status) query.status = status; const orders = await ProductionOrder.find(query) .populate('designId', 'name dimensions previewUrl') .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await ProductionOrder.countDocuments(query); res.json({ orders, total, page: parseInt(page), pages: Math.ceil(total / limit) }); } catch (error) { res.status(500).json({ error: error.message }); } }); // تحديث حالة طلب الإنتاج (للمسؤول) app.put('/api/admin/production-orders/:orderId/status', authenticateToken, isAdmin, async (req, res) => { try { const { orderId } = req.params; const { status, trackingNumber, shippingCompany, partnerNotes } = req.body; const productionOrder = await ProductionOrder.findById(orderId); if (!productionOrder) { return res.status(404).json({ error: 'Production order not found' }); } productionOrder.status = status; if (trackingNumber) productionOrder.trackingNumber = trackingNumber; if (shippingCompany) productionOrder.shippingCompany = shippingCompany; if (partnerNotes) productionOrder.partnerNotes = partnerNotes; if (status === 'completed') { productionOrder.actualCompletion = new Date(); } await productionOrder.save(); res.json({ success: true, productionOrder }); } catch (error) { console.error('Update production order error:', error); res.status(500).json({ error: error.message }); } }); // ________________________ app.get('/api/factory/orders', async (req, res) => { try { const apiKey = req.headers['x-api-key']; const apiSecret = req.headers['x-api-secret']; if (!apiKey || !apiSecret) { return res.status(401).json({ error: 'API credentials required' }); } const factory = await PartnerFactory.findOne({ apiKey, apiSecret, isActive: true }); if (!factory) { return res.status(401).json({ error: 'Invalid API credentials' }); } const { status, page = 1, limit = 50 } = req.query; const query = { partnerId: factory.partnerId }; if (status) query.status = status; const orders = await ProductionOrder.find(query) .populate('designId', 'name dimensions previewUrl colors material pattern') .sort({ createdAt: -1 }) .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await ProductionOrder.countDocuments(query); res.json({ factory: { id: factory.partnerId, name: factory.name }, orders, total, page: parseInt(page), pages: Math.ceil(total / limit) }); } catch (error) { console.error('Factory get orders error:', error); res.status(500).json({ error: error.message }); } }); // جلب طلب إنتاج محدد للمصنع app.get('/api/factory/orders/:orderId', async (req, res) => { try { const apiKey = req.headers['x-api-key']; const apiSecret = req.headers['x-api-secret']; if (!apiKey || !apiSecret) { return res.status(401).json({ error: 'API credentials required' }); } const factory = await PartnerFactory.findOne({ apiKey, apiSecret, isActive: true }); if (!factory) { return res.status(401).json({ error: 'Invalid API credentials' }); } const order = await ProductionOrder.findOne({ _id: req.params.orderId, partnerId: factory.partnerId }).populate('designId', 'name dimensions previewUrl colors material pattern'); if (!order) { return res.status(404).json({ error: 'Order not found' }); } res.json(order); } catch (error) { console.error('Factory get order error:', error); res.status(500).json({ error: error.message }); } }); // تحديث حالة طلب الإنتاج من المصنع app.put('/api/factory/orders/:orderId/status', async (req, res) => { try { const apiKey = req.headers['x-api-key']; const apiSecret = req.headers['x-api-secret']; if (!apiKey || !apiSecret) { return res.status(401).json({ error: 'API credentials required' }); } const factory = await PartnerFactory.findOne({ apiKey, apiSecret, isActive: true }); if (!factory) { return res.status(401).json({ error: 'Invalid API credentials' }); } const { orderId } = req.params; const { status, trackingNumber, shippingCompany, notes, progress, estimatedCompletion } = req.body; const order = await ProductionOrder.findOne({ _id: orderId, partnerId: factory.partnerId }); if (!order) { return res.status(404).json({ error: 'Order not found' }); } // تحديث الحالة if (status) { order.status = status; // تسجيل في سجل التتبع if (!order.trackingHistory) order.trackingHistory = []; order.trackingHistory.push({ status, note: notes || `Status updated to ${status} by ${factory.name}`, timestamp: new Date() }); } if (trackingNumber) order.trackingNumber = trackingNumber; if (shippingCompany) order.shippingCompany = shippingCompany; if (partnerNotes) order.partnerNotes = notes; if (progress !== undefined) order.progress = progress; if (estimatedCompletion) order.estimatedCompletion = new Date(estimatedCompletion); if (status === 'completed') { order.actualCompletion = new Date(); } if (status === 'shipped') { order.shippedAt = new Date(); } await order.save(); // إرسال إشعار للمستخدم (WebSocket أو Email) // يمكن إضافة إشعار في قاعدة البيانات للمستخدم res.json({ success: true, order: { _id: order._id, status: order.status, trackingNumber: order.trackingNumber, estimatedCompletion: order.estimatedCompletion } }); } catch (error) { console.error('Factory update order error:', error); res.status(500).json({ error: error.message }); } }); // جلب تفاصيل التصميم (G-code) للمصنع app.get('/api/factory/orders/:orderId/gcode', async (req, res) => { try { const apiKey = req.headers['x-api-key']; const apiSecret = req.headers['x-api-secret']; if (!apiKey || !apiSecret) { return res.status(401).json({ error: 'API credentials required' }); } const factory = await PartnerFactory.findOne({ apiKey, apiSecret, isActive: true }); if (!factory) { return res.status(401).json({ error: 'Invalid API credentials' }); } const order = await ProductionOrder.findOne({ _id: req.params.orderId, partnerId: factory.partnerId }); if (!order) { return res.status(404).json({ error: 'Order not found' }); } res.json({ designId: order.designId, gcode: order.gcode, format: 'gcode', version: '1.0' }); } catch (error) { console.error('Factory get gcode error:', error); res.status(500).json({ error: error.message }); } }); // _________________ app.get('/api/admin/factories/:factoryId', authenticateToken, isAdmin, async (req, res) => { try { const factory = await PartnerFactory.findById(req.params.factoryId); if (!factory) { return res.status(404).json({ error: 'Factory not found' }); } res.json(factory); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/admin/factories', authenticateToken, isAdmin, async (req, res) => { try { const factories = await PartnerFactory.find().sort({ createdAt: -1 }); res.json(factories); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/partners/my-orders', authenticateToken, async (req, res) => { try { // جلب تصاميم المستخدم أولاً const userDesigns = await Design.find({ userId: req.user.userId }).select('_id'); const designIds = userDesigns.map(d => d._id); const orders = await ProductionOrder.find({ designId: { $in: designIds } }) .populate('designId', 'name dimensions previewUrl') .sort({ createdAt: -1 }); res.json(orders); } catch (error) { console.error('Get my production orders error:', error); res.status(500).json({ error: error.message }); } }); // إنشاء طلب إنتاج لمصنع شريك (للمستخدمين) app.post('/api/partners/production-orders', authenticateToken, async (req, res) => { try { const { designId, partnerId, webhookUrl } = req.body; // جلب التصميم const design = await Design.findById(designId); if (!design) { return res.status(404).json({ error: 'Design not found' }); } if (design.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } // جلب المصنع الشريك const partner = await PartnerFactory.findOne({ partnerId, isActive: true }); if (!partner) { return res.status(404).json({ error: 'Partner factory not found' }); } // توليد G-Code للإنتاج const gcode = generateAdvancedGCode(design, partner.capabilities); // حساب التكلفة const area = (design.dimensions.width * design.dimensions.height) / 10000; const materialMultiplier = partner.pricing.materialMultiplier?.get(design.material.type) || 1; const complexityMultiplier = partner.pricing.complexityMultiplier?.get(design.pattern.type) || 1; const estimatedCost = area * partner.pricing.basePricePerSqm * materialMultiplier * complexityMultiplier; // إنشاء طلب إنتاج const productionOrder = new ProductionOrder({ orderId: null, designId: design._id, partnerId: partner.partnerId, gcode: gcode, estimatedCompletion: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), cost: Math.round(estimatedCost), webhookUrl: webhookUrl || partner.webhookUrl, status: 'pending' }); await productionOrder.save(); // ================ إرسال إشعار للمصنع ================ // استخدام apiEndpoint الخاص بالمصنع (إن وجد) أو webhookUrl const notificationUrl = partner.apiEndpoint || productionOrder.webhookUrl; if (notificationUrl) { try { const notificationData = { event: 'new_production_order', orderId: productionOrder._id, orderNumber: `PROD-${productionOrder._id.toString().slice(-8)}`, design: { id: design._id, name: design.name, dimensions: design.dimensions, material: design.material, pattern: design.pattern, gcode: gcode, previewUrl: design.previewUrl, colors: design.colors }, requirements: { quantity: 1, deadline: productionOrder.estimatedCompletion, specialInstructions: design.aiPrompt || '' }, customer: { id: req.user.userId, username: req.user.username, email: req.user.email }, createdAt: new Date().toISOString() }; const headers = {}; if (partner.webhookSecret) { headers['X-Webhook-Secret'] = partner.webhookSecret; headers['X-Webhook-Signature'] = crypto .createHmac('sha256', partner.webhookSecret) .update(JSON.stringify(notificationData)) .digest('hex'); } const response = await axios.post(notificationUrl, notificationData, { headers, timeout: 10000 }); console.log(`✅ Notification sent to ${partner.name} at ${notificationUrl}`); // تحديث حالة الطلب إذا استجاب المصنع بنجاح if (response.data?.status === 'approved') { productionOrder.status = 'approved'; await productionOrder.save(); } } catch (webhookError) { console.error(`❌ Failed to send notification to ${partner.name}:`, webhookError.message); // لا نرجع خطأ للمستخدم، فقط نسجل المشكلة } } else { console.log(`⚠️ No notification URL configured for factory: ${partner.name}`); } res.status(201).json({ success: true, productionOrder: { _id: productionOrder._id, status: productionOrder.status, estimatedCompletion: productionOrder.estimatedCompletion, cost: productionOrder.cost } }); } catch (error) { console.error('Create production order error:', error); res.status(500).json({ error: error.message }); } }); // إضافة مصنع شريك جديد (للمسؤول فقط) app.post('/api/partners/factories', authenticateToken, isAdmin, async (req, res) => { try { const { name, partnerId, capabilities, pricing, contactInfo } = req.body; // إنشاء API Key فريد const apiKey = `naseej_${Date.now()}_${Math.random().toString(36).substring(7)}`; const apiSecret = Math.random().toString(36).substring(2, 15); const factory = new PartnerFactory({ name, partnerId, apiKey, apiSecret, capabilities, pricing, contactInfo, isActive: true }); await factory.save(); res.status(201).json({ success: true, factory: { _id: factory._id, name: factory.name, partnerId: factory.partnerId, apiKey: factory.apiKey, apiSecret: factory.apiSecret } }); } catch (error) { console.error('Create factory error:', error); res.status(500).json({ error: error.message }); } }); // تحديث حالة طلب الإنتاج (للمصنع الشريك) app.put('/api/partners/production-orders/:orderId/status', async (req, res) => { try { const { orderId } = req.params; const { status, trackingNumber, shippingCompany, partnerNotes, apiKey } = req.body; // التحقق من المصنع const partner = await PartnerFactory.findOne({ apiKey: apiKey }); if (!partner) { return res.status(401).json({ error: 'Invalid API key' }); } const productionOrder = await ProductionOrder.findById(orderId); if (!productionOrder) { return res.status(404).json({ error: 'Production order not found' }); } if (productionOrder.partnerId !== partner.partnerId) { return res.status(403).json({ error: 'Unauthorized' }); } productionOrder.status = status; if (trackingNumber) productionOrder.trackingNumber = trackingNumber; if (shippingCompany) productionOrder.shippingCompany = shippingCompany; if (partnerNotes) productionOrder.partnerNotes = partnerNotes; if (status === 'completed') { productionOrder.actualCompletion = new Date(); } await productionOrder.save(); res.json({ success: true, productionOrder }); } catch (error) { console.error('Update production order error:', error); res.status(500).json({ error: error.message }); } }); // جلب حالة طلب الإنتاج (للمستخدم) app.get('/api/partners/production-orders/:orderId/status', authenticateToken, async (req, res) => { try { const productionOrder = await ProductionOrder.findById(req.params.orderId) .populate('designId', 'name dimensions previewUrl'); if (!productionOrder) { return res.status(404).json({ error: 'Production order not found' }); } // التحقق من أن المستخدم يملك التصميم const design = await Design.findById(productionOrder.designId); if (design.userId.toString() !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Unauthorized' }); } res.json({ _id: productionOrder._id, status: productionOrder.status, estimatedCompletion: productionOrder.estimatedCompletion, actualCompletion: productionOrder.actualCompletion, trackingNumber: productionOrder.trackingNumber, shippingCompany: productionOrder.shippingCompany, cost: productionOrder.cost, design: productionOrder.designId }); } catch (error) { console.error('Get production order error:', error); res.status(500).json({ error: error.message }); } }); app.post('/api/partners/webhook/:factoryId', async (req, res) => { try { const { factoryId } = req.params; const { event, orderId, design, gcode, estimatedCompletion } = req.body; // التحقق من المصنع (يمكن إضافة API Key للتحقق) const factory = await PartnerFactory.findOne({ partnerId: factoryId }); if (!factory) { return res.status(401).json({ error: 'Invalid factory' }); } console.log(`📦 Webhook received from ${factory.name}:`, { event, orderId }); // معالجة الحدث switch (event) { case 'new_production_order': // تحديث حالة الطلب await ProductionOrder.findByIdAndUpdate(orderId, { status: 'approved' }); break; case 'production_started': await ProductionOrder.findByIdAndUpdate(orderId, { status: 'in_progress' }); break; case 'production_completed': await ProductionOrder.findByIdAndUpdate(orderId, { status: 'completed', actualCompletion: new Date() }); break; case 'order_shipped': await ProductionOrder.findByIdAndUpdate(orderId, { status: 'shipped', trackingNumber: req.body.trackingNumber, shippingCompany: req.body.shippingCompany }); break; } res.json({ success: true }); } catch (error) { console.error('Webhook error:', error); res.status(500).json({ error: error.message }); } }); app.get('/api/partners/factory/orders', async (req, res) => { try { const apiKey = req.headers['x-api-key']; const apiSecret = req.headers['x-api-secret']; if (!apiKey || !apiSecret) { return res.status(401).json({ error: 'API credentials required' }); } const factory = await PartnerFactory.findOne({ apiKey, apiSecret }); if (!factory) { return res.status(401).json({ error: 'Invalid API credentials' }); } const orders = await ProductionOrder.find({ partnerId: factory.partnerId }) .populate('designId', 'name dimensions previewUrl') .sort({ createdAt: -1 }); res.json({ factory: { name: factory.name, partnerId: factory.partnerId }, orders }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.put('/api/partners/factory/orders/:orderId', async (req, res) => { try { const apiKey = req.headers['x-api-key']; const apiSecret = req.headers['x-api-secret']; if (!apiKey || !apiSecret) { return res.status(401).json({ error: 'API credentials required' }); } const factory = await PartnerFactory.findOne({ apiKey, apiSecret }); if (!factory) { return res.status(401).json({ error: 'Invalid API credentials' }); } const { orderId } = req.params; const { status, trackingNumber, shippingCompany, notes } = req.body; const order = await ProductionOrder.findOne({ _id: orderId, partnerId: factory.partnerId }); if (!order) { return res.status(404).json({ error: 'Order not found' }); } order.status = status || order.status; if (trackingNumber) order.trackingNumber = trackingNumber; if (shippingCompany) order.shippingCompany = shippingCompany; if (notes) order.partnerNotes = notes; if (status === 'completed') { order.actualCompletion = new Date(); } await order.save(); res.json({ success: true, order }); } catch (error) { console.error('Factory update order error:', error); res.status(500).json({ error: error.message }); } }); // ================ Health Check ================ // مسار للتحقق من slugs (مؤقت - يمكن حذفه بعد التصحيح) app.get('/api/debug/store-products/:storeSlug', async (req, res) => { try { const store = await Store.findOne({ slug: req.params.storeSlug }); if (!store) { return res.status(404).json({ error: 'Store not found' }); } const products = await Product.find({ storeId: store._id }, 'name slug'); res.json({ store: { id: store._id, name: store.name, slug: store.slug }, products: products.map(p => ({ name: p.name, slug: p.slug })), totalProducts: products.length }); } catch (error) { res.status(500).json({ error: error.message }); } }); // مسار بسيط لجلب المستخدمين للاختبار (بدون توكن - أزله بعد التصحيح) app.get('/api/public/users', async (req, res) => { try { const users = await User.find({}).select('username email _id'); res.json({ users }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/health', (req, res) => { res.json({ status: 'ok', message: 'Naseej System API is running!', timestamp: new Date() }); }); // ================ Error Handler ================ app.use((err, req, res, next) => { console.error('Error:', err.stack); res.status(500).json({ error: 'Something went wrong!', message: err.message }); }); // ================ Start Server ================ module.exports = app; // تصدير الموديلات للاستخدام في مكان آخر (اختياري) module.exports.User = User; module.exports.Product = Product; module.exports.Customer = Customer; module.exports.Coupon = Coupon; module.exports.ShippingRate = ShippingRate; module.exports.Order = Order; module.exports.Invoice = Invoice; module.exports.Review = Review; if (require.main === module) { const PORT = process.env.PORT || 7860; app.listen(PORT, () => { console.log(`🚀 Server running on http://localhost:${PORT}`); console.log(`📋 API Documentation: http://localhost:${PORT}/api/health`); }); }