Merge remote-tracking branch 'origin/master'

This commit is contained in:
sabazadam
2025-12-18 19:13:12 +03:00
11 changed files with 440 additions and 231 deletions

View File

@@ -15,20 +15,33 @@ import org.example.se302.service.DataManager;
*/ */
public class CoursesController { public class CoursesController {
@FXML private TextField searchField; @FXML
@FXML private Label resultCountLabel; private TextField searchField;
@FXML private TableView<Course> coursesTable; @FXML
@FXML private TableColumn<Course, String> courseCodeColumn; private Label resultCountLabel;
@FXML private TableColumn<Course, Number> studentCountColumn; @FXML
@FXML private TableColumn<Course, String> classroomColumn; private TableView<Course> coursesTable;
@FXML private TableColumn<Course, String> examDateColumn; @FXML
@FXML private TableColumn<Course, Void> actionColumn; private TableColumn<Course, String> courseCodeColumn;
@FXML
private TableColumn<Course, Number> studentCountColumn;
@FXML
private TableColumn<Course, String> classroomColumn;
@FXML
private TableColumn<Course, String> examDateColumn;
@FXML
private TableColumn<Course, Void> actionColumn;
@FXML private VBox studentListPanel; @FXML
@FXML private Label studentListTitleLabel; private VBox studentListPanel;
@FXML private TableView<String> enrolledStudentsTable; @FXML
@FXML private TableColumn<String, String> enrolledStudentIdColumn; private Label studentListTitleLabel;
@FXML private Label enrolledCountLabel; @FXML
private TableView<String> enrolledStudentsTable;
@FXML
private TableColumn<String, String> enrolledStudentIdColumn;
@FXML
private Label enrolledCountLabel;
private DataManager dataManager; private DataManager dataManager;
private FilteredList<Course> filteredCourses; private FilteredList<Course> filteredCourses;
@@ -38,11 +51,10 @@ public class CoursesController {
dataManager = DataManager.getInstance(); dataManager = DataManager.getInstance();
// Set up table columns // Set up table columns
courseCodeColumn.setCellValueFactory(cellData -> courseCodeColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getCourseCode()));
new SimpleStringProperty(cellData.getValue().getCourseCode()));
studentCountColumn.setCellValueFactory(cellData -> studentCountColumn.setCellValueFactory(
new SimpleIntegerProperty(cellData.getValue().getEnrolledStudentsCount())); cellData -> new SimpleIntegerProperty(cellData.getValue().getEnrolledStudentsCount()));
classroomColumn.setCellValueFactory(cellData -> { classroomColumn.setCellValueFactory(cellData -> {
String classroom = cellData.getValue().getAssignedClassroom(); String classroom = cellData.getValue().getAssignedClassroom();
@@ -50,8 +62,12 @@ public class CoursesController {
}); });
examDateColumn.setCellValueFactory(cellData -> { examDateColumn.setCellValueFactory(cellData -> {
String examDate = cellData.getValue().getExamDateTime(); Course course = cellData.getValue();
return new SimpleStringProperty(examDate != null ? examDate : "Not Scheduled"); if (course.isScheduled()) {
return new SimpleStringProperty("Day " + (course.getExamDay() + 1) +
", Slot " + (course.getExamTimeSlot() + 1));
}
return new SimpleStringProperty("Not Scheduled");
}); });
// Add "View Students" button to action column // Add "View Students" button to action column
@@ -73,8 +89,7 @@ public class CoursesController {
}); });
// Set up enrolled students table column // Set up enrolled students table column
enrolledStudentIdColumn.setCellValueFactory(cellData -> enrolledStudentIdColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue()));
new SimpleStringProperty(cellData.getValue()));
// Set up filtered list // Set up filtered list
filteredCourses = new FilteredList<>(dataManager.getCourses(), p -> true); filteredCourses = new FilteredList<>(dataManager.getCourses(), p -> true);
@@ -106,14 +121,13 @@ public class CoursesController {
filteredCourses.setPredicate(course -> true); filteredCourses.setPredicate(course -> true);
} else { } else {
String lowerCaseFilter = searchText.toLowerCase().trim(); String lowerCaseFilter = searchText.toLowerCase().trim();
filteredCourses.setPredicate(course -> filteredCourses.setPredicate(course -> course.getCourseCode().toLowerCase().contains(lowerCaseFilter));
course.getCourseCode().toLowerCase().contains(lowerCaseFilter)
);
} }
} }
private void showEnrolledStudents(Course course) { private void showEnrolledStudents(Course course) {
if (course == null) return; if (course == null)
return;
studentListTitleLabel.setText("Students Enrolled in " + course.getCourseCode()); studentListTitleLabel.setText("Students Enrolled in " + course.getCourseCode());
enrolledStudentsTable.setItems(FXCollections.observableArrayList(course.getEnrolledStudents())); enrolledStudentsTable.setItems(FXCollections.observableArrayList(course.getEnrolledStudents()));

View File

@@ -236,13 +236,13 @@ public class ExamEditDialog extends Dialog<ExamEditDialog.EditResult> {
validationPanel.setStyle("-fx-background-color: #e8f5e9; -fx-background-radius: 5;"); validationPanel.setStyle("-fx-background-color: #e8f5e9; -fx-background-radius: 5;");
updateButtonStates(true, false); updateButtonStates(true, false);
} else if (currentValidation.hasHardViolations()) { } else if (currentValidation.hasHardViolations()) {
validationStatusLabel.setText("Constraint violations found"); validationStatusLabel.setText("Constraint violations found!");
validationStatusLabel.setTextFill(Color.web("#e74c3c")); validationStatusLabel.setTextFill(Color.web("#e74c3c"));
violationDetails.setText(currentValidation.getFormattedMessage()); violationDetails.setText(currentValidation.getFormattedMessage());
validationPanel.setStyle("-fx-background-color: #ffebee; -fx-background-radius: 5;"); validationPanel.setStyle("-fx-background-color: #ffebee; -fx-background-radius: 5;");
updateButtonStates(false, true); updateButtonStates(false, true);
} else { } else {
validationStatusLabel.setText("⚠️ Warnings found"); validationStatusLabel.setText("Warnings found!");
validationStatusLabel.setTextFill(Color.web("#f39c12")); validationStatusLabel.setTextFill(Color.web("#f39c12"));
violationDetails.setText(currentValidation.getFormattedMessage()); violationDetails.setText(currentValidation.getFormattedMessage());
validationPanel.setStyle("-fx-background-color: #fff3e0; -fx-background-radius: 5;"); validationPanel.setStyle("-fx-background-color: #fff3e0; -fx-background-radius: 5;");

View File

@@ -737,4 +737,68 @@ public class ScheduleCalendarController {
"Could not update the exam assignment. The exam may be locked."); "Could not update the exam assignment. The exam may be locked.");
} }
} }
/**
* Exports the schedule as a CSV grid (days × time slots).
*/
@FXML
private void onExportCSV() {
if (currentSchedule == null || currentConfig == null) {
showAlert(Alert.AlertType.WARNING, "No Schedule",
"Please generate a schedule first.");
return;
}
javafx.stage.FileChooser fileChooser = new javafx.stage.FileChooser();
fileChooser.setTitle("Export Schedule as CSV");
fileChooser.getExtensionFilters().add(
new javafx.stage.FileChooser.ExtensionFilter("CSV Files", "*.csv"));
fileChooser.setInitialFileName("schedule_calendar.csv");
java.io.File file = fileChooser.showSaveDialog(scheduleGrid.getScene().getWindow());
if (file == null)
return;
try (java.io.PrintWriter writer = new java.io.PrintWriter(file)) {
int numDays = currentConfig.getNumDays();
int slotsPerDay = currentConfig.getSlotsPerDay();
// Header row: Time Slot, Day 1, Day 2, ...
StringBuilder header = new StringBuilder("Time Slot");
for (int day = 0; day < numDays; day++) {
LocalDate date = currentConfig.getStartDate().plusDays(day);
header.append(",").append(date.toString());
}
writer.println(header);
// Data rows: one per time slot
for (int slot = 0; slot < slotsPerDay; slot++) {
StringBuilder row = new StringBuilder();
TimeSlot timeSlot = currentConfig.getTimeSlot(0, slot);
String slotLabel = timeSlot != null
? timeSlot.getStartTime() + "-" + timeSlot.getEndTime()
: "Slot " + (slot + 1);
row.append(slotLabel);
for (int day = 0; day < numDays; day++) {
row.append(",");
// Find exams at this day/slot
List<String> exams = new ArrayList<>();
for (ExamAssignment a : currentSchedule.getAssignments().values()) {
if (a.isAssigned() && a.getDay() == day && a.getTimeSlotIndex() == slot) {
exams.add(a.getCourseCode() + " (" + a.getClassroomId() + ")");
}
}
row.append("\"").append(String.join("; ", exams)).append("\"");
}
writer.println(row);
}
showAlert(Alert.AlertType.INFORMATION, "Export Complete",
"Schedule exported to:\n" + file.getAbsolutePath());
} catch (java.io.IOException e) {
showAlert(Alert.AlertType.ERROR, "Export Failed",
"Could not write file: " + e.getMessage());
}
}
} }

View File

@@ -11,14 +11,14 @@ import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow; import javafx.scene.control.TableRow;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import org.example.se302.model.Classroom; import org.example.se302.model.Classroom;
import org.example.se302.model.ExamAssignment; import org.example.se302.model.Course;
import org.example.se302.model.ScheduleConfiguration; import org.example.se302.model.ScheduleConfiguration;
import org.example.se302.model.TimeSlot;
import org.example.se302.service.DataManager; import org.example.se302.service.DataManager;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Comparator; import java.util.Comparator;
import java.util.Map;
/** /**
* Controller for the Classroom Schedule view. * Controller for the Classroom Schedule view.
@@ -47,10 +47,6 @@ public class ScheduleClassroomController {
private DataManager dataManager; private DataManager dataManager;
// Reference to the current schedule state (set by parent controller)
private Map<String, ExamAssignment> currentAssignments;
private ScheduleConfiguration configuration;
@FXML @FXML
public void initialize() { public void initialize() {
dataManager = DataManager.getInstance(); dataManager = DataManager.getInstance();
@@ -112,15 +108,35 @@ public class ScheduleClassroomController {
} }
} }
}); });
}
/** // Refresh when tab is selected - find TabPane once scene is available
* Sets the current schedule assignments. Called by parent controller after scheduleTable.sceneProperty().addListener((obs, oldScene, newScene) -> {
* schedule generation. if (newScene != null) {
*/ // Find parent TabPane and listen for selection changes
public void setScheduleData(Map<String, ExamAssignment> assignments, ScheduleConfiguration config) { javafx.scene.Parent parent = scheduleTable.getParent();
this.currentAssignments = assignments; while (parent != null) {
this.configuration = config; if (parent.getParent() instanceof javafx.scene.control.TabPane) {
javafx.scene.control.TabPane tabPane = (javafx.scene.control.TabPane) parent
.getParent();
// Find which tab contains our content
for (javafx.scene.control.Tab tab : tabPane.getTabs()) {
if (tab.getContent() == parent) {
tab.selectedProperty().addListener(
(o, wasSelected, isSelected) -> {
if (isSelected && classroomComboBox
.getValue() != null) {
onShowSchedule();
}
});
break;
}
}
break;
}
parent = parent.getParent();
}
}
});
} }
@FXML @FXML
@@ -132,69 +148,64 @@ public class ScheduleClassroomController {
selectedClassroomLabel.setText("Schedule for: " + selected.getClassroomId() + selectedClassroomLabel.setText("Schedule for: " + selected.getClassroomId() +
" (Capacity: " + selected.getCapacity() + ")"); " (Capacity: " + selected.getCapacity() + ")");
ScheduleConfiguration config = dataManager.getActiveConfiguration();
ObservableList<ClassroomSlotEntry> entries = FXCollections.observableArrayList(); ObservableList<ClassroomSlotEntry> entries = FXCollections.observableArrayList();
int totalSlots = 0;
int usedSlots = 0; int usedSlots = 0;
int totalStudents = 0; int totalStudents = 0;
// If we have assignments, filter by this classroom for (Course course : dataManager.getCourses()) {
if (currentAssignments != null && !currentAssignments.isEmpty()) { if (!course.isScheduled() || !selected.getClassroomId().equals(course.getAssignedClassroom()))
for (ExamAssignment assignment : currentAssignments.values()) { continue;
if (assignment.isAssigned() &&
selected.getClassroomId().equals(assignment.getClassroomId())) {
// Format date int dayIndex = course.getExamDay();
String dateStr; int slotIndex = course.getExamTimeSlot();
if (configuration != null && configuration.getStartDate() != null) { int studentCount = course.getEnrolledStudentsCount();
LocalDate examDate = configuration.getStartDate() int utilization = selected.getCapacity() > 0
.plusDays(assignment.getDay()); ? (studentCount * 100) / selected.getCapacity()
dateStr = examDate.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); : 0;
} else {
dateStr = "Day " + (assignment.getDay() + 1);
}
// Format time entries.add(new ClassroomSlotEntry(
String timeStr = "Slot " + (assignment.getTimeSlotIndex() + 1); formatDate(config, dayIndex),
formatTime(config, dayIndex, slotIndex),
course.getCourseCode(), studentCount, utilization + "%",
utilization, dayIndex, slotIndex));
// Calculate utilization percentage for this slot usedSlots++;
int studentCount = assignment.getStudentCount(); totalStudents += studentCount;
int capacity = selected.getCapacity();
int utilizationPercent = capacity > 0 ? (studentCount * 100) / capacity : 0;
String utilizationStr = utilizationPercent + "%";
entries.add(new ClassroomSlotEntry(
dateStr, timeStr, assignment.getCourseCode(),
studentCount, utilizationStr, utilizationPercent,
assignment.getDay(), assignment.getTimeSlotIndex()));
usedSlots++;
totalStudents += studentCount;
}
}
// Calculate total possible slots
if (configuration != null) {
totalSlots = configuration.getNumDays() * configuration.getSlotsPerDay();
}
} }
// Sort by day then slot
entries.sort(Comparator.comparingInt(ClassroomSlotEntry::getDayIndex) entries.sort(Comparator.comparingInt(ClassroomSlotEntry::getDayIndex)
.thenComparingInt(ClassroomSlotEntry::getSlotIndex)); .thenComparingInt(ClassroomSlotEntry::getSlotIndex));
scheduleTable.setItems(entries); scheduleTable.setItems(entries);
// Update overall utilization label int totalSlots = config != null ? config.getNumDays() * config.getSlotsPerDay() : 0;
if (totalSlots > 0) { if (totalSlots > 0) {
int overallUtilization = (usedSlots * 100) / totalSlots; int overallUtil = (usedSlots * 100) / totalSlots;
utilizationLabel.setText(String.format( utilizationLabel.setText(String.format(
"Overall Utilization: %d%% (%d/%d slots used, %d total students)", "Overall Utilization: %d%% (%d/%d slots used, %d total students)",
overallUtilization, usedSlots, totalSlots, totalStudents)); overallUtil, usedSlots, totalSlots, totalStudents));
} else { } else {
utilizationLabel.setText("Overall Utilization: 0% (No schedule data available)"); utilizationLabel.setText("Overall Utilization: 0% (No schedule data available)");
} }
} }
private String formatDate(ScheduleConfiguration config, int dayIndex) {
if (config != null && config.getStartDate() != null) {
LocalDate examDate = config.getStartDate().plusDays(dayIndex);
return examDate.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
}
return "Day " + (dayIndex + 1);
}
private String formatTime(ScheduleConfiguration config, int dayIndex, int slotIndex) {
if (config != null) {
TimeSlot slot = config.getTimeSlot(dayIndex, slotIndex);
if (slot != null)
return slot.getStartTime() + " - " + slot.getEndTime();
}
return "Slot " + (slotIndex + 1);
}
// Helper class for table entries // Helper class for table entries
public static class ClassroomSlotEntry { public static class ClassroomSlotEntry {
private final String date; private final String date;
@@ -250,4 +261,62 @@ public class ScheduleClassroomController {
return slotIndex; return slotIndex;
} }
} }
/**
* Exports the selected classroom's schedule as CSV.
*/
@FXML
private void onExportCSV() {
Classroom selected = classroomComboBox.getValue();
if (selected == null) {
showAlert(javafx.scene.control.Alert.AlertType.WARNING, "No Classroom",
"Please select a classroom first.");
return;
}
if (scheduleTable.getItems().isEmpty()) {
showAlert(javafx.scene.control.Alert.AlertType.WARNING, "No Data",
"No schedule data to export.");
return;
}
javafx.stage.FileChooser fileChooser = new javafx.stage.FileChooser();
fileChooser.setTitle("Export Classroom Schedule as CSV");
fileChooser.getExtensionFilters().add(
new javafx.stage.FileChooser.ExtensionFilter("CSV Files", "*.csv"));
fileChooser.setInitialFileName("schedule_classroom_" + selected.getClassroomId() + ".csv");
java.io.File file = fileChooser.showSaveDialog(scheduleTable.getScene().getWindow());
if (file == null)
return;
try (java.io.PrintWriter writer = new java.io.PrintWriter(file)) {
// Header
writer.println("Classroom,Date,Time,Course,Students");
// Data rows
for (ClassroomSlotEntry entry : scheduleTable.getItems()) {
writer.println(String.format("%s,\"%s\",%s,%s,%d",
selected.getClassroomId(),
entry.getDate(),
entry.getTime(),
entry.getCourse(),
entry.getStudentCount()));
}
showAlert(javafx.scene.control.Alert.AlertType.INFORMATION, "Export Complete",
"Classroom schedule exported to:\n" + file.getAbsolutePath());
} catch (java.io.IOException e) {
showAlert(javafx.scene.control.Alert.AlertType.ERROR, "Export Failed",
"Could not write file: " + e.getMessage());
}
}
private void showAlert(javafx.scene.control.Alert.AlertType type, String title, String message) {
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(type);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
} }

View File

@@ -9,14 +9,13 @@ import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow; import javafx.scene.control.TableRow;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import org.example.se302.model.Course; import org.example.se302.model.Course;
import org.example.se302.model.ExamAssignment;
import org.example.se302.model.ScheduleConfiguration; import org.example.se302.model.ScheduleConfiguration;
import org.example.se302.model.TimeSlot;
import org.example.se302.service.DataManager; import org.example.se302.service.DataManager;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Comparator; import java.util.Comparator;
import java.util.Map;
// Color palette for days (pastel colors for readability) // Color palette for days (pastel colors for readability)
// Day 0 = Light Blue, Day 1 = Light Green, Day 2 = Light Yellow, etc. // Day 0 = Light Blue, Day 1 = Light Green, Day 2 = Light Yellow, etc.
@@ -43,11 +42,6 @@ public class ScheduleCourseController {
private DataManager dataManager; private DataManager dataManager;
// Reference to the current schedule state (set by parent controller or loaded
// from DB)
private Map<String, ExamAssignment> currentAssignments;
private ScheduleConfiguration configuration;
@FXML @FXML
public void initialize() { public void initialize() {
dataManager = DataManager.getInstance(); dataManager = DataManager.getInstance();
@@ -122,68 +116,83 @@ public class ScheduleCourseController {
// Listen for data changes // Listen for data changes
dataManager.getCourses().addListener( dataManager.getCourses().addListener(
(javafx.collections.ListChangeListener<Course>) c -> loadScheduleData()); (javafx.collections.ListChangeListener<Course>) c -> loadScheduleData());
}
/** // Refresh when tab is selected - find TabPane once scene is available
* Sets the current schedule assignments. Called by parent controller after courseScheduleTable.sceneProperty().addListener((obs, oldScene, newScene) -> {
* schedule generation. if (newScene != null) {
*/ loadScheduleData();
public void setScheduleData(Map<String, ExamAssignment> assignments, ScheduleConfiguration config) { // Find parent TabPane and listen for selection changes
this.currentAssignments = assignments; javafx.scene.Parent parent = courseScheduleTable.getParent();
this.configuration = config; while (parent != null) {
loadScheduleData(); if (parent.getParent() instanceof javafx.scene.control.TabPane) {
javafx.scene.control.TabPane tabPane = (javafx.scene.control.TabPane) parent
.getParent();
// Find which tab contains our content
for (javafx.scene.control.Tab tab : tabPane.getTabs()) {
if (tab.getContent() == parent) {
tab.selectedProperty().addListener(
(o, wasSelected, isSelected) -> {
if (isSelected)
loadScheduleData();
});
break;
}
}
break;
}
parent = parent.getParent();
}
}
});
} }
private void loadScheduleData() { private void loadScheduleData() {
ScheduleConfiguration config = dataManager.getActiveConfiguration();
ObservableList<CourseScheduleEntry> entries = FXCollections.observableArrayList(); ObservableList<CourseScheduleEntry> entries = FXCollections.observableArrayList();
for (Course course : dataManager.getCourses()) { for (Course course : dataManager.getCourses()) {
String courseCode = course.getCourseCode();
int enrolled = course.getEnrolledStudentsCount();
String dateStr = "Not Scheduled"; String dateStr = "Not Scheduled";
String timeStr = "-"; String timeStr = "-";
String classroomStr = "-"; String classroomStr = "-";
int dayIndex = Integer.MAX_VALUE; // For sorting unscheduled items last int dayIndex = Integer.MAX_VALUE;
int slotIndex = Integer.MAX_VALUE; int slotIndex = Integer.MAX_VALUE;
// Check if we have an assignment for this course if (course.isScheduled()) {
if (currentAssignments != null && currentAssignments.containsKey(courseCode)) { dayIndex = course.getExamDay();
ExamAssignment assignment = currentAssignments.get(courseCode); slotIndex = course.getExamTimeSlot();
classroomStr = course.getAssignedClassroom();
if (assignment.isAssigned()) { dateStr = formatDate(config, dayIndex);
dayIndex = assignment.getDay(); timeStr = formatTime(config, dayIndex, slotIndex);
slotIndex = assignment.getTimeSlotIndex();
// Format date based on configuration start date + day offset
if (configuration != null && configuration.getStartDate() != null) {
LocalDate examDate = configuration.getStartDate().plusDays(dayIndex);
dateStr = examDate.format(
DateTimeFormatter.ofPattern("dd/MM/yyyy (EEEE)"));
} else {
dateStr = "Day " + (dayIndex + 1);
}
// Format time slot
timeStr = "Slot " + (slotIndex + 1);
// Classroom
classroomStr = assignment.getClassroomId() != null ? assignment.getClassroomId()
: "-";
}
} }
entries.add(new CourseScheduleEntry(courseCode, enrolled, dateStr, timeStr, classroomStr, entries.add(new CourseScheduleEntry(course.getCourseCode(),
course.getEnrolledStudentsCount(), dateStr, timeStr, classroomStr,
dayIndex, slotIndex)); dayIndex, slotIndex));
} }
// Sort by day first, then by slot
entries.sort(Comparator.comparingInt(CourseScheduleEntry::getDayIndex) entries.sort(Comparator.comparingInt(CourseScheduleEntry::getDayIndex)
.thenComparingInt(CourseScheduleEntry::getSlotIndex)); .thenComparingInt(CourseScheduleEntry::getSlotIndex));
courseScheduleTable.setItems(entries); courseScheduleTable.setItems(entries);
} }
private String formatDate(ScheduleConfiguration config, int dayIndex) {
if (config != null && config.getStartDate() != null) {
LocalDate examDate = config.getStartDate().plusDays(dayIndex);
return examDate.format(DateTimeFormatter.ofPattern("dd/MM/yyyy (EEEE)"));
}
return "Day " + (dayIndex + 1);
}
private String formatTime(ScheduleConfiguration config, int dayIndex, int slotIndex) {
if (config != null) {
TimeSlot slot = config.getTimeSlot(dayIndex, slotIndex);
if (slot != null) {
return slot.getStartTime() + " - " + slot.getEndTime();
}
}
return "Slot " + (slotIndex + 1);
}
// 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;
@@ -233,4 +242,55 @@ public class ScheduleCourseController {
return slotIndex; return slotIndex;
} }
} }
/**
* Exports the course schedule as a CSV file.
*/
@FXML
private void onExportCSV() {
if (courseScheduleTable.getItems().isEmpty()) {
showAlert(javafx.scene.control.Alert.AlertType.WARNING, "No Data",
"No schedule data to export.");
return;
}
javafx.stage.FileChooser fileChooser = new javafx.stage.FileChooser();
fileChooser.setTitle("Export Course Schedule as CSV");
fileChooser.getExtensionFilters().add(
new javafx.stage.FileChooser.ExtensionFilter("CSV Files", "*.csv"));
fileChooser.setInitialFileName("schedule_courses.csv");
java.io.File file = fileChooser.showSaveDialog(courseScheduleTable.getScene().getWindow());
if (file == null)
return;
try (java.io.PrintWriter writer = new java.io.PrintWriter(file)) {
// Header
writer.println("Course Code,Date,Time,Classroom,Enrolled Students");
// Data rows
for (CourseScheduleEntry entry : courseScheduleTable.getItems()) {
writer.println(String.format("%s,\"%s\",%s,%s,%d",
entry.getCourseCode(),
entry.getDate(),
entry.getTime(),
entry.getClassroom(),
entry.getEnrolledCount()));
}
showAlert(javafx.scene.control.Alert.AlertType.INFORMATION, "Export Complete",
"Course schedule exported to:\n" + file.getAbsolutePath());
} catch (java.io.IOException e) {
showAlert(javafx.scene.control.Alert.AlertType.ERROR, "Export Failed",
"Could not write file: " + e.getMessage());
}
}
private void showAlert(javafx.scene.control.Alert.AlertType type, String title, String message) {
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(type);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
} }

View File

@@ -105,77 +105,58 @@ public class ScheduleStudentController {
for (String courseCode : student.getEnrolledCourses()) { for (String courseCode : student.getEnrolledCourses()) {
Course course = dataManager.getCourse(courseCode); Course course = dataManager.getCourse(courseCode);
if (course != null) { if (course == null)
String dateStr = "Not Scheduled"; continue;
String timeStr = "-";
String classroom = "-";
int dayIndex = -1;
int slotIndex = -1;
if (course.isScheduled()) { String dateStr = "Not Scheduled";
dayIndex = course.getExamDay(); String timeStr = "-";
slotIndex = course.getExamTimeSlot(); String classroom = "-";
classroom = course.getAssignedClassroom(); int dayIndex = -1, slotIndex = -1;
if (config != null) { if (course.isScheduled()) {
TimeSlot slot = config.getTimeSlot(dayIndex, slotIndex); dayIndex = course.getExamDay();
if (slot != null) { slotIndex = course.getExamTimeSlot();
dateStr = slot.getDate().toString(); // YYYY-MM-DD classroom = course.getAssignedClassroom();
timeStr = slot.getStartTime().toString() + " - " + slot.getEndTime().toString();
} else { TimeSlot slot = config != null ? config.getTimeSlot(dayIndex, slotIndex) : null;
dateStr = "Day " + (dayIndex + 1); if (slot != null) {
timeStr = "Slot " + (slotIndex + 1); dateStr = slot.getDate().toString();
} timeStr = slot.getStartTime() + " - " + slot.getEndTime();
} else { } else {
// Fallback if no config saved dateStr = "Day " + (dayIndex + 1);
dateStr = "Day " + (dayIndex + 1); timeStr = "Slot " + (slotIndex + 1);
timeStr = "Slot " + (slotIndex + 1);
}
} }
entries.add(new CourseScheduleEntry(courseCode, dateStr, timeStr, classroom, dayIndex, slotIndex));
} }
entries.add(new CourseScheduleEntry(courseCode, dateStr, timeStr, classroom, dayIndex, slotIndex));
} }
// Sort by day and time
entries.sort(Comparator.comparingInt(CourseScheduleEntry::getDayIndex) entries.sort(Comparator.comparingInt(CourseScheduleEntry::getDayIndex)
.thenComparingInt(CourseScheduleEntry::getSlotIndex)); .thenComparingInt(CourseScheduleEntry::getSlotIndex));
// Analyze for highlights
analyzeSchedule(entries); analyzeSchedule(entries);
scheduleTable.setItems(FXCollections.observableArrayList(entries)); scheduleTable.setItems(FXCollections.observableArrayList(entries));
} }
private void analyzeSchedule(List<CourseScheduleEntry> entries) { private void analyzeSchedule(List<CourseScheduleEntry> entries) {
if (entries.isEmpty())
return;
for (int i = 0; i < entries.size(); i++) { for (int i = 0; i < entries.size(); i++) {
CourseScheduleEntry current = entries.get(i); CourseScheduleEntry current = entries.get(i);
if (current.getDayIndex() == -1) if (current.getDayIndex() == -1)
continue; // Skip unscheduled continue;
// Check for multiple exams on same day // Check for multiple exams and conflicts on same day
int examsOnDay = 0;
for (CourseScheduleEntry other : entries) { for (CourseScheduleEntry other : entries) {
if (other.getDayIndex() == current.getDayIndex() && other.getDayIndex() != -1) { if (other.getDayIndex() == current.getDayIndex() && other.getDayIndex() != -1) {
examsOnDay++; current.isMultipleExamsOnDay = true;
if (other.getSlotIndex() == current.getSlotIndex() && other != current) { if (other.getSlotIndex() == current.getSlotIndex() && other != current) {
current.hasConflictWarning = true; current.hasConflictWarning = true;
} }
} }
} }
if (examsOnDay > 1) {
current.isMultipleExamsOnDay = true;
}
// Check for consecutive days (look at previous scheduled exam) // Check for consecutive days with previous exam
// Since list is sorted, we can look at previous entry if it exists
if (i > 0) { if (i > 0) {
CourseScheduleEntry prev = entries.get(i - 1); CourseScheduleEntry prev = entries.get(i - 1);
if (prev.getDayIndex() != -1 && if (prev.getDayIndex() != -1 && current.getDayIndex() == prev.getDayIndex() + 1) {
current.getDayIndex() == prev.getDayIndex() + 1) {
current.isConsecutiveDay = true; current.isConsecutiveDay = true;
} }
} }
@@ -230,4 +211,62 @@ public class ScheduleStudentController {
return slotIndex; return slotIndex;
} }
} }
/**
* Exports the selected student's schedule as CSV.
*/
@FXML
private void onExportCSV() {
Student selected = studentComboBox.getValue();
if (selected == null) {
showAlert(Alert.AlertType.WARNING, "No Student",
"Please select a student first.");
return;
}
if (scheduleTable.getItems().isEmpty()) {
showAlert(Alert.AlertType.WARNING, "No Data",
"No schedule data to export.");
return;
}
javafx.stage.FileChooser fileChooser = new javafx.stage.FileChooser();
fileChooser.setTitle("Export Student Schedule as CSV");
fileChooser.getExtensionFilters().add(
new javafx.stage.FileChooser.ExtensionFilter("CSV Files", "*.csv"));
fileChooser.setInitialFileName("schedule_student_" + selected.getStudentId() + ".csv");
java.io.File file = fileChooser.showSaveDialog(scheduleTable.getScene().getWindow());
if (file == null)
return;
try (java.io.PrintWriter writer = new java.io.PrintWriter(file)) {
// Header
writer.println("Student ID,Course,Date,Time,Classroom");
// Data rows
for (CourseScheduleEntry entry : scheduleTable.getItems()) {
writer.println(String.format("%s,%s,\"%s\",%s,%s",
selected.getStudentId(),
entry.getCourseCode(),
entry.getDateDisplay(),
entry.getTimeDisplay(),
entry.getClassroom()));
}
showAlert(Alert.AlertType.INFORMATION, "Export Complete",
"Student schedule exported to:\n" + file.getAbsolutePath());
} catch (java.io.IOException e) {
showAlert(Alert.AlertType.ERROR, "Export Failed",
"Could not write file: " + e.getMessage());
}
}
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();
}
} }

View File

@@ -4,72 +4,40 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* Represents a course in the exam scheduling system. * Represents a course with exam scheduling details.
* Contains course information and exam scheduling details.
*/ */
public class Course { public class Course {
private String courseCode; private String courseCode;
private List<String> enrolledStudents; private List<String> enrolledStudents;
private int examDay = -1;
// Exam schedule fields (index-based) private int examTimeSlot = -1;
private int examDay; // -1 if not scheduled, 0-based day index private String assignedClassroom;
private int examTimeSlot; // -1 if not scheduled, 0-based slot index
private String assignedClassroom; // null if not scheduled
// Legacy field for backward compatibility
private String examDateTime; // null if not scheduled (string format)
public Course(String courseCode) { public Course(String courseCode) {
this.courseCode = courseCode; this.courseCode = courseCode;
this.enrolledStudents = new ArrayList<>(); this.enrolledStudents = new ArrayList<>();
this.examDay = -1;
this.examTimeSlot = -1;
this.assignedClassroom = null;
this.examDateTime = null;
} }
/**
* Checks if this course has been scheduled for an exam.
*/
public boolean isScheduled() { public boolean isScheduled() {
return examDay >= 0 && examTimeSlot >= 0 && assignedClassroom != null; return examDay >= 0 && examTimeSlot >= 0 && assignedClassroom != null;
} }
/**
* Clears the exam schedule for this course.
*/
public void clearSchedule() { public void clearSchedule() {
this.examDay = -1; this.examDay = -1;
this.examTimeSlot = -1; this.examTimeSlot = -1;
this.assignedClassroom = null; this.assignedClassroom = null;
this.examDateTime = null;
} }
/**
* Sets the complete exam schedule.
*
* @param day Day index (0-based)
* @param timeSlot Time slot index (0-based)
* @param classroomId Classroom ID
*/
public void setExamSchedule(int day, int timeSlot, String classroomId) { public void setExamSchedule(int day, int timeSlot, String classroomId) {
this.examDay = day; this.examDay = day;
this.examTimeSlot = timeSlot; this.examTimeSlot = timeSlot;
this.assignedClassroom = classroomId; this.assignedClassroom = classroomId;
} }
/**
* Gets a unique key for this course's time slot.
*/
public String getTimeSlotKey() { public String getTimeSlotKey() {
if (!isScheduled()) { return isScheduled() ? "D" + examDay + "_S" + examTimeSlot : null;
return null;
}
return "D" + examDay + "_S" + examTimeSlot;
} }
// Basic getters and setters
public String getCourseCode() { public String getCourseCode() {
return courseCode; return courseCode;
} }
@@ -82,14 +50,13 @@ public class Course {
return enrolledStudents; return enrolledStudents;
} }
public void setEnrolledStudents(List<String> enrolledStudents) { public void setEnrolledStudents(List<String> students) {
this.enrolledStudents = enrolledStudents; this.enrolledStudents = students;
} }
public void addStudent(String studentId) { public void addStudent(String studentId) {
if (!enrolledStudents.contains(studentId)) { if (!enrolledStudents.contains(studentId))
enrolledStudents.add(studentId); enrolledStudents.add(studentId);
}
} }
public void removeStudent(String studentId) { public void removeStudent(String studentId) {
@@ -100,8 +67,6 @@ public class Course {
return enrolledStudents.size(); return enrolledStudents.size();
} }
// Schedule field getters and setters
public int getExamDay() { public int getExamDay() {
return examDay; return examDay;
} }
@@ -114,24 +79,16 @@ public class Course {
return examTimeSlot; return examTimeSlot;
} }
public void setExamTimeSlot(int examTimeSlot) { public void setExamTimeSlot(int slot) {
this.examTimeSlot = examTimeSlot; this.examTimeSlot = slot;
} }
public String getAssignedClassroom() { public String getAssignedClassroom() {
return assignedClassroom; return assignedClassroom;
} }
public void setAssignedClassroom(String assignedClassroom) { public void setAssignedClassroom(String classroom) {
this.assignedClassroom = assignedClassroom; this.assignedClassroom = classroom;
}
public String getExamDateTime() {
return examDateTime;
}
public void setExamDateTime(String examDateTime) {
this.examDateTime = examDateTime;
} }
@Override @Override

View File

@@ -123,7 +123,7 @@
<HBox spacing="12" alignment="CENTER_LEFT"> <HBox spacing="12" alignment="CENTER_LEFT">
<Label text="📆 Generated Schedule" styleClass="subsection-title"/> <Label text="📆 Generated Schedule" styleClass="subsection-title"/>
<Region HBox.hgrow="ALWAYS"/> <Region HBox.hgrow="ALWAYS"/>
<Button text="📥 Export PDF" styleClass="small-button"/> <Button fx:id="exportButton" text="📥 Export CSV" onAction="#onExportCSV" styleClass="small-button"/>
</HBox> </HBox>
<!-- Schedule Grid --> <!-- Schedule Grid -->

View File

@@ -19,6 +19,7 @@
<Label text="Select Classroom:"/> <Label text="Select Classroom:"/>
<ComboBox fx:id="classroomComboBox" promptText="Choose a classroom..." prefWidth="200"/> <ComboBox fx:id="classroomComboBox" promptText="Choose a classroom..." prefWidth="200"/>
<Button text="Show Schedule" onAction="#onShowSchedule" styleClass="primary-button"/> <Button text="Show Schedule" onAction="#onShowSchedule" styleClass="primary-button"/>
<Button text="📥 Export CSV" onAction="#onExportCSV"/>
</HBox> </HBox>
</VBox> </VBox>
</top> </top>

View File

@@ -14,7 +14,11 @@
<padding> <padding>
<Insets top="20" right="20" bottom="20" left="20"/> <Insets top="20" right="20" bottom="20" left="20"/>
</padding> </padding>
<Label text="Course Schedule View" styleClass="section-title"/> <HBox spacing="10" alignment="CENTER_LEFT">
<Label text="Course Schedule View" styleClass="section-title"/>
<Region HBox.hgrow="ALWAYS"/>
<Button text="📥 Export CSV" onAction="#onExportCSV"/>
</HBox>
<Label text="All courses and their exam assignments" wrapText="true"/> <Label text="All courses and their exam assignments" wrapText="true"/>
<Separator/> <Separator/>
<TableView fx:id="courseScheduleTable" VBox.vgrow="ALWAYS"> <TableView fx:id="courseScheduleTable" VBox.vgrow="ALWAYS">

View File

@@ -19,6 +19,7 @@
<Label text="Select Student:"/> <Label text="Select Student:"/>
<ComboBox fx:id="studentComboBox" promptText="Choose a student..." prefWidth="200"/> <ComboBox fx:id="studentComboBox" promptText="Choose a student..." prefWidth="200"/>
<Button text="Show Schedule" onAction="#onShowSchedule" styleClass="primary-button"/> <Button text="Show Schedule" onAction="#onShowSchedule" styleClass="primary-button"/>
<Button text="📥 Export CSV" onAction="#onExportCSV"/>
</HBox> </HBox>
</VBox> </VBox>
</top> </top>