package services import ( "context" "fmt" "github.com/breakpilot/school-service/internal/models" "github.com/google/uuid" ) // School-event CRUD. Ownership is per-user via created_by_user_id. Public // holiday/Ferien data lives in cal_public_event and is handled by // calendar_service.go. func (s *CalendarService) CreateEvent(ctx context.Context, userID string, req *models.CreateSchoolEventRequest) (*models.SchoolEvent, error) { classIDs, err := parseClassIDs(req.AffectedClassIDs) if err != nil { return nil, err } var e models.SchoolEvent leadDays := req.NotificationLeadDays if leadDays == nil { leadDays = []int{7, 1} } row := s.db.QueryRow(ctx, ` INSERT INTO cal_school_event (created_by_user_id, title, description, event_type, is_school_free, start_date, end_date, start_time, end_time, affected_class_ids, visible_to_parents, notify_parents, notify_students, notification_lead_days) VALUES ($1, $2, $3, $4, $5, $6::date, $7::date, NULLIF($8, '')::time, NULLIF($9, '')::time, $10, $11, $12, $13, $14) RETURNING id, created_by_user_id, title, COALESCE(description,''), event_type, is_school_free, start_date::text, end_date::text, start_time::text, end_time::text, affected_class_ids, visible_to_parents, notify_parents, notify_students, notification_lead_days, created_at, updated_at `, userID, req.Title, req.Description, req.EventType, req.IsSchoolFree, req.StartDate, req.EndDate, strOrEmpty(req.StartTime), strOrEmpty(req.EndTime), classIDs, req.VisibleToParents, req.NotifyParents, req.NotifyStudents, leadDays) if err := scanEvent(row, &e); err != nil { return nil, err } return &e, nil } func (s *CalendarService) ListEvents(ctx context.Context, userID, from, to string) ([]models.SchoolEvent, error) { if from == "" { from = "1900-01-01" } if to == "" { to = "2100-12-31" } rows, err := s.db.Query(ctx, ` SELECT id, created_by_user_id, title, COALESCE(description,''), event_type, is_school_free, start_date::text, end_date::text, start_time::text, end_time::text, affected_class_ids, visible_to_parents, notify_parents, notify_students, notification_lead_days, created_at, updated_at FROM cal_school_event WHERE created_by_user_id = $1 AND end_date >= $2::date AND start_date <= $3::date ORDER BY start_date, start_time NULLS FIRST, title `, userID, from, to) if err != nil { return nil, err } defer rows.Close() var out []models.SchoolEvent for rows.Next() { var e models.SchoolEvent if err := scanEvent(rows, &e); err != nil { return nil, err } out = append(out, e) } return out, nil } func (s *CalendarService) DeleteEvent(ctx context.Context, id, userID string) error { res, err := s.db.Exec(ctx, ` DELETE FROM cal_school_event WHERE id = $1 AND created_by_user_id = $2 `, id, userID) if err != nil { return err } if res.RowsAffected() == 0 { return fmt.Errorf("event not found or not owned") } return nil } // parseClassIDs validates the array of UUID strings the request sent. // Returns a typed []uuid.UUID so asyncpg/pgx encodes it correctly into the // UUID[] column. func parseClassIDs(in []string) ([]uuid.UUID, error) { out := make([]uuid.UUID, 0, len(in)) for _, s := range in { if s == "" { continue } u, err := uuid.Parse(s) if err != nil { return nil, fmt.Errorf("invalid class_id %q: %w", s, err) } out = append(out, u) } return out, nil } // row interface so the same scan logic works for both QueryRow and Rows. type rowScanner interface { Scan(dest ...any) error } func scanEvent(r rowScanner, e *models.SchoolEvent) error { var startTime, endTime *string if err := r.Scan( &e.ID, &e.CreatedByUserID, &e.Title, &e.Description, &e.EventType, &e.IsSchoolFree, &e.StartDate, &e.EndDate, &startTime, &endTime, &e.AffectedClassIDs, &e.VisibleToParents, &e.NotifyParents, &e.NotifyStudents, &e.NotificationLeadDays, &e.CreatedAt, &e.UpdatedAt, ); err != nil { return err } e.StartTime = startTime e.EndTime = endTime return nil }