canina/backend/src/admin/admin.service.ts

322 lines
9.3 KiB
TypeScript

import { Injectable, HttpException } 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 }
}),
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) {
return 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,
}
});
}
async updateProduct(id: string, data: any) {
return 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,
}
});
}
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.id = { 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: true }
}),
this.prisma.order.count({ where })
]);
return { data, meta: { total, page, limit, lastPage: Math.ceil(total / limit) } };
}
async updateOrderStatus(id: string, status: string) {
return this.prisma.order.update({
where: { id },
data: { status }
});
}
// --- 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<string, string>) {
// 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();
}
}