Files
breakpilot-lehrer/school-service/internal/services/timetable_constraints_teacher.go
T
Benjamin Admin e958f88a2d Add timetable scheduler Phases 1 + 2 to school-service
Phase 1 — Stammdaten (7 tables):
  tt_class, tt_period, tt_room, tt_subject, tt_teacher,
  tt_curriculum, tt_assignment with CRUD endpoints.

Phase 2 — Constraints (15 typed tables):
  Teacher (6): unavailable_day, unavailable_window, max_hours_day,
    max_hours_week, excluded_subject, excluded_room
  Subject (5): min_day_gap, max_consecutive, contiguous_when_repeated,
    preferred_period, double_lesson
  Class (2): max_hours_day, no_gaps
  Room (2): requires_type, unavailable

Each constraint row carries is_hard / weight / active / note /
created_by_user_id; ownership enforced via WHERE EXISTS against the
parent tt_teacher/tt_class/tt_subject/tt_room row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:12:23 +02:00

264 lines
12 KiB
Go

package services
import (
"context"
"github.com/breakpilot/school-service/internal/models"
)
// Teacher-scoped constraint CRUD. Ownership is enforced via the parent
// tt_teacher row's created_by_user_id (and tt_subject / tt_room for the
// composite excluded-* constraints).
// ---------- Teacher Unavailable Day ----------
func (s *TimetableService) CreateTeacherUnavailableDay(ctx context.Context, userID string, req *models.CreateTeacherUnavailableDayRequest) (*models.TeacherUnavailableDay, error) {
var c models.TeacherUnavailableDay
err := s.db.QueryRow(ctx, `
INSERT INTO tt_constraint_teacher_unavailable_day
(created_by_user_id, teacher_id, day_of_week, is_hard, weight, active, note)
SELECT $1, $2, $3, $4, $5, $6, $7
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
RETURNING id, created_by_user_id, teacher_id, day_of_week, is_hard, weight, active, COALESCE(note,''), created_at
`, userID, req.TeacherID, req.DayOfWeek, req.IsHard, req.Weight, req.Active, req.Note).Scan(
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.DayOfWeek, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
)
return &c, err
}
func (s *TimetableService) ListTeacherUnavailableDays(ctx context.Context, userID string) ([]models.TeacherUnavailableDay, error) {
rows, err := s.db.Query(ctx, `
SELECT id, created_by_user_id, teacher_id, day_of_week, is_hard, weight, active, COALESCE(note,''), created_at
FROM tt_constraint_teacher_unavailable_day
WHERE created_by_user_id = $1
ORDER BY teacher_id, day_of_week
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.TeacherUnavailableDay
for rows.Next() {
var c models.TeacherUnavailableDay
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.DayOfWeek, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
return nil, err
}
out = append(out, c)
}
return out, nil
}
func (s *TimetableService) DeleteTeacherUnavailableDay(ctx context.Context, id, userID string) error {
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_unavailable_day WHERE id = $1 AND created_by_user_id = $2`, id, userID)
return err
}
// ---------- Teacher Unavailable Window ----------
func (s *TimetableService) CreateTeacherUnavailableWindow(ctx context.Context, userID string, req *models.CreateTeacherUnavailableWindowRequest) (*models.TeacherUnavailableWindow, error) {
var c models.TeacherUnavailableWindow
err := s.db.QueryRow(ctx, `
INSERT INTO tt_constraint_teacher_unavailable_window
(created_by_user_id, teacher_id, day_of_week, start_time, end_time, is_hard, weight, active, note)
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
RETURNING id, created_by_user_id, teacher_id, day_of_week, start_time::text, end_time::text, is_hard, weight, active, COALESCE(note,''), created_at
`, userID, req.TeacherID, req.DayOfWeek, req.StartTime, req.EndTime, req.IsHard, req.Weight, req.Active, req.Note).Scan(
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.DayOfWeek, &c.StartTime, &c.EndTime, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
)
return &c, err
}
func (s *TimetableService) ListTeacherUnavailableWindows(ctx context.Context, userID string) ([]models.TeacherUnavailableWindow, error) {
rows, err := s.db.Query(ctx, `
SELECT id, created_by_user_id, teacher_id, day_of_week, start_time::text, end_time::text, is_hard, weight, active, COALESCE(note,''), created_at
FROM tt_constraint_teacher_unavailable_window
WHERE created_by_user_id = $1
ORDER BY teacher_id, day_of_week, start_time
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.TeacherUnavailableWindow
for rows.Next() {
var c models.TeacherUnavailableWindow
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.DayOfWeek, &c.StartTime, &c.EndTime, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
return nil, err
}
out = append(out, c)
}
return out, nil
}
func (s *TimetableService) DeleteTeacherUnavailableWindow(ctx context.Context, id, userID string) error {
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_unavailable_window WHERE id = $1 AND created_by_user_id = $2`, id, userID)
return err
}
// ---------- Teacher Max Hours / Day ----------
func (s *TimetableService) CreateTeacherMaxHoursDay(ctx context.Context, userID string, req *models.CreateTeacherMaxHoursDayRequest) (*models.TeacherMaxHoursDay, error) {
var c models.TeacherMaxHoursDay
err := s.db.QueryRow(ctx, `
INSERT INTO tt_constraint_teacher_max_hours_day
(created_by_user_id, teacher_id, max_hours, is_hard, weight, active, note)
SELECT $1, $2, $3, $4, $5, $6, $7
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
RETURNING id, created_by_user_id, teacher_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
`, userID, req.TeacherID, req.MaxHours, req.IsHard, req.Weight, req.Active, req.Note).Scan(
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
)
return &c, err
}
func (s *TimetableService) ListTeacherMaxHoursDay(ctx context.Context, userID string) ([]models.TeacherMaxHoursDay, error) {
rows, err := s.db.Query(ctx, `
SELECT id, created_by_user_id, teacher_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
FROM tt_constraint_teacher_max_hours_day WHERE created_by_user_id = $1 ORDER BY teacher_id
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.TeacherMaxHoursDay
for rows.Next() {
var c models.TeacherMaxHoursDay
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
return nil, err
}
out = append(out, c)
}
return out, nil
}
func (s *TimetableService) DeleteTeacherMaxHoursDay(ctx context.Context, id, userID string) error {
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_max_hours_day WHERE id = $1 AND created_by_user_id = $2`, id, userID)
return err
}
// ---------- Teacher Max Hours / Week ----------
func (s *TimetableService) CreateTeacherMaxHoursWeek(ctx context.Context, userID string, req *models.CreateTeacherMaxHoursWeekRequest) (*models.TeacherMaxHoursWeek, error) {
var c models.TeacherMaxHoursWeek
err := s.db.QueryRow(ctx, `
INSERT INTO tt_constraint_teacher_max_hours_week
(created_by_user_id, teacher_id, max_hours, is_hard, weight, active, note)
SELECT $1, $2, $3, $4, $5, $6, $7
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
RETURNING id, created_by_user_id, teacher_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
`, userID, req.TeacherID, req.MaxHours, req.IsHard, req.Weight, req.Active, req.Note).Scan(
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
)
return &c, err
}
func (s *TimetableService) ListTeacherMaxHoursWeek(ctx context.Context, userID string) ([]models.TeacherMaxHoursWeek, error) {
rows, err := s.db.Query(ctx, `
SELECT id, created_by_user_id, teacher_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
FROM tt_constraint_teacher_max_hours_week WHERE created_by_user_id = $1 ORDER BY teacher_id
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.TeacherMaxHoursWeek
for rows.Next() {
var c models.TeacherMaxHoursWeek
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
return nil, err
}
out = append(out, c)
}
return out, nil
}
func (s *TimetableService) DeleteTeacherMaxHoursWeek(ctx context.Context, id, userID string) error {
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_max_hours_week WHERE id = $1 AND created_by_user_id = $2`, id, userID)
return err
}
// ---------- Teacher Excluded Subject ----------
func (s *TimetableService) CreateTeacherExcludedSubject(ctx context.Context, userID string, req *models.CreateTeacherExcludedSubjectRequest) (*models.TeacherExcludedSubject, error) {
var c models.TeacherExcludedSubject
err := s.db.QueryRow(ctx, `
INSERT INTO tt_constraint_teacher_excluded_subject
(created_by_user_id, teacher_id, subject_id, is_hard, weight, active, note)
SELECT $1, $2, $3, $4, $5, $6, $7
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
AND EXISTS (SELECT 1 FROM tt_subject WHERE id = $3 AND created_by_user_id = $1)
RETURNING id, created_by_user_id, teacher_id, subject_id, is_hard, weight, active, COALESCE(note,''), created_at
`, userID, req.TeacherID, req.SubjectID, req.IsHard, req.Weight, req.Active, req.Note).Scan(
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.SubjectID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
)
return &c, err
}
func (s *TimetableService) ListTeacherExcludedSubjects(ctx context.Context, userID string) ([]models.TeacherExcludedSubject, error) {
rows, err := s.db.Query(ctx, `
SELECT id, created_by_user_id, teacher_id, subject_id, is_hard, weight, active, COALESCE(note,''), created_at
FROM tt_constraint_teacher_excluded_subject WHERE created_by_user_id = $1 ORDER BY teacher_id, subject_id
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.TeacherExcludedSubject
for rows.Next() {
var c models.TeacherExcludedSubject
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.SubjectID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
return nil, err
}
out = append(out, c)
}
return out, nil
}
func (s *TimetableService) DeleteTeacherExcludedSubject(ctx context.Context, id, userID string) error {
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_excluded_subject WHERE id = $1 AND created_by_user_id = $2`, id, userID)
return err
}
// ---------- Teacher Excluded Room ----------
func (s *TimetableService) CreateTeacherExcludedRoom(ctx context.Context, userID string, req *models.CreateTeacherExcludedRoomRequest) (*models.TeacherExcludedRoom, error) {
var c models.TeacherExcludedRoom
err := s.db.QueryRow(ctx, `
INSERT INTO tt_constraint_teacher_excluded_room
(created_by_user_id, teacher_id, room_id, is_hard, weight, active, note)
SELECT $1, $2, $3, $4, $5, $6, $7
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
AND EXISTS (SELECT 1 FROM tt_room WHERE id = $3 AND created_by_user_id = $1)
RETURNING id, created_by_user_id, teacher_id, room_id, is_hard, weight, active, COALESCE(note,''), created_at
`, userID, req.TeacherID, req.RoomID, req.IsHard, req.Weight, req.Active, req.Note).Scan(
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.RoomID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
)
return &c, err
}
func (s *TimetableService) ListTeacherExcludedRooms(ctx context.Context, userID string) ([]models.TeacherExcludedRoom, error) {
rows, err := s.db.Query(ctx, `
SELECT id, created_by_user_id, teacher_id, room_id, is_hard, weight, active, COALESCE(note,''), created_at
FROM tt_constraint_teacher_excluded_room WHERE created_by_user_id = $1 ORDER BY teacher_id, room_id
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.TeacherExcludedRoom
for rows.Next() {
var c models.TeacherExcludedRoom
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.RoomID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
return nil, err
}
out = append(out, c)
}
return out, nil
}
func (s *TimetableService) DeleteTeacherExcludedRoom(ctx context.Context, id, userID string) error {
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_excluded_room WHERE id = $1 AND created_by_user_id = $2`, id, userID)
return err
}