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>
214 lines
8.6 KiB
Go
214 lines
8.6 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/breakpilot/school-service/internal/models"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// TimetableService handles all CRUD for the school-wide timetable scheduler:
|
|
// classes, periods, rooms, subjects, teachers, curriculum, assignments.
|
|
type TimetableService struct {
|
|
db *pgxpool.Pool
|
|
}
|
|
|
|
func NewTimetableService(db *pgxpool.Pool) *TimetableService {
|
|
return &TimetableService{db: db}
|
|
}
|
|
|
|
// ---------- Classes ----------
|
|
|
|
func (s *TimetableService) CreateClass(ctx context.Context, userID string, req *models.CreateTimetableClassRequest) (*models.TimetableClass, error) {
|
|
var c models.TimetableClass
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_class (created_by_user_id, name, grade_level, student_count, notes)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, created_by_user_id, name, grade_level, student_count, notes, created_at
|
|
`, userID, req.Name, req.GradeLevel, req.StudentCount, req.Notes).Scan(
|
|
&c.ID, &c.CreatedByUserID, &c.Name, &c.GradeLevel, &c.StudentCount, &c.Notes, &c.CreatedAt,
|
|
)
|
|
return &c, err
|
|
}
|
|
|
|
func (s *TimetableService) ListClasses(ctx context.Context, userID string) ([]models.TimetableClass, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, name, grade_level, student_count, COALESCE(notes,''), created_at
|
|
FROM tt_class WHERE created_by_user_id = $1 ORDER BY grade_level, name
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.TimetableClass
|
|
for rows.Next() {
|
|
var c models.TimetableClass
|
|
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.Name, &c.GradeLevel, &c.StudentCount, &c.Notes, &c.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *TimetableService) DeleteClass(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_class WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|
|
|
|
// ---------- Periods ----------
|
|
|
|
func (s *TimetableService) CreatePeriod(ctx context.Context, userID string, req *models.CreateTimetablePeriodRequest) (*models.TimetablePeriod, error) {
|
|
var p models.TimetablePeriod
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_period (created_by_user_id, day_of_week, period_index, start_time, end_time, is_break, label)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, created_by_user_id, day_of_week, period_index, start_time::text, end_time::text, is_break, COALESCE(label,''), created_at
|
|
`, userID, req.DayOfWeek, req.PeriodIndex, req.StartTime, req.EndTime, req.IsBreak, req.Label).Scan(
|
|
&p.ID, &p.CreatedByUserID, &p.DayOfWeek, &p.PeriodIndex, &p.StartTime, &p.EndTime, &p.IsBreak, &p.Label, &p.CreatedAt,
|
|
)
|
|
return &p, err
|
|
}
|
|
|
|
func (s *TimetableService) ListPeriods(ctx context.Context, userID string) ([]models.TimetablePeriod, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, day_of_week, period_index, start_time::text, end_time::text, is_break, COALESCE(label,''), created_at
|
|
FROM tt_period WHERE created_by_user_id = $1 ORDER BY day_of_week, period_index
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.TimetablePeriod
|
|
for rows.Next() {
|
|
var p models.TimetablePeriod
|
|
if err := rows.Scan(&p.ID, &p.CreatedByUserID, &p.DayOfWeek, &p.PeriodIndex, &p.StartTime, &p.EndTime, &p.IsBreak, &p.Label, &p.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *TimetableService) DeletePeriod(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_period WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|
|
|
|
// ---------- Rooms ----------
|
|
|
|
func (s *TimetableService) CreateRoom(ctx context.Context, userID string, req *models.CreateTimetableRoomRequest) (*models.TimetableRoom, error) {
|
|
var r models.TimetableRoom
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_room (created_by_user_id, name, room_type, capacity, floor_level, has_elevator, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, created_by_user_id, name, COALESCE(room_type,''), capacity, floor_level, has_elevator, COALESCE(notes,''), created_at
|
|
`, userID, req.Name, req.RoomType, req.Capacity, req.FloorLevel, req.HasElevator, req.Notes).Scan(
|
|
&r.ID, &r.CreatedByUserID, &r.Name, &r.RoomType, &r.Capacity, &r.FloorLevel, &r.HasElevator, &r.Notes, &r.CreatedAt,
|
|
)
|
|
return &r, err
|
|
}
|
|
|
|
func (s *TimetableService) ListRooms(ctx context.Context, userID string) ([]models.TimetableRoom, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, name, COALESCE(room_type,''), capacity, floor_level, has_elevator, COALESCE(notes,''), created_at
|
|
FROM tt_room WHERE created_by_user_id = $1 ORDER BY name
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.TimetableRoom
|
|
for rows.Next() {
|
|
var r models.TimetableRoom
|
|
if err := rows.Scan(&r.ID, &r.CreatedByUserID, &r.Name, &r.RoomType, &r.Capacity, &r.FloorLevel, &r.HasElevator, &r.Notes, &r.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *TimetableService) DeleteRoom(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_room WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|
|
|
|
// ---------- Subjects ----------
|
|
|
|
func (s *TimetableService) CreateSubject(ctx context.Context, userID string, req *models.CreateTimetableSubjectRequest) (*models.TimetableSubject, error) {
|
|
var sub models.TimetableSubject
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_subject (created_by_user_id, name, short_code, color, is_main_subject, required_room_type)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id, created_by_user_id, name, short_code, COALESCE(color,''), is_main_subject, COALESCE(required_room_type,''), created_at
|
|
`, userID, req.Name, req.ShortCode, req.Color, req.IsMainSubject, req.RequiredRoomType).Scan(
|
|
&sub.ID, &sub.CreatedByUserID, &sub.Name, &sub.ShortCode, &sub.Color, &sub.IsMainSubject, &sub.RequiredRoomType, &sub.CreatedAt,
|
|
)
|
|
return &sub, err
|
|
}
|
|
|
|
func (s *TimetableService) ListSubjects(ctx context.Context, userID string) ([]models.TimetableSubject, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, name, short_code, COALESCE(color,''), is_main_subject, COALESCE(required_room_type,''), created_at
|
|
FROM tt_subject WHERE created_by_user_id = $1 ORDER BY name
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.TimetableSubject
|
|
for rows.Next() {
|
|
var sub models.TimetableSubject
|
|
if err := rows.Scan(&sub.ID, &sub.CreatedByUserID, &sub.Name, &sub.ShortCode, &sub.Color, &sub.IsMainSubject, &sub.RequiredRoomType, &sub.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, sub)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *TimetableService) DeleteSubject(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_subject WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|
|
|
|
// ---------- Teachers ----------
|
|
|
|
func (s *TimetableService) CreateTeacher(ctx context.Context, userID string, req *models.CreateTimetableTeacherRequest) (*models.TimetableTeacher, error) {
|
|
var t models.TimetableTeacher
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO tt_teacher (created_by_user_id, first_name, last_name, short_code, employment_percentage, max_hours_week, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, created_by_user_id, first_name, last_name, short_code, employment_percentage, max_hours_week, COALESCE(notes,''), created_at
|
|
`, userID, req.FirstName, req.LastName, req.ShortCode, req.EmploymentPercentage, req.MaxHoursWeek, req.Notes).Scan(
|
|
&t.ID, &t.CreatedByUserID, &t.FirstName, &t.LastName, &t.ShortCode, &t.EmploymentPercentage, &t.MaxHoursWeek, &t.Notes, &t.CreatedAt,
|
|
)
|
|
return &t, err
|
|
}
|
|
|
|
func (s *TimetableService) ListTeachers(ctx context.Context, userID string) ([]models.TimetableTeacher, error) {
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, created_by_user_id, first_name, last_name, short_code, employment_percentage, max_hours_week, COALESCE(notes,''), created_at
|
|
FROM tt_teacher WHERE created_by_user_id = $1 ORDER BY last_name, first_name
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []models.TimetableTeacher
|
|
for rows.Next() {
|
|
var t models.TimetableTeacher
|
|
if err := rows.Scan(&t.ID, &t.CreatedByUserID, &t.FirstName, &t.LastName, &t.ShortCode, &t.EmploymentPercentage, &t.MaxHoursWeek, &t.Notes, &t.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *TimetableService) DeleteTeacher(ctx context.Context, id, userID string) error {
|
|
_, err := s.db.Exec(ctx, `DELETE FROM tt_teacher WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
|
return err
|
|
}
|