import { Injectable, HttpException, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { RedisService } from '../redis/redis.service'; @Injectable() export class AdminService { constructor( private prisma: PrismaService, private redisService: RedisService, ) {} async getDashboardStats() { // total revenue const orders = await this.prisma.order.findMany({ where: { status: { not: 'failed' } }, select: { totalAmount: true }, }); const revenue = orders.reduce((sum, o) => sum + Number(o.totalAmount), 0); // new orders count (processing status) const newOrders = await this.prisma.order.count({ where: { status: 'processing' }, }); // active users count const users = await this.prisma.user.count(); // Today's request count from Redis counter (set by MetricsController) const todayKey = `visits:${new Date().toISOString().split('T')[0]}`; let todayVisits = 0; try { const count = await this.redisService.get(todayKey); todayVisits = count ? parseInt(count, 10) : 0; } catch { todayVisits = 0; } return { revenue, newOrders, users, todayVisits, }; } async getUsers(query: any) { const page = Number(query.page) || 1; const limit = Number(query.limit) || 10; const skip = (page - 1) * limit; const where: any = {}; if (query.search) { where.OR = [ { firstName: { contains: query.search, mode: 'insensitive' } }, { lastName: { contains: query.search, mode: 'insensitive' } }, { email: { contains: query.search, mode: 'insensitive' } }, { mobile: { contains: query.search } }, ]; } if (query.role) { if (query.role === 'B2B') { where.role = { contains: 'B2B' }; } else { where.role = query.role; } } const [data, total] = await Promise.all([ this.prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, }), this.prisma.user.count({ where }), ]); return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) }, }; } async updateUserRole(id: string, role: string) { return this.prisma.user.update({ where: { id }, data: { role }, }); } async getProducts(query: any) { try { const page = Number(query.page) || 1; const limit = Number(query.limit) || 10; const skip = (page - 1) * limit; const where: any = {}; if (query.search) { where.OR = [ { name: { contains: query.search, mode: 'insensitive' } }, { artNo: { contains: query.search } }, ]; } if (query.categoryId) { where.categoryId = query.categoryId; } const [data, total] = await Promise.all([ this.prisma.product.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, include: { category: true, symptoms: true }, }), this.prisma.product.count({ where }), ]); return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) }, }; } catch (error) { console.error('[AdminService] getProducts error:', error); throw new HttpException(error.message || 'Error fetching products', 500); } } async createProduct(data: any) { const product = await this.prisma.product.create({ data: { artNo: data.artNo, nameFa: data.nameFa, nameEn: data.nameEn, scientificTagline: data.scientificTagline, description: data.description, shortDescription: data.shortDescription, categoryId: data.categoryId, categorySlug: data.categorySlug || 'general', priceValue: data.priceValue, priceDisplay: data.priceDisplay, unit: data.unit, packageSize: data.packageSize, dosageLogic: data.dosageLogic, suitableFor: data.suitableFor, imageUrl: data.imageUrl, metaTitle: data.metaTitle, metaDescription: data.metaDescription, keywords: data.keywords, canonicalUrl: data.canonicalUrl, slug: data.slug || data.artNo, }, }); if (data.symptoms && Array.isArray(data.symptoms)) { await this.prisma.productSymptom.createMany({ data: data.symptoms .map((s: string) => ({ productId: product.id, symptom: s.trim(), })) .filter((s: any) => s.symptom.length > 0), }); } return product; } async updateProduct(id: string, data: any) { const existing = await this.prisma.product.findUnique({ where: { id } }); if (!existing) { throw new NotFoundException(`محصولی با شناسه ${id} یافت نشد.`); } const product = await this.prisma.product.update({ where: { id }, data: { artNo: data.artNo, nameFa: data.nameFa, nameEn: data.nameEn, scientificTagline: data.scientificTagline, description: data.description, shortDescription: data.shortDescription, categoryId: data.categoryId, categorySlug: data.categorySlug, priceValue: data.priceValue, priceDisplay: data.priceDisplay, unit: data.unit, packageSize: data.packageSize, dosageLogic: data.dosageLogic, suitableFor: data.suitableFor, imageUrl: data.imageUrl, metaTitle: data.metaTitle, metaDescription: data.metaDescription, keywords: data.keywords, canonicalUrl: data.canonicalUrl, slug: data.slug || data.artNo, }, }); if (data.symptoms !== undefined && Array.isArray(data.symptoms)) { await this.prisma.productSymptom.deleteMany({ where: { productId: id } }); if (data.symptoms.length > 0) { await this.prisma.productSymptom.createMany({ data: data.symptoms .map((s: string) => ({ productId: id, symptom: s.trim(), })) .filter((s: any) => s.symptom.length > 0), }); } } return product; } async deleteProduct(id: string) { return this.prisma.product.delete({ where: { id }, }); } async getOrders(query: any) { const page = Number(query.page) || 1; const limit = Number(query.limit) || 10; const skip = (page - 1) * limit; const where: any = {}; if (query.search) { where.OR = [ { id: { contains: query.search } }, { trackingNumber: { contains: query.search, mode: 'insensitive' } }, { user: { firstName: { contains: query.search, mode: 'insensitive' } }, }, { user: { lastName: { contains: query.search, mode: 'insensitive' } } }, { user: { phone: { contains: query.search } } }, ]; } if (query.status) { where.status = query.status; } const [data, total] = await Promise.all([ this.prisma.order.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, include: { user: true, orderItems: { include: { product: true, }, }, coupon: true, }, }), this.prisma.order.count({ where }), ]); return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) }, }; } async updateOrderStatus(id: string, status: string, trackingNumber?: string) { const dataToUpdate: any = { status }; if (trackingNumber !== undefined) { dataToUpdate.trackingNumber = trackingNumber; } return this.prisma.order.update({ where: { id }, data: dataToUpdate, include: { user: true, orderItems: { include: { product: true, }, }, }, }); } // --- Coupons Engine --- async getCoupons(query: any) { const { page = 1, limit = 10, search = '' } = query; const skip = (Number(page) - 1) * Number(limit); const where = search ? { code: { contains: search, mode: 'insensitive' as any } } : {}; const [data, total] = await Promise.all([ this.prisma.coupon.findMany({ where, skip, take: Number(limit), orderBy: { createdAt: 'desc' }, include: { targets: true }, // Include polymorphic targets }), this.prisma.coupon.count({ where }), ]); return { data, meta: { total, page: Number(page), limit: Number(limit), lastPage: Math.ceil(total / Number(limit)), }, }; } async createCoupon(data: any) { return this.prisma.coupon.create({ data: { code: data.code, type: data.type || 'percent', value: data.value, minCartValue: data.minCartValue || null, maxCartValue: data.maxCartValue || null, maxUses: data.maxUses || null, expiresAt: data.expiresAt ? new Date(data.expiresAt) : null, isActive: data.isActive !== undefined ? data.isActive : true, targets: data.targets && data.targets.length > 0 ? { create: data.targets.map((t: any) => ({ targetType: t.targetType, targetId: t.targetId, modifierType: t.modifierType || 'override', modifierValue: t.modifierValue || null, })), } : undefined, }, include: { targets: true }, }); } async updateCoupon(id: string, data: any) { // Delete old targets and recreate them await this.prisma.couponTarget.deleteMany({ where: { couponId: id } }); return this.prisma.coupon.update({ where: { id }, data: { code: data.code, type: data.type, value: data.value, minCartValue: data.minCartValue, maxCartValue: data.maxCartValue, maxUses: data.maxUses, expiresAt: data.expiresAt ? new Date(data.expiresAt) : null, isActive: data.isActive, targets: data.targets && data.targets.length > 0 ? { create: data.targets.map((t: any) => ({ targetType: t.targetType, targetId: t.targetId, modifierType: t.modifierType || 'override', modifierValue: t.modifierValue || null, })), } : undefined, }, include: { targets: true }, }); } async toggleCoupon(id: string, isActive: boolean) { return this.prisma.coupon.update({ where: { id }, data: { isActive }, }); } async deleteCoupon(id: string) { return this.prisma.coupon.delete({ where: { id }, }); } // --- Settings --- async getSettings() { const keys = [ 'SHIPPING_FEE', 'MIN_ORDER_AMOUNT', 'B2B_DISCOUNT_PERCENT', 'MAINTENANCE_MODE', ]; const settings = await this.prisma.uiText.findMany({ where: { key: { in: keys } }, }); // Transform to an object { SHIPPING_FEE: '50000', ... } return settings.reduce( (acc, curr) => ({ ...acc, [curr.key]: curr.value }), {}, ); } async updateSettings(data: Record) { // Upsert all keys const operations = Object.entries(data).map(([key, value]) => { return this.prisma.uiText.upsert({ where: { key }, update: { value: String(value) }, create: { key, value: String(value) }, }); }); await this.prisma.$transaction(operations); return this.getSettings(); } }