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 final DataManager dataManager = DataManager.getInstance();
|
||||||
private ScheduleGeneratorService generatorService;
|
private ScheduleGeneratorService generatorService;
|
||||||
private ScheduleState currentSchedule;
|
private ScheduleState currentSchedule;
|
||||||
|
private ScheduleConfiguration currentConfig;
|
||||||
private Thread generationThread;
|
private Thread generationThread;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -300,6 +301,7 @@ public class ScheduleCalendarController {
|
|||||||
|
|
||||||
if (result.isSuccess()) {
|
if (result.isSuccess()) {
|
||||||
currentSchedule = result.getScheduleState();
|
currentSchedule = result.getScheduleState();
|
||||||
|
currentConfig = config;
|
||||||
|
|
||||||
// Save schedule to DataManager (so other views can see it)
|
// Save schedule to DataManager (so other views can see it)
|
||||||
saveScheduleToDataManager(currentSchedule, config);
|
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.setStyle("-fx-background-color: #3498db; -fx-background-radius: 3; -fx-padding: 3 6;");
|
||||||
examBox.setCursor(Cursor.HAND);
|
examBox.setCursor(Cursor.HAND);
|
||||||
|
|
||||||
// Add click handler
|
|
||||||
examBox.setOnMouseClicked(e -> showAssignmentDetails(exam));
|
|
||||||
|
|
||||||
Label courseLabel = new Label(exam.getCourseCode());
|
Label courseLabel = new Label(exam.getCourseCode());
|
||||||
courseLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 11;");
|
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);
|
examBox.getChildren().addAll(courseLabel, roomLabel);
|
||||||
|
|
||||||
// Add tooltip
|
// Hover highlight
|
||||||
Tooltip tooltip = new Tooltip(exam.getCourseCode() + "\n" + exam.getClassroomId());
|
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);
|
Tooltip.install(examBox, tooltip);
|
||||||
|
|
||||||
cell.getChildren().add(examBox);
|
cell.getChildren().add(examBox);
|
||||||
@@ -503,35 +512,6 @@ public class ScheduleCalendarController {
|
|||||||
return cell;
|
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
|
@FXML
|
||||||
private void onCancelGeneration() {
|
private void onCancelGeneration() {
|
||||||
if (generatorService != null) {
|
if (generatorService != null) {
|
||||||
@@ -549,4 +529,60 @@ public class ScheduleCalendarController {
|
|||||||
alert.setContentText(message);
|
alert.setContentText(message);
|
||||||
alert.showAndWait();
|
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