naseej-backend / server.js
Mark-Lasfar
✨ Major update: Integrate FAL.AI Seedance 2.0 for realistic carpet generation
bb8cecf
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)}
; 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)}
; 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`);
});
}