e958f88a2d
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>
175 lines
7.3 KiB
Go
175 lines
7.3 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/breakpilot/school-service/internal/models"
|
|
)
|
|
|
|
// Class- and room-scoped constraint CRUD. Ownership is via tt_class /
|
|
// tt_subject / tt_room.created_by_user_id.
|
|
|
|
// ---------- Class Max Hours / Day ----------
|
|
|
|
func (s *TimetableService) CreateClassMaxHoursDay(ctx context.Context, userID string, req *models.CreateClassMaxHoursDayRequest) (*models.ClassMaxHoursDay, error) {
|
|
var c models.ClassMaxHoursDay
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_constraint_class_max_hours_day
|
|
(created_by_user_id, class_id, max_hours, is_hard, weight, active, note)
|
|
SELECT $1, $2, $3, $4, $5, $6, $7
|
|
WHERE EXISTS (SELECT 1 FROM tt_class WHERE id = $2 AND created_by_user_id = $1)
|
|
RETURNING id, created_by_user_id, class_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
|
|
`, userID, req.ClassID, req.MaxHours, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
|
&c.ID, &c.CreatedByUserID, &c.ClassID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
|
)
|
|
return &c, err
|
|
}
|
|
|
|
func (s *TimetableService) ListClassMaxHoursDay(ctx context.Context, userID string) ([]models.ClassMaxHoursDay, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, class_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
|
|
FROM tt_constraint_class_max_hours_day WHERE created_by_user_id = $1 ORDER BY class_id
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.ClassMaxHoursDay
|
|
for rows.Next() {
|
|
var c models.ClassMaxHoursDay
|
|
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.ClassID, &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) DeleteClassMaxHoursDay(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_class_max_hours_day WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|
|
|
|
// ---------- Class No Gaps ----------
|
|
|
|
func (s *TimetableService) CreateClassNoGaps(ctx context.Context, userID string, req *models.CreateClassNoGapsRequest) (*models.ClassNoGaps, error) {
|
|
var c models.ClassNoGaps
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_constraint_class_no_gaps
|
|
(created_by_user_id, class_id, is_hard, weight, active, note)
|
|
SELECT $1, $2, $3, $4, $5, $6
|
|
WHERE EXISTS (SELECT 1 FROM tt_class WHERE id = $2 AND created_by_user_id = $1)
|
|
RETURNING id, created_by_user_id, class_id, is_hard, weight, active, COALESCE(note,''), created_at
|
|
`, userID, req.ClassID, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
|
&c.ID, &c.CreatedByUserID, &c.ClassID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
|
)
|
|
return &c, err
|
|
}
|
|
|
|
func (s *TimetableService) ListClassNoGaps(ctx context.Context, userID string) ([]models.ClassNoGaps, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, class_id, is_hard, weight, active, COALESCE(note,''), created_at
|
|
FROM tt_constraint_class_no_gaps WHERE created_by_user_id = $1 ORDER BY class_id
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.ClassNoGaps
|
|
for rows.Next() {
|
|
var c models.ClassNoGaps
|
|
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.ClassID, &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) DeleteClassNoGaps(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_class_no_gaps WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|
|
|
|
// ---------- Room Requires Type ----------
|
|
|
|
func (s *TimetableService) CreateRoomRequiresType(ctx context.Context, userID string, req *models.CreateRoomRequiresTypeRequest) (*models.RoomRequiresType, error) {
|
|
var c models.RoomRequiresType
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_constraint_room_requires_type
|
|
(created_by_user_id, subject_id, room_type, is_hard, weight, active, note)
|
|
SELECT $1, $2, $3, $4, $5, $6, $7
|
|
WHERE EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $1)
|
|
RETURNING id, created_by_user_id, subject_id, room_type, is_hard, weight, active, COALESCE(note,''), created_at
|
|
`, userID, req.SubjectID, req.RoomType, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
|
&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.RoomType, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
|
)
|
|
return &c, err
|
|
}
|
|
|
|
func (s *TimetableService) ListRoomRequiresTypes(ctx context.Context, userID string) ([]models.RoomRequiresType, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, subject_id, room_type, is_hard, weight, active, COALESCE(note,''), created_at
|
|
FROM tt_constraint_room_requires_type WHERE created_by_user_id = $1 ORDER BY subject_id
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.RoomRequiresType
|
|
for rows.Next() {
|
|
var c models.RoomRequiresType
|
|
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.RoomType, &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) DeleteRoomRequiresType(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_room_requires_type WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|
|
|
|
// ---------- Room Unavailable ----------
|
|
|
|
func (s *TimetableService) CreateRoomUnavailable(ctx context.Context, userID string, req *models.CreateRoomUnavailableRequest) (*models.RoomUnavailable, error) {
|
|
var c models.RoomUnavailable
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_constraint_room_unavailable
|
|
(created_by_user_id, room_id, day_of_week, period_index, is_hard, weight, active, note)
|
|
SELECT $1, $2, $3, $4, $5, $6, $7, $8
|
|
WHERE EXISTS (SELECT 1 FROM tt_room WHERE id = $2 AND created_by_user_id = $1)
|
|
RETURNING id, created_by_user_id, room_id, day_of_week, period_index, is_hard, weight, active, COALESCE(note,''), created_at
|
|
`, userID, req.RoomID, req.DayOfWeek, req.PeriodIndex, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
|
&c.ID, &c.CreatedByUserID, &c.RoomID, &c.DayOfWeek, &c.PeriodIndex, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
|
)
|
|
return &c, err
|
|
}
|
|
|
|
func (s *TimetableService) ListRoomUnavailable(ctx context.Context, userID string) ([]models.RoomUnavailable, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, room_id, day_of_week, period_index, is_hard, weight, active, COALESCE(note,''), created_at
|
|
FROM tt_constraint_room_unavailable WHERE created_by_user_id = $1 ORDER BY room_id, day_of_week, period_index
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.RoomUnavailable
|
|
for rows.Next() {
|
|
var c models.RoomUnavailable
|
|
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.RoomID, &c.DayOfWeek, &c.PeriodIndex, &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) DeleteRoomUnavailable(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_room_unavailable WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|