Files
breakpilot-lehrer/school-service/internal/models/timetable_solutions.go
T
Benjamin Admin bf5ea860cc
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 29s
CI / test-python-klausur (push) Failing after 3m56s
CI / test-python-agent-core (push) Successful in 19s
CI / test-nodejs-website (push) Successful in 23s
Phase 7: pinning, plan versions, solver budget + UX polish
Backend (school-service):
  - tt_solution gains parent_solution_id (self-FK, ON DELETE SET NULL)
    and seconds_limit columns via ALTER TABLE IF NOT EXISTS.
  - CreateTimetableSolutionRequest accepts optional parent_solution_id
    and seconds_limit (5-600s) with binding validation.
  - CreateSolution checks parent ownership before INSERT so users can't
    fork another tenant's plan.
  - New PUT /timetable/lessons/:id/pin endpoint; ownership enforced via
    the lesson's solution.created_by_user_id JOIN.

Solver:
  - Lesson.pinned now carries @PlanningPin so Timefold leaves locked
    cells untouched during the search.
  - build_problem() takes optional parent_solution_id; if set, copies
    pinned (class_id, subject_id, day, period, room) tuples onto fresh
    Lesson objects via greedy first-fit matching. Surplus pinned rows
    from curriculum changes are silently dropped.
  - _build_factory(seconds) replaces the module-level factory so each
    job honours its tt_solution.seconds_limit override.
  - persist_solution writes lesson.pinned back so subsequent re-solves
    inherit it.

Frontend (studio-v2):
  - SolutionList grows three knobs in the create-form: Basieren auf
    (parent dropdown, only completed solutions, disabled when none),
    Sekunden-Limit (5-600), and the existing Name.
  - PlanView cells get a pin/unpin button with optimistic update and
    rollback on error. Pinned cells gain an amber ring.
  - types.ts + api.ts mirror the new fields; lessonsApi.pin(id, bool).
  - HelpPanel: collapsible 6-step Bedienungsanleitung explaining the
    setup-to-plan workflow. Anchored at the top of /stundenplan above
    the dev token banner.
  - page.tsx switches to the same gradient + animated-blob background
    used on /korrektur so /stundenplan stops looking like a slate-900
    test page.
  - JWT dev banner gets a step-by-step explanation of how to grab the
    token from DevTools and a non-blocking success indicator (no more
    alert()).

Tests:
  - school-service: 6 new validator cases for parent_solution_id +
    seconds_limit boundaries. 73 subtests total, all green.
  - studio-v2: mockSchoolApi adds PUT /lessons/:id/pin route. 5 new
    Playwright tests across two suites (parent-selector visibility +
    options, seconds-limit input, pin button render, pin-icon flip).
    Existing tests adjusted to the new help panel + JWT banner wording.

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

64 lines
2.7 KiB
Go

package models
import (
"time"
"github.com/google/uuid"
)
// TimetableSolution is one run of the solver — exactly one row per solve.
// Lessons attached via tt_lesson.solution_id.
type TimetableSolution struct {
ID uuid.UUID `json:"id" db:"id"`
CreatedByUserID uuid.UUID `json:"created_by_user_id" db:"created_by_user_id"`
Name string `json:"name,omitempty" db:"name"`
Status string `json:"status" db:"status"`
HardScore *int `json:"hard_score,omitempty" db:"hard_score"`
SoftScore *int `json:"soft_score,omitempty" db:"soft_score"`
ErrorMessage string `json:"error_message,omitempty" db:"error_message"`
StartedAt *time.Time `json:"started_at,omitempty" db:"started_at"`
FinishedAt *time.Time `json:"finished_at,omitempty" db:"finished_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
ParentSolutionID *uuid.UUID `json:"parent_solution_id,omitempty" db:"parent_solution_id"`
SecondsLimit *int `json:"seconds_limit,omitempty" db:"seconds_limit"`
}
// TimetableLesson is one scheduled class-period in a solution.
type TimetableLesson struct {
ID uuid.UUID `json:"id" db:"id"`
SolutionID uuid.UUID `json:"solution_id" db:"solution_id"`
ClassID uuid.UUID `json:"class_id" db:"class_id"`
SubjectID uuid.UUID `json:"subject_id" db:"subject_id"`
TeacherID uuid.UUID `json:"teacher_id" db:"teacher_id"`
RoomID *uuid.UUID `json:"room_id,omitempty" db:"room_id"`
DayOfWeek int `json:"day_of_week" db:"day_of_week"`
PeriodIndex int `json:"period_index" db:"period_index"`
Pinned bool `json:"pinned" db:"pinned"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
// Joined fields for display
ClassName string `json:"class_name,omitempty"`
SubjectName string `json:"subject_name,omitempty"`
TeacherName string `json:"teacher_name,omitempty"`
RoomName string `json:"room_name,omitempty"`
}
// CreateTimetableSolutionRequest kicks off a solve. The solver-service is
// invoked async — this endpoint only registers the solution row and queues
// the job.
//
// ParentSolutionID, if set, instructs the solver to seed the new problem
// with the parent solution's pinned lessons (Phase 7 plan versioning).
// SecondsLimit overrides the default 60s solver budget.
type CreateTimetableSolutionRequest struct {
Name string `json:"name"`
ParentSolutionID *string `json:"parent_solution_id,omitempty" binding:"omitempty,uuid"`
SecondsLimit *int `json:"seconds_limit,omitempty" binding:"omitempty,min=5,max=600"`
}
// UpdateLessonPinRequest toggles tt_lesson.pinned. Used by the Plan view's
// pin/unpin button.
type UpdateLessonPinRequest struct {
Pinned bool `json:"pinned"`
}