package services import ( "context" "fmt" "time" "github.com/breakpilot/consent-service/internal/database" "github.com/breakpilot/consent-service/internal/models" "github.com/breakpilot/consent-service/internal/services/matrix" "github.com/google/uuid" ) // SchoolService handles school management operations type SchoolService struct { db *database.DB matrix *matrix.MatrixService } // NewSchoolService creates a new school service func NewSchoolService(db *database.DB, matrixService *matrix.MatrixService) *SchoolService { return &SchoolService{ db: db, matrix: matrixService, } } // ======================================== // School CRUD // ======================================== // CreateSchool creates a new school func (s *SchoolService) CreateSchool(ctx context.Context, req models.CreateSchoolRequest) (*models.School, error) { school := &models.School{ ID: uuid.New(), Name: req.Name, ShortName: req.ShortName, Type: req.Type, Address: req.Address, City: req.City, PostalCode: req.PostalCode, State: req.State, Country: "DE", Phone: req.Phone, Email: req.Email, Website: req.Website, IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } query := ` INSERT INTO schools (id, name, short_name, type, address, city, postal_code, state, country, phone, email, website, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id` err := s.db.Pool.QueryRow(ctx, query, school.ID, school.Name, school.ShortName, school.Type, school.Address, school.City, school.PostalCode, school.State, school.Country, school.Phone, school.Email, school.Website, school.IsActive, school.CreatedAt, school.UpdatedAt, ).Scan(&school.ID) if err != nil { return nil, fmt.Errorf("failed to create school: %w", err) } // Create default timetable slots for the school if err := s.createDefaultTimetableSlots(ctx, school.ID); err != nil { // Log but don't fail fmt.Printf("Warning: failed to create default timetable slots: %v\n", err) } // Create default grade scale if err := s.createDefaultGradeScale(ctx, school.ID); err != nil { fmt.Printf("Warning: failed to create default grade scale: %v\n", err) } return school, nil } // GetSchool retrieves a school by ID func (s *SchoolService) GetSchool(ctx context.Context, schoolID uuid.UUID) (*models.School, error) { query := ` SELECT id, name, short_name, type, address, city, postal_code, state, country, phone, email, website, matrix_server_name, logo_url, is_active, created_at, updated_at FROM schools WHERE id = $1` school := &models.School{} err := s.db.Pool.QueryRow(ctx, query, schoolID).Scan( &school.ID, &school.Name, &school.ShortName, &school.Type, &school.Address, &school.City, &school.PostalCode, &school.State, &school.Country, &school.Phone, &school.Email, &school.Website, &school.MatrixServerName, &school.LogoURL, &school.IsActive, &school.CreatedAt, &school.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to get school: %w", err) } return school, nil } // ListSchools lists all active schools func (s *SchoolService) ListSchools(ctx context.Context) ([]models.School, error) { query := ` SELECT id, name, short_name, type, address, city, postal_code, state, country, phone, email, website, matrix_server_name, logo_url, is_active, created_at, updated_at FROM schools WHERE is_active = true ORDER BY name` rows, err := s.db.Pool.Query(ctx, query) if err != nil { return nil, fmt.Errorf("failed to list schools: %w", err) } defer rows.Close() var schools []models.School for rows.Next() { var school models.School err := rows.Scan( &school.ID, &school.Name, &school.ShortName, &school.Type, &school.Address, &school.City, &school.PostalCode, &school.State, &school.Country, &school.Phone, &school.Email, &school.Website, &school.MatrixServerName, &school.LogoURL, &school.IsActive, &school.CreatedAt, &school.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to scan school: %w", err) } schools = append(schools, school) } return schools, nil } // ======================================== // School Year Management // ======================================== // CreateSchoolYear creates a new school year func (s *SchoolService) CreateSchoolYear(ctx context.Context, schoolID uuid.UUID, name string, startDate, endDate time.Time) (*models.SchoolYear, error) { schoolYear := &models.SchoolYear{ ID: uuid.New(), SchoolID: schoolID, Name: name, StartDate: startDate, EndDate: endDate, IsCurrent: false, CreatedAt: time.Now(), } query := ` INSERT INTO school_years (id, school_id, name, start_date, end_date, is_current, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id` err := s.db.Pool.QueryRow(ctx, query, schoolYear.ID, schoolYear.SchoolID, schoolYear.Name, schoolYear.StartDate, schoolYear.EndDate, schoolYear.IsCurrent, schoolYear.CreatedAt, ).Scan(&schoolYear.ID) if err != nil { return nil, fmt.Errorf("failed to create school year: %w", err) } return schoolYear, nil } // SetCurrentSchoolYear sets a school year as the current one func (s *SchoolService) SetCurrentSchoolYear(ctx context.Context, schoolID, schoolYearID uuid.UUID) error { // First, unset all current school years for this school _, err := s.db.Pool.Exec(ctx, `UPDATE school_years SET is_current = false WHERE school_id = $1`, schoolID) if err != nil { return fmt.Errorf("failed to unset current school years: %w", err) } // Then set the specified school year as current _, err = s.db.Pool.Exec(ctx, `UPDATE school_years SET is_current = true WHERE id = $1 AND school_id = $2`, schoolYearID, schoolID) if err != nil { return fmt.Errorf("failed to set current school year: %w", err) } return nil } // GetCurrentSchoolYear gets the current school year for a school func (s *SchoolService) GetCurrentSchoolYear(ctx context.Context, schoolID uuid.UUID) (*models.SchoolYear, error) { query := ` SELECT id, school_id, name, start_date, end_date, is_current, created_at FROM school_years WHERE school_id = $1 AND is_current = true` schoolYear := &models.SchoolYear{} err := s.db.Pool.QueryRow(ctx, query, schoolID).Scan( &schoolYear.ID, &schoolYear.SchoolID, &schoolYear.Name, &schoolYear.StartDate, &schoolYear.EndDate, &schoolYear.IsCurrent, &schoolYear.CreatedAt, ) if err != nil { return nil, fmt.Errorf("failed to get current school year: %w", err) } return schoolYear, nil } // ======================================== // Class Management // ======================================== // CreateClass creates a new class func (s *SchoolService) CreateClass(ctx context.Context, schoolID uuid.UUID, req models.CreateClassRequest) (*models.Class, error) { schoolYearID, err := uuid.Parse(req.SchoolYearID) if err != nil { return nil, fmt.Errorf("invalid school year ID: %w", err) } class := &models.Class{ ID: uuid.New(), SchoolID: schoolID, SchoolYearID: schoolYearID, Name: req.Name, Grade: req.Grade, Section: req.Section, Room: req.Room, IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } query := ` INSERT INTO classes (id, school_id, school_year_id, name, grade, section, room, is_active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id` err = s.db.Pool.QueryRow(ctx, query, class.ID, class.SchoolID, class.SchoolYearID, class.Name, class.Grade, class.Section, class.Room, class.IsActive, class.CreatedAt, class.UpdatedAt, ).Scan(&class.ID) if err != nil { return nil, fmt.Errorf("failed to create class: %w", err) } return class, nil } // GetClass retrieves a class by ID func (s *SchoolService) GetClass(ctx context.Context, classID uuid.UUID) (*models.Class, error) { query := ` SELECT id, school_id, school_year_id, name, grade, section, room, matrix_info_room, matrix_rep_room, is_active, created_at, updated_at FROM classes WHERE id = $1` class := &models.Class{} err := s.db.Pool.QueryRow(ctx, query, classID).Scan( &class.ID, &class.SchoolID, &class.SchoolYearID, &class.Name, &class.Grade, &class.Section, &class.Room, &class.MatrixInfoRoom, &class.MatrixRepRoom, &class.IsActive, &class.CreatedAt, &class.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to get class: %w", err) } return class, nil } // ListClasses lists all classes for a school in a school year func (s *SchoolService) ListClasses(ctx context.Context, schoolID, schoolYearID uuid.UUID) ([]models.Class, error) { query := ` SELECT id, school_id, school_year_id, name, grade, section, room, matrix_info_room, matrix_rep_room, is_active, created_at, updated_at FROM classes WHERE school_id = $1 AND school_year_id = $2 AND is_active = true ORDER BY grade, name` rows, err := s.db.Pool.Query(ctx, query, schoolID, schoolYearID) if err != nil { return nil, fmt.Errorf("failed to list classes: %w", err) } defer rows.Close() var classes []models.Class for rows.Next() { var class models.Class err := rows.Scan( &class.ID, &class.SchoolID, &class.SchoolYearID, &class.Name, &class.Grade, &class.Section, &class.Room, &class.MatrixInfoRoom, &class.MatrixRepRoom, &class.IsActive, &class.CreatedAt, &class.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to scan class: %w", err) } classes = append(classes, class) } return classes, nil } // ======================================== // Helper Functions // ======================================== func (s *SchoolService) createDefaultTimetableSlots(ctx context.Context, schoolID uuid.UUID) error { slots := []struct { Number int StartTime string EndTime string IsBreak bool Name string }{ {1, "08:00", "08:45", false, "1. Stunde"}, {2, "08:45", "09:30", false, "2. Stunde"}, {3, "09:30", "09:50", true, "Erste Pause"}, {4, "09:50", "10:35", false, "3. Stunde"}, {5, "10:35", "11:20", false, "4. Stunde"}, {6, "11:20", "11:40", true, "Zweite Pause"}, {7, "11:40", "12:25", false, "5. Stunde"}, {8, "12:25", "13:10", false, "6. Stunde"}, {9, "13:10", "14:00", true, "Mittagspause"}, {10, "14:00", "14:45", false, "7. Stunde"}, {11, "14:45", "15:30", false, "8. Stunde"}, } query := ` INSERT INTO timetable_slots (id, school_id, slot_number, start_time, end_time, is_break, name) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (school_id, slot_number) DO NOTHING` for _, slot := range slots { _, err := s.db.Pool.Exec(ctx, query, uuid.New(), schoolID, slot.Number, slot.StartTime, slot.EndTime, slot.IsBreak, slot.Name, ) if err != nil { return err } } return nil } func (s *SchoolService) createDefaultGradeScale(ctx context.Context, schoolID uuid.UUID) error { query := ` INSERT INTO grade_scales (id, school_id, name, min_value, max_value, passing_value, is_ascending, is_default, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT DO NOTHING` _, err := s.db.Pool.Exec(ctx, query, uuid.New(), schoolID, "1-6 (Noten)", 1.0, 6.0, 4.0, false, true, time.Now(), ) return err }