Skip to content

Commit 6c334e5

Browse files
carddev81corypride
authored andcommitted
feat: add necessary wiring and formatting to make page mirror figma
1 parent 0e8dcd6 commit 6c334e5

File tree

11 files changed

+1160
-409
lines changed

11 files changed

+1160
-409
lines changed

backend/src/database/class_events.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,10 @@ func generateEventInstances(event models.ProgramClassEvent, startDate, endDate t
830830
return eventInstances
831831
}
832832

833+
func GenerateEventInstances(event models.ProgramClassEvent, startDate, endDate time.Time) []models.EventInstance {
834+
return generateEventInstances(event, startDate, endDate)
835+
}
836+
833837
// GetClassEventInstancesWithAttendanceForRecurrence returns all occurrences for events
834838
// for a given class based on each event's recurrence rule (from DTSTART until UNTIL)
835839
// along with their associated attendance records.

backend/src/database/dashboard.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package database
2+
3+
import (
4+
"UnlockEdv2/src/models"
5+
)
6+
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
12+
13+
classQuery := db.WithContext(ctx.Ctx).
14+
Model(&models.ProgramClass{}).
15+
Where("status = ?", models.Active).
16+
Where("archived_at IS NULL")
17+
if facilityID != nil {
18+
classQuery = classQuery.Where("facility_id = ?", *facilityID)
19+
}
20+
21+
if err := classQuery.Count(&activeClasses).Error; err != nil {
22+
return 0, 0, 0, 0, err
23+
}
24+
25+
if err := classQuery.Select("COALESCE(SUM(capacity), 0)").Scan(&totalSeats).Error; err != nil {
26+
return 0, 0, 0, 0, err
27+
}
28+
29+
enrollmentQuery := db.WithContext(ctx.Ctx).
30+
Table("program_class_enrollments e").
31+
Select("COUNT(*)").
32+
Joins("JOIN program_classes c ON c.id = e.class_id").
33+
Where("e.enrollment_status = ?", models.Enrolled).
34+
Where("c.status = ?", models.Active).
35+
Where("c.archived_at IS NULL")
36+
if facilityID != nil {
37+
enrollmentQuery = enrollmentQuery.Where("c.facility_id = ?", *facilityID)
38+
}
39+
if err := enrollmentQuery.Scan(&totalEnrollments).Error; err != nil {
40+
return 0, 0, 0, 0, err
41+
}
42+
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+
`
51+
args := []any{models.Active, models.Enrolled}
52+
if facilityID != nil {
53+
attendanceSQL += "AND c.facility_id = ?\n"
54+
args = append(args, *facilityID)
55+
}
56+
attendanceSQL += `
57+
AND (
58+
select count(*)
59+
from program_class_events evt
60+
inner join program_class_event_attendance att on att.event_id = evt.id
61+
where evt.class_id = c.id
62+
and att.user_id = e.user_id
63+
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
68+
}
69+
70+
return activeClasses, totalEnrollments, totalSeats, attendanceConcerns, nil
71+
}

backend/src/database/events_attendance.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,68 @@ func (db *DB) GetMissingAttendance(classID int, args *models.QueryContext) (int,
399399
return missingAttendanceCount, nil
400400
}
401401

402+
func (db *DB) GetActiveClassesForMissingAttendance(args *models.QueryContext, facilityID *uint) ([]models.MissingAttendanceClass, error) {
403+
var missClasses []models.MissingAttendanceClass
404+
classQuery := db.WithContext(args.Ctx).
405+
Table("program_classes c").
406+
Select("c.id, c.name, f.name as facility_name").
407+
Joins("JOIN facilities f ON f.id = c.facility_id").
408+
Where("c.status = ?", models.Active).
409+
Where("c.archived_at IS NULL")
410+
if facilityID != nil {
411+
classQuery = classQuery.Where("c.facility_id = ?", *facilityID)
412+
}
413+
if err := classQuery.Find(&missClasses).Error; err != nil {
414+
return nil, newGetRecordsDBError(err, "program_classes")
415+
}
416+
return missClasses, nil
417+
}
418+
419+
func (db *DB) GetClassEventsWithOverrides(args *models.QueryContext, classIDs []uint) ([]models.ProgramClassEvent, error) {
420+
var events []models.ProgramClassEvent
421+
if err := db.WithContext(args.Ctx).
422+
Model(&models.ProgramClassEvent{}).
423+
Preload("Overrides").
424+
Preload("RoomRef").
425+
Where("class_id IN ?", classIDs).
426+
Find(&events).Error; err != nil {
427+
return nil, newGetRecordsDBError(err, "program_class_events")
428+
}
429+
return events, nil
430+
}
431+
432+
type AttendanceCount struct {
433+
EventID uint `json:"event_id"`
434+
Date string `json:"date"`
435+
Count int64 `json:"count"`
436+
}
437+
438+
func (db *DB) GetAttendanceCountsForEvents(args *models.QueryContext, eventIDs []uint, dates []string) ([]AttendanceCount, error) {
439+
var attendanceCounts []AttendanceCount
440+
if err := db.WithContext(args.Ctx).
441+
Model(&models.ProgramClassEventAttendance{}).
442+
Select("event_id, date, COUNT(*) as count").
443+
Where("event_id IN ? AND date IN ?", eventIDs, dates).
444+
Group("event_id, date").
445+
Scan(&attendanceCounts).Error; err != nil {
446+
return nil, newGetRecordsDBError(err, "program_class_event_attendance")
447+
}
448+
return attendanceCounts, nil
449+
}
450+
451+
func (db *DB) GetActiveEnrollmentsForClasses(args *models.QueryContext, classIDs []uint) ([]models.ProgramClassEnrollment, error) {
452+
var enrollments []models.ProgramClassEnrollment
453+
if err := db.WithContext(args.Ctx).
454+
Model(&models.ProgramClassEnrollment{}).
455+
Select("class_id, enrolled_at, enrollment_ended_at").
456+
Where("class_id IN ?", classIDs).
457+
Where("enrollment_status = ?", models.Enrolled).
458+
Find(&enrollments).Error; err != nil {
459+
return nil, newGetRecordsDBError(err, "program_class_enrollments")
460+
}
461+
return enrollments, nil
462+
}
463+
402464
func (db *DB) CreateAttendanceAuditTrail(ctx context.Context, att *models.ProgramClassEventAttendance, adminID *uint, className string) error {
403465

404466
sessionDateParsed, err := time.ParseInLocation("2006-01-02", att.Date, time.Local)

backend/src/database/program_classes.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"context"
11+
1112
log "github.com/sirupsen/logrus"
1213
"github.com/teambition/rrule-go"
1314
"gorm.io/gorm"
@@ -65,7 +66,12 @@ func (db *DB) GetClassesForFacility(args *models.QueryContext) ([]models.Program
6566
if err := tx.Count(&args.Total).Error; err != nil {
6667
return nil, newGetRecordsDBError(err, "program classes")
6768
}
68-
if err := tx.Preload("Events").Preload("Events.RoomRef").Limit(args.PerPage).Offset(args.CalcOffset()).Find(&content).Error; err != nil {
69+
70+
tx = tx.Preload("Events").Preload("Events.RoomRef")
71+
if !args.All {
72+
tx = tx.Limit(args.PerPage).Offset(args.CalcOffset())
73+
}
74+
if err := tx.Find(&content).Error; err != nil {
6975
return nil, newGetRecordsDBError(err, "program classes")
7076
}
7177
return content, nil

backend/src/handlers/classes_handler.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handlers
33
import (
44
"UnlockEdv2/src/database"
55
"UnlockEdv2/src/models"
6+
"UnlockEdv2/src/services"
67
"encoding/json"
78
"fmt"
89
"io"
@@ -41,6 +42,7 @@ func (srv *Server) registerClassesRoutes() []routeDef {
4142
validatedFeatureRoute("GET /api/program-classes/{class_id}", srv.handleGetClass, axx, resolver),
4243
adminValidatedFeatureRoute("GET /api/programs/{program_id}/classes/outcomes", srv.handleGetProgramClassOutcomes, axx, validateFacility("")),
4344
adminValidatedFeatureRoute("GET /api/program-classes/{class_id}/attendance-flags", srv.handleGetAttendanceFlagsForClass, axx, resolver),
45+
adminFeatureRoute("GET /api/program-classes/missing-attendance", srv.handleGetMissingAttendanceForFacility, axx),
4446
adminValidatedFeatureRoute("GET /api/program-classes/{class_id}/missing-attendance", srv.handleGetMissingAttendance, axx, resolver),
4547
adminValidatedFeatureRoute("GET /api/program-classes/{class_id}/attendance-rate", srv.handleGetCumulativeAttendanceRate, axx, resolver),
4648
adminValidatedFeatureRoute("GET /api/program-classes/{class_id}/history", srv.handleGetClassHistory, axx, resolver),
@@ -331,6 +333,49 @@ func (srv *Server) handleGetMissingAttendance(w http.ResponseWriter, r *http.Req
331333
return writeJsonResponse(w, http.StatusOK, totalMissing)
332334
}
333335

336+
func (srv *Server) handleGetMissingAttendanceForFacility(w http.ResponseWriter, r *http.Request, log sLog) error {
337+
args := srv.getQueryContext(r)
338+
facility := r.URL.Query().Get("facility")
339+
var facilityId *uint
340+
switch facility {
341+
case "all":
342+
facilityId = nil
343+
case "":
344+
facilityId = &args.FacilityID
345+
default:
346+
facilityId = &args.FacilityID
347+
}
348+
349+
days := 3
350+
if daysQuery := r.URL.Query().Get("days"); daysQuery != "" {
351+
if parsedDays, err := strconv.Atoi(daysQuery); err == nil {
352+
days = parsedDays
353+
}
354+
}
355+
356+
service := services.NewClassesService(srv.Db)
357+
items, err := service.GetMissingAttendanceForFacility(&args, facilityId, days)
358+
if err != nil {
359+
return newDatabaseServiceError(err)
360+
}
361+
362+
args.Total = int64(len(items))
363+
if !args.All {
364+
start := args.CalcOffset()
365+
if start < len(items) {
366+
end := start + args.PerPage
367+
if end > len(items) {
368+
end = len(items)
369+
}
370+
items = items[start:end]
371+
} else {
372+
items = []models.MissingAttendanceItem{}
373+
}
374+
}
375+
376+
return writePaginatedResponse(w, http.StatusOK, items, args.IntoMeta())
377+
}
378+
334379
func (srv *Server) handleGetCumulativeAttendanceRate(w http.ResponseWriter, r *http.Request, log sLog) error {
335380
classID, err := strconv.Atoi(r.PathValue("class_id"))
336381
if err != nil {

backend/src/handlers/dashboard.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func (srv *Server) registerDashboardRoutes() []routeDef {
1818
resolver := UserRoleResolver("id")
1919
return []routeDef{
2020
newAdminRoute("GET /api/department-metrics", srv.handleDepartmentMetrics),
21+
newAdminRoute("GET /api/dashboard/class-metrics", srv.handleClassDashboardMetrics),
2122
newAdminRoute("GET /api/users/{id}/admin-layer2", srv.handleAdminLayer2),
2223
validatedFeatureRoute("GET /api/users/{id}/catalog", srv.handleUserCatalog, axx, resolver),
2324
validatedFeatureRoute("GET /api/users/{id}/courses", srv.handleUserCourses, axx, resolver),
@@ -132,6 +133,13 @@ type DashboardMetrics struct {
132133
PeakLoginTimes []models.LoginActivity `json:"peak_login_times"`
133134
}
134135

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+
135143
func (srv *Server) handleDepartmentMetrics(w http.ResponseWriter, r *http.Request, log sLog) error {
136144
args := srv.getQueryContext(r)
137145
facility := r.URL.Query().Get("facility")
@@ -237,6 +245,37 @@ func (srv *Server) handleDepartmentMetrics(w http.ResponseWriter, r *http.Reques
237245
return writeJsonResponse(w, http.StatusOK, cachedData)
238246
}
239247

248+
func (srv *Server) handleClassDashboardMetrics(w http.ResponseWriter, r *http.Request, log sLog) error {
249+
args := srv.getQueryContext(r)
250+
facility := r.URL.Query().Get("facility")
251+
var facilityId *uint
252+
switch facility {
253+
case "all":
254+
facilityId = nil
255+
default:
256+
facilityId = &args.FacilityID
257+
}
258+
259+
activeClasses, totalEnrollments, totalSeats, attendanceConcerns, err := srv.Db.GetClassDashboardMetrics(&args, facilityId)
260+
if err != nil {
261+
return newDatabaseServiceError(err)
262+
}
263+
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,
274+
}
275+
276+
return writeJsonResponse(w, http.StatusOK, response)
277+
}
278+
240279
/**
241280
* GET: /api/users/{id}/catalog
242281
* @Query Params:

backend/src/models/class_event.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,21 @@ type AttendanceFlag struct {
255255
FlagType AttendanceFlagType `json:"flag_type"`
256256
}
257257

258+
type MissingAttendanceItem struct {
259+
ClassID uint `json:"class_id"`
260+
ClassName string `json:"class_name"`
261+
FacilityName string `json:"facility_name,omitempty"`
262+
EventID uint `json:"event_id"`
263+
Date string `json:"date"`
264+
StartTime string `json:"start_time"`
265+
}
266+
267+
type MissingAttendanceClass struct {
268+
ID uint `json:"id"`
269+
Name string `json:"name"`
270+
FacilityName string `json:"facility_name"`
271+
}
272+
258273
type EventDates struct {
259274
EventID uint `json:"event_id"`
260275
Date string `json:"date"`

0 commit comments

Comments
 (0)