add full dashboard: activity chart, recent entries, group photos, quick actions, active groups, top students
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user