Files
sm-bm-plugin/controllers/api/dashboard.go
Jakub Zych 98cabfd51d feat: add the student API under /_bm/api/v1
Dashboard, courses with counters, schedule, meetings, deadlines,
readings with per-user progress (mark and unmark) and paginated
published materials with tags and files, behind golem15.user's
jwt.auth. Responses are built field by field with Warsaw RFC 3339
timestamps; bad query parameters answer 422 in Laravel's shape.
2026-10-02 14:26:34 +02:00

89 lines
2.6 KiB
Go

package api
import (
"net/http"
"git.golem15.com/jakub/sm-bm-plugin/models"
"gorm.io/gorm"
)
// dashboardJSON is the answer of /dashboard.
type dashboardJSON struct {
NextSessions []sessionJSON `json:"next_sessions"`
UpcomingDeadlines []deadlineJSON `json:"upcoming_deadlines"`
CurrentMeeting *meetingJSON `json:"current_meeting"`
NextMeeting *meetingJSON `json:"next_meeting"`
Readings readingsSummary `json:"readings"`
}
// Dashboard answers GET /dashboard: the next class sessions (those not yet
// over), the deadlines not yet due, the meeting under way today and the next
// one, and the caller's reading progress.
func (h *Handlers) Dashboard(w http.ResponseWriter, r *http.Request) {
userID, ok := h.caller(w, r)
if !ok {
return
}
db, ok := h.db(w, r)
if !ok {
return
}
now := h.now().UTC()
limit := h.dashboardLimit()
out := dashboardJSON{}
var sessions []models.ClassSession
if err := preloadSession(db).Where("ends_at > ?", now).Order("starts_at, id").Limit(limit).Find(&sessions).Error; err != nil {
writeOpaque500(w)
return
}
out.NextSessions = newSessionsJSON(sessions)
var deadlines []models.Deadline
if err := db.Preload("Course").Where("due_at >= ?", now).Order("due_at, id").Limit(limit).Find(&deadlines).Error; err != nil {
writeOpaque500(w)
return
}
out.UpcomingDeadlines = newDeadlinesJSON(deadlines)
day := today(h.now()).Format(models.DateLayout)
var err error
if out.CurrentMeeting, err = meetingWhere(db, "starts_on <= ? AND ends_on >= ?", day, day); err != nil {
writeOpaque500(w)
return
}
if out.NextMeeting, err = meetingWhere(db, "starts_on > ?", day); err != nil {
writeOpaque500(w)
return
}
err = db.Raw(`SELECT
COUNT(*) AS total,
COUNT(p.id) AS read,
COUNT(*) FILTER (WHERE r.is_required) AS required_total,
COUNT(p.id) FILTER (WHERE r.is_required) AS required_read
FROM jz_bm_readings r
LEFT JOIN jz_bm_reading_progress p ON p.reading_id = r.id AND p.user_id = ?`, userID).Scan(&out.Readings).Error
if err != nil {
writeOpaque500(w)
return
}
writeData(w, out)
}
// meetingWhere returns the first meeting by start date that matches the
// condition, with its session count, or nil.
func meetingWhere(db *gorm.DB, cond string, args ...any) (*meetingJSON, error) {
var m models.Meeting
res := db.Where(cond, args...).Order("starts_on, number").Limit(1).Find(&m)
if res.Error != nil || res.RowsAffected == 0 {
return nil, res.Error
}
var sessions int64
if err := db.Model(&models.ClassSession{}).Where("meeting_id = ?", m.ID).Count(&sessions).Error; err != nil {
return nil, err
}
out := newMeetingJSON(m, sessions)
return &out, nil
}