feat: implement comprehensive gamification, analytics, and leaderboard system
This commit adds a complete gamification system with analytics dashboards, leaderboards, and enhanced badge tracking functionality. Backend Features: - Analytics API with overview, user stats, activity trends, top contributors, and street statistics endpoints - Leaderboard API supporting global, weekly, monthly, and friends views - Profile API for viewing and managing user profiles - Enhanced gamification service with badge progress tracking and user stats - Comprehensive test coverage for analytics and leaderboard endpoints - Profile validation middleware for secure profile updates Frontend Features: - Analytics dashboard with multiple tabs (Overview, Activity, Personal Stats) - Interactive charts for activity trends and street statistics - Leaderboard component with pagination and timeframe filtering - Badge collection display with progress tracking - Personal stats component showing user achievements - Contributors list for top performing users - Profile management components (View/Edit) - Toast notifications integrated throughout - Comprehensive test coverage for Leaderboard component Enhancements: - User model enhanced with stats tracking and badge management - Fixed express.Router() capitalization bug in users route - Badge service improvements for better criteria matching - Removed unused imports in Profile component This feature enables users to track their contributions, view community analytics, compete on leaderboards, and earn badges for achievements. 🤖 Generated with OpenCode Co-Authored-By: AI Assistant <noreply@opencode.ai>
This commit is contained in:
@@ -113,14 +113,14 @@ async function checkAndAwardBadges(userId, userPoints = null) {
|
||||
// Check each badge criteria
|
||||
for (const badge of allBadges) {
|
||||
// Skip if user already has this badge
|
||||
if (userBadges.some(ub => ub.badgeId === badge._id)) {
|
||||
if (userBadges.some(ub => ub.badge?._id === badge._id || ub.badge === badge._id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let qualifies = false;
|
||||
|
||||
// Check different badge criteria
|
||||
switch (badge.criteria.type) {
|
||||
switch (badge.criteria?.type) {
|
||||
case 'points_earned':
|
||||
qualifies = userPoints >= badge.criteria.threshold;
|
||||
break;
|
||||
@@ -128,13 +128,13 @@ async function checkAndAwardBadges(userId, userPoints = null) {
|
||||
qualifies = userStats.streetAdoptions >= badge.criteria.threshold;
|
||||
break;
|
||||
case 'task_completions':
|
||||
qualifies = userStats.taskCompletions >= badge.criteria.threshold;
|
||||
qualifies = userStats.tasksCompleted >= badge.criteria.threshold;
|
||||
break;
|
||||
case 'post_creations':
|
||||
qualifies = userStats.postCreations >= badge.criteria.threshold;
|
||||
qualifies = userStats.postsCreated >= badge.criteria.threshold;
|
||||
break;
|
||||
case 'event_participations':
|
||||
qualifies = userStats.eventParticipations >= badge.criteria.threshold;
|
||||
qualifies = userStats.eventsParticipated >= badge.criteria.threshold;
|
||||
break;
|
||||
case 'consecutive_days':
|
||||
qualifies = userStats.consecutiveDays >= badge.criteria.threshold;
|
||||
@@ -168,9 +168,9 @@ async function awardBadge(userId, badgeId) {
|
||||
|
||||
// Create user badge record
|
||||
const userBadge = await UserBadge.create({
|
||||
userId: userId,
|
||||
badgeId: badgeId,
|
||||
awardedAt: new Date().toISOString(),
|
||||
user: userId,
|
||||
badge: badgeId,
|
||||
earnedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Award points for earning badge (if it's a rare or higher badge)
|
||||
@@ -205,16 +205,19 @@ async function awardBadge(userId, badgeId) {
|
||||
*/
|
||||
async function getUserStats(userId) {
|
||||
try {
|
||||
// This would typically involve querying various collections
|
||||
// For now, return basic stats - this should be enhanced
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
streetAdoptions: 0, // Would query Street collection
|
||||
taskCompletions: 0, // Would query Task collection
|
||||
postCreations: 0, // Would query Post collection
|
||||
eventParticipations: 0, // Would query Event participation
|
||||
consecutiveDays: 0, // Would calculate from login history
|
||||
points: user.points || 0,
|
||||
streetsAdopted: user.stats?.streetsAdopted || 0,
|
||||
tasksCompleted: user.stats?.tasksCompleted || 0,
|
||||
postsCreated: user.stats?.postsCreated || 0,
|
||||
eventsParticipated: user.stats?.eventsParticipated || 0,
|
||||
badgesEarned: user.stats?.badgesEarned || 0,
|
||||
consecutiveDays: user.stats?.consecutiveDays || 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting user stats:", error);
|
||||
@@ -222,6 +225,71 @@ async function getUserStats(userId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's badge progress for all badges (earned and unearned)
|
||||
*/
|
||||
async function getUserBadgeProgress(userId) {
|
||||
try {
|
||||
const allBadges = await Badge.findAll();
|
||||
const userStats = await getUserStats(userId);
|
||||
const userEarnedBadges = await UserBadge.findByUser(userId);
|
||||
|
||||
const badgeProgress = allBadges.map(badge => {
|
||||
const earnedBadge = userEarnedBadges.find(ub => ub.badge?._id === badge._id || ub.badge === badge._id);
|
||||
const isEarned = !!earnedBadge;
|
||||
let progress = 0;
|
||||
let threshold = badge.criteria?.threshold || 0;
|
||||
|
||||
if (isEarned) {
|
||||
progress = threshold; // If earned, progress is full
|
||||
} else if (badge.criteria?.type) {
|
||||
switch (badge.criteria.type) {
|
||||
case 'points_earned':
|
||||
progress = userStats.points || 0;
|
||||
break;
|
||||
case 'street_adoptions':
|
||||
progress = userStats.streetsAdopted;
|
||||
break;
|
||||
case 'task_completions':
|
||||
progress = userStats.tasksCompleted;
|
||||
break;
|
||||
case 'post_creations':
|
||||
progress = userStats.postsCreated;
|
||||
break;
|
||||
case 'event_participations':
|
||||
progress = userStats.eventsParticipated;
|
||||
break;
|
||||
case 'consecutive_days':
|
||||
progress = userStats.consecutiveDays;
|
||||
break;
|
||||
case 'special':
|
||||
progress = 0; // Special badges have no progress bar
|
||||
threshold = 1;
|
||||
break;
|
||||
default:
|
||||
progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure progress doesn't exceed threshold
|
||||
progress = Math.min(progress, threshold);
|
||||
|
||||
return {
|
||||
...badge,
|
||||
isEarned,
|
||||
progress,
|
||||
threshold,
|
||||
earnedAt: isEarned ? earnedBadge.earnedAt : null,
|
||||
};
|
||||
});
|
||||
|
||||
return badgeProgress;
|
||||
} catch (error) {
|
||||
console.error("Error getting user badge progress:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's badges
|
||||
*/
|
||||
@@ -231,11 +299,13 @@ async function getUserBadges(userId) {
|
||||
const badges = [];
|
||||
|
||||
for (const userBadge of userBadges) {
|
||||
const badge = await Badge.findById(userBadge.badgeId);
|
||||
const badgeData = userBadge.badge;
|
||||
// If badge is already populated (object), use it; otherwise fetch it
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
if (badge) {
|
||||
badges.push({
|
||||
...badge,
|
||||
awardedAt: userBadge.awardedAt,
|
||||
earnedAt: userBadge.earnedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -301,6 +371,455 @@ async function getLeaderboard(limit = 10) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global leaderboard (all time)
|
||||
* @param {number} limit - Number of users to return
|
||||
* @param {number} offset - Offset for pagination
|
||||
* @returns {Promise<Array>} Leaderboard data
|
||||
*/
|
||||
async function getGlobalLeaderboard(limit = 100, offset = 0) {
|
||||
try {
|
||||
const couchdbService = require("./couchdbService");
|
||||
const Street = require("../models/Street");
|
||||
const Task = require("../models/Task");
|
||||
|
||||
// Get all users sorted by points
|
||||
const result = await couchdbService.find({
|
||||
selector: {
|
||||
type: "user",
|
||||
points: { $gt: 0 }
|
||||
},
|
||||
sort: [{ points: "desc" }],
|
||||
limit: limit,
|
||||
skip: offset
|
||||
});
|
||||
|
||||
// Enrich with stats and badges
|
||||
const leaderboard = await Promise.all(result.map(async (user, index) => {
|
||||
// Get user badges
|
||||
const userBadges = await UserBadge.findByUser(user._id);
|
||||
const badges = await Promise.all(userBadges.map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
return badge ? {
|
||||
_id: badge._id,
|
||||
name: badge.name,
|
||||
icon: badge.icon,
|
||||
rarity: badge.rarity
|
||||
} : null;
|
||||
}));
|
||||
|
||||
return {
|
||||
rank: offset + index + 1,
|
||||
userId: user._id,
|
||||
username: user.name,
|
||||
avatar: user.profilePicture || null,
|
||||
points: user.points || 0,
|
||||
streetsAdopted: user.stats?.streetsAdopted || user.adoptedStreets?.length || 0,
|
||||
tasksCompleted: user.stats?.tasksCompleted || user.completedTasks?.length || 0,
|
||||
badges: badges.filter(b => b !== null)
|
||||
};
|
||||
}));
|
||||
|
||||
return leaderboard;
|
||||
} catch (error) {
|
||||
console.error("Error getting global leaderboard:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get weekly leaderboard
|
||||
* @param {number} limit - Number of users to return
|
||||
* @param {number} offset - Offset for pagination
|
||||
* @returns {Promise<Array>} Leaderboard data
|
||||
*/
|
||||
async function getWeeklyLeaderboard(limit = 100, offset = 0) {
|
||||
try {
|
||||
const couchdbService = require("./couchdbService");
|
||||
|
||||
// Calculate start of week (Monday 00:00:00)
|
||||
const now = new Date();
|
||||
const dayOfWeek = now.getDay();
|
||||
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
||||
const startOfWeek = new Date(now);
|
||||
startOfWeek.setDate(now.getDate() - daysToMonday);
|
||||
startOfWeek.setHours(0, 0, 0, 0);
|
||||
|
||||
// Get all point transactions since start of week
|
||||
const transactions = await couchdbService.find({
|
||||
selector: {
|
||||
type: "point_transaction",
|
||||
createdAt: { $gte: startOfWeek.toISOString() }
|
||||
}
|
||||
});
|
||||
|
||||
// Aggregate points by user
|
||||
const userPointsMap = {};
|
||||
transactions.forEach(transaction => {
|
||||
if (!userPointsMap[transaction.user]) {
|
||||
userPointsMap[transaction.user] = 0;
|
||||
}
|
||||
userPointsMap[transaction.user] += transaction.amount;
|
||||
});
|
||||
|
||||
// Convert to array and sort
|
||||
const userPoints = Object.entries(userPointsMap)
|
||||
.map(([userId, points]) => ({ userId, points }))
|
||||
.filter(entry => entry.points > 0)
|
||||
.sort((a, b) => b.points - a.points)
|
||||
.slice(offset, offset + limit);
|
||||
|
||||
// Enrich with user data
|
||||
const leaderboard = await Promise.all(userPoints.map(async (entry, index) => {
|
||||
const user = await User.findById(entry.userId);
|
||||
if (!user) return null;
|
||||
|
||||
// Get user badges
|
||||
const userBadges = await UserBadge.findByUser(userId);
|
||||
const badges = await Promise.all(userBadges.map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
return badge ? {
|
||||
_id: badge._id,
|
||||
name: badge.name,
|
||||
icon: badge.icon,
|
||||
rarity: badge.rarity
|
||||
} : null;
|
||||
}));
|
||||
|
||||
return {
|
||||
rank: offset + index + 1,
|
||||
userId: user._id,
|
||||
username: user.name,
|
||||
avatar: user.profilePicture || null,
|
||||
points: entry.points,
|
||||
streetsAdopted: user.stats?.streetsAdopted || user.adoptedStreets?.length || 0,
|
||||
tasksCompleted: user.stats?.tasksCompleted || user.completedTasks?.length || 0,
|
||||
badges: badges.filter(b => b !== null)
|
||||
};
|
||||
}));
|
||||
|
||||
return leaderboard.filter(entry => entry !== null);
|
||||
} catch (error) {
|
||||
console.error("Error getting weekly leaderboard:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monthly leaderboard
|
||||
* @param {number} limit - Number of users to return
|
||||
* @param {number} offset - Offset for pagination
|
||||
* @returns {Promise<Array>} Leaderboard data
|
||||
*/
|
||||
async function getMonthlyLeaderboard(limit = 100, offset = 0) {
|
||||
try {
|
||||
const couchdbService = require("./couchdbService");
|
||||
|
||||
// Calculate start of month
|
||||
const now = new Date();
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
// Get all point transactions since start of month
|
||||
const transactions = await couchdbService.find({
|
||||
selector: {
|
||||
type: "point_transaction",
|
||||
createdAt: { $gte: startOfMonth.toISOString() }
|
||||
}
|
||||
});
|
||||
|
||||
// Aggregate points by user
|
||||
const userPointsMap = {};
|
||||
transactions.forEach(transaction => {
|
||||
if (!userPointsMap[transaction.user]) {
|
||||
userPointsMap[transaction.user] = 0;
|
||||
}
|
||||
userPointsMap[transaction.user] += transaction.amount;
|
||||
});
|
||||
|
||||
// Convert to array and sort
|
||||
const userPoints = Object.entries(userPointsMap)
|
||||
.map(([userId, points]) => ({ userId, points }))
|
||||
.filter(entry => entry.points > 0)
|
||||
.sort((a, b) => b.points - a.points)
|
||||
.slice(offset, offset + limit);
|
||||
|
||||
// Enrich with user data
|
||||
const leaderboard = await Promise.all(userPoints.map(async (entry, index) => {
|
||||
const user = await User.findById(entry.userId);
|
||||
if (!user) return null;
|
||||
|
||||
// Get user badges
|
||||
const userBadges = await UserBadge.findByUser(user._id);
|
||||
const badges = await Promise.all(userBadges.slice(0, 5).map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
return badge ? {
|
||||
_id: badge._id,
|
||||
name: badge.name,
|
||||
icon: badge.icon,
|
||||
rarity: badge.rarity
|
||||
} : null;
|
||||
}));
|
||||
|
||||
return {
|
||||
rank: offset + index + 1,
|
||||
userId: user._id,
|
||||
username: user.name,
|
||||
avatar: user.profilePicture || null,
|
||||
points: entry.points,
|
||||
streetsAdopted: user.stats?.streetsAdopted || user.adoptedStreets?.length || 0,
|
||||
tasksCompleted: user.stats?.tasksCompleted || user.completedTasks?.length || 0,
|
||||
badges: badges.filter(b => b !== null)
|
||||
};
|
||||
}));
|
||||
|
||||
return leaderboard.filter(entry => entry !== null);
|
||||
} catch (error) {
|
||||
console.error("Error getting monthly leaderboard:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get friends leaderboard
|
||||
* @param {string} userId - User ID
|
||||
* @param {number} limit - Number of users to return
|
||||
* @param {number} offset - Offset for pagination
|
||||
* @returns {Promise<Array>} Leaderboard data
|
||||
*/
|
||||
async function getFriendsLeaderboard(userId, limit = 100, offset = 0) {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
// For now, return empty array as friends system isn't implemented
|
||||
// In future, would get user's friends list and filter leaderboard
|
||||
const friendIds = user.friends || [];
|
||||
|
||||
if (friendIds.length === 0) {
|
||||
// Include self if no friends
|
||||
friendIds.push(userId);
|
||||
}
|
||||
|
||||
const couchdbService = require("./couchdbService");
|
||||
|
||||
// Get friends' data
|
||||
const friends = await couchdbService.find({
|
||||
selector: {
|
||||
type: "user",
|
||||
_id: { $in: friendIds }
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by points
|
||||
const sortedFriends = friends
|
||||
.sort((a, b) => (b.points || 0) - (a.points || 0))
|
||||
.slice(offset, offset + limit);
|
||||
|
||||
// Enrich with badges
|
||||
const leaderboard = await Promise.all(sortedFriends.map(async (friend, index) => {
|
||||
const userBadges = await UserBadge.findByUser(friend._id);
|
||||
const badges = await Promise.all(userBadges.slice(0, 5).map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
return badge ? {
|
||||
_id: badge._id,
|
||||
name: badge.name,
|
||||
icon: badge.icon,
|
||||
rarity: badge.rarity
|
||||
} : null;
|
||||
}));
|
||||
|
||||
return {
|
||||
rank: offset + index + 1,
|
||||
userId: friend._id,
|
||||
username: friend.name,
|
||||
avatar: friend.profilePicture || null,
|
||||
points: friend.points || 0,
|
||||
streetsAdopted: friend.stats?.streetsAdopted || friend.adoptedStreets?.length || 0,
|
||||
tasksCompleted: friend.stats?.tasksCompleted || friend.completedTasks?.length || 0,
|
||||
badges: badges.filter(b => b !== null),
|
||||
isFriend: true
|
||||
};
|
||||
}));
|
||||
|
||||
return leaderboard;
|
||||
} catch (error) {
|
||||
console.error("Error getting friends leaderboard:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's leaderboard position
|
||||
* @param {string} userId - User ID
|
||||
* @param {string} timeframe - 'all', 'week', or 'month'
|
||||
* @returns {Promise<Object>} User's position data
|
||||
*/
|
||||
async function getUserLeaderboardPosition(userId, timeframe = "all") {
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rank = 0;
|
||||
let totalUsers = 0;
|
||||
let userPoints = 0;
|
||||
|
||||
const couchdbService = require("./couchdbService");
|
||||
|
||||
if (timeframe === "all") {
|
||||
// Get all users with points
|
||||
const allUsers = await couchdbService.find({
|
||||
selector: {
|
||||
type: "user",
|
||||
points: { $gt: 0 }
|
||||
},
|
||||
sort: [{ points: "desc" }]
|
||||
});
|
||||
|
||||
totalUsers = allUsers.length;
|
||||
userPoints = user.points || 0;
|
||||
|
||||
// Find user's rank
|
||||
rank = allUsers.findIndex(u => u._id === userId) + 1;
|
||||
} else if (timeframe === "week" || timeframe === "month") {
|
||||
// Calculate start date
|
||||
const now = new Date();
|
||||
let startDate;
|
||||
|
||||
if (timeframe === "week") {
|
||||
const dayOfWeek = now.getDay();
|
||||
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
||||
startDate = new Date(now);
|
||||
startDate.setDate(now.getDate() - daysToMonday);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
} else {
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
}
|
||||
|
||||
// Get all transactions for period
|
||||
const transactions = await couchdbService.find({
|
||||
selector: {
|
||||
type: "point_transaction",
|
||||
createdAt: { $gte: startDate.toISOString() }
|
||||
}
|
||||
});
|
||||
|
||||
// Aggregate points by user
|
||||
const userPointsMap = {};
|
||||
transactions.forEach(transaction => {
|
||||
if (!userPointsMap[transaction.user]) {
|
||||
userPointsMap[transaction.user] = 0;
|
||||
}
|
||||
userPointsMap[transaction.user] += transaction.amount;
|
||||
});
|
||||
|
||||
// Sort users by points
|
||||
const sortedUsers = Object.entries(userPointsMap)
|
||||
.filter(([_, points]) => points > 0)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
totalUsers = sortedUsers.length;
|
||||
userPoints = userPointsMap[userId] || 0;
|
||||
rank = sortedUsers.findIndex(([id, _]) => id === userId) + 1;
|
||||
}
|
||||
|
||||
// Get user badges
|
||||
const userBadges = await UserBadge.findByUser(user._id);
|
||||
const badges = await Promise.all(userBadges.slice(0, 5).map(async (ub) => {
|
||||
const badgeData = ub.badge;
|
||||
const badge = typeof badgeData === 'object' && badgeData._id ? badgeData : await Badge.findById(badgeData);
|
||||
return badge ? {
|
||||
_id: badge._id,
|
||||
name: badge.name,
|
||||
icon: badge.icon,
|
||||
rarity: badge.rarity
|
||||
} : null;
|
||||
}));
|
||||
|
||||
return {
|
||||
rank: rank || null,
|
||||
totalUsers,
|
||||
userId: user._id,
|
||||
username: user.name,
|
||||
avatar: user.profilePicture || null,
|
||||
points: userPoints,
|
||||
streetsAdopted: user.stats?.streetsAdopted || user.adoptedStreets?.length || 0,
|
||||
tasksCompleted: user.stats?.tasksCompleted || user.completedTasks?.length || 0,
|
||||
badges: badges.filter(b => b !== null),
|
||||
percentile: totalUsers > 0 ? Math.round((1 - (rank - 1) / totalUsers) * 100) : 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting user leaderboard position:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leaderboard statistics
|
||||
* @returns {Promise<Object>} Statistics data
|
||||
*/
|
||||
async function getLeaderboardStats() {
|
||||
try {
|
||||
const couchdbService = require("./couchdbService");
|
||||
|
||||
// Get all users with points
|
||||
const allUsers = await couchdbService.find({
|
||||
selector: {
|
||||
type: "user",
|
||||
points: { $gt: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate statistics
|
||||
const totalUsers = allUsers.length;
|
||||
const totalPoints = allUsers.reduce((sum, user) => sum + (user.points || 0), 0);
|
||||
const avgPoints = totalUsers > 0 ? Math.round(totalPoints / totalUsers) : 0;
|
||||
const maxPoints = allUsers.length > 0 ? Math.max(...allUsers.map(u => u.points || 0)) : 0;
|
||||
const minPoints = allUsers.length > 0 ? Math.min(...allUsers.map(u => u.points || 0)) : 0;
|
||||
|
||||
// Get weekly stats
|
||||
const now = new Date();
|
||||
const dayOfWeek = now.getDay();
|
||||
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
||||
const startOfWeek = new Date(now);
|
||||
startOfWeek.setDate(now.getDate() - daysToMonday);
|
||||
startOfWeek.setHours(0, 0, 0, 0);
|
||||
|
||||
const weeklyTransactions = await couchdbService.find({
|
||||
selector: {
|
||||
type: "point_transaction",
|
||||
createdAt: { $gte: startOfWeek.toISOString() }
|
||||
}
|
||||
});
|
||||
|
||||
const weeklyPoints = weeklyTransactions.reduce((sum, t) => sum + (t.amount || 0), 0);
|
||||
const activeUsersThisWeek = new Set(weeklyTransactions.map(t => t.user)).size;
|
||||
|
||||
return {
|
||||
totalUsers,
|
||||
totalPoints,
|
||||
avgPoints,
|
||||
maxPoints,
|
||||
minPoints,
|
||||
weeklyStats: {
|
||||
totalPoints: weeklyPoints,
|
||||
activeUsers: activeUsersThisWeek,
|
||||
transactions: weeklyTransactions.length
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting leaderboard statistics:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
awardPoints,
|
||||
getUserPoints,
|
||||
@@ -308,7 +827,15 @@ module.exports = {
|
||||
checkAndAwardBadges,
|
||||
awardBadge,
|
||||
getUserBadges,
|
||||
getUserBadgeProgress,
|
||||
getUserStats,
|
||||
redeemPoints,
|
||||
getLeaderboard,
|
||||
getGlobalLeaderboard,
|
||||
getWeeklyLeaderboard,
|
||||
getMonthlyLeaderboard,
|
||||
getFriendsLeaderboard,
|
||||
getUserLeaderboardPosition,
|
||||
getLeaderboardStats,
|
||||
POINT_VALUES,
|
||||
};
|
||||
Reference in New Issue
Block a user