@@ -34,10 +34,10 @@ func (db *DB) FetchEnrollmentMetrics(programID int, facilityId uint) (*models.Pr
3434 COUNT(CASE WHEN pce.enrollment_status = 'Completed' THEN 1 END) AS completions,
3535 COUNT(CASE WHEN pce.enrolled_at IS NOT NULL THEN 1 END) AS total_enrollments,
3636 COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' AND pce.enrolled_at IS NOT NULL AND (pce.enrollment_ended_at IS NULL OR pce.enrollment_ended_at > CURRENT_TIMESTAMP) THEN pce.user_id END) as active_residents,
37- CASE
37+ CASE
3838 WHEN COUNT(CASE WHEN pce.enrolled_at IS NOT NULL THEN 1 END) = 0 THEN 0
3939 WHEN COUNT(CASE WHEN pce.enrollment_status = 'Enrolled' AND pce.enrolled_at IS NOT NULL AND (pce.enrollment_ended_at IS NULL OR pce.enrollment_ended_at > CURRENT_TIMESTAMP) THEN 1 END) = 0
40- AND COUNT(CASE WHEN pce.enrollment_status = 'Completed' THEN 1 END) = COUNT(CASE WHEN pce.enrolled_at IS NOT NULL THEN 1 END)
40+ AND COUNT(CASE WHEN pce.enrollment_status = 'Completed' THEN 1 END) = COUNT(CASE WHEN pce.enrolled_at IS NOT NULL THEN 1 END)
4141 THEN 100
4242 ELSE COUNT(CASE WHEN pce.enrollment_status = 'Completed' THEN 1 END) * 1.0 / COUNT(CASE WHEN pce.enrolled_at IS NOT NULL THEN 1 END) * 100
4343 END AS completion_rate
@@ -363,20 +363,14 @@ func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter i
363363 attendanceTimeFilter = completionTimeFilter
364364 }
365365
366- castMinutes := "COALESCE(pcea.minutes_attended, pcea.scheduled_minutes, 0)::numeric"
367- if db .Name () == "sqlite" {
368- castMinutes = "CAST(COALESCE(pcea.minutes_attended, pcea.scheduled_minutes, 0) AS REAL)"
369- }
370- ratioExpr := fmt .Sprintf ("%s / NULLIF(COALESCE(pcea.scheduled_minutes, pcea.minutes_attended, 0), 0)" , castMinutes )
371- ratioCapped := fmt .Sprintf ("CASE WHEN %s < 1 THEN %s ELSE 1 END" , ratioExpr , ratioExpr )
372-
366+ partialAttendanceSQL := buildPartialAttendanceSQL (db .Name (), "pcea" )
373367 var avgResult struct {
374368 TotalAssociations int64 `json:"total_associations"`
375369 TotalFacilities int64 `json:"total_facilities"`
376370 }
377371
378372 if err := db .WithContext (args .Ctx ).Raw (`
379- SELECT
373+ SELECT
380374 COUNT(DISTINCT fp.id) as total_associations,
381375 COUNT(DISTINCT f.id) as total_facilities
382376 FROM facilities f
@@ -396,8 +390,8 @@ func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter i
396390 }
397391
398392 query := fmt .Sprintf (`
399- SELECT
400- COALESCE(COUNT(CASE WHEN pce.enrollment_status = 'Completed' AND pce.enrolled_at IS NOT NULL %s THEN 1 END) * 100.0 /
393+ SELECT
394+ COALESCE(COUNT(CASE WHEN pce.enrollment_status = 'Completed' AND pce.enrolled_at IS NOT NULL %s THEN 1 END) * 100.0 /
401395 NULLIF(COUNT(CASE WHEN pce.enrolled_at IS NOT NULL THEN 1 END), 0), 0) AS completion_rate,
402396 COALESCE(SUM(
403397 CASE
@@ -412,7 +406,7 @@ func (db *DB) GetProgramsFacilitiesStats(args *models.QueryContext, timeFilter i
412406 LEFT JOIN program_class_events pcev ON pcev.class_id = pc.id
413407 LEFT JOIN program_class_event_attendance pcea ON pcea.event_id = pcev.id AND pcea.user_id = pce.user_id
414408 WHERE pce.enrolled_at IS NOT NULL %s
415- ` , completionTimeFilter , attendanceTimeFilter , attendanceTimeFilter , ratioCapped , attendanceTimeFilter , completionTimeFilter )
409+ ` , completionTimeFilter , attendanceTimeFilter , attendanceTimeFilter , partialAttendanceSQL , attendanceTimeFilter , completionTimeFilter )
416410
417411 var completionAttendanceRates struct {
418412 AttendanceRate float64 `json:"attendance_rate"`
@@ -477,16 +471,10 @@ func (db *DB) GetProgramsFacilityStats(args *models.QueryContext, timeFilter int
477471 attendanceTimeFilter = completionTimeFilter
478472 }
479473
480- castMinutes := "COALESCE(pcea.minutes_attended, pcea.scheduled_minutes, 0)::numeric"
481- if db .Name () == "sqlite" {
482- castMinutes = "CAST(COALESCE(pcea.minutes_attended, pcea.scheduled_minutes, 0) AS REAL)"
483- }
484- ratioExpr := fmt .Sprintf ("%s / NULLIF(COALESCE(pcea.scheduled_minutes, pcea.minutes_attended, 0), 0)" , castMinutes )
485- ratioCapped := fmt .Sprintf ("CASE WHEN %s < 1 THEN %s ELSE 1 END" , ratioExpr , ratioExpr )
486-
474+ partialAttendanceSQL := buildPartialAttendanceSQL (db .Name (), "pcea" )
487475 query := fmt .Sprintf (`
488- SELECT
489- COALESCE(COUNT(CASE WHEN pce.enrollment_status = 'Completed' AND pce.enrolled_at IS NOT NULL %s THEN 1 END) * 100.0 /
476+ SELECT
477+ COALESCE(COUNT(CASE WHEN pce.enrollment_status = 'Completed' AND pce.enrolled_at IS NOT NULL %s THEN 1 END) * 100.0 /
490478 NULLIF(COUNT(CASE WHEN pce.enrolled_at IS NOT NULL THEN 1 END), 0), 0) AS completion_rate,
491479 COALESCE(SUM(
492480 CASE
@@ -501,7 +489,7 @@ func (db *DB) GetProgramsFacilityStats(args *models.QueryContext, timeFilter int
501489 LEFT JOIN program_class_events pcev ON pcev.class_id = pc.id
502490 LEFT JOIN program_class_event_attendance pcea ON pcea.event_id = pcev.id AND pcea.user_id = pce.user_id
503491 WHERE pc.facility_id = ? AND pce.enrolled_at IS NOT NULL %s
504- ` , completionTimeFilter , attendanceTimeFilter , attendanceTimeFilter , ratioCapped , attendanceTimeFilter , completionTimeFilter )
492+ ` , completionTimeFilter , attendanceTimeFilter , attendanceTimeFilter , partialAttendanceSQL , attendanceTimeFilter , completionTimeFilter )
505493
506494 if err := db .WithContext (args .Ctx ).Raw (query , args .FacilityID ).Scan (& rateStats ).Error ; err != nil {
507495 return programsFacilityStats , newGetRecordsDBError (err , "facility completion and attendance rates" )
@@ -596,7 +584,7 @@ func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int
596584 if adminRole == models .FacilityAdmin {
597585 currentMetricsSubquery = fmt .Sprintf (`
598586 LEFT JOIN (
599- SELECT
587+ SELECT
600588 p.id as program_id,
601589 COUNT(DISTINCT pce.id) AS total_enrollments,
602590 COUNT(DISTINCT CASE WHEN pce.enrollment_status = 'Enrolled' AND pce.enrolled_at IS NOT NULL AND (pce.enrollment_ended_at IS NULL OR pce.enrollment_ended_at > CURRENT_TIMESTAMP) THEN pce.id END) AS total_active_enrollments,
@@ -612,7 +600,7 @@ func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int
612600 } else {
613601 currentMetricsSubquery = `
614602 LEFT JOIN (
615- SELECT
603+ SELECT
616604 p.id as program_id,
617605 COUNT(DISTINCT CASE WHEN p.is_active = true AND p.archived_at IS NULL THEN fp.facility_id END) AS total_active_facilities,
618606 COUNT(DISTINCT pce.id) AS total_enrollments,
@@ -640,13 +628,7 @@ func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int
640628 facilityFilterForRates = fmt .Sprintf ("AND pc.facility_id = %d" , args .FacilityID )
641629 }
642630
643- rateCast := "COALESCE(pcea.minutes_attended, pcea.scheduled_minutes, 0)::numeric"
644- if db .Name () == "sqlite" {
645- rateCast = "CAST(COALESCE(pcea.minutes_attended, pcea.scheduled_minutes, 0) AS REAL)"
646- }
647- rateRatio := fmt .Sprintf ("%s / NULLIF(COALESCE(pcea.scheduled_minutes, pcea.minutes_attended, 0), 0)" , rateCast )
648- rateCapped := fmt .Sprintf ("CASE WHEN %s < 1 THEN %s ELSE 1 END" , rateRatio , rateRatio )
649-
631+ partialAttendanceSQL := buildPartialAttendanceSQL (db .Name (), "pcea" )
650632 timeFilteredRatesSubquery := `
651633 LEFT JOIN (
652634 SELECT
@@ -656,7 +638,7 @@ func (db *DB) GetProgramsOverviewTable(args *models.QueryContext, timeFilter int
656638 COALESCE(SUM(
657639 CASE
658640 WHEN pcea.attendance_status = 'present' ` + timeFilterCondition + ` THEN 1
659- WHEN pcea.attendance_status = 'partial' ` + timeFilterCondition + ` THEN ` + rateCapped + `
641+ WHEN pcea.attendance_status = 'partial' ` + timeFilterCondition + ` THEN ` + partialAttendanceSQL + `
660642 ELSE 0
661643 END
662644 ) * 100.0 /
@@ -790,13 +772,7 @@ func (db *DB) GetProgramsCSVData(args *models.QueryContext) ([]models.ProgramCSV
790772 models .EnrollmentIncompleteTransfered ,
791773 }
792774
793- csvRateCast := "COALESCE(pca.minutes_attended, pca.scheduled_minutes, 0)::numeric"
794- if db .Name () == "sqlite" {
795- csvRateCast = "CAST(COALESCE(pca.minutes_attended, pca.scheduled_minutes, 0) AS REAL)"
796- }
797- csvRateRatio := fmt .Sprintf ("%s / NULLIF(COALESCE(pca.scheduled_minutes, pca.minutes_attended, 0), 0)" , csvRateCast )
798- csvRateCapped := fmt .Sprintf ("CASE WHEN %s < 1 THEN %s ELSE 1 END" , csvRateRatio , csvRateRatio )
799-
775+ partialAttendanceSQL := buildPartialAttendanceSQL (db .Name (), "pca" )
800776 tx := db .WithContext (args .Ctx ).Table ("programs p" ).
801777 Joins ("JOIN program_classes pc ON pc.program_id = p.id" ).
802778 Joins ("JOIN program_class_enrollments pe ON pe.class_id = pc.id" ).
@@ -805,7 +781,7 @@ func (db *DB) GetProgramsCSVData(args *models.QueryContext) ([]models.ProgramCSV
805781 Joins ("LEFT JOIN program_completions c on c.program_class_id = pc.id and c.user_id = u.id" ).
806782 Joins ("LEFT JOIN program_class_events pce on pce.class_id = pc.id" ).
807783 Joins ("LEFT JOIN program_class_event_attendance pca ON pca.event_id = pce.id AND pca.user_id = u.id" ).
808- Select (`
784+ Select (`
809785 f.name AS facility_name,
810786 p.name AS program_name,
811787 pc.name AS class_name,
@@ -819,18 +795,18 @@ func (db *DB) GetProgramsCSVData(args *models.QueryContext) ([]models.ProgramCSV
819795 CASE
820796 WHEN c.id IS NOT NULL THEN c.created_at
821797 WHEN pe.enrollment_status IN (?) THEN pe.updated_at
822- END,
798+ END,
823799 pc.end_dt
824800 ) as end_date,
825801 pe.enrollment_status AS end_status,
826- CASE
802+ CASE
827803 WHEN COUNT(CASE WHEN pca.attendance_status IS NOT NULL AND pca.attendance_status != '' THEN 1 END) = 0 THEN 0
828- ELSE
804+ ELSE
829805 COALESCE(
830806 SUM(
831807 CASE
832808 WHEN pca.attendance_status = 'present' THEN 1
833- WHEN pca.attendance_status = 'partial' THEN ` + csvRateCapped + `
809+ WHEN pca.attendance_status = 'partial' THEN ` + partialAttendanceSQL + `
834810 ELSE 0
835811 END
836812 ) * 100.0 /
@@ -840,7 +816,7 @@ func (db *DB) GetProgramsCSVData(args *models.QueryContext) ([]models.ProgramCSV
840816 END AS attendance_percentage` , statuses ).
841817 Where (`
842818 (
843- c.id IS NOT NULL
819+ c.id IS NOT NULL
844820 OR pe.enrollment_status IN (?)
845821 )
846822 ` , statuses ).
@@ -856,3 +832,14 @@ func (db *DB) GetProgramsCSVData(args *models.QueryContext) ([]models.ProgramCSV
856832 }
857833 return programCSVData , nil
858834}
835+
836+ func buildPartialAttendanceSQL (dialect , alias string ) string {
837+ minutesCast := "COALESCE(%s.minutes_attended, %s.scheduled_minutes, 0)::numeric"
838+ if dialect == "sqlite" {
839+ minutesCast = "CAST(COALESCE(%s.minutes_attended, %s.scheduled_minutes, 0) AS REAL)"
840+ }
841+ minutesCast = fmt .Sprintf (minutesCast , alias , alias )
842+ ratioExpr := fmt .Sprintf ("%s / NULLIF(COALESCE(%s.scheduled_minutes, %s.minutes_attended, 0), 0)" , minutesCast , alias , alias )
843+ partialAttendanceSQL := fmt .Sprintf ("CASE WHEN %s < 1 THEN %s ELSE 1 END" , ratioExpr , ratioExpr )
844+ return partialAttendanceSQL
845+ }
0 commit comments