119 lines
3.5 KiB
JavaScript
119 lines
3.5 KiB
JavaScript
import { StudyItem } from '../models/StudyItem.js';
|
|
import { ReviewLog } from '../models/ReviewLog.js';
|
|
import { getDaysDiff } from '../utils/dateUtils.js';
|
|
|
|
export const getUserStats = async (user) => {
|
|
const userId = user._id;
|
|
|
|
const srsCounts = await StudyItem.aggregate([
|
|
{ $match: { userId: userId } },
|
|
{ $group: { _id: "$srsLevel", count: { $sum: 1 } } }
|
|
]);
|
|
const dist = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
|
|
srsCounts.forEach(g => { if (dist[g._id] !== undefined) dist[g._id] = g.count; });
|
|
|
|
const now = new Date();
|
|
const next24h = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
const upcoming = await StudyItem.find({
|
|
userId: userId,
|
|
srsLevel: { $lt: 6, $gt: 0 },
|
|
nextReview: { $lte: next24h }
|
|
}).select('nextReview');
|
|
|
|
const forecast = new Array(24).fill(0);
|
|
upcoming.forEach(item => {
|
|
const diff = Math.floor((new Date(item.nextReview) - now) / 3600000);
|
|
if (diff < 0) forecast[0]++;
|
|
else if (diff < 24) forecast[diff]++;
|
|
});
|
|
|
|
const queueItems = await StudyItem.find({
|
|
userId: userId,
|
|
srsLevel: { $lt: 6, $gt: 0 },
|
|
nextReview: { $lte: now }
|
|
}).select('srsLevel');
|
|
const queueCount = queueItems.length;
|
|
|
|
const lessonCount = await StudyItem.countDocuments({
|
|
userId: userId,
|
|
srsLevel: 0
|
|
});
|
|
|
|
let hasLowerLevels = false;
|
|
let lowerLevelCount = 0;
|
|
if (queueCount > 0) {
|
|
const levels = queueItems.map(i => i.srsLevel);
|
|
const minSrs = Math.min(...levels);
|
|
const maxSrs = Math.max(...levels);
|
|
if (minSrs < maxSrs) {
|
|
hasLowerLevels = true;
|
|
lowerLevelCount = queueItems.filter(i => i.srsLevel === minSrs).length;
|
|
}
|
|
}
|
|
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setDate(oneYearAgo.getDate() - 365);
|
|
const dateStr = oneYearAgo.toISOString().split('T')[0];
|
|
const logs = await ReviewLog.find({ userId: userId, date: { $gte: dateStr } });
|
|
const heatmap = {};
|
|
logs.forEach(l => { heatmap[l.date] = l.count; });
|
|
|
|
const todayStr = new Date().toISOString().split('T')[0];
|
|
const lastStudyStr = user.stats.lastStudyDate || null;
|
|
let displayStreak = user.stats.currentStreak || 0;
|
|
|
|
if (lastStudyStr) {
|
|
const diff = getDaysDiff(lastStudyStr, todayStr);
|
|
if (diff > 1) displayStreak = 0;
|
|
}
|
|
|
|
const lastFreeze = user.stats.lastFreezeDate ? new Date(user.stats.lastFreezeDate) : null;
|
|
let daysSinceFreeze = 999;
|
|
if (lastFreeze) {
|
|
daysSinceFreeze = getDaysDiff(lastFreeze.toISOString().split('T')[0], todayStr);
|
|
}
|
|
|
|
const shieldReady = daysSinceFreeze >= 7;
|
|
const shieldCooldown = shieldReady ? 0 : (7 - daysSinceFreeze);
|
|
|
|
const history7Days = [];
|
|
for (let i = 6; i >= 0; i--) {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() - i);
|
|
const dStr = d.toISOString().split('T')[0];
|
|
const count = heatmap[dStr] || 0;
|
|
history7Days.push({ date: dStr, active: count > 0 });
|
|
}
|
|
|
|
const allGhosts = await StudyItem.find({ userId: userId, 'stats.total': { $gte: 2 } })
|
|
.select('char meaning stats srsLevel');
|
|
|
|
const sortedGhosts = allGhosts.map(item => ({
|
|
...item.toObject(),
|
|
accuracy: Math.round((item.stats.correct / item.stats.total) * 100)
|
|
})).filter(i => i.accuracy < 85)
|
|
.sort((a, b) => a.accuracy - b.accuracy)
|
|
.slice(0, 10);
|
|
|
|
return {
|
|
distribution: dist,
|
|
forecast: forecast,
|
|
queueLength: queueCount,
|
|
lessonCount: lessonCount,
|
|
hasLowerLevels,
|
|
lowerLevelCount,
|
|
heatmap: heatmap,
|
|
ghosts: sortedGhosts,
|
|
streak: {
|
|
current: displayStreak,
|
|
best: user.stats.maxStreak || 0,
|
|
shield: { ready: shieldReady, cooldown: Math.max(0, shieldCooldown) },
|
|
history: history7Days
|
|
},
|
|
accuracy: {
|
|
total: user.stats.totalReviews || 0,
|
|
correct: user.stats.correctReviews || 0
|
|
}
|
|
};
|
|
};
|