flwo / apps /server /src /modules /admin /admin.routes.js
moustafa7's picture
feat: show subscription requests in admin orders page with type column
beb4b18
Raw History Blame Contribute Delete
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