mirror of
https://github.com/sabazadam/Se302.git
synced 2025-12-31 12:21:22 +00:00
Completed edit functionality for examinations in the calendar view.
This commit is contained in:
372
src/main/java/org/example/se302/controller/ExamEditDialog.java
Normal file
372
src/main/java/org/example/se302/controller/ExamEditDialog.java
Normal file
@@ -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<ExamEditDialog.EditResult> {
|
||||
|
||||
private final String courseCode;
|
||||
private final ScheduleState scheduleState;
|
||||
private final ScheduleConfiguration config;
|
||||
private final DataManager dataManager;
|
||||
private final ConstraintValidationService validationService;
|
||||
|
||||
// UI Components
|
||||
private ComboBox<Integer> dayComboBox;
|
||||
private ComboBox<Integer> slotComboBox;
|
||||
private ComboBox<Classroom> 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<ButtonType> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StudentConflictInfo> studentConflicts = getStudentConflicts(courseCode, newDay, newSlot, currentState);
|
||||
if (!studentConflicts.isEmpty()) {
|
||||
// Group by conflicting course
|
||||
Map<String, List<String>> conflictsByCourse = new LinkedHashMap<>();
|
||||
for (StudentConflictInfo conflict : studentConflicts) {
|
||||
conflictsByCourse
|
||||
.computeIfAbsent(conflict.conflictingCourse, k -> new ArrayList<>())
|
||||
.add(conflict.studentId);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : conflictsByCourse.entrySet()) {
|
||||
String conflictCourse = entry.getKey();
|
||||
List<String> 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<StudentConflictInfo> getStudentConflicts(String courseCode, int day, int slot,
|
||||
ScheduleState state) {
|
||||
List<StudentConflictInfo> conflicts = new ArrayList<>();
|
||||
|
||||
Course course = dataManager.getCourse(courseCode);
|
||||
if (course == null) {
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
Set<String> 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<String> students) {
|
||||
if (students.size() <= 5) {
|
||||
return String.join(", ", students);
|
||||
} else {
|
||||
List<String> 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<ConstraintViolation> violations = new ArrayList<>();
|
||||
|
||||
public void addViolation(ConstraintViolation violation) {
|
||||
violations.add(violation);
|
||||
}
|
||||
|
||||
public List<ConstraintViolation> 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<String> affectedStudents;
|
||||
private final String conflictingCourse;
|
||||
|
||||
public ConstraintViolation(String constraintName, boolean isHard, String message,
|
||||
List<String> 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<String> getAffectedStudents() {
|
||||
return affectedStudents;
|
||||
}
|
||||
|
||||
public String getConflictingCourse() {
|
||||
return conflictingCourse;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user