Fix: Remove broken getKlausurApiUrl and clean up empty lines
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
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
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>
This commit is contained in:
@@ -200,447 +200,3 @@ func (s *GradeService) UpdateGradeWeights(ctx context.Context, studentID, subjec
|
||||
`, studentID, subjectID, writtenWeight, oralWeight)
|
||||
return err
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// STATISTICS METHODS
|
||||
// =============================================
|
||||
|
||||
// ClassStatistics holds statistics for a class
|
||||
type ClassStatistics struct {
|
||||
ClassID string `json:"class_id"`
|
||||
ClassName string `json:"class_name"`
|
||||
StudentCount int `json:"student_count"`
|
||||
ClassAverage float64 `json:"class_average"`
|
||||
GradeDistribution map[string]int `json:"grade_distribution"`
|
||||
BestGrade float64 `json:"best_grade"`
|
||||
WorstGrade float64 `json:"worst_grade"`
|
||||
PassRate float64 `json:"pass_rate"`
|
||||
StudentsAtRisk int `json:"students_at_risk"`
|
||||
SubjectAverages map[string]float64 `json:"subject_averages"`
|
||||
}
|
||||
|
||||
// SubjectStatistics holds statistics for a subject within a class
|
||||
type SubjectStatistics struct {
|
||||
ClassID string `json:"class_id"`
|
||||
SubjectID string `json:"subject_id"`
|
||||
SubjectName string `json:"subject_name"`
|
||||
StudentCount int `json:"student_count"`
|
||||
Average float64 `json:"average"`
|
||||
Median float64 `json:"median"`
|
||||
GradeDistribution map[string]int `json:"grade_distribution"`
|
||||
BestGrade float64 `json:"best_grade"`
|
||||
WorstGrade float64 `json:"worst_grade"`
|
||||
PassRate float64 `json:"pass_rate"`
|
||||
ExamAverages []ExamAverage `json:"exam_averages"`
|
||||
}
|
||||
|
||||
// ExamAverage holds average for a single exam
|
||||
type ExamAverage struct {
|
||||
ExamID string `json:"exam_id"`
|
||||
Title string `json:"title"`
|
||||
Average float64 `json:"average"`
|
||||
ExamDate string `json:"exam_date"`
|
||||
}
|
||||
|
||||
// StudentStatistics holds statistics for a single student
|
||||
type StudentStatistics struct {
|
||||
StudentID string `json:"student_id"`
|
||||
StudentName string `json:"student_name"`
|
||||
OverallAverage float64 `json:"overall_average"`
|
||||
SubjectGrades map[string]float64 `json:"subject_grades"`
|
||||
Trend string `json:"trend"` // "improving", "stable", "declining"
|
||||
AbsenceDays int `json:"absence_days"`
|
||||
ExamsCompleted int `json:"exams_completed"`
|
||||
StrongestSubject string `json:"strongest_subject"`
|
||||
WeakestSubject string `json:"weakest_subject"`
|
||||
}
|
||||
|
||||
// Notenspiegel represents grade distribution
|
||||
type Notenspiegel struct {
|
||||
ClassID string `json:"class_id"`
|
||||
SubjectID string `json:"subject_id,omitempty"`
|
||||
ExamID string `json:"exam_id,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Distribution map[string]int `json:"distribution"`
|
||||
Total int `json:"total"`
|
||||
Average float64 `json:"average"`
|
||||
PassRate float64 `json:"pass_rate"`
|
||||
}
|
||||
|
||||
// GetClassStatistics returns statistics for a class
|
||||
func (s *GradeService) GetClassStatistics(ctx context.Context, classID string, semester int) (*ClassStatistics, error) {
|
||||
stats := &ClassStatistics{
|
||||
ClassID: classID,
|
||||
GradeDistribution: make(map[string]int),
|
||||
SubjectAverages: make(map[string]float64),
|
||||
}
|
||||
|
||||
// Initialize grade distribution
|
||||
for i := 1; i <= 6; i++ {
|
||||
stats.GradeDistribution[string('0'+rune(i))] = 0
|
||||
}
|
||||
|
||||
// Get class info and student count
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT c.name, COUNT(s.id)
|
||||
FROM classes c
|
||||
LEFT JOIN students s ON s.class_id = c.id
|
||||
WHERE c.id = $1
|
||||
GROUP BY c.name
|
||||
`, classID).Scan(&stats.ClassName, &stats.StudentCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build semester condition
|
||||
semesterCond := ""
|
||||
if semester > 0 {
|
||||
semesterCond = " AND go.semester = " + string('0'+rune(semester))
|
||||
}
|
||||
|
||||
// Get overall statistics from grade_overview
|
||||
var avgGrade, bestGrade, worstGrade float64
|
||||
var totalPassed, totalStudents int
|
||||
err = s.db.QueryRow(ctx, `
|
||||
SELECT
|
||||
COALESCE(AVG(go.final_grade), 0),
|
||||
COALESCE(MIN(go.final_grade), 0),
|
||||
COALESCE(MAX(go.final_grade), 0),
|
||||
COUNT(CASE WHEN go.final_grade <= 4.0 THEN 1 END),
|
||||
COUNT(go.id)
|
||||
FROM grade_overview go
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
`, classID).Scan(&avgGrade, &bestGrade, &worstGrade, &totalPassed, &totalStudents)
|
||||
|
||||
if err == nil {
|
||||
stats.ClassAverage = avgGrade
|
||||
stats.BestGrade = bestGrade
|
||||
stats.WorstGrade = worstGrade
|
||||
if totalStudents > 0 {
|
||||
stats.PassRate = float64(totalPassed) / float64(totalStudents) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// Get grade distribution
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT FLOOR(go.final_grade) as grade_bucket, COUNT(*) as count
|
||||
FROM grade_overview go
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
GROUP BY FLOOR(go.final_grade)
|
||||
`, classID)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var bucket int
|
||||
var count int
|
||||
if err := rows.Scan(&bucket, &count); err == nil && bucket >= 1 && bucket <= 6 {
|
||||
stats.GradeDistribution[string('0'+rune(bucket))] = count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count students at risk
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT COUNT(DISTINCT s.id)
|
||||
FROM students s
|
||||
JOIN grade_overview go ON go.student_id = s.id
|
||||
WHERE s.class_id = $1 AND go.final_grade >= 4.5
|
||||
`, classID).Scan(&stats.StudentsAtRisk)
|
||||
|
||||
// Get subject averages
|
||||
subjectRows, err := s.db.Query(ctx, `
|
||||
SELECT sub.name, AVG(go.final_grade)
|
||||
FROM grade_overview go
|
||||
JOIN subjects sub ON sub.id = go.subject_id
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
GROUP BY sub.name
|
||||
`, classID)
|
||||
if err == nil {
|
||||
defer subjectRows.Close()
|
||||
for subjectRows.Next() {
|
||||
var name string
|
||||
var avg float64
|
||||
if err := subjectRows.Scan(&name, &avg); err == nil {
|
||||
stats.SubjectAverages[name] = avg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetSubjectStatistics returns statistics for a specific subject in a class
|
||||
func (s *GradeService) GetSubjectStatistics(ctx context.Context, classID, subjectID string, semester int) (*SubjectStatistics, error) {
|
||||
stats := &SubjectStatistics{
|
||||
ClassID: classID,
|
||||
SubjectID: subjectID,
|
||||
GradeDistribution: make(map[string]int),
|
||||
ExamAverages: []ExamAverage{},
|
||||
}
|
||||
|
||||
// Initialize grade distribution
|
||||
for i := 1; i <= 6; i++ {
|
||||
stats.GradeDistribution[string('0'+rune(i))] = 0
|
||||
}
|
||||
|
||||
// Get subject name and basic stats
|
||||
semesterCond := ""
|
||||
if semester > 0 {
|
||||
semesterCond = " AND go.semester = " + string('0'+rune(semester))
|
||||
}
|
||||
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT
|
||||
sub.name,
|
||||
COUNT(DISTINCT s.id),
|
||||
COALESCE(AVG(go.final_grade), 0),
|
||||
COALESCE(MIN(go.final_grade), 0),
|
||||
COALESCE(MAX(go.final_grade), 0),
|
||||
COUNT(CASE WHEN go.final_grade <= 4.0 THEN 1 END),
|
||||
COUNT(go.id)
|
||||
FROM grade_overview go
|
||||
JOIN subjects sub ON sub.id = go.subject_id
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.subject_id = $2 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
GROUP BY sub.name
|
||||
`, classID, subjectID).Scan(
|
||||
&stats.SubjectName, &stats.StudentCount, &stats.Average,
|
||||
&stats.BestGrade, &stats.WorstGrade,
|
||||
new(int), new(int), // We'll calculate pass rate separately
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get grade distribution
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT FLOOR(go.final_grade), COUNT(*)
|
||||
FROM grade_overview go
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.subject_id = $2 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
GROUP BY FLOOR(go.final_grade)
|
||||
`, classID, subjectID)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
var passed, total int
|
||||
for rows.Next() {
|
||||
var bucket, count int
|
||||
if err := rows.Scan(&bucket, &count); err == nil && bucket >= 1 && bucket <= 6 {
|
||||
stats.GradeDistribution[string('0'+rune(bucket))] = count
|
||||
total += count
|
||||
if bucket <= 4 {
|
||||
passed += count
|
||||
}
|
||||
}
|
||||
}
|
||||
if total > 0 {
|
||||
stats.PassRate = float64(passed) / float64(total) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// Get exam averages
|
||||
examRows, err := s.db.Query(ctx, `
|
||||
SELECT e.id, e.title, AVG(er.grade), COALESCE(e.exam_date::text, '')
|
||||
FROM exams e
|
||||
JOIN exam_results er ON er.exam_id = e.id
|
||||
JOIN students s ON er.student_id = s.id
|
||||
WHERE s.class_id = $1 AND e.subject_id = $2 AND er.grade IS NOT NULL
|
||||
GROUP BY e.id, e.title, e.exam_date
|
||||
ORDER BY e.exam_date DESC NULLS LAST
|
||||
`, classID, subjectID)
|
||||
if err == nil {
|
||||
defer examRows.Close()
|
||||
for examRows.Next() {
|
||||
var ea ExamAverage
|
||||
if err := examRows.Scan(&ea.ExamID, &ea.Title, &ea.Average, &ea.ExamDate); err == nil {
|
||||
stats.ExamAverages = append(stats.ExamAverages, ea)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetStudentStatistics returns statistics for a specific student
|
||||
func (s *GradeService) GetStudentStatistics(ctx context.Context, studentID string) (*StudentStatistics, error) {
|
||||
stats := &StudentStatistics{
|
||||
StudentID: studentID,
|
||||
SubjectGrades: make(map[string]float64),
|
||||
}
|
||||
|
||||
// Get student name
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT CONCAT(first_name, ' ', last_name) FROM students WHERE id = $1
|
||||
`, studentID).Scan(&stats.StudentName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get subject grades and calculate overall average
|
||||
var totalGrade, numSubjects float64
|
||||
var bestGrade float64 = 6
|
||||
var worstGrade float64 = 1
|
||||
var bestSubject, worstSubject string
|
||||
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT sub.name, go.final_grade
|
||||
FROM grade_overview go
|
||||
JOIN subjects sub ON sub.id = go.subject_id
|
||||
WHERE go.student_id = $1 AND go.final_grade IS NOT NULL
|
||||
ORDER BY sub.name
|
||||
`, studentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var grade float64
|
||||
if err := rows.Scan(&name, &grade); err == nil {
|
||||
stats.SubjectGrades[name] = grade
|
||||
totalGrade += grade
|
||||
numSubjects++
|
||||
if grade < bestGrade {
|
||||
bestGrade = grade
|
||||
bestSubject = name
|
||||
}
|
||||
if grade > worstGrade {
|
||||
worstGrade = grade
|
||||
worstSubject = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if numSubjects > 0 {
|
||||
stats.OverallAverage = totalGrade / numSubjects
|
||||
stats.StrongestSubject = bestSubject
|
||||
stats.WeakestSubject = worstSubject
|
||||
}
|
||||
|
||||
// Count exams completed
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT COUNT(*) FROM exam_results WHERE student_id = $1 AND grade IS NOT NULL
|
||||
`, studentID).Scan(&stats.ExamsCompleted)
|
||||
|
||||
// Count absence days
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(periods), 0) FROM attendance
|
||||
WHERE student_id = $1 AND status IN ('absent_excused', 'absent_unexcused')
|
||||
`, studentID).Scan(&stats.AbsenceDays)
|
||||
|
||||
// Determine trend (simplified - compare first and last exam grades)
|
||||
stats.Trend = "stable"
|
||||
var firstGrade, lastGrade float64
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT grade FROM exam_results
|
||||
WHERE student_id = $1 AND grade IS NOT NULL
|
||||
ORDER BY created_at ASC LIMIT 1
|
||||
`, studentID).Scan(&firstGrade)
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT grade FROM exam_results
|
||||
WHERE student_id = $1 AND grade IS NOT NULL
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
`, studentID).Scan(&lastGrade)
|
||||
|
||||
if lastGrade < firstGrade-0.5 {
|
||||
stats.Trend = "improving"
|
||||
} else if lastGrade > firstGrade+0.5 {
|
||||
stats.Trend = "declining"
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetNotenspiegel returns grade distribution (Notenspiegel)
|
||||
func (s *GradeService) GetNotenspiegel(ctx context.Context, classID, subjectID, examID string, semester int) (*Notenspiegel, error) {
|
||||
ns := &Notenspiegel{
|
||||
ClassID: classID,
|
||||
SubjectID: subjectID,
|
||||
ExamID: examID,
|
||||
Distribution: make(map[string]int),
|
||||
}
|
||||
|
||||
// Initialize distribution
|
||||
for i := 1; i <= 6; i++ {
|
||||
ns.Distribution[string('0'+rune(i))] = 0
|
||||
}
|
||||
|
||||
var query string
|
||||
var args []interface{}
|
||||
|
||||
if examID != "" {
|
||||
// Notenspiegel for specific exam
|
||||
query = `
|
||||
SELECT e.title, FLOOR(er.grade), COUNT(*), AVG(er.grade)
|
||||
FROM exam_results er
|
||||
JOIN exams e ON e.id = er.exam_id
|
||||
WHERE er.exam_id = $1 AND er.grade IS NOT NULL
|
||||
GROUP BY e.title, FLOOR(er.grade)
|
||||
`
|
||||
args = []interface{}{examID}
|
||||
} else if subjectID != "" {
|
||||
// Notenspiegel for subject in class
|
||||
semesterCond := ""
|
||||
if semester > 0 {
|
||||
semesterCond = " AND go.semester = " + string('0'+rune(semester))
|
||||
}
|
||||
query = `
|
||||
SELECT sub.name, FLOOR(go.final_grade), COUNT(*), AVG(go.final_grade)
|
||||
FROM grade_overview go
|
||||
JOIN subjects sub ON sub.id = go.subject_id
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.subject_id = $2 AND go.final_grade IS NOT NULL` + semesterCond + `
|
||||
GROUP BY sub.name, FLOOR(go.final_grade)
|
||||
`
|
||||
args = []interface{}{classID, subjectID}
|
||||
} else {
|
||||
// Notenspiegel for entire class
|
||||
semesterCond := ""
|
||||
if semester > 0 {
|
||||
semesterCond = " AND go.semester = " + string('0'+rune(semester))
|
||||
}
|
||||
query = `
|
||||
SELECT c.name, FLOOR(go.final_grade), COUNT(*), AVG(go.final_grade)
|
||||
FROM grade_overview go
|
||||
JOIN students s ON s.id = go.student_id
|
||||
JOIN classes c ON c.id = s.class_id
|
||||
WHERE s.class_id = $1 AND go.final_grade IS NOT NULL` + semesterCond + `
|
||||
GROUP BY c.name, FLOOR(go.final_grade)
|
||||
`
|
||||
args = []interface{}{classID}
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var passed int
|
||||
for rows.Next() {
|
||||
var title string
|
||||
var bucket, count int
|
||||
var avg float64
|
||||
if err := rows.Scan(&title, &bucket, &count, &avg); err == nil {
|
||||
ns.Title = title
|
||||
if bucket >= 1 && bucket <= 6 {
|
||||
ns.Distribution[string('0'+rune(bucket))] = count
|
||||
ns.Total += count
|
||||
if bucket <= 4 {
|
||||
passed += count
|
||||
}
|
||||
}
|
||||
ns.Average = avg
|
||||
}
|
||||
}
|
||||
|
||||
if ns.Total > 0 {
|
||||
ns.PassRate = float64(passed) / float64(ns.Total) * 100
|
||||
}
|
||||
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
449
school-service/internal/services/grade_service_calc.go
Normal file
449
school-service/internal/services/grade_service_calc.go
Normal file
@@ -0,0 +1,449 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// =============================================
|
||||
// STATISTICS METHODS
|
||||
// =============================================
|
||||
|
||||
// ClassStatistics holds statistics for a class
|
||||
type ClassStatistics struct {
|
||||
ClassID string `json:"class_id"`
|
||||
ClassName string `json:"class_name"`
|
||||
StudentCount int `json:"student_count"`
|
||||
ClassAverage float64 `json:"class_average"`
|
||||
GradeDistribution map[string]int `json:"grade_distribution"`
|
||||
BestGrade float64 `json:"best_grade"`
|
||||
WorstGrade float64 `json:"worst_grade"`
|
||||
PassRate float64 `json:"pass_rate"`
|
||||
StudentsAtRisk int `json:"students_at_risk"`
|
||||
SubjectAverages map[string]float64 `json:"subject_averages"`
|
||||
}
|
||||
|
||||
// SubjectStatistics holds statistics for a subject within a class
|
||||
type SubjectStatistics struct {
|
||||
ClassID string `json:"class_id"`
|
||||
SubjectID string `json:"subject_id"`
|
||||
SubjectName string `json:"subject_name"`
|
||||
StudentCount int `json:"student_count"`
|
||||
Average float64 `json:"average"`
|
||||
Median float64 `json:"median"`
|
||||
GradeDistribution map[string]int `json:"grade_distribution"`
|
||||
BestGrade float64 `json:"best_grade"`
|
||||
WorstGrade float64 `json:"worst_grade"`
|
||||
PassRate float64 `json:"pass_rate"`
|
||||
ExamAverages []ExamAverage `json:"exam_averages"`
|
||||
}
|
||||
|
||||
// ExamAverage holds average for a single exam
|
||||
type ExamAverage struct {
|
||||
ExamID string `json:"exam_id"`
|
||||
Title string `json:"title"`
|
||||
Average float64 `json:"average"`
|
||||
ExamDate string `json:"exam_date"`
|
||||
}
|
||||
|
||||
// StudentStatistics holds statistics for a single student
|
||||
type StudentStatistics struct {
|
||||
StudentID string `json:"student_id"`
|
||||
StudentName string `json:"student_name"`
|
||||
OverallAverage float64 `json:"overall_average"`
|
||||
SubjectGrades map[string]float64 `json:"subject_grades"`
|
||||
Trend string `json:"trend"` // "improving", "stable", "declining"
|
||||
AbsenceDays int `json:"absence_days"`
|
||||
ExamsCompleted int `json:"exams_completed"`
|
||||
StrongestSubject string `json:"strongest_subject"`
|
||||
WeakestSubject string `json:"weakest_subject"`
|
||||
}
|
||||
|
||||
// Notenspiegel represents grade distribution
|
||||
type Notenspiegel struct {
|
||||
ClassID string `json:"class_id"`
|
||||
SubjectID string `json:"subject_id,omitempty"`
|
||||
ExamID string `json:"exam_id,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Distribution map[string]int `json:"distribution"`
|
||||
Total int `json:"total"`
|
||||
Average float64 `json:"average"`
|
||||
PassRate float64 `json:"pass_rate"`
|
||||
}
|
||||
|
||||
// GetClassStatistics returns statistics for a class
|
||||
func (s *GradeService) GetClassStatistics(ctx context.Context, classID string, semester int) (*ClassStatistics, error) {
|
||||
stats := &ClassStatistics{
|
||||
ClassID: classID,
|
||||
GradeDistribution: make(map[string]int),
|
||||
SubjectAverages: make(map[string]float64),
|
||||
}
|
||||
|
||||
// Initialize grade distribution
|
||||
for i := 1; i <= 6; i++ {
|
||||
stats.GradeDistribution[string('0'+rune(i))] = 0
|
||||
}
|
||||
|
||||
// Get class info and student count
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT c.name, COUNT(s.id)
|
||||
FROM classes c
|
||||
LEFT JOIN students s ON s.class_id = c.id
|
||||
WHERE c.id = $1
|
||||
GROUP BY c.name
|
||||
`, classID).Scan(&stats.ClassName, &stats.StudentCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build semester condition
|
||||
semesterCond := ""
|
||||
if semester > 0 {
|
||||
semesterCond = " AND go.semester = " + string('0'+rune(semester))
|
||||
}
|
||||
|
||||
// Get overall statistics from grade_overview
|
||||
var avgGrade, bestGrade, worstGrade float64
|
||||
var totalPassed, totalStudents int
|
||||
err = s.db.QueryRow(ctx, `
|
||||
SELECT
|
||||
COALESCE(AVG(go.final_grade), 0),
|
||||
COALESCE(MIN(go.final_grade), 0),
|
||||
COALESCE(MAX(go.final_grade), 0),
|
||||
COUNT(CASE WHEN go.final_grade <= 4.0 THEN 1 END),
|
||||
COUNT(go.id)
|
||||
FROM grade_overview go
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
`, classID).Scan(&avgGrade, &bestGrade, &worstGrade, &totalPassed, &totalStudents)
|
||||
|
||||
if err == nil {
|
||||
stats.ClassAverage = avgGrade
|
||||
stats.BestGrade = bestGrade
|
||||
stats.WorstGrade = worstGrade
|
||||
if totalStudents > 0 {
|
||||
stats.PassRate = float64(totalPassed) / float64(totalStudents) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// Get grade distribution
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT FLOOR(go.final_grade) as grade_bucket, COUNT(*) as count
|
||||
FROM grade_overview go
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
GROUP BY FLOOR(go.final_grade)
|
||||
`, classID)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var bucket int
|
||||
var count int
|
||||
if err := rows.Scan(&bucket, &count); err == nil && bucket >= 1 && bucket <= 6 {
|
||||
stats.GradeDistribution[string('0'+rune(bucket))] = count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count students at risk
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT COUNT(DISTINCT s.id)
|
||||
FROM students s
|
||||
JOIN grade_overview go ON go.student_id = s.id
|
||||
WHERE s.class_id = $1 AND go.final_grade >= 4.5
|
||||
`, classID).Scan(&stats.StudentsAtRisk)
|
||||
|
||||
// Get subject averages
|
||||
subjectRows, err := s.db.Query(ctx, `
|
||||
SELECT sub.name, AVG(go.final_grade)
|
||||
FROM grade_overview go
|
||||
JOIN subjects sub ON sub.id = go.subject_id
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
GROUP BY sub.name
|
||||
`, classID)
|
||||
if err == nil {
|
||||
defer subjectRows.Close()
|
||||
for subjectRows.Next() {
|
||||
var name string
|
||||
var avg float64
|
||||
if err := subjectRows.Scan(&name, &avg); err == nil {
|
||||
stats.SubjectAverages[name] = avg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetSubjectStatistics returns statistics for a specific subject in a class
|
||||
func (s *GradeService) GetSubjectStatistics(ctx context.Context, classID, subjectID string, semester int) (*SubjectStatistics, error) {
|
||||
stats := &SubjectStatistics{
|
||||
ClassID: classID,
|
||||
SubjectID: subjectID,
|
||||
GradeDistribution: make(map[string]int),
|
||||
ExamAverages: []ExamAverage{},
|
||||
}
|
||||
|
||||
// Initialize grade distribution
|
||||
for i := 1; i <= 6; i++ {
|
||||
stats.GradeDistribution[string('0'+rune(i))] = 0
|
||||
}
|
||||
|
||||
// Get subject name and basic stats
|
||||
semesterCond := ""
|
||||
if semester > 0 {
|
||||
semesterCond = " AND go.semester = " + string('0'+rune(semester))
|
||||
}
|
||||
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT
|
||||
sub.name,
|
||||
COUNT(DISTINCT s.id),
|
||||
COALESCE(AVG(go.final_grade), 0),
|
||||
COALESCE(MIN(go.final_grade), 0),
|
||||
COALESCE(MAX(go.final_grade), 0),
|
||||
COUNT(CASE WHEN go.final_grade <= 4.0 THEN 1 END),
|
||||
COUNT(go.id)
|
||||
FROM grade_overview go
|
||||
JOIN subjects sub ON sub.id = go.subject_id
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.subject_id = $2 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
GROUP BY sub.name
|
||||
`, classID, subjectID).Scan(
|
||||
&stats.SubjectName, &stats.StudentCount, &stats.Average,
|
||||
&stats.BestGrade, &stats.WorstGrade,
|
||||
new(int), new(int), // We'll calculate pass rate separately
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get grade distribution
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT FLOOR(go.final_grade), COUNT(*)
|
||||
FROM grade_overview go
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.subject_id = $2 AND go.final_grade IS NOT NULL`+semesterCond+`
|
||||
GROUP BY FLOOR(go.final_grade)
|
||||
`, classID, subjectID)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
var passed, total int
|
||||
for rows.Next() {
|
||||
var bucket, count int
|
||||
if err := rows.Scan(&bucket, &count); err == nil && bucket >= 1 && bucket <= 6 {
|
||||
stats.GradeDistribution[string('0'+rune(bucket))] = count
|
||||
total += count
|
||||
if bucket <= 4 {
|
||||
passed += count
|
||||
}
|
||||
}
|
||||
}
|
||||
if total > 0 {
|
||||
stats.PassRate = float64(passed) / float64(total) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// Get exam averages
|
||||
examRows, err := s.db.Query(ctx, `
|
||||
SELECT e.id, e.title, AVG(er.grade), COALESCE(e.exam_date::text, '')
|
||||
FROM exams e
|
||||
JOIN exam_results er ON er.exam_id = e.id
|
||||
JOIN students s ON er.student_id = s.id
|
||||
WHERE s.class_id = $1 AND e.subject_id = $2 AND er.grade IS NOT NULL
|
||||
GROUP BY e.id, e.title, e.exam_date
|
||||
ORDER BY e.exam_date DESC NULLS LAST
|
||||
`, classID, subjectID)
|
||||
if err == nil {
|
||||
defer examRows.Close()
|
||||
for examRows.Next() {
|
||||
var ea ExamAverage
|
||||
if err := examRows.Scan(&ea.ExamID, &ea.Title, &ea.Average, &ea.ExamDate); err == nil {
|
||||
stats.ExamAverages = append(stats.ExamAverages, ea)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetStudentStatistics returns statistics for a specific student
|
||||
func (s *GradeService) GetStudentStatistics(ctx context.Context, studentID string) (*StudentStatistics, error) {
|
||||
stats := &StudentStatistics{
|
||||
StudentID: studentID,
|
||||
SubjectGrades: make(map[string]float64),
|
||||
}
|
||||
|
||||
// Get student name
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT CONCAT(first_name, ' ', last_name) FROM students WHERE id = $1
|
||||
`, studentID).Scan(&stats.StudentName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get subject grades and calculate overall average
|
||||
var totalGrade, numSubjects float64
|
||||
var bestGrade float64 = 6
|
||||
var worstGrade float64 = 1
|
||||
var bestSubject, worstSubject string
|
||||
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT sub.name, go.final_grade
|
||||
FROM grade_overview go
|
||||
JOIN subjects sub ON sub.id = go.subject_id
|
||||
WHERE go.student_id = $1 AND go.final_grade IS NOT NULL
|
||||
ORDER BY sub.name
|
||||
`, studentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var grade float64
|
||||
if err := rows.Scan(&name, &grade); err == nil {
|
||||
stats.SubjectGrades[name] = grade
|
||||
totalGrade += grade
|
||||
numSubjects++
|
||||
if grade < bestGrade {
|
||||
bestGrade = grade
|
||||
bestSubject = name
|
||||
}
|
||||
if grade > worstGrade {
|
||||
worstGrade = grade
|
||||
worstSubject = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if numSubjects > 0 {
|
||||
stats.OverallAverage = totalGrade / numSubjects
|
||||
stats.StrongestSubject = bestSubject
|
||||
stats.WeakestSubject = worstSubject
|
||||
}
|
||||
|
||||
// Count exams completed
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT COUNT(*) FROM exam_results WHERE student_id = $1 AND grade IS NOT NULL
|
||||
`, studentID).Scan(&stats.ExamsCompleted)
|
||||
|
||||
// Count absence days
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(periods), 0) FROM attendance
|
||||
WHERE student_id = $1 AND status IN ('absent_excused', 'absent_unexcused')
|
||||
`, studentID).Scan(&stats.AbsenceDays)
|
||||
|
||||
// Determine trend (simplified - compare first and last exam grades)
|
||||
stats.Trend = "stable"
|
||||
var firstGrade, lastGrade float64
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT grade FROM exam_results
|
||||
WHERE student_id = $1 AND grade IS NOT NULL
|
||||
ORDER BY created_at ASC LIMIT 1
|
||||
`, studentID).Scan(&firstGrade)
|
||||
s.db.QueryRow(ctx, `
|
||||
SELECT grade FROM exam_results
|
||||
WHERE student_id = $1 AND grade IS NOT NULL
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
`, studentID).Scan(&lastGrade)
|
||||
|
||||
if lastGrade < firstGrade-0.5 {
|
||||
stats.Trend = "improving"
|
||||
} else if lastGrade > firstGrade+0.5 {
|
||||
stats.Trend = "declining"
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetNotenspiegel returns grade distribution (Notenspiegel)
|
||||
func (s *GradeService) GetNotenspiegel(ctx context.Context, classID, subjectID, examID string, semester int) (*Notenspiegel, error) {
|
||||
ns := &Notenspiegel{
|
||||
ClassID: classID,
|
||||
SubjectID: subjectID,
|
||||
ExamID: examID,
|
||||
Distribution: make(map[string]int),
|
||||
}
|
||||
|
||||
// Initialize distribution
|
||||
for i := 1; i <= 6; i++ {
|
||||
ns.Distribution[string('0'+rune(i))] = 0
|
||||
}
|
||||
|
||||
var query string
|
||||
var args []interface{}
|
||||
|
||||
if examID != "" {
|
||||
// Notenspiegel for specific exam
|
||||
query = `
|
||||
SELECT e.title, FLOOR(er.grade), COUNT(*), AVG(er.grade)
|
||||
FROM exam_results er
|
||||
JOIN exams e ON e.id = er.exam_id
|
||||
WHERE er.exam_id = $1 AND er.grade IS NOT NULL
|
||||
GROUP BY e.title, FLOOR(er.grade)
|
||||
`
|
||||
args = []interface{}{examID}
|
||||
} else if subjectID != "" {
|
||||
// Notenspiegel for subject in class
|
||||
semesterCond := ""
|
||||
if semester > 0 {
|
||||
semesterCond = " AND go.semester = " + string('0'+rune(semester))
|
||||
}
|
||||
query = `
|
||||
SELECT sub.name, FLOOR(go.final_grade), COUNT(*), AVG(go.final_grade)
|
||||
FROM grade_overview go
|
||||
JOIN subjects sub ON sub.id = go.subject_id
|
||||
JOIN students s ON s.id = go.student_id
|
||||
WHERE s.class_id = $1 AND go.subject_id = $2 AND go.final_grade IS NOT NULL` + semesterCond + `
|
||||
GROUP BY sub.name, FLOOR(go.final_grade)
|
||||
`
|
||||
args = []interface{}{classID, subjectID}
|
||||
} else {
|
||||
// Notenspiegel for entire class
|
||||
semesterCond := ""
|
||||
if semester > 0 {
|
||||
semesterCond = " AND go.semester = " + string('0'+rune(semester))
|
||||
}
|
||||
query = `
|
||||
SELECT c.name, FLOOR(go.final_grade), COUNT(*), AVG(go.final_grade)
|
||||
FROM grade_overview go
|
||||
JOIN students s ON s.id = go.student_id
|
||||
JOIN classes c ON c.id = s.class_id
|
||||
WHERE s.class_id = $1 AND go.final_grade IS NOT NULL` + semesterCond + `
|
||||
GROUP BY c.name, FLOOR(go.final_grade)
|
||||
`
|
||||
args = []interface{}{classID}
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var passed int
|
||||
for rows.Next() {
|
||||
var title string
|
||||
var bucket, count int
|
||||
var avg float64
|
||||
if err := rows.Scan(&title, &bucket, &count, &avg); err == nil {
|
||||
ns.Title = title
|
||||
if bucket >= 1 && bucket <= 6 {
|
||||
ns.Distribution[string('0'+rune(bucket))] = count
|
||||
ns.Total += count
|
||||
if bucket <= 4 {
|
||||
passed += count
|
||||
}
|
||||
}
|
||||
ns.Average = avg
|
||||
}
|
||||
}
|
||||
|
||||
if ns.Total > 0 {
|
||||
ns.PassRate = float64(passed) / float64(ns.Total) * 100
|
||||
}
|
||||
|
||||
return ns, nil
|
||||
}
|
||||
Reference in New Issue
Block a user