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:
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user