Skip to content

Commit ae0a9eb

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

File tree

8 files changed

+303
-47
lines changed

8 files changed

+303
-47
lines changed

SESSION_STATE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Session State
2+
3+
## Current status
4+
- Facility dashboard aligned with Carolina: metrics, missing attendance, today's schedule, grey background, spacing fixes.
5+
- Missing attendance service uses DST-safe canonical time and filters future occurrences.
6+
- Department admin now pulls all facilities' classes via `facility=all` support.
7+
- Facility health heading layout matches Carolina (heading outside table card).
8+
9+
## Key file changes
10+
### Backend
11+
- `backend/src/database/dashboard.go`: attendance concerns now based only on 3+ unexcused absences.
12+
- `backend/src/services/classes.go`: missing attendance business logic, DST canonical time, future filter.
13+
- `backend/src/handlers/classes_handler.go`: missing attendance endpoint uses service; `/api/program-classes` supports `facility=all`.
14+
- `backend/src/database/events_attendance.go`: query helpers for missing attendance.
15+
- `backend/src/database/class_events.go`: exported `GenerateEventInstances`.
16+
- `backend/src/database/program_classes.go`: added `GetClasses` with optional facility; `GetClassesForFacility` delegates.
17+
18+
### Frontend
19+
- `frontend-v2/src/pages/Dashboard.tsx`: metrics from `/api/dashboard/class-metrics`, missing attendance wiring, today’s schedule attendance route fix, layout/spacing updates, dept admin classes fetch uses `facility=all`, memoization for `allClasses`/`programs`.
20+
21+
## Current endpoints in use
22+
- `GET /api/dashboard/class-metrics` (dept admin uses `?facility=all`)
23+
- `GET /api/program-classes?facility=all&all=true` (dept admin)
24+
- `GET /api/program-classes/missing-attendance?facility=all&days=3&all=true`
25+
26+
## Next steps (if needed)
27+
1) Add a department-admin backend endpoint for per-facility health metrics (program count, missing attendance, attendance concerns) and wire `FacilityHealthTable` to it.
28+
2) Final layout adjustments to department admin view to match Carolina pixel-for-pixel if any remain.

backend/src/database/events_attendance.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,24 @@ func (db *DB) GetActiveClassesForMissingAttendance(args *models.QueryContext, fa
416416
return missClasses, nil
417417
}
418418

419+
func (db *DB) GetActiveClassesWithEvents(args *models.QueryContext, facilityID *uint) ([]models.ProgramClass, error) {
420+
var classes []models.ProgramClass
421+
classQuery := db.WithContext(args.Ctx).
422+
Model(&models.ProgramClass{}).
423+
Preload("Events.Overrides").
424+
Preload("Events.RoomRef").
425+
Preload("Facility").
426+
Where("status = ?", models.Active).
427+
Where("archived_at IS NULL")
428+
if facilityID != nil {
429+
classQuery = classQuery.Where("facility_id = ?", *facilityID)
430+
}
431+
if err := classQuery.Find(&classes).Error; err != nil {
432+
return nil, newGetRecordsDBError(err, "program_classes")
433+
}
434+
return classes, nil
435+
}
436+
419437
func (db *DB) GetClassEventsWithOverrides(args *models.QueryContext, classIDs []uint) ([]models.ProgramClassEvent, error) {
420438
var events []models.ProgramClassEvent
421439
if err := db.WithContext(args.Ctx).

backend/src/handlers/class_events.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handlers
33
import (
44
"UnlockEdv2/src"
55
"UnlockEdv2/src/models"
6+
"UnlockEdv2/src/services"
67
"encoding/json"
78
"errors"
89
"net/http"
@@ -17,6 +18,7 @@ func (srv *Server) registerClassEventsRoutes() []routeDef {
1718
featureRoute("GET /api/program-classes/{class_id}/events", srv.handleGetProgramClassEvents, axx),
1819
/* admin */
1920
adminFeatureRoute("GET /api/admin-calendar", srv.handleGetAdminCalendar, axx),
21+
adminFeatureRoute("GET /api/program-classes/todays-schedule", srv.handleGetTodaysSchedule, axx),
2022
adminValidatedFeatureRoute("PUT /api/program-classes/{class_id}/events/{event_id}", srv.handleEventOverrides, axx, resolver),
2123
adminValidatedFeatureRoute("DELETE /api/program-classes/{class_id}/events/{event_override_id}", srv.handleDeleteEventOverride, axx, resolver),
2224
adminValidatedFeatureRoute("POST /api/program-classes/{class_id}/events", srv.handleCreateEvent, axx, resolver),
@@ -59,6 +61,40 @@ func (srv *Server) handleGetStudentCalendar(w http.ResponseWriter, r *http.Reque
5961
return writeJsonResponse(w, http.StatusOK, events)
6062
}
6163

64+
func (srv *Server) handleGetTodaysSchedule(w http.ResponseWriter, r *http.Request, log sLog) error {
65+
args := srv.getQueryContext(r)
66+
facility := r.URL.Query().Get("facility")
67+
var facilityId *uint
68+
switch facility {
69+
case "all":
70+
facilityId = nil
71+
default:
72+
facilityId = &args.FacilityID
73+
}
74+
75+
service := services.NewClassesService(srv.Db)
76+
items, err := service.GetTodaysSchedule(&args, facilityId)
77+
if err != nil {
78+
return newDatabaseServiceError(err)
79+
}
80+
81+
args.Total = int64(len(items))
82+
if !args.All {
83+
start := args.CalcOffset()
84+
if start < len(items) {
85+
end := start + args.PerPage
86+
if end > len(items) {
87+
end = len(items)
88+
}
89+
items = items[start:end]
90+
} else {
91+
items = []models.TodaysScheduleItem{}
92+
}
93+
}
94+
95+
return writePaginatedResponse(w, http.StatusOK, items, args.IntoMeta())
96+
}
97+
6298
func (srv *Server) handleEventOverrides(w http.ResponseWriter, r *http.Request, log sLog) error {
6399
eventId, err := strconv.Atoi(r.PathValue("event_id"))
64100
if err != nil {

backend/src/models/program_classes.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ type ProgramClass struct {
5050

5151
func (ProgramClass) TableName() string { return "program_classes" }
5252

53+
type TodaysScheduleItem struct {
54+
ClassID uint `json:"class_id"`
55+
ClassName string `json:"class_name"`
56+
InstructorName string `json:"instructor_name"`
57+
FacilityID uint `json:"facility_id"`
58+
FacilityName string `json:"facility_name"`
59+
EventID uint `json:"event_id"`
60+
Date string `json:"date"`
61+
StartTime string `json:"start_time"`
62+
Room string `json:"room"`
63+
}
64+
5365
func (c *ProgramClass) BeforeCreate(tx *gorm.DB) error {
5466
if err := c.DatabaseFields.BeforeCreate(tx); err != nil {
5567
return err

backend/src/services/classes.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"sort"
88
"time"
9+
10+
"github.com/teambition/rrule-go"
911
)
1012

1113
type ClassesService struct {
@@ -185,3 +187,113 @@ func (svc *ClassesService) GetMissingAttendanceForFacility(args *models.QueryCon
185187

186188
return items, nil
187189
}
190+
191+
func (svc *ClassesService) GetTodaysSchedule(args *models.QueryContext, facilityID *uint) ([]models.TodaysScheduleItem, error) {
192+
classes, err := svc.db.GetActiveClassesWithEvents(args, facilityID)
193+
if err != nil {
194+
return nil, err
195+
}
196+
if len(classes) == 0 {
197+
return []models.TodaysScheduleItem{}, nil
198+
}
199+
200+
loc, err := time.LoadLocation(args.Timezone)
201+
if err != nil {
202+
loc = time.UTC
203+
}
204+
nowLocal := time.Now().In(loc)
205+
startOfToday := time.Date(nowLocal.Year(), nowLocal.Month(), nowLocal.Day(), 0, 0, 0, 0, loc)
206+
endOfToday := startOfToday.AddDate(0, 0, 1)
207+
208+
items := make([]models.TodaysScheduleItem, 0)
209+
for _, class := range classes {
210+
startDt := class.StartDt.In(loc)
211+
if startDt.After(endOfToday) {
212+
continue
213+
}
214+
if class.EndDt != nil {
215+
endDt := class.EndDt.In(loc)
216+
if endDt.Before(startOfToday) {
217+
continue
218+
}
219+
}
220+
221+
for _, event := range class.Events {
222+
rule, err := event.GetRRuleWithTimezone(args.Timezone)
223+
if err != nil {
224+
continue
225+
}
226+
firstOccurrence := rule.After(time.Time{}, false)
227+
canonicalHour, canonicalMinute := getCanonicalHourAndMinute([]time.Time{firstOccurrence}, args.Timezone)
228+
overrideStartTimes := make(map[string]struct{})
229+
for _, override := range event.Overrides {
230+
if override.IsCancelled {
231+
continue
232+
}
233+
overrideOptions, err := rrule.StrToROption(override.OverrideRrule)
234+
if err != nil {
235+
continue
236+
}
237+
overrideOptions.Dtstart = overrideOptions.Dtstart.In(time.UTC)
238+
overrideRule, err := rrule.NewRRule(*overrideOptions)
239+
if err != nil {
240+
continue
241+
}
242+
overrideOccurrences := overrideRule.Between(startOfToday.UTC(), endOfToday.UTC(), true)
243+
for _, occ := range overrideOccurrences {
244+
overrideStartTimes[occ.UTC().Format(time.RFC3339Nano)] = struct{}{}
245+
}
246+
}
247+
eventInstances := database.GenerateEventInstances(event, startOfToday, endOfToday)
248+
for _, inst := range eventInstances {
249+
if inst.IsCancelled {
250+
continue
251+
}
252+
localStart := inst.StartTime.In(loc)
253+
localOcc := localStart
254+
if _, isOverride := overrideStartTimes[inst.StartTime.UTC().Format(time.RFC3339Nano)]; !isOverride {
255+
localOcc = time.Date(
256+
localStart.Year(),
257+
localStart.Month(),
258+
localStart.Day(),
259+
canonicalHour,
260+
canonicalMinute,
261+
0,
262+
0,
263+
loc,
264+
)
265+
}
266+
if localOcc.Before(startOfToday) || !localOcc.Before(endOfToday) {
267+
continue
268+
}
269+
facilityName := ""
270+
if class.Facility != nil {
271+
facilityName = class.Facility.Name
272+
}
273+
items = append(items, models.TodaysScheduleItem{
274+
ClassID: class.ID,
275+
ClassName: class.Name,
276+
InstructorName: class.InstructorName,
277+
FacilityID: class.FacilityID,
278+
FacilityName: facilityName,
279+
EventID: inst.EventID,
280+
Date: localOcc.Format("2006-01-02"),
281+
StartTime: localOcc.Format("15:04"),
282+
Room: inst.Room,
283+
})
284+
}
285+
}
286+
}
287+
288+
sort.Slice(items, func(i, j int) bool {
289+
if items[i].Date == items[j].Date { //sorting by date, time, then by name
290+
if items[i].StartTime == items[j].StartTime {
291+
return items[i].ClassName < items[j].ClassName
292+
}
293+
return items[i].StartTime < items[j].StartTime
294+
}
295+
return items[i].Date < items[j].Date
296+
})
297+
298+
return items, nil
299+
}

frontend-v2/src/lib/formatters.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,34 @@ export function getClassSchedule(cls: Class): ClassScheduleInfo {
130130

131131
export function isClassToday(cls: Class): boolean {
132132
const schedule = getClassSchedule(cls);
133-
const today = new Date().toLocaleDateString('en-US', { weekday: 'long' });
134-
return schedule.days.includes(today);
133+
const now = new Date();
134+
const todayName = now.toLocaleDateString('en-US', { weekday: 'long' });
135+
if (!schedule.days.includes(todayName)) {
136+
return false;
137+
}
138+
139+
const startOfToday = new Date(
140+
now.getFullYear(),
141+
now.getMonth(),
142+
now.getDate()
143+
);
144+
const endOfToday = new Date(
145+
now.getFullYear(),
146+
now.getMonth(),
147+
now.getDate(),
148+
23,
149+
59,
150+
59,
151+
999
152+
);
153+
const start = cls.start_dt ? new Date(cls.start_dt) : null;
154+
const end = cls.end_dt ? new Date(cls.end_dt) : null;
155+
const startOk =
156+
!start || Number.isNaN(start.getTime()) || start <= endOfToday;
157+
const endOk =
158+
!end || Number.isNaN(end.getTime()) || end >= startOfToday;
159+
160+
return startOk && endOk;
135161
}
136162

137163
export function getStatusColor(status: string): string {

0 commit comments

Comments
 (0)