canina/backend/src/admin/admin.service.ts
parsa aghaei 2de442dbae
Some checks failed
Deploy Canina / deploy (push) Failing after 30m46s
fix(lint): clean up type definitions and missing imports
2026-07-29 16:44:53 +03:30

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();
}
}