Spaces:
Runtime error
Runtime error
Download apps/server/src/modules/admin/admin.routes.js from moustafa7/flwo: direct link, hf CLI and curl.
- Browser
- Download file 29 kB
-
https://huggingface.co/spaces/moustafa7/flwo/resolve/main/apps/server/src/modules/admin/admin.routes.js
- Command line
-
hf download hf://spaces/moustafa7/flwo/apps/server/src/modules/admin/admin.routes.js
-
curl -L -o admin.routes.js https://huggingface.co/spaces/moustafa7/flwo/resolve/main/apps/server/src/modules/admin/admin.routes.js
29 kB
| import { success, created, paginated } from '../../lib/response.js' | |
| import { logger } from '../../lib/logger.js' | |
| import { Router } from 'express' | |
| import { z } from 'zod' | |
| import bcrypt from 'bcryptjs' | |
| import { authenticate, requireRole } from '../../middleware/auth.js' | |
| import { validate } from '../../middleware/validate.js' | |
| import { uploadTemplateImage, uploadMedia, uploadManifest, uploadSource, uploadTemplateZip, validateFileContent, toPublicUrl } from '../../middleware/upload.js' | |
| import { prisma } from '../../config/database.js' | |
| import { AppError } from '../../lib/AppError.js' | |
| import { signAccessToken, signRefreshToken } from '../../lib/jwt.js' | |
| import { hashToken } from '../auth/auth.service.js' | |
| import * as templatesCtrl from '../templates/templates.controller.js' | |
| import { createTemplateSchema, updateTemplateSchema } from '../templates/templates.schema.js' | |
| import * as serverPlansCtrl from '../serverPlans/serverPlans.controller.js' | |
| import { createPlanSchema, updatePlanSchema, updateSubscriptionSchema } from '../serverPlans/serverPlans.schema.js' | |
| import { loginSchema } from '../auth/auth.schema.js' | |
| import { adminLoginLimiter, uploadLimiter } from '../../middleware/rateLimiter.js' | |
| import { getPagination } from '../../lib/pagination.js' | |
| import * as contactCtrl from '../contact/contact.controller.js' | |
| import * as landingCtrl from '../landing/landing.controller.js' | |
| import * as notificationsCtrl from '../notifications/notifications.controller.js' | |
| import { SAFE_USER_SELECT, ROLE_HIERARCHY } from '../../lib/constants.js' | |
| import { getLandingSchema } from '../landing/landing.schema.js' | |
| import { userStatusSchema, userRoleSchema, orderStatusSchema, banUserSchema, setVipSchema, createAccountSchema } from './admin.schema.js' | |
| const router = Router() | |
| router.post('/login', adminLoginLimiter, validate(loginSchema), async (req, res, next) => { | |
| try { | |
| const { email, password } = req.validatedData | |
| const user = await prisma.user.findUnique({ where: { email: email.toLowerCase() } }) | |
| if (!user || user.role === 'CLIENT') { | |
| throw new AppError('بيانات الدخول غير صحيحة', 401, 'INVALID_CREDENTIALS') | |
| } | |
| const valid = await bcrypt.compare(password, user.passwordHash) | |
| if (!valid || !user.isActive) { | |
| throw new AppError('بيانات الدخول غير صحيحة', 401, 'INVALID_CREDENTIALS') | |
| } | |
| const accessToken = signAccessToken(user.id, user.role) | |
| const refreshToken = signRefreshToken(user.id) | |
| const hashedRefresh = hashToken(refreshToken) | |
| await prisma.refreshToken.create({ | |
| data: { token: hashedRefresh, userId: user.id, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) } | |
| }) | |
| const { passwordHash, ...safeUser } = user | |
| res.cookie('admin_refresh_token', refreshToken, { | |
| httpOnly: true, secure: process.env.NODE_ENV === 'production', | |
| sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax', maxAge: 7 * 24 * 60 * 60 * 1000, path: '/api/auth/refresh' | |
| }) | |
| success(res, { user: safeUser, accessToken }) | |
| } catch (err) { next(err) } | |
| }) | |
| router.use(authenticate) | |
| const requireSupport = requireRole('ADMIN', 'SUPPORT', 'ACCOUNTS', 'SUPER_ADMIN') | |
| const requireAdmin = requireRole('ADMIN', 'SUPER_ADMIN') | |
| router.use(requireSupport) | |
| router.get('/users', async (req, res, next) => { | |
| try { | |
| const { page, limit } = getPagination(req.query) | |
| const where = { role: 'CLIENT' } | |
| const [users, total] = await Promise.all([ | |
| prisma.user.findMany({ | |
| where, | |
| select: { ...SAFE_USER_SELECT, isVIP: true, bannedUntil: true }, | |
| orderBy: { createdAt: 'desc' }, | |
| take: limit, | |
| skip: (page - 1) * limit, | |
| }), | |
| prisma.user.count({ where }) | |
| ]) | |
| const usersWithStats = await Promise.all(users.map(async (u) => { | |
| const [completed, cancelled, totalOrders] = await Promise.all([ | |
| prisma.order.count({ where: { userId: u.id, status: 'COMPLETED' } }), | |
| prisma.order.count({ where: { userId: u.id, status: 'CANCELLED' } }), | |
| prisma.order.count({ where: { userId: u.id } }), | |
| ]) | |
| return { ...u, completedOrders: completed, cancelledOrders: cancelled, totalOrders } | |
| })) | |
| paginated(res, { users: usersWithStats, total, page, totalPages: Math.ceil(total / limit) }, total, page, limit) | |
| } catch (err) { next(err) } | |
| }) | |
| router.patch('/users/:id/status', validate(userStatusSchema), async (req, res, next) => { | |
| try { | |
| const { isActive } = req.validatedData | |
| const target = await prisma.user.findUnique({ | |
| where: { id: req.params.id }, | |
| select: { role: true } | |
| }) | |
| if (!target) throw new AppError('المستخدم غير موجود', 404, 'USER_NOT_FOUND') | |
| const allowed = ROLE_HIERARCHY[req.user.role] || [] | |
| if (!allowed.includes(target.role)) { | |
| throw new AppError('ليس لديك صلاحية تعديل هذا الحساب', 403, 'INSUFFICIENT_PERMISSIONS') | |
| } | |
| const user = await prisma.user.update({ | |
| where: { id: req.params.id }, | |
| data: { isActive }, | |
| select: SAFE_USER_SELECT | |
| }) | |
| success(res, { user }, isActive ? 'تم تفعيل الحساب' : 'تم إيقاف الحساب') | |
| } catch (err) { next(err) } | |
| }) | |
| router.patch('/users/:id/role', validate(userRoleSchema), async (req, res, next) => { | |
| try { | |
| const { role } = req.validatedData | |
| if (req.user.role !== 'ADMIN') { | |
| throw new AppError('فقط الأدمن يمكنه تغيير الصلاحيات', 403, 'FORBIDDEN') | |
| } | |
| const target = await prisma.user.findUnique({ | |
| where: { id: req.params.id }, | |
| select: { role: true } | |
| }) | |
| if (!target) throw new AppError('المستخدم غير موجود', 404, 'USER_NOT_FOUND') | |
| const allowed = ROLE_HIERARCHY[req.user.role] || [] | |
| if (!allowed.includes(target.role)) { | |
| throw new AppError('ليس لديك صلاحية تعديل هذا الحساب', 403, 'INSUFFICIENT_PERMISSIONS') | |
| } | |
| const user = await prisma.user.update({ | |
| where: { id: req.params.id }, | |
| data: { role }, | |
| select: SAFE_USER_SELECT | |
| }) | |
| success(res, { user }) | |
| } catch (err) { next(err) } | |
| }) | |
| router.patch('/users/:id/ban', validate(banUserSchema), async (req, res, next) => { | |
| try { | |
| const { bannedUntil } = req.validatedData | |
| const target = await prisma.user.findUnique({ where: { id: req.params.id }, select: { role: true } }) | |
| if (!target) throw new AppError('المستخدم غير موجود', 404, 'USER_NOT_FOUND') | |
| const allowed = ROLE_HIERARCHY[req.user.role] || [] | |
| if (!allowed.includes(target.role)) throw new AppError('ليس لديك صلاحية', 403, 'FORBIDDEN') | |
| const user = await prisma.user.update({ | |
| where: { id: req.params.id }, | |
| data: { bannedUntil: bannedUntil ? new Date(bannedUntil) : null }, | |
| select: { ...SAFE_USER_SELECT, isVIP: true, bannedUntil: true } | |
| }) | |
| success(res, { user }, bannedUntil ? 'تم حظر المستخدم' : 'تم إلغاء الحظر') | |
| } catch (err) { next(err) } | |
| }) | |
| router.patch('/users/:id/vip', validate(setVipSchema), async (req, res, next) => { | |
| try { | |
| const { isVIP } = req.validatedData | |
| const target = await prisma.user.findUnique({ where: { id: req.params.id }, select: { role: true } }) | |
| if (!target) throw new AppError('المستخدم غير موجود', 404, 'USER_NOT_FOUND') | |
| const allowed = ROLE_HIERARCHY[req.user.role] || [] | |
| if (!allowed.includes(target.role)) throw new AppError('ليس لديك صلاحية', 403, 'FORBIDDEN') | |
| const user = await prisma.user.update({ | |
| where: { id: req.params.id }, | |
| data: { isVIP }, | |
| select: { ...SAFE_USER_SELECT, isVIP: true, bannedUntil: true } | |
| }) | |
| success(res, { user }, isVIP ? 'تم تعيين المستخدم كمميز' : 'تم إلغاء التميز') | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/accounts', async (req, res, next) => { | |
| try { | |
| const { page, limit } = getPagination(req.query) | |
| const staffRoles = ['SUPPORT', 'ACCOUNTS', 'ADMIN', 'SUPER_ADMIN'] | |
| const [users, total] = await Promise.all([ | |
| prisma.user.findMany({ | |
| where: { role: { in: staffRoles } }, | |
| select: { ...SAFE_USER_SELECT, isVIP: true, bannedUntil: true }, | |
| orderBy: { createdAt: 'desc' }, | |
| take: limit, | |
| skip: (page - 1) * limit, | |
| }), | |
| prisma.user.count({ where: { role: { in: staffRoles } } }), | |
| ]) | |
| paginated(res, { accounts: users, total, page, totalPages: Math.ceil(total / limit) }, total, page, limit) | |
| } catch (err) { next(err) } | |
| }) | |
| router.post('/accounts', requireAdmin, validate(createAccountSchema), async (req, res, next) => { | |
| try { | |
| const { firstName, lastName, email, phone, password, role } = req.validatedData | |
| const allowed = ROLE_HIERARCHY[req.user.role] || [] | |
| if (!allowed.includes(role)) throw new AppError('ليس لديك صلاحية إنشاء هذا الدور', 403, 'FORBIDDEN') | |
| const existing = await prisma.user.findFirst({ | |
| where: { OR: [{ email: email.toLowerCase() }, { phone }] } | |
| }) | |
| if (existing) throw new AppError('البريد الإلكتروني أو رقم الهاتف موجود بالفعل', 409, 'DUPLICATE') | |
| const passwordHash = await bcrypt.hash(password, 12) | |
| const user = await prisma.user.create({ | |
| data: { firstName, lastName, email: email.toLowerCase(), phone, passwordHash, role, isActive: true }, | |
| select: SAFE_USER_SELECT, | |
| }) | |
| created(res, { user }, 'تم إنشاء الحساب بنجاح') | |
| } catch (err) { next(err) } | |
| }) | |
| router.patch('/accounts/:id/status', requireAdmin, async (req, res, next) => { | |
| try { | |
| const { isActive } = req.body | |
| const target = await prisma.user.findUnique({ where: { id: req.params.id }, select: { role: true } }) | |
| if (!target) throw new AppError('الحساب غير موجود', 404, 'NOT_FOUND') | |
| if (target.role === 'CLIENT') throw new AppError('هذا المستخدم ليس حساب موظف', 400, 'NOT_STAFF') | |
| const allowed = ROLE_HIERARCHY[req.user.role] || [] | |
| if (!allowed.includes(target.role)) throw new AppError('ليس لديك صلاحية', 403, 'FORBIDDEN') | |
| const user = await prisma.user.update({ | |
| where: { id: req.params.id }, | |
| data: { isActive }, | |
| select: SAFE_USER_SELECT, | |
| }) | |
| success(res, { user }, isActive ? 'تم تفعيل الحساب' : 'تم إيقاف الحساب') | |
| } catch (err) { next(err) } | |
| }) | |
| router.patch('/accounts/:id/role', requireAdmin, async (req, res, next) => { | |
| try { | |
| const { role } = req.body | |
| const validRoles = ['SUPPORT', 'ACCOUNTS', 'ADMIN'] | |
| if (!validRoles.includes(role)) throw new AppError('دور غير صالح', 400, 'INVALID_ROLE') | |
| const target = await prisma.user.findUnique({ where: { id: req.params.id }, select: { role: true } }) | |
| if (!target) throw new AppError('الحساب غير موجود', 404, 'NOT_FOUND') | |
| if (target.role === 'CLIENT') throw new AppError('هذا المستخدم ليس حساب موظف', 400, 'NOT_STAFF') | |
| if (target.role === 'SUPER_ADMIN') throw new AppError('لا يمكن تعديل دور المشرف العام', 403, 'FORBIDDEN') | |
| const allowed = ROLE_HIERARCHY[req.user.role] || [] | |
| if (!allowed.includes(target.role) || !allowed.includes(role)) throw new AppError('ليس لديك صلاحية', 403, 'FORBIDDEN') | |
| const user = await prisma.user.update({ | |
| where: { id: req.params.id }, | |
| data: { role }, | |
| select: SAFE_USER_SELECT, | |
| }) | |
| success(res, { user }, 'تم تغيير الدور بنجاح') | |
| } catch (err) { next(err) } | |
| }) | |
| router.delete('/accounts/:id', requireAdmin, async (req, res, next) => { | |
| try { | |
| const target = await prisma.user.findUnique({ where: { id: req.params.id }, select: { role: true } }) | |
| if (!target) throw new AppError('الحساب غير موجود', 404, 'NOT_FOUND') | |
| if (target.role === 'CLIENT') throw new AppError('هذا المستخدم ليس حساب موظف', 400, 'NOT_STAFF') | |
| if (target.role === 'SUPER_ADMIN') throw new AppError('لا يمكن حذف المشرف العام', 403, 'FORBIDDEN') | |
| const allowed = ROLE_HIERARCHY[req.user.role] || [] | |
| if (!allowed.includes(target.role)) throw new AppError('ليس لديك صلاحية', 403, 'FORBIDDEN') | |
| try { | |
| await prisma.message.deleteMany({ where: { senderId: req.params.id } }) | |
| await prisma.refreshToken.deleteMany({ where: { userId: req.params.id } }) | |
| await prisma.notification.deleteMany({ where: { userId: req.params.id } }) | |
| await prisma.user.delete({ where: { id: req.params.id } }) | |
| } catch (e) { | |
| await prisma.user.update({ where: { id: req.params.id }, data: { isActive: false, email: `deleted-${Date.now()}@deleted.com` } }) | |
| } | |
| success(res, null, 'تم حذف الحساب') | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/orders', async (req, res, next) => { | |
| try { | |
| const { page, limit } = getPagination(req.query) | |
| const status = req.query.status | |
| const typeFilter = req.query.type // 'ORDER' | 'SUBSCRIPTION' | undefined | |
| const orderWhere = status && (!typeFilter || typeFilter === 'ORDER') ? { status } : {} | |
| const subWhere = status && (!typeFilter || typeFilter === 'SUBSCRIPTION') ? { status } : {} | |
| const [orders, subRows, totalOrders, totalSubs] = await Promise.all([ | |
| typeFilter !== 'SUBSCRIPTION' ? prisma.order.findMany({ | |
| where: orderWhere, | |
| include: { | |
| user: { select: SAFE_USER_SELECT }, | |
| template: { select: { id: true, title: true } } | |
| }, | |
| orderBy: { createdAt: 'desc' }, | |
| take: limit, | |
| skip: (page - 1) * limit, | |
| }) : [], | |
| typeFilter !== 'ORDER' ? prisma.serverSubscription.findMany({ | |
| where: subWhere, | |
| include: { | |
| user: { select: SAFE_USER_SELECT }, | |
| plan: { select: { id: true, name: true } } | |
| }, | |
| orderBy: { createdAt: 'desc' }, | |
| take: limit, | |
| skip: (page - 1) * limit, | |
| }) : [], | |
| typeFilter !== 'SUBSCRIPTION' ? prisma.order.count({ where: orderWhere }) : 0, | |
| typeFilter !== 'ORDER' ? prisma.serverSubscription.count({ where: subWhere }) : 0, | |
| ]) | |
| const mapped = [ | |
| ...orders.map(o => ({ | |
| id: o.id, | |
| type: 'ORDER', | |
| orderNumber: o.orderNumber, | |
| user: o.user, | |
| item: o.template, | |
| itemLabel: 'القالب', | |
| totalAmount: Number(o.totalAmount), | |
| status: o.status, | |
| createdAt: o.createdAt, | |
| notes: o.notes, | |
| })), | |
| ...subRows.map(s => ({ | |
| id: s.id, | |
| type: 'SUBSCRIPTION', | |
| orderNumber: `SUB-${s.id.slice(0, 8).toUpperCase()}`, | |
| user: s.user, | |
| item: s.plan, | |
| itemLabel: 'الباقة', | |
| totalAmount: Number(s.price), | |
| status: s.status, | |
| createdAt: s.createdAt, | |
| notes: s.adminNotes, | |
| })), | |
| ].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) | |
| const total = totalOrders + totalSubs | |
| paginated(res, { orders: mapped, total, page, totalPages: Math.ceil(total / limit) }, total, page, limit) | |
| } catch (err) { next(err) } | |
| }) | |
| router.patch('/orders/:id/status', validate(orderStatusSchema), async (req, res, next) => { | |
| try { | |
| const { status } = req.validatedData | |
| const order = await prisma.order.update({ | |
| where: { id: req.params.id }, | |
| data: { status }, | |
| select: { id: true, status: true, orderNumber: true, updatedAt: true } | |
| }) | |
| success(res, order) | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/stats', async (req, res, next) => { | |
| try { | |
| const [usersCount, ordersCount, templatesCount, ordersByStatus] = await Promise.all([ | |
| prisma.user.count(), | |
| prisma.order.count(), | |
| prisma.template.count(), | |
| prisma.order.groupBy({ | |
| by: ['status'], | |
| _count: true, | |
| }) | |
| ]) | |
| success(res, { | |
| users: usersCount, | |
| orders: ordersCount, | |
| templates: templatesCount, | |
| ordersByStatus: ordersByStatus.reduce((acc, cur) => { | |
| acc[cur.status] = cur._count | |
| return acc | |
| }, {}) | |
| }) | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/orders/:id', async (req, res, next) => { | |
| try { | |
| const order = await prisma.order.findUnique({ | |
| where: { id: req.params.id }, | |
| include: { | |
| user: { select: SAFE_USER_SELECT }, | |
| template: { select: { id: true, title: true, category: true, price: true, previewUrl: true } } | |
| } | |
| }) | |
| if (!order) throw new AppError('الطلب غير موجود', 404, 'ORDER_NOT_FOUND') | |
| success(res, order) | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/templates', templatesCtrl.adminList) | |
| router.get('/templates/:id', templatesCtrl.adminGetById) | |
| router.post('/templates', uploadLimiter, uploadTemplateImage, validateFileContent, validate(createTemplateSchema), templatesCtrl.create) | |
| router.put('/templates/:id', uploadLimiter, uploadTemplateImage, validateFileContent, validate(updateTemplateSchema), templatesCtrl.update) | |
| router.delete('/templates/:id', templatesCtrl.remove) | |
| router.patch('/templates/:id/publish', templatesCtrl.publish) | |
| router.post('/templates/:id/manifest', uploadLimiter, uploadManifest, templatesCtrl.uploadManifestFile) | |
| router.post('/templates/:id/source', uploadLimiter, uploadSource, templatesCtrl.uploadSourceCode) | |
| router.post('/templates/:id/preview-zip', uploadLimiter, uploadTemplateZip, templatesCtrl.uploadPreviewZip) | |
| router.get('/landing', landingCtrl.getContent) | |
| router.put('/landing', validate(z.object({}).passthrough()), async (req, res, next) => { | |
| try { | |
| const data = req.validatedData | |
| const ops = [] | |
| for (const [section, content] of Object.entries(data)) { | |
| if (section === 'site') continue | |
| try { | |
| const schema = getLandingSchema(section) | |
| if (!schema) continue | |
| const parsed = schema.safeParse(content) | |
| if (!parsed.success) continue | |
| ops.push(prisma.landingContent.upsert({ | |
| where: { section }, | |
| create: { section, content: parsed.data }, | |
| update: { content: parsed.data }, | |
| })) | |
| } catch (e) { | |
| logger.warn('Skipping invalid section "%s": %s', section, e.message) | |
| } | |
| } | |
| if (ops.length) await prisma.$transaction(ops) | |
| success(res, null, 'تم حفظ التغييرات بنجاح') | |
| } catch (err) { next(err) } | |
| }) | |
| router.post('/upload/media', uploadLimiter, uploadMedia, validateFileContent, (req, res, next) => { | |
| try { | |
| if (!req.file) throw new AppError('الملف مطلوب', 400, 'FILE_REQUIRED') | |
| success(res, { url: toPublicUrl(req.file.path), name: req.file.filename }) | |
| } catch (err) { next(err) } | |
| }) | |
| router.put('/site', async (req, res, next) => { | |
| try { | |
| const { logoUrl } = req.body | |
| await prisma.landingContent.upsert({ | |
| where: { section: 'site' }, | |
| create: { section: 'site', content: { logoUrl: logoUrl || '' } }, | |
| update: { content: { logoUrl: logoUrl || '' } }, | |
| }) | |
| success(res, null, 'تم حفظ الشعار') | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/orders/:id/conversation', async (req, res, next) => { | |
| try { | |
| let conversation = await prisma.conversation.findUnique({ | |
| where: { orderId: req.params.id }, | |
| include: { | |
| order: { select: { id: true, orderNumber: true, userId: true } } | |
| } | |
| }) | |
| if (!conversation) { | |
| conversation = await prisma.conversation.create({ | |
| data: { orderId: req.params.id }, | |
| include: { | |
| order: { select: { id: true, orderNumber: true, userId: true } } | |
| } | |
| }) | |
| } | |
| success(res, conversation) | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/conversations', async (req, res, next) => { | |
| try { | |
| const { page, limit } = getPagination(req.query) | |
| const [conversations, total] = await Promise.all([ | |
| prisma.conversation.findMany({ | |
| take: limit, | |
| skip: (page - 1) * limit, | |
| include: { | |
| order: { | |
| select: { | |
| id: true, orderNumber: true, userId: true, | |
| template: { select: { title: true } }, | |
| user: { select: SAFE_USER_SELECT } | |
| } | |
| }, | |
| client: { select: SAFE_USER_SELECT }, | |
| messages: { orderBy: { createdAt: 'desc' }, take: 1 } | |
| }, | |
| orderBy: { updatedAt: 'desc' } | |
| }), | |
| prisma.conversation.count() | |
| ]) | |
| paginated(res, { conversations, total, page, totalPages: Math.ceil(total / limit) }, total, page, limit) | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/notifications', notificationsCtrl.list) | |
| router.get('/notifications/count', notificationsCtrl.unreadCount) | |
| router.patch('/notifications/:id/read', notificationsCtrl.markRead) | |
| router.patch('/notifications/read-all', notificationsCtrl.markAllRead) | |
| router.get('/contact', contactCtrl.adminList) | |
| router.get('/contact/:id', contactCtrl.adminGetById) | |
| router.post('/contact/:id/reply', validate(z.object({ content: z.string().min(1, 'محتوى الرد مطلوب') })), contactCtrl.adminReply) | |
| // Accounting | |
| router.get('/accounts/summary', async (req, res, next) => { | |
| try { | |
| const now = new Date() | |
| const firstDay = new Date(now.getFullYear(), now.getMonth(), 1) | |
| const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1) | |
| const [paidOrders, pendingOrders, totalExpenses, monthExpenses, lastMonthOrders, paidSubs, monthSubs, lastMonthSubs] = await Promise.all([ | |
| prisma.order.aggregate({ _sum: { totalAmount: true }, where: { status: 'COMPLETED' } }), | |
| prisma.order.aggregate({ _sum: { totalAmount: true }, where: { status: { in: ['PENDING', 'ACCEPTED', 'IN_PROGRESS'] } } }), | |
| prisma.expense.aggregate({ _sum: { amount: true } }), | |
| prisma.expense.aggregate({ _sum: { amount: true }, where: { date: { gte: firstDay } } }), | |
| prisma.order.aggregate({ _sum: { totalAmount: true }, where: { status: 'COMPLETED', createdAt: { gte: lastMonth, lt: firstDay } } }), | |
| prisma.serverSubscription.aggregate({ _sum: { price: true }, where: { status: 'ACTIVE' } }), | |
| prisma.serverSubscription.aggregate({ _sum: { price: true }, where: { status: 'ACTIVE', startDate: { gte: firstDay } } }), | |
| prisma.serverSubscription.aggregate({ _sum: { price: true }, where: { status: 'ACTIVE', startDate: { gte: lastMonth, lt: firstDay } } }), | |
| ]) | |
| const orderRevenue = Number(paidOrders._sum.totalAmount || 0) | |
| const subRevenue = Number(paidSubs._sum.price || 0) | |
| const totalRevenue = orderRevenue + subRevenue | |
| const pendingRevenue = Number(pendingOrders._sum.totalAmount || 0) | |
| const totalExpense = Number(totalExpenses._sum.amount || 0) | |
| success(res, { | |
| totalRevenue, | |
| totalExpenses: totalExpense, | |
| netProfit: totalRevenue - totalExpense, | |
| pendingRevenue, | |
| monthExpenses: Number(monthExpenses._sum.amount || 0), | |
| lastMonthRevenue: Number(lastMonthOrders._sum.totalAmount || 0) + Number(lastMonthSubs._sum.price || 0), | |
| monthSubscriptionRevenue: Number(monthSubs._sum.price || 0), | |
| orderCounts: { | |
| completed: await prisma.order.count({ where: { status: 'COMPLETED' } }), | |
| pending: await prisma.order.count({ where: { status: 'PENDING' } }), | |
| inProgress: await prisma.order.count({ where: { status: 'IN_PROGRESS' } }), | |
| cancelled: await prisma.order.count({ where: { status: 'CANCELLED' } }), | |
| }, | |
| pendingSubscriptions: await prisma.serverSubscription.count({ where: { status: 'PENDING' } }), | |
| activeSubscriptions: await prisma.serverSubscription.count({ where: { status: 'ACTIVE' } }), | |
| }) | |
| } catch (err) { next(err) } | |
| }) | |
| router.get('/accounts/expenses', async (req, res, next) => { | |
| try { | |
| const { page, limit } = getPagination(req.query) | |
| const category = req.query.category | |
| const from = req.query.from ? new Date(req.query.from) : null | |
| const to = req.query.to ? new Date(req.query.to) : null | |
| const where = {} | |
| if (category) where.category = category | |
| if (from || to) { | |
| where.date = {} | |
| if (from) where.date.gte = from | |
| if (to) where.date.lte = to | |
| } | |
| const [expenses, total] = await Promise.all([ | |
| prisma.expense.findMany({ | |
| where, | |
| orderBy: { date: 'desc' }, | |
| take: limit, | |
| skip: (page - 1) * limit, | |
| }), | |
| prisma.expense.count({ where }), | |
| ]) | |
| const totalAmount = await prisma.expense.aggregate({ _sum: { amount: true }, where }) | |
| success(res, { | |
| expenses: expenses.map(e => ({ ...e, amount: Number(e.amount) })), | |
| total, | |
| page, | |
| totalPages: Math.ceil(total / limit), | |
| totalAmount: Number(totalAmount._sum.amount || 0), | |
| }) | |
| } catch (err) { next(err) } | |
| }) | |
| router.post('/accounts/expenses', validate(z.object({ | |
| description: z.string().min(1).max(300), | |
| amount: z.number().positive(), | |
| category: z.string().min(1).max(50), | |
| date: z.string().optional(), | |
| notes: z.string().max(500).optional(), | |
| })), async (req, res, next) => { | |
| try { | |
| const d = req.validatedData | |
| const expense = await prisma.expense.create({ | |
| data: { | |
| description: d.description, | |
| amount: d.amount, | |
| category: d.category, | |
| date: d.date ? new Date(d.date) : new Date(), | |
| notes: d.notes || null, | |
| createdBy: req.user.firstName + ' ' + req.user.lastName, | |
| }, | |
| }) | |
| success(res, { ...expense, amount: Number(expense.amount) }) | |
| } catch (err) { next(err) } | |
| }) | |
| router.put('/accounts/expenses/:id', validate(z.object({ | |
| description: z.string().min(1).max(300).optional(), | |
| amount: z.number().positive().optional(), | |
| category: z.string().min(1).max(50).optional(), | |
| date: z.string().optional(), | |
| notes: z.string().max(500).optional().nullable(), | |
| })), async (req, res, next) => { | |
| try { | |
| const d = req.validatedData | |
| const updateData = {} | |
| if (d.description !== undefined) updateData.description = d.description | |
| if (d.amount !== undefined) updateData.amount = d.amount | |
| if (d.category !== undefined) updateData.category = d.category | |
| if (d.date !== undefined) updateData.date = new Date(d.date) | |
| if (d.notes !== undefined) updateData.notes = d.notes | |
| const expense = await prisma.expense.update({ | |
| where: { id: req.params.id }, | |
| data: updateData, | |
| }) | |
| success(res, { ...expense, amount: Number(expense.amount) }) | |
| } catch (err) { next(err) } | |
| }) | |
| router.delete('/accounts/expenses/:id', async (req, res, next) => { | |
| try { | |
| await prisma.expense.delete({ where: { id: req.params.id } }) | |
| success(res, null, 'تم حذف المصروف') | |
| } catch (err) { next(err) } | |
| }) | |
| // Revenue breakdown by month | |
| router.get('/accounts/revenue-history', async (req, res, next) => { | |
| try { | |
| const orderMonths = await prisma.$queryRaw` | |
| SELECT | |
| TO_CHAR(DATE_TRUNC('month', "createdAt"), 'YYYY-MM') as month, | |
| SUM("totalAmount")::float as revenue, | |
| COUNT(*)::int as orders | |
| FROM orders | |
| WHERE status = 'COMPLETED' | |
| GROUP BY DATE_TRUNC('month', "createdAt") | |
| ORDER BY month DESC | |
| LIMIT 12 | |
| ` | |
| const subMonths = await prisma.$queryRaw` | |
| SELECT | |
| TO_CHAR(DATE_TRUNC('month', "startDate"), 'YYYY-MM') as month, | |
| SUM("price")::float as revenue, | |
| COUNT(*)::int as subscriptions | |
| FROM server_subscriptions | |
| WHERE status = 'ACTIVE' AND "startDate" IS NOT NULL | |
| GROUP BY DATE_TRUNC('month', "startDate") | |
| ORDER BY month DESC | |
| LIMIT 12 | |
| ` | |
| const merged = {} | |
| for (const r of orderMonths) { merged[r.month] = { month: r.month, revenue: r.revenue, orders: r.orders, subscriptions: 0 } } | |
| for (const r of subMonths) { | |
| if (merged[r.month]) { merged[r.month].revenue = Number(merged[r.month].revenue) + Number(r.revenue); merged[r.month].subscriptions = r.subscriptions } | |
| else merged[r.month] = { month: r.month, revenue: r.revenue, orders: 0, subscriptions: r.subscriptions } | |
| } | |
| success(res, Object.values(merged).sort((a, b) => b.month.localeCompare(a.month))) | |
| } catch (err) { next(err) } | |
| }) | |
| router.patch('/contact/:id/read', contactCtrl.adminToggleRead) | |
| router.patch('/contact/:id/status', contactCtrl.adminToggleStatus) | |
| router.delete('/contact/:id', contactCtrl.adminDelete) | |
| router.get('/server-plans', requireAdmin, serverPlansCtrl.getAllPlans) | |
| router.get('/server-plans/:id', requireAdmin, serverPlansCtrl.getPlanById) | |
| router.post('/server-plans', requireAdmin, validate(createPlanSchema), serverPlansCtrl.createPlan) | |
| router.put('/server-plans/:id', requireAdmin, validate(updatePlanSchema), serverPlansCtrl.updatePlan) | |
| router.delete('/server-plans/:id', requireAdmin, serverPlansCtrl.deletePlan) | |
| router.get('/server-subscriptions', requireAdmin, serverPlansCtrl.getAllSubscriptions) | |
| router.get('/server-subscriptions/:id', requireAdmin, serverPlansCtrl.getSubscriptionById) | |
| router.patch('/server-subscriptions/:id', requireAdmin, validate(updateSubscriptionSchema), serverPlansCtrl.updateSubscription) | |
| router.post('/server-subscriptions/check-expiry', requireAdmin, serverPlansCtrl.checkExpiringSubscriptions) | |
| export default router | |