Spaces:
Sleeping
Sleeping
| 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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"> | |
| <defs> | |
| <!-- تدرج الخلفية --> | |
| <radialGradient id="carpetGrad" cx="50%" cy="50%" r="75%"> | |
| <stop offset="0%" stop-color="${primaryColor}" stop-opacity="1"/> | |
| <stop offset="40%" stop-color="${adjustColor(primaryColor, -8)}" stop-opacity="1"/> | |
| <stop offset="100%" stop-color="${adjustColor(primaryColor, -20)}" stop-opacity="1"/> | |
| </radialGradient> | |
| <!-- نسيج الوبر --> | |
| <filter id="pileTexture"> | |
| <feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="4" result="noise"/> | |
| <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.15 0" in="noise" result="coloredNoise"/> | |
| <feBlend mode="multiply" in="coloredNoise" in2="SourceGraphic"/> | |
| </filter> | |
| <!-- نمط الخيوط --> | |
| <pattern id="weavePattern" width="8" height="8" patternUnits="userSpaceOnUse"> | |
| <rect width="8" height="8" fill="none"/> | |
| <line x1="0" y1="4" x2="8" y2="4" stroke="rgba(0,0,0,0.1)" stroke-width="0.8"/> | |
| <line x1="4" y1="0" x2="4" y2="8" stroke="rgba(0,0,0,0.08)" stroke-width="0.8"/> | |
| <circle cx="4" cy="4" r="1" fill="rgba(255,255,255,0.05)"/> | |
| </pattern> | |
| </defs> | |
| <!-- الطبقات --> | |
| <rect width="100%" height="100%" fill="url(#carpetGrad)"/> | |
| <rect width="100%" height="100%" fill="url(#weavePattern)"/> | |
| <!-- إطار خارجي --> | |
| <rect x="10" y="10" width="${width-20}" height="${height-20}" fill="none" stroke="${accentColors[0]}" stroke-width="6" rx="8" opacity="0.6"/> | |
| <rect x="18" y="18" width="${width-36}" height="${height-36}" fill="none" stroke="${secondaryColors[0]}" stroke-width="3" rx="5" opacity="0.4"/>`; | |
| // إضافة الزخارف حسب النوع | |
| 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 += `<rect x="${x}" y="${y}" width="${cellSize-15}" height="${cellSize-15}" fill="${secondaryColors[colorIdx]}" opacity="0.3" rx="5"/>`; | |
| svgContent += `<polygon points="${x+(cellSize-15)/2},${y+5} ${x+cellSize-20},${y+cellSize-20} ${x+5},${y+cellSize-20}" fill="${accentColors[0]}" opacity="0.5"/>`; | |
| } | |
| } | |
| } else if (patternType === 'traditional') { | |
| const centerX = width/2; | |
| const centerY = height/2; | |
| const radius = Math.min(width, height) * 0.2; | |
| svgContent += `<ellipse cx="${centerX}" cy="${centerY}" rx="${radius}" ry="${radius*1.2}" fill="none" stroke="${accentColors[0]}" stroke-width="5" opacity="0.7"/>`; | |
| svgContent += `<ellipse cx="${centerX}" cy="${centerY}" rx="${radius-20}" ry="${(radius-20)*1.2}" fill="${secondaryColors[0]}" opacity="0.25"/>`; | |
| svgContent += `<circle cx="${centerX}" cy="${centerY}" r="15" fill="${accentColors[0]}" opacity="0.8"/>`; | |
| } | |
| svgContent += `<text x="${width-30}" y="${height-20}" font-size="11" fill="${accentColors[0]}" opacity="0.4" text-anchor="end" font-family="Georgia">✦ AI Generated ✦</text>`; | |
| svgContent += `</svg>`; | |
| 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 = `<?xml version="1.0" encoding="UTF-8"?> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"> | |
| <defs> | |
| <!-- تدرج الخلفية الرئيسي --> | |
| <radialGradient id="bgGrad" cx="50%" cy="50%" r="70%"> | |
| <stop offset="0%" stop-color="${primaryColor}" /> | |
| <stop offset="60%" stop-color="${darkenColor(primaryColor, 8)}" /> | |
| <stop offset="100%" stop-color="${darkenColor(primaryColor, 18)}" /> | |
| </radialGradient> | |
| <!-- تدرج للإضاءة --> | |
| <radialGradient id="lightGrad" cx="40%" cy="35%" r="60%"> | |
| <stop offset="0%" stop-color="rgba(255,255,255,0.18)" /> | |
| <stop offset="50%" stop-color="rgba(255,255,255,0.05)" /> | |
| <stop offset="100%" stop-color="rgba(0,0,0,0.15)" /> | |
| </radialGradient> | |
| <!-- تدرج الظل المحيطي --> | |
| <radialGradient id="shadowGrad" cx="50%" cy="50%" r="50%"> | |
| <stop offset="70%" stop-color="rgba(0,0,0,0)" /> | |
| <stop offset="100%" stop-color="rgba(0,0,0,0.4)" /> | |
| </radialGradient> | |
| <!-- تأثير نسيج السجاد --> | |
| <filter id="carpetTexture" x="0%" y="0%" width="100%" height="100%"> | |
| <feTurbulence type="fractalNoise" baseFrequency="0.12" numOctaves="4" result="noise" seed="${randomSeed}"/> | |
| <feColorMatrix type="matrix" values="0.8 0 0 0 0 0 0.8 0 0 0 0 0 0.8 0 0 0 0 0 0.25 0" in="noise" result="coloredNoise"/> | |
| <feBlend mode="multiply" in="coloredNoise" in2="SourceGraphic" result="textured"/> | |
| </filter> | |
| <!-- تأثير الوبر --> | |
| <filter id="pileEffect" x="-2%" y="-2%" width="104%" height="104%"> | |
| <feTurbulence type="turbulence" baseFrequency="0.08" numOctaves="3" result="turbulence" seed="${randomSeed + 1}"/> | |
| <feDisplacementMap in="SourceGraphic" in2="turbulence" scale="${props.pileIntensity}" xChannelSelector="R" yChannelSelector="G"/> | |
| </filter> | |
| <!-- تأثير الخيوط --> | |
| <pattern id="threadPattern" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"> | |
| <rect width="6" height="6" fill="none"/> | |
| <line x1="0" y1="3" x2="6" y2="3" stroke="rgba(0,0,0,0.12)" stroke-width="0.8"/> | |
| <line x1="3" y1="0" x2="3" y2="6" stroke="rgba(0,0,0,0.08)" stroke-width="0.8"/> | |
| <line x1="0" y1="0" x2="6" y2="6" stroke="rgba(255,255,255,0.06)" stroke-width="0.5"/> | |
| </pattern> | |
| <!-- تدرجات الألوان الثانوية --> | |
| ${secondaryColors.map((c, i) => ` | |
| <linearGradient id="secGrad${i}" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" stop-color="${c}" /> | |
| <stop offset="40%" stop-color="${lightenColor(c, 10)}" /> | |
| <stop offset="100%" stop-color="${darkenColor(c, 15)}" /> | |
| </linearGradient>`).join('')} | |
| <!-- تدرجات الألوان المميزة --> | |
| ${accentColors.map((c, i) => ` | |
| <linearGradient id="accGrad${i}" x1="0%" y1="100%" x2="100%" y2="0%"> | |
| <stop offset="0%" stop-color="${darkenColor(c, 10)}" /> | |
| <stop offset="50%" stop-color="${c}" /> | |
| <stop offset="100%" stop-color="${lightenColor(c, 15)}" /> | |
| </linearGradient>`).join('')} | |
| </defs> | |
| <!-- الخلفية --> | |
| <rect width="100%" height="100%" fill="url(#bgGrad)"/> | |
| <!-- طبقة النسيج --> | |
| <rect width="100%" height="100%" fill="url(#threadPattern)"/> | |
| <rect width="100%" height="100%" filter="url(#carpetTexture)" opacity="0.6"/> | |
| <!-- الطبقة الزخرفية الرئيسية --> | |
| <g filter="url(#pileEffect)">`; | |
| // ============ 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 += `<rect x="${x}" y="${y}" width="${cellWidth}" height="${cellHeight}" fill="url(#secGrad${colorIdx})" opacity="0.35" rx="4"/>`; | |
| // إطار الخلية | |
| svgContent += `<rect x="${x + 4}" y="${y + 4}" width="${cellWidth - 8}" height="${cellHeight - 8}" fill="none" stroke="${accentColors[accentIdx]}" stroke-width="2" opacity="0.6" rx="3"/>`; | |
| // نجمة ثمانية الأطراف | |
| 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 += `<polygon points="${starPoints.join(' ')}" fill="url(#accGrad${accentIdx})" opacity="0.9"/>`; | |
| // دائرة مركزية | |
| svgContent += `<circle cx="${centerX}" cy="${centerY}" r="${starSize * 0.35}" fill="${primaryColor}" opacity="0.95"/>`; | |
| svgContent += `<circle cx="${centerX}" cy="${centerY}" r="${starSize * 0.18}" fill="${accentColors[accentIdx]}" opacity="0.9"/>`; | |
| // زخارف الزوايا | |
| if (complexity >= 5) { | |
| const cornerSize = cellWidth * 0.12; | |
| svgContent += `<path d="M ${x + 2} ${y + cornerSize + 2} L ${x + 2} ${y + 2} L ${x + cornerSize + 2} ${y + 2}" fill="url(#accGrad${accentIdx})" opacity="0.75"/>`; | |
| svgContent += `<path d="M ${x + cellWidth - 2} ${y + cornerSize + 2} L ${x + cellWidth - 2} ${y + 2} L ${x + cellWidth - cornerSize - 2} ${y + 2}" fill="url(#accGrad${accentIdx})" opacity="0.75"/>`; | |
| svgContent += `<path d="M ${x + 2} ${y + cellHeight - cornerSize - 2} L ${x + 2} ${y + cellHeight - 2} L ${x + cornerSize + 2} ${y + cellHeight - 2}" fill="url(#accGrad${accentIdx})" opacity="0.75"/>`; | |
| svgContent += `<path d="M ${x + cellWidth - 2} ${y + cellHeight - cornerSize - 2} L ${x + cellWidth - 2} ${y + cellHeight - 2} L ${x + cellWidth - cornerSize - 2} ${y + cellHeight - 2}" fill="url(#accGrad${accentIdx})" opacity="0.75"/>`; | |
| } | |
| } | |
| } | |
| // الإطار الخارجي الفاخر | |
| svgContent += `<rect x="12" y="12" width="${width - 24}" height="${height - 24}" fill="none" stroke="${accentColors[0]}" stroke-width="5" rx="10" opacity="0.85"/>`; | |
| svgContent += `<rect x="20" y="20" width="${width - 40}" height="${height - 40}" fill="none" stroke="${secondaryColors[0]}" stroke-width="2.5" rx="7" opacity="0.6"/>`; | |
| // شرائط زخرفية أفقية | |
| for (let i = 0; i < 3; i++) { | |
| const yPos = 28 + i * 14; | |
| svgContent += `<line x1="28" y1="${yPos}" x2="${width - 28}" y2="${yPos}" stroke="${accentColors[i % accentColors.length]}" stroke-width="3" opacity="0.7"/>`; | |
| svgContent += `<line x1="28" y1="${height - yPos}" x2="${width - 28}" y2="${height - yPos}" stroke="${accentColors[i % accentColors.length]}" stroke-width="3" opacity="0.7"/>`; | |
| } | |
| } | |
| // ============ 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 += `<ellipse cx="${petalX}" cy="${petalY}" rx="${petalW}" ry="${petalH}" fill="url(#secGrad${colorIdx})" opacity="0.85" transform="rotate(${p * 30}, ${petalX}, ${petalY})"/>`; | |
| } | |
| // مركز الزهرة | |
| svgContent += `<circle cx="${flowerX}" cy="${flowerY}" r="${flowerSize * 0.25}" fill="url(#accGrad${accentIdx})"/>`; | |
| svgContent += `<circle cx="${flowerX}" cy="${flowerY}" r="${flowerSize * 0.12}" fill="${primaryColor}"/>`; | |
| // أوراق | |
| svgContent += `<path d="M ${flowerX - flowerSize * 0.8} ${flowerY + flowerSize * 0.4} Q ${flowerX - flowerSize * 0.4} ${flowerY + flowerSize * 0.8} ${flowerX} ${flowerY + flowerSize * 0.5}" fill="url(#secGrad${(colorIdx + 1) % secondaryColors.length})" opacity="0.7"/>`; | |
| svgContent += `<path d="M ${flowerX + flowerSize * 0.8} ${flowerY + flowerSize * 0.4} Q ${flowerX + flowerSize * 0.4} ${flowerY + flowerSize * 0.8} ${flowerX} ${flowerY + flowerSize * 0.5}" fill="url(#secGrad${(colorIdx + 1) % secondaryColors.length})" opacity="0.7"/>`; | |
| } | |
| // زهور متوسطة | |
| 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 += `<circle cx="${petalX}" cy="${petalY}" r="${flowerSize * 0.28}" fill="url(#secGrad${colorIdx})" opacity="0.8"/>`; | |
| } | |
| svgContent += `<circle cx="${flowerX}" cy="${flowerY}" r="${flowerSize * 0.22}" fill="url(#accGrad${f % accentColors.length})" opacity="0.9"/>`; | |
| } | |
| // زهور صغيرة | |
| 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 += `<circle cx="${petalX}" cy="${petalY}" r="${flowerSize * 0.32}" fill="url(#secGrad${colorIdx})" opacity="0.7"/>`; | |
| } | |
| svgContent += `<circle cx="${flowerX}" cy="${flowerY}" r="${flowerSize * 0.18}" fill="url(#accGrad${Math.floor(random() * accentColors.length)})" opacity="0.85"/>`; | |
| } | |
| // أغصان متعرجة | |
| 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 += `<path d="M ${startX} ${startY} Q ${startX + 35} ${startY - 55} ${endX} ${endY}" fill="none" stroke="url(#secGrad${s % secondaryColors.length})" stroke-width="3.5" opacity="0.5"/>`; | |
| } | |
| } | |
| // ============ 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 += `<ellipse cx="${medallionCenterX}" cy="${medallionCenterY}" rx="${radius}" ry="${radius * 1.12}" fill="none" stroke="url(#accGrad${i % accentColors.length})" stroke-width="5" opacity="0.9"/>`; | |
| } | |
| // حشوة المدالية | |
| svgContent += `<ellipse cx="${medallionCenterX}" cy="${medallionCenterY}" rx="${medallionRadius - 22}" ry="${(medallionRadius - 22) * 1.12}" fill="url(#secGrad0)" opacity="0.4"/>`; | |
| // نجمة مركزية 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 += `<polygon points="${starPoints.join(' ')}" fill="url(#accGrad0)" opacity="0.95"/>`; | |
| // مركز ذهبي | |
| svgContent += `<circle cx="${medallionCenterX}" cy="${medallionCenterY}" r="${medallionRadius * 0.2}" fill="${accentColors[0]}" opacity="0.95"/>`; | |
| svgContent += `<circle cx="${medallionCenterX}" cy="${medallionCenterY}" r="${medallionRadius * 0.1}" fill="${primaryColor}"/>`; | |
| // زوايا فاخرة | |
| 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 += `<rect x="${corner.x}" y="${corner.y}" width="${cornerSize}" height="${cornerSize}" fill="url(#secGrad${idx % secondaryColors.length})" opacity="0.6" rx="10"/>`; | |
| svgContent += `<path d="M ${corner.x + cornerSize} ${corner.y + cornerSize} A ${cornerSize / 2.2} ${cornerSize / 2.2} 0 0 1 ${corner.x + cornerSize / 2.2} ${corner.y + cornerSize / 2.2}" fill="url(#accGrad${idx % accentColors.length})" opacity="0.9"/>`; | |
| svgContent += `<circle cx="${corner.x + cornerSize / 2}" cy="${corner.y + cornerSize / 2}" r="${cornerSize / 3.5}" fill="url(#accGrad${(idx + 1) % accentColors.length})" opacity="0.8"/>`; | |
| }); | |
| // حزام حدودي | |
| const borderStep = 38; | |
| for (let x = 45; x < width - 45; x += borderStep) { | |
| svgContent += `<rect x="${x}" y="28" width="28" height="22" fill="url(#accGrad0)" opacity="0.85" rx="4"/>`; | |
| svgContent += `<rect x="${x}" y="${height - 50}" width="28" height="22" fill="url(#accGrad0)" opacity="0.85" rx="4"/>`; | |
| } | |
| } | |
| // ============ 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 += `<circle cx="${x}" cy="${y}" r="${size / 2.2}" fill="none" stroke="url(#secGrad${colorIdx})" stroke-width="5" opacity="0.7"/>`; | |
| svgContent += `<circle cx="${x}" cy="${y}" r="${size / 3.5}" fill="url(#accGrad${accentIdx})" opacity="0.5"/>`; | |
| break; | |
| case 1: | |
| svgContent += `<path d="M ${x} ${y} C ${x + size / 2} ${y - size / 3} ${x + size / 2} ${y + size / 3} ${x + size} ${y}" fill="none" stroke="url(#accGrad${accentIdx})" stroke-width="5" opacity="0.75"/>`; | |
| break; | |
| case 2: | |
| svgContent += `<path d="M ${x} ${y + size / 2} C ${x + size / 3.5} ${y} ${x + 2.5 * size / 3.5} ${y + size} ${x + size} ${y + size / 2} Z" fill="url(#secGrad${colorIdx})" opacity="0.5"/>`; | |
| break; | |
| case 3: | |
| for (let w = 0; w < 3; w++) { | |
| svgContent += `<path d="M ${x} ${y + w * 16} Q ${x + size / 2} ${y + w * 16 - 10} ${x + size} ${y + w * 16}" fill="none" stroke="url(#accGrad${accentIdx})" stroke-width="3.5" opacity="0.6"/>`; | |
| } | |
| break; | |
| case 4: | |
| svgContent += `<polygon points="${x},${y} ${x + size / 2},${y - size / 2} ${x + size},${y} ${x + size / 2},${y + size / 2}" fill="url(#secGrad${colorIdx})" opacity="0.5" transform="rotate(${rotation}, ${x + size / 2}, ${y})"/>`; | |
| 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 += `<line x1="${startX}" y1="${startY}" x2="${endX}" y2="${endY}" stroke="url(#accGrad${Math.floor(random() * accentColors.length)})" stroke-width="2.5" opacity="0.45"/>`; | |
| } | |
| } | |
| svgContent += `</g> | |
| <!-- طبقة الإضاءة والظل --> | |
| <rect width="100%" height="100%" fill="url(#shadowGrad)"/> | |
| <rect width="100%" height="100%" fill="url(#lightGrad)"/> | |
| <!-- تأثير النسيج الإضافي --> | |
| <rect width="100%" height="100%" fill="url(#threadPattern)" opacity="0.4"/> | |
| <!-- ختم التصميم --> | |
| <text x="${width - 15}" y="${height - 15}" font-size="10" fill="${accentColors[0]}" opacity="0.55" text-anchor="end" font-family="Georgia, serif" font-style="italic"> | |
| ✦ Naseej AI Artisan ✦ | |
| </text> | |
| <!-- إطار نهائي رفيع --> | |
| <rect x="5" y="5" width="${width - 10}" height="${height - 10}" fill="none" stroke="${accentColors[0]}" stroke-width="1.5" rx="12" opacity="0.6"/> | |
| </svg>`; | |
| 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`); | |
| }); | |
| } |