package seed import ( "context" "fmt" "time" "github.com/google/uuid" ) // StudentResult holds created student info type StudentResult struct { ID uuid.UUID FirstName string LastName string } func (s *Seeder) seedStudentsForClass(ctx context.Context, classID uuid.UUID) ([]StudentResult, error) { // 15-25 students per class numStudents := 15 + s.rng.Intn(11) // 15-25 var students []StudentResult usedNames := make(map[string]bool) for i := 0; i < numStudents; i++ { // Generate unique name combination var firstName, lastName string for { firstName = firstNames[s.rng.Intn(len(firstNames))] lastName = lastNames[s.rng.Intn(len(lastNames))] key := firstName + lastName if !usedNames[key] { usedNames[key] = true break } } id := uuid.New() // Generate birth date (10-13 years old) birthYear := time.Now().Year() - 10 - s.rng.Intn(4) birthMonth := time.Month(1 + s.rng.Intn(12)) birthDay := 1 + s.rng.Intn(28) birthDate := time.Date(birthYear, birthMonth, birthDay, 0, 0, 0, 0, time.UTC) studentNumber := fmt.Sprintf("S%d%04d", time.Now().Year()%100, i+1) _, err := s.pool.Exec(ctx, ` INSERT INTO students (id, class_id, first_name, last_name, birth_date, student_number) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT DO NOTHING `, id, classID, firstName, lastName, birthDate, studentNumber) if err != nil { return nil, err } students = append(students, StudentResult{ ID: id, FirstName: firstName, LastName: lastName, }) } return students, nil } func (s *Seeder) seedExamsAndResults( ctx context.Context, classID uuid.UUID, subjectID uuid.UUID, students []StudentResult, schoolYearID uuid.UUID, subj SubjectConfig, ) error { if subj.ExamsPerYear == 0 { return nil // No exams for this subject (e.g., Sport) } examDates := []time.Time{ time.Date(2024, 10, 15, 0, 0, 0, 0, time.UTC), time.Date(2024, 12, 10, 0, 0, 0, 0, time.UTC), time.Date(2025, 2, 20, 0, 0, 0, 0, time.UTC), time.Date(2025, 5, 15, 0, 0, 0, 0, time.UTC), } for i := 0; i < subj.ExamsPerYear; i++ { examID := uuid.New() examDate := examDates[i%len(examDates)] maxPoints := 50.0 + float64(s.rng.Intn(51)) // 50-100 points title := fmt.Sprintf("%s Klassenarbeit %d", subj.Name, i+1) _, err := s.pool.Exec(ctx, ` INSERT INTO exams (id, teacher_id, class_id, subject_id, title, exam_type, max_points, status, exam_date, difficulty_level, duration_minutes) VALUES ($1, $2, $3, $4, $5, 'klassenarbeit', $6, 'archived', $7, $8, $9) ON CONFLICT DO NOTHING `, examID, s.teacherID, classID, subjectID, title, maxPoints, examDate, 3, 45) if err != nil { return err } // Create results for each student for _, student := range students { // Generate realistic grade distribution (mostly 2-4) grade := s.generateRealisticGrade() percentage := s.gradeToPercentage(grade) points := (percentage / 100.0) * maxPoints isAbsent := s.rng.Float64() < 0.03 // 3% absence rate if isAbsent { _, err = s.pool.Exec(ctx, ` INSERT INTO exam_results (exam_id, student_id, is_absent, approved_by_teacher) VALUES ($1, $2, true, true) ON CONFLICT (exam_id, student_id) DO NOTHING `, examID, student.ID) } else { _, err = s.pool.Exec(ctx, ` INSERT INTO exam_results (exam_id, student_id, points_achieved, grade, percentage, approved_by_teacher) VALUES ($1, $2, $3, $4, $5, true) ON CONFLICT (exam_id, student_id) DO NOTHING `, examID, student.ID, points, grade, percentage) } if err != nil { return err } } } // Create grade overview entries for each student for _, student := range students { err := s.createGradeOverview(ctx, student.ID, subjectID, schoolYearID, subj) if err != nil { return err } } return nil } func (s *Seeder) generateRealisticGrade() float64 { // German grading: 1 (best) to 6 (worst) // Normal distribution centered around 3.0 grades := []float64{1.0, 1.3, 1.7, 2.0, 2.3, 2.7, 3.0, 3.3, 3.7, 4.0, 4.3, 4.7, 5.0, 5.3, 5.7, 6.0} weights := []int{2, 4, 6, 10, 12, 14, 16, 14, 12, 8, 5, 3, 2, 1, 1, 1} // Weighted distribution total := 0 for _, w := range weights { total += w } r := s.rng.Intn(total) cumulative := 0 for i, w := range weights { cumulative += w if r < cumulative { return grades[i] } } return 3.0 } func (s *Seeder) gradeToPercentage(grade float64) float64 { // Convert German grade to percentage // 1.0 = 100%, 6.0 = 0% return 100.0 - ((grade - 1.0) * 20.0) } func (s *Seeder) createGradeOverview( ctx context.Context, studentID uuid.UUID, subjectID uuid.UUID, schoolYearID uuid.UUID, subj SubjectConfig, ) error { // Create for both semesters for semester := 1; semester <= 2; semester++ { writtenAvg := s.generateRealisticGrade() oralGrade := s.generateRealisticGrade() // Calculate final grade based on weights finalGrade := (writtenAvg*float64(subj.WrittenWeight) + oralGrade*float64(100-subj.WrittenWeight)) / 100.0 // Round to nearest 0.5 finalGrade = float64(int(finalGrade*2+0.5)) / 2.0 _, err := s.pool.Exec(ctx, ` INSERT INTO grade_overview (student_id, subject_id, school_year_id, semester, written_grade_avg, written_grade_count, oral_grade, final_grade, written_weight, oral_weight) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (student_id, subject_id, school_year_id, semester) DO UPDATE SET written_grade_avg = EXCLUDED.written_grade_avg, oral_grade = EXCLUDED.oral_grade, final_grade = EXCLUDED.final_grade `, studentID, subjectID, schoolYearID, semester, writtenAvg, subj.ExamsPerYear/2+1, oralGrade, finalGrade, subj.WrittenWeight, 100-subj.WrittenWeight) if err != nil { return err } } return nil } func (s *Seeder) seedAttendance(ctx context.Context, students []StudentResult, sy SchoolYearResult) error { for _, student := range students { // 0-10 absence days per student absenceDays := s.rng.Intn(11) for i := 0; i < absenceDays; i++ { // Random date within school year dayOffset := s.rng.Intn(int(sy.EndDate.Sub(sy.StartDate).Hours() / 24)) absenceDate := sy.StartDate.AddDate(0, 0, dayOffset) // Skip weekends if absenceDate.Weekday() == time.Saturday || absenceDate.Weekday() == time.Sunday { continue } // 80% excused, 20% unexcused status := "absent_excused" if s.rng.Float64() < 0.2 { status = "absent_unexcused" } reasons := []string{ "Krankheit", "Arzttermin", "Familiaere Gruende", "", } reason := reasons[s.rng.Intn(len(reasons))] _, err := s.pool.Exec(ctx, ` INSERT INTO attendance (student_id, date, status, periods, reason) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (student_id, date) DO NOTHING `, student.ID, absenceDate, status, s.rng.Intn(6)+1, reason) if err != nil { return err } } } return nil } // Statistics holds calculated statistics for a class type Statistics struct { ClassID uuid.UUID `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"` } // CalculateClassStatistics calculates statistics for a class func (s *Seeder) CalculateClassStatistics(ctx context.Context, classID uuid.UUID) (*Statistics, error) { stats := &Statistics{ ClassID: classID, GradeDistribution: make(map[string]int), SubjectAverages: make(map[string]float64), } // Initialize grade distribution for i := 1; i <= 6; i++ { stats.GradeDistribution[fmt.Sprintf("%d", i)] = 0 } // Get class info and student count err := s.pool.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 } // Get grade statistics from exam results rows, err := s.pool.Query(ctx, ` SELECT COALESCE(AVG(er.grade), 0) as avg_grade, COALESCE(MIN(er.grade), 0) as best_grade, COALESCE(MAX(er.grade), 0) as worst_grade, COUNT(CASE WHEN er.grade <= 4.0 THEN 1 END) as passed, COUNT(er.id) as total, FLOOR(er.grade) as grade_bucket, COUNT(*) as bucket_count FROM exam_results er JOIN exams e ON e.id = er.exam_id JOIN students s ON s.id = er.student_id WHERE s.class_id = $1 AND er.grade IS NOT NULL GROUP BY FLOOR(er.grade) `, classID) if err != nil { return nil, err } defer rows.Close() var totalPassed, totalExams int for rows.Next() { var avgGrade, bestGrade, worstGrade float64 var passed, total int var gradeBucket float64 var bucketCount int err := rows.Scan(&avgGrade, &bestGrade, &worstGrade, &passed, &total, &gradeBucket, &bucketCount) if err != nil { continue } stats.ClassAverage = avgGrade stats.BestGrade = bestGrade stats.WorstGrade = worstGrade totalPassed += passed totalExams += total gradeKey := fmt.Sprintf("%d", int(gradeBucket)) stats.GradeDistribution[gradeKey] = bucketCount } if totalExams > 0 { stats.PassRate = float64(totalPassed) / float64(totalExams) * 100 } // Count students at risk (average >= 4.5) err = s.pool.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) if err != nil { stats.StudentsAtRisk = 0 } // Get subject averages subjectRows, err := s.pool.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 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 }