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, @InjectModel(SessionModel.name) private readonly sessionModel: Model, @InjectModel(UserModel.name) private readonly userModel: Model, @InjectModel(ReassignedLogsModel.name) private readonly reassignedLogsModel: Model, @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): Promise { return await this.adminModel.findOne(query); } async getProfile(adminIdentity): Promise { 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({ // 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 { 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 { 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(); 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, ); } } }