From a8475ec6ede92d32e8f01d789f60621b917efbc6 Mon Sep 17 00:00:00 2001 From: feyzagereme Date: Sun, 14 Dec 2025 18:27:04 +0300 Subject: [PATCH] Implement exam schedule generation UI, data management, and associated controllers. --- .../ScheduleCalendarController.java | 85 ++++++- .../controller/ScheduleStudentController.java | 224 +++++++++++++++--- .../example/se302/service/DataManager.java | 11 + .../se302/view/schedule-calendar-view.fxml | 17 +- 4 files changed, 293 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/example/se302/controller/ScheduleCalendarController.java b/src/main/java/org/example/se302/controller/ScheduleCalendarController.java index 9899fc1..47925c0 100644 --- a/src/main/java/org/example/se302/controller/ScheduleCalendarController.java +++ b/src/main/java/org/example/se302/controller/ScheduleCalendarController.java @@ -5,6 +5,7 @@ import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.control.*; import javafx.scene.layout.*; import org.example.se302.model.*; @@ -51,9 +52,13 @@ public class ScheduleCalendarController { @FXML private VBox progressContainer; @FXML + private ProgressIndicator progressIndicator; + @FXML private ProgressBar progressBar; @FXML private Label progressLabel; + @FXML + private Label progressDetailLabel; // Schedule display @FXML @@ -248,7 +253,11 @@ public class ScheduleCalendarController { progressContainer.setVisible(true); progressContainer.setManaged(true); 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.setStyle("-fx-text-fill: #3498db;"); @@ -259,7 +268,10 @@ public class ScheduleCalendarController { generatorService.setProgressListener((progress, message) -> { Platform.runLater(() -> { 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()) { currentSchedule = result.getScheduleState(); + // Save schedule to DataManager (so other views can see it) + saveScheduleToDataManager(currentSchedule, config); + // Update status statusLabel.setText("✓ Schedule generated successfully!"); statusLabel.setStyle("-fx-text-fill: #27ae60;"); @@ -303,7 +318,8 @@ public class ScheduleCalendarController { "Exam schedule generated successfully!\n\n" + "Scheduled: " + currentSchedule.getAssignedCourses() + "/" + 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()) { statusLabel.setText("⚠️ Generation cancelled"); statusLabel.setStyle("-fx-text-fill: #f39c12;"); @@ -313,7 +329,30 @@ public class ScheduleCalendarController { showAlert(Alert.AlertType.ERROR, "Generation Failed", 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); examBox.setAlignment(Pos.CENTER_LEFT); 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;"); @@ -441,6 +484,11 @@ public class ScheduleCalendarController { roomLabel.setStyle("-fx-text-fill: #d4e6f1; -fx-font-size: 10;"); examBox.getChildren().addAll(courseLabel, roomLabel); + + // Add tooltip + Tooltip tooltip = new Tooltip(exam.getCourseCode() + "\n" + exam.getClassroomId()); + Tooltip.install(examBox, tooltip); + cell.getChildren().add(examBox); } @@ -455,6 +503,35 @@ 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) { diff --git a/src/main/java/org/example/se302/controller/ScheduleStudentController.java b/src/main/java/org/example/se302/controller/ScheduleStudentController.java index afecefa..72175e1 100644 --- a/src/main/java/org/example/se302/controller/ScheduleStudentController.java +++ b/src/main/java/org/example/se302/controller/ScheduleStudentController.java @@ -2,27 +2,39 @@ package org.example.se302.controller; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.fxml.FXML; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; +import javafx.scene.control.*; +import javafx.util.Callback; +import org.example.se302.model.Course; +import org.example.se302.model.ScheduleConfiguration; import org.example.se302.model.Student; +import org.example.se302.model.TimeSlot; import org.example.se302.service.DataManager; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + /** * Controller for the Student Schedule view. + * Displays exam schedule for a selected student. */ public class ScheduleStudentController { - @FXML private ComboBox studentComboBox; - @FXML private Label selectedStudentLabel; - @FXML private TableView scheduleTable; - @FXML private TableColumn courseColumn; - @FXML private TableColumn dateColumn; - @FXML private TableColumn timeColumn; - @FXML private TableColumn classroomColumn; + @FXML + private ComboBox studentComboBox; + @FXML + private Label selectedStudentLabel; + @FXML + private TableView scheduleTable; + @FXML + private TableColumn courseColumn; + @FXML + private TableColumn dateColumn; + @FXML + private TableColumn timeColumn; + @FXML + private TableColumn classroomColumn; private DataManager dataManager; @@ -33,50 +45,190 @@ public class ScheduleStudentController { // Populate student combo box studentComboBox.setItems(dataManager.getStudents()); + // Enable filtering/search in combobox ideally, but for now simple selection + studentComboBox.setPromptText("Select a student..."); + // Set up table columns - courseColumn.setCellValueFactory(cellData -> - new SimpleStringProperty(cellData.getValue().getCourseCode())); - dateColumn.setCellValueFactory(cellData -> - new SimpleStringProperty(cellData.getValue().getDate())); - timeColumn.setCellValueFactory(cellData -> - new SimpleStringProperty(cellData.getValue().getTime())); - classroomColumn.setCellValueFactory(cellData -> - new SimpleStringProperty(cellData.getValue().getClassroom())); + courseColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getCourseCode())); + dateColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getDateDisplay())); + timeColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getTimeDisplay())); + classroomColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getClassroom())); + + // Custom row factory for highlighting + scheduleTable.setRowFactory(tv -> new TableRow() { + @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 private void onShowSchedule() { 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 - ObservableList entries = FXCollections.observableArrayList(); - for (String courseCode : selected.getEnrolledCourses()) { - entries.add(new CourseScheduleEntry(courseCode, "Not Scheduled", "-", "-")); + refreshTable(selected); + } + + private void refreshTable(Student student) { + List 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 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 public static class CourseScheduleEntry { private final String courseCode; - private final String date; - private final String time; + private final String dateDisplay; + private final String timeDisplay; 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.date = date; - this.time = time; + this.dateDisplay = dateDisplay; + this.timeDisplay = timeDisplay; this.classroom = classroom; + this.dayIndex = dayIndex; + this.slotIndex = slotIndex; } - public String getCourseCode() { return courseCode; } - public String getDate() { return date; } - public String getTime() { return time; } - public String getClassroom() { return classroom; } + public String getCourseCode() { + return courseCode; + } + + 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; + } } } diff --git a/src/main/java/org/example/se302/service/DataManager.java b/src/main/java/org/example/se302/service/DataManager.java index 5d8d3d5..331e600 100644 --- a/src/main/java/org/example/se302/service/DataManager.java +++ b/src/main/java/org/example/se302/service/DataManager.java @@ -131,4 +131,15 @@ public class DataManager { 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; + } } diff --git a/src/main/resources/org/example/se302/view/schedule-calendar-view.fxml b/src/main/resources/org/example/se302/view/schedule-calendar-view.fxml index 1df0dab..0e16ced 100644 --- a/src/main/resources/org/example/se302/view/schedule-calendar-view.fxml +++ b/src/main/resources/org/example/se302/view/schedule-calendar-view.fxml @@ -94,10 +94,19 @@