Files
breakpilot-lehrer/school-service/internal/services/gradebook_service.go
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

262 lines
8.3 KiB
Go

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
}