feat(dashboard): блок активности — все виды учёбы, тренд, разбивка по типам, empty-state

- Бэкенд /api/dashboard/activity: per-day агрегация активности по типам (тест/экзамен/карты/уроки/
  онлайн/домашка) из 6 таблиц за ~182 дня (раньше карта считала только тесты).
- Карта раскрашивается по доминирующему типу активности + легенда типов; интенсивность/размер по числу.
- Недельный тренд в футере («эта неделя N · +K к прошлой»).
- Тултип и попап по клику показывают разбивку дня по типам.
- Empty-state для новичков (вместо пустой сетки — призыв + CTA). Календарь «Месяц» тоже от всех активностей.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 13:06:46 +03:00
parent 7a2a07c96e
commit 8e8f54b41b
4 changed files with 144 additions and 134 deletions
+47
View File
@@ -0,0 +1,47 @@
'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;
+1
View File
@@ -194,6 +194,7 @@ app.use('/api/access', accessRoutes);
app.use('/api/teacher-students', teacherStudentsRoutes);
app.use('/api/lab', labRoutes);
app.use('/api/materials', require('./routes/materials'));
app.use('/api/dashboard', require('./routes/dashboard'));
/* ── Public features endpoint (merges global + per-class for authenticated students) ── */
const _featDb = require('./db/db');