Skip to content

Commit 3becb6f

Browse files
committed
feat: add necessary wiring and formatting to make department admin dashboard page mirror figma
1 parent ae0a9eb commit 3becb6f

File tree

10 files changed

+356
-282
lines changed

10 files changed

+356
-282
lines changed

SESSION_STATE.md

Lines changed: 0 additions & 28 deletions
This file was deleted.

backend/src/database/dashboard.go

Lines changed: 155 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,68 +4,192 @@ import (
44
"UnlockEdv2/src/models"
55
)
66

7-
func (db *DB) GetClassDashboardMetrics(ctx *models.QueryContext, facilityID *uint) (int64, int64, int64, int64, error) {
8-
var activeClasses int64
9-
var totalEnrollments int64
10-
var totalSeats int64
11-
var attendanceConcerns int64
7+
func (db *DB) GetClassDashboardMetrics(ctx *models.QueryContext, facilityID *uint) (models.ClassDashboardMetrics, error) {
8+
var metrics models.ClassDashboardMetrics
9+
10+
var err error
11+
if metrics.ActiveClasses, err = db.GetActiveClassCount(ctx, facilityID); err != nil {
12+
return metrics, err
13+
}
14+
if metrics.TotalSeats, err = db.GetTotalSeatCount(ctx, facilityID); err != nil {
15+
return metrics, err
16+
}
17+
if metrics.ScheduledClasses, err = db.GetScheduledClassCount(ctx, facilityID); err != nil {
18+
return metrics, err
19+
}
20+
if metrics.TotalEnrollments, err = db.GetTotalEnrollmentsCount(ctx, facilityID); err != nil {
21+
return metrics, err
22+
}
23+
if metrics.AttendanceConcerns, err = db.GetAttendanceConcernsCount(ctx, facilityID); err != nil {
24+
return metrics, err
25+
}
1226

13-
classQuery := db.WithContext(ctx.Ctx).
27+
return metrics, nil
28+
}
29+
30+
func (db *DB) GetActiveClassCount(ctx *models.QueryContext, facilityID *uint) (int64, error) {
31+
var count int64
32+
query := db.WithContext(ctx.Ctx).
1433
Model(&models.ProgramClass{}).
1534
Where("status = ?", models.Active).
1635
Where("archived_at IS NULL")
1736
if facilityID != nil {
18-
classQuery = classQuery.Where("facility_id = ?", *facilityID)
37+
query = query.Where("facility_id = ?", *facilityID)
38+
}
39+
if err := query.Count(&count).Error; err != nil {
40+
return 0, err
1941
}
42+
return count, nil
43+
}
2044

21-
if err := classQuery.Count(&activeClasses).Error; err != nil {
22-
return 0, 0, 0, 0, err
45+
func (db *DB) GetTotalSeatCount(ctx *models.QueryContext, facilityID *uint) (int64, error) {
46+
var totalSeats int64
47+
query := db.WithContext(ctx.Ctx).
48+
Model(&models.ProgramClass{}).
49+
Where("status = ?", models.Active).
50+
Where("archived_at IS NULL")
51+
if facilityID != nil {
52+
query = query.Where("facility_id = ?", *facilityID)
2353
}
54+
if err := query.Select("COALESCE(SUM(capacity), 0)").Scan(&totalSeats).Error; err != nil {
55+
return 0, err
56+
}
57+
return totalSeats, nil
58+
}
2459

25-
if err := classQuery.Select("COALESCE(SUM(capacity), 0)").Scan(&totalSeats).Error; err != nil {
26-
return 0, 0, 0, 0, err
60+
func (db *DB) GetScheduledClassCount(ctx *models.QueryContext, facilityID *uint) (int64, error) {
61+
var count int64
62+
query := db.WithContext(ctx.Ctx).
63+
Model(&models.ProgramClass{}).
64+
Where("status = ?", models.Scheduled).
65+
Where("archived_at IS NULL")
66+
if facilityID != nil {
67+
query = query.Where("facility_id = ?", *facilityID)
68+
}
69+
if err := query.Count(&count).Error; err != nil {
70+
return 0, err
2771
}
72+
return count, nil
73+
}
2874

29-
enrollmentQuery := db.WithContext(ctx.Ctx).
75+
func (db *DB) GetTotalEnrollmentsCount(ctx *models.QueryContext, facilityID *uint) (int64, error) {
76+
var count int64
77+
query := db.WithContext(ctx.Ctx).
3078
Table("program_class_enrollments e").
3179
Select("COUNT(*)").
3280
Joins("JOIN program_classes c ON c.id = e.class_id").
3381
Where("e.enrollment_status = ?", models.Enrolled).
3482
Where("c.status = ?", models.Active).
3583
Where("c.archived_at IS NULL")
3684
if facilityID != nil {
37-
enrollmentQuery = enrollmentQuery.Where("c.facility_id = ?", *facilityID)
85+
query = query.Where("c.facility_id = ?", *facilityID)
3886
}
39-
if err := enrollmentQuery.Scan(&totalEnrollments).Error; err != nil {
40-
return 0, 0, 0, 0, err
87+
if err := query.Scan(&count).Error; err != nil {
88+
return 0, err
4189
}
90+
return count, nil
91+
}
4292

43-
attendanceSQL := `
44-
SELECT COUNT(DISTINCT c.id)
45-
FROM program_classes c
46-
JOIN program_class_enrollments e ON e.class_id = c.id
47-
WHERE c.status = ?
48-
AND c.archived_at IS NULL
49-
AND e.enrollment_status = ?
50-
`
93+
func (db *DB) GetAttendanceConcernsCount(ctx *models.QueryContext, facilityID *uint) (int64, error) {
94+
attendanceSQL := `SELECT COUNT(DISTINCT c.id)
95+
FROM program_classes c
96+
JOIN program_class_enrollments e ON e.class_id = c.id
97+
WHERE c.status = ?
98+
AND c.archived_at IS NULL
99+
AND e.enrollment_status = ? `
51100
args := []any{models.Active, models.Enrolled}
52101
if facilityID != nil {
53-
attendanceSQL += "AND c.facility_id = ?\n"
102+
attendanceSQL += "AND c.facility_id = ? "
54103
args = append(args, *facilityID)
55104
}
56-
attendanceSQL += `
57-
AND (
105+
attendanceSQL += ` AND (
58106
select count(*)
59107
from program_class_events evt
60108
inner join program_class_event_attendance att on att.event_id = evt.id
61109
where evt.class_id = c.id
62110
and att.user_id = e.user_id
63111
and att.attendance_status = 'absent_unexcused'
64-
) >= 3
65-
`
66-
if err := db.WithContext(ctx.Ctx).Raw(attendanceSQL, args...).Scan(&attendanceConcerns).Error; err != nil {
67-
return 0, 0, 0, 0, err
112+
) >= 3 `
113+
var count int64
114+
if err := db.WithContext(ctx.Ctx).Raw(attendanceSQL, args...).Scan(&count).Error; err != nil {
115+
return 0, err
68116
}
117+
return count, nil
118+
}
69119

70-
return activeClasses, totalEnrollments, totalSeats, attendanceConcerns, nil
120+
type facilityCount struct {
121+
FacilityID uint `gorm:"column:facility_id"`
122+
Count int64 `gorm:"column:count"`
123+
}
124+
125+
func (db *DB) GetFacilityHealthSummaries(ctx *models.QueryContext, facilityID *uint) ([]models.FacilityHealthSummary, error) {
126+
var summaries []models.FacilityHealthSummary
127+
query := `SELECT
128+
f.id as facility_id,
129+
f.name as facility_name,
130+
COALESCE(COUNT(DISTINCT p.id), 0) as programs,
131+
COALESCE(COUNT(DISTINCT pc.id), 0) as active_classes,
132+
COALESCE(COUNT(DISTINCT pce.id), 0) as enrollment
133+
FROM facilities f
134+
LEFT JOIN facilities_programs fp
135+
ON fp.facility_id = f.id
136+
AND fp.deleted_at IS NULL
137+
LEFT JOIN programs p
138+
ON p.id = fp.program_id
139+
AND p.is_active = true
140+
AND p.deleted_at IS NULL
141+
AND p.archived_at IS NULL
142+
LEFT JOIN program_classes pc
143+
ON pc.facility_id = f.id
144+
AND pc.status = ?
145+
AND pc.archived_at IS NULL
146+
LEFT JOIN program_class_enrollments pce
147+
ON pce.class_id = pc.id
148+
AND pce.enrollment_status = ?
149+
WHERE f.deleted_at IS NULL `
150+
args := []any{models.Active, models.Enrolled}
151+
if facilityID != nil {
152+
query += " AND f.id = ? "
153+
args = append(args, *facilityID)
154+
}
155+
query += "GROUP BY f.id, f.name ORDER BY f.name"
156+
157+
if err := db.WithContext(ctx.Ctx).Raw(query, args...).Scan(&summaries).Error; err != nil {
158+
return nil, err
159+
}
160+
return summaries, nil
161+
}
162+
163+
func (db *DB) GetFacilityAttendanceConcerns(ctx *models.QueryContext, facilityID *uint) (map[uint]int64, error) {
164+
attendanceSQL := `SELECT c.facility_id as facility_id, COUNT(DISTINCT c.id) as count
165+
FROM program_classes c
166+
JOIN program_class_enrollments e ON e.class_id = c.id
167+
WHERE c.status = ?
168+
AND c.archived_at IS NULL
169+
AND e.enrollment_status = ?`
170+
args := []any{models.Active, models.Enrolled}
171+
if facilityID != nil {
172+
attendanceSQL += "AND c.facility_id = ? "
173+
args = append(args, *facilityID)
174+
}
175+
attendanceSQL += ` AND (
176+
select count(*)
177+
from program_class_events evt
178+
inner join program_class_event_attendance att on att.event_id = evt.id
179+
where evt.class_id = c.id
180+
and att.user_id = e.user_id
181+
and att.attendance_status = 'absent_unexcused'
182+
) >= 3
183+
GROUP BY c.facility_id`
184+
185+
var counts []facilityCount
186+
if err := db.WithContext(ctx.Ctx).Raw(attendanceSQL, args...).Scan(&counts).Error; err != nil {
187+
return nil, err
188+
}
189+
190+
results := make(map[uint]int64, len(counts))
191+
for _, entry := range counts {
192+
results[entry.FacilityID] = entry.Count
193+
}
194+
return results, nil
71195
}

backend/src/database/program_classes.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,15 @@ func (db *DB) GetClassByID(id int) (*models.ProgramClass, error) {
5858
}
5959

6060
func (db *DB) GetClassesForFacility(args *models.QueryContext) ([]models.ProgramClass, error) {
61+
return db.GetClasses(args, &args.FacilityID)
62+
}
63+
64+
func (db *DB) GetClasses(args *models.QueryContext, facilityID *uint) ([]models.ProgramClass, error) {
6165
content := []models.ProgramClass{}
62-
tx := db.WithContext(args.Ctx).Model(&models.ProgramClass{}).Where("facility_id = ?", args.FacilityID)
66+
tx := db.WithContext(args.Ctx).Model(&models.ProgramClass{})
67+
if facilityID != nil {
68+
tx = tx.Where("facility_id = ?", *facilityID)
69+
}
6370
if args.Search != "" {
6471
tx = tx.Where("LOWER(name) LIKE ?", args.SearchQuery())
6572
}

backend/src/handlers/classes_handler.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,15 @@ func (srv *Server) handleGetClass(w http.ResponseWriter, r *http.Request, log sL
9090

9191
func (srv *Server) handleIndexClassesForFacility(w http.ResponseWriter, r *http.Request, log sLog) error {
9292
args := srv.getQueryContext(r)
93-
classes, err := srv.Db.GetClassesForFacility(&args)
93+
claims := r.Context().Value(ClaimsKey).(*Claims)
94+
facilityParam := r.URL.Query().Get("facility")
95+
var facilityID *uint
96+
if facilityParam == "all" && claims.canSwitchFacility() {
97+
facilityID = nil
98+
} else {
99+
facilityID = &args.FacilityID
100+
}
101+
classes, err := srv.Db.GetClasses(&args, facilityID)
94102
if err != nil {
95103
return newDatabaseServiceError(err)
96104
}

backend/src/handlers/dashboard.go

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package handlers
22

33
import (
44
"UnlockEdv2/src/models"
5+
"UnlockEdv2/src/services"
56
"encoding/json"
67
"errors"
78
"fmt"
@@ -19,6 +20,7 @@ func (srv *Server) registerDashboardRoutes() []routeDef {
1920
return []routeDef{
2021
newAdminRoute("GET /api/department-metrics", srv.handleDepartmentMetrics),
2122
newAdminRoute("GET /api/dashboard/class-metrics", srv.handleClassDashboardMetrics),
23+
newAdminRoute("GET /api/dashboard/facility-health", srv.handleFacilityHealthSummary),
2224
newAdminRoute("GET /api/users/{id}/admin-layer2", srv.handleAdminLayer2),
2325
validatedFeatureRoute("GET /api/users/{id}/catalog", srv.handleUserCatalog, axx, resolver),
2426
validatedFeatureRoute("GET /api/users/{id}/courses", srv.handleUserCourses, axx, resolver),
@@ -133,13 +135,6 @@ type DashboardMetrics struct {
133135
PeakLoginTimes []models.LoginActivity `json:"peak_login_times"`
134136
}
135137

136-
type ClassDashboardMetrics struct {
137-
ActiveClasses int64 `json:"active_classes"`
138-
TotalEnrollments int64 `json:"total_enrollments"`
139-
TotalSeats int64 `json:"total_seats"`
140-
AttendanceConcerns int64 `json:"attendance_concerns"`
141-
}
142-
143138
func (srv *Server) handleDepartmentMetrics(w http.ResponseWriter, r *http.Request, log sLog) error {
144139
args := srv.getQueryContext(r)
145140
facility := r.URL.Query().Get("facility")
@@ -256,24 +251,47 @@ func (srv *Server) handleClassDashboardMetrics(w http.ResponseWriter, r *http.Re
256251
facilityId = &args.FacilityID
257252
}
258253

259-
activeClasses, totalEnrollments, totalSeats, attendanceConcerns, err := srv.Db.GetClassDashboardMetrics(&args, facilityId)
254+
metrics, err := srv.Db.GetClassDashboardMetrics(&args, facilityId)
260255
if err != nil {
261256
return newDatabaseServiceError(err)
262257
}
263258

264-
response := struct {
265-
ActiveClasses int64 `json:"active_classes"`
266-
TotalEnrollments int64 `json:"total_enrollments"`
267-
TotalSeats int64 `json:"total_seats"`
268-
AttendanceConcerns int64 `json:"attendance_concerns"`
269-
}{
270-
ActiveClasses: activeClasses,
271-
TotalEnrollments: totalEnrollments,
272-
TotalSeats: totalSeats,
273-
AttendanceConcerns: attendanceConcerns,
259+
return writeJsonResponse(w, http.StatusOK, metrics)
260+
}
261+
262+
func (srv *Server) handleFacilityHealthSummary(w http.ResponseWriter, r *http.Request, log sLog) error {
263+
args := srv.getQueryContext(r)
264+
claims := r.Context().Value(ClaimsKey).(*Claims)
265+
facility := r.URL.Query().Get("facility")
266+
var facilityID *uint
267+
switch facility {
268+
case "all":
269+
facilityID = nil
270+
case "":
271+
facilityID = &claims.FacilityID
272+
default:
273+
facilityIDInt, err := strconv.Atoi(facility)
274+
if err != nil {
275+
return newInvalidIdServiceError(err, "facility")
276+
}
277+
ref := uint(facilityIDInt)
278+
facilityID = &ref
274279
}
275280

276-
return writeJsonResponse(w, http.StatusOK, response)
281+
days := 3
282+
if daysQuery := r.URL.Query().Get("days"); daysQuery != "" {
283+
if parsedDays, err := strconv.Atoi(daysQuery); err == nil {
284+
days = parsedDays
285+
}
286+
}
287+
288+
service := services.NewClassesService(srv.Db)
289+
summaries, err := service.GetFacilityHealthSummaries(&args, facilityID, days)
290+
if err != nil {
291+
return newDatabaseServiceError(err)
292+
}
293+
294+
return writeJsonResponse(w, http.StatusOK, summaries)
277295
}
278296

279297
/**

backend/src/models/dashboard.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,21 @@ type CachedDashboard[T any] struct {
4141
LastCache time.Time `json:"last_cache"`
4242
Data T `json:"data"`
4343
}
44+
45+
type FacilityHealthSummary struct {
46+
FacilityID uint `json:"facility_id" gorm:"column:facility_id"`
47+
FacilityName string `json:"facility_name" gorm:"column:facility_name"`
48+
Programs int64 `json:"programs" gorm:"column:programs"`
49+
ActiveClasses int64 `json:"active_classes" gorm:"column:active_classes"`
50+
Enrollment int64 `json:"enrollment" gorm:"column:enrollment"`
51+
MissingAttendance int64 `json:"missing_attendance" gorm:"column:missing_attendance"`
52+
AttendanceConcerns int64 `json:"attendance_concerns" gorm:"column:attendance_concerns"`
53+
}
54+
55+
type ClassDashboardMetrics struct {
56+
ActiveClasses int64 `json:"active_classes"`
57+
ScheduledClasses int64 `json:"scheduled_classes"`
58+
TotalEnrollments int64 `json:"total_enrollments"`
59+
TotalSeats int64 `json:"total_seats"`
60+
AttendanceConcerns int64 `json:"attendance_concerns"`
61+
}

0 commit comments

Comments
 (0)