8e8f54b41b
- Бэкенд /api/dashboard/activity: per-day агрегация активности по типам (тест/экзамен/карты/уроки/ онлайн/домашка) из 6 таблиц за ~182 дня (раньше карта считала только тесты). - Карта раскрашивается по доминирующему типу активности + легенда типов; интенсивность/размер по числу. - Недельный тренд в футере («эта неделя N · +K к прошлой»). - Тултип и попап по клику показывают разбивку дня по типам. - Empty-state для новичков (вместо пустой сетки — призыв + CTA). Календарь «Месяц» тоже от всех активностей. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
2.3 KiB
JavaScript
48 lines
2.3 KiB
JavaScript
'use strict';
|
|
/* Dashboard aggregates. /api/dashboard/activity — per-day study activity of the
|
|
* current user across ALL engagement types (not just tests), for the activity
|
|
* heatmap. Each source contributes a per-day count by type. */
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../db/db');
|
|
const { authMiddleware } = require('../middleware/auth');
|
|
|
|
router.use(authMiddleware);
|
|
|
|
const WINDOW_DAYS = 182; // ~6 months (max heatmap scale)
|
|
|
|
/* GET /api/dashboard/activity → { days: { 'YYYY-MM-DD': { test, exam, cards, lesson, live, homework } } } */
|
|
router.get('/activity', (req, res) => {
|
|
const uid = req.user.id;
|
|
const days = {};
|
|
const add = (d, type, n) => {
|
|
if (!d || !n) return;
|
|
(days[d] || (days[d] = {}))[type] = (days[d][type] || 0) + n;
|
|
};
|
|
// TEXT datetime sources use a SQL cutoff; exam_attempts stores epoch-ms.
|
|
const txtCut = `datetime('now','-${WINDOW_DAYS} days')`;
|
|
const epochCut = Date.now() - WINDOW_DAYS * 86400000;
|
|
|
|
const sources = [
|
|
['test', `SELECT DATE(started_at) d, COUNT(*) n FROM test_sessions
|
|
WHERE user_id=? AND status='completed' AND started_at >= ${txtCut} GROUP BY d`, [uid]],
|
|
['exam', `SELECT DATE(created_at/1000,'unixepoch') d, COUNT(*) n FROM exam_attempts
|
|
WHERE user_id=? AND created_at >= ? GROUP BY d`, [uid, epochCut]],
|
|
['cards', `SELECT DATE(last_reviewed) d, COUNT(*) n FROM flashcard_reviews
|
|
WHERE user_id=? AND last_reviewed IS NOT NULL AND last_reviewed >= ${txtCut} GROUP BY d`, [uid]],
|
|
['lesson', `SELECT DATE(updated_at) d, COUNT(*) n FROM lesson_progress
|
|
WHERE user_id=? AND completed=1 AND updated_at >= ${txtCut} GROUP BY d`, [uid]],
|
|
['live', `SELECT DATE(joined_at) d, COUNT(*) n FROM classroom_attendance
|
|
WHERE user_id=? AND joined_at >= ${txtCut} GROUP BY d`, [uid]],
|
|
['homework', `SELECT DATE(completed_at) d, COUNT(*) n FROM assignment_completion
|
|
WHERE user_id=? AND completed_at >= ${txtCut} GROUP BY d`, [uid]],
|
|
];
|
|
for (const [type, sql, args] of sources) {
|
|
try { db.prepare(sql).all(...args).forEach(r => add(r.d, type, r.n)); }
|
|
catch (e) { /* tolerate a missing/legacy table — skip that source */ }
|
|
}
|
|
res.json({ days });
|
|
});
|
|
|
|
module.exports = router;
|