425 lines
12 KiB
TypeScript
425 lines
12 KiB
TypeScript
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<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();
|
|
}
|
|
}
|