322 lines
9.3 KiB
TypeScript
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();
|
|
}
|
|
}
|