add full dashboard: activity chart, recent entries, group photos, quick actions, active groups, top students

This commit is contained in:
dev
2026-09-06 15:08:39 +03:00
parent 65e5651be2
commit 84fed96097
3 changed files with 262 additions and 6 deletions
+64
View File
@@ -695,6 +695,70 @@ app.get('/api/stats', requireAdmin, async (_, res) => {
});
});
app.get('/api/dashboard', requireAdmin, async (req, res) => {
const DAYS = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
const [stats, activity, active, recent, top, photos, latestPhotos] = await Promise.all([
(async () => {
const [entries, trash, groups, students, today] = await Promise.all([
pool.query('SELECT count(*)::int AS n FROM entries WHERE deleted_at IS NULL'),
pool.query('SELECT count(*)::int AS n FROM entries WHERE deleted_at IS NOT NULL'),
pool.query('SELECT count(*)::int AS n FROM groups'),
pool.query('SELECT count(DISTINCT student_name)::int AS n FROM entries WHERE deleted_at IS NULL'),
pool.query("SELECT count(*)::int AS n FROM entries WHERE deleted_at IS NULL AND created_at >= now()::date"),
]);
return { entries: entries.rows[0].n, trash: trash.rows[0].n, groups: groups.rows[0].n, students: students.rows[0].n, today: today.rows[0].n };
})(),
pool.query(
`SELECT to_char(created_at, 'YYYY-MM-DD') AS d, count(*)::int AS n
FROM entries WHERE deleted_at IS NULL
AND created_at >= (now() - interval '13 days')::date
GROUP BY 1 ORDER BY 1`
),
pool.query(
`SELECT * FROM groups
WHERE day_of_week IS NOT NULL AND time_start IS NOT NULL AND time_end IS NOT NULL
AND day_of_week = EXTRACT(DOW FROM (now() AT TIME ZONE 'Europe/Moscow'))::int
AND (now() AT TIME ZONE 'Europe/Moscow')::time BETWEEN time_start AND time_end
ORDER BY id`
),
pool.query(
`SELECT e.id, e.student_name, e.photo_path, e.created_at, g.name AS group_name
FROM entries e JOIN groups g ON g.id = e.group_id
WHERE e.deleted_at IS NULL
ORDER BY e.created_at DESC LIMIT 7`
),
pool.query(
`SELECT student_name, count(*)::int AS n FROM entries
WHERE deleted_at IS NULL GROUP BY student_name ORDER BY n DESC, student_name LIMIT 5`
),
pool.query(
`SELECT gp.id, gp.photo_path, gp.caption, gp.taken_at, g.name AS group_name
FROM group_photos gp JOIN groups g ON g.id = gp.group_id
ORDER BY gp.taken_at DESC NULLS LAST, gp.created_at DESC LIMIT 8`
),
pool.query(
`SELECT photo_path, taken_at FROM group_photos
ORDER BY taken_at DESC NULLS LAST, created_at DESC LIMIT 1`
),
]);
const activeGroups = active.rows.map(g => ({
id: g.id,
name: g.name,
day_label: DAYS[g.day_of_week],
time_start: (g.time_start || '').slice(0, 5),
time_end: (g.time_end || '').slice(0, 5),
}));
res.json({
stats: stats,
activity: activity.rows,
active_groups: activeGroups,
recent_entries: recent.rows,
top_students: top.rows,
photos: photos.rows,
latest_photo_taken: (latestPhotos.rows[0] || {}).taken_at || null,
});
});
app.post('/api/entries', upload.fields([{ name: 'photo', maxCount: 1 }, { name: 'files', maxCount: 10 }]), async (req, res) => {
const { student_name, group_id, description } = req.body;
const photo = req.files?.photo?.[0] || null;