Implement exam schedule generation UI, data management, and associated controllers.

This commit is contained in:
feyzagereme
2025-12-14 18:27:04 +03:00
parent dbdb46e5de
commit a8475ec6ed
4 changed files with 293 additions and 44 deletions

View File

@@ -5,6 +5,7 @@ import javafx.collections.FXCollections;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import org.example.se302.model.*; import org.example.se302.model.*;
@@ -51,9 +52,13 @@ public class ScheduleCalendarController {
@FXML @FXML
private VBox progressContainer; private VBox progressContainer;
@FXML @FXML
private ProgressIndicator progressIndicator;
@FXML
private ProgressBar progressBar; private ProgressBar progressBar;
@FXML @FXML
private Label progressLabel; private Label progressLabel;
@FXML
private Label progressDetailLabel;
// Schedule display // Schedule display
@FXML @FXML
@@ -248,7 +253,11 @@ public class ScheduleCalendarController {
progressContainer.setVisible(true); progressContainer.setVisible(true);
progressContainer.setManaged(true); progressContainer.setManaged(true);
progressBar.setProgress(0); progressBar.setProgress(0);
progressLabel.setText("Initializing schedule generation..."); if (progressIndicator != null)
progressIndicator.setProgress(-1.0); // Indeterminate
progressLabel.setText("Generating Schedule...");
if (progressDetailLabel != null)
progressDetailLabel.setText("Initializing CSP solver...");
statusLabel.setText("⏳ Generating schedule..."); statusLabel.setText("⏳ Generating schedule...");
statusLabel.setStyle("-fx-text-fill: #3498db;"); statusLabel.setStyle("-fx-text-fill: #3498db;");
@@ -259,7 +268,10 @@ public class ScheduleCalendarController {
generatorService.setProgressListener((progress, message) -> { generatorService.setProgressListener((progress, message) -> {
Platform.runLater(() -> { Platform.runLater(() -> {
progressBar.setProgress(progress); progressBar.setProgress(progress);
progressLabel.setText(message); if (progressIndicator != null)
progressIndicator.setProgress(progress);
if (progressDetailLabel != null)
progressDetailLabel.setText(message);
}); });
}); });
@@ -289,6 +301,9 @@ public class ScheduleCalendarController {
if (result.isSuccess()) { if (result.isSuccess()) {
currentSchedule = result.getScheduleState(); currentSchedule = result.getScheduleState();
// Save schedule to DataManager (so other views can see it)
saveScheduleToDataManager(currentSchedule, config);
// Update status // Update status
statusLabel.setText("✓ Schedule generated successfully!"); statusLabel.setText("✓ Schedule generated successfully!");
statusLabel.setStyle("-fx-text-fill: #27ae60;"); statusLabel.setStyle("-fx-text-fill: #27ae60;");
@@ -303,7 +318,8 @@ public class ScheduleCalendarController {
"Exam schedule generated successfully!\n\n" + "Exam schedule generated successfully!\n\n" +
"Scheduled: " + currentSchedule.getAssignedCourses() + "/" "Scheduled: " + currentSchedule.getAssignedCourses() + "/"
+ currentSchedule.getTotalCourses() + " courses\n" + + currentSchedule.getTotalCourses() + " courses\n" +
"Time: " + durationMs + "ms"); "Time: " + durationMs + "ms\n\n" +
"The schedule has been saved. You can now view individual schedules in 'Student Schedule' and 'Course Schedule' tabs.");
} else if (result.wasCancelled()) { } else if (result.wasCancelled()) {
statusLabel.setText("⚠️ Generation cancelled"); statusLabel.setText("⚠️ Generation cancelled");
statusLabel.setStyle("-fx-text-fill: #f39c12;"); statusLabel.setStyle("-fx-text-fill: #f39c12;");
@@ -313,7 +329,30 @@ public class ScheduleCalendarController {
showAlert(Alert.AlertType.ERROR, "Generation Failed", showAlert(Alert.AlertType.ERROR, "Generation Failed",
result.getMessage() result.getMessage()
+ "\n\nTry:\n• Increasing number of days\n• Increasing slots per day\n• Adding more classrooms"); + "\n\nTry:\n• Increasing number of days\n• Increasing slots per day\n• Adding more classrooms\n• Switching to 'Allow back-to-back exams'");
}
}
private void saveScheduleToDataManager(ScheduleState schedule, ScheduleConfiguration config) {
// 1. Save configuration
dataManager.setActiveConfiguration(config);
// 2. Clear old schedule data from courses
for (Course course : dataManager.getCourses()) {
course.setExamSchedule(-1, -1, null);
}
// 3. Apply new schedule
for (ExamAssignment assignment : schedule.getAssignments().values()) {
if (assignment.isAssigned()) {
Course course = dataManager.getCourse(assignment.getCourseCode());
if (course != null) {
course.setExamSchedule(
assignment.getDay(),
assignment.getTimeSlotIndex(),
assignment.getClassroomId());
}
}
} }
} }
@@ -433,6 +472,10 @@ public class ScheduleCalendarController {
HBox examBox = new HBox(5); HBox examBox = new HBox(5);
examBox.setAlignment(Pos.CENTER_LEFT); examBox.setAlignment(Pos.CENTER_LEFT);
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);
// 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;");
@@ -441,6 +484,11 @@ public class ScheduleCalendarController {
roomLabel.setStyle("-fx-text-fill: #d4e6f1; -fx-font-size: 10;"); roomLabel.setStyle("-fx-text-fill: #d4e6f1; -fx-font-size: 10;");
examBox.getChildren().addAll(courseLabel, roomLabel); examBox.getChildren().addAll(courseLabel, roomLabel);
// Add tooltip
Tooltip tooltip = new Tooltip(exam.getCourseCode() + "\n" + exam.getClassroomId());
Tooltip.install(examBox, tooltip);
cell.getChildren().add(examBox); cell.getChildren().add(examBox);
} }
@@ -455,6 +503,35 @@ 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) {

View File

@@ -2,27 +2,39 @@ package org.example.se302.controller;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ComboBox; import javafx.scene.control.*;
import javafx.scene.control.Label; import javafx.util.Callback;
import javafx.scene.control.TableColumn; import org.example.se302.model.Course;
import javafx.scene.control.TableView; import org.example.se302.model.ScheduleConfiguration;
import org.example.se302.model.Student; import org.example.se302.model.Student;
import org.example.se302.model.TimeSlot;
import org.example.se302.service.DataManager; import org.example.se302.service.DataManager;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/** /**
* Controller for the Student Schedule view. * Controller for the Student Schedule view.
* Displays exam schedule for a selected student.
*/ */
public class ScheduleStudentController { public class ScheduleStudentController {
@FXML private ComboBox<Student> studentComboBox; @FXML
@FXML private Label selectedStudentLabel; private ComboBox<Student> studentComboBox;
@FXML private TableView<CourseScheduleEntry> scheduleTable; @FXML
@FXML private TableColumn<CourseScheduleEntry, String> courseColumn; private Label selectedStudentLabel;
@FXML private TableColumn<CourseScheduleEntry, String> dateColumn; @FXML
@FXML private TableColumn<CourseScheduleEntry, String> timeColumn; private TableView<CourseScheduleEntry> scheduleTable;
@FXML private TableColumn<CourseScheduleEntry, String> classroomColumn; @FXML
private TableColumn<CourseScheduleEntry, String> courseColumn;
@FXML
private TableColumn<CourseScheduleEntry, String> dateColumn;
@FXML
private TableColumn<CourseScheduleEntry, String> timeColumn;
@FXML
private TableColumn<CourseScheduleEntry, String> classroomColumn;
private DataManager dataManager; private DataManager dataManager;
@@ -33,50 +45,190 @@ public class ScheduleStudentController {
// Populate student combo box // Populate student combo box
studentComboBox.setItems(dataManager.getStudents()); studentComboBox.setItems(dataManager.getStudents());
// Enable filtering/search in combobox ideally, but for now simple selection
studentComboBox.setPromptText("Select a student...");
// Set up table columns // Set up table columns
courseColumn.setCellValueFactory(cellData -> courseColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getCourseCode()));
new SimpleStringProperty(cellData.getValue().getCourseCode())); dateColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getDateDisplay()));
dateColumn.setCellValueFactory(cellData -> timeColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getTimeDisplay()));
new SimpleStringProperty(cellData.getValue().getDate())); classroomColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getClassroom()));
timeColumn.setCellValueFactory(cellData ->
new SimpleStringProperty(cellData.getValue().getTime())); // Custom row factory for highlighting
classroomColumn.setCellValueFactory(cellData -> scheduleTable.setRowFactory(tv -> new TableRow<CourseScheduleEntry>() {
new SimpleStringProperty(cellData.getValue().getClassroom())); @Override
protected void updateItem(CourseScheduleEntry item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setStyle("");
setTooltip(null);
} else {
// Logic to highlight rows
boolean styleSet = false;
if (item.hasConflictWarning) {
setStyle("-fx-background-color: #ffcdd2;"); // Red tint (conflict)
setTooltip(new Tooltip("Conflict: Multiple exams at same time!"));
styleSet = true;
} else if (item.isMultipleExamsOnDay) {
setStyle("-fx-background-color: #fff3cd;"); // Yellow tint
setTooltip(new Tooltip("Warning: Multiple exams on this day"));
styleSet = true;
} else if (item.isConsecutiveDay) {
setStyle("-fx-background-color: #e3f2fd;"); // Blue tint
setTooltip(new Tooltip("Info: Exam on consecutive day"));
styleSet = true;
}
if (!styleSet) {
setStyle("");
setTooltip(null);
}
}
}
});
} }
@FXML @FXML
private void onShowSchedule() { private void onShowSchedule() {
Student selected = studentComboBox.getValue(); Student selected = studentComboBox.getValue();
if (selected == null) return; if (selected == null)
return;
selectedStudentLabel.setText("Exam Schedule for: " + selected.getStudentId()); selectedStudentLabel.setText("Exam Schedule for: " + selected.getStudentId() + " (" + selected.getName() + ")");
// For demo: show enrolled courses with "Not Scheduled" status refreshTable(selected);
ObservableList<CourseScheduleEntry> entries = FXCollections.observableArrayList(); }
for (String courseCode : selected.getEnrolledCourses()) {
entries.add(new CourseScheduleEntry(courseCode, "Not Scheduled", "-", "-")); private void refreshTable(Student student) {
List<CourseScheduleEntry> entries = new ArrayList<>();
ScheduleConfiguration config = dataManager.getActiveConfiguration();
for (String courseCode : student.getEnrolledCourses()) {
Course course = dataManager.getCourse(courseCode);
if (course != null) {
String dateStr = "Not Scheduled";
String timeStr = "-";
String classroom = "-";
int dayIndex = -1;
int slotIndex = -1;
if (course.isScheduled()) {
dayIndex = course.getExamDay();
slotIndex = course.getExamTimeSlot();
classroom = course.getAssignedClassroom();
if (config != null) {
TimeSlot slot = config.getTimeSlot(dayIndex, slotIndex);
if (slot != null) {
dateStr = slot.getDate().toString(); // YYYY-MM-DD
timeStr = slot.getStartTime().toString() + " - " + slot.getEndTime().toString();
} else {
dateStr = "Day " + (dayIndex + 1);
timeStr = "Slot " + (slotIndex + 1);
}
} else {
// Fallback if no config saved
dateStr = "Day " + (dayIndex + 1);
timeStr = "Slot " + (slotIndex + 1);
}
}
entries.add(new CourseScheduleEntry(courseCode, dateStr, timeStr, classroom, dayIndex, slotIndex));
}
} }
scheduleTable.setItems(entries); // Sort by day and time
entries.sort(Comparator.comparingInt(CourseScheduleEntry::getDayIndex)
.thenComparingInt(CourseScheduleEntry::getSlotIndex));
// Analyze for highlights
analyzeSchedule(entries);
scheduleTable.setItems(FXCollections.observableArrayList(entries));
}
private void analyzeSchedule(List<CourseScheduleEntry> entries) {
if (entries.isEmpty())
return;
for (int i = 0; i < entries.size(); i++) {
CourseScheduleEntry current = entries.get(i);
if (current.getDayIndex() == -1)
continue; // Skip unscheduled
// Check for multiple exams on same day
int examsOnDay = 0;
for (CourseScheduleEntry other : entries) {
if (other.getDayIndex() == current.getDayIndex() && other.getDayIndex() != -1) {
examsOnDay++;
if (other.getSlotIndex() == current.getSlotIndex() && other != current) {
current.hasConflictWarning = true;
}
}
}
if (examsOnDay > 1) {
current.isMultipleExamsOnDay = true;
}
// Check for consecutive days (look at previous scheduled exam)
// Since list is sorted, we can look at previous entry if it exists
if (i > 0) {
CourseScheduleEntry prev = entries.get(i - 1);
if (prev.getDayIndex() != -1 &&
current.getDayIndex() == prev.getDayIndex() + 1) {
current.isConsecutiveDay = true;
}
}
}
} }
// Helper class for table entries // Helper class for table entries
public static class CourseScheduleEntry { public static class CourseScheduleEntry {
private final String courseCode; private final String courseCode;
private final String date; private final String dateDisplay;
private final String time; private final String timeDisplay;
private final String classroom; private final String classroom;
private final int dayIndex;
private final int slotIndex;
public CourseScheduleEntry(String courseCode, String date, String time, String classroom) { // View flags
public boolean isMultipleExamsOnDay = false;
public boolean isConsecutiveDay = false;
public boolean hasConflictWarning = false;
public CourseScheduleEntry(String courseCode, String dateDisplay, String timeDisplay, String classroom,
int dayIndex, int slotIndex) {
this.courseCode = courseCode; this.courseCode = courseCode;
this.date = date; this.dateDisplay = dateDisplay;
this.time = time; this.timeDisplay = timeDisplay;
this.classroom = classroom; this.classroom = classroom;
this.dayIndex = dayIndex;
this.slotIndex = slotIndex;
} }
public String getCourseCode() { return courseCode; } public String getCourseCode() {
public String getDate() { return date; } return courseCode;
public String getTime() { return time; } }
public String getClassroom() { return classroom; }
public String getDateDisplay() {
return dateDisplay;
}
public String getTimeDisplay() {
return timeDisplay;
}
public String getClassroom() {
return classroom;
}
public int getDayIndex() {
return dayIndex;
}
public int getSlotIndex() {
return slotIndex;
}
} }
} }

View File

@@ -131,4 +131,15 @@ public class DataManager {
course.addStudent(studentId); course.addStudent(studentId);
} }
} }
// Schedule Configuration
private org.example.se302.model.ScheduleConfiguration activeConfiguration;
public org.example.se302.model.ScheduleConfiguration getActiveConfiguration() {
return activeConfiguration;
}
public void setActiveConfiguration(org.example.se302.model.ScheduleConfiguration activeConfiguration) {
this.activeConfiguration = activeConfiguration;
}
} }

View File

@@ -94,10 +94,19 @@
<Label fx:id="statusLabel" text="Ready" styleClass="status-label"/> <Label fx:id="statusLabel" text="Ready" styleClass="status-label"/>
</HBox> </HBox>
<!-- Progress Bar --> <!-- Progress Container with Indicator -->
<VBox fx:id="progressContainer" spacing="5" visible="false" managed="false"> <VBox fx:id="progressContainer" spacing="8" visible="false" managed="false"
<ProgressBar fx:id="progressBar" prefWidth="400" progress="0"/> style="-fx-background-color: #f8f9fa; -fx-padding: 15; -fx-background-radius: 5;">
<Label fx:id="progressLabel" text="Initializing..."/> <HBox spacing="15" alignment="CENTER_LEFT">
<ProgressIndicator fx:id="progressIndicator" prefWidth="30" prefHeight="30"/>
<VBox spacing="3">
<Label fx:id="progressLabel" text="Initializing..."
style="-fx-font-weight: bold; -fx-font-size: 13;"/>
<ProgressBar fx:id="progressBar" prefWidth="350" progress="0"/>
</VBox>
</HBox>
<Label fx:id="progressDetailLabel" text="Please wait..."
style="-fx-text-fill: #7f8c8d; -fx-font-size: 11;"/>
</VBox> </VBox>
</VBox> </VBox>
</top> </top>