import UserActivity, { ActivityAction, ActivityCategory } from '../model/userActivity.model'; import UserStats from '../model/userStats.model'; import { Types } from 'mongoose'; /** * Service for querying user activities and generating reports */ export class ActivityService { /** * Get user's activity timeline */ static async getUserActivityTimeline( userId?: string, options: { limit?: number; skip?: number; startDate?: Date; endDate?: Date; category?: ActivityCategory; action?: ActivityAction; role?: string; } = {} ) { const { limit = 50, skip = 0, startDate, endDate, category, action, role } = options; const query: any = {}; // User filter if (userId) { query.user = userId; } else if (role) { // If role is specified, find all users with that role first // This is more efficient than a massive lookup aggregation for this specific case const User = (await import('../model/user.model')).default; const usersWithRole = await User.find({ role }).select('_id'); const userIds = usersWithRole.map(u => u._id); query.user = { $in: userIds }; } // Date range filter if (startDate || endDate) { query.timestamp = {}; if (startDate) query.timestamp.$gte = startDate; if (endDate) query.timestamp.$lte = endDate; } // Category filter if (category) query.category = category; // Action filter if (action) query.action = action; const [activities, total] = await Promise.all([ UserActivity.find(query) .sort({ timestamp: -1 }) .limit(limit) .skip(skip) .populate('user', 'username email profilePhoto role') .populate('metadata.eventId', 'title code') .populate('metadata.formId', 'title') .lean(), UserActivity.countDocuments(query) ]); return { activities, pagination: { total, limit, skip, pages: Math.ceil(total / limit) } }; } /** * Get user statistics */ static async getUserStats(userId: string) { const stats = await UserStats.findOne({ user: userId }); if (!stats) { // Create initial stats if not exists return await UserStats.create({ user: userId }); } return stats; } /** * Get activity breakdown by category */ static async getActivityBreakdown( userId: string, startDate?: Date, endDate?: Date ) { const matchStage: any = { user: new Types.ObjectId(userId) }; if (startDate || endDate) { matchStage.timestamp = {}; if (startDate) matchStage.timestamp.$gte = startDate; if (endDate) matchStage.timestamp.$lte = endDate; } const breakdown = await UserActivity.aggregate([ { $match: matchStage }, { $group: { _id: '$category', count: { $sum: 1 }, lastActivity: { $max: '$timestamp' } } }, { $sort: { count: -1 } } ]); return breakdown; } /** * Get daily activity counts for a user (for charts) */ static async getDailyActivityCounts( userId: string, days: number = 30 ) { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const dailyStats = await UserActivity.aggregate([ { $match: { user: new Types.ObjectId(userId), timestamp: { $gte: startDate } } }, { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } }, count: { $sum: 1 }, categories: { $addToSet: '$category' } } }, { $sort: { _id: 1 } } ]); return dailyStats; } /** * Get most active users (leaderboard) */ static async getActiveUsersLeaderboard( criteria: 'eventsCreated' | 'eventsJoined' | 'loginCount' | 'totalOnlineTime' = 'eventsCreated', limit: number = 10 ) { const leaderboard = await UserStats.find() .sort({ [criteria]: -1 }) .limit(limit) .populate('user', 'username email profilePhoto role organization') .lean(); return leaderboard; } /** * Calculate online time for a user */ static async calculateOnlineTime(userId: string, sessionId: string) { const activities = await UserActivity.find({ user: userId, sessionId, timestamp: { $exists: true } }) .sort({ timestamp: 1 }) .select('timestamp') .lean(); if (activities.length < 2) return 0; const firstActivity = activities[0].timestamp; const lastActivity = activities[activities.length - 1].timestamp; const sessionDuration = (lastActivity.getTime() - firstActivity.getTime()) / (1000 * 60); // minutes // Update user stats await UserStats.findOneAndUpdate( { user: userId }, { $inc: { totalOnlineTime: sessionDuration }, $set: { lastOnlineTime: lastActivity } } ); return sessionDuration; } /** * Get event participation history for a user */ static async getEventParticipationHistory(userId: string) { const eventActivities = await UserActivity.find({ user: userId, category: ActivityCategory.EVENT, action: { $in: [ActivityAction.EVENT_JOIN, ActivityAction.EVENT_CHECKIN] } }) .populate('metadata.eventId', 'title theme startDate endDate organization') .sort({ timestamp: -1 }) .lean(); return eventActivities; } /** * Get form submission history for a user */ static async getFormSubmissionHistory(userId: string) { const formActivities = await UserActivity.find({ user: userId, category: ActivityCategory.FORM, action: ActivityAction.FORM_SUBMIT }) .populate('metadata.formId', 'title event') .sort({ timestamp: -1 }) .lean(); return formActivities; } /** * Get verification activity (for organizations and admins) */ static async getVerificationActivity(userId: string) { const verifications = await UserActivity.find({ user: userId, category: ActivityCategory.VERIFICATION }) .sort({ timestamp: -1 }) .lean(); const summary = { trainersVerified: verifications.filter(v => v.action === ActivityAction.TRAINER_VERIFY).length, trainersRejected: verifications.filter(v => v.action === ActivityAction.TRAINER_REJECT).length, organizationsVerified: verifications.filter(v => v.action === ActivityAction.ORG_VERIFY).length, organizationsRejected: verifications.filter(v => v.action === ActivityAction.ORG_REJECT).length }; return { verifications, summary }; } /** * Export user activity to CSV */ static async exportUserActivityToCSV(userId: string, startDate?: Date, endDate?: Date) { const query: any = { user: userId }; if (startDate || endDate) { query.timestamp = {}; if (startDate) query.timestamp.$gte = startDate; if (endDate) query.timestamp.$lte = endDate; } const activities = await UserActivity.find(query) .sort({ timestamp: -1 }) .lean(); // Convert to CSV format const headers = ['Timestamp', 'Action', 'Category', 'Description', 'Success', 'IP Address', 'Device']; const rows = activities.map(activity => [ activity.timestamp.toISOString(), activity.action, activity.category, activity.description, activity.success ? 'Yes' : 'No', activity.metadata?.ipAddress || 'N/A', activity.metadata?.device || 'N/A' ]); const csvContent = [ headers.join(','), ...rows.map(row => row.map(cell => `"${cell}"`).join(',')) ].join('\n'); return csvContent; } /** * Get security alerts (failed logins, unauthorized access) */ static async getSecurityAlerts(userId: string, days: number = 7) { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); const alerts = await UserActivity.find({ user: userId, success: false, timestamp: { $gte: startDate }, action: { $in: [ ActivityAction.LOGIN, ActivityAction.UNAUTHORIZED_ACCESS, ActivityAction.API_ERROR ] } }) .sort({ timestamp: -1 }) .lean(); return alerts; } /** * Generate comprehensive user report */ static async generateUserReport(userId: string) { const [stats, breakdown, recentActivities, securityAlerts] = await Promise.all([ this.getUserStats(userId), this.getActivityBreakdown(userId), this.getUserActivityTimeline(userId, { limit: 20 }), this.getSecurityAlerts(userId) ]); return { stats, breakdown, recentActivities: recentActivities.activities, securityAlerts, generatedAt: new Date() }; } /** * Get system-wide recent activities (admin only) * Filters for important activities like form creation, org registration, trainer approval, etc. */ static async getSystemRecentActivities(options: { limit?: number; skip?: number; } = {}) { const { limit = 20, skip = 0 } = options; // Important actions to track const importantActions = [ ActivityAction.FORM_CREATE, ActivityAction.SIGNUP, // New organization/trainer registration ActivityAction.TRAINER_VERIFY, ActivityAction.ORG_VERIFY, ActivityAction.EVENT_CREATE, ActivityAction.EVENT_CHECKIN, // Training started ]; const query = { action: { $in: importantActions }, success: true // Only successful activities }; const [activities, total] = await Promise.all([ UserActivity.find(query) .sort({ timestamp: -1 }) .limit(limit) .skip(skip) .populate('user', 'username email profilePhoto role organization') .populate('metadata.eventId', 'title code') .populate('metadata.formId', 'title') .lean(), UserActivity.countDocuments(query) ]); return { activities, pagination: { total, limit, skip, pages: Math.ceil(total / limit) } }; } } export default ActivityService;