package services import ( "context" "time" "github.com/breakpilot/school-service/internal/models" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // GradebookService handles gradebook-related operations type GradebookService struct { db *pgxpool.Pool } // NewGradebookService creates a new GradebookService func NewGradebookService(db *pgxpool.Pool) *GradebookService { return &GradebookService{db: db} } // Attendance Operations // CreateAttendance creates or updates an attendance record func (s *GradebookService) CreateAttendance(ctx context.Context, req *models.CreateAttendanceRequest) (*models.Attendance, error) { date, _ := time.Parse("2006-01-02", req.Date) periods := req.Periods if periods == 0 { periods = 1 } var attendance models.Attendance err := s.db.QueryRow(ctx, ` INSERT INTO attendance (student_id, date, status, periods, reason) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (student_id, date) DO UPDATE SET status = EXCLUDED.status, periods = EXCLUDED.periods, reason = EXCLUDED.reason RETURNING id, student_id, date, status, periods, reason, created_at `, req.StudentID, date, req.Status, periods, req.Reason).Scan( &attendance.ID, &attendance.StudentID, &attendance.Date, &attendance.Status, &attendance.Periods, &attendance.Reason, &attendance.CreatedAt, ) return &attendance, err } // GetClassAttendance returns attendance records for a class func (s *GradebookService) GetClassAttendance(ctx context.Context, classID string, startDate, endDate *time.Time) ([]models.Attendance, error) { query := ` SELECT a.id, a.student_id, a.date, a.status, a.periods, a.reason, a.created_at, CONCAT(st.first_name, ' ', st.last_name) as student_name FROM attendance a JOIN students st ON a.student_id = st.id WHERE st.class_id = $1 ` args := []interface{}{classID} if startDate != nil { query += ` AND a.date >= $2` args = append(args, startDate) } if endDate != nil { query += ` AND a.date <= $` + string(rune('0'+len(args)+1)) args = append(args, endDate) } query += ` ORDER BY a.date DESC, st.last_name, st.first_name` rows, err := s.db.Query(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var records []models.Attendance for rows.Next() { var a models.Attendance if err := rows.Scan(&a.ID, &a.StudentID, &a.Date, &a.Status, &a.Periods, &a.Reason, &a.CreatedAt, &a.StudentName); err != nil { return nil, err } records = append(records, a) } return records, nil } // GetStudentAttendance returns attendance records for a student func (s *GradebookService) GetStudentAttendance(ctx context.Context, studentID string) ([]models.Attendance, error) { rows, err := s.db.Query(ctx, ` SELECT id, student_id, date, status, periods, reason, created_at FROM attendance WHERE student_id = $1 ORDER BY date DESC `, studentID) if err != nil { return nil, err } defer rows.Close() var records []models.Attendance for rows.Next() { var a models.Attendance if err := rows.Scan(&a.ID, &a.StudentID, &a.Date, &a.Status, &a.Periods, &a.Reason, &a.CreatedAt); err != nil { return nil, err } records = append(records, a) } return records, nil } // GetAttendanceSummary returns absence counts for a student func (s *GradebookService) GetAttendanceSummary(ctx context.Context, studentID string, schoolYearID string) (int, int, error) { var excused, unexcused int err := s.db.QueryRow(ctx, ` SELECT COALESCE(SUM(CASE WHEN status = 'absent_excused' THEN periods ELSE 0 END), 0) as excused, COALESCE(SUM(CASE WHEN status = 'absent_unexcused' THEN periods ELSE 0 END), 0) as unexcused FROM attendance a JOIN students st ON a.student_id = st.id JOIN classes c ON st.class_id = c.id WHERE a.student_id = $1 AND c.school_year_id = $2 `, studentID, schoolYearID).Scan(&excused, &unexcused) return excused, unexcused, err } // DeleteAttendance deletes an attendance record func (s *GradebookService) DeleteAttendance(ctx context.Context, attendanceID string) error { _, err := s.db.Exec(ctx, `DELETE FROM attendance WHERE id = $1`, attendanceID) return err } // Gradebook Entry Operations // CreateGradebookEntry creates a new gradebook entry func (s *GradebookService) CreateGradebookEntry(ctx context.Context, req *models.CreateGradebookEntryRequest) (*models.GradebookEntry, error) { date, _ := time.Parse("2006-01-02", req.Date) var studentID *uuid.UUID if req.StudentID != "" { id, _ := uuid.Parse(req.StudentID) studentID = &id } var entry models.GradebookEntry err := s.db.QueryRow(ctx, ` INSERT INTO gradebook_entries (class_id, student_id, date, entry_type, content, is_visible_to_parents) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, class_id, student_id, date, entry_type, content, is_visible_to_parents, created_at `, req.ClassID, studentID, date, req.EntryType, req.Content, req.IsVisibleToParents).Scan( &entry.ID, &entry.ClassID, &entry.StudentID, &entry.Date, &entry.EntryType, &entry.Content, &entry.IsVisibleToParents, &entry.CreatedAt, ) return &entry, err } // GetGradebookEntries returns entries for a class func (s *GradebookService) GetGradebookEntries(ctx context.Context, classID string, entryType *string, startDate, endDate *time.Time) ([]models.GradebookEntry, error) { query := ` SELECT ge.id, ge.class_id, ge.student_id, ge.date, ge.entry_type, ge.content, ge.is_visible_to_parents, ge.created_at, COALESCE(CONCAT(st.first_name, ' ', st.last_name), '') as student_name FROM gradebook_entries ge LEFT JOIN students st ON ge.student_id = st.id WHERE ge.class_id = $1 ` args := []interface{}{classID} argCount := 1 if entryType != nil { argCount++ query += ` AND ge.entry_type = $` + string(rune('0'+argCount)) args = append(args, *entryType) } if startDate != nil { argCount++ query += ` AND ge.date >= $` + string(rune('0'+argCount)) args = append(args, startDate) } if endDate != nil { argCount++ query += ` AND ge.date <= $` + string(rune('0'+argCount)) args = append(args, endDate) } query += ` ORDER BY ge.date DESC, ge.created_at DESC` rows, err := s.db.Query(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var entries []models.GradebookEntry for rows.Next() { var e models.GradebookEntry if err := rows.Scan(&e.ID, &e.ClassID, &e.StudentID, &e.Date, &e.EntryType, &e.Content, &e.IsVisibleToParents, &e.CreatedAt, &e.StudentName); err != nil { return nil, err } entries = append(entries, e) } return entries, nil } // GetStudentEntries returns gradebook entries for a specific student func (s *GradebookService) GetStudentEntries(ctx context.Context, studentID string) ([]models.GradebookEntry, error) { rows, err := s.db.Query(ctx, ` SELECT ge.id, ge.class_id, ge.student_id, ge.date, ge.entry_type, ge.content, ge.is_visible_to_parents, ge.created_at, CONCAT(st.first_name, ' ', st.last_name) as student_name FROM gradebook_entries ge JOIN students st ON ge.student_id = st.id WHERE ge.student_id = $1 ORDER BY ge.date DESC `, studentID) if err != nil { return nil, err } defer rows.Close() var entries []models.GradebookEntry for rows.Next() { var e models.GradebookEntry if err := rows.Scan(&e.ID, &e.ClassID, &e.StudentID, &e.Date, &e.EntryType, &e.Content, &e.IsVisibleToParents, &e.CreatedAt, &e.StudentName); err != nil { return nil, err } entries = append(entries, e) } return entries, nil } // DeleteGradebookEntry deletes a gradebook entry func (s *GradebookService) DeleteGradebookEntry(ctx context.Context, entryID string) error { _, err := s.db.Exec(ctx, `DELETE FROM gradebook_entries WHERE id = $1`, entryID) return err } // BulkCreateAttendance creates attendance records for multiple students at once func (s *GradebookService) BulkCreateAttendance(ctx context.Context, classID string, date string, records []struct { StudentID string Status models.AttendanceStatus Periods int Reason string }) error { parsedDate, _ := time.Parse("2006-01-02", date) for _, r := range records { periods := r.Periods if periods == 0 { periods = 1 } _, err := s.db.Exec(ctx, ` INSERT INTO attendance (student_id, date, status, periods, reason) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (student_id, date) DO UPDATE SET status = EXCLUDED.status, periods = EXCLUDED.periods, reason = EXCLUDED.reason `, r.StudentID, parsedDate, r.Status, periods, r.Reason) if err != nil { return err } } return nil }