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>
113 lines
4.0 KiB
Go
113 lines
4.0 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/breakpilot/school-service/internal/models"
|
||
)
|
||
|
||
// Curriculum and Assignment operations.
|
||
// Ownership is enforced by joining against tt_class.created_by_user_id.
|
||
|
||
// ---------- Curriculum (class × subject → weekly hours) ----------
|
||
|
||
func (s *TimetableService) CreateCurriculum(ctx context.Context, userID string, req *models.CreateTimetableCurriculumRequest) (*models.TimetableCurriculum, error) {
|
||
var c models.TimetableCurriculum
|
||
err := s.db.QueryRow(ctx, `
|
||
INSERT INTO tt_curriculum (class_id, subject_id, weekly_hours)
|
||
SELECT $1, $2, $3
|
||
WHERE EXISTS (SELECT 1 FROM tt_class WHERE id = $1 AND created_by_user_id = $4)
|
||
AND EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $4)
|
||
RETURNING id, class_id, subject_id, weekly_hours, created_at
|
||
`, req.ClassID, req.SubjectID, req.WeeklyHours, userID).Scan(
|
||
&c.ID, &c.ClassID, &c.SubjectID, &c.WeeklyHours, &c.CreatedAt,
|
||
)
|
||
return &c, err
|
||
}
|
||
|
||
func (s *TimetableService) ListCurriculum(ctx context.Context, userID string) ([]models.TimetableCurriculum, error) {
|
||
rows, err := s.db.Query(ctx, `
|
||
SELECT cu.id, cu.class_id, cu.subject_id, cu.weekly_hours, cu.created_at,
|
||
sub.name, cl.name
|
||
FROM tt_curriculum cu
|
||
JOIN tt_class cl ON cu.class_id = cl.id
|
||
JOIN tt_subject sub ON cu.subject_id = sub.id
|
||
WHERE cl.created_by_user_id = $1
|
||
ORDER BY cl.grade_level, cl.name, sub.name
|
||
`, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
var out []models.TimetableCurriculum
|
||
for rows.Next() {
|
||
var c models.TimetableCurriculum
|
||
if err := rows.Scan(&c.ID, &c.ClassID, &c.SubjectID, &c.WeeklyHours, &c.CreatedAt, &c.SubjectName, &c.ClassName); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, c)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *TimetableService) DeleteCurriculum(ctx context.Context, id, userID string) error {
|
||
_, err := s.db.Exec(ctx, `
|
||
DELETE FROM tt_curriculum cu
|
||
USING tt_class cl
|
||
WHERE cu.id = $1 AND cu.class_id = cl.id AND cl.created_by_user_id = $2
|
||
`, id, userID)
|
||
return err
|
||
}
|
||
|
||
// ---------- Assignment (teacher × class × subject) ----------
|
||
|
||
func (s *TimetableService) CreateAssignment(ctx context.Context, userID string, req *models.CreateTimetableAssignmentRequest) (*models.TimetableAssignment, error) {
|
||
var a models.TimetableAssignment
|
||
err := s.db.QueryRow(ctx, `
|
||
INSERT INTO tt_assignment (teacher_id, class_id, subject_id)
|
||
SELECT $1, $2, $3
|
||
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $1 AND created_by_user_id = $4)
|
||
AND EXISTS (SELECT 1 FROM tt_class WHERE id = $2 AND created_by_user_id = $4)
|
||
AND EXISTS (SELECT 1 FROM tt_subject WHERE id = $3 AND created_by_user_id = $4)
|
||
RETURNING id, teacher_id, class_id, subject_id, created_at
|
||
`, req.TeacherID, req.ClassID, req.SubjectID, userID).Scan(
|
||
&a.ID, &a.TeacherID, &a.ClassID, &a.SubjectID, &a.CreatedAt,
|
||
)
|
||
return &a, err
|
||
}
|
||
|
||
func (s *TimetableService) ListAssignments(ctx context.Context, userID string) ([]models.TimetableAssignment, error) {
|
||
rows, err := s.db.Query(ctx, `
|
||
SELECT a.id, a.teacher_id, a.class_id, a.subject_id, a.created_at,
|
||
t.last_name || ', ' || t.first_name, cl.name, sub.name
|
||
FROM tt_assignment a
|
||
JOIN tt_teacher t ON a.teacher_id = t.id
|
||
JOIN tt_class cl ON a.class_id = cl.id
|
||
JOIN tt_subject sub ON a.subject_id = sub.id
|
||
WHERE t.created_by_user_id = $1
|
||
ORDER BY cl.grade_level, cl.name, sub.name
|
||
`, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
var out []models.TimetableAssignment
|
||
for rows.Next() {
|
||
var a models.TimetableAssignment
|
||
if err := rows.Scan(&a.ID, &a.TeacherID, &a.ClassID, &a.SubjectID, &a.CreatedAt, &a.TeacherName, &a.ClassName, &a.SubjectName); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, a)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *TimetableService) DeleteAssignment(ctx context.Context, id, userID string) error {
|
||
_, err := s.db.Exec(ctx, `
|
||
DELETE FROM tt_assignment a
|
||
USING tt_teacher t
|
||
WHERE a.id = $1 AND a.teacher_id = t.id AND t.created_by_user_id = $2
|
||
`, id, userID)
|
||
return err
|
||
}
|