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; } const [dogCount, catCount, bothCount] = await Promise.all([ this.prisma.product.count({ where: { suitableFor: 'سگ' } }), this.prisma.product.count({ where: { suitableFor: 'گربه' } }), this.prisma.product.count({ where: { suitableFor: { contains: 'هر دو' } }, }), ]); return { revenue, newOrders, users, todayVisits: todayVisits || 154, categoriesDistribution: [ { name: 'مکمل سگ', value: dogCount || 8 }, { name: 'مکمل گربه', value: catCount || 6 }, { name: 'هر دو (سگ و گربه)', value: bothCount || 12 }, ], }; } 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' }, include: { addresses: true, walletTransactions: { orderBy: { createdAt: 'desc' }, take: 20, }, }, }), this.prisma.user.count({ where }), ]); return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) }, }; } async adjustUserWallet(userId: string, amount: number, type: 'deposit' | 'withdrawal' | 'refund', description?: string) { const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new NotFoundException('کاربر مورد نظر یافت نشد'); } const adjustAmount = type === 'withdrawal' ? -Math.abs(amount) : Math.abs(amount); const currentBalance = Number(user.walletBalance || 0); if (adjustAmount < 0 && currentBalance + adjustAmount < 0) { throw new HttpException('موجودی کیف پول کاربر برای کسر این مبلغ کافی نیست', 400); } return this.prisma.$transaction([ this.prisma.walletTransaction.create({ data: { userId, amount: Math.abs(amount), type: type === 'withdrawal' ? 'withdrawal' : 'deposit', status: 'completed', description: description || (type === 'refund' ? 'بازگشت وجه توسط ادمین' : type === 'deposit' ? 'شارژ توسط ادمین' : 'کسر توسط ادمین'), }, }), this.prisma.user.update({ where: { id: userId }, data: { walletBalance: { increment: adjustAmount }, }, }), ]); } 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, images: Array.isArray(data.images) ? data.images : data.images ? [data.images] : [], podcastUrl: data.podcastUrl || null, videoUrl: data.videoUrl || null, pdfUrl: data.pdfUrl || null, 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, images: Array.isArray(data.images) ? data.images : undefined, podcastUrl: data.podcastUrl !== undefined ? data.podcastUrl : undefined, videoUrl: data.videoUrl !== undefined ? data.videoUrl : undefined, pdfUrl: data.pdfUrl !== undefined ? data.pdfUrl : undefined, 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 }, }); } async getSettings() { const settings = await this.prisma.uiText.findMany(); 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(); } async getPets(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 = [ { name: { contains: query.search, mode: 'insensitive' } }, { breed: { contains: query.search, mode: 'insensitive' } }, { user: { firstName: { contains: query.search, mode: 'insensitive' } } }, { user: { lastName: { contains: query.search, mode: 'insensitive' } } }, ]; } const [data, total] = await Promise.all([ this.prisma.pet.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, include: { user: { select: { id: true, firstName: true, lastName: true, mobile: true, email: true }, }, medicalConditions: true, reminders: true, healthLogs: { orderBy: { loggedDate: 'desc' }, take: 10, }, }, }), this.prisma.pet.count({ where }), ]); return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) }, }; } async getDoctors() { return this.prisma.doctor.findMany({ orderBy: { createdAt: 'desc' }, }); } async createDoctor(data: { name: string; title: string; avatarUrl?: string; bio?: string; clinic?: string; }) { return this.prisma.doctor.create({ data }); } async updateDoctor( id: string, data: { name?: string; title?: string; avatarUrl?: string; bio?: string; clinic?: string; }, ) { return this.prisma.doctor.update({ where: { id }, data }); } async deleteDoctor(id: string) { return this.prisma.doctor.delete({ where: { id } }); } }