| import UserActivity, { ActivityAction, ActivityCategory } from '../model/userActivity.model'; |
| import UserStats from '../model/userStats.model'; |
| import { Types } from 'mongoose'; |
|
|
| |
| |
| |
| export class ActivityService { |
|
|
| |
| |
| |
| 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 = {}; |
|
|
| |
| if (userId) { |
| query.user = userId; |
| } else if (role) { |
| |
| |
| 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 }; |
| } |
|
|
| |
| if (startDate || endDate) { |
| query.timestamp = {}; |
| if (startDate) query.timestamp.$gte = startDate; |
| if (endDate) query.timestamp.$lte = endDate; |
| } |
|
|
| |
| if (category) query.category = category; |
|
|
| |
| 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) |
| } |
| }; |
| } |
|
|
| |
| |
| |
| static async getUserStats(userId: string) { |
| const stats = await UserStats.findOne({ user: userId }); |
|
|
| if (!stats) { |
| |
| return await UserStats.create({ user: userId }); |
| } |
|
|
| return stats; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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); |
|
|
| |
| await UserStats.findOneAndUpdate( |
| { user: userId }, |
| { |
| $inc: { totalOnlineTime: sessionDuration }, |
| $set: { lastOnlineTime: lastActivity } |
| } |
| ); |
|
|
| return sessionDuration; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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 }; |
| } |
|
|
| |
| |
| |
| 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(); |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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() |
| }; |
| } |
|
|
| |
| |
| |
| |
| static async getSystemRecentActivities(options: { |
| limit?: number; |
| skip?: number; |
| } = {}) { |
| const { limit = 20, skip = 0 } = options; |
|
|
| |
| const importantActions = [ |
| ActivityAction.FORM_CREATE, |
| ActivityAction.SIGNUP, |
| ActivityAction.TRAINER_VERIFY, |
| ActivityAction.ORG_VERIFY, |
| ActivityAction.EVENT_CREATE, |
| ActivityAction.EVENT_CHECKIN, |
| ]; |
|
|
| const query = { |
| action: { $in: importantActions }, |
| success: true |
| }; |
|
|
| 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; |