Files
v3-api/src/api/admin/admin.service.ts

334 lines
12 KiB
TypeScript

import { HttpException, HttpStatus, Injectable, Optional, Inject } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilterQuery, Model, Types } from 'mongoose';
import { BaseResponseDTO } from 'src/common/dto/base-response.dto';
import { Role } from 'src/common/types/role.type';
import { AdminDocument, AdminModel } from 'src/database/model/admin.model';
import { SessionModel } from 'src/database/model/sessions.model';
import { UserModel } from 'src/database/model/user.model';
import { ReassignedLogsModel } from 'src/database/model/reassigned-logs.model';
import { ChatService } from 'src/socket/support-management/chat.service';
import { RedisService } from 'src/common/helpers/redis.service';
import { CreateAdminDto } from './dto/create-admin.dto';
import { EditProfileDto } from './dto/edit-profile.dto';
import { createExpertDto } from './dto/create-new-expert.dto';
import * as crypto from 'node:crypto';
@Injectable()
export class AdminService {
constructor(
@InjectModel(AdminModel.name)
private readonly adminModel: Model<AdminModel>,
@InjectModel(SessionModel.name)
private readonly sessionModel: Model<SessionModel>,
@InjectModel(UserModel.name)
private readonly userModel: Model<UserModel>,
@InjectModel(ReassignedLogsModel.name)
private readonly reassignedLogsModel: Model<ReassignedLogsModel>,
@Optional() @Inject(ChatService)
private readonly chatService: ChatService | null,
private readonly redisService: RedisService,
) {}
async create(createAdminDto: CreateAdminDto, role: Role.Admin | Role.Expert | Role.Supervisor) {
return await this.adminModel.create({
...createAdminDto,
username: createAdminDto.email,
role: role,
});
}
async findOneAdmin(query: FilterQuery<AdminModel>): Promise<AdminDocument> {
return await this.adminModel.findOne(query);
}
async getProfile(adminIdentity): Promise<any> {
try {
const admin = await this.adminModel
.findOne(
{
_id: new Types.ObjectId(adminIdentity.userData._id),
isActive: true,
},
{ password: 0, resetToken: 0 },
)
.populate('avatar');
if (!admin) throw new HttpException('not_found', HttpStatus.NOT_FOUND);
// Use expert's email to find sessions (expert field stores email)
const expertEmail = admin.email;
// Calculate total answered: sessions where expert actually connected (connectedToExpert === true)
// and the expert field matches this expert's email
const answeredSessions = await this.sessionModel.find({
expert: expertEmail,
connectedToExpert: true,
onlineStartDate: { $exists: true },
});
// Calculate activity time (sum of all answered session durations in seconds)
const activityTime = Math.round(
answeredSessions.reduce((total, session) => {
if (session.onlineStartDate && session.onlineEndDate) {
const duration =
(session.onlineEndDate.getTime() -
session.onlineStartDate.getTime()) /
1000;
// Only add positive durations to avoid negative activity time
return total + (duration > 0 ? duration : 0);
}
return total;
}, 0),
);
// Calculate total answered (sessions where expert connected)
const totalAnswered = answeredSessions.length;
// Calculate total not answered: count from reassigned_logs where this expert missed the chat
// (reassigned from them, meaning they were online but didn't answer)
const totalNotAnswered = await this.reassignedLogsModel.countDocuments({
from: expertEmail,
});
// Calculate total sessions (answered + not answered)
const totalSessions = totalAnswered + totalNotAnswered;
// Calculate total rate (average of all non-null rates from answered sessions)
const rates = answeredSessions
.filter(
(session) =>
session.expertRate !== null && session.expertRate !== undefined,
)
.map((session) => session.expertRate);
const totalRate =
rates.length > 0
? parseFloat((rates.reduce((sum, rate) => sum + rate, 0) / rates.length).toFixed(2))
: 0;
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
_id: admin._id,
name: admin.name,
family: admin.family,
mobile: admin.mobile,
email: admin.email,
username: admin.username,
role: admin.role,
isActive: admin.isActive,
avatar: admin.avatar
? `https://${process.env.BASEURL}/${(admin.avatar as any).filePath}`
: null,
birthDate: admin.birthDate,
reports: {
activityTime,
totalSessions,
totalAnswered,
totalNotAnswered,
totalRate,
currentNotActive: 0, // Will be implemented later
},
});
} catch (err) {
console.log(err);
throw new HttpException(err.message, HttpStatus.BAD_REQUEST);
}
}
async createNewExpert(body:createExpertDto,adminIdentity:any){
try {
const existingExpert = await this.adminModel.findOne<AdminModel | null>({ // Explicitly type existingExpert as AdminModel | null
$or: [{ email: body.email }, { mobile: body.mobile }],
});
if (existingExpert) {
throw new HttpException('Expert with this email or mobile already exists', HttpStatus.BAD_REQUEST);
}
const hashedPassword = crypto.createHash('sha256').update(body.password).digest('hex');
const newExpert = await this.adminModel.create({
email: body.email,
password: hashedPassword,
mobile: body.mobile,
name: body.name,
family: body.family,
role: 'expert',
username: body.email, // Set username to email
});
// Exclude password from the returned object
const { password, ...expertWithoutPassword } = newExpert.toObject();
return new BaseResponseDTO(HttpStatus.CREATED, 'Expert created successfully', expertWithoutPassword);
} catch (err) {
console.log(err);
throw new HttpException(err.message, HttpStatus.BAD_REQUEST);
}
}
async modifyProfile(body: EditProfileDto, adminIdentity: any) {
const admin = await this.adminModel.findOne({
_id: new Types.ObjectId(adminIdentity.userData._id),
isActive: true,
});
if (!admin) {
throw new HttpException('not_found', HttpStatus.NOT_FOUND);
}
const updatePayload: any = { ...body };
const updatedUser = await this.adminModel
.findByIdAndUpdate(adminIdentity.userData._id, updatePayload, {
new: true,
projection: {
_id: 1,
role: 1,
mobile: 1,
email: 1,
username: 1,
name: 1,
family: 1,
createdAt: 1,
updatedAt: 1,
birthDate: 1,
birthday: 1,
avatar: 1,
},
})
.populate('avatar');
return updatedUser;
}
async toggleExpertActivation(expertId: string): Promise<BaseResponseDTO> {
try {
const expert = await this.adminModel.findOne({
_id: new Types.ObjectId(expertId),
role: Role.Expert,
});
if (!expert) {
throw new HttpException('Expert not found', HttpStatus.NOT_FOUND);
}
expert.isActive = !expert.isActive;
await expert.save();
const message = expert.isActive ? 'Expert activated successfully' : 'Expert deactivated successfully';
const { password, ...expertWithoutPassword } = expert.toObject();
return new BaseResponseDTO(HttpStatus.OK, message, expertWithoutPassword);
} catch (err) {
console.log(err);
throw new HttpException(err.message, HttpStatus.BAD_REQUEST);
}
}
async getDashboard(): Promise<BaseResponseDTO> {
try {
const redisClient = this.redisService.getClient();
const ONLINE_EXPERTS_KEY = 'onlineExperts';
const ROOMS_HASH_KEY = 'rooms:active';
const WAITING_QUEUE_KEY = 'waitingQueue';
const WAITING_USERS_KEY = 'waiting_users';
// Get all experts from Redis onlineExperts hash
let allExpertsFromRedis: any[] = [];
try {
const onlineExpertsEntries = await redisClient.hgetall(ONLINE_EXPERTS_KEY);
allExpertsFromRedis = Object.values(onlineExpertsEntries)
.map((v) => {
try {
return JSON.parse(v as string);
} catch {
return null;
}
})
.filter((expert: any) => expert);
} catch (err) {
console.error('Error getting experts from Redis:', err);
}
// 1. تعداد کارشناس در حال مکالمه - Number of experts currently in conversation
// Experts who have activeSessions > 0
const expertsInConversation = allExpertsFromRedis.filter(
(expert: any) => expert.activeSessions > 0
);
const expertsInConversationCount = expertsInConversation.length;
// 2. تعداد کارشناس آنلاین - Number of online experts
// Experts who are marked as online OR have active sessions (if in conversation, they should be online)
const onlineExpertIds = new Set<string>();
allExpertsFromRedis.forEach((expert: any) => {
if (expert.isOnline === true || expert.activeSessions > 0) {
onlineExpertIds.add(expert.expertId);
}
});
const onlineExpertsCount = onlineExpertIds.size;
// 3. تعداد کاربر در حال مکاتبه با کارشناس - Number of users currently chatting with expert
// Each room in rooms:active represents one active chat session
// Since each user can only have one active expert session, counting active rooms = counting unique users
let usersWithExpert = 0;
try {
if (this.chatService) {
const activeRooms = await this.chatService.getActiveRooms();
usersWithExpert = activeRooms ? activeRooms.length : 0;
} else {
// Fallback: get directly from Redis if chatService is not available
const roomsEntries = await redisClient.hgetall(ROOMS_HASH_KEY);
const activeRooms = Object.values(roomsEntries)
.map((v) => {
try {
return JSON.parse(v as string);
} catch {
return null;
}
})
.filter((room: any) => room && room.isActive);
usersWithExpert = activeRooms.length;
}
} catch (err) {
console.error('Error getting users with expert from Redis:', err);
}
// 4. تعداد کاربر در انتظار ارتباط با کارشناس - Number of users waiting to connect with expert
// Get from Redis waitingQueue list and waiting_users hash
let waitingUsersCount = 0;
try {
const waitingQueueLength = await redisClient.llen(WAITING_QUEUE_KEY);
const waitingUsersKeys = await redisClient.hkeys(WAITING_USERS_KEY);
waitingUsersCount = waitingQueueLength + waitingUsersKeys.length;
} catch (err) {
console.error('Error getting waiting users from Redis:', err);
}
// 5. تعداد کاربر چت بات - Number of chatbot users (total unique users with sessions)
const totalChatbotUsers = await this.sessionModel.distinct('userId').then(ids => ids.length);
// 6. تعداد کاربر بیمه گذار چت بات - Number of insurance holder chatbot users
// Users with nationalCode who have sessions
const usersWithSessions = await this.sessionModel.distinct('userId');
const insuranceHolderUsers = await this.userModel.countDocuments({
_id: { $in: usersWithSessions },
nationalCode: { $exists: true, $nin: [null, ''] },
});
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
expertsInConversation: expertsInConversationCount,
onlineExperts: onlineExpertsCount,
usersWithExpert: usersWithExpert,
waitingUsers: waitingUsersCount,
totalChatbotUsers: totalChatbotUsers,
insuranceHolderUsers: insuranceHolderUsers,
});
} catch (err) {
console.error('Error in getDashboard:', err);
throw new HttpException(
err.message || 'Failed to fetch dashboard data',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}