From 24997710efe0d98efdaf77f18c9e45ef5d62349e Mon Sep 17 00:00:00 2001 From: haxala1r <53535669+haxala1r@users.noreply.github.com> Date: Sun, 14 Dec 2025 19:21:14 +0300 Subject: [PATCH] Completed edit functionality for examinations in the calendar view. --- .../se302/controller/ExamEditDialog.java | 372 ++++++++++++++++++ .../ScheduleCalendarController.java | 104 +++-- .../service/ConstraintValidationService.java | 274 +++++++++++++ 3 files changed, 716 insertions(+), 34 deletions(-) create mode 100644 src/main/java/org/example/se302/controller/ExamEditDialog.java create mode 100644 src/main/java/org/example/se302/service/ConstraintValidationService.java diff --git a/src/main/java/org/example/se302/controller/ExamEditDialog.java b/src/main/java/org/example/se302/controller/ExamEditDialog.java new file mode 100644 index 0000000..3334921 --- /dev/null +++ b/src/main/java/org/example/se302/controller/ExamEditDialog.java @@ -0,0 +1,372 @@ +package org.example.se302.controller; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.util.StringConverter; +import org.example.se302.model.*; +import org.example.se302.service.ConstraintValidationService; +import org.example.se302.service.ConstraintValidationService.ValidationResult; +import org.example.se302.service.DataManager; + +import java.util.Optional; + +/** + * Dialog for editing an exam assignment. + * Provides real-time constraint validation with detailed violation info. + */ +public class ExamEditDialog extends Dialog { + + private final String courseCode; + private final ScheduleState scheduleState; + private final ScheduleConfiguration config; + private final DataManager dataManager; + private final ConstraintValidationService validationService; + + // UI Components + private ComboBox dayComboBox; + private ComboBox slotComboBox; + private ComboBox classroomComboBox; + private VBox validationPanel; + private Label validationStatusLabel; + private TextArea violationDetails; + private Button applyButton; + private Button applyAnywayButton; + + // Current state + private ValidationResult currentValidation; + + public ExamEditDialog(ExamAssignment assignment, ScheduleState scheduleState, ScheduleConfiguration config) { + this.courseCode = assignment.getCourseCode(); + this.scheduleState = scheduleState; + this.config = config; + this.dataManager = DataManager.getInstance(); + this.validationService = new ConstraintValidationService(); + + setTitle("Exam Details"); + setHeaderText(courseCode); + + buildUI(assignment); + setupValidation(); + setupButtons(); + + // Initial validation + validateCurrentSelection(); + } + + private void buildUI(ExamAssignment assignment) { + GridPane grid = new GridPane(); + grid.setHgap(15); + grid.setVgap(15); + grid.setPadding(new Insets(20)); + + // Course info header + Course course = dataManager.getCourse(courseCode); + Classroom currentRoom = dataManager.getClassroom(assignment.getClassroomId()); + int studentCount = course != null ? course.getEnrolledStudentsCount() : 0; + + Label courseLabel = new Label("Course: " + courseCode); + courseLabel.setFont(Font.font("System", FontWeight.BOLD, 14)); + + Label studentLabel = new Label("Students enrolled: " + studentCount); + studentLabel.setStyle("-fx-text-fill: #666;"); + + // Current assignment info + String currentInfo = String.format("Current: Day %d, Slot %d, Room %s", + assignment.getDay() + 1, assignment.getTimeSlotIndex() + 1, assignment.getClassroomId()); + Label currentLabel = new Label(currentInfo); + currentLabel.setStyle("-fx-text-fill: #666;"); + + // Capacity info + String capacityInfo = ""; + if (currentRoom != null) { + double utilization = (double) studentCount / currentRoom.getCapacity() * 100; + capacityInfo = String.format("Capacity: %d/%d (%.0f%% utilization)", + studentCount, currentRoom.getCapacity(), utilization); + } + Label capacityLabel = new Label(capacityInfo); + capacityLabel.setStyle("-fx-text-fill: #666;"); + + VBox headerBox = new VBox(5, courseLabel, studentLabel, currentLabel, capacityLabel); + headerBox.setStyle("-fx-padding: 5; -fx-background-color: #f8f9fa; -fx-background-radius: 5;"); + grid.add(headerBox, 0, 0, 2, 1); + + // Separator + Separator sep = new Separator(); + grid.add(sep, 0, 1, 2, 1); + + // Day selector + grid.add(new Label("Day:"), 0, 2); + dayComboBox = new ComboBox<>(); + for (int i = 0; i < config.getNumDays(); i++) { + dayComboBox.getItems().add(i); + } + dayComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Integer day) { + if (day == null) + return ""; + TimeSlot slot = config.getTimeSlot(day, 0); + String dateStr = slot != null ? " (" + slot.getDate().toString() + ")" : ""; + return "Day " + (day + 1) + dateStr; + } + + @Override + public Integer fromString(String string) { + return null; + } + }); + dayComboBox.setValue(assignment.getDay()); + dayComboBox.setMaxWidth(Double.MAX_VALUE); + grid.add(dayComboBox, 1, 2); + + // Time slot selector + grid.add(new Label("Time Slot:"), 0, 3); + slotComboBox = new ComboBox<>(); + for (int i = 0; i < config.getSlotsPerDay(); i++) { + slotComboBox.getItems().add(i); + } + slotComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Integer slot) { + if (slot == null) + return ""; + TimeSlot ts = config.getTimeSlot(0, slot); + String timeStr = ts != null ? " (" + ts.getStartTime() + " - " + ts.getEndTime() + ")" : ""; + return "Slot " + (slot + 1) + timeStr; + } + + @Override + public Integer fromString(String string) { + return null; + } + }); + slotComboBox.setValue(assignment.getTimeSlotIndex()); + slotComboBox.setMaxWidth(Double.MAX_VALUE); + grid.add(slotComboBox, 1, 3); + + // Classroom selector + grid.add(new Label("Classroom:"), 0, 4); + classroomComboBox = new ComboBox<>(); + classroomComboBox.getItems().addAll(dataManager.getClassrooms()); + classroomComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Classroom classroom) { + if (classroom == null) + return ""; + return classroom.getClassroomId() + " (Capacity: " + classroom.getCapacity() + ")"; + } + + @Override + public Classroom fromString(String string) { + return null; + } + }); + // Set current classroom + Classroom currentClassroom = dataManager.getClassroom(assignment.getClassroomId()); + classroomComboBox.setValue(currentClassroom); + classroomComboBox.setMaxWidth(Double.MAX_VALUE); + grid.add(classroomComboBox, 1, 4); + + // Column constraints + ColumnConstraints col1 = new ColumnConstraints(); + col1.setMinWidth(80); + ColumnConstraints col2 = new ColumnConstraints(); + col2.setMinWidth(250); + col2.setHgrow(Priority.ALWAYS); + grid.getColumnConstraints().addAll(col1, col2); + + // Separator before validation + Separator sep2 = new Separator(); + grid.add(sep2, 0, 5, 2, 1); + + // Validation panel + validationPanel = new VBox(10); + validationPanel.setPadding(new Insets(10)); + validationPanel.setStyle("-fx-background-color: #f5f5f5; -fx-background-radius: 5;"); + + validationStatusLabel = new Label("Checking constraints..."); + validationStatusLabel.setFont(Font.font("System", FontWeight.BOLD, 12)); + + violationDetails = new TextArea(); + violationDetails.setEditable(false); + violationDetails.setWrapText(true); + violationDetails.setPrefRowCount(4); + violationDetails.setMaxHeight(120); + violationDetails.setStyle("-fx-control-inner-background: #f5f5f5;"); + + validationPanel.getChildren().addAll(validationStatusLabel, violationDetails); + grid.add(validationPanel, 0, 6, 2, 1); + + getDialogPane().setContent(grid); + getDialogPane().setPrefWidth(450); + } + + private void setupValidation() { + // Add listeners to all inputs + dayComboBox.valueProperty().addListener((obs, old, newVal) -> validateCurrentSelection()); + slotComboBox.valueProperty().addListener((obs, old, newVal) -> validateCurrentSelection()); + classroomComboBox.valueProperty().addListener((obs, old, newVal) -> validateCurrentSelection()); + } + + private void validateCurrentSelection() { + Integer day = dayComboBox.getValue(); + Integer slot = slotComboBox.getValue(); + Classroom classroom = classroomComboBox.getValue(); + + if (day == null || slot == null || classroom == null) { + validationStatusLabel.setText("⚠️ Please select all fields"); + validationStatusLabel.setTextFill(Color.ORANGE); + violationDetails.setText(""); + updateButtonStates(false, false); + return; + } + + // Perform validation + currentValidation = validationService.validateAssignment( + courseCode, day, slot, classroom.getClassroomId(), scheduleState); + + if (currentValidation.isValid()) { + validationStatusLabel.setText("✓ No constraint violations"); + validationStatusLabel.setTextFill(Color.web("#27ae60")); + violationDetails.setText(""); + validationPanel.setStyle("-fx-background-color: #e8f5e9; -fx-background-radius: 5;"); + updateButtonStates(true, false); + } else if (currentValidation.hasHardViolations()) { + validationStatusLabel.setText("❌ Constraint violations found"); + validationStatusLabel.setTextFill(Color.web("#e74c3c")); + violationDetails.setText(currentValidation.getFormattedMessage()); + validationPanel.setStyle("-fx-background-color: #ffebee; -fx-background-radius: 5;"); + updateButtonStates(false, true); + } else { + validationStatusLabel.setText("⚠️ Warnings found"); + validationStatusLabel.setTextFill(Color.web("#f39c12")); + violationDetails.setText(currentValidation.getFormattedMessage()); + validationPanel.setStyle("-fx-background-color: #fff3e0; -fx-background-radius: 5;"); + updateButtonStates(true, true); + } + } + + private void updateButtonStates(boolean applyEnabled, boolean showApplyAnyway) { + if (applyButton != null) { + applyButton.setDisable(!applyEnabled); + } + if (applyAnywayButton != null) { + applyAnywayButton.setVisible(showApplyAnyway); + applyAnywayButton.setManaged(showApplyAnyway); + } + } + + private void setupButtons() { + // Create custom button types + ButtonType cancelButtonType = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); + ButtonType applyButtonType = new ButtonType("Apply", ButtonBar.ButtonData.OK_DONE); + getDialogPane().getButtonTypes().addAll(cancelButtonType, applyButtonType); + + applyButton = (Button) getDialogPane().lookupButton(applyButtonType); + applyButton.setDefaultButton(true); + + // Add "Apply Anyway" button for when there are violations + applyAnywayButton = new Button("Apply Anyway"); + applyAnywayButton.setStyle("-fx-background-color: #e74c3c; -fx-text-fill: white;"); + applyAnywayButton.setVisible(false); + applyAnywayButton.setManaged(false); + + // Find the button bar and add our custom button + ButtonBar buttonBar = (ButtonBar) getDialogPane().lookup(".button-bar"); + if (buttonBar != null) { + ButtonBar.setButtonData(applyAnywayButton, ButtonBar.ButtonData.LEFT); + buttonBar.getButtons().add(0, applyAnywayButton); + } + + // Handle apply anyway + applyAnywayButton.setOnAction(e -> { + if (currentValidation != null && currentValidation.hasHardViolations()) { + // Show confirmation for hard violations + Alert confirm = new Alert(Alert.AlertType.WARNING); + confirm.setTitle("Confirm Override"); + confirm.setHeaderText("Override Constraint Violations?"); + confirm.setContentText( + "You are about to apply changes that violate hard constraints:\n\n" + + currentValidation.getFormattedMessage() + "\n\n" + + "This may cause scheduling conflicts for students. Are you sure?"); + confirm.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO); + + Optional result = confirm.showAndWait(); + if (result.isPresent() && result.get() == ButtonType.YES) { + setResultAndClose(true); + } + } else { + setResultAndClose(true); + } + }); + + // Set result converter + setResultConverter(dialogButton -> { + if (dialogButton == applyButtonType) { + return createResult(false); + } + return null; + }); + } + + private void setResultAndClose(boolean forced) { + EditResult result = createResult(forced); + setResult(result); + close(); + } + + private EditResult createResult(boolean forced) { + Integer day = dayComboBox.getValue(); + Integer slot = slotComboBox.getValue(); + Classroom classroom = classroomComboBox.getValue(); + + if (day == null || slot == null || classroom == null) { + return null; + } + + return new EditResult(courseCode, day, slot, classroom.getClassroomId(), forced); + } + + /** + * Result of the edit dialog. + */ + public static class EditResult { + private final String courseCode; + private final int day; + private final int timeSlot; + private final String classroomId; + private final boolean forcedOverride; + + public EditResult(String courseCode, int day, int timeSlot, String classroomId, boolean forcedOverride) { + this.courseCode = courseCode; + this.day = day; + this.timeSlot = timeSlot; + this.classroomId = classroomId; + this.forcedOverride = forcedOverride; + } + + public String getCourseCode() { + return courseCode; + } + + public int getDay() { + return day; + } + + public int getTimeSlot() { + return timeSlot; + } + + public String getClassroomId() { + return classroomId; + } + + public boolean isForcedOverride() { + return forcedOverride; + } + } +} diff --git a/src/main/java/org/example/se302/controller/ScheduleCalendarController.java b/src/main/java/org/example/se302/controller/ScheduleCalendarController.java index 47925c0..bb47a4f 100644 --- a/src/main/java/org/example/se302/controller/ScheduleCalendarController.java +++ b/src/main/java/org/example/se302/controller/ScheduleCalendarController.java @@ -80,6 +80,7 @@ public class ScheduleCalendarController { private final DataManager dataManager = DataManager.getInstance(); private ScheduleGeneratorService generatorService; private ScheduleState currentSchedule; + private ScheduleConfiguration currentConfig; private Thread generationThread; @FXML @@ -300,6 +301,7 @@ public class ScheduleCalendarController { if (result.isSuccess()) { currentSchedule = result.getScheduleState(); + currentConfig = config; // Save schedule to DataManager (so other views can see it) saveScheduleToDataManager(currentSchedule, config); @@ -474,9 +476,6 @@ public class ScheduleCalendarController { examBox.setStyle("-fx-background-color: #3498db; -fx-background-radius: 3; -fx-padding: 3 6;"); examBox.setCursor(Cursor.HAND); - // Add click handler - examBox.setOnMouseClicked(e -> showAssignmentDetails(exam)); - Label courseLabel = new Label(exam.getCourseCode()); courseLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 11;"); @@ -485,8 +484,18 @@ public class ScheduleCalendarController { examBox.getChildren().addAll(courseLabel, roomLabel); - // Add tooltip - Tooltip tooltip = new Tooltip(exam.getCourseCode() + "\n" + exam.getClassroomId()); + // Hover highlight + examBox.setOnMouseEntered(e -> examBox + .setStyle("-fx-background-color: #2980b9; -fx-background-radius: 3; -fx-padding: 3 6;")); + examBox.setOnMouseExited(e -> examBox + .setStyle("-fx-background-color: #3498db; -fx-background-radius: 3; -fx-padding: 3 6;")); + + // Click opens the edit dialog (which also shows details) + examBox.setOnMouseClicked(e -> openEditDialog(exam)); + + // Tooltip with hint + Tooltip tooltip = new Tooltip( + exam.getCourseCode() + "\n" + exam.getClassroomId() + "\nClick to view/edit"); Tooltip.install(examBox, tooltip); cell.getChildren().add(examBox); @@ -503,35 +512,6 @@ public class ScheduleCalendarController { return cell; } - private void showAssignmentDetails(ExamAssignment exam) { - Course course = dataManager.getCourse(exam.getCourseCode()); - Classroom room = dataManager.getClassroom(exam.getClassroomId()); - - Alert alert = new Alert(Alert.AlertType.INFORMATION); - alert.setTitle("Exam Details"); - alert.setHeaderText(exam.getCourseCode()); - - StringBuilder content = new StringBuilder(); - content.append("Course: ").append(exam.getCourseCode()).append("\n"); - if (course != null) { - content.append("Students: ").append(course.getEnrolledStudentsCount()).append("\n"); - } - content.append("\n"); - content.append("Assigned Classroom: ").append(exam.getClassroomId()).append("\n"); - if (room != null) { - content.append("Capacity: ").append(room.getCapacity()).append("\n"); - double fillRatio = course != null ? (double) course.getEnrolledStudentsCount() / room.getCapacity() * 100 - : 0; - content.append(String.format("Utilization: %.1f%%\n", fillRatio)); - } - content.append("\n"); - content.append("Day: ").append(exam.getDay() + 1).append("\n"); - content.append("Slot: ").append(exam.getTimeSlotIndex() + 1).append("\n"); - - alert.setContentText(content.toString()); - alert.showAndWait(); - } - @FXML private void onCancelGeneration() { if (generatorService != null) { @@ -549,4 +529,60 @@ public class ScheduleCalendarController { alert.setContentText(message); alert.showAndWait(); } + + /** + * Opens the edit dialog for an exam assignment. + */ + private void openEditDialog(ExamAssignment exam) { + if (currentSchedule == null || currentConfig == null) { + showAlert(Alert.AlertType.WARNING, "Cannot Edit", + "No schedule is currently loaded. Please generate a schedule first."); + return; + } + + ExamEditDialog dialog = new ExamEditDialog(exam, currentSchedule, currentConfig); + dialog.showAndWait().ifPresent(result -> { + applyExamEdit(result); + }); + } + + /** + * Applies an edit result to the schedule. + */ + private void applyExamEdit(ExamEditDialog.EditResult result) { + if (result == null) + return; + + String courseCode = result.getCourseCode(); + int newDay = result.getDay(); + int newSlot = result.getTimeSlot(); + String newClassroom = result.getClassroomId(); + + // Update the ScheduleState + boolean updated = currentSchedule.updateAssignment(courseCode, newDay, newSlot, newClassroom); + + if (updated) { + // Also update the Course in DataManager + Course course = dataManager.getCourse(courseCode); + if (course != null) { + course.setExamSchedule(newDay, newSlot, newClassroom); + } + + // Refresh the grid display + displayScheduleGrid(currentSchedule, currentConfig); + + // Update status + String message = result.isForcedOverride() ? "⚠️ Exam moved (with override)" : "✓ Exam moved successfully"; + statusLabel.setText(message); + statusLabel.setStyle(result.isForcedOverride() ? "-fx-text-fill: #f39c12;" : "-fx-text-fill: #27ae60;"); + + // Log the change + System.out.println("Exam edited: " + courseCode + " -> Day " + (newDay + 1) + + ", Slot " + (newSlot + 1) + ", Room " + newClassroom + + (result.isForcedOverride() ? " (forced)" : "")); + } else { + showAlert(Alert.AlertType.ERROR, "Edit Failed", + "Could not update the exam assignment. The exam may be locked."); + } + } } diff --git a/src/main/java/org/example/se302/service/ConstraintValidationService.java b/src/main/java/org/example/se302/service/ConstraintValidationService.java new file mode 100644 index 0000000..d5484ad --- /dev/null +++ b/src/main/java/org/example/se302/service/ConstraintValidationService.java @@ -0,0 +1,274 @@ +package org.example.se302.service; + +import org.example.se302.model.*; + +import java.util.*; + +/** + * Service for validating exam assignment changes against constraints. + * Provides detailed violation information including affected student names. + */ +public class ConstraintValidationService { + + private final DataManager dataManager; + + public ConstraintValidationService() { + this.dataManager = DataManager.getInstance(); + } + + /** + * Validates a proposed exam assignment change. + * + * @param courseCode The course being moved + * @param newDay Proposed day (0-based) + * @param newSlot Proposed time slot (0-based) + * @param newClassroom Proposed classroom ID + * @param currentState Current schedule state (to check against other exams) + * @return ValidationResult with all violations found + */ + public ValidationResult validateAssignment(String courseCode, int newDay, int newSlot, + String newClassroom, ScheduleState currentState) { + ValidationResult result = new ValidationResult(); + + // Get course info + Course course = dataManager.getCourse(courseCode); + if (course == null) { + result.addViolation(new ConstraintViolation( + "Unknown Course", + true, + "Course " + courseCode + " not found", + Collections.emptyList(), + null)); + return result; + } + + // Check classroom capacity + Classroom classroom = dataManager.getClassroom(newClassroom); + if (classroom == null) { + result.addViolation(new ConstraintViolation( + "Unknown Classroom", + true, + "Classroom " + newClassroom + " not found", + Collections.emptyList(), + null)); + } else if (classroom.getCapacity() < course.getEnrolledStudentsCount()) { + result.addViolation(new ConstraintViolation( + "Capacity Exceeded", + true, + String.format("Classroom %s has capacity %d, but course has %d students", + newClassroom, classroom.getCapacity(), course.getEnrolledStudentsCount()), + Collections.emptyList(), + null)); + } + + // Check classroom conflict (another exam in same classroom at same time) + String classroomConflictCourse = getClassroomConflict(courseCode, newClassroom, newDay, newSlot, currentState); + if (classroomConflictCourse != null) { + result.addViolation(new ConstraintViolation( + "Classroom Conflict", + true, + String.format("Classroom %s is already used by %s at this time", + newClassroom, classroomConflictCourse), + Collections.emptyList(), + classroomConflictCourse)); + } + + // Check student conflicts (students with exams at the same time) + List studentConflicts = getStudentConflicts(courseCode, newDay, newSlot, currentState); + if (!studentConflicts.isEmpty()) { + // Group by conflicting course + Map> conflictsByCourse = new LinkedHashMap<>(); + for (StudentConflictInfo conflict : studentConflicts) { + conflictsByCourse + .computeIfAbsent(conflict.conflictingCourse, k -> new ArrayList<>()) + .add(conflict.studentId); + } + + for (Map.Entry> entry : conflictsByCourse.entrySet()) { + String conflictCourse = entry.getKey(); + List students = entry.getValue(); + + String studentList = formatStudentList(students); + + result.addViolation(new ConstraintViolation( + "Student Conflict", + true, + String.format("%d student(s) have exams for both %s and %s at this time: %s", + students.size(), courseCode, conflictCourse, studentList), + new ArrayList<>(students), + conflictCourse)); + } + } + + return result; + } + + /** + * Gets the course that conflicts with the given classroom at the specified + * time. + */ + private String getClassroomConflict(String excludeCourse, String classroomId, + int day, int slot, ScheduleState state) { + for (ExamAssignment assignment : state.getAssignments().values()) { + if (assignment.getCourseCode().equals(excludeCourse)) { + continue; // Skip the course being moved + } + if (assignment.isAssigned() && + assignment.getDay() == day && + assignment.getTimeSlotIndex() == slot && + classroomId.equals(assignment.getClassroomId())) { + return assignment.getCourseCode(); + } + } + return null; + } + + /** + * Gets all student conflicts for a proposed assignment. + */ + private List getStudentConflicts(String courseCode, int day, int slot, + ScheduleState state) { + List conflicts = new ArrayList<>(); + + Course course = dataManager.getCourse(courseCode); + if (course == null) { + return conflicts; + } + + Set enrolledStudents = new HashSet<>(course.getEnrolledStudents()); + + // Find all other courses at the same time + for (ExamAssignment assignment : state.getAssignments().values()) { + if (assignment.getCourseCode().equals(courseCode)) { + continue; // Skip self + } + if (!assignment.isAssigned() || + assignment.getDay() != day || + assignment.getTimeSlotIndex() != slot) { + continue; // Not at the same time + } + + Course otherCourse = dataManager.getCourse(assignment.getCourseCode()); + if (otherCourse == null) { + continue; + } + + // Check for shared students + for (String studentId : otherCourse.getEnrolledStudents()) { + if (enrolledStudents.contains(studentId)) { + conflicts.add(new StudentConflictInfo(studentId, assignment.getCourseCode())); + } + } + } + + return conflicts; + } + + /** + * Formats a list of student IDs for display. + */ + private String formatStudentList(List students) { + if (students.size() <= 5) { + return String.join(", ", students); + } else { + List first5 = students.subList(0, 5); + return String.join(", ", first5) + " and " + (students.size() - 5) + " more"; + } + } + + // ============= Inner Classes ============= + + /** + * Holds information about a student conflict. + */ + private static class StudentConflictInfo { + final String studentId; + final String conflictingCourse; + + StudentConflictInfo(String studentId, String conflictingCourse) { + this.studentId = studentId; + this.conflictingCourse = conflictingCourse; + } + } + + /** + * Represents the result of a constraint validation. + */ + public static class ValidationResult { + private final List violations = new ArrayList<>(); + + public void addViolation(ConstraintViolation violation) { + violations.add(violation); + } + + public List getViolations() { + return Collections.unmodifiableList(violations); + } + + public boolean isValid() { + return violations.isEmpty(); + } + + public boolean hasHardViolations() { + return violations.stream().anyMatch(ConstraintViolation::isHard); + } + + public boolean hasSoftViolations() { + return violations.stream().anyMatch(v -> !v.isHard()); + } + + public String getFormattedMessage() { + if (violations.isEmpty()) { + return "✓ No constraint violations"; + } + + StringBuilder sb = new StringBuilder(); + for (ConstraintViolation v : violations) { + sb.append(v.isHard() ? "❌ " : "⚠️ "); + sb.append(v.getConstraintName()).append(": "); + sb.append(v.getMessage()).append("\n"); + } + return sb.toString().trim(); + } + } + + /** + * Represents a single constraint violation. + */ + public static class ConstraintViolation { + private final String constraintName; + private final boolean isHard; + private final String message; + private final List affectedStudents; + private final String conflictingCourse; + + public ConstraintViolation(String constraintName, boolean isHard, String message, + List affectedStudents, String conflictingCourse) { + this.constraintName = constraintName; + this.isHard = isHard; + this.message = message; + this.affectedStudents = affectedStudents != null ? affectedStudents : Collections.emptyList(); + this.conflictingCourse = conflictingCourse; + } + + public String getConstraintName() { + return constraintName; + } + + public boolean isHard() { + return isHard; + } + + public String getMessage() { + return message; + } + + public List getAffectedStudents() { + return affectedStudents; + } + + public String getConflictingCourse() { + return conflictingCourse; + } + } +}