Files
tasks-backend/controllers/admin.controller.js
2025-12-26 23:56:32 +02:00

1076 lines
30 KiB
JavaScript

const { User, QuizSession, Category, Question, GuestSettings, sequelize } = require('../models');
const { Op } = require('sequelize');
/**
* Get system-wide statistics (admin only)
* @route GET /api/admin/statistics
* @access Private (admin only)
*/
exports.getSystemStatistics = async (req, res) => {
try {
// Calculate date 7 days ago for active users
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
// Calculate date 30 days ago for user growth
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// 1. Total Users
const totalUsers = await User.count({
where: { role: { [Op.ne]: 'admin' } } // Exclude admins from user count
});
// 2. Active Users (last 7 days)
const activeUsers = await User.count({
where: {
role: { [Op.ne]: 'admin' },
lastQuizDate: {
[Op.gte]: sevenDaysAgo
}
}
});
// 3. Total Quiz Sessions
const totalQuizSessions = await QuizSession.count({
where: {
status: { [Op.in]: ['completed', 'timeout'] }
}
});
// 4. Average Score (from completed quizzes)
const averageScoreResult = await QuizSession.findAll({
attributes: [
[sequelize.fn('AVG', sequelize.col('score')), 'avgScore'],
[sequelize.fn('AVG', sequelize.col('total_points')), 'avgTotal']
],
where: {
status: { [Op.in]: ['completed', 'timeout'] }
},
raw: true
});
const avgScore = parseFloat(averageScoreResult[0].avgScore) || 0;
const avgTotal = parseFloat(averageScoreResult[0].avgTotal) || 1;
const averageScorePercentage = avgTotal > 0 ? Math.round((avgScore / avgTotal) * 100) : 0;
// 5. Popular Categories (top 5 by quiz count)
const popularCategories = await sequelize.query(`
SELECT
c.id,
c.name,
c.slug,
c.icon,
c.color,
COUNT(qs.id) as quizCount,
ROUND(AVG(qs.score / qs.total_points * 100), 2) as avgScore
FROM categories c
LEFT JOIN quiz_sessions qs ON qs.category_id = c.id AND qs.status IN ('completed', 'timeout')
WHERE c.is_active = true
GROUP BY c.id, c.name, c.slug, c.icon, c.color
ORDER BY quizCount DESC
LIMIT 5
`, {
type: sequelize.QueryTypes.SELECT
});
// Format popular categories
const formattedPopularCategories = popularCategories.map(cat => ({
id: cat.id,
name: cat.name,
slug: cat.slug,
icon: cat.icon,
color: cat.color,
quizCount: parseInt(cat.quizCount),
averageScore: parseFloat(cat.avgScore) || 0
}));
// 6. User Growth (last 30 days)
const userGrowth = await sequelize.query(`
SELECT
DATE(created_at) as date,
COUNT(*) as newUsers
FROM users
WHERE created_at >= :thirtyDaysAgo
AND role != 'admin'
GROUP BY DATE(created_at)
ORDER BY date ASC
`, {
replacements: { thirtyDaysAgo },
type: sequelize.QueryTypes.SELECT
});
// Format user growth data
const formattedUserGrowth = userGrowth.map(item => ({
date: item.date instanceof Date ? item.date.toISOString().split('T')[0] : item.date,
newUsers: parseInt(item.newUsers)
}));
// 7. Quiz Activity (last 30 days)
const quizActivity = await sequelize.query(`
SELECT
DATE(completed_at) as date,
COUNT(*) as quizzesCompleted
FROM quiz_sessions
WHERE completed_at >= :thirtyDaysAgo
AND status IN ('completed', 'timeout')
GROUP BY DATE(completed_at)
ORDER BY date ASC
`, {
replacements: { thirtyDaysAgo },
type: sequelize.QueryTypes.SELECT
});
// Format quiz activity data
const formattedQuizActivity = quizActivity.map(item => ({
date: item.date instanceof Date ? item.date.toISOString().split('T')[0] : item.date,
quizzesCompleted: parseInt(item.quizzesCompleted)
}));
// 8. Total Questions
const totalQuestions = await Question.count({
where: { isActive: true }
});
// 9. Total Categories
const totalCategories = await Category.count({
where: { isActive: true }
});
// 10. Questions by Difficulty
const questionsByDifficulty = await Question.findAll({
attributes: [
'difficulty',
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
],
where: { isActive: true },
group: ['difficulty'],
raw: true
});
const difficultyBreakdown = {
easy: 0,
medium: 0,
hard: 0
};
questionsByDifficulty.forEach(item => {
difficultyBreakdown[item.difficulty] = parseInt(item.count);
});
// 11. Pass Rate
const completedQuizzes = await QuizSession.count({
where: {
status: { [Op.in]: ['completed', 'timeout'] }
}
});
const passedQuizzes = await QuizSession.count({
where: {
status: { [Op.in]: ['completed', 'timeout'] },
score: {
[Op.gte]: sequelize.literal('total_points * 0.7') // 70% pass threshold
}
}
});
const passRate = completedQuizzes > 0
? Math.round((passedQuizzes / completedQuizzes) * 100)
: 0;
// Build response
const statistics = {
users: {
total: totalUsers,
active: activeUsers,
inactiveLast7Days: totalUsers - activeUsers
},
quizzes: {
totalSessions: totalQuizSessions,
averageScore: Math.round(avgScore),
averageScorePercentage,
passRate,
passedQuizzes,
failedQuizzes: completedQuizzes - passedQuizzes
},
content: {
totalCategories,
totalQuestions,
questionsByDifficulty: difficultyBreakdown
},
popularCategories: formattedPopularCategories,
userGrowth: formattedUserGrowth,
quizActivity: formattedQuizActivity
};
res.status(200).json({
success: true,
data: statistics,
message: 'System statistics retrieved successfully'
});
} catch (error) {
console.error('Get system statistics error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Get guest settings
* @route GET /api/admin/guest-settings
* @access Private (admin only)
*/
exports.getGuestSettings = async (req, res) => {
try {
// Try to get existing settings
let settings = await GuestSettings.findOne();
// If no settings exist, return defaults
if (!settings) {
settings = {
maxQuizzes: 3,
expiryHours: 24,
publicCategories: [],
featureRestrictions: {
allowBookmarks: false,
allowReview: true,
allowPracticeMode: true,
allowTimedMode: false,
allowExamMode: false
}
};
}
res.status(200).json({
success: true,
data: {
maxQuizzes: settings.maxQuizzes,
expiryHours: settings.expiryHours,
publicCategories: settings.publicCategories,
featureRestrictions: settings.featureRestrictions
},
message: 'Guest settings retrieved successfully'
});
} catch (error) {
console.error('Get guest settings error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Update guest settings
* @route PUT /api/admin/guest-settings
* @access Private (admin only)
*/
exports.updateGuestSettings = async (req, res) => {
try {
const { maxQuizzes, expiryHours, publicCategories, featureRestrictions } = req.body;
// Validate maxQuizzes
if (maxQuizzes !== undefined) {
if (typeof maxQuizzes !== 'number' || !Number.isInteger(maxQuizzes)) {
return res.status(400).json({
success: false,
message: 'Max quizzes must be an integer'
});
}
if (maxQuizzes < 1 || maxQuizzes > 50) {
return res.status(400).json({
success: false,
message: 'Max quizzes must be between 1 and 50'
});
}
}
// Validate expiryHours
if (expiryHours !== undefined) {
if (typeof expiryHours !== 'number' || !Number.isInteger(expiryHours)) {
return res.status(400).json({
success: false,
message: 'Expiry hours must be an integer'
});
}
if (expiryHours < 1 || expiryHours > 168) {
return res.status(400).json({
success: false,
message: 'Expiry hours must be between 1 and 168 (7 days)'
});
}
}
// Validate publicCategories
if (publicCategories !== undefined) {
if (!Array.isArray(publicCategories)) {
return res.status(400).json({
success: false,
message: 'Public categories must be an array'
});
}
// Validate each category UUID
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
for (const categoryId of publicCategories) {
if (!uuidRegex.test(categoryId)) {
return res.status(400).json({
success: false,
message: `Invalid category UUID format: ${categoryId}`
});
}
// Verify category exists
const category = await Category.findByPk(categoryId);
if (!category) {
return res.status(404).json({
success: false,
message: `Category not found: ${categoryId}`
});
}
}
}
// Validate featureRestrictions
if (featureRestrictions !== undefined) {
if (typeof featureRestrictions !== 'object' || Array.isArray(featureRestrictions)) {
return res.status(400).json({
success: false,
message: 'Feature restrictions must be an object'
});
}
// Validate boolean fields
const validFields = ['allowBookmarks', 'allowReview', 'allowPracticeMode', 'allowTimedMode', 'allowExamMode'];
for (const key in featureRestrictions) {
if (!validFields.includes(key)) {
return res.status(400).json({
success: false,
message: `Invalid feature restriction field: ${key}`
});
}
if (typeof featureRestrictions[key] !== 'boolean') {
return res.status(400).json({
success: false,
message: `Feature restriction "${key}" must be a boolean`
});
}
}
}
// Check if settings exist
let settings = await GuestSettings.findOne();
if (settings) {
// Update existing settings
if (maxQuizzes !== undefined) settings.maxQuizzes = maxQuizzes;
if (expiryHours !== undefined) settings.expiryHours = expiryHours;
if (publicCategories !== undefined) settings.publicCategories = publicCategories;
if (featureRestrictions !== undefined) {
settings.featureRestrictions = {
...settings.featureRestrictions,
...featureRestrictions
};
}
await settings.save();
} else {
// Create new settings
settings = await GuestSettings.create({
maxQuizzes: maxQuizzes !== undefined ? maxQuizzes : 3,
expiryHours: expiryHours !== undefined ? expiryHours : 24,
publicCategories: publicCategories || [],
featureRestrictions: featureRestrictions || {
allowBookmarks: false,
allowReview: true,
allowPracticeMode: true,
allowTimedMode: false,
allowExamMode: false
}
});
}
res.status(200).json({
success: true,
data: {
maxQuizzes: settings.maxQuizzes,
expiryHours: settings.expiryHours,
publicCategories: settings.publicCategories,
featureRestrictions: settings.featureRestrictions
},
message: 'Guest settings updated successfully'
});
} catch (error) {
console.error('Update guest settings error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Get all users with pagination and filters (admin only)
* @route GET /api/admin/users
* @access Private (admin only)
*/
exports.getAllUsers = async (req, res) => {
try {
const { page = 1, limit = 10, role, isActive, sortBy = 'createdAt', sortOrder = 'desc' } = req.query;
// Validate pagination
const pageNum = Math.max(parseInt(page) || 1, 1);
const limitNum = Math.min(Math.max(parseInt(limit) || 10, 1), 100);
const offset = (pageNum - 1) * limitNum;
// Build where clause
const where = {};
if (role) {
if (!['user', 'admin'].includes(role)) {
return res.status(400).json({
success: false,
message: 'Invalid role. Must be "user" or "admin"'
});
}
where.role = role;
}
if (isActive !== undefined) {
if (isActive !== 'true' && isActive !== 'false') {
return res.status(400).json({
success: false,
message: 'Invalid isActive value. Must be "true" or "false"'
});
}
where.isActive = isActive === 'true';
}
// Validate sort
const validSortFields = ['createdAt', 'username', 'email', 'lastLogin'];
const sortField = validSortFields.includes(sortBy) ? sortBy : 'createdAt';
const order = sortOrder.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
// Get total count
const total = await User.count({ where });
// Get users
const users = await User.findAll({
where,
attributes: {
exclude: ['password']
},
order: [[sortField, order]],
limit: limitNum,
offset
});
// Format response
const formattedUsers = users.map(user => ({
id: user.id,
username: user.username,
email: user.email,
role: user.role,
isActive: user.isActive,
profileImage: user.profileImage,
totalQuizzes: user.totalQuizzes,
quizzesPassed: user.quizzesPassed,
totalQuestionsAnswered: user.totalQuestionsAnswered,
correctAnswers: user.correctAnswers,
currentStreak: user.currentStreak,
longestStreak: user.longestStreak,
lastLogin: user.lastLogin,
lastQuizDate: user.lastQuizDate,
createdAt: user.createdAt
}));
// Pagination metadata
const totalPages = Math.ceil(total / limitNum);
res.status(200).json({
success: true,
data: {
users: formattedUsers,
pagination: {
currentPage: pageNum,
totalPages,
totalItems: total,
itemsPerPage: limitNum,
hasNextPage: pageNum < totalPages,
hasPreviousPage: pageNum > 1
},
filters: {
role: role || null,
isActive: isActive !== undefined ? (isActive === 'true') : null
},
sorting: {
sortBy: sortField,
sortOrder: order
}
},
message: 'Users retrieved successfully'
});
} catch (error) {
console.error('Get all users error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Get user by ID (admin only)
* @route GET /api/admin/users/:userId
* @access Private (admin only)
*/
exports.getUserById = async (req, res) => {
try {
const { userId } = req.params;
// Validate UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(userId)) {
return res.status(400).json({
success: false,
message: 'Invalid user ID format'
});
}
// Get user
const user = await User.findByPk(userId, {
attributes: {
exclude: ['password']
}
});
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Get recent quiz sessions (last 10)
const recentSessions = await QuizSession.findAll({
where: { userId },
include: [{
model: Category,
as: 'category',
attributes: ['id', 'name', 'slug', 'icon', 'color']
}],
order: [['completedAt', 'DESC']],
limit: 10
});
// Format user data
const userData = {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
isActive: user.isActive,
profileImage: user.profileImage,
stats: {
totalQuizzes: user.totalQuizzes,
quizzesPassed: user.quizzesPassed,
passRate: user.totalQuizzes > 0 ? Math.round((user.quizzesPassed / user.totalQuizzes) * 100) : 0,
totalQuestionsAnswered: user.totalQuestionsAnswered,
correctAnswers: user.correctAnswers,
accuracy: user.totalQuestionsAnswered > 0
? Math.round((user.correctAnswers / user.totalQuestionsAnswered) * 100)
: 0,
currentStreak: user.currentStreak,
longestStreak: user.longestStreak
},
activity: {
lastLogin: user.lastLogin,
lastQuizDate: user.lastQuizDate,
memberSince: user.createdAt
},
recentSessions: recentSessions.map(session => ({
id: session.id,
category: session.category,
quizType: session.quizType,
difficulty: session.difficulty,
status: session.status,
score: parseFloat(session.score) || 0,
totalPoints: parseFloat(session.totalPoints) || 0,
percentage: session.totalPoints > 0
? Math.round((parseFloat(session.score) / parseFloat(session.totalPoints)) * 100)
: 0,
questionsAnswered: session.questionsAnswered,
correctAnswers: session.correctAnswers,
completedAt: session.completedAt
}))
};
res.status(200).json({
success: true,
data: userData,
message: 'User details retrieved successfully'
});
} catch (error) {
console.error('Get user by ID error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Update user role (admin only)
* @route PUT /api/admin/users/:userId/role
* @access Private (admin only)
*/
exports.updateUserRole = async (req, res) => {
try {
const { userId } = req.params;
const { role } = req.body;
// Validate UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(userId)) {
return res.status(400).json({
success: false,
message: 'Invalid user ID format'
});
}
// Validate role
if (!role) {
return res.status(400).json({
success: false,
message: 'Role is required'
});
}
if (!['user', 'admin'].includes(role)) {
return res.status(400).json({
success: false,
message: 'Invalid role. Must be "user" or "admin"'
});
}
// Get user
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// If demoting from admin to user, check if there are other admins
if (user.role === 'admin' && role === 'user') {
const adminCount = await User.count({ where: { role: 'admin' } });
if (adminCount <= 1) {
return res.status(400).json({
success: false,
message: 'Cannot demote the last admin user'
});
}
}
// Update role
user.role = role;
await user.save();
res.status(200).json({
success: true,
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
},
message: `User role updated to ${role} successfully`
});
} catch (error) {
console.error('Update user role error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Deactivate user (soft delete) (admin only)
* @route DELETE /api/admin/users/:userId
* @access Private (admin only)
*/
exports.deactivateUser = async (req, res) => {
try {
const { userId } = req.params;
// Validate UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(userId)) {
return res.status(400).json({
success: false,
message: 'Invalid user ID format'
});
}
// Get user
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Check if already deactivated
if (!user.isActive) {
return res.status(400).json({
success: false,
message: 'User is already deactivated'
});
}
// Prevent deactivating admin if they are the last admin
if (user.role === 'admin') {
const adminCount = await User.count({
where: {
role: 'admin',
isActive: true
}
});
if (adminCount <= 1) {
return res.status(400).json({
success: false,
message: 'Cannot deactivate the last active admin user'
});
}
}
// Deactivate user
user.isActive = false;
await user.save();
res.status(200).json({
success: true,
data: {
id: user.id,
username: user.username,
email: user.email,
isActive: user.isActive
},
message: 'User deactivated successfully'
});
} catch (error) {
console.error('Deactivate user error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Reactivate user (admin only)
* @route PUT /api/admin/users/:userId/activate
* @access Private (admin only)
*/
exports.reactivateUser = async (req, res) => {
try {
const { userId } = req.params;
// Validate UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(userId)) {
return res.status(400).json({
success: false,
message: 'Invalid user ID format'
});
}
// Get user
const user = await User.findByPk(userId);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Check if already active
if (user.isActive) {
return res.status(400).json({
success: false,
message: 'User is already active'
});
}
// Reactivate user
user.isActive = true;
await user.save();
res.status(200).json({
success: true,
data: {
id: user.id,
username: user.username,
email: user.email,
isActive: user.isActive
},
message: 'User reactivated successfully'
});
} catch (error) {
console.error('Reactivate user error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};
/**
* Get guest analytics (admin only)
* @route GET /api/admin/guest-analytics
* @access Private (admin only)
*/
exports.getGuestAnalytics = async (req, res) => {
try {
const { GuestSession } = require('../models');
// Calculate date ranges for time-based analytics
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// 1. Total guest sessions created
const totalGuestSessions = await GuestSession.count();
// 2. Active guest sessions (not expired, not converted)
const activeGuestSessions = await GuestSession.count({
where: {
expiresAt: {
[Op.gte]: new Date()
},
convertedUserId: null
}
});
// 3. Expired guest sessions
const expiredGuestSessions = await GuestSession.count({
where: {
expiresAt: {
[Op.lt]: new Date()
},
convertedUserId: null
}
});
// 4. Converted guest sessions (guests who registered)
const convertedGuestSessions = await GuestSession.count({
where: {
convertedUserId: {
[Op.ne]: null
}
}
});
// 5. Conversion rate
const conversionRate = totalGuestSessions > 0
? ((convertedGuestSessions / totalGuestSessions) * 100).toFixed(2)
: 0;
// 6. Guest quiz sessions (total quizzes taken by guests)
const guestQuizSessions = await QuizSession.count({
where: {
guestSessionId: {
[Op.ne]: null
}
}
});
// 7. Completed guest quiz sessions
const completedGuestQuizzes = await QuizSession.count({
where: {
guestSessionId: {
[Op.ne]: null
},
status: { [Op.in]: ['completed', 'timeout'] }
}
});
// 8. Guest quiz completion rate
const guestQuizCompletionRate = guestQuizSessions > 0
? ((completedGuestQuizzes / guestQuizSessions) * 100).toFixed(2)
: 0;
// 9. Average quizzes per guest session
const avgQuizzesPerGuest = totalGuestSessions > 0
? (guestQuizSessions / totalGuestSessions).toFixed(2)
: 0;
// 10. Average quizzes before conversion
// Get all converted guests with their quiz counts
const convertedGuests = await GuestSession.findAll({
where: {
convertedUserId: {
[Op.ne]: null
}
},
attributes: ['id'],
raw: true
});
let avgQuizzesBeforeConversion = 0;
if (convertedGuests.length > 0) {
const guestSessionIds = convertedGuests.map(g => g.id);
const quizCountsResult = await QuizSession.findAll({
attributes: [
'guestSessionId',
[sequelize.fn('COUNT', sequelize.col('id')), 'quizCount']
],
where: {
guestSessionId: {
[Op.in]: guestSessionIds
}
},
group: ['guestSessionId'],
raw: true
});
const totalQuizzes = quizCountsResult.reduce((sum, item) => sum + parseInt(item.quizCount), 0);
avgQuizzesBeforeConversion = (totalQuizzes / convertedGuests.length).toFixed(2);
}
// 11. Guest bounce rate (guests who took 0 quizzes)
// Get all guest sessions
const allGuestSessionIds = await GuestSession.findAll({
attributes: ['id'],
raw: true
});
if (allGuestSessionIds.length > 0) {
// Count guest sessions with at least one quiz
const guestsWithQuizzes = await QuizSession.count({
attributes: ['guestSessionId'],
where: {
guestSessionId: {
[Op.in]: allGuestSessionIds.map(g => g.id)
}
},
group: ['guestSessionId'],
raw: true
});
const guestsWithoutQuizzes = allGuestSessionIds.length - guestsWithQuizzes.length;
var bounceRate = ((guestsWithoutQuizzes / allGuestSessionIds.length) * 100).toFixed(2);
} else {
var bounceRate = 0;
}
// 12. Recent guest activity (last 30 days)
const recentGuestSessions = await GuestSession.count({
where: {
createdAt: {
[Op.gte]: thirtyDaysAgo
}
}
});
// Count recent conversions (guests converted in last 30 days)
// Since we don't have convertedAt, we use updatedAt for converted sessions
const recentConversions = await GuestSession.count({
where: {
convertedUserId: {
[Op.ne]: null
},
updatedAt: {
[Op.gte]: thirtyDaysAgo
}
}
});
// 13. Average session duration for converted guests
// Calculate time from creation to last update (approximation since no convertedAt field)
const convertedWithDuration = await GuestSession.findAll({
where: {
convertedUserId: {
[Op.ne]: null
}
},
attributes: [
'createdAt',
'updatedAt',
[sequelize.literal('TIMESTAMPDIFF(MINUTE, created_at, updated_at)'), 'durationMinutes']
],
raw: true
});
let avgSessionDuration = 0;
if (convertedWithDuration.length > 0) {
const totalMinutes = convertedWithDuration.reduce((sum, item) => {
return sum + (parseInt(item.durationMinutes) || 0);
}, 0);
avgSessionDuration = (totalMinutes / convertedWithDuration.length).toFixed(2);
}
res.status(200).json({
success: true,
data: {
overview: {
totalGuestSessions,
activeGuestSessions,
expiredGuestSessions,
convertedGuestSessions,
conversionRate: parseFloat(conversionRate)
},
quizActivity: {
totalGuestQuizzes: guestQuizSessions,
completedGuestQuizzes,
guestQuizCompletionRate: parseFloat(guestQuizCompletionRate),
avgQuizzesPerGuest: parseFloat(avgQuizzesPerGuest),
avgQuizzesBeforeConversion: parseFloat(avgQuizzesBeforeConversion)
},
behavior: {
bounceRate: parseFloat(bounceRate),
avgSessionDurationMinutes: parseFloat(avgSessionDuration)
},
recentActivity: {
last30Days: {
newGuestSessions: recentGuestSessions,
conversions: recentConversions
}
}
},
message: 'Guest analytics retrieved successfully'
});
} catch (error) {
console.error('Get guest analytics error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
};