Files
breakpilot-lehrer/school-service/internal/services/grade_service.go
Benjamin Admin 9ba420fa91
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
Fix: Remove broken getKlausurApiUrl and clean up empty lines
sed replacement left orphaned hostname references in story page
and empty lines in getApiBase functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 16:02:04 +02:00

203 lines
7.8 KiB
Go

package services
import (
"context"
"github.com/breakpilot/school-service/internal/models"
"github.com/jackc/pgx/v5/pgxpool"
)
// GradeService handles grade-related operations
type GradeService struct {
db *pgxpool.Pool
}
// NewGradeService creates a new GradeService
func NewGradeService(db *pgxpool.Pool) *GradeService {
return &GradeService{db: db}
}
// GetGradeOverview returns grade overview for a class
func (s *GradeService) GetGradeOverview(ctx context.Context, classID string, semester int) ([]models.GradeOverview, error) {
rows, err := s.db.Query(ctx, `
SELECT go.id, go.student_id, go.subject_id, go.school_year_id, go.semester,
go.written_grade_avg, go.written_grade_count, go.oral_grade, go.oral_notes,
go.final_grade, go.final_grade_locked, go.written_weight, go.oral_weight,
go.created_at, go.updated_at,
CONCAT(st.first_name, ' ', st.last_name) as student_name,
sub.name as subject_name
FROM grade_overview go
JOIN students st ON go.student_id = st.id
JOIN subjects sub ON go.subject_id = sub.id
WHERE st.class_id = $1 AND go.semester = $2
ORDER BY st.last_name, st.first_name, sub.name
`, classID, semester)
if err != nil {
return nil, err
}
defer rows.Close()
var grades []models.GradeOverview
for rows.Next() {
var g models.GradeOverview
if err := rows.Scan(&g.ID, &g.StudentID, &g.SubjectID, &g.SchoolYearID, &g.Semester, &g.WrittenGradeAvg, &g.WrittenGradeCount, &g.OralGrade, &g.OralNotes, &g.FinalGrade, &g.FinalGradeLocked, &g.WrittenWeight, &g.OralWeight, &g.CreatedAt, &g.UpdatedAt, &g.StudentName, &g.SubjectName); err != nil {
return nil, err
}
grades = append(grades, g)
}
return grades, nil
}
// GetStudentGrades returns all grades for a student
func (s *GradeService) GetStudentGrades(ctx context.Context, studentID string) ([]models.GradeOverview, error) {
rows, err := s.db.Query(ctx, `
SELECT go.id, go.student_id, go.subject_id, go.school_year_id, go.semester,
go.written_grade_avg, go.written_grade_count, go.oral_grade, go.oral_notes,
go.final_grade, go.final_grade_locked, go.written_weight, go.oral_weight,
go.created_at, go.updated_at,
CONCAT(st.first_name, ' ', st.last_name) as student_name,
sub.name as subject_name
FROM grade_overview go
JOIN students st ON go.student_id = st.id
JOIN subjects sub ON go.subject_id = sub.id
WHERE go.student_id = $1
ORDER BY go.school_year_id DESC, go.semester DESC, sub.name
`, studentID)
if err != nil {
return nil, err
}
defer rows.Close()
var grades []models.GradeOverview
for rows.Next() {
var g models.GradeOverview
if err := rows.Scan(&g.ID, &g.StudentID, &g.SubjectID, &g.SchoolYearID, &g.Semester, &g.WrittenGradeAvg, &g.WrittenGradeCount, &g.OralGrade, &g.OralNotes, &g.FinalGrade, &g.FinalGradeLocked, &g.WrittenWeight, &g.OralWeight, &g.CreatedAt, &g.UpdatedAt, &g.StudentName, &g.SubjectName); err != nil {
return nil, err
}
grades = append(grades, g)
}
return grades, nil
}
// UpdateOralGrade updates the oral grade for a student in a subject
func (s *GradeService) UpdateOralGrade(ctx context.Context, studentID, subjectID string, req *models.UpdateOralGradeRequest) (*models.GradeOverview, error) {
// First, get or create the grade overview record
var grade models.GradeOverview
// Try to get the current school year and semester
var schoolYearID string
err := s.db.QueryRow(ctx, `
SELECT sy.id FROM school_years sy
JOIN classes c ON c.school_year_id = sy.id
JOIN students st ON st.class_id = c.id
WHERE st.id = $1 AND sy.is_current = true
LIMIT 1
`, studentID).Scan(&schoolYearID)
if err != nil {
// No current school year found, cannot update
return nil, err
}
// Current semester (simplified - could be calculated from date)
semester := 1
err = s.db.QueryRow(ctx, `
INSERT INTO grade_overview (student_id, subject_id, school_year_id, semester, oral_grade, oral_notes)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (student_id, subject_id, school_year_id, semester) DO UPDATE SET
oral_grade = EXCLUDED.oral_grade,
oral_notes = EXCLUDED.oral_notes,
updated_at = NOW()
RETURNING id, student_id, subject_id, school_year_id, semester, written_grade_avg, written_grade_count, oral_grade, oral_notes, final_grade, final_grade_locked, written_weight, oral_weight, created_at, updated_at
`, studentID, subjectID, schoolYearID, semester, req.OralGrade, req.OralNotes).Scan(
&grade.ID, &grade.StudentID, &grade.SubjectID, &grade.SchoolYearID, &grade.Semester, &grade.WrittenGradeAvg, &grade.WrittenGradeCount, &grade.OralGrade, &grade.OralNotes, &grade.FinalGrade, &grade.FinalGradeLocked, &grade.WrittenWeight, &grade.OralWeight, &grade.CreatedAt, &grade.UpdatedAt,
)
return &grade, err
}
// CalculateFinalGrades calculates final grades for all students in a class
func (s *GradeService) CalculateFinalGrades(ctx context.Context, classID string, semester int) error {
// Update written grade averages from approved exam results
_, err := s.db.Exec(ctx, `
WITH avg_grades AS (
SELECT er.student_id, e.subject_id, AVG(er.grade) as avg_grade, COUNT(*) as grade_count
FROM exam_results er
JOIN exams e ON er.exam_id = e.id
JOIN students st ON er.student_id = st.id
WHERE st.class_id = $1 AND er.approved_by_teacher = true AND er.grade IS NOT NULL
GROUP BY er.student_id, e.subject_id
)
UPDATE grade_overview go SET
written_grade_avg = ag.avg_grade,
written_grade_count = ag.grade_count,
updated_at = NOW()
FROM avg_grades ag
WHERE go.student_id = ag.student_id AND go.subject_id = ag.subject_id AND go.semester = $2
`, classID, semester)
if err != nil {
return err
}
// Calculate final grades based on weights
_, err = s.db.Exec(ctx, `
UPDATE grade_overview SET
final_grade = CASE
WHEN oral_grade IS NULL THEN written_grade_avg
WHEN written_grade_avg IS NULL THEN oral_grade
ELSE ROUND((written_grade_avg * written_weight + oral_grade * oral_weight) / (written_weight + oral_weight), 1)
END,
updated_at = NOW()
WHERE student_id IN (SELECT id FROM students WHERE class_id = $1)
AND semester = $2
AND final_grade_locked = false
`, classID, semester)
return err
}
// TransferApprovedGrades transfers approved exam results to grade overview
func (s *GradeService) TransferApprovedGrades(ctx context.Context, teacherID string) error {
// Get current school year
var schoolYearID string
err := s.db.QueryRow(ctx, `
SELECT id FROM school_years WHERE teacher_id = $1 AND is_current = true LIMIT 1
`, teacherID).Scan(&schoolYearID)
if err != nil {
return err
}
// Current semester
semester := 1
// Ensure grade_overview records exist for all students with approved results
_, err = s.db.Exec(ctx, `
INSERT INTO grade_overview (student_id, subject_id, school_year_id, semester)
SELECT DISTINCT er.student_id, e.subject_id, $1, $2
FROM exam_results er
JOIN exams e ON er.exam_id = e.id
WHERE er.approved_by_teacher = true AND e.subject_id IS NOT NULL
ON CONFLICT (student_id, subject_id, school_year_id, semester) DO NOTHING
`, schoolYearID, semester)
return err
}
// LockFinalGrade locks a final grade (prevents further changes)
func (s *GradeService) LockFinalGrade(ctx context.Context, studentID, subjectID string, semester int) error {
_, err := s.db.Exec(ctx, `
UPDATE grade_overview SET final_grade_locked = true, updated_at = NOW()
WHERE student_id = $1 AND subject_id = $2 AND semester = $3
`, studentID, subjectID, semester)
return err
}
// UpdateGradeWeights updates the written/oral weights for grade calculation
func (s *GradeService) UpdateGradeWeights(ctx context.Context, studentID, subjectID string, writtenWeight, oralWeight int) error {
_, err := s.db.Exec(ctx, `
UPDATE grade_overview SET written_weight = $3, oral_weight = $4, updated_at = NOW()
WHERE student_id = $1 AND subject_id = $2
`, studentID, subjectID, writtenWeight, oralWeight)
return err
}