mirror of
https://github.com/sabazadam/Se302.git
synced 2025-12-31 12:21:22 +00:00
introduce exam scheduling feature with CSP algorithm and calendar UI
This commit is contained in:
@@ -1,33 +1,475 @@
|
|||||||
package org.example.se302.controller;
|
package org.example.se302.controller;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.DatePicker;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import org.example.se302.model.*;
|
||||||
|
import org.example.se302.service.DataManager;
|
||||||
|
import org.example.se302.service.ScheduleGeneratorService;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller for the Calendar/Day schedule view.
|
* Controller for the Calendar/Day schedule view.
|
||||||
|
* Handles schedule configuration and generation using CSP algorithm.
|
||||||
*/
|
*/
|
||||||
public class ScheduleCalendarController {
|
public class ScheduleCalendarController {
|
||||||
|
|
||||||
@FXML private DatePicker startDatePicker;
|
// Configuration controls
|
||||||
@FXML private DatePicker endDatePicker;
|
@FXML
|
||||||
|
private Spinner<Integer> numDaysSpinner;
|
||||||
|
@FXML
|
||||||
|
private Spinner<Integer> slotsPerDaySpinner;
|
||||||
|
@FXML
|
||||||
|
private DatePicker startDatePicker;
|
||||||
|
@FXML
|
||||||
|
private Spinner<Integer> slotDurationSpinner;
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> strategyComboBox;
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> startTimeComboBox;
|
||||||
|
@FXML
|
||||||
|
private CheckBox allowBackToBackCheckBox;
|
||||||
|
@FXML
|
||||||
|
private Label summaryLabel;
|
||||||
|
|
||||||
|
// Action controls
|
||||||
|
@FXML
|
||||||
|
private Button generateButton;
|
||||||
|
@FXML
|
||||||
|
private Button cancelButton;
|
||||||
|
@FXML
|
||||||
|
private Label statusLabel;
|
||||||
|
|
||||||
|
// Progress controls
|
||||||
|
@FXML
|
||||||
|
private VBox progressContainer;
|
||||||
|
@FXML
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
@FXML
|
||||||
|
private Label progressLabel;
|
||||||
|
|
||||||
|
// Schedule display
|
||||||
|
@FXML
|
||||||
|
private ScrollPane scheduleScrollPane;
|
||||||
|
@FXML
|
||||||
|
private GridPane scheduleGrid;
|
||||||
|
|
||||||
|
// Statistics labels
|
||||||
|
@FXML
|
||||||
|
private Label totalCoursesLabel;
|
||||||
|
@FXML
|
||||||
|
private Label scheduledCoursesLabel;
|
||||||
|
@FXML
|
||||||
|
private Label classroomsUsedLabel;
|
||||||
|
@FXML
|
||||||
|
private Label generationTimeLabel;
|
||||||
|
|
||||||
|
// Services
|
||||||
|
private final DataManager dataManager = DataManager.getInstance();
|
||||||
|
private ScheduleGeneratorService generatorService;
|
||||||
|
private ScheduleState currentSchedule;
|
||||||
|
private Thread generationThread;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
// Initialize with default date range if needed
|
// Initialize spinners
|
||||||
|
initializeSpinners();
|
||||||
|
|
||||||
|
// Initialize combo boxes
|
||||||
|
initializeComboBoxes();
|
||||||
|
|
||||||
|
// Set default date to next week
|
||||||
|
startDatePicker.setValue(LocalDate.now().plusDays(7));
|
||||||
|
|
||||||
|
// Set default checkbox - allow back-to-back for easier scheduling
|
||||||
|
allowBackToBackCheckBox.setSelected(true);
|
||||||
|
|
||||||
|
// Add listeners to update summary
|
||||||
|
numDaysSpinner.valueProperty().addListener((obs, oldVal, newVal) -> updateSummary());
|
||||||
|
slotsPerDaySpinner.valueProperty().addListener((obs, oldVal, newVal) -> updateSummary());
|
||||||
|
|
||||||
|
// Initial summary update
|
||||||
|
updateSummary();
|
||||||
|
|
||||||
|
// Update status with data info
|
||||||
|
updateDataStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeSpinners() {
|
||||||
|
// Number of days spinner (1-30)
|
||||||
|
SpinnerValueFactory<Integer> daysFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 30, 5);
|
||||||
|
numDaysSpinner.setValueFactory(daysFactory);
|
||||||
|
|
||||||
|
// Slots per day spinner (1-10)
|
||||||
|
SpinnerValueFactory<Integer> slotsFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 10, 4);
|
||||||
|
slotsPerDaySpinner.setValueFactory(slotsFactory);
|
||||||
|
|
||||||
|
// Slot duration spinner (30-240 minutes)
|
||||||
|
SpinnerValueFactory<Integer> durationFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(30, 240, 120,
|
||||||
|
30);
|
||||||
|
slotDurationSpinner.setValueFactory(durationFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeComboBoxes() {
|
||||||
|
// Optimization strategies
|
||||||
|
List<String> strategies = Arrays.asList(
|
||||||
|
"Default (Balanced)",
|
||||||
|
"Minimize Days",
|
||||||
|
"Balanced Distribution",
|
||||||
|
"Minimize Classrooms",
|
||||||
|
"Balance Classrooms",
|
||||||
|
"Student Friendly");
|
||||||
|
strategyComboBox.setItems(FXCollections.observableArrayList(strategies));
|
||||||
|
strategyComboBox.getSelectionModel().selectFirst();
|
||||||
|
|
||||||
|
// Start times (8:00 - 14:00)
|
||||||
|
List<String> times = Arrays.asList(
|
||||||
|
"08:00", "08:30", "09:00", "09:30", "10:00");
|
||||||
|
startTimeComboBox.setItems(FXCollections.observableArrayList(times));
|
||||||
|
startTimeComboBox.getSelectionModel().select("09:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSummary() {
|
||||||
|
int days = numDaysSpinner.getValue();
|
||||||
|
int slots = slotsPerDaySpinner.getValue();
|
||||||
|
int total = days * slots;
|
||||||
|
summaryLabel.setText(String.format("%d days × %d slots = %d total time slots", days, slots, total));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDataStatus() {
|
||||||
|
int courses = dataManager.getTotalCourses();
|
||||||
|
int classrooms = dataManager.getTotalClassrooms();
|
||||||
|
int students = dataManager.getTotalStudents();
|
||||||
|
|
||||||
|
if (courses == 0 || classrooms == 0) {
|
||||||
|
statusLabel
|
||||||
|
.setText("⚠️ Please import data first (Courses: " + courses + ", Classrooms: " + classrooms + ")");
|
||||||
|
statusLabel.setStyle("-fx-text-fill: #e74c3c;");
|
||||||
|
} else {
|
||||||
|
statusLabel.setText(
|
||||||
|
"✓ Ready - " + courses + " courses, " + classrooms + " classrooms, " + students + " students");
|
||||||
|
statusLabel.setStyle("-fx-text-fill: #27ae60;");
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCoursesLabel.setText(String.valueOf(courses));
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void onGenerateSchedule() {
|
private void onGenerateSchedule() {
|
||||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
// Validate data
|
||||||
alert.setTitle("Coming Soon");
|
if (dataManager.getTotalCourses() == 0) {
|
||||||
alert.setHeaderText("Schedule Generation");
|
showAlert(Alert.AlertType.WARNING, "No Data",
|
||||||
alert.setContentText("The CSP-based schedule generation algorithm will be implemented in Phase 2.\n\n" +
|
"Please import course data before generating a schedule.");
|
||||||
"It will automatically create an exam schedule that satisfies all constraints:\n" +
|
return;
|
||||||
"- No consecutive exams for students\n" +
|
}
|
||||||
"- Max 2 exams per day per student\n" +
|
|
||||||
"- Classroom capacity limits\n" +
|
if (dataManager.getTotalClassrooms() == 0) {
|
||||||
"- No double-booking");
|
showAlert(Alert.AlertType.WARNING, "No Classrooms",
|
||||||
|
"Please import classroom data before generating a schedule.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
if (startDatePicker.getValue() == null) {
|
||||||
|
showAlert(Alert.AlertType.WARNING, "Missing Date",
|
||||||
|
"Please select a start date for the exam period.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build configuration
|
||||||
|
ScheduleConfiguration config = buildConfiguration();
|
||||||
|
|
||||||
|
// Validate total slots
|
||||||
|
int totalSlots = config.getTotalSlots();
|
||||||
|
int totalCourses = dataManager.getTotalCourses();
|
||||||
|
int totalClassrooms = dataManager.getTotalClassrooms();
|
||||||
|
|
||||||
|
if (totalSlots * totalClassrooms < totalCourses) {
|
||||||
|
showAlert(Alert.AlertType.WARNING, "Insufficient Capacity",
|
||||||
|
String.format(
|
||||||
|
"Not enough time slots! You have %d courses but only %d total capacity (%d slots × %d classrooms).\n\nPlease increase days or slots per day.",
|
||||||
|
totalCourses, totalSlots * totalClassrooms, totalSlots, totalClassrooms));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start generation
|
||||||
|
startGeneration(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduleConfiguration buildConfiguration() {
|
||||||
|
ScheduleConfiguration config = new ScheduleConfiguration();
|
||||||
|
|
||||||
|
config.setNumDays(numDaysSpinner.getValue());
|
||||||
|
config.setSlotsPerDay(slotsPerDaySpinner.getValue());
|
||||||
|
config.setStartDate(startDatePicker.getValue());
|
||||||
|
config.setSlotDurationMinutes(slotDurationSpinner.getValue());
|
||||||
|
config.setAllowBackToBackExams(allowBackToBackCheckBox.isSelected());
|
||||||
|
|
||||||
|
// Parse start time
|
||||||
|
String timeStr = startTimeComboBox.getValue();
|
||||||
|
if (timeStr != null) {
|
||||||
|
String[] parts = timeStr.split(":");
|
||||||
|
config.setDayStartTime(LocalTime.of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set optimization strategy
|
||||||
|
String strategyStr = strategyComboBox.getValue();
|
||||||
|
ScheduleConfiguration.OptimizationStrategy strategy = ScheduleConfiguration.OptimizationStrategy.DEFAULT;
|
||||||
|
|
||||||
|
if (strategyStr != null) {
|
||||||
|
if (strategyStr.contains("Minimize Days")) {
|
||||||
|
strategy = ScheduleConfiguration.OptimizationStrategy.MINIMIZE_DAYS;
|
||||||
|
} else if (strategyStr.contains("Balanced Distribution")) {
|
||||||
|
strategy = ScheduleConfiguration.OptimizationStrategy.BALANCED_DISTRIBUTION;
|
||||||
|
} else if (strategyStr.contains("Minimize Classrooms")) {
|
||||||
|
strategy = ScheduleConfiguration.OptimizationStrategy.MINIMIZE_CLASSROOMS;
|
||||||
|
} else if (strategyStr.contains("Balance Classrooms")) {
|
||||||
|
strategy = ScheduleConfiguration.OptimizationStrategy.BALANCE_CLASSROOMS;
|
||||||
|
} else if (strategyStr.contains("Student Friendly")) {
|
||||||
|
strategy = ScheduleConfiguration.OptimizationStrategy.STUDENT_FRIENDLY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.setOptimizationStrategy(strategy);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startGeneration(ScheduleConfiguration config) {
|
||||||
|
// Update UI for generation
|
||||||
|
generateButton.setDisable(true);
|
||||||
|
cancelButton.setDisable(false);
|
||||||
|
progressContainer.setVisible(true);
|
||||||
|
progressContainer.setManaged(true);
|
||||||
|
progressBar.setProgress(0);
|
||||||
|
progressLabel.setText("Initializing schedule generation...");
|
||||||
|
statusLabel.setText("⏳ Generating schedule...");
|
||||||
|
statusLabel.setStyle("-fx-text-fill: #3498db;");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Create generator service
|
||||||
|
generatorService = new ScheduleGeneratorService();
|
||||||
|
generatorService.setProgressListener((progress, message) -> {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
progressBar.setProgress(progress);
|
||||||
|
progressLabel.setText(message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run generation in background thread
|
||||||
|
generationThread = new Thread(() -> {
|
||||||
|
ScheduleGeneratorService.ScheduleResult result = generatorService.generateSchedule(config);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
long duration = endTime - startTime;
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
onGenerationComplete(result, config, duration);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
generationThread.setDaemon(true);
|
||||||
|
generationThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onGenerationComplete(ScheduleGeneratorService.ScheduleResult result,
|
||||||
|
ScheduleConfiguration config, long durationMs) {
|
||||||
|
// Reset UI
|
||||||
|
generateButton.setDisable(false);
|
||||||
|
cancelButton.setDisable(true);
|
||||||
|
progressContainer.setVisible(false);
|
||||||
|
progressContainer.setManaged(false);
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
currentSchedule = result.getScheduleState();
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
statusLabel.setText("✓ Schedule generated successfully!");
|
||||||
|
statusLabel.setStyle("-fx-text-fill: #27ae60;");
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
updateStatistics(currentSchedule, durationMs);
|
||||||
|
|
||||||
|
// Display schedule in grid
|
||||||
|
displayScheduleGrid(currentSchedule, config);
|
||||||
|
|
||||||
|
showAlert(Alert.AlertType.INFORMATION, "Success",
|
||||||
|
"Exam schedule generated successfully!\n\n" +
|
||||||
|
"Scheduled: " + currentSchedule.getAssignedCourses() + "/"
|
||||||
|
+ currentSchedule.getTotalCourses() + " courses\n" +
|
||||||
|
"Time: " + durationMs + "ms");
|
||||||
|
} else if (result.wasCancelled()) {
|
||||||
|
statusLabel.setText("⚠️ Generation cancelled");
|
||||||
|
statusLabel.setStyle("-fx-text-fill: #f39c12;");
|
||||||
|
} else {
|
||||||
|
statusLabel.setText("❌ Generation failed");
|
||||||
|
statusLabel.setStyle("-fx-text-fill: #e74c3c;");
|
||||||
|
|
||||||
|
showAlert(Alert.AlertType.ERROR, "Generation Failed",
|
||||||
|
result.getMessage()
|
||||||
|
+ "\n\nTry:\n• Increasing number of days\n• Increasing slots per day\n• Adding more classrooms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatistics(ScheduleState schedule, long durationMs) {
|
||||||
|
scheduledCoursesLabel.setText(String.valueOf(schedule.getAssignedCourses()));
|
||||||
|
|
||||||
|
// Count unique classrooms used
|
||||||
|
Set<String> usedClassrooms = new HashSet<>();
|
||||||
|
for (ExamAssignment assignment : schedule.getAssignments().values()) {
|
||||||
|
if (assignment.isAssigned() && assignment.getClassroomId() != null) {
|
||||||
|
usedClassrooms.add(assignment.getClassroomId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
classroomsUsedLabel.setText(String.valueOf(usedClassrooms.size()));
|
||||||
|
|
||||||
|
// Format generation time
|
||||||
|
if (durationMs < 1000) {
|
||||||
|
generationTimeLabel.setText(durationMs + "ms");
|
||||||
|
} else {
|
||||||
|
generationTimeLabel.setText(String.format("%.2fs", durationMs / 1000.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayScheduleGrid(ScheduleState schedule, ScheduleConfiguration config) {
|
||||||
|
scheduleGrid.getChildren().clear();
|
||||||
|
scheduleGrid.getColumnConstraints().clear();
|
||||||
|
scheduleGrid.getRowConstraints().clear();
|
||||||
|
|
||||||
|
int numDays = config.getNumDays();
|
||||||
|
int slotsPerDay = config.getSlotsPerDay();
|
||||||
|
|
||||||
|
// Column constraints - make columns wider for better visibility
|
||||||
|
ColumnConstraints headerCol = new ColumnConstraints();
|
||||||
|
headerCol.setMinWidth(100);
|
||||||
|
headerCol.setPrefWidth(120);
|
||||||
|
scheduleGrid.getColumnConstraints().add(headerCol);
|
||||||
|
|
||||||
|
for (int day = 0; day < numDays; day++) {
|
||||||
|
ColumnConstraints dayCol = new ColumnConstraints();
|
||||||
|
dayCol.setMinWidth(180);
|
||||||
|
dayCol.setPrefWidth(220);
|
||||||
|
dayCol.setHgrow(Priority.ALWAYS);
|
||||||
|
scheduleGrid.getColumnConstraints().add(dayCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row constraints - make rows taller for better visibility
|
||||||
|
for (int slot = 0; slot <= slotsPerDay; slot++) {
|
||||||
|
RowConstraints rowConstraint = new RowConstraints();
|
||||||
|
rowConstraint.setMinHeight(80);
|
||||||
|
rowConstraint.setPrefHeight(100);
|
||||||
|
rowConstraint.setVgrow(Priority.SOMETIMES);
|
||||||
|
scheduleGrid.getRowConstraints().add(rowConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header row - Days
|
||||||
|
Label cornerLabel = createHeaderLabel("");
|
||||||
|
scheduleGrid.add(cornerLabel, 0, 0);
|
||||||
|
|
||||||
|
for (int day = 0; day < numDays; day++) {
|
||||||
|
Label dayLabel = createHeaderLabel("Day " + (day + 1) + "\n" +
|
||||||
|
config.getStartDate().plusDays(day).toString());
|
||||||
|
scheduleGrid.add(dayLabel, day + 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time slot rows
|
||||||
|
for (int slot = 0; slot < slotsPerDay; slot++) {
|
||||||
|
// Time slot label
|
||||||
|
TimeSlot timeSlot = config.getTimeSlot(0, slot);
|
||||||
|
String timeText = timeSlot != null ? timeSlot.getStartTime() + "\n-\n" + timeSlot.getEndTime()
|
||||||
|
: "Slot " + (slot + 1);
|
||||||
|
Label slotLabel = createHeaderLabel(timeText);
|
||||||
|
scheduleGrid.add(slotLabel, 0, slot + 1);
|
||||||
|
|
||||||
|
// Cells for each day
|
||||||
|
for (int day = 0; day < numDays; day++) {
|
||||||
|
VBox cellContent = createScheduleCell(schedule, day, slot);
|
||||||
|
scheduleGrid.add(cellContent, day + 1, slot + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Label createHeaderLabel(String text) {
|
||||||
|
Label label = new Label(text);
|
||||||
|
label.setStyle("-fx-font-weight: bold; -fx-background-color: #34495e; -fx-text-fill: white; " +
|
||||||
|
"-fx-padding: 10; -fx-alignment: center;");
|
||||||
|
label.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
label.setMaxHeight(Double.MAX_VALUE);
|
||||||
|
label.setAlignment(Pos.CENTER);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox createScheduleCell(ScheduleState schedule, int day, int slot) {
|
||||||
|
VBox cell = new VBox(3);
|
||||||
|
cell.setPadding(new Insets(5));
|
||||||
|
cell.setAlignment(Pos.TOP_LEFT);
|
||||||
|
cell.setStyle("-fx-background-color: #ecf0f1; -fx-border-color: #bdc3c7; -fx-border-width: 1;");
|
||||||
|
|
||||||
|
// Find all exams at this day/slot
|
||||||
|
List<ExamAssignment> examsAtSlot = new ArrayList<>();
|
||||||
|
for (ExamAssignment assignment : schedule.getAssignments().values()) {
|
||||||
|
if (assignment.isAssigned() &&
|
||||||
|
assignment.getDay() == day &&
|
||||||
|
assignment.getTimeSlotIndex() == slot) {
|
||||||
|
examsAtSlot.add(assignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (examsAtSlot.isEmpty()) {
|
||||||
|
Label emptyLabel = new Label("-");
|
||||||
|
emptyLabel.setStyle("-fx-text-fill: #95a5a6;");
|
||||||
|
cell.getChildren().add(emptyLabel);
|
||||||
|
} else {
|
||||||
|
// Sort by classroom
|
||||||
|
examsAtSlot.sort(Comparator.comparing(ExamAssignment::getClassroomId));
|
||||||
|
|
||||||
|
for (ExamAssignment exam : examsAtSlot) {
|
||||||
|
HBox examBox = new HBox(5);
|
||||||
|
examBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
examBox.setStyle("-fx-background-color: #3498db; -fx-background-radius: 3; -fx-padding: 3 6;");
|
||||||
|
|
||||||
|
Label courseLabel = new Label(exam.getCourseCode());
|
||||||
|
courseLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 11;");
|
||||||
|
|
||||||
|
Label roomLabel = new Label("(" + exam.getClassroomId() + ")");
|
||||||
|
roomLabel.setStyle("-fx-text-fill: #d4e6f1; -fx-font-size: 10;");
|
||||||
|
|
||||||
|
examBox.getChildren().addAll(courseLabel, roomLabel);
|
||||||
|
cell.getChildren().add(examBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change cell color based on load
|
||||||
|
if (examsAtSlot.size() >= 3) {
|
||||||
|
cell.setStyle("-fx-background-color: #fadbd8; -fx-border-color: #e74c3c; -fx-border-width: 1;");
|
||||||
|
} else if (examsAtSlot.size() >= 2) {
|
||||||
|
cell.setStyle("-fx-background-color: #fcf3cf; -fx-border-color: #f39c12; -fx-border-width: 1;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void onCancelGeneration() {
|
||||||
|
if (generatorService != null) {
|
||||||
|
generatorService.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelButton.setDisable(true);
|
||||||
|
progressLabel.setText("Cancelling...");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showAlert(Alert.AlertType type, String title, String message) {
|
||||||
|
Alert alert = new Alert(type);
|
||||||
|
alert.setTitle(title);
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText(message);
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,19 @@ public class ConstraintValidator {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a single assignment against the current schedule state.
|
* Validate a single assignment against the current schedule state.
|
||||||
|
* Uses default strict validation (no back-to-back allowed).
|
||||||
*/
|
*/
|
||||||
public ValidationResult validateAssignment(ExamAssignment assignment, ScheduleState scheduleState) {
|
public ValidationResult validateAssignment(ExamAssignment assignment, ScheduleState scheduleState) {
|
||||||
|
return validateAssignment(assignment, scheduleState, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a single assignment against the current schedule state.
|
||||||
|
*
|
||||||
|
* @param allowBackToBack If true, consecutive exams are allowed
|
||||||
|
*/
|
||||||
|
public ValidationResult validateAssignment(ExamAssignment assignment, ScheduleState scheduleState,
|
||||||
|
boolean allowBackToBack) {
|
||||||
ValidationResult result = new ValidationResult();
|
ValidationResult result = new ValidationResult();
|
||||||
|
|
||||||
if (!assignment.isAssigned()) {
|
if (!assignment.isAssigned()) {
|
||||||
@@ -48,15 +59,22 @@ public class ConstraintValidator {
|
|||||||
ValidationResult capacityResult = checkClassroomCapacity(assignment);
|
ValidationResult capacityResult = checkClassroomCapacity(assignment);
|
||||||
result.merge(capacityResult);
|
result.merge(capacityResult);
|
||||||
|
|
||||||
// Check no double-booking
|
// Check no double-booking (always required)
|
||||||
ValidationResult doubleBookingResult = checkNoDoubleBooking(assignment, scheduleState);
|
ValidationResult doubleBookingResult = checkNoDoubleBooking(assignment, scheduleState);
|
||||||
result.merge(doubleBookingResult);
|
result.merge(doubleBookingResult);
|
||||||
|
|
||||||
// Check student constraints
|
// Check student constraints - only check same time slot conflicts (always
|
||||||
|
// required)
|
||||||
Course course = dataManager.getCourse(assignment.getCourseCode());
|
Course course = dataManager.getCourse(assignment.getCourseCode());
|
||||||
if (course != null) {
|
if (course != null) {
|
||||||
|
// Check if student has another exam at the SAME time slot (hard constraint)
|
||||||
|
ValidationResult sameTimeResult = checkNoSameTimeExams(assignment, scheduleState, course);
|
||||||
|
result.merge(sameTimeResult);
|
||||||
|
|
||||||
|
// Only check consecutive and max per day if back-to-back is NOT allowed
|
||||||
|
if (!allowBackToBack) {
|
||||||
for (String studentId : course.getEnrolledStudents()) {
|
for (String studentId : course.getEnrolledStudents()) {
|
||||||
// Check no consecutive exams
|
// Check no consecutive exams (soft - skip if allowBackToBack)
|
||||||
ValidationResult consecutiveResult = checkNoConsecutiveExams(
|
ValidationResult consecutiveResult = checkNoConsecutiveExams(
|
||||||
studentId, assignment, scheduleState);
|
studentId, assignment, scheduleState);
|
||||||
result.merge(consecutiveResult);
|
result.merge(consecutiveResult);
|
||||||
@@ -65,6 +83,50 @@ public class ConstraintValidator {
|
|||||||
ValidationResult maxPerDayResult = checkMaxTwoExamsPerDay(
|
ValidationResult maxPerDayResult = checkMaxTwoExamsPerDay(
|
||||||
studentId, assignment, scheduleState);
|
studentId, assignment, scheduleState);
|
||||||
result.merge(maxPerDayResult);
|
result.merge(maxPerDayResult);
|
||||||
|
|
||||||
|
// If already invalid, no need to check more students
|
||||||
|
if (!result.isValid()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that no student has two exams at the exact same time slot.
|
||||||
|
* This is a hard constraint that must always be satisfied.
|
||||||
|
*/
|
||||||
|
private ValidationResult checkNoSameTimeExams(ExamAssignment newAssignment,
|
||||||
|
ScheduleState scheduleState,
|
||||||
|
Course course) {
|
||||||
|
ValidationResult result = new ValidationResult();
|
||||||
|
|
||||||
|
int newDay = newAssignment.getDay();
|
||||||
|
int newSlot = newAssignment.getTimeSlotIndex();
|
||||||
|
|
||||||
|
for (ExamAssignment existing : scheduleState.getAssignments().values()) {
|
||||||
|
if (!existing.isAssigned() || existing.getCourseCode().equals(newAssignment.getCourseCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if same day and slot
|
||||||
|
if (existing.getDay() == newDay && existing.getTimeSlotIndex() == newSlot) {
|
||||||
|
// Check if any student is enrolled in both courses
|
||||||
|
Course otherCourse = dataManager.getCourse(existing.getCourseCode());
|
||||||
|
if (otherCourse != null) {
|
||||||
|
for (String studentId : course.getEnrolledStudents()) {
|
||||||
|
if (otherCourse.getEnrolledStudents().contains(studentId)) {
|
||||||
|
result.addError(String.format(
|
||||||
|
"Student %s has two exams at the same time: %s and %s (Day %d, Slot %d)",
|
||||||
|
studentId, newAssignment.getCourseCode(), existing.getCourseCode(),
|
||||||
|
newDay + 1, newSlot + 1));
|
||||||
|
return result; // One conflict is enough
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +193,8 @@ public class ConstraintValidator {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that a student has no consecutive exams.
|
* Check that a student has no consecutive exams.
|
||||||
* Consecutive means exams in adjacent time slots on the same day (back-to-back).
|
* Consecutive means exams in adjacent time slots on the same day
|
||||||
|
* (back-to-back).
|
||||||
*/
|
*/
|
||||||
public ValidationResult checkNoConsecutiveExams(String studentId,
|
public ValidationResult checkNoConsecutiveExams(String studentId,
|
||||||
ExamAssignment newAssignment,
|
ExamAssignment newAssignment,
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import java.util.*;
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for generating exam schedules using Constraint Satisfaction Problem (CSP) solving.
|
* Service for generating exam schedules using Constraint Satisfaction Problem
|
||||||
|
* (CSP) solving.
|
||||||
* Implements backtracking algorithm with MRV and LCV heuristics.
|
* Implements backtracking algorithm with MRV and LCV heuristics.
|
||||||
* Works with the day/timeSlotIndex based ExamAssignment architecture.
|
* Works with the day/timeSlotIndex based ExamAssignment architecture.
|
||||||
*/
|
*/
|
||||||
@@ -57,7 +58,8 @@ public class ScheduleGeneratorService {
|
|||||||
updateProgress(coursesToSchedule.size(), coursesToSchedule.size(), "Schedule generated successfully!");
|
updateProgress(coursesToSchedule.size(), coursesToSchedule.size(), "Schedule generated successfully!");
|
||||||
return ScheduleResult.success(scheduleState);
|
return ScheduleResult.success(scheduleState);
|
||||||
} else {
|
} else {
|
||||||
return ScheduleResult.failure("No valid schedule found. Try increasing days/slots or relaxing constraints.");
|
return ScheduleResult
|
||||||
|
.failure("No valid schedule found. Try increasing days/slots or relaxing constraints.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +85,8 @@ public class ScheduleGeneratorService {
|
|||||||
/**
|
/**
|
||||||
* Backtracking algorithm core.
|
* Backtracking algorithm core.
|
||||||
*/
|
*/
|
||||||
private boolean backtrack(ScheduleState scheduleState, List<Course> courses, int courseIndex, ScheduleConfiguration config) {
|
private boolean backtrack(ScheduleState scheduleState, List<Course> courses, int courseIndex,
|
||||||
|
ScheduleConfiguration config) {
|
||||||
// Check cancellation
|
// Check cancellation
|
||||||
if (cancelled.get()) {
|
if (cancelled.get()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -122,9 +125,9 @@ public class ScheduleGeneratorService {
|
|||||||
assignment.setTimeSlotIndex(timeSlot.slot);
|
assignment.setTimeSlotIndex(timeSlot.slot);
|
||||||
assignment.setClassroomId(classroom.getClassroomId());
|
assignment.setClassroomId(classroom.getClassroomId());
|
||||||
|
|
||||||
// Validate assignment
|
// Validate assignment - pass allowBackToBack from config
|
||||||
ConstraintValidator.ValidationResult validationResult =
|
ConstraintValidator.ValidationResult validationResult = validator.validateAssignment(assignment,
|
||||||
validator.validateAssignment(assignment, scheduleState);
|
scheduleState, config.isAllowBackToBackExams());
|
||||||
|
|
||||||
if (validationResult.isValid()) {
|
if (validationResult.isValid()) {
|
||||||
// Assignment is valid, try to assign remaining courses
|
// Assignment is valid, try to assign remaining courses
|
||||||
@@ -154,8 +157,7 @@ public class ScheduleGeneratorService {
|
|||||||
// More students = more constrained
|
// More students = more constrained
|
||||||
courses.sort((c1, c2) -> Integer.compare(
|
courses.sort((c1, c2) -> Integer.compare(
|
||||||
c2.getEnrolledStudentsCount(),
|
c2.getEnrolledStudentsCount(),
|
||||||
c1.getEnrolledStudentsCount()
|
c1.getEnrolledStudentsCount()));
|
||||||
));
|
|
||||||
|
|
||||||
return courses;
|
return courses;
|
||||||
}
|
}
|
||||||
@@ -181,14 +183,16 @@ public class ScheduleGeneratorService {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case BALANCED_DISTRIBUTION:
|
case BALANCED_DISTRIBUTION:
|
||||||
// Round-robin across days: day 0 slot 0, day 1 slot 0, day 2 slot 0, ... day 0 slot 1, ...
|
// Round-robin across days: day 0 slot 0, day 1 slot 0, day 2 slot 0, ... day 0
|
||||||
|
// slot 1, ...
|
||||||
timeSlots.sort(Comparator.comparingInt((DaySlotPair p) -> p.slot)
|
timeSlots.sort(Comparator.comparingInt((DaySlotPair p) -> p.slot)
|
||||||
.thenComparingInt(p -> p.day));
|
.thenComparingInt(p -> p.day));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STUDENT_FRIENDLY:
|
case STUDENT_FRIENDLY:
|
||||||
// Try to space out exams - prefer later slots on same day to avoid consecutive
|
// Try to space out exams - prefer later slots on same day to avoid consecutive
|
||||||
// (This is a simple heuristic - more sophisticated would track student conflicts)
|
// (This is a simple heuristic - more sophisticated would track student
|
||||||
|
// conflicts)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -320,6 +324,7 @@ public class ScheduleGeneratorService {
|
|||||||
public interface ProgressListener {
|
public interface ProgressListener {
|
||||||
/**
|
/**
|
||||||
* Called when progress updates.
|
* Called when progress updates.
|
||||||
|
*
|
||||||
* @param progress Value between 0.0 and 1.0
|
* @param progress Value between 0.0 and 1.0
|
||||||
* @param message Current status message
|
* @param message Current status message
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,64 +4,153 @@
|
|||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
<BorderPane xmlns="http://javafx.com/javafx"
|
<ScrollPane xmlns="http://javafx.com/javafx"
|
||||||
xmlns:fx="http://javafx.com/fxml"
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
fx:controller="org.example.se302.controller.ScheduleCalendarController"
|
fx:controller="org.example.se302.controller.ScheduleCalendarController"
|
||||||
styleClass="content-pane">
|
fitToWidth="true" fitToHeight="true"
|
||||||
|
hbarPolicy="AS_NEEDED" vbarPolicy="AS_NEEDED">
|
||||||
|
|
||||||
|
<BorderPane styleClass="content-pane">
|
||||||
<top>
|
<top>
|
||||||
<VBox spacing="10">
|
<VBox spacing="15">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets top="20" right="20" bottom="10" left="20"/>
|
<Insets top="20" right="20" bottom="10" left="20"/>
|
||||||
</padding>
|
</padding>
|
||||||
<Label text="Calendar View - Exam Schedule" styleClass="section-title"/>
|
|
||||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
<!-- Page Title -->
|
||||||
<Label text="Exam Period:"/>
|
<Label text="📅 Calendar View - Exam Schedule Generator" styleClass="section-title"/>
|
||||||
<DatePicker fx:id="startDatePicker" promptText="Start Date"/>
|
<Label text="Configure and generate an optimized exam schedule using CSP algorithm."
|
||||||
<Label text="to"/>
|
wrapText="true" styleClass="description-label"/>
|
||||||
<DatePicker fx:id="endDatePicker" promptText="End Date"/>
|
|
||||||
<Button text="Generate Schedule" onAction="#onGenerateSchedule" styleClass="primary-button"/>
|
<Separator/>
|
||||||
|
|
||||||
|
<!-- Configuration Panel -->
|
||||||
|
<VBox spacing="15" styleClass="config-panel">
|
||||||
|
<Label text="Schedule Configuration" styleClass="subsection-title"/>
|
||||||
|
|
||||||
|
<!-- Row 1: Days and Slots -->
|
||||||
|
<HBox spacing="20" alignment="CENTER_LEFT">
|
||||||
|
<VBox spacing="5">
|
||||||
|
<Label text="Number of Days:"/>
|
||||||
|
<Spinner fx:id="numDaysSpinner" min="1" max="30" initialValue="5"
|
||||||
|
editable="true" prefWidth="100"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox spacing="5">
|
||||||
|
<Label text="Slots per Day:"/>
|
||||||
|
<Spinner fx:id="slotsPerDaySpinner" min="1" max="10" initialValue="4"
|
||||||
|
editable="true" prefWidth="100"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox spacing="5">
|
||||||
|
<Label text="Start Date:"/>
|
||||||
|
<DatePicker fx:id="startDatePicker" promptText="Select Start Date" prefWidth="150"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox spacing="5">
|
||||||
|
<Label text="Slot Duration (min):"/>
|
||||||
|
<Spinner fx:id="slotDurationSpinner" min="30" max="240" initialValue="120"
|
||||||
|
editable="true" prefWidth="100"/>
|
||||||
|
</VBox>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
||||||
|
<!-- Row 2: Strategy and Options -->
|
||||||
|
<HBox spacing="20" alignment="CENTER_LEFT">
|
||||||
|
<VBox spacing="5">
|
||||||
|
<Label text="Optimization Strategy:"/>
|
||||||
|
<ComboBox fx:id="strategyComboBox" prefWidth="200" promptText="Select Strategy"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox spacing="5">
|
||||||
|
<Label text="Day Start Time:"/>
|
||||||
|
<ComboBox fx:id="startTimeComboBox" prefWidth="120" promptText="09:00"/>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox spacing="5" alignment="CENTER_LEFT">
|
||||||
|
<padding>
|
||||||
|
<Insets top="18"/>
|
||||||
|
</padding>
|
||||||
|
<CheckBox fx:id="allowBackToBackCheckBox" text="Allow back-to-back exams" selected="true"/>
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- Row 3: Summary -->
|
||||||
|
<HBox spacing="10" alignment="CENTER_LEFT" styleClass="summary-row">
|
||||||
|
<Label text="Configuration Summary:"/>
|
||||||
|
<Label fx:id="summaryLabel" text="5 days × 4 slots = 20 total time slots"
|
||||||
|
styleClass="summary-value"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<Separator/>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<HBox spacing="15" alignment="CENTER_LEFT">
|
||||||
|
<Button fx:id="generateButton" text="🚀 Generate Schedule"
|
||||||
|
onAction="#onGenerateSchedule" styleClass="primary-button" prefWidth="180"/>
|
||||||
|
<Button fx:id="cancelButton" text="Cancel" onAction="#onCancelGeneration"
|
||||||
|
disable="true" prefWidth="100"/>
|
||||||
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
|
<Label fx:id="statusLabel" text="Ready" styleClass="status-label"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<VBox fx:id="progressContainer" spacing="5" visible="false" managed="false">
|
||||||
|
<ProgressBar fx:id="progressBar" prefWidth="400" progress="0"/>
|
||||||
|
<Label fx:id="progressLabel" text="Initializing..."/>
|
||||||
|
</VBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
</top>
|
</top>
|
||||||
|
|
||||||
<center>
|
<center>
|
||||||
<VBox spacing="10" alignment="CENTER">
|
<VBox spacing="15">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets top="50" right="20" bottom="50" left="20"/>
|
<Insets top="20" right="20" bottom="20" left="20"/>
|
||||||
</padding>
|
</padding>
|
||||||
<Label text="📅" style="-fx-font-size: 48px;"/>
|
|
||||||
<Label text="Schedule Generation Coming Soon" styleClass="info-title"/>
|
<!-- Schedule Grid Container -->
|
||||||
<Label text="The calendar grid will display exam schedules across days and time slots."
|
<Label text="Generated Schedule" styleClass="subsection-title"/>
|
||||||
wrapText="true" textAlignment="CENTER" maxWidth="500"/>
|
|
||||||
<Separator prefWidth="300"/>
|
<ScrollPane fx:id="scheduleScrollPane" fitToWidth="true" fitToHeight="false"
|
||||||
<Label text="Preview: Grid Layout" styleClass="subsection-title"/>
|
prefHeight="500" minHeight="300"
|
||||||
<GridPane gridLinesVisible="true" styleClass="schedule-grid">
|
hbarPolicy="ALWAYS" vbarPolicy="ALWAYS"
|
||||||
|
VBox.vgrow="ALWAYS"
|
||||||
|
style="-fx-background-color: white; -fx-border-color: #bdc3c7; -fx-border-width: 1;">
|
||||||
|
<GridPane fx:id="scheduleGrid" gridLinesVisible="true" styleClass="schedule-grid"
|
||||||
|
minWidth="800" minHeight="400">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets top="10" right="10" bottom="10" left="10"/>
|
<Insets top="10" right="10" bottom="10" left="10"/>
|
||||||
</padding>
|
</padding>
|
||||||
<!-- Header row -->
|
<!-- Grid will be populated dynamically -->
|
||||||
<Label text="" GridPane.columnIndex="0" GridPane.rowIndex="0" styleClass="grid-header"/>
|
<Label text="Click 'Generate Schedule' to create an exam schedule"
|
||||||
<Label text="Day 1" GridPane.columnIndex="1" GridPane.rowIndex="0" styleClass="grid-header"/>
|
GridPane.columnIndex="0" GridPane.rowIndex="0"
|
||||||
<Label text="Day 2" GridPane.columnIndex="2" GridPane.rowIndex="0" styleClass="grid-header"/>
|
styleClass="placeholder-text" wrapText="true"/>
|
||||||
<Label text="Day 3" GridPane.columnIndex="3" GridPane.rowIndex="0" styleClass="grid-header"/>
|
|
||||||
|
|
||||||
<!-- Time slots -->
|
|
||||||
<Label text="09:00" GridPane.columnIndex="0" GridPane.rowIndex="1" styleClass="grid-header"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="1" GridPane.rowIndex="1" styleClass="grid-cell"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="2" GridPane.rowIndex="1" styleClass="grid-cell"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="3" GridPane.rowIndex="1" styleClass="grid-cell"/>
|
|
||||||
|
|
||||||
<Label text="11:00" GridPane.columnIndex="0" GridPane.rowIndex="2" styleClass="grid-header"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="1" GridPane.rowIndex="2" styleClass="grid-cell"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="2" GridPane.rowIndex="2" styleClass="grid-cell"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="3" GridPane.rowIndex="2" styleClass="grid-cell"/>
|
|
||||||
|
|
||||||
<Label text="14:00" GridPane.columnIndex="0" GridPane.rowIndex="3" styleClass="grid-header"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="1" GridPane.rowIndex="3" styleClass="grid-cell"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="2" GridPane.rowIndex="3" styleClass="grid-cell"/>
|
|
||||||
<Label text="-" GridPane.columnIndex="3" GridPane.rowIndex="3" styleClass="grid-cell"/>
|
|
||||||
</GridPane>
|
</GridPane>
|
||||||
|
</ScrollPane>
|
||||||
|
|
||||||
|
<!-- Statistics Panel -->
|
||||||
|
<HBox spacing="30" alignment="CENTER_LEFT" styleClass="stats-panel">
|
||||||
|
<VBox spacing="3" alignment="CENTER">
|
||||||
|
<Label fx:id="totalCoursesLabel" text="0" styleClass="stat-value"/>
|
||||||
|
<Label text="Total Courses" styleClass="stat-label"/>
|
||||||
|
</VBox>
|
||||||
|
<Separator orientation="VERTICAL"/>
|
||||||
|
<VBox spacing="3" alignment="CENTER">
|
||||||
|
<Label fx:id="scheduledCoursesLabel" text="0" styleClass="stat-value"/>
|
||||||
|
<Label text="Scheduled" styleClass="stat-label"/>
|
||||||
|
</VBox>
|
||||||
|
<Separator orientation="VERTICAL"/>
|
||||||
|
<VBox spacing="3" alignment="CENTER">
|
||||||
|
<Label fx:id="classroomsUsedLabel" text="0" styleClass="stat-value"/>
|
||||||
|
<Label text="Classrooms Used" styleClass="stat-label"/>
|
||||||
|
</VBox>
|
||||||
|
<Separator orientation="VERTICAL"/>
|
||||||
|
<VBox spacing="3" alignment="CENTER">
|
||||||
|
<Label fx:id="generationTimeLabel" text="-" styleClass="stat-value"/>
|
||||||
|
<Label text="Generation Time" styleClass="stat-label"/>
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
</center>
|
</center>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
</ScrollPane>
|
||||||
|
|||||||
Reference in New Issue
Block a user