Repository: UnlockedLabs/UnlockEdv2
Pull request #844: feat: add programs history tables, update program management page
Author: login: calisio
Labels: name: frontend
name: Backend
name: CronTasks
name: ProviderMiddleware
name: HAS_MIGRATION
Created: 2025-04-22T19:42:18Z | Merged: 2025-04-30T17:46:42Z

## Description of the change

Draft PR for now, just to give insight into tables / cron jobs that are made for now

- **Related issues**: Closes asana ticket [View All Programs for Department & Facility](https://app.asana.com/1/1201607307149189/project/1209460078641109/task/1209485911445017?focus=true)

## Screenshot(s)

<img width="1512" alt="Screenshot 2025-04-23 at 7 36 00 PM" src="https://github.com/user-attachments/assets/9216f8a1-5002-41e8-8e05-355351a8cbb8" />



Diff:
diff --git a/backend/migrations/00046_add_program_history.sql b/backend/migrations/00046_add_program_history.sql
new file mode 100644
index 000000000..8a81c9d47
--- /dev/null
+++ b/backend/migrations/00046_add_program_history.sql
@@ -0,0 +1,53 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
+    date DATE NOT NULL DEFAULT CURRENT_DATE,
+    total_programs INTEGER NOT NULL,
+    total_active_programs INTEGER NOT NULL,
+    total_archived_programs INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_program_offerings INTEGER NOT NULL,
+    total_facilities INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE INDEX idx_daily_programs_facilities_history_date ON public.daily_programs_facilities_history USING btree (date DESC);
+CREATE TABLE public.daily_program_facilities_history (
+    date DATE NOT NULL DEFAULT CURRENT_DATE,
+    program_id INTEGER NOT NULL,
+    total_active_facilities INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_active_enrollments INTEGER NOT NULL,
+    total_classes INTEGER NOT NULL,
+    total_archived_classes INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE INDEX idx_daily_program_facilities_history_date_program_id ON public.daily_program_facilities_history USING btree (date DESC, program_id);
+CREATE TABLE public.daily_program_facility_history (
+    date DATE NOT NULL DEFAULT CURRENT_DATE,
+    program_id INTEGER NOT NULL,
+    facility_id INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_active_enrollments INTEGER NOT NULL,
+    total_classes INTEGER NOT NULL,
+    total_archived_classes INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE INDEX idx_daily_program_facility_history_history_date_program_id_facility_id ON public.daily_program_facility_history USING btree (date DESC, program_id, facility_id);
+
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+ALTER TABLE public.programs DROP COLUMN archived_at;
+DROP TABLE public.daily_programs_facilities_history;
+DROP TABLE public.daily_program_facilities_history;
+DROP TABLE public.daily_program_facility_history;
+-- +goose StatementEnd
+
diff --git a/backend/src/database/open_content.go b/backend/src/database/open_content.go
index 8f4b6187a..87c88feb9 100644
--- a/backend/src/database/open_content.go
+++ b/backend/src/database/open_content.go
@@ -350,13 +350,17 @@ func (db *DB) GetTopUserOpenContent(id int, args *models.QueryContext) ([]models
 	return content, nil
 }
 
-func (db *DB) GetTopFacilityLibraries(id int, perPage int, days int) ([]models.OpenContentItem, error) {
+func (db *DB) GetTopFacilityLibraries(args *models.QueryContext, id int, perPage int, days int) ([]models.OpenContentItem, error) {
 	libraries := make([]models.OpenContentItem, 0, perPage)
 	daysAgo := time.Now().AddDate(0, 0, -days)
-	if err := db.Table("open_content_activities oca").
+	tx := db.WithContext(args.Ctx).Table("open_content_activities oca").
 		Select("l.title, l.url, l.thumbnail_url as thumbnail_url, l.open_content_provider_id, l.id as content_id, 'library' as type, count(l.id) as visits").
 		Joins("JOIN libraries l on l.id = oca.content_id AND l.open_content_provider_id = oca.open_content_provider_id and l.deleted_at is null").
-		Where("oca.facility_id = ? AND oca.request_ts >= ?", id, daysAgo).
+		Where("oca.facility_id = ?", id)
+	if days != -1 {
+		tx = tx.Where("oca.request_ts >= ?", daysAgo)
+	}
+	if err := tx.
 		Group("l.title, l.url, l.thumbnail_url, l.open_content_provider_id, l.id").
 		Order("visits DESC").
 		Limit(perPage).
diff --git a/backend/src/database/programs.go b/backend/src/database/programs.go
index be9de6ea5..2ee5a6f05 100644
--- a/backend/src/database/programs.go
+++ b/backend/src/database/programs.go
@@ -3,6 +3,8 @@ package database
 import (
 	"UnlockEdv2/src"
 	"UnlockEdv2/src/models"
+	"fmt"
+	"time"
 )
 
 type EnrollmentAndCompletionMetrics struct {
@@ -12,7 +14,6 @@ type EnrollmentAndCompletionMetrics struct {
 	CompletionRate    float64 `json:"completion_rate"`
 }
 
-
 func (db *DB) GetProgramByID(id int) (*models.Program, error) {
 	content := &models.Program{}
 	if err := db.Preload("Facilities").First(content, id).Error; err != nil {
@@ -123,3 +124,127 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&programsFacilitiesStats).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = &totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = &totals.TotalEnrollments
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsFacilityStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilityStats models.ProgramsFacilitiesStats
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramFacilityHistory{}).
+		Select(`
+		COUNT(*) AS total_programs,
+		SUM(total_enrollments) AS total_enrollments,
+		SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0) * 100 AS attendance_rate,
+		SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0) * 100 AS completion_rate
+	`).Where("facility_id = ?", args.FacilityID)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&programsFacilityStats).Error; err != nil {
+		return programsFacilityStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	return programsFacilityStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool, adminRole models.UserRole) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	var tableName string
+	var totalActiveFacilitiesQuery string
+	var totalActiveFacilitiesSubQuery string
+	var facilitySubQueryFilter string
+	if adminRole == models.FacilityAdmin {
+		tableName = "daily_program_facility_history"
+		facilitySubQueryFilter = fmt.Sprintf("AND facility_id = %d", args.FacilityID)
+	} else {
+		tableName = "daily_program_facilities_history"
+		totalActiveFacilitiesQuery = "mr.total_active_facilities,"
+		totalActiveFacilitiesSubQuery = "total_active_facilities,"
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.Program{}).
+		Select(fmt.Sprintf(`
+			programs.id AS program_id,
+			programs.name AS program_name,
+			programs.archived_at AS archived_at,
+			%s
+			mr.total_enrollments AS total_enrollments,
+			mr.total_active_enrollments AS total_active_enrollments,
+			mr.total_classes AS total_classes,
+			(SUM(total_completions) * 1.0 / NULLIF(SUM(dpfh.total_enrollments), 0)) * 100 AS completion_rate,
+			(SUM(total_students_present) * 1.0 / NULLIF(SUM(dpfh.total_attendances_marked), 0)) * 100 AS attendance_rate,
+			STRING_AGG(DISTINCT pt.program_type::text, ',') AS program_types,
+			STRING_AGG(DISTINCT pct.credit_type::text, ',') AS credit_types,
+			programs.funding_type AS funding_type,
+			BOOL_OR(programs.is_active) AS status
+		`, totalActiveFacilitiesQuery))
+	if timeFilter > 0 {
+		joinCondition := fmt.Sprintf(`LEFT JOIN %s AS dpfh ON dpfh.program_id = programs.id AND dpfh.date >= ?`, tableName)
+		tx = tx.Joins(joinCondition, time.Now().AddDate(0, 0, -timeFilter))
+	} else {
+		tx = tx.Joins(fmt.Sprintf(`LEFT JOIN %s AS dpfh ON dpfh.program_id = programs.id`, tableName))
+	}
+	tx = tx.Joins(fmt.Sprintf(`
+			LEFT JOIN (
+				SELECT 
+					program_id,
+					%s
+					total_enrollments,
+					total_active_enrollments,
+					total_classes
+				FROM %s
+				WHERE date = (SELECT MAX(date) FROM %s)
+				%s
+			) AS mr ON mr.program_id = programs.id
+		`, totalActiveFacilitiesSubQuery, tableName, tableName, facilitySubQueryFilter))
+	tx = tx.Joins("JOIN program_types pt ON pt.program_id = programs.id").
+		Joins("JOIN program_credit_types pct ON pct.program_id = programs.id").
+		Group(totalActiveFacilitiesQuery + "programs.id, programs.name, mr.total_enrollments, mr.total_active_enrollments, mr.total_classes, programs.funding_type")
+	if adminRole == models.FacilityAdmin {
+		tx = tx.Joins("JOIN facilities_programs fp ON fp.program_id = programs.id").Where("fp.facility_id = ?", args.FacilityID)
+	}
+	if !includeArchived {
+		tx = tx.Where("programs.archived_at IS NULL")
+	}
+	if len(args.Tags) > 0 {
+		tx = tx.Where("pt.program_id IN (?)", args.Tags)
+	}
+	if args.OrderBy != "" {
+		tx = tx.Order(args.OrderClause())
+	}
+	if args.Search != "" {
+		tx = tx.Where("LOWER(name) LIKE ? OR LOWER(description) LIKE ? ", args.SearchQuery(), args.SearchQuery())
+	}
+	if err := tx.Count(&args.Total).Error; err != nil {
+		return nil, newGetRecordsDBError(err, "programs table")
+	}
+	if err := tx.Limit(args.PerPage).Offset(args.CalcOffset()).Scan(&programsTable).Error; err != nil {
+		return programsTable, newGetRecordsDBError(err, "programs table")
+	}
+	return programsTable, nil
+
+}
diff --git a/backend/src/database/users.go b/backend/src/database/users.go
index 7e4b91d15..ba5c90186 100644
--- a/backend/src/database/users.go
+++ b/backend/src/database/users.go
@@ -297,16 +297,22 @@ func (db *DB) LogUserSessionEnded(userID uint, sessionID string) {
 	}
 }
 
-func (db *DB) GetNumberOfActiveUsersForTimePeriod(active bool, days int, facilityId *uint) (int64, error) {
+func (db *DB) GetNumberOfActiveUsersForTimePeriod(args *models.QueryContext, active bool, days int, facilityId *uint) (int64, error) {
 	var count int64
-	daysAgo := time.Now().AddDate(0, 0, -days)
-	join := "JOIN login_metrics on users.id = login_metrics.user_id AND login_metrics.last_login "
-	if active {
-		join += "> ?"
+	var tx *gorm.DB
+	join := "JOIN login_metrics on users.id = login_metrics.user_id"
+	tx = db.WithContext(args.Ctx).Model(&models.User{})
+	if days == -1 {
+		tx = tx.Joins(join)
 	} else {
-		join += "< ?"
+		daysAgo := time.Now().AddDate(0, 0, -days)
+		if active {
+			join += " AND login_metrics.last_login > ?"
+		} else {
+			join += " AND login_metrics.last_login < ?"
+		}
+		tx = tx.Joins(join, daysAgo)
 	}
-	tx := db.Model(&models.User{}).Joins(join, daysAgo).Where("role = 'student'")
 	if facilityId != nil {
 		tx = tx.Where("facility_id = ?", *facilityId)
 	}
@@ -316,31 +322,40 @@ func (db *DB) GetNumberOfActiveUsersForTimePeriod(active bool, days int, facilit
 	return count, nil
 }
 
-func (db *DB) NewUsersInTimePeriod(days int, facilityId *uint) (int64, int64, error) {
+func (db *DB) NewUsersInTimePeriod(args *models.QueryContext, days int, facilityId *uint) (int64, int64, error) {
 	var admin_count int64
-	var user_count int64
+	var resident_count int64
 	daysAgo := time.Now().AddDate(0, 0, -days)
-	tx := db.Model(&models.User{}).Where("created_at >= ? AND role NOT IN ('system_admin', 'admin')", daysAgo)
+	tx := db.WithContext(args.Ctx).Model(&models.User{}).Where("role ='student'")
+	if days != -1 {
+		tx = tx.Where("created_at >= ?", daysAgo)
+	}
 	if facilityId != nil {
 		tx = tx.Where("facility_id = ?", *facilityId)
 	}
-	if err := tx.Count(&user_count).Error; err != nil {
+	if err := tx.Count(&resident_count).Error; err != nil {
 		return 0, 0, newGetRecordsDBError(err, "users")
 	}
-	tx = db.Model(&models.User{}).Where("created_at >= ? AND role NOT IN ('student', 'system_admin')", daysAgo)
+	tx = db.WithContext(args.Ctx).Model(&models.User{}).Where("role IN ?", models.AdminRoles)
+	if days != -1 {
+		tx = tx.Where("created_at >= ?", daysAgo)
+	}
 	if facilityId != nil {
 		tx = tx.Where("facility_id = ?", *facilityId)
 	}
 	if err := tx.Count(&admin_count).Error; err != nil {
 		return 0, 0, newGetRecordsDBError(err, "users")
 	}
-	return user_count, admin_count, nil
+	return resident_count, admin_count, nil
 }
 
-func (db *DB) GetTotalLogins(days int, facilityId *uint) (int64, error) {
+func (db *DB) GetTotalLogins(args *models.QueryContext, days int, facilityId *uint) (int64, error) {
 	var total int64
-	daysAgo := time.Now().AddDate(0, 0, -days)
-	tx := db.Model(&models.LoginActivity{}).Select("SUM(total_logins)").Where("time_interval >= ?", daysAgo)
+	tx := db.WithContext(args.Ctx).Model(&models.LoginActivity{}).Select("SUM(total_logins)")
+	if days != -1 {
+		daysAgo := time.Now().AddDate(0, 0, -days)
+		tx = tx.Where("time_interval >= ?", daysAgo)
+	}
 	if facilityId != nil {
 		tx = tx.Where("facility_id = ?", *facilityId)
 	}
@@ -350,17 +365,17 @@ func (db *DB) GetTotalLogins(days int, facilityId *uint) (int64, error) {
 	return total, nil
 }
 
-func (db *DB) GetTotalUsers(facilityId *uint) (int64, int64, error) {
+func (db *DB) GetTotalUsers(args *models.QueryContext, facilityId *uint) (int64, int64, error) {
 	var totalResidents int64
 	var totalAdmins int64
-	tx := db.Model(&models.User{}).Where("role NOT IN ('admin', 'system_admin')")
+	tx := db.WithContext(args.Ctx).Model(&models.User{}).Where("role = student")
 	if facilityId != nil {
 		tx = tx.Where("facility_id = ?", *facilityId)
 	}
 	if err := tx.Count(&totalResidents).Error; err != nil {
 		return 0, 0, newGetRecordsDBError(err, "users")
 	}
-	tx = db.Model(&models.User{}).Where("role NOT IN ('student', 'system_admin')")
+	tx = db.WithContext(args.Ctx).Model(&models.User{}).Where("role IN ?", models.AdminRoles)
 	if facilityId != nil {
 		tx = tx.Where("facility_id = ?", *facilityId)
 	}
@@ -370,15 +385,23 @@ func (db *DB) GetTotalUsers(facilityId *uint) (int64, int64, error) {
 	return totalResidents, totalAdmins, nil
 }
 
-func (db *DB) GetLoginActivity(days int, facilityID *uint) ([]models.LoginActivity, error) {
+func (db *DB) GetLoginActivity(args *models.QueryContext, days int, facilityID *uint) ([]models.LoginActivity, error) {
 	acitvity := make([]models.LoginActivity, 0, 3)
-	daysAgo := time.Now().AddDate(0, 0, -days)
-	if err := db.Raw(`SELECT time_interval, total_logins
+	var query *gorm.DB
+	if days == -1 {
+		query = db.WithContext(args.Ctx).Raw(`SELECT time_interval, total_logins
+						FROM login_activity
+						ORDER BY total_logins DESC
+						LIMIT 3;`)
+	} else {
+		daysAgo := time.Now().AddDate(0, 0, -days)
+		query = db.WithContext(args.Ctx).Raw(`SELECT time_interval, total_logins
 						FROM login_activity
 						WHERE time_interval >= ?
 						ORDER BY total_logins DESC
-						LIMIT 3;`, daysAgo).
-		Scan(&acitvity).Error; err != nil {
+						LIMIT 3;`, daysAgo)
+	}
+	if err := query.Scan(&acitvity).Error; err != nil {
 		return nil, newGetRecordsDBError(err, "login_activity")
 	}
 	return acitvity, nil
diff --git a/backend/src/handlers/dashboard.go b/backend/src/handlers/dashboard.go
index 529abb20c..b3e979d20 100644
--- a/backend/src/handlers/dashboard.go
+++ b/backend/src/handlers/dashboard.go
@@ -127,6 +127,7 @@ func (srv *Server) handleAdminLayer2(w http.ResponseWriter, r *http.Request, log
 }
 
 func (srv *Server) handleLoginMetrics(w http.ResponseWriter, r *http.Request, log sLog) error {
+	args := srv.getQueryContext(r)
 	facility := r.URL.Query().Get("facility")
 	claims := r.Context().Value(ClaimsKey).(*Claims)
 	var facilityId *uint
@@ -160,30 +161,30 @@ func (srv *Server) handleLoginMetrics(w http.ResponseWriter, r *http.Request, lo
 	} else {
 		numDays, err := strconv.Atoi(daysQ)
 		if err != nil {
-			return newInvalidIdServiceError(err, "days")
+			numDays = -1
 		}
 		days = numDays
 	}
 	key := fmt.Sprintf("%s-%d", facility, days)
 	cached, err := srv.buckets[LoginMetrics].Get(key)
 	if err != nil && errors.Is(err, nats.ErrKeyNotFound) || clearCache {
-		activeUsers, err := srv.Db.GetNumberOfActiveUsersForTimePeriod(true, days, facilityId)
+		activeUsers, err := srv.Db.GetNumberOfActiveUsersForTimePeriod(&args, true, days, facilityId)
 		if err != nil {
 			return newDatabaseServiceError(err)
 		}
-		totalLogins, err := srv.Db.GetTotalLogins(days, facilityId)
+		totalLogins, err := srv.Db.GetTotalLogins(&args, days, facilityId)
 		if err != nil {
 			return newDatabaseServiceError(err)
 		}
-		totalResidents, totalAdmins, err := srv.Db.GetTotalUsers(facilityId)
+		totalResidents, totalAdmins, err := srv.Db.GetTotalUsers(&args, facilityId)
 		if err != nil {
 			return newDatabaseServiceError(err)
 		}
-		newResidentsAdded, newAdminsAdded, err := srv.Db.NewUsersInTimePeriod(days, facilityId)
+		newResidentsAdded, newAdminsAdded, err := srv.Db.NewUsersInTimePeriod(&args, days, facilityId)
 		if err != nil {
 			return newDatabaseServiceError(err)
 		}
-		loginTimes, err := srv.Db.GetLoginActivity(days, facilityId)
+		loginTimes, err := srv.Db.GetLoginActivity(&args, days, facilityId)
 		if err != nil {
 			return newDatabaseServiceError(err)
 		}
diff --git a/backend/src/handlers/open_content_activity_handler.go b/backend/src/handlers/open_content_activity_handler.go
index c393689f2..af19d992a 100644
--- a/backend/src/handlers/open_content_activity_handler.go
+++ b/backend/src/handlers/open_content_activity_handler.go
@@ -38,13 +38,14 @@ func (srv *Server) handleGetTopUserOpenContent(w http.ResponseWriter, r *http.Re
 }
 
 func (srv *Server) handleGetTopFacilityLibraries(w http.ResponseWriter, r *http.Request, log sLog) error {
+	args := srv.getQueryContext(r)
 	facilityId := srv.getFacilityID(r)
 	_, perPage := srv.getPaginationInfo(r)
 	days, err := strconv.Atoi(r.URL.Query().Get("days"))
 	if err != nil {
-		days = 7
+		days = -1
 	}
-	topLibraries, err := srv.Db.GetTopFacilityLibraries(int(facilityId), perPage, days)
+	topLibraries, err := srv.Db.GetTopFacilityLibraries(&args, int(facilityId), perPage, days)
 	if err != nil {
 		return newDatabaseServiceError(err)
 	}
diff --git a/backend/src/handlers/programs_handler.go b/backend/src/handlers/programs_handler.go
index 3eb09a1f2..dea738a8b 100644
--- a/backend/src/handlers/programs_handler.go
+++ b/backend/src/handlers/programs_handler.go
@@ -11,6 +11,8 @@ func (srv *Server) registerProgramsRoutes() []routeDef {
 	axx := []models.FeatureAccess{models.ProgramAccess}
 	return []routeDef{
 		{"GET /api/programs", srv.handleIndexPrograms, false, axx},
+		{"GET /api/programs/stats", srv.handleIndexProgramsFacilitiesStats, true, axx},
+		{"GET /api/programs/detailed-list", srv.handleIndexProgramsOverviewTable, true, axx},
 		{"GET /api/programs/{id}", srv.handleShowProgram, false, axx},
 		{"POST /api/programs", srv.handleCreateProgram, true, axx},
 		{"DELETE /api/programs/{id}", srv.handleDeleteProgram, true, axx},
@@ -45,6 +47,40 @@ type ProgramOverviewResponse struct {
 	CompletionRate    float64 `json:"completion_rate"`
 }
 
+func (srv *Server) handleIndexProgramsFacilitiesStats(w http.ResponseWriter, r *http.Request, log sLog) error {
+	args := srv.getQueryContext(r)
+	adminRole := r.Context().Value(ClaimsKey).(*Claims).Role
+	timeFilter, err := strconv.Atoi(r.URL.Query().Get("days"))
+	if err != nil {
+		timeFilter = -1
+	}
+	var programs models.ProgramsFacilitiesStats
+	if adminRole == models.FacilityAdmin {
+		programs, err = srv.Db.GetProgramsFacilityStats(&args, timeFilter)
+	} else {
+		programs, err = srv.Db.GetProgramsFacilitiesStats(&args, timeFilter)
+	}
+	if err != nil {
+		return newDatabaseServiceError(err)
+	}
+	return writeJsonResponse(w, http.StatusOK, programs)
+}
+
+func (srv *Server) handleIndexProgramsOverviewTable(w http.ResponseWriter, r *http.Request, log sLog) error {
+	args := srv.getQueryContext(r)
+	adminRole := r.Context().Value(ClaimsKey).(*Claims).Role
+	timeFilter, err := strconv.Atoi(r.URL.Query().Get("days"))
+	if err != nil {
+		timeFilter = -1
+	}
+	includeArchived := r.URL.Query().Get("include_archived") == "true"
+	programs, err := srv.Db.GetProgramsOverviewTable(&args, timeFilter, includeArchived, adminRole)
+	if err != nil {
+		return newDatabaseServiceError(err)
+	}
+	return writePaginatedResponse(w, http.StatusOK, programs, args.IntoMeta())
+}
+
 func (srv *Server) handleShowProgram(w http.ResponseWriter, r *http.Request, log sLog) error {
 	id, err := strconv.Atoi(r.PathValue("id"))
 	if err != nil {
@@ -65,7 +101,7 @@ func (srv *Server) handleShowProgram(w http.ResponseWriter, r *http.Request, log
 		log.add("program_id", id)
 		return newDatabaseServiceError(err)
 	}
-	
+
 	resultSet := ProgramOverviewResponse{
 		Program:           *program,
 		ActiveEnrollments: metrics.ActiveEnrollments,
diff --git a/backend/src/models/jobs.go b/backend/src/models/jobs.go
index 68b22086a..00fb0243d 100644
--- a/backend/src/models/jobs.go
+++ b/backend/src/models/jobs.go
@@ -69,6 +69,7 @@ const (
 	GetActivityJob   JobType = "get_activity"
 
 	ScrapeKiwixJob         JobType   = "scrape_kiwix"
+	DailyProgHistoryJob    JobType   = "daily_prog_history"
 	RetryVideoDownloadsJob JobType   = "retry_video_downloads"
 	RetryManualDownloadJob JobType   = "retry_manual_download"
 	SyncVideoMetadataJob   JobType   = "sync_video_metadata"
diff --git a/backend/src/models/program.go b/backend/src/models/program.go
index 1589f7a15..e5fa5cb1a 100644
--- a/backend/src/models/program.go
+++ b/backend/src/models/program.go
@@ -1,5 +1,7 @@
 package models
 
+import "time"
+
 type FundingType string
 type ProgType string
 type CreditType string
@@ -34,6 +36,7 @@ type Program struct {
 	FundingType FundingType `json:"funding_type" gorm:"type:funding_type" validate:"required"`
 	IsActive    bool        `json:"is_active" gorm:"not null"`
 	IsFavorited bool        `json:"is_favorited" gorm:"-"`
+	ArchivedAt  *time.Time  `json:"archived_at"`
 
 	ProgramTypes       []ProgramType       `json:"program_types" gorm:"foreignKey:ProgramID;references:ID"`
 	ProgramCreditTypes []ProgramCreditType `json:"credit_types" gorm:"foreignKey:ProgramID;references:ID"`
@@ -78,3 +81,78 @@ type FacilitiesPrograms struct {
 }
 
 func (FacilitiesPrograms) TableName() string { return "facilities_programs" }
+
+type DailyProgramsFacilitiesHistory struct {
+	Date                   time.Time `json:"date" gorm:"not null"`
+	TotalPrograms          int64     `json:"total_programs" gorm:"not null"`
+	TotalActivePrograms    int64     `json:"total_active_programs" gorm:"not null"`
+	TotalArchivedPrograms  int64     `json:"total_archived_programs" gorm:"not null"`
+	TotalEnrollments       int64     `json:"total_enrollments" gorm:"not null"`
+	TotalCompletions       int64     `json:"total_completions" gorm:"not null"`
+	TotalProgramOfferings  int64     `json:"total_program_offerings" gorm:"not null"`
+	TotalFacilities        int64     `json:"total_facilities" gorm:"not null"`
+	TotalAttendancesMarked int64     `json:"total_attendances_marked" gorm:"not null"`
+	TotalStudentsPresent   int64     `json:"total_students_present" gorm:"not null"`
+}
+
+func (DailyProgramsFacilitiesHistory) TableName() string {
+	return "daily_programs_facilities_history"
+}
+
+type DailyProgramFacilitiesHistory struct {
+	Date                   time.Time `json:"date" gorm:"not null"`
+	ProgramID              uint      `json:"program_id" gorm:"not null"`
+	TotalActiveFacilities  int64     `json:"total_active_facilities" gorm:"not null"`
+	TotalEnrollments       int64     `json:"total_enrollments" gorm:"not null"`
+	TotalCompletions       int64     `json:"total_completions" gorm:"not null"`
+	TotalActiveEnrollments int64     `json:"total_active_enrollments" gorm:"not null"`
+	TotalClasses           int64     `json:"total_classes" gorm:"not null"`
+	TotalArchivedClasses   int64     `json:"total_archived_classes" gorm:"not null"`
+	TotalAttendancesMarked int64     `json:"total_attendances_marked" gorm:"not null"`
+	TotalStudentsPresent   int64     `json:"total_students_present" gorm:"not null"`
+}
+
+func (DailyProgramFacilitiesHistory) TableName() string {
+	return "daily_program_facilities_history"
+}
+
+type DailyProgramFacilityHistory struct {
+	Date                   time.Time `json:"date" gorm:"not null"`
+	ProgramID              uint      `json:"program_id" gorm:"not null"`
+	FacilityID             uint      `json:"facility_id" gorm:"not null"`
+	TotalEnrollments       int64     `json:"total_enrollments" gorm:"not null"`
+	TotalCompletions       int64     `json:"total_completions" gorm:"not null"`
+	TotalActiveEnrollments int64     `json:"total_active_enrollments" gorm:"not null"`
+	TotalClasses           int64     `json:"total_classes" gorm:"not null"`
+	TotalArchivedClasses   int64     `json:"total_archived_classes" gorm:"not null"`
+	TotalAttendancesMarked int64     `json:"total_attendances_marked" gorm:"not null"`
+	TotalStudentsPresent   int64     `json:"total_students_present" gorm:"not null"`
+}
+
+func (DailyProgramFacilityHistory) TableName() string {
+	return "daily_program_facility_history"
+}
+
+type ProgramsFacilitiesStats struct {
+	TotalPrograms                *int64   `json:"total_programs"`
+	AvgActiveProgramsPerFacility *int64   `json:"avg_active_programs_per_facility"`
+	TotalEnrollments             *int64   `json:"total_enrollments"`
+	AttendanceRate               *float64 `json:"attendance_rate"`
+	CompletionRate               *float64 `json:"completion_rate"`
+}
+
+type ProgramsOverviewTable struct {
+	ProgramID              uint     `json:"program_id"`
+	ProgramName            string   `json:"program_name"`
+	ArchivedAt             *string  `json:"archived_at"`
+	TotalActiveFacilities  *int64   `json:"total_active_facilities"`
+	TotalEnrollments       *int64   `json:"total_enrollments"`
+	TotalActiveEnrollments *int64   `json:"total_active_enrollments"`
+	TotalClasses           *int64   `json:"total_classes"`
+	CompletionRate         *float64 `json:"completion_rate"`
+	AttendanceRate         *float64 `json:"attendance_rate"`
+	Types                  string   `json:"program_types" gorm:"column:program_types"`
+	CreditTypes            string   `json:"credit_types"`
+	FundingType            string   `json:"funding_type"`
+	Status                 bool     `json:"status"`
+}
diff --git a/backend/tasks/database.go b/backend/tasks/database.go
index 3922dcb6a..c0609488b 100644
--- a/backend/tasks/database.go
+++ b/backend/tasks/database.go
@@ -68,6 +68,17 @@ func (jr *JobRunner) intoOpenContentTask(cj *models.CronJob, provId uint, task *
 	return nil
 }
 
+func (jr *JobRunner) intoGeneralTask(cj *models.CronJob, task *models.RunnableTask) error {
+	if task.ID == 0 {
+		if err := jr.db.Preload("Job").Where("job_id = ?", cj.ID).FirstOrCreate(task).Error; err != nil {
+			log.Errorln("failed to create general task from cronjob")
+			return err
+		}
+	}
+	task.Job = cj
+	return nil
+}
+
 func (jr *JobRunner) createIfNotExists(cj models.JobType) (*models.CronJob, error) {
 	job := models.CronJob{Name: string(cj)}
 	if err := jr.db.Model(&models.CronJob{}).Where("name = ?", cj).FirstOrCreate(&job).Error; err != nil {
diff --git a/backend/tasks/scheduler.go b/backend/tasks/scheduler.go
index 38726ec70..a76f8bd09 100644
--- a/backend/tasks/scheduler.go
+++ b/backend/tasks/scheduler.go
@@ -33,8 +33,11 @@ func newJobRunner() *JobRunner {
 
 func (jr *JobRunner) generateTasks() ([]models.RunnableTask, error) {
 	allTasks := make([]models.RunnableTask, 0)
-	if otherTasks, err := jr.generateOpenContentProviderTasks(); err == nil {
-		allTasks = append(allTasks, otherTasks...)
+	if dailyProgramHistoryTasks, err := jr.generateProgramHistoryTasks(); err == nil {
+		allTasks = append(allTasks, dailyProgramHistoryTasks...)
+	}
+	if ocpTasks, err := jr.generateOpenContentProviderTasks(); err == nil {
+		allTasks = append(allTasks, ocpTasks...)
 	} else {
 		log.Println("Failed to generate open content provider tasks")
 	}
@@ -46,6 +49,24 @@ func (jr *JobRunner) generateTasks() ([]models.RunnableTask, error) {
 	return allTasks, nil
 }
 
+func (jr *JobRunner) generateProgramHistoryTasks() ([]models.RunnableTask, error) {
+	tasksToRun := make([]models.RunnableTask, 0)
+	job, err := jr.createIfNotExists(models.DailyProgHistoryJob)
+	if err != nil {
+		log.Errorf("failed to create job: %v", err)
+		return nil, err
+	}
+	newTask := models.RunnableTask{JobID: job.ID, Status: models.StatusPending}
+	err = jr.intoGeneralTask(job, &newTask)
+	if err != nil {
+		log.Errorf("failed to create task: %v", err)
+		return nil, err
+	}
+	tasksToRun = append(tasksToRun, newTask)
+	log.Infof("Generated %d total tasks", len(tasksToRun))
+	return tasksToRun, nil
+}
+
 func (jr *JobRunner) generateOpenContentProviderTasks() ([]models.RunnableTask, error) {
 	otherTasks := make([]models.RunnableTask, 0)
 	providers := make([]models.OpenContentProvider, 0)
diff --git a/frontend/src/Components/OperationalInsightsCharts.tsx b/frontend/src/Components/OperationalInsightsCharts.tsx
index 853dc3ec2..5dbbe3543 100644
--- a/frontend/src/Components/OperationalInsightsCharts.tsx
+++ b/frontend/src/Components/OperationalInsightsCharts.tsx
@@ -1,14 +1,22 @@
 import { useEffect, useState } from 'react';
 import useSWR from 'swr';
-import { Facility, LoginMetrics, ServerResponseOne } from '@/common';
+import {
+    Facility,
+    FilterPastTime,
+    LoginMetrics,
+    ServerResponseOne
+} from '@/common';
 import StatsCard from './StatsCard';
 import { ResponsiveContainer } from 'recharts';
 import EngagementRateGraph from './EngagementRateGraph';
 import { useAuth, canSwitchFacility } from '@/useAuth';
+import DropdownControl from './inputs/DropdownControl';
 
 const OperationalInsights = () => {
     const [facility, setFacility] = useState('all');
-    const [days, setDays] = useState(7);
+    const [timeFilter, setTimeFilter] = useState<FilterPastTime>(
+        FilterPastTime['Past 30 days']
+    );
     const [resetCache, setResetCache] = useState(false);
     const { user } = useAuth();
 
@@ -16,14 +24,14 @@ const OperationalInsights = () => {
         ServerResponseOne<LoginMetrics>,
         Error
     >(
-        `/api/login-metrics?facility=${facility}&days=${days}&reset=${resetCache}`
+        `/api/login-metrics?facility=${facility}&days=${timeFilter}&reset=${resetCache}`
     );
     const { data: facilitiesData } =
         useSWR<ServerResponseOne<Facility[]>>('/api/facilities');
 
     useEffect(() => {
         void mutate();
-    }, [facility, days, resetCache]);
+    }, [facility, timeFilter, resetCache]);
 
     const facilities = facilitiesData?.data;
     useEffect(() => {
@@ -39,6 +47,7 @@ const OperationalInsights = () => {
     const totalUsers =
         (metrics?.data.total_residents ?? 0) +
         (metrics?.data.total_admins ?? 0);
+
     return (
         <div className="overflow-x-hidden">
             {error && <div>Error loading data</div>}
@@ -54,17 +63,11 @@ const OperationalInsights = () => {
                                 >
                                     <span className="label-text">Days</span>
                                 </label>
-                                <select
-                                    id="days"
-                                    className="select select-bordered w-full max-w-xs"
-                                    value={days}
-                                    onChange={(e) =>
-                                        setDays(parseInt(e.target.value))
-                                    }
-                                >
-                                    <option value={7}>Last 7 days</option>
-                                    <option value={30}>Last 30 days</option>
-                                </select>
+                                <DropdownControl
+                                    enumType={FilterPastTime}
+                                    value={timeFilter}
+                                    setState={setTimeFilter}
+                                />
                             </div>
 
                             {canSwitchFacility(user!) && (
@@ -116,6 +119,7 @@ const OperationalInsights = () => {
                             number={totalUsers.toString()}
                             label="Users"
                             tooltip="Total number of admins and residents in the facility"
+                            tooltipClassName="tooltip-right"
                         />
                         <StatsCard
                             title="Active Users"
@@ -124,7 +128,11 @@ const OperationalInsights = () => {
                                 (metrics.data.active_users / totalUsers) *
                                 100
                             ).toFixed(2)}% of total`}
-                            tooltip={`Number of users who have logged in in the last ${days} days`}
+                            tooltip={`${
+                                timeFilter === FilterPastTime['All time']
+                                    ? 'All time number of users who have logged in'
+                                    : `Number of users who have logged in in the last ${timeFilter} days`
+                            }`}
                         />
                         <StatsCard
                             title="Inactive Users"
@@ -136,7 +144,11 @@ const OperationalInsights = () => {
                                     ? 'User'
                                     : 'Users'
                             }
-                            tooltip={`Number of users who have not logged in in the last ${days} days`}
+                            tooltip={`${
+                                timeFilter === FilterPastTime['All time']
+                                    ? 'All time number of users who have not logged in'
+                                    : `Number of users who have not logged in in the last ${timeFilter} days`
+                            }`}
                         />
                         <StatsCard
                             title="New Admins Added"
@@ -146,7 +158,11 @@ const OperationalInsights = () => {
                                     ? 'Admin'
                                     : 'Admins'
                             }
-                            tooltip={`Number of new admins added in the last ${days} days`}
+                            tooltip={`${
+                                timeFilter === FilterPastTime['All time']
+                                    ? 'All time number of new admins added'
+                                    : `Number of new admins added in the last ${timeFilter} days`
+                            }`}
                         />
                         <StatsCard
                             title="New Residents Added"
@@ -156,7 +172,11 @@ const OperationalInsights = () => {
                                     ? 'Resident'
                                     : 'Residents'
                             }
-                            tooltip={`Number of new residents added in the last ${days} days`}
+                            tooltip={`${
+                                timeFilter === FilterPastTime['All time']
+                                    ? 'All time number of new residents added'
+                                    : `Number of new residents added in the last ${timeFilter} days`
+                            }`}
                         />
                         <StatsCard
                             title="Total Logins"
@@ -166,7 +186,7 @@ const OperationalInsights = () => {
                                     ? 'Login'
                                     : 'Logins'
                             }
-                            tooltip={`Total number of logins in the last ${days} days`}
+                            tooltip={`${timeFilter === FilterPastTime['All time'] ? 'All time number of logins' : `Number of logins in the last ${timeFilter} days`}`}
                         />
                     </div>
 
diff --git a/frontend/src/Components/PageNav.tsx b/frontend/src/Components/PageNav.tsx
index 2ae5c1b1d..ecd560d07 100644
--- a/frontend/src/Components/PageNav.tsx
+++ b/frontend/src/Components/PageNav.tsx
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react';
 import { useAuth, canSwitchFacility } from '@/useAuth';
 import { Bars3Icon, BuildingOffice2Icon } from '@heroicons/react/24/solid';
 import ULIComponent from '@/Components/ULIComponent.tsx';
-import { Facility, RouteTitleHandler, TitleHandler } from '@/common';
+import { Facility, RouteTitleHandler, TitleHandler, UserRole } from '@/common';
 import { useMatches } from 'react-router-dom';
 import API from '@/api/api';
 import { usePageTitle } from '@/Context/AuthLayoutPageTitleContext';
@@ -57,6 +57,38 @@ export default function PageNav({
         }
     };
 
+    const path = window.location.pathname;
+    const FacilityDropdownToggle = () => {
+        return (
+            <ul className="menu menu-horizontal px-1">
+                <li>
+                    <details className="dropdown dropdown-end" ref={detailsRef}>
+                        <summary>
+                            <ULIComponent icon={BuildingOffice2Icon} />
+                            <span className="font-semibold">
+                                {user?.facility_name}
+                            </span>
+                        </summary>
+                        <ul className="dropdown-content w-max bg-grey-2 z-[1] dark:bg-grey-1 flex flex-col">
+                            {facilities?.map((facility: Facility) => (
+                                <li
+                                    key={facility.id}
+                                    onClick={() => {
+                                        void handleSwitchFacility(facility);
+                                    }}
+                                >
+                                    <label className="body">
+                                        {facility.name}
+                                    </label>
+                                </li>
+                            ))}
+                        </ul>
+                    </details>
+                </li>
+            </ul>
+        );
+    };
+
     return (
         <div className="px-2 py-3 flex justify-between items-center">
             <div
@@ -81,41 +113,16 @@ export default function PageNav({
                         : pageTitle}
                 </h1>
             </div>
-            {user && canSwitchFacility(user) ? (
-                <ul className="menu menu-horizontal px-1">
-                    <li>
-                        <details
-                            className="dropdown dropdown-end"
-                            ref={detailsRef}
-                        >
-                            <summary>
-                                <ULIComponent icon={BuildingOffice2Icon} />
-                                <span className="font-semibold">
-                                    {user.facility_name}
-                                </span>
-                            </summary>
-                            <ul className="dropdown-content w-max bg-grey-2 z-[1] dark:bg-grey-1 flex flex-col">
-                                {facilities?.map((facility: Facility) => (
-                                    <li
-                                        key={facility.id}
-                                        onClick={() => {
-                                            void handleSwitchFacility(facility);
-                                        }}
-                                    >
-                                        <label className="body">
-                                            {facility.name}
-                                        </label>
-                                    </li>
-                                ))}
-                            </ul>
-                        </details>
-                    </li>
-                </ul>
+            {user && canSwitchFacility(user) && path !== '/programs' ? (
+                <FacilityDropdownToggle />
             ) : (
                 <div className="flex flex-row items-center gap-2 px-6 py-4">
                     <ULIComponent icon={BuildingOffice2Icon} />
                     <label className="font-semibold body">
-                        {user?.facility_name}
+                        {user?.role === UserRole.DepartmentAdmin ||
+                        user?.role === UserRole.SystemAdmin
+                            ? 'All Facilities'
+                            : user?.facility_name}
                     </label>
                 </div>
             )}
diff --git a/frontend/src/Components/StatsCard.tsx b/frontend/src/Components/StatsCard.tsx
index 45a911ea7..c44fb461e 100644
--- a/frontend/src/Components/StatsCard.tsx
+++ b/frontend/src/Components/StatsCard.tsx
@@ -7,6 +7,7 @@ interface StatsCardProps {
     tooltip?: string;
     tooltipClassName?: string;
     useToLocaleString?: boolean; //true will be the default bc component already existed with the formatNumber function operating on the number that is passed into the function
+    tooltipClass?: string;
 }
 export default function StatsCard({
     title,
diff --git a/frontend/src/Components/helperFunctions/formatting.tsx b/frontend/src/Components/helperFunctions/formatting.tsx
new file mode 100644
index 000000000..567eca4be
--- /dev/null
+++ b/frontend/src/Components/helperFunctions/formatting.tsx
@@ -0,0 +1,13 @@
+export function formatPercent(value?: number | string): string {
+    if (typeof value === 'string') return value;
+    if (typeof value === 'number') return `${Math.round(value * 100) / 100}`;
+    return '--';
+}
+
+export function transformStringToArray(input: string): string | string[] {
+    const transformed = input.replace(/_/g, ' ');
+    if (!transformed.includes(',')) {
+        return transformed.trim();
+    }
+    return transformed.split(',').map((s) => s.trim());
+}
diff --git a/frontend/src/Components/helperFunctions/index.ts b/frontend/src/Components/helperFunctions/index.ts
new file mode 100644
index 000000000..70f496da6
--- /dev/null
+++ b/frontend/src/Components/helperFunctions/index.ts
@@ -0,0 +1 @@
+export { formatPercent, transformStringToArray } from './formatting';
diff --git a/frontend/src/Components/inputs/DropdownControl.tsx b/frontend/src/Components/inputs/DropdownControl.tsx
index 3c7952184..c12c8ae71 100644
--- a/frontend/src/Components/inputs/DropdownControl.tsx
+++ b/frontend/src/Components/inputs/DropdownControl.tsx
@@ -2,7 +2,8 @@ import { Dispatch, SetStateAction } from 'react';
 
 interface DropdownControlProps {
     label?: string;
-    setState?: Dispatch<SetStateAction<string>>;
+    value?: any; // eslint-disable-line
+    setState?: Dispatch<SetStateAction<any>>; // eslint-disable-line
     customCallback?: (value: string) => void;
     enumType: Record<string, string>;
 }
@@ -10,6 +11,7 @@ interface DropdownControlProps {
 /* a dropdown that executes a callback function on change */
 export default function DropdownControl({
     label,
+    value,
     setState: callback,
     customCallback,
     enumType
@@ -17,7 +19,7 @@ export default function DropdownControl({
     return (
         <label className="form-control">
             <select
-                defaultValue={label}
+                value={label ?? value} // eslint-disable-line
                 className="select select-bordered"
                 onChange={(e) => {
                     if (callback) {
@@ -28,11 +30,6 @@ export default function DropdownControl({
                     }
                 }}
             >
-                {label && (
-                    <option value="" disabled>
-                        {label}
-                    </option>
-                )}
                 {Object.entries(enumType).map(([key, value]) => (
                     <option key={key} value={value}>
                         {key}
diff --git a/frontend/src/Pages/ClassEnrollmentDetails.tsx b/frontend/src/Pages/ClassEnrollmentDetails.tsx
index e7e581a06..97f7c442b 100644
--- a/frontend/src/Pages/ClassEnrollmentDetails.tsx
+++ b/frontend/src/Pages/ClassEnrollmentDetails.tsx
@@ -166,7 +166,7 @@ export default function ClassEnrollmentDetails() {
                         />
                         <DropdownControl
                             label="Sort by"
-                            setState={(value) => {
+                            customCallback={(value) => {
                                 setSortQuery(value);
                                 setPage(1);
                             }}
@@ -181,7 +181,7 @@ export default function ClassEnrollmentDetails() {
                         />
                         <DropdownControl
                             label="Filter by Status"
-                            setState={(value) => {
+                            customCallback={(value) => {
                                 setFilterStatus(value);
                                 setPage(1);
                             }}
diff --git a/frontend/src/Pages/ProgramManagement.tsx b/frontend/src/Pages/ProgramManagement.tsx
index c7b6e1fd5..9f017e30f 100644
--- a/frontend/src/Pages/ProgramManagement.tsx
+++ b/frontend/src/Pages/ProgramManagement.tsx
@@ -1,50 +1,259 @@
 import { isAdministrator, useAuth } from '@/useAuth';
-import { useState } from 'react';
-import ProgramCard from '@/Components/ProgramCard';
+import { useMemo, useState } from 'react';
 import SearchBar from '@/Components/inputs/SearchBar';
-import { Program, ViewType, Option, ServerResponseMany } from '@/common';
+import {
+    Option,
+    ServerResponseMany,
+    FilterPastTime,
+    ServerResponseOne,
+    ProgramsOverviewTable,
+    ProgramsFacilitiesStats,
+    UserRole
+} from '@/common';
 import useSWR from 'swr';
-import { PlusCircleIcon } from '@heroicons/react/24/outline';
-import ToggleView from '@/Components/ToggleView';
+import {
+    InformationCircleIcon,
+    PlusCircleIcon
+} from '@heroicons/react/24/outline';
 import { useLoaderData } from 'react-router-dom';
 import Pagination from '@/Components/Pagination';
 import { useNavigate } from 'react-router-dom';
 import CategoryDropdownFilter from '@/Components/CategoryDropdownFilter';
-import { useSessionViewType } from '@/Hooks/sessionView';
 import { useUrlPagination } from '@/Hooks/paginationUrlSync';
+import DropdownControl from '@/Components/inputs/DropdownControl';
+import StatsCard from '@/Components/StatsCard';
+import GreyPill from '@/Components/pill-labels/GreyPill';
+import TealPill from '@/Components/pill-labels/TealPill';
+import ULIComponent from '@/Components/ULIComponent';
+import { useDebounceValue } from 'usehooks-ts';
+import {
+    formatPercent,
+    transformStringToArray
+} from '@/Components/helperFunctions';
 
-export enum sortPrograms {}
+export function ProgramRow({
+    program,
+    tableCols
+}: {
+    program: ProgramsOverviewTable;
+    tableCols: string;
+}) {
+    const { user } = useAuth();
+    const navigate = useNavigate();
+    let background = '';
+    if (program.archived_at !== null) background = 'bg-grey-1';
+    const programTypes = transformStringToArray(program.program_types);
+    const creditTypes = transformStringToArray(program.credit_types);
+    const {
+        total_active_facilities,
+        total_enrollments,
+        total_active_enrollments,
+        total_classes,
+        completion_rate,
+        attendance_rate
+    } = program;
+    function TagsWithFormatting({ types }: { types: string[] | string }) {
+        return typeof types === 'string' ? (
+            <>{types}</>
+        ) : (
+            <div className="tooltip" data-tip={types.join(', ')}>
+                {types[0]} (...)
+            </div>
+        );
+    }
+    function NullValue() {
+        return (
+            <div
+                className="tooltip"
+                data-tip={
+                    'Data currently unavailable. Please check back in 24 hours for updated statistics.'
+                }
+            >
+                --
+            </div>
+        );
+    }
+    return (
+        <tr
+            className={`grid ${tableCols} justify-items-center gap-2 items-center text-center card !mr-0 px-2 py-2 ${background} cursor-pointer`}
+            onClick={() => navigate(`${program.program_id}`)}
+        >
+            <td>{program.program_name}</td>
+            {user?.role != UserRole.FacilityAdmin && (
+                <td>{total_active_facilities ?? <NullValue />}</td>
+            )}
+            <td>{total_enrollments ?? <NullValue />}</td>
+            <td>{total_active_enrollments ?? <NullValue />}</td>
+            <td>{total_classes ?? <NullValue />}</td>
+            <td>
+                {completion_rate ? (
+                    formatPercent(completion_rate) + '%'
+                ) : (
+                    <NullValue />
+                )}
+            </td>
+            <td>
+                {attendance_rate ? (
+                    formatPercent(attendance_rate) + '%'
+                ) : (
+                    <NullValue />
+                )}
+            </td>
+            <td>
+                <TagsWithFormatting types={programTypes} />
+            </td>
+            <td>
+                <TagsWithFormatting types={creditTypes} />
+            </td>
+            <td>{transformStringToArray(program.funding_type)}</td>
+            <td>
+                {program.status ? (
+                    <TealPill children={'Active'} />
+                ) : (
+                    <GreyPill children={'Inactive'} />
+                )}
+            </td>
+        </tr>
+    );
+}
 
 export default function ProgramManagement() {
     const { user } = useAuth();
-    const [activeView, setActiveView] = useSessionViewType('programView');
+    const { statsCols, tableCols } =
+        user?.role === UserRole.FacilityAdmin
+            ? { statsCols: 'grid-cols-4', tableCols: 'grid-cols-10' }
+            : { statsCols: 'grid-cols-5', tableCols: 'grid-cols-11' };
+    if (!isAdministrator(user)) {
+        return;
+    }
     const [searchTerm, setSearchTerm] = useState('');
-    const {
-        page: page,
-        perPage,
-        setPage: setPage,
-        setPerPage
-    } = useUrlPagination(1, 20);
+    const searchQuery = useDebounceValue(searchTerm, 500);
+    const [dateRange, setDateRange] = useState<FilterPastTime>(
+        FilterPastTime['Past 30 days']
+    );
+    const { page, perPage, setPage, setPerPage } = useUrlPagination(1, 10);
+    const [includeArchived, setIncludeArchived] = useState<boolean>(false);
+
     const [categoryQueryString, setCategoryQueryString] = useState<string>('');
     const navigate = useNavigate();
-    const { data, error, mutate } = useSWR<ServerResponseMany<Program>, Error>(
-        `/api/programs?page=${page}&per_page=${perPage}&search=${searchTerm}&${categoryQueryString}&order=asc&order_by=name`
+
+    const { data: programsFacilitiesStats } = useSWR<
+        ServerResponseOne<ProgramsFacilitiesStats>,
+        Error
+    >(`/api/programs/stats?days=${dateRange}`);
+
+    const {
+        total_programs,
+        avg_active_programs_per_facility,
+        total_enrollments,
+        attendance_rate,
+        completion_rate
+    } = programsFacilitiesStats?.data ?? {
+        total_programs: '--',
+        avg_active_programs_per_facility: '--',
+        total_enrollments: '--',
+        attendance_rate: '--',
+        completion_rate: '--'
+    };
+
+    const {
+        data: programs,
+        isLoading: programsLoading,
+        mutate
+    } = useSWR<ServerResponseMany<ProgramsOverviewTable>, Error>(
+        `/api/programs/detailed-list?days=${dateRange}&page=${page}&per_page=${perPage}&search=${searchQuery[0]}&${categoryQueryString}&order=asc&order_by=name&include_archived=${includeArchived}`
     );
-    const programData = data?.data;
-    const meta = data?.meta;
+    const meta = programs?.meta;
 
     const { categories } = useLoaderData() as {
         categories: Option[];
     };
 
+    const tableDateRangeLabel = useMemo(() => {
+        if (dateRange === FilterPastTime['All time']) return 'all time';
+        const startDate = new Date(
+            Date.now() - parseInt(dateRange, 10) * 86400000
+        ).toLocaleDateString('en-US', {
+            year: 'numeric',
+            month: 'long',
+            day: 'numeric'
+        });
+        const endDate = new Date().toLocaleDateString('en-US', {
+            year: 'numeric',
+            month: 'long',
+            day: 'numeric'
+        });
+        return `${startDate} to ${endDate}`;
+    }, [dateRange]);
+
     function handleSearch(newSearch: string) {
         setSearchTerm(newSearch);
         setPage(1);
     }
 
+    console.log(programsFacilitiesStats);
+
     return (
-        <div className="px-5 py-4">
-            <div className="flex flex-row justify-between items-center mb-4">
+        <div className="px-5 py-4 flex flex-col gap-4 overflow-x-hidden">
+            <div className="flex flex-row justify-end">
+                <DropdownControl
+                    enumType={FilterPastTime}
+                    setState={setDateRange}
+                />
+            </div>
+            <div className={`grid ${statsCols} gap-4`}>
+                <StatsCard
+                    title={'Total Programs'}
+                    number={
+                        total_programs === null
+                            ? '--'
+                            : total_programs.toString()
+                    }
+                    label={'programs'}
+                    tooltip="Count of unique programs offered across all facilities."
+                    useToLocaleString={false}
+                />
+                {user?.role !== UserRole.FacilityAdmin && (
+                    <StatsCard
+                        title={'Active Programs Per Facility'}
+                        number={
+                            avg_active_programs_per_facility === null
+                                ? '--'
+                                : avg_active_programs_per_facility.toString()
+                        }
+                        label={'programs'}
+                        tooltip="Average number of programs per facility with at least one active class and enrolled students."
+                        useToLocaleString={false}
+                    />
+                )}
+                <StatsCard
+                    title={'Total Enrollments'}
+                    number={
+                        total_enrollments === null
+                            ? '--'
+                            : total_enrollments.toString()
+                    }
+                    label={'residents'}
+                    tooltip="Total number of enrollments. A single resident may be enrolled in more than one program."
+                    useToLocaleString={false}
+                />
+                <StatsCard
+                    title={'Avg Attendance Rate'}
+                    number={formatPercent(attendance_rate)}
+                    label={'%'}
+                    tooltip="Average attendance across all sessions where attendance was recorded."
+                    useToLocaleString={false}
+                />
+                <StatsCard
+                    title={'Avg Completion Rate'}
+                    number={formatPercent(completion_rate)}
+                    label={'%'}
+                    tooltip="Average percentage of participants who completed a program. Only includes classes with defined end dates."
+                    useToLocaleString={false}
+                    tooltipClass="tooltip-left"
+                />
+            </div>
+            <div className="flex flex-row justify-between items-center my-4">
                 <div className="flex flex-row items-center space-x-4">
                     <SearchBar
                         searchTerm={searchTerm}
@@ -55,54 +264,97 @@ export default function ProgramManagement() {
                         setCategoryQueryString={setCategoryQueryString}
                         options={categories ?? []}
                     />
+                    <div className="flex items-center">
+                        <input
+                            type="checkbox"
+                            className="checkbox checkbox-sm mr-2"
+                            onChange={(e) => {
+                                setIncludeArchived(e.target.checked);
+                            }}
+                        />
+                        <span className="text-sm">
+                            Include Archived Programs
+                        </span>
+                    </div>
                 </div>
                 <div className="flex items-center space-x-4">
-                    <ToggleView
-                        activeView={activeView}
-                        setActiveView={setActiveView}
-                    />
-                    {isAdministrator(user) && (
-                        <button
-                            className="button flex items-center space-x-2"
-                            onClick={() => {
-                                navigate('detail');
-                            }}
-                        >
-                            <PlusCircleIcon className="w-4 my-auto" />
-                            <span>Add Program</span>
-                        </button>
-                    )}
+                    <button
+                        className="button flex items-center space-x-2"
+                        onClick={() => {
+                            navigate('detail');
+                        }}
+                    >
+                        <PlusCircleIcon className="w-4 my-auto" />
+                        <span>Add Program</span>
+                    </button>
                 </div>
             </div>
-            <div
-                className={`mt-8 ${activeView === ViewType.Grid ? 'grid grid-cols-4 gap-6' : 'space-y-4'}`}
-            >
-                {error ? (
-                    <p className="text-error">Error loading programs.</p>
-                ) : programData?.length === 0 ? (
-                    <p className="text-error">No programs to display.</p>
-                ) : (
-                    programData?.map((program: Program) => {
-                        return (
-                            <ProgramCard
-                                program={program}
-                                callMutate={() => void mutate()}
-                                view={activeView}
-                                key={program.id}
-                            />
-                        );
-                    })
+            <div className="card px-6 pb-6 space-y-4">
+                <table className="table-2">
+                    <thead>
+                        <tr
+                            className={`grid ${tableCols} justify-items-center gap-2 items-center text-center px-2 !text-xs`}
+                        >
+                            <th>Name</th>
+                            {user?.role != UserRole.FacilityAdmin && (
+                                <th># of Facilities Assigned</th>
+                            )}
+                            <th>Total Enrollments</th>
+                            <th>Active Enrollments</th>
+                            <th># of Classes</th>
+                            <th>Avg. Completion Rate</th>
+                            <th>Avg. Attendance Rate</th>
+                            <th>Category</th>
+                            <th>Credit Type</th>
+                            <th>Funding Type</th>
+                            <th className="flex items-center">
+                                Status
+                                {user?.role === UserRole.FacilityAdmin && (
+                                    <ULIComponent
+                                        icon={InformationCircleIcon}
+                                        dataTip={
+                                            'This status reflects whether the program is currently available for scheduling new classes. Inactive programs may still have active classes running if they were scheduled before deactivation.'
+                                        }
+                                        tooltipClassName="tooltip-left"
+                                        iconClassName="text-teal-4 cursor-help"
+                                    />
+                                )}
+                            </th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {programsLoading ? null : !programs?.data?.length ? (
+                            <tr>
+                                <th>
+                                    <p className="body text-center text-error">
+                                        No programs to show.
+                                    </p>
+                                </th>
+                            </tr>
+                        ) : (
+                            programs.data.map((program, index) => (
+                                <ProgramRow
+                                    key={index}
+                                    program={program}
+                                    tableCols={tableCols}
+                                />
+                            ))
+                        )}
+                    </tbody>
+                </table>
+                <p className="flex justify-center body italic">
+                    {`Program data in table reflects the selected date range: ${tableDateRangeLabel}`}
+                </p>
+                {meta && (
+                    <div className="flex justify-center">
+                        <Pagination
+                            meta={meta}
+                            setPage={setPage}
+                            setPerPage={setPerPage}
+                        />
+                    </div>
                 )}
             </div>
-            {meta && (
-                <div className="flex justify-center mt-4">
-                    <Pagination
-                        meta={meta}
-                        setPage={setPage}
-                        setPerPage={setPerPage}
-                    />
-                </div>
-            )}
         </div>
     );
 }
diff --git a/frontend/src/common.ts b/frontend/src/common.ts
index c3bb4a100..40a7de427 100644
--- a/frontend/src/common.ts
+++ b/frontend/src/common.ts
@@ -696,6 +696,35 @@ export interface ProgramOverview extends Program {
     completion_rate: number;
 }
 
+export interface ProgramsFacilitiesStats {
+    total_programs: number;
+    avg_active_programs_per_facility: number;
+    total_enrollments: number;
+    attendance_rate: number;
+    completion_rate: number;
+}
+
+export interface ProgramsOverviewTable {
+    program_id: number;
+    program_name: string;
+    archived_at: string;
+    total_active_facilities: number;
+    total_enrollments: number;
+    total_active_enrollments: number;
+    total_classes: number;
+    completion_rate: number;
+    attendance_rate: number;
+    program_types: string;
+    credit_types: string;
+    funding_type: string;
+    status: boolean;
+}
+
+export interface ProgramsOverview {
+    programs_facilities_stats: ProgramsFacilitiesStats;
+    programs_table: ProgramsOverviewTable[];
+}
+
 export interface Class {
     id: number;
     program_id: number;
@@ -890,11 +919,9 @@ export enum Timezones {
 }
 
 export enum FilterPastTime {
-    'Past 7 days' = '7',
     'Past 30 days' = '30',
     'Past 90 days' = '90',
-    'Past 6 months' = '182',
-    'Past year' = '365'
+    'All time' = 'all'
 }
 
 export interface OpenContentItem {
diff --git a/provider-middleware/daily_program_history.go b/provider-middleware/daily_program_history.go
new file mode 100644
index 000000000..6b2c6ee70
--- /dev/null
+++ b/provider-middleware/daily_program_history.go
@@ -0,0 +1,189 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true AND archived_at IS NULL THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Select("COUNT(*) AS total_program_offerings").
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true AND programs.archived_at IS NULL").
+		Scan(&totalProgramOfferings).Error; err != nil {
+		log.Errorln("error getting total program offerings")
+		return err
+	}
+	history.TotalProgramOfferings = totalProgramOfferings
+
+	var totalFacilities int64
+	if err := db.WithContext(ctx).Model(&models.Facility{}).
+		Select("COUNT(*) AS total_facilities").
+		Scan(&totalFacilities).Error; err != nil {
+		log.Errorln("error getting total facilities")
+		return err
+	}
+	history.TotalFacilities = totalFacilities
+
+	var attendanceStats struct {
+		TotalAttendancesMarked int64 `json:"total_attendances_marked"`
+		TotalStudentsPresent   int64 `json:"total_students_present"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEventAttendance{}).
+		Select(`
+		COUNT(CASE WHEN attendance_status IS NOT NULL AND attendance_status != '' THEN 1 END) AS total_attendances_marked,
+		COALESCE(SUM(CASE WHEN attendance_status = 'present' THEN 1 ELSE 0 END), 0) AS total_students_present
+		`).
+		Scan(&attendanceStats).Error; err != nil {
+		log.Errorln("error getting total students present")
+		return err
+	}
+	history.TotalStudentsPresent = attendanceStats.TotalStudentsPresent
+	history.TotalAttendancesMarked = attendanceStats.TotalAttendancesMarked
+
+	history.Date = time.Now()
+
+	log.Infof("History struct before insert: %+v", history)
+	if err := db.Create(&history).Error; err != nil {
+		log.Errorln("error creating daily program history")
+		return err
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	programs := make([]models.DailyProgramFacilitiesHistory, 10)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active AND programs.archived_at IS NULL THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL AND pcea.attendance_status != '' THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("JOIN facilities_programs fp ON fp.program_id = programs.id").
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
+		Joins("LEFT JOIN program_class_enrollments pce ON pce.class_id = pc.id").
+		Joins("LEFT JOIN program_class_events pcev ON pcev.class_id = pc.id").
+		Joins("LEFT JOIN program_class_event_attendance pcea ON pcea.event_id = pcev.id").
+		Group("programs.id")
+	if err := tx.Scan(&programs).Error; err != nil {
+		log.Errorln("error getting daily program facilities history")
+		return err
+	}
+
+	now := time.Now()
+	for i := range programs {
+		programs[i].Date = now
+	}
+
+	if len(programs) > 0 {
+		if err := db.Create(&programs).Error; err != nil {
+			log.Errorln("error creating daily program facilities history")
+			return err
+		}
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilityHistory(ctx context.Context, db *gorm.DB) error {
+	histories := make([]models.DailyProgramFacilityHistory, 25)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			fp.facility_id AS facility_id,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL AND pcea.attendance_status != '' THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("JOIN facilities_programs fp ON fp.program_id = programs.id").
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id AND pc.facility_id = fp.facility_id").
+		Joins("LEFT JOIN program_class_enrollments pce ON pce.class_id = pc.id").
+		Joins("LEFT JOIN program_class_events pcev ON pcev.class_id = pc.id").
+		Joins("LEFT JOIN program_class_event_attendance pcea ON pcea.event_id = pcev.id").
+		Group("programs.id, fp.facility_id")
+	if err := tx.Scan(&histories).Error; err != nil {
+		log.Errorln("error getting daily program facility history")
+		return err
+	}
+
+	now := time.Now()
+	for i := range histories {
+		histories[i].Date = now
+	}
+
+	if len(histories) > 0 {
+		if err := db.Create(&histories).Error; err != nil {
+			log.Errorln("error creating daily program facility history")
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/provider-middleware/handlers.go b/provider-middleware/handlers.go
index 40875b726..d20b4bafd 100644
--- a/provider-middleware/handlers.go
+++ b/provider-middleware/handlers.go
@@ -37,6 +37,7 @@ func (sh *ServiceHandler) initSubscription() error {
 		{models.RetryManualDownloadJob.PubName(), sh.handleManualRetryDownload},
 		{models.SyncVideoMetadataJob.PubName(), sh.handleSyncVideoMetadata},
 		{models.PutVideoMetadataJob.PubName(), sh.handlePutVideoMetadata},
+		{models.DailyProgHistoryJob.PubName(), sh.handleInsertDailyProgHistory},
 	}
 	for _, sub := range subscriptions {
 		_, err := sh.nats.QueueSubscribe(sub.topic, "middleware", func(msg *nats.Msg) {
@@ -51,6 +52,23 @@ func (sh *ServiceHandler) initSubscription() error {
 	return nil
 }
 
+func (sh *ServiceHandler) handleInsertDailyProgHistory(ctx context.Context, msg *nats.Msg) {
+	contxt, cancel := context.WithTimeout(ctx, CANCEL_TIMEOUT)
+	defer cancel()
+	var body map[string]any
+	if err := json.Unmarshal(msg.Data, &body); err != nil {
+		logger().Errorf("failed to unmarshal message: %v", err)
+		return
+	}
+	jobId, ok := body["job_id"].(string)
+	if !ok {
+		logger().Errorf("failed to parse job_id: %v", body["job_id"])
+		return
+	}
+	success := InsertDailyProgHistory(contxt, sh.db) != nil
+	sh.cleanupJob(contxt, nil, jobId, success)
+}
+
 /**
 * GET: /api/courses
 * This handler will be responsible for importing courses from Providers
@@ -68,7 +86,7 @@ func (sh *ServiceHandler) handleCourses(ctx context.Context, msg *nats.Msg) {
 	jobId := params["job_id"].(string)
 	providerPlatformId := int(params["provider_platform_id"].(float64))
 	success := service.ImportCourses(sh.db) != nil
-	sh.cleanupJob(contxt, providerPlatformId, jobId, success)
+	sh.cleanupJob(contxt, &providerPlatformId, jobId, success)
 }
 
 func (sh *ServiceHandler) handleScrapeLibraries(ctx context.Context, msg *nats.Msg) {
@@ -82,7 +100,8 @@ func (sh *ServiceHandler) handleScrapeLibraries(ctx context.Context, msg *nats.M
 	jobId := body["job_id"].(string)
 	kiwixService := NewKiwixService(provider, &body)
 	success := kiwixService.ImportLibraries(contxt, sh.db) != nil
-	sh.cleanupJob(contxt, int(provider.ID), jobId, success)
+	providerIdPtr := int(provider.ID)
+	sh.cleanupJob(contxt, &providerIdPtr, jobId, success)
 }
 
 /**
@@ -136,7 +155,7 @@ func (sh *ServiceHandler) handleMilestonesForCourseUser(ctx context.Context, msg
 	providerPlatformId := int(params["provider_platform_id"].(float64))
 	lastRun, err := time.Parse(time.RFC3339, lastRunStr)
 	if err != nil {
-		sh.cleanupJob(contxt, providerPlatformId, jobId, false)
+		sh.cleanupJob(contxt, &providerPlatformId, jobId, false)
 		return
 	}
 	for _, course := range courses {
@@ -154,7 +173,7 @@ func (sh *ServiceHandler) handleMilestonesForCourseUser(ctx context.Context, msg
 			}
 		}
 	}
-	sh.cleanupJob(contxt, providerPlatformId, jobId, success)
+	sh.cleanupJob(contxt, &providerPlatformId, jobId, success)
 }
 
 func (sh *ServiceHandler) handleAcitivityForCourse(ctx context.Context, msg *nats.Msg) {
@@ -185,7 +204,7 @@ func (sh *ServiceHandler) handleAcitivityForCourse(ctx context.Context, msg *nat
 			}
 		}
 	}
-	sh.cleanupJob(contxt, providerPlatformId, jobId, success)
+	sh.cleanupJob(contxt, &providerPlatformId, jobId, success)
 }
 
 func (sh *ServiceHandler) handleAddVideos(ctx context.Context, msg *nats.Msg) {
@@ -242,7 +261,8 @@ func (sh *ServiceHandler) handleRetryFailedVideos(ctx context.Context, msg *nats
 		logger().Errorf("error retrying failed videos: %v", err)
 		success = false
 	}
-	sh.cleanupJob(contxt, int(provider.ID), body["job_id"].(string), success)
+	providerIdPtr := int(provider.ID)
+	sh.cleanupJob(contxt, &providerIdPtr, body["job_id"].(string), success)
 }
 
 func (sh *ServiceHandler) handleSyncVideoMetadata(ctx context.Context, msg *nats.Msg) {
@@ -261,7 +281,8 @@ func (sh *ServiceHandler) handleSyncVideoMetadata(ctx context.Context, msg *nats
 		logger().Errorf("error syncing video metadata: %v", err)
 		success = false
 	}
-	sh.cleanupJob(contxt, int(provider.ID), body["job_id"].(string), success)
+	providerIdPtr := int(provider.ID)
+	sh.cleanupJob(contxt, &providerIdPtr, body["job_id"].(string), success)
 }
 
 func (sh *ServiceHandler) handlePutVideoMetadata(ctx context.Context, msg *nats.Msg) {
@@ -280,7 +301,8 @@ func (sh *ServiceHandler) handlePutVideoMetadata(ctx context.Context, msg *nats.
 		logger().Errorf("error putting video metadata: %v", err)
 		success = false
 	}
-	sh.cleanupJob(contxt, int(provider.ID), body["job_id"].(string), success)
+	providerIdPtr := int(provider.ID)
+	sh.cleanupJob(contxt, &providerIdPtr, body["job_id"].(string), success)
 }
 
 func extractArrayMap(params map[string]interface{}, mapType string) []map[string]interface{} {
diff --git a/provider-middleware/middleware.go b/provider-middleware/middleware.go
index 7c5dfa2fc..3e9f36ad6 100644
--- a/provider-middleware/middleware.go
+++ b/provider-middleware/middleware.go
@@ -51,11 +51,12 @@ func (sh *ServiceHandler) initProviderPlatformService(ctx context.Context, msg *
 		return nil, fmt.Errorf("failed to parse job_id: %v", body["job_id"])
 	}
 	// prior to here, we are unable to cleanup the job
+	providerIdPtr := int(providerId)
 	var provider models.ProviderPlatform
-	err := sh.db.WithContext(ctx).First(&provider, "id = ?", int(providerId)).Error
+	err := sh.db.WithContext(ctx).First(&provider, "id = ?", providerIdPtr).Error
 	if err != nil {
 		log.Errorf("error looking up provider platform: %v", err)
-		sh.cleanupJob(ctx, int(providerId), jobId, false)
+		sh.cleanupJob(ctx, &providerIdPtr, jobId, false)
 		return nil, fmt.Errorf("failed to find provider: %v", err)
 	}
 	switch provider.Type {
@@ -87,11 +88,17 @@ func (sh *ServiceHandler) getContentProvider(msg *nats.Msg) (*models.OpenContent
 	return openContentProvider, body, nil
 }
 
-func (sh *ServiceHandler) cleanupJob(ctx context.Context, provId int, jobId string, success bool) {
+func (sh *ServiceHandler) cleanupJob(ctx context.Context, provId *int, jobId string, success bool) {
 	log.Infof("job %s succeeded?: %v \n cleaning up task", jobId, success)
 	var task models.RunnableTask
-	if err := sh.db.WithContext(ctx).Model(models.RunnableTask{}).
-		First(&task, "(provider_platform_id = ? AND job_id = ?) OR (open_content_provider_id = ? AND job_id = ?)", provId, jobId, provId, jobId).
+	tx := sh.db.WithContext(ctx).Model(models.RunnableTask{})
+	if provId != nil {
+		tx = tx.Where("(provider_platform_id = ? AND job_id = ?) OR (open_content_provider_id = ? AND job_id = ?)", *provId, jobId, *provId, jobId)
+	} else {
+		tx = tx.Where("job_id = ?", jobId)
+	}
+	if err := tx.
+		First(&task).
 		Error; err != nil {
 		log.Errorf("failed to fetch task: %v", err)
 		return


Reviews:
user: login: carddev81
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T19:12:54Z
user: login: PThorpe92
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T14:59:38Z
user: login: PThorpe92
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T15:05:36Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T16:18:41Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T16:18:55Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T16:22:15Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T16:36:47Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T16:39:09Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T16:40:42Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T16:43:08Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T17:13:12Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T17:34:15Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T17:43:29Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:08:02Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:42:08Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:25:29Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:27:30Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T20:16:43Z
user: login: PThorpe92
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T23:25:53Z
user: login: carddev81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T12:54:01Z
user: login: carddev81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T12:56:30Z
user: login: carddev81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T12:57:52Z
user: login: carddev81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:14:19Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:43:48Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T13:44:34Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T14:43:13Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T14:47:41Z
user: login: carddev81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T17:21:04Z
user: login: carddev81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T17:22:48Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:36:26Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T20:26:03Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T23:26:10Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:21:00Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:31:53Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:48:52Z
user: login: jtucholski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T14:07:35Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T14:19:01Z
user: login: calisio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T14:23:19Z
user: login: carddev81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:03:20Z
user: login: carddev81
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:24:22Z
user: login: carddev81
body: Fix only the issue that I found, but all else tested good, ready to go!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:26:12Z
user: login: jtucholski
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T16:57:54Z

Review comments:
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
path: provider-middleware/daily_program_history.go
user: login: carddev81
body: FYI, in my ticket it specified `# of residents marked as completed ÷  # of residents who were ever enrolled in the class (all status except ‘canceled’ b/c that’s before class start)`
created_at: 2025-04-26T15:16:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
+    date DATE NOT NULL,
path: backend/migrations/00045_add_program_history.sql
user: login: carddev81
body: You should create an index for the date column for optimizing performance
created_at: 2025-04-27T04:14:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
+    date DATE NOT NULL,
+    total_programs INTEGER NOT NULL,
+    total_active_programs INTEGER NOT NULL,
+    total_archived_programs INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_program_offerings INTEGER NOT NULL,
+    total_facilities INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE TABLE public.daily_program_facilities_history (
+    date DATE NOT NULL,
path: backend/migrations/00045_add_program_history.sql
user: login: carddev81
body: Suggest to create composite index for date/program_id
created_at: 2025-04-27T04:18:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
+    date DATE NOT NULL,
+    total_programs INTEGER NOT NULL,
+    total_active_programs INTEGER NOT NULL,
+    total_archived_programs INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_program_offerings INTEGER NOT NULL,
+    total_facilities INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE TABLE public.daily_program_facilities_history (
+    date DATE NOT NULL,
+    program_id INTEGER NOT NULL,
+    total_active_facilities INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_active_enrollments INTEGER NOT NULL,
+    total_classes INTEGER NOT NULL,
+    total_archived_classes INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE TABLE public.daily_program_facility_history (
+    date DATE NOT NULL,
path: backend/migrations/00045_add_program_history.sql
user: login: carddev81
body: Suggest to create composite index for date, program_id, and facility_id?  I don't see where you are referencing this table? Maybe I missed something?
created_at: 2025-04-27T04:20:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +92,99 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+
+	var calculations struct {
path: backend/src/database/programs.go
user: login: carddev81
body: I think you can remove the `calculations` struct here and just use the reference `programsFacilitiesStats` to fill up with the data from the query call. I would then move the following two assignment lines to after the second db call. This will minimize duplication of code?

```
	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
```
created_at: 2025-04-27T04:40:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +92,99 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+
+	var calculations struct {
+		AvgActiveProgramsPerFacility int64   `json:"avg_active_programs_per_facility"`
+		AttendanceRate               float64 `json:"attendance_rate"`
+		CompletionRate               float64 `json:"completion_rate"`
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&calculations).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.AvgActiveProgramsPerFacility = calculations.AvgActiveProgramsPerFacility
+	programsFacilitiesStats.AttendanceRate = calculations.AttendanceRate
+	programsFacilitiesStats.CompletionRate = calculations.CompletionRate
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	log.Printf("Tags: %v", args.Tags)
path: backend/src/database/programs.go
user: login: carddev81
body: did you mean to keep the logging line here?
created_at: 2025-04-27T04:41:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +92,99 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+
+	var calculations struct {
+		AvgActiveProgramsPerFacility int64   `json:"avg_active_programs_per_facility"`
+		AttendanceRate               float64 `json:"attendance_rate"`
+		CompletionRate               float64 `json:"completion_rate"`
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&calculations).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.AvgActiveProgramsPerFacility = calculations.AvgActiveProgramsPerFacility
+	programsFacilitiesStats.AttendanceRate = calculations.AttendanceRate
+	programsFacilitiesStats.CompletionRate = calculations.CompletionRate
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	log.Printf("Tags: %v", args.Tags)
+	tx := db.WithContext(args.Ctx).Table("daily_program_facilities_history AS dpfh").
+		Select(`
+			programs.id AS program_id,
+			programs.name AS program_name,
+			programs.archived_at AS archived_at,
+			MAX(mr.total_active_facilities) AS total_active_facilities,
+			MAX(mr.total_enrollments) AS total_enrollments,
+			MAX(mr.total_active_enrollments) AS total_active_enrollments,
+			MAX(mr.total_classes) AS total_classes,
+			COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(dpfh.total_enrollments), 0), 0) * 100 AS completion_rate,
+			COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(dpfh.total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+			STRING_AGG(DISTINCT pt.program_type::text, ',') AS program_types,
+			STRING_AGG(DISTINCT pct.credit_type::text, ',') AS credit_types,
+			MAX(programs.funding_type) AS funding_type,
+			BOOL_OR(programs.is_active) AS status
+		`).
+		Joins(`
+			JOIN (
+				SELECT 
+					program_id,
path: backend/src/database/programs.go
user: login: carddev81
body: The `sum()` scalar function along with `group by` can be removed here because this join subselect will only ever return 1 record per (date/program_id) due to the where clause `max(date)` and there would only ever be one record returned for that particular date and program. Also, you could remove the `Max()` functions too due to the same reasoning.   
created_at: 2025-04-27T16:09:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +92,99 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+
+	var calculations struct {
+		AvgActiveProgramsPerFacility int64   `json:"avg_active_programs_per_facility"`
+		AttendanceRate               float64 `json:"attendance_rate"`
+		CompletionRate               float64 `json:"completion_rate"`
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&calculations).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.AvgActiveProgramsPerFacility = calculations.AvgActiveProgramsPerFacility
+	programsFacilitiesStats.AttendanceRate = calculations.AttendanceRate
+	programsFacilitiesStats.CompletionRate = calculations.CompletionRate
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	log.Printf("Tags: %v", args.Tags)
+	tx := db.WithContext(args.Ctx).Table("daily_program_facilities_history AS dpfh").
+		Select(`
+			programs.id AS program_id,
+			programs.name AS program_name,
+			programs.archived_at AS archived_at,
+			MAX(mr.total_active_facilities) AS total_active_facilities,
+			MAX(mr.total_enrollments) AS total_enrollments,
+			MAX(mr.total_active_enrollments) AS total_active_enrollments,
+			MAX(mr.total_classes) AS total_classes,
+			COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(dpfh.total_enrollments), 0), 0) * 100 AS completion_rate,
+			COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(dpfh.total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+			STRING_AGG(DISTINCT pt.program_type::text, ',') AS program_types,
+			STRING_AGG(DISTINCT pct.credit_type::text, ',') AS credit_types,
+			MAX(programs.funding_type) AS funding_type,
path: backend/src/database/programs.go
user: login: carddev81
body: You should be able to remove the `max` function here on `funding_type` since there is only ever one `funding_type` per program?
created_at: 2025-04-27T16:15:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +92,99 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+
+	var calculations struct {
+		AvgActiveProgramsPerFacility int64   `json:"avg_active_programs_per_facility"`
+		AttendanceRate               float64 `json:"attendance_rate"`
+		CompletionRate               float64 `json:"completion_rate"`
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&calculations).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.AvgActiveProgramsPerFacility = calculations.AvgActiveProgramsPerFacility
+	programsFacilitiesStats.AttendanceRate = calculations.AttendanceRate
+	programsFacilitiesStats.CompletionRate = calculations.CompletionRate
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	log.Printf("Tags: %v", args.Tags)
+	tx := db.WithContext(args.Ctx).Table("daily_program_facilities_history AS dpfh").
+		Select(`
+			programs.id AS program_id,
+			programs.name AS program_name,
+			programs.archived_at AS archived_at,
+			MAX(mr.total_active_facilities) AS total_active_facilities,
+			MAX(mr.total_enrollments) AS total_enrollments,
+			MAX(mr.total_active_enrollments) AS total_active_enrollments,
+			MAX(mr.total_classes) AS total_classes,
+			COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(dpfh.total_enrollments), 0), 0) * 100 AS completion_rate,
+			COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(dpfh.total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+			STRING_AGG(DISTINCT pt.program_type::text, ',') AS program_types,
+			STRING_AGG(DISTINCT pct.credit_type::text, ',') AS credit_types,
+			MAX(programs.funding_type) AS funding_type,
+			BOOL_OR(programs.is_active) AS status
+		`).
+		Joins(`
+			JOIN (
+				SELECT 
+					program_id,
+					SUM(total_active_facilities) AS total_active_facilities,
+					SUM(total_enrollments) AS total_enrollments,
+					SUM(total_active_enrollments) AS total_active_enrollments,
+					SUM(total_classes) AS total_classes
+				FROM daily_program_facilities_history
+				WHERE date = (SELECT MAX(date) FROM daily_program_facilities_history)
+				GROUP BY program_id
+			) AS mr ON mr.program_id = dpfh.program_id
+		`).
+		Joins("JOIN programs ON programs.id = dpfh.program_id").
+		Joins("JOIN program_types pt ON pt.program_id = dpfh.program_id").
+		Joins("JOIN program_credit_types pct ON pct.program_id = dpfh.program_id").
+		Group("programs.id, programs.name")
+	if timeFilter > 0 {
+		tx = tx.Where("dpfh.date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if !includeArchived {
+		tx = tx.Where("programs.archived_at IS NULL")
+	}
+	if len(args.Tags) > 0 {
+		tx = tx.Where("pt.id IN (?)", args.Tags)
path: backend/src/database/programs.go
user: login: carddev81
body: `pt.id` should be `pt.program_id`, this should fix the categories drop-down
created_at: 2025-04-27T16:22:15Z
author_association: CONTRIBUTOR
path: backend/src/database/users.go
user: login: carddev81
body: Since these methods are being modified, should we add the context from the request to each of these database calls for consistency?
created_at: 2025-04-27T16:50:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +39,33 @@ func (srv *Server) handleIndexPrograms(w http.ResponseWriter, r *http.Request, l
 	return writePaginatedResponse(w, http.StatusOK, programs, args.IntoMeta())
 }
 
+func (srv *Server) handleIndexProgramsFacilitiesStats(w http.ResponseWriter, r *http.Request, log sLog) error {
+	args := srv.getQueryContext(r)
+	timeFilter, err := strconv.Atoi(r.URL.Query().Get("time_filter"))
path: backend/src/handlers/programs_handler.go
user: login: carddev81
body: noticed most other datepickers use the query parameter days but here it is using `time_filter`, should this be changed for consistency? If so, change the query parameter within the `handleIndexProgramsOverviewTable` too.
created_at: 2025-04-27T16:52:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +68,17 @@ func (jr *JobRunner) intoOpenContentTask(cj *models.CronJob, provId uint, task *
 	return nil
 }
 
+func (jr *JobRunner) intoGeneralTask(cj *models.CronJob, task *models.RunnableTask) error {
+	if task.ID == 0 {
path: backend/tasks/database.go
user: login: carddev81
body: I don't think this check is needed? When this method is called the task.ID will always be 0? I may be missing something?
created_at: 2025-04-27T17:03:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -39,6 +47,11 @@ const OperationalInsights = () => {
     const totalUsers =
         (metrics?.data.total_residents ?? 0) +
         (metrics?.data.total_admins ?? 0);
+
+    useEffect(() => {
path: frontend/src/Components/OperationalInsightsCharts.tsx
user: login: carddev81
body: can this `useEffect` be removed? It is only logging the `timeFilter` value to the console?
created_at: 2025-04-27T17:06:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,17 +67,11 @@ const OperationalInsights = () => {
                                 >
                                     <span className="label-text">Days</span>
                                 </label>
-                                <select
-                                    id="days"
-                                    className="select select-bordered w-full max-w-xs"
-                                    value={days}
-                                    onChange={(e) =>
-                                        setDays(parseInt(e.target.value))
-                                    }
-                                >
-                                    <option value={7}>Last 7 days</option>
-                                    <option value={30}>Last 30 days</option>
-                                </select>
+                                <DropdownControl
+                                    enumType={FilterPastTime}
+                                    value={timeFilter}
+                                    setState={setTimeFilter}
+                                />
                             </div>
 
                             {canSwitchFacility(user!) && (
path: frontend/src/Components/OperationalInsightsCharts.tsx
user: login: carddev81
body: Noticed that the tooltip on the following statscard is being clipped a bit?
```
                        <StatsCard
                            title="Total Users"
                            number={totalUsers.toString()}
                            label="Users"
                            tooltip="Total number of admins and residents in the facility"
                        />
```

![image](https://github.com/user-attachments/assets/210dea2f-f5ae-49c6-8a5e-94690e63be02)

created_at: 2025-04-27T17:09:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,7 +147,7 @@ const OperationalInsights = () => {
                                     ? 'User'
                                     : 'Users'
                             }
-                            tooltip={`Number of users who have not logged in in the last ${days} days`}
+                            tooltip={`Number of users who have not logged in in the last ${timeFilter} days`}
path: frontend/src/Components/OperationalInsightsCharts.tsx
user: login: carddev81
body: Should these tooltips be consistent with wording (All time) like you did on lines 134-138?
created_at: 2025-04-27T17:12:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,13 +6,15 @@ interface StatsCardProps {
     label: string;
     tooltip?: string;
     useToLocaleString?: boolean; //true will be the default bc component already existed with the formatNumber function operating on the number that is passed into the function
+    tooltipClass?: string;
path: frontend/src/Components/StatsCard.tsx
user: login: carddev81
body: i think I made this same change in my PR?
created_at: 2025-04-27T17:53:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,7 +55,8 @@ func (sh *ServiceHandler) initProviderPlatformService(ctx context.Context, msg *
 	err := sh.db.WithContext(ctx).First(&provider, "id = ?", int(providerId)).Error
path: provider-middleware/middleware.go
user: login: carddev81
body: maybe create a variable to hold the value of `int(providerId)`? That way you can reuse it's value rather than recast on line 58. Also, why did you create a pointer for this?
created_at: 2025-04-27T18:42:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true").
+		Select("COUNT(*) AS total_program_offerings").
+		Scan(&totalProgramOfferings).Error; err != nil {
+		log.Errorln("error getting total program offerings")
+		return err
+	}
+	history.TotalProgramOfferings = totalProgramOfferings
+
+	var totalFacilities int64
+	if err := db.WithContext(ctx).Model(&models.Facility{}).
+		Select("COUNT(*) AS total_facilities").
+		Scan(&totalFacilities).Error; err != nil {
+		log.Errorln("error getting total facilities")
+		return err
+	}
+	history.TotalFacilities = totalFacilities
+
+	var attendanceStats struct {
+		TotalAttendancesMarked int64 `json:"total_attendances_marked"`
+		TotalStudentsPresent   int64 `json:"total_students_present"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEventAttendance{}).
+		Select(`
+		COUNT(CASE WHEN attendance_status IS NOT NULL THEN 1 END) AS total_attendances_marked,
+		COALESCE(SUM(CASE WHEN attendance_status = 'present' THEN 1 ELSE 0 END), 0) AS total_students_present
+		`).
+		Scan(&attendanceStats).Error; err != nil {
+		log.Errorln("error getting total students present")
+		return err
+	}
+	history.TotalStudentsPresent = attendanceStats.TotalStudentsPresent
+	history.TotalAttendancesMarked = attendanceStats.TotalAttendancesMarked
+
+	history.Date = time.Now()
+
+	log.Infof("History struct before insert: %+v", history)
+	if err := db.Create(&history).Error; err != nil {
+		log.Errorln("error creating daily program history")
+		return err
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	programs := make([]models.DailyProgramFacilitiesHistory, 10)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
+		Joins("LEFT JOIN facilities_programs fp ON fp.program_id = programs.id").
path: provider-middleware/daily_program_history.go
user: login: carddev81
body: I believe this should be an inner join because I think we are only wanting programs that are tied to a facility?
created_at: 2025-04-27T18:54:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true").
+		Select("COUNT(*) AS total_program_offerings").
+		Scan(&totalProgramOfferings).Error; err != nil {
+		log.Errorln("error getting total program offerings")
+		return err
+	}
+	history.TotalProgramOfferings = totalProgramOfferings
+
+	var totalFacilities int64
+	if err := db.WithContext(ctx).Model(&models.Facility{}).
+		Select("COUNT(*) AS total_facilities").
+		Scan(&totalFacilities).Error; err != nil {
+		log.Errorln("error getting total facilities")
+		return err
+	}
+	history.TotalFacilities = totalFacilities
+
+	var attendanceStats struct {
+		TotalAttendancesMarked int64 `json:"total_attendances_marked"`
+		TotalStudentsPresent   int64 `json:"total_students_present"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEventAttendance{}).
+		Select(`
+		COUNT(CASE WHEN attendance_status IS NOT NULL THEN 1 END) AS total_attendances_marked,
+		COALESCE(SUM(CASE WHEN attendance_status = 'present' THEN 1 ELSE 0 END), 0) AS total_students_present
+		`).
+		Scan(&attendanceStats).Error; err != nil {
+		log.Errorln("error getting total students present")
+		return err
+	}
+	history.TotalStudentsPresent = attendanceStats.TotalStudentsPresent
+	history.TotalAttendancesMarked = attendanceStats.TotalAttendancesMarked
+
+	history.Date = time.Now()
+
+	log.Infof("History struct before insert: %+v", history)
+	if err := db.Create(&history).Error; err != nil {
+		log.Errorln("error creating daily program history")
+		return err
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	programs := make([]models.DailyProgramFacilitiesHistory, 10)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
+		Joins("LEFT JOIN facilities_programs fp ON fp.program_id = programs.id").
+		Joins("LEFT JOIN program_class_enrollments pce ON pce.class_id = pc.id").
+		Joins("LEFT JOIN program_class_events pcev ON pcev.class_id = pc.id").
+		Joins("LEFT JOIN program_class_event_attendance pcea ON pcea.event_id = pcev.id").
+		Group("programs.id")
+	if err := tx.Scan(&programs).Error; err != nil {
+		log.Errorln("error getting daily program facilities history")
+		return err
+	}
+
+	now := time.Now()
+	for i := range programs {
+		programs[i].Date = now
+	}
+
+	if len(programs) > 0 {
+		if err := db.Create(&programs).Error; err != nil {
+			log.Errorln("error creating daily program facilities history")
+			return err
+		}
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilityHistory(ctx context.Context, db *gorm.DB) error {
+	histories := make([]models.DailyProgramFacilityHistory, 25)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			fp.facility_id AS facility_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
+		Joins("LEFT JOIN facilities_programs fp ON fp.program_id = programs.id").
path: provider-middleware/daily_program_history.go
user: login: carddev81
body: same here as before 'I believe this should be an inner join because I think we are only wanting programs that are tied to a facility?'
created_at: 2025-04-27T19:08:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true").
+		Select("COUNT(*) AS total_program_offerings").
path: provider-middleware/daily_program_history.go
user: login: carddev81
body: select should go before the join for consistency?
created_at: 2025-04-27T19:10:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,51 +1,196 @@
 import { isAdministrator, useAuth } from '@/useAuth';
 import { useState } from 'react';
-import ProgramCard from '@/Components/ProgramCard';
 import SearchBar from '@/Components/inputs/SearchBar';
-import { Program, ViewType, Option, ServerResponseMany } from '@/common';
+import {
+    Option,
+    ServerResponseMany,
+    FilterPastTime,
+    ServerResponseOne,
+    ProgramsOverviewTable,
+    ProgramsFacilitiesStats
+} from '@/common';
 import useSWR from 'swr';
-import { PlusCircleIcon } from '@heroicons/react/24/outline';
-import ToggleView from '@/Components/ToggleView';
+import {
+    InformationCircleIcon,
+    PlusCircleIcon
+} from '@heroicons/react/24/outline';
 import { useLoaderData } from 'react-router-dom';
 import Pagination from '@/Components/Pagination';
 import { useNavigate } from 'react-router-dom';
 import CategoryDropdownFilter from '@/Components/CategoryDropdownFilter';
-import { useSessionViewType } from '@/Hooks/sessionView';
 import { useUrlPagination } from '@/Hooks/paginationUrlSync';
+import DropdownControl from '@/Components/inputs/DropdownControl';
+import StatsCard from '@/Components/StatsCard';
+import GreyPill from '@/Components/pill-labels/GreyPill';
+import TealPill from '@/Components/pill-labels/TealPill';
+import ULIComponent from '@/Components/ULIComponent';
+import { useDebounceValue } from 'usehooks-ts';
 
-export enum sortPrograms {}
+export function ProgramRow({ program }: { program: ProgramsOverviewTable }) {
+    const navigate = useNavigate();
+    let background = '';
+    if (program.archived_at !== null) background = 'bg-grey-1';
+    return (
+        <tr
+            className={`grid grid-cols-11 justify-items-center gap-2 items-center text-center card !mr-0 px-2 py-2 ${background}`}
path: frontend/src/Pages/ProgramManagement.tsx
user: login: carddev81
body: since the row is clickable should the onhover cursor be using the hand pointer icon?
created_at: 2025-04-27T19:12:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
+    date DATE NOT NULL,
path: backend/migrations/00045_add_program_history.sql
user: login: PThorpe92
body: I see in the query we are ordering by `date desc` so we prob want to specify a `desc` index (`asc` is the default) 
created_at: 2025-04-28T14:59:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,49 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
+    date DATE NOT NULL,
+    total_programs INTEGER NOT NULL,
+    total_active_programs INTEGER NOT NULL,
+    total_archived_programs INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_program_offerings INTEGER NOT NULL,
+    total_facilities INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE TABLE public.daily_program_facilities_history (
+    date DATE NOT NULL,
+    program_id INTEGER NOT NULL,
+    total_active_facilities INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_active_enrollments INTEGER NOT NULL,
+    total_classes INTEGER NOT NULL,
+    total_archived_classes INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE TABLE public.daily_program_facility_history (
+    date DATE NOT NULL,
path: backend/migrations/00045_add_program_history.sql
user: login: PThorpe92
body: Also if we make this `date DATE NOT NULL DEFAULT CURRENT_DATE` we can avoid that loop in the query
created_at: 2025-04-28T15:05:36Z
author_association: MEMBER
diff_hunk: @@ -89,3 +92,99 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+
+	var calculations struct {
+		AvgActiveProgramsPerFacility int64   `json:"avg_active_programs_per_facility"`
+		AttendanceRate               float64 `json:"attendance_rate"`
+		CompletionRate               float64 `json:"completion_rate"`
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&calculations).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.AvgActiveProgramsPerFacility = calculations.AvgActiveProgramsPerFacility
+	programsFacilitiesStats.AttendanceRate = calculations.AttendanceRate
+	programsFacilitiesStats.CompletionRate = calculations.CompletionRate
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	log.Printf("Tags: %v", args.Tags)
+	tx := db.WithContext(args.Ctx).Table("daily_program_facilities_history AS dpfh").
+		Select(`
+			programs.id AS program_id,
+			programs.name AS program_name,
+			programs.archived_at AS archived_at,
+			MAX(mr.total_active_facilities) AS total_active_facilities,
+			MAX(mr.total_enrollments) AS total_enrollments,
+			MAX(mr.total_active_enrollments) AS total_active_enrollments,
+			MAX(mr.total_classes) AS total_classes,
+			COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(dpfh.total_enrollments), 0), 0) * 100 AS completion_rate,
+			COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(dpfh.total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+			STRING_AGG(DISTINCT pt.program_type::text, ',') AS program_types,
+			STRING_AGG(DISTINCT pct.credit_type::text, ',') AS credit_types,
+			MAX(programs.funding_type) AS funding_type,
path: backend/src/database/programs.go
user: login: calisio
body: Gorm requires you to either put it in the group by or use an aggregate function if on at least one of the rows you are executing an aggregate function, but I didn't want to put it in the group by, so thats why i chose to use max for it (because it doesn't impact the result). This is the same for the comment above. See example of the error if remove the aggregate function.
```
server-1            |            JOIN programs ON programs.id = dpfh.program_id JOIN program_types pt ON pt.program_id = dpfh.program_id JOIN program_credit_types pct ON pct.program_id = dpfh.program_id WHERE dpfh.date >= '2025-03-29 16:16:14.933' AND programs.archived_at IS NULL GROUP BY programs.id, programs.name ORDER BY name asc
server-1            | INFO[0018] /app/backend/src/database/programs.go:185 ERROR: column "daily_program_facilities_history.total_active_facilities" must appear in the GROUP BY clause or be used in an aggregate function (SQLSTATE 42803)
server-1            | [0.029ms] [rows:0]
server-1            | {"handler":"handleIndexProgramsOverviewTable","level":"error","method":"GET","msg":"Error occurred is ERROR: column \"daily_program_facilities_history.total_active_facilities\" must appear in the GROUP BY clause or be used in an aggregate function (SQLSTATE 42803)","path":"/api/programs-overview-table","time":"2025-04-28T16:16:14Z"}
```
created_at: 2025-04-28T16:18:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +92,99 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+
+	var calculations struct {
+		AvgActiveProgramsPerFacility int64   `json:"avg_active_programs_per_facility"`
+		AttendanceRate               float64 `json:"attendance_rate"`
+		CompletionRate               float64 `json:"completion_rate"`
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&calculations).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.AvgActiveProgramsPerFacility = calculations.AvgActiveProgramsPerFacility
+	programsFacilitiesStats.AttendanceRate = calculations.AttendanceRate
+	programsFacilitiesStats.CompletionRate = calculations.CompletionRate
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	log.Printf("Tags: %v", args.Tags)
+	tx := db.WithContext(args.Ctx).Table("daily_program_facilities_history AS dpfh").
+		Select(`
+			programs.id AS program_id,
+			programs.name AS program_name,
+			programs.archived_at AS archived_at,
+			MAX(mr.total_active_facilities) AS total_active_facilities,
+			MAX(mr.total_enrollments) AS total_enrollments,
+			MAX(mr.total_active_enrollments) AS total_active_enrollments,
+			MAX(mr.total_classes) AS total_classes,
+			COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(dpfh.total_enrollments), 0), 0) * 100 AS completion_rate,
+			COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(dpfh.total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+			STRING_AGG(DISTINCT pt.program_type::text, ',') AS program_types,
+			STRING_AGG(DISTINCT pct.credit_type::text, ',') AS credit_types,
+			MAX(programs.funding_type) AS funding_type,
+			BOOL_OR(programs.is_active) AS status
+		`).
+		Joins(`
+			JOIN (
+				SELECT 
+					program_id,
path: backend/src/database/programs.go
user: login: calisio
body: See comment below
created_at: 2025-04-28T16:18:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
path: provider-middleware/daily_program_history.go
user: login: calisio
body: Not sure if I am following what you mentioned here. These are the totals, there is no division happening? Are you saying that total enrollments should NOT include 'cancelled'?
created_at: 2025-04-28T16:22:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +92,99 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+
+	var calculations struct {
+		AvgActiveProgramsPerFacility int64   `json:"avg_active_programs_per_facility"`
+		AttendanceRate               float64 `json:"attendance_rate"`
+		CompletionRate               float64 `json:"completion_rate"`
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&calculations).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.AvgActiveProgramsPerFacility = calculations.AvgActiveProgramsPerFacility
+	programsFacilitiesStats.AttendanceRate = calculations.AttendanceRate
+	programsFacilitiesStats.CompletionRate = calculations.CompletionRate
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	log.Printf("Tags: %v", args.Tags)
path: backend/src/database/programs.go
user: login: calisio
body: nope, my bad!
created_at: 2025-04-28T16:36:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,7 +147,7 @@ const OperationalInsights = () => {
                                     ? 'User'
                                     : 'Users'
                             }
-                            tooltip={`Number of users who have not logged in in the last ${days} days`}
+                            tooltip={`Number of users who have not logged in in the last ${timeFilter} days`}
path: frontend/src/Components/OperationalInsightsCharts.tsx
user: login: calisio
body: You know, I actually wrote this down to fix, and then at the end of the ticket I was trying to find what I was referring to, but was only looking on program management page tooltips, and was so confused as to what I was referring to 😂 Now I remember it was this haha. Yes, I will fix this today!
created_at: 2025-04-28T16:39:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,7 +55,8 @@ func (sh *ServiceHandler) initProviderPlatformService(ctx context.Context, msg *
 	err := sh.db.WithContext(ctx).First(&provider, "id = ?", int(providerId)).Error
path: provider-middleware/middleware.go
user: login: calisio
body: I created a pointer for this because for my task you don't need a provider id, so it has to have the option to be nullable. Therefore, that's why I changed all the providerId's to be pointers. 
created_at: 2025-04-28T16:40:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
+    date DATE NOT NULL,
+    total_programs INTEGER NOT NULL,
+    total_active_programs INTEGER NOT NULL,
+    total_archived_programs INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_program_offerings INTEGER NOT NULL,
+    total_facilities INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE TABLE public.daily_program_facilities_history (
+    date DATE NOT NULL,
+    program_id INTEGER NOT NULL,
+    total_active_facilities INTEGER NOT NULL,
+    total_enrollments INTEGER NOT NULL,
+    total_completions INTEGER NOT NULL,
+    total_active_enrollments INTEGER NOT NULL,
+    total_classes INTEGER NOT NULL,
+    total_archived_classes INTEGER NOT NULL,
+    total_attendances_marked INTEGER NOT NULL,
+    total_students_present INTEGER NOT NULL
+);
+CREATE TABLE public.daily_program_facility_history (
+    date DATE NOT NULL,
path: backend/migrations/00045_add_program_history.sql
user: login: calisio
body: I am not referencing this table yet, but I will be soon when I implement the facility admin program management page (coming in a PR probably today). Just wanted to get this PR up quickly so left that out and will put it in my next one!
created_at: 2025-04-28T16:43:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +68,17 @@ func (jr *JobRunner) intoOpenContentTask(cj *models.CronJob, provId uint, task *
 	return nil
 }
 
+func (jr *JobRunner) intoGeneralTask(cj *models.CronJob, task *models.RunnableTask) error {
+	if task.ID == 0 {
path: backend/tasks/database.go
user: login: calisio
body: I'm not 100% positive, I mimicked behavior of the task above (intoOpenContentTask) for consistency. But, I believe it is just cautionary to make sure that the runnable task is a new task (default id is 0 for a new task) to avoid calling the DB when it is duplicate. Open to remove it, just included for consistency
created_at: 2025-04-28T17:13:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true").
+		Select("COUNT(*) AS total_program_offerings").
+		Scan(&totalProgramOfferings).Error; err != nil {
+		log.Errorln("error getting total program offerings")
+		return err
+	}
+	history.TotalProgramOfferings = totalProgramOfferings
+
+	var totalFacilities int64
+	if err := db.WithContext(ctx).Model(&models.Facility{}).
+		Select("COUNT(*) AS total_facilities").
+		Scan(&totalFacilities).Error; err != nil {
+		log.Errorln("error getting total facilities")
+		return err
+	}
+	history.TotalFacilities = totalFacilities
+
+	var attendanceStats struct {
+		TotalAttendancesMarked int64 `json:"total_attendances_marked"`
+		TotalStudentsPresent   int64 `json:"total_students_present"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEventAttendance{}).
+		Select(`
+		COUNT(CASE WHEN attendance_status IS NOT NULL THEN 1 END) AS total_attendances_marked,
+		COALESCE(SUM(CASE WHEN attendance_status = 'present' THEN 1 ELSE 0 END), 0) AS total_students_present
+		`).
+		Scan(&attendanceStats).Error; err != nil {
+		log.Errorln("error getting total students present")
+		return err
+	}
+	history.TotalStudentsPresent = attendanceStats.TotalStudentsPresent
+	history.TotalAttendancesMarked = attendanceStats.TotalAttendancesMarked
+
+	history.Date = time.Now()
+
+	log.Infof("History struct before insert: %+v", history)
+	if err := db.Create(&history).Error; err != nil {
+		log.Errorln("error creating daily program history")
+		return err
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	programs := make([]models.DailyProgramFacilitiesHistory, 10)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
+		Joins("LEFT JOIN facilities_programs fp ON fp.program_id = programs.id").
path: provider-middleware/daily_program_history.go
user: login: calisio
body: for both or just for facilities programs?
created_at: 2025-04-28T17:34:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
path: provider-middleware/daily_program_history.go
user: login: calisio
body: @jtucholski in my ticket, it specifies total enrollment just as 'Total number of enrollments in the program across all facilities'. Should I update this to be consistent with what was included in Rich's ticket? (ie exclude the residents who are cancelled)
created_at: 2025-04-28T17:43:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
path: backend/migrations/00046_add_program_history.sql
user: login: jtucholski
body: @calisio I'm having a difficult time following the naming convention.

What do you think about
- daily_programs_facilities_history -> daily_department_history
- daily_program_facilities_history  -> daily_department_program_history
- daily_program_facility_history    -> daily_facility_program_history

Open to understanding how to look at the existing names and make sense of them
created_at: 2025-04-28T18:08:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+-- +goose Up
+-- +goose StatementBegin
+ALTER TABLE public.programs ADD COLUMN archived_at TIMESTAMP WITH TIME ZONE;
+CREATE TABLE public.daily_programs_facilities_history (
path: backend/migrations/00046_add_program_history.sql
user: login: calisio
body: Not tied to current naming convention. The current naming is 
- plural programS and facilitIES (all programs and all facilities)
- singular prograM and plural facilitIES (per one program, all facilities)
- singular prograM and singular facilitY (per program per facility)

I just thought to leave it directly as the table is structured in case language changes in the future, but can definitely change names if you feel we should
created_at: 2025-04-28T18:42:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,18 +321,24 @@ func (db *DB) GetNumberOfActiveUsersForTimePeriod(active bool, days int, facilit
 	return count, nil
 }
 
-func (db *DB) NewUsersInTimePeriod(days int, facilityId *uint) (int64, int64, error) {
+func (db *DB) NewUsersInTimePeriod(args *models.QueryContext, days int, facilityId *uint) (int64, int64, error) {
 	var admin_count int64
 	var user_count int64
 	daysAgo := time.Now().AddDate(0, 0, -days)
-	tx := db.Model(&models.User{}).Where("created_at >= ? AND role NOT IN ('system_admin', 'admin')", daysAgo)
+	tx := db.WithContext(args.Ctx).Model(&models.User{})
+	if days != -1 {
+		tx = tx.Where("created_at >= ? AND role NOT IN ('system_admin', 'admin')", daysAgo)
+	}
path: backend/src/database/users.go
user: login: jtucholski
body: @calisio I noticed that this doesn't say `facility_admin` or `department_admin`? 
created_at: 2025-04-28T19:25:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -350,17 +364,17 @@ func (db *DB) GetTotalLogins(days int, facilityId *uint) (int64, error) {
 	return total, nil
 }
 
-func (db *DB) GetTotalUsers(facilityId *uint) (int64, int64, error) {
+func (db *DB) GetTotalUsers(args *models.QueryContext, facilityId *uint) (int64, int64, error) {
 	var totalResidents int64
 	var totalAdmins int64
-	tx := db.Model(&models.User{}).Where("role NOT IN ('admin', 'system_admin')")
+	tx := db.WithContext(args.Ctx).Model(&models.User{}).Where("role NOT IN ('admin', 'system_admin')")
path: backend/src/database/users.go
user: login: jtucholski
body: @calisio Will this count department_admins and facility_admins as residents?
created_at: 2025-04-28T19:27:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,8 @@ func (srv *Server) registerProgramsRoutes() []routeDef {
 	axx := []models.FeatureAccess{models.ProgramAccess}
 	return []routeDef{
 		{"GET /api/programs", srv.handleIndexPrograms, false, axx},
+		{"GET /api/programs-overview-stats", srv.handleIndexProgramsFacilitiesStats, true, axx},
+		{"GET /api/programs-overview-table", srv.handleIndexProgramsOverviewTable, true, axx},
path: backend/src/handlers/programs_handler.go
user: login: jtucholski
body: @calisio It feels like these might be too coupled to the front-end especially when naming it closely to how it'll be presented. What do you think about these?

`/api/programs/stats` or `api/programs/summary` for `/api/programs-overview-stats` 
and
`/api/programs/detailed-list` for `/api/programs-overview-table`

created_at: 2025-04-28T20:16:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +91,119 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&programsFacilitiesStats).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsFacilityStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilityStats models.ProgramsFacilitiesStats
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramFacilityHistory{}).
+		Select(`
+		COUNT(*) AS total_programs,
+		COALESCE(SUM(total_enrollments), 0) AS total_enrollments,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`).Where("facility_id = ?", args.FacilityID)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&programsFacilityStats).Error; err != nil {
+		return programsFacilityStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	return programsFacilityStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool, adminRole models.UserRole) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	var tableName string
+	var totalActiveFacilitiesQuery string
+	var totalActiveFacilitiesSubQuery string
+	if adminRole == models.FacilityAdmin {
+		tableName = "daily_program_facility_history"
+	} else {
+		tableName = "daily_program_facilities_history"
+		totalActiveFacilitiesQuery = "mr.total_active_facilities,"
+		totalActiveFacilitiesSubQuery = "total_active_facilities,"
+	}
+	tx := db.WithContext(args.Ctx).Table(tableName + " as dpfh").
+		Select(fmt.Sprintf(`
+			programs.id AS program_id,
+			programs.name AS program_name,
+			programs.archived_at AS archived_at,
+			%s
+			mr.total_enrollments AS total_enrollments,
+			mr.total_active_enrollments AS total_active_enrollments,
+			mr.total_classes AS total_classes,
+			COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(dpfh.total_enrollments), 0), 0) * 100 AS completion_rate,
+			COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(dpfh.total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+			STRING_AGG(DISTINCT pt.program_type::text, ',') AS program_types,
+			STRING_AGG(DISTINCT pct.credit_type::text, ',') AS credit_types,
+			programs.funding_type AS funding_type,
+			BOOL_OR(programs.is_active) AS status
+		`, totalActiveFacilitiesQuery)).
+		Joins(fmt.Sprintf(`
+			JOIN (
+				SELECT 
+					program_id,
+					%s
+					total_enrollments,
+					total_active_enrollments,
+					total_classes
+				FROM daily_program_facilities_history
+				WHERE date = (SELECT MAX(date) FROM daily_program_facilities_history)
+			) AS mr ON mr.program_id = dpfh.program_id
+		`, totalActiveFacilitiesSubQuery)).
+		Joins("JOIN programs ON programs.id = dpfh.program_id").
+		Joins("JOIN program_types pt ON pt.program_id = dpfh.program_id").
+		Joins("JOIN program_credit_types pct ON pct.program_id = dpfh.program_id").
+		Group(totalActiveFacilitiesQuery + "programs.id, programs.name, mr.total_enrollments, mr.total_active_enrollments, mr.total_classes, programs.funding_type")
+	if timeFilter > 0 {
+		tx = tx.Where("dpfh.date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if adminRole == models.FacilityAdmin {
+		tx = tx.Where("dpfh.facility_id = ?", args.FacilityID)
+	}
+	if !includeArchived {
+		tx = tx.Where("programs.archived_at IS NULL")
+	}
+	if len(args.Tags) > 0 {
+		tx = tx.Where("pt.program_id IN (?)", args.Tags)
+	}
+	if args.OrderBy != "" {
+		tx = tx.Order(args.OrderClause())
+	}
+	if args.Search != "" {
+		tx = tx.Where("LOWER(name) LIKE ? OR LOWER(description) LIKE ? ", args.SearchQuery(), args.SearchQuery())
+	}
+	if err := tx.Count(&args.Total).Limit(args.PerPage).Offset(args.CalcOffset()).Scan(&programsTable).Error; err != nil {
path: backend/src/database/programs.go
user: login: PThorpe92
body: IIRC doing `tx.Count()` inline like this here has caused us issues in the past since it's a separate query. we should move this out beforehand and check the error, then reuse the tx for this query.
created_at: 2025-04-28T23:25:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Select("COUNT(*) AS total_program_offerings").
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true").
+		Scan(&totalProgramOfferings).Error; err != nil {
+		log.Errorln("error getting total program offerings")
+		return err
+	}
+	history.TotalProgramOfferings = totalProgramOfferings
+
+	var totalFacilities int64
+	if err := db.WithContext(ctx).Model(&models.Facility{}).
+		Select("COUNT(*) AS total_facilities").
+		Scan(&totalFacilities).Error; err != nil {
+		log.Errorln("error getting total facilities")
+		return err
+	}
+	history.TotalFacilities = totalFacilities
+
+	var attendanceStats struct {
+		TotalAttendancesMarked int64 `json:"total_attendances_marked"`
+		TotalStudentsPresent   int64 `json:"total_students_present"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEventAttendance{}).
+		Select(`
+		COUNT(CASE WHEN attendance_status IS NOT NULL THEN 1 END) AS total_attendances_marked,
+		COALESCE(SUM(CASE WHEN attendance_status = 'present' THEN 1 ELSE 0 END), 0) AS total_students_present
+		`).
+		Scan(&attendanceStats).Error; err != nil {
+		log.Errorln("error getting total students present")
+		return err
+	}
+	history.TotalStudentsPresent = attendanceStats.TotalStudentsPresent
+	history.TotalAttendancesMarked = attendanceStats.TotalAttendancesMarked
+
+	history.Date = time.Now()
+
+	log.Infof("History struct before insert: %+v", history)
+	if err := db.Create(&history).Error; err != nil {
+		log.Errorln("error creating daily program history")
+		return err
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	programs := make([]models.DailyProgramFacilitiesHistory, 10)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
+		Joins("JOIN facilities_programs fp ON fp.program_id = programs.id").
+		Joins("LEFT JOIN program_class_enrollments pce ON pce.class_id = pc.id").
+		Joins("LEFT JOIN program_class_events pcev ON pcev.class_id = pc.id").
+		Joins("LEFT JOIN program_class_event_attendance pcea ON pcea.event_id = pcev.id").
+		Group("programs.id")
+	if err := tx.Scan(&programs).Error; err != nil {
+		log.Errorln("error getting daily program facilities history")
+		return err
+	}
+
+	now := time.Now()
+	for i := range programs {
+		programs[i].Date = now
+	}
+
+	if len(programs) > 0 {
+		if err := db.Create(&programs).Error; err != nil {
+			log.Errorln("error creating daily program facilities history")
+			return err
+		}
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilityHistory(ctx context.Context, db *gorm.DB) error {
+	histories := make([]models.DailyProgramFacilityHistory, 25)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			fp.facility_id AS facility_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
path: provider-middleware/daily_program_history.go
user: login: carddev81
body: @calisio I noticed that the data was off so I rechecked this query and in order to correct 99% of the issues you need to (1) move the join of facilities_programs up above the join on program_classes and (2) add the additional criteria of pc.facility_id = fp.facility_id to program_classes.  This will make sure the data is consistent with each facility.  Also, the count of `COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities` is not correct? should this even be a part of this query? if so then you will need to rework this scalar function, maybe a subselect count here?

```
 JOIN facilities_programs fp ON fp.program_id = programs.id
 LEFT JOIN program_classes pc ON pc.program_id = programs.id
        and pc.facility_id = fp.facility_id 
```
created_at: 2025-04-29T12:54:01Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: carddev81
body: @calisio I missed this during my initial testing of this, but I just realized that when I add a new program it will not show up in this listing until the cron/batch job runs to populate the history tables. Should these new programs be displayed in this listing as soon as you create them?  Here's a video of what I'm talking about:

[Programs.webm](https://github.com/user-attachments/assets/6810619f-554a-4891-bb8a-e2a49ff3ddab)


created_at: 2025-04-29T12:56:29Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: carddev81
body: Noticed the content in the table is out of alignment?

![image](https://github.com/user-attachments/assets/edbd7841-dd9c-4f1e-b88b-431eb15fc82c)

created_at: 2025-04-29T12:57:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +91,119 @@ func (db *DB) DeleteProgram(id int) error {
 	}
 	return nil
 }
+
+func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilitiesStats models.ProgramsFacilitiesStats
+	var totals struct {
+		TotalPrograms    int64 `json:"total_programs"`
+		TotalEnrollments int64 `json:"total_enrollments"`
+	}
+	if err := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select("total_programs, total_enrollments").
+		Order("date DESC").
+		First(&totals).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramsFacilitiesHistory{}).
+		Select(`
+		COALESCE(SUM(total_program_offerings) / NULLIF(SUM(total_facilities), 0), 0) AS avg_active_programs_per_facility,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&programsFacilitiesStats).Error; err != nil {
+		return programsFacilitiesStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	programsFacilitiesStats.TotalPrograms = totals.TotalPrograms
+	programsFacilitiesStats.TotalEnrollments = totals.TotalEnrollments
+	return programsFacilitiesStats, nil
+}
+
+func (db *DB) GetProgramsFacilityStats(args *models.QueryContext, timeFilter int) (models.ProgramsFacilitiesStats, error) {
+	var programsFacilityStats models.ProgramsFacilitiesStats
+	tx := db.WithContext(args.Ctx).Model(&models.DailyProgramFacilityHistory{}).
+		Select(`
+		COUNT(*) AS total_programs,
+		COALESCE(SUM(total_enrollments), 0) AS total_enrollments,
+		COALESCE(SUM(total_students_present) * 1.0 / NULLIF(SUM(total_attendances_marked), 0), 0) * 100 AS attendance_rate,
+		COALESCE(SUM(total_completions) * 1.0 / NULLIF(SUM(total_enrollments), 0), 0) * 100 AS completion_rate
+	`).Where("facility_id = ?", args.FacilityID)
+	if timeFilter > 0 {
+		tx = tx.Where("date >= ?", time.Now().AddDate(0, 0, -timeFilter))
+	}
+	if err := tx.Scan(&programsFacilityStats).Error; err != nil {
+		return programsFacilityStats, newGetRecordsDBError(err, "programs facilities stats")
+	}
+	return programsFacilityStats, nil
+}
+
+func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int, includeArchived bool, adminRole models.UserRole) ([]models.ProgramsOverviewTable, error) {
+	var programsTable []models.ProgramsOverviewTable
+	var tableName string
+	var totalActiveFacilitiesQuery string
+	var totalActiveFacilitiesSubQuery string
+	if adminRole == models.FacilityAdmin {
+		tableName = "daily_program_facility_history"
+	} else {
+		tableName = "daily_program_facilities_history"
+		totalActiveFacilitiesQuery = "mr.total_active_facilities,"
+		totalActiveFacilitiesSubQuery = "total_active_facilities,"
+	}
+	tx := db.WithContext(args.Ctx).Table(tableName + " as dpfh").
path: backend/src/database/programs.go
user: login: carddev81
body: @calisio this query is going to need some more work for facility admins. Currently this query pulls in a mix of data (combination of just the individual facility being queried for and all facilities). I think it would be better if we work through this together when you have time.
created_at: 2025-04-29T13:14:19Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: calisio
body: Right, I also noticed this. I am not sure what the best solution is, since we decided only one run per day. Maybe the cron job should also be triggered when creating a program so its updated and the user isn't lost on where that program went?
created_at: 2025-04-29T13:43:48Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: calisio
body: Oh weird, I thought I fixed that (was appearing on mine too). I think i must have missed a hyphen somewhere!
created_at: 2025-04-29T13:44:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Select("COUNT(*) AS total_program_offerings").
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true").
+		Scan(&totalProgramOfferings).Error; err != nil {
+		log.Errorln("error getting total program offerings")
+		return err
+	}
+	history.TotalProgramOfferings = totalProgramOfferings
+
+	var totalFacilities int64
+	if err := db.WithContext(ctx).Model(&models.Facility{}).
+		Select("COUNT(*) AS total_facilities").
+		Scan(&totalFacilities).Error; err != nil {
+		log.Errorln("error getting total facilities")
+		return err
+	}
+	history.TotalFacilities = totalFacilities
+
+	var attendanceStats struct {
+		TotalAttendancesMarked int64 `json:"total_attendances_marked"`
+		TotalStudentsPresent   int64 `json:"total_students_present"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEventAttendance{}).
+		Select(`
+		COUNT(CASE WHEN attendance_status IS NOT NULL THEN 1 END) AS total_attendances_marked,
+		COALESCE(SUM(CASE WHEN attendance_status = 'present' THEN 1 ELSE 0 END), 0) AS total_students_present
+		`).
+		Scan(&attendanceStats).Error; err != nil {
+		log.Errorln("error getting total students present")
+		return err
+	}
+	history.TotalStudentsPresent = attendanceStats.TotalStudentsPresent
+	history.TotalAttendancesMarked = attendanceStats.TotalAttendancesMarked
+
+	history.Date = time.Now()
+
+	log.Infof("History struct before insert: %+v", history)
+	if err := db.Create(&history).Error; err != nil {
+		log.Errorln("error creating daily program history")
+		return err
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	programs := make([]models.DailyProgramFacilitiesHistory, 10)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
+		Joins("JOIN facilities_programs fp ON fp.program_id = programs.id").
+		Joins("LEFT JOIN program_class_enrollments pce ON pce.class_id = pc.id").
+		Joins("LEFT JOIN program_class_events pcev ON pcev.class_id = pc.id").
+		Joins("LEFT JOIN program_class_event_attendance pcea ON pcea.event_id = pcev.id").
+		Group("programs.id")
+	if err := tx.Scan(&programs).Error; err != nil {
+		log.Errorln("error getting daily program facilities history")
+		return err
+	}
+
+	now := time.Now()
+	for i := range programs {
+		programs[i].Date = now
+	}
+
+	if len(programs) > 0 {
+		if err := db.Create(&programs).Error; err != nil {
+			log.Errorln("error creating daily program facilities history")
+			return err
+		}
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilityHistory(ctx context.Context, db *gorm.DB) error {
+	histories := make([]models.DailyProgramFacilityHistory, 25)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			fp.facility_id AS facility_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
path: provider-middleware/daily_program_history.go
user: login: calisio
body: Just to double check, I believe this should ONLY be for the program facility history insert, correct? (as in, i only need to add `AND pc.facility_id = fp.facility_id` to the program facility one) also, yes, I totally missed removing total_active_facilities! It is so similar to the query above I completely skipped on removing it, my bad!
created_at: 2025-04-29T14:43:13Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: calisio
body: <img width="1512" alt="Screenshot 2025-04-29 at 10 46 43 AM" src="https://github.com/user-attachments/assets/5d75805b-e995-4349-a03d-56c59dafb48b" />
hm... weird... mind doesn't look like that? Is there anything special that you did to get this result? Just making sure, you are logged in as a department level admin, correct?
created_at: 2025-04-29T14:47:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Select("COUNT(*) AS total_program_offerings").
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true").
+		Scan(&totalProgramOfferings).Error; err != nil {
+		log.Errorln("error getting total program offerings")
+		return err
+	}
+	history.TotalProgramOfferings = totalProgramOfferings
+
+	var totalFacilities int64
+	if err := db.WithContext(ctx).Model(&models.Facility{}).
+		Select("COUNT(*) AS total_facilities").
+		Scan(&totalFacilities).Error; err != nil {
+		log.Errorln("error getting total facilities")
+		return err
+	}
+	history.TotalFacilities = totalFacilities
+
+	var attendanceStats struct {
+		TotalAttendancesMarked int64 `json:"total_attendances_marked"`
+		TotalStudentsPresent   int64 `json:"total_students_present"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEventAttendance{}).
+		Select(`
+		COUNT(CASE WHEN attendance_status IS NOT NULL THEN 1 END) AS total_attendances_marked,
+		COALESCE(SUM(CASE WHEN attendance_status = 'present' THEN 1 ELSE 0 END), 0) AS total_students_present
+		`).
+		Scan(&attendanceStats).Error; err != nil {
+		log.Errorln("error getting total students present")
+		return err
+	}
+	history.TotalStudentsPresent = attendanceStats.TotalStudentsPresent
+	history.TotalAttendancesMarked = attendanceStats.TotalAttendancesMarked
+
+	history.Date = time.Now()
+
+	log.Infof("History struct before insert: %+v", history)
+	if err := db.Create(&history).Error; err != nil {
+		log.Errorln("error creating daily program history")
+		return err
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	programs := make([]models.DailyProgramFacilitiesHistory, 10)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
+		Joins("JOIN facilities_programs fp ON fp.program_id = programs.id").
+		Joins("LEFT JOIN program_class_enrollments pce ON pce.class_id = pc.id").
+		Joins("LEFT JOIN program_class_events pcev ON pcev.class_id = pc.id").
+		Joins("LEFT JOIN program_class_event_attendance pcea ON pcea.event_id = pcev.id").
+		Group("programs.id")
+	if err := tx.Scan(&programs).Error; err != nil {
+		log.Errorln("error getting daily program facilities history")
+		return err
+	}
+
+	now := time.Now()
+	for i := range programs {
+		programs[i].Date = now
+	}
+
+	if len(programs) > 0 {
+		if err := db.Create(&programs).Error; err != nil {
+			log.Errorln("error creating daily program facilities history")
+			return err
+		}
+	}
+
+	return nil
+}
+
+func InsertDailyProgramFacilityHistory(ctx context.Context, db *gorm.DB) error {
+	histories := make([]models.DailyProgramFacilityHistory, 25)
+	tx := db.Model(&models.Program{}).
+		Select(`
+			programs.id AS program_id,
+			fp.facility_id AS facility_id,
+			COUNT(DISTINCT CASE WHEN programs.is_active THEN fp.facility_id END) AS total_active_facilities,
+			COUNT(DISTINCT pce.id) AS total_enrollments,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Completed' THEN pce.id END) AS total_completions,
+			COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' THEN pce.id END) AS total_active_enrollments,
+			COUNT(DISTINCT CASE WHEN pc.status != 'Cancelled' THEN pc.id END) AS total_classes,
+			COUNT(DISTINCT CASE WHEN programs.archived_at IS NOT NULL THEN programs.id END) AS total_archived_classes,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status IS NOT NULL THEN pcea.id END) AS total_attendances_marked,
+			COUNT(DISTINCT CASE WHEN pcea.attendance_status = 'present' THEN pcea.id END) AS total_students_present
+		`).
+		Joins("LEFT JOIN program_classes pc ON pc.program_id = programs.id").
path: provider-middleware/daily_program_history.go
user: login: carddev81
body: correct this should only be for the insert into the `daily_program_facility_history` table.
created_at: 2025-04-29T17:21:04Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: carddev81
body: logged in as a superadmin and facility admin.
created_at: 2025-04-29T17:22:47Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: jtucholski
body: Same, I'm looking at it too to see if something sticks out to me.
created_at: 2025-04-29T19:36:26Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: calisio
body: It's a strange bug, I am only able to reproduce if I quit, then run `make dev` again. But then as soon as I make any edit on `ProgramManagement.tsx`, it has the correct columns. I made a change just now that incorporates the entire `grid-cols-{num}`; let me know if that fixed it.
created_at: 2025-04-29T20:26:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,189 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
+			SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS total_archived_programs
+		`).
+		Scan(&programStats).Error; err != nil {
+		log.Errorln("error getting total, active, archived programs")
+		return err
+	}
+	history.TotalPrograms = programStats.TotalPrograms
+	history.TotalActivePrograms = programStats.TotalActivePrograms
+	history.TotalArchivedPrograms = programStats.TotalArchivedPrograms
+
+	var enrollmentStats struct {
+		TotalEnrollments int64 `json:"total_enrollments"`
+		TotalCompletions int64 `json:"total_completions"`
+	}
+	if err := db.WithContext(ctx).Model(&models.ProgramClassEnrollment{}).
+		Select(`
+			COUNT(*) AS total_enrollments,
+			SUM(CASE WHEN enrollment_status = 'Completed' THEN 1 ELSE 0 END) AS total_completions
+		`).
+		Scan(&enrollmentStats).Error; err != nil {
+		log.Errorln("error getting total, completed enrollments")
+		return err
+	}
+	history.TotalEnrollments = enrollmentStats.TotalEnrollments
+	history.TotalCompletions = enrollmentStats.TotalCompletions
+
+	var totalProgramOfferings int64
+	if err := db.WithContext(ctx).Model(&models.FacilitiesPrograms{}).
+		Select("COUNT(*) AS total_program_offerings").
+		Joins("JOIN programs ON programs.id = facilities_programs.program_id AND programs.is_active = true").
path: provider-middleware/daily_program_history.go
user: login: jtucholski
body: @calisio Do we need to filter out programs where the archived_at IS NOT NULL so that we don't count  archived ones?
created_at: 2025-04-29T23:26:09Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: jtucholski
body: @calisio It looks better for me after pulling latest.
created_at: 2025-04-30T13:20:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,189 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
path: provider-middleware/daily_program_history.go
user: login: jtucholski
body: @calisio Do we need to look for is_active = true AND archived_at IS NULL? Otherwise this would include programs that were archived as active.
created_at: 2025-04-30T13:31:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,189 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
path: provider-middleware/daily_program_history.go
user: login: calisio
body: Ahh, is this what you mentioned in slack? So programs that are active CAN be archived? I will add this in now.
created_at: 2025-04-30T13:48:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,189 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
path: provider-middleware/daily_program_history.go
user: login: jtucholski
body: Yes. I'm anticipating that possibility happening. Part of me considered setting it to inactive before archiving it, but I think that creates some inconsistency with the data. 

In the case of classes, they can be Completed OR Cancelled and still get archived. We treat archived as a separate value so that we could preserve the class status before it was archived.
created_at: 2025-04-30T14:07:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,189 @@
+package main
+
+import (
+	"UnlockEdv2/src/models"
+	"context"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+)
+
+func InsertDailyProgHistory(ctx context.Context, db *gorm.DB) error {
+	if err := InsertDailyProgramsFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily programs facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilitiesHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facilities history")
+		return err
+	}
+	if err := InsertDailyProgramFacilityHistory(ctx, db); err != nil {
+		log.Errorln("error inserting daily program facility history")
+		return err
+	}
+	return nil
+}
+
+func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) error {
+	var history models.DailyProgramsFacilitiesHistory
+	var programStats struct {
+		TotalPrograms         int64 `json:"total_programs"`
+		TotalActivePrograms   int64 `json:"total_active_programs"`
+		TotalArchivedPrograms int64 `json:"total_archived_programs"`
+	}
+	if err := db.WithContext(ctx).Model(&models.Program{}).
+		Select(`
+			COUNT(*) AS total_programs,
+			SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS total_active_programs,
path: provider-middleware/daily_program_history.go
user: login: calisio
body: Great, I just pushed a change that fixes this.
created_at: 2025-04-30T14:19:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -316,18 +321,24 @@ func (db *DB) GetNumberOfActiveUsersForTimePeriod(active bool, days int, facilit
 	return count, nil
 }
 
-func (db *DB) NewUsersInTimePeriod(days int, facilityId *uint) (int64, int64, error) {
+func (db *DB) NewUsersInTimePeriod(args *models.QueryContext, days int, facilityId *uint) (int64, int64, error) {
 	var admin_count int64
 	var user_count int64
 	daysAgo := time.Now().AddDate(0, 0, -days)
-	tx := db.Model(&models.User{}).Where("created_at >= ? AND role NOT IN ('system_admin', 'admin')", daysAgo)
+	tx := db.WithContext(args.Ctx).Model(&models.User{})
+	if days != -1 {
+		tx = tx.Where("created_at >= ? AND role NOT IN ('system_admin', 'admin')", daysAgo)
+	}
path: backend/src/database/users.go
user: login: calisio
body: Just so I can mark this comment resolved, we decided to handle these issues in a different ticket since filtering seemed to be a larger issue. (ie coupling these updates with [revisit mixing admin & resident user logins on operational insights](https://app.asana.com/1/1201607307149189/project/1209460078641109/task/1210093059550570?focus=true))

-- this PR does not modify any functionality based on filtering, just adds in the time filter.
created_at: 2025-04-30T14:23:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +87,7 @@ func InsertDailyProgramsFacilitiesHistory(ctx context.Context, db *gorm.DB) erro
 	}
 	if err := db.WithContext(ctx).Model(&models.ProgramClassEventAttendance{}).
 		Select(`
-		COUNT(CASE WHEN attendance_status IS NOT NULL THEN 1 END) AS total_attendances_marked,
+		COUNT(CASE WHEN attendance_status IS NOT NULL AND pcea.attendance_status != '' THEN 1 END) AS total_attendances_marked,
path: provider-middleware/daily_program_history.go
user: login: carddev81
body: @calisio remove the alias pcea here it causes issue during execution
created_at: 2025-04-30T15:03:20Z
author_association: CONTRIBUTOR
path: frontend/src/Pages/ProgramManagement.tsx
user: login: carddev81
body: @calisio it is good on my end too. Just ran through my tests
created_at: 2025-04-30T15:24:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: calisio
created_at: 2025-04-23T23:36:35Z
body: Update: Finished up the api calls and now the frontend is pulling in real time data from the backend. For it to pull in anything, you'll have to kick off the task to insert into the tables for at least today. To do this, you'll have to check for the `job_id` in the `runnable_tasks` table. It should be the row with a null value for `provider_platform_id` AND `open_content_provider_id`. It usually prints on initialization of the platform, but in case you miss it you can query into the db to grab it. Then you have to run `nats pub tasks.daily_prog_history '{"job_id":"JOB ID FROM THE TABLE"}'` (obviously fill in whatever job_id you get from the table).
This should populate your tables, so it will fill out the program management page.
I am still looking over calculations to make sure they are saved/done correctly, and need to handle for some cases in the frontend (for some reason `program_types` just refuses to populate in the query???).
I also updated the screenshot to show what it now looks like. I still need to implement pagination for the table too.
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-04-24T15:07:02Z
body: Full steps to get it up and working:
1. Run `make reset` to get migrations
2. Run `make dev` -- you'll have to run this TWICE for the cron jobs to actually kick off. In the middle of all of the logs, it should print something like 
```
cron-tasks-1        | {"level":"info","msg":"initializing database","time":"2025-04-24T14:57:16Z"}
cron-tasks-1        | {"level":"info","msg":"Connected to the PostgreSQL database","time":"2025-04-24T14:57:16Z"}
cron-tasks-1        | {"level":"info","msg":"CronJob daily_prog_history has ID: 99ca80b5-8c0c-4545-82ce-ef08cf529aff","time":"2025-04-24T14:57:16Z"}
cron-tasks-1        | {"level":"info","msg":"Generated 1 total tasks","time":"2025-04-24T14:57:16Z"}
```
here, you can grab the id (in this case it is `99ca80b5-8c0c-4545-82ce-ef08cf529aff`). If you miss this in the logs, you can also get it by checking for the `job_id` in the `runnable_tasks` table. It should be the row with a null value for `provider_platform_id` AND `open_content_provider_id`.
3. Run `make seed` (you need the tables populated to do the calculations)
4. Run `nats pub tasks.daily_prog_history '{"job_id":"JOB ID FROM THE TABLE"}'` (obviously fill in whatever `job_id` you get from the table).

That should update the table with at least a record for today, which should be enough to populate the programs page! The filter won't change anything for now since you only have 1 row in the table.
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-04-25T15:56:09Z
body: PS- I updated the dropdown filter so it would be consistent across pages, but there might be a small bug in that. I'm looking into it this afternoon. Also, I decided to leave out fixing tags from this PR since it was already an existing problem, so should be a separate ticket.

Just to note- I decided to go with two separate queries for this page because I didn't want the stats cards to re render when the user used the search or filter- it caused unnecessary flashing. I know we've changed a lot of the api naming, so open to changes on that
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-04-25T16:22:02Z
body: Oops- forgot to include filtering for archived programs. Adding that now!
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-04-25T16:44:32Z
body: <img width="1512" alt="Screenshot 2025-04-25 at 12 42 50 PM" src="https://github.com/user-attachments/assets/49b38c48-7860-4a1f-95a6-5df4ebb0bb3a" />
all done! should be updated ui now
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-04-28T22:24:15Z
body: @jtucholski @carddev81 The last commit I have pushed updates the query to handle for a facility-level admin to get the correct information for them. It changes some of the logic in the backend queries, so just wanted to make you aware of that (especially Rich who has already done a full review). Rich, it might be easiest to just directly look at what I updated in that specific commit since you have already reviewed the rest (to be able to see the direct changes made). Let me know if I missed anything/if anything does not make sense!

While it was not part of the original ticket, I removed Average programs/facility for the facility level view. I did not think this was relevant information since it is essentially the total programs (since we are only looking at one facility). 
author_association: CONTRIBUTOR
user: login: calisio
created_at: 2025-04-30T13:46:54Z
body: Reformatted to pull all programs and display '--' for the programs that are not updated. I have used a null values component to have a tooltip to display that the data will be updated at the end of the day. 
<img width="1512" alt="Screenshot 2025-04-29 at 4 35 17 PM" src="https://github.com/user-attachments/assets/fc72c17f-6fe8-41fe-bb6e-3a86c0ee0f7e" />

author_association: CONTRIBUTOR