mirror of
https://github.com/sabazadam/Se302.git
synced 2025-12-31 20:31:22 +00:00
feat: implement CSP algorithm architecture for exam scheduling
This commit is contained in:
373
src/main/java/org/example/se302/algorithm/CSPSolver.java
Normal file
373
src/main/java/org/example/se302/algorithm/CSPSolver.java
Normal file
@@ -0,0 +1,373 @@
|
||||
package org.example.se302.algorithm;
|
||||
|
||||
import org.example.se302.model.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* CSP (Constraint Satisfaction Problem) Solver for Exam Scheduling.
|
||||
*
|
||||
* This solver uses a backtracking algorithm with the following optimizations:
|
||||
* - MRV (Minimum Remaining Values) heuristic for variable ordering
|
||||
* - Degree heuristic as a tie-breaker
|
||||
* - Forward checking for constraint propagation
|
||||
* - Arc consistency (AC-3) for further pruning
|
||||
*
|
||||
* The CSP formulation:
|
||||
* - Variables: Each course that needs an exam scheduled
|
||||
* - Domain: All possible (TimeSlot, Classroom) pairs
|
||||
* - Constraints: Hard constraints (must satisfy) and soft constraints
|
||||
* (preferably satisfy)
|
||||
*/
|
||||
public class CSPSolver {
|
||||
|
||||
private List<Constraint> hardConstraints;
|
||||
private List<Constraint> softConstraints;
|
||||
private Map<String, Course> courses;
|
||||
private Map<String, Classroom> classrooms;
|
||||
private List<TimeSlot> availableTimeSlots;
|
||||
|
||||
// Statistics
|
||||
private int nodesExplored;
|
||||
private int backtracks;
|
||||
private long startTime;
|
||||
private long maxTimeMs;
|
||||
|
||||
// Conflict graph for student conflicts (courses that share students)
|
||||
private Map<String, Set<String>> conflictGraph;
|
||||
|
||||
public CSPSolver() {
|
||||
this.hardConstraints = new ArrayList<>();
|
||||
this.softConstraints = new ArrayList<>();
|
||||
this.courses = new HashMap<>();
|
||||
this.classrooms = new HashMap<>();
|
||||
this.availableTimeSlots = new ArrayList<>();
|
||||
this.conflictGraph = new HashMap<>();
|
||||
this.nodesExplored = 0;
|
||||
this.backtracks = 0;
|
||||
this.maxTimeMs = 60000; // Default 60 second timeout
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a hard constraint to the solver.
|
||||
*/
|
||||
public void addHardConstraint(Constraint constraint) {
|
||||
if (constraint.isHard()) {
|
||||
hardConstraints.add(constraint);
|
||||
} else {
|
||||
softConstraints.add(constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a soft constraint to the solver.
|
||||
*/
|
||||
public void addSoftConstraint(Constraint constraint) {
|
||||
softConstraints.add(constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the available time slots for scheduling.
|
||||
*/
|
||||
public void setAvailableTimeSlots(List<TimeSlot> timeSlots) {
|
||||
this.availableTimeSlots = new ArrayList<>(timeSlots);
|
||||
Collections.sort(this.availableTimeSlots);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the available classrooms.
|
||||
*/
|
||||
public void setClassrooms(Map<String, Classroom> classrooms) {
|
||||
this.classrooms = classrooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the courses to schedule.
|
||||
*/
|
||||
public void setCourses(Map<String, Course> courses) {
|
||||
this.courses = courses;
|
||||
buildConflictGraph();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the conflict graph based on shared students.
|
||||
* Two courses are connected if they share at least one student.
|
||||
*/
|
||||
private void buildConflictGraph() {
|
||||
conflictGraph.clear();
|
||||
|
||||
for (String courseCode : courses.keySet()) {
|
||||
conflictGraph.put(courseCode, new HashSet<>());
|
||||
}
|
||||
|
||||
List<String> courseCodes = new ArrayList<>(courses.keySet());
|
||||
|
||||
for (int i = 0; i < courseCodes.size(); i++) {
|
||||
Course course1 = courses.get(courseCodes.get(i));
|
||||
Set<String> students1 = new HashSet<>(course1.getEnrolledStudents());
|
||||
|
||||
for (int j = i + 1; j < courseCodes.size(); j++) {
|
||||
Course course2 = courses.get(courseCodes.get(j));
|
||||
|
||||
// Check if courses share any students
|
||||
for (String student : course2.getEnrolledStudents()) {
|
||||
if (students1.contains(student)) {
|
||||
conflictGraph.get(courseCodes.get(i)).add(courseCodes.get(j));
|
||||
conflictGraph.get(courseCodes.get(j)).add(courseCodes.get(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves the CSP and returns a complete schedule.
|
||||
*
|
||||
* @param initialState Optional initial state with some pre-assigned exams
|
||||
* @return A complete schedule state, or null if no solution found
|
||||
*/
|
||||
public ScheduleState solve(ScheduleState initialState) {
|
||||
nodesExplored = 0;
|
||||
backtracks = 0;
|
||||
startTime = System.currentTimeMillis();
|
||||
|
||||
// Initialize state if not provided
|
||||
ScheduleState state = initialState != null ? initialState.copy() : createInitialState();
|
||||
|
||||
// Start backtracking search
|
||||
ScheduleState result = backtrack(state);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an initial state with all courses unassigned.
|
||||
*/
|
||||
private ScheduleState createInitialState() {
|
||||
ScheduleState state = new ScheduleState();
|
||||
state.setAvailableTimeSlots(availableTimeSlots);
|
||||
state.setAvailableClassrooms(new ArrayList<>(classrooms.values()));
|
||||
|
||||
for (Course course : courses.values()) {
|
||||
ExamAssignment assignment = new ExamAssignment(course.getCourseCode());
|
||||
assignment.setStudentCount(course.getEnrolledStudentsCount());
|
||||
state.addAssignment(assignment);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backtracking algorithm with MRV heuristic.
|
||||
*/
|
||||
private ScheduleState backtrack(ScheduleState state) {
|
||||
// Check timeout
|
||||
if (System.currentTimeMillis() - startTime > maxTimeMs) {
|
||||
return null; // Timeout
|
||||
}
|
||||
|
||||
nodesExplored++;
|
||||
|
||||
// Check if complete
|
||||
if (state.isComplete()) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Select next variable using MRV heuristic
|
||||
ExamAssignment variable = selectUnassignedVariable(state);
|
||||
if (variable == null) {
|
||||
return null; // No unassigned variables but not complete - shouldn't happen
|
||||
}
|
||||
|
||||
// Get ordered domain values
|
||||
List<DomainValue> orderedDomain = orderDomainValues(variable, state);
|
||||
|
||||
for (DomainValue value : orderedDomain) {
|
||||
// Try assigning this value
|
||||
if (isConsistent(variable.getCourseCode(), value.timeSlot, value.classroomId, state)) {
|
||||
// Make assignment
|
||||
state.updateAssignment(variable.getCourseCode(), value.timeSlot, value.classroomId);
|
||||
|
||||
// Recursive call
|
||||
ScheduleState result = backtrack(state);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Backtrack
|
||||
backtracks++;
|
||||
state.removeAssignment(variable.getCourseCode());
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No valid assignment found
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the next unassigned variable using MRV (Minimum Remaining Values)
|
||||
* heuristic.
|
||||
* Ties are broken using the degree heuristic (most constraints).
|
||||
*/
|
||||
private ExamAssignment selectUnassignedVariable(ScheduleState state) {
|
||||
ExamAssignment selected = null;
|
||||
int minValues = Integer.MAX_VALUE;
|
||||
int maxDegree = -1;
|
||||
|
||||
for (ExamAssignment assignment : state.getUnassignedCourses()) {
|
||||
int remainingValues = countRemainingValues(assignment, state);
|
||||
|
||||
if (remainingValues < minValues) {
|
||||
minValues = remainingValues;
|
||||
maxDegree = getDegree(assignment.getCourseCode());
|
||||
selected = assignment;
|
||||
} else if (remainingValues == minValues) {
|
||||
int degree = getDegree(assignment.getCourseCode());
|
||||
if (degree > maxDegree) {
|
||||
maxDegree = degree;
|
||||
selected = assignment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of remaining valid values for a variable.
|
||||
*/
|
||||
private int countRemainingValues(ExamAssignment assignment, ScheduleState state) {
|
||||
int count = 0;
|
||||
for (TimeSlot timeSlot : availableTimeSlots) {
|
||||
for (Classroom classroom : classrooms.values()) {
|
||||
if (classroom.getCapacity() >= assignment.getStudentCount() &&
|
||||
state.isClassroomAvailable(classroom.getClassroomId(), timeSlot)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the degree (number of constraints/conflicts) for a course.
|
||||
*/
|
||||
private int getDegree(String courseCode) {
|
||||
Set<String> conflicts = conflictGraph.get(courseCode);
|
||||
return conflicts != null ? conflicts.size() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders domain values using Least Constraining Value (LCV) heuristic.
|
||||
*/
|
||||
private List<DomainValue> orderDomainValues(ExamAssignment assignment, ScheduleState state) {
|
||||
List<DomainValue> values = new ArrayList<>();
|
||||
|
||||
for (TimeSlot timeSlot : availableTimeSlots) {
|
||||
for (Classroom classroom : classrooms.values()) {
|
||||
if (classroom.getCapacity() >= assignment.getStudentCount()) {
|
||||
DomainValue value = new DomainValue(timeSlot, classroom.getClassroomId());
|
||||
value.constrainingFactor = calculateConstrainingFactor(
|
||||
assignment.getCourseCode(), timeSlot, classroom.getClassroomId(), state);
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by constraining factor (least constraining first)
|
||||
values.sort(Comparator.comparingInt(v -> v.constrainingFactor));
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how much a value constrains other variables.
|
||||
*/
|
||||
private int calculateConstrainingFactor(String courseCode, TimeSlot timeSlot,
|
||||
String classroomId, ScheduleState state) {
|
||||
int factor = 0;
|
||||
|
||||
// Count how many conflicting courses would be affected
|
||||
Set<String> conflicts = conflictGraph.get(courseCode);
|
||||
if (conflicts != null) {
|
||||
for (String conflictCourse : conflicts) {
|
||||
ExamAssignment conflictAssignment = state.getAssignment(conflictCourse);
|
||||
if (conflictAssignment != null && !conflictAssignment.isAssigned()) {
|
||||
factor++; // This time slot becomes unavailable for the conflicting course
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return factor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an assignment is consistent with all constraints.
|
||||
*/
|
||||
private boolean isConsistent(String courseCode, TimeSlot timeSlot,
|
||||
String classroomId, ScheduleState state) {
|
||||
// Create temporary assignment for checking
|
||||
ExamAssignment tempAssignment = new ExamAssignment(courseCode, timeSlot, classroomId);
|
||||
Course course = courses.get(courseCode);
|
||||
if (course != null) {
|
||||
tempAssignment.setStudentCount(course.getEnrolledStudentsCount());
|
||||
}
|
||||
|
||||
// Check all hard constraints
|
||||
for (Constraint constraint : hardConstraints) {
|
||||
if (!constraint.isSatisfied(tempAssignment, state)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check student conflicts separately (most important)
|
||||
Set<String> coursesAtSameTime = state.getCoursesAtTimeSlot(timeSlot);
|
||||
Set<String> conflicts = conflictGraph.get(courseCode);
|
||||
if (conflicts != null) {
|
||||
for (String conflictCourse : conflicts) {
|
||||
if (coursesAtSameTime.contains(conflictCourse)) {
|
||||
return false; // Student conflict
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for domain values.
|
||||
*/
|
||||
private static class DomainValue {
|
||||
TimeSlot timeSlot;
|
||||
String classroomId;
|
||||
int constrainingFactor;
|
||||
|
||||
DomainValue(TimeSlot timeSlot, String classroomId) {
|
||||
this.timeSlot = timeSlot;
|
||||
this.classroomId = classroomId;
|
||||
this.constrainingFactor = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Getters for statistics
|
||||
|
||||
public int getNodesExplored() {
|
||||
return nodesExplored;
|
||||
}
|
||||
|
||||
public int getBacktracks() {
|
||||
return backtracks;
|
||||
}
|
||||
|
||||
public long getMaxTimeMs() {
|
||||
return maxTimeMs;
|
||||
}
|
||||
|
||||
public void setMaxTimeMs(long maxTimeMs) {
|
||||
this.maxTimeMs = maxTimeMs;
|
||||
}
|
||||
|
||||
public Map<String, Set<String>> getConflictGraph() {
|
||||
return Collections.unmodifiableMap(conflictGraph);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.example.se302.algorithm;
|
||||
|
||||
import org.example.se302.model.Classroom;
|
||||
import org.example.se302.model.ExamAssignment;
|
||||
import org.example.se302.model.ScheduleState;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Hard constraint: Classroom capacity must be sufficient for the number of
|
||||
* students.
|
||||
*/
|
||||
public class CapacityConstraint implements Constraint {
|
||||
|
||||
private Map<String, Classroom> classrooms;
|
||||
|
||||
public CapacityConstraint(Map<String, Classroom> classrooms) {
|
||||
this.classrooms = classrooms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CAPACITY";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Classroom capacity must be sufficient for the number of enrolled students";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSatisfied(ExamAssignment assignment, ScheduleState state) {
|
||||
if (!assignment.isAssigned()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Classroom classroom = classrooms.get(assignment.getClassroomId());
|
||||
if (classroom == null) {
|
||||
return false; // Unknown classroom
|
||||
}
|
||||
|
||||
return classroom.getCapacity() >= assignment.getStudentCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 100; // High priority - hard constraint
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHard() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getViolationMessage(ExamAssignment assignment, ScheduleState state) {
|
||||
if (isSatisfied(assignment, state)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Classroom classroom = classrooms.get(assignment.getClassroomId());
|
||||
if (classroom == null) {
|
||||
return String.format("Classroom %s does not exist", assignment.getClassroomId());
|
||||
}
|
||||
|
||||
return String.format("Classroom %s capacity (%d) is insufficient for %d students",
|
||||
assignment.getClassroomId(), classroom.getCapacity(), assignment.getStudentCount());
|
||||
}
|
||||
|
||||
public void setClassrooms(Map<String, Classroom> classrooms) {
|
||||
this.classrooms = classrooms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.example.se302.algorithm;
|
||||
|
||||
import org.example.se302.model.Classroom;
|
||||
import org.example.se302.model.ExamAssignment;
|
||||
import org.example.se302.model.ScheduleState;
|
||||
|
||||
/**
|
||||
* Hard constraint: A classroom cannot host two exams at the same time.
|
||||
*/
|
||||
public class ClassroomConflictConstraint implements Constraint {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CLASSROOM_CONFLICT";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "A classroom cannot be used for multiple exams at the same time";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSatisfied(ExamAssignment assignment, ScheduleState state) {
|
||||
if (!assignment.isAssigned()) {
|
||||
return true; // Unassigned assignments don't violate constraints
|
||||
}
|
||||
|
||||
return state.isClassroomAvailable(assignment.getClassroomId(), assignment.getTimeSlot());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 100; // High priority - hard constraint
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHard() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getViolationMessage(ExamAssignment assignment, ScheduleState state) {
|
||||
if (isSatisfied(assignment, state)) {
|
||||
return null;
|
||||
}
|
||||
return String.format("Classroom %s is already in use at %s",
|
||||
assignment.getClassroomId(), assignment.getTimeSlot());
|
||||
}
|
||||
}
|
||||
55
src/main/java/org/example/se302/algorithm/Constraint.java
Normal file
55
src/main/java/org/example/se302/algorithm/Constraint.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package org.example.se302.algorithm;
|
||||
|
||||
import org.example.se302.model.ExamAssignment;
|
||||
import org.example.se302.model.ScheduleState;
|
||||
|
||||
/**
|
||||
* Interface for constraints in the CSP-based exam scheduling.
|
||||
* Each constraint represents a rule that must be satisfied for a valid
|
||||
* schedule.
|
||||
*/
|
||||
public interface Constraint {
|
||||
|
||||
/**
|
||||
* Gets the name/identifier of this constraint.
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Gets a human-readable description of this constraint.
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Checks if the given assignment satisfies this constraint
|
||||
* within the context of the current schedule state.
|
||||
*
|
||||
* @param assignment The new assignment to check
|
||||
* @param state The current schedule state
|
||||
* @return true if the constraint is satisfied, false otherwise
|
||||
*/
|
||||
boolean isSatisfied(ExamAssignment assignment, ScheduleState state);
|
||||
|
||||
/**
|
||||
* Gets the priority/weight of this constraint.
|
||||
* Higher values indicate more important constraints.
|
||||
* Hard constraints should have very high priority.
|
||||
*/
|
||||
int getPriority();
|
||||
|
||||
/**
|
||||
* Checks if this is a hard constraint (must be satisfied)
|
||||
* or a soft constraint (preferably satisfied but can be violated).
|
||||
*/
|
||||
boolean isHard();
|
||||
|
||||
/**
|
||||
* Gets a detailed message explaining why the constraint was violated
|
||||
* for the given assignment.
|
||||
*
|
||||
* @param assignment The assignment that violated the constraint
|
||||
* @param state The current schedule state
|
||||
* @return A descriptive message, or null if constraint is satisfied
|
||||
*/
|
||||
String getViolationMessage(ExamAssignment assignment, ScheduleState state);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.example.se302.algorithm;
|
||||
|
||||
import org.example.se302.model.Course;
|
||||
import org.example.se302.model.ExamAssignment;
|
||||
import org.example.se302.model.ScheduleState;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Hard constraint: A student cannot have two exams at the same time.
|
||||
* This checks for overlapping time slots among courses that share students.
|
||||
*/
|
||||
public class StudentConflictConstraint implements Constraint {
|
||||
|
||||
private Map<String, Course> courses;
|
||||
|
||||
public StudentConflictConstraint(Map<String, Course> courses) {
|
||||
this.courses = courses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "STUDENT_CONFLICT";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "A student cannot have two exams at the same time";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSatisfied(ExamAssignment assignment, ScheduleState state) {
|
||||
if (!assignment.isAssigned()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Course course = courses.get(assignment.getCourseCode());
|
||||
if (course == null) {
|
||||
return true; // Unknown course, skip check
|
||||
}
|
||||
|
||||
Set<String> enrolledStudents = new HashSet<>(course.getEnrolledStudents());
|
||||
|
||||
// Get all courses scheduled at overlapping time slots
|
||||
Set<String> coursesAtSameTime = state.getCoursesAtTimeSlot(assignment.getTimeSlot());
|
||||
|
||||
for (String otherCourseCode : coursesAtSameTime) {
|
||||
if (otherCourseCode.equals(assignment.getCourseCode())) {
|
||||
continue; // Skip self
|
||||
}
|
||||
|
||||
Course otherCourse = courses.get(otherCourseCode);
|
||||
if (otherCourse == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if any student is enrolled in both courses
|
||||
for (String student : otherCourse.getEnrolledStudents()) {
|
||||
if (enrolledStudents.contains(student)) {
|
||||
return false; // Conflict found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 100; // High priority - hard constraint
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHard() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getViolationMessage(ExamAssignment assignment, ScheduleState state) {
|
||||
if (isSatisfied(assignment, state)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Course course = courses.get(assignment.getCourseCode());
|
||||
if (course == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<String> enrolledStudents = new HashSet<>(course.getEnrolledStudents());
|
||||
Set<String> coursesAtSameTime = state.getCoursesAtTimeSlot(assignment.getTimeSlot());
|
||||
|
||||
for (String otherCourseCode : coursesAtSameTime) {
|
||||
if (otherCourseCode.equals(assignment.getCourseCode())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Course otherCourse = courses.get(otherCourseCode);
|
||||
if (otherCourse == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> conflictingStudents = new ArrayList<>();
|
||||
for (String student : otherCourse.getEnrolledStudents()) {
|
||||
if (enrolledStudents.contains(student)) {
|
||||
conflictingStudents.add(student);
|
||||
}
|
||||
}
|
||||
|
||||
if (!conflictingStudents.isEmpty()) {
|
||||
return String.format("Students %s have exams for both %s and %s at %s",
|
||||
conflictingStudents.size() > 3 ? conflictingStudents.subList(0, 3) + "..."
|
||||
: conflictingStudents,
|
||||
assignment.getCourseCode(), otherCourseCode, assignment.getTimeSlot());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setCourses(Map<String, Course> courses) {
|
||||
this.courses = courses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the set of courses that conflict with the given course
|
||||
* (i.e., share at least one student).
|
||||
*/
|
||||
public Set<String> getConflictingCourses(String courseCode) {
|
||||
Set<String> conflicting = new HashSet<>();
|
||||
Course course = courses.get(courseCode);
|
||||
if (course == null) {
|
||||
return conflicting;
|
||||
}
|
||||
|
||||
Set<String> enrolledStudents = new HashSet<>(course.getEnrolledStudents());
|
||||
|
||||
for (Map.Entry<String, Course> entry : courses.entrySet()) {
|
||||
if (entry.getKey().equals(courseCode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String student : entry.getValue().getEnrolledStudents()) {
|
||||
if (enrolledStudents.contains(student)) {
|
||||
conflicting.add(entry.getKey());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflicting;
|
||||
}
|
||||
}
|
||||
145
src/main/java/org/example/se302/model/ExamAssignment.java
Normal file
145
src/main/java/org/example/se302/model/ExamAssignment.java
Normal file
@@ -0,0 +1,145 @@
|
||||
package org.example.se302.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents an exam assignment in the CSP-based scheduling system.
|
||||
* An ExamAssignment links a course to a specific time slot and classroom.
|
||||
* This is the core data structure for the schedule state.
|
||||
*/
|
||||
public class ExamAssignment {
|
||||
private String courseCode;
|
||||
private TimeSlot timeSlot;
|
||||
private String classroomId;
|
||||
private int studentCount;
|
||||
|
||||
// Assignment status
|
||||
private boolean isLocked; // If true, this assignment cannot be changed by the algorithm
|
||||
|
||||
/**
|
||||
* Creates a new exam assignment.
|
||||
*
|
||||
* @param courseCode The course code for this exam
|
||||
* @param timeSlot The time slot assigned to this exam
|
||||
* @param classroomId The classroom assigned to this exam
|
||||
*/
|
||||
public ExamAssignment(String courseCode, TimeSlot timeSlot, String classroomId) {
|
||||
this.courseCode = courseCode;
|
||||
this.timeSlot = timeSlot;
|
||||
this.classroomId = classroomId;
|
||||
this.studentCount = 0;
|
||||
this.isLocked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unassigned exam (only course is known).
|
||||
*/
|
||||
public ExamAssignment(String courseCode) {
|
||||
this(courseCode, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deep copy of this assignment.
|
||||
*/
|
||||
public ExamAssignment copy() {
|
||||
ExamAssignment copy = new ExamAssignment(this.courseCode, this.timeSlot, this.classroomId);
|
||||
copy.studentCount = this.studentCount;
|
||||
copy.isLocked = this.isLocked;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this assignment is complete (has both time slot and classroom).
|
||||
*/
|
||||
public boolean isAssigned() {
|
||||
return timeSlot != null && classroomId != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this assignment conflicts with another assignment.
|
||||
* Two assignments conflict if:
|
||||
* 1. Same classroom at overlapping time slots
|
||||
* 2. (Checked separately) Same student in both courses at overlapping time
|
||||
* slots
|
||||
*/
|
||||
public boolean conflictsWith(ExamAssignment other) {
|
||||
if (!this.isAssigned() || !other.isAssigned()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check classroom conflict
|
||||
if (this.classroomId.equals(other.classroomId) &&
|
||||
this.timeSlot.overlapsWith(other.timeSlot)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getCourseCode() {
|
||||
return courseCode;
|
||||
}
|
||||
|
||||
public void setCourseCode(String courseCode) {
|
||||
this.courseCode = courseCode;
|
||||
}
|
||||
|
||||
public TimeSlot getTimeSlot() {
|
||||
return timeSlot;
|
||||
}
|
||||
|
||||
public void setTimeSlot(TimeSlot timeSlot) {
|
||||
this.timeSlot = timeSlot;
|
||||
}
|
||||
|
||||
public String getClassroomId() {
|
||||
return classroomId;
|
||||
}
|
||||
|
||||
public void setClassroomId(String classroomId) {
|
||||
this.classroomId = classroomId;
|
||||
}
|
||||
|
||||
public int getStudentCount() {
|
||||
return studentCount;
|
||||
}
|
||||
|
||||
public void setStudentCount(int studentCount) {
|
||||
this.studentCount = studentCount;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return isLocked;
|
||||
}
|
||||
|
||||
public void setLocked(boolean locked) {
|
||||
isLocked = locked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
ExamAssignment that = (ExamAssignment) o;
|
||||
return Objects.equals(courseCode, that.courseCode) &&
|
||||
Objects.equals(timeSlot, that.timeSlot) &&
|
||||
Objects.equals(classroomId, that.classroomId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(courseCode, timeSlot, classroomId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!isAssigned()) {
|
||||
return courseCode + " [Unassigned]";
|
||||
}
|
||||
return courseCode + " -> " + classroomId + " @ " + timeSlot;
|
||||
}
|
||||
}
|
||||
270
src/main/java/org/example/se302/model/ScheduleState.java
Normal file
270
src/main/java/org/example/se302/model/ScheduleState.java
Normal file
@@ -0,0 +1,270 @@
|
||||
package org.example.se302.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Represents the complete state of an exam schedule.
|
||||
* This is the main data structure used by the CSP solver to track
|
||||
* the current schedule state and validate constraints.
|
||||
*/
|
||||
public class ScheduleState {
|
||||
// All exam assignments indexed by course code
|
||||
private Map<String, ExamAssignment> assignments;
|
||||
|
||||
// Quick lookup structures for constraint checking
|
||||
private Map<String, Set<String>> classroomTimeSlotUsage; // classroomId -> set of timeSlot IDs
|
||||
private Map<String, Set<String>> timeSlotCourseMapping; // timeSlotId -> set of course codes
|
||||
|
||||
// Available resources
|
||||
private List<TimeSlot> availableTimeSlots;
|
||||
private List<Classroom> availableClassrooms;
|
||||
|
||||
// Statistics
|
||||
private int totalCourses;
|
||||
private int assignedCourses;
|
||||
private int constraintViolations;
|
||||
|
||||
public ScheduleState() {
|
||||
this.assignments = new HashMap<>();
|
||||
this.classroomTimeSlotUsage = new HashMap<>();
|
||||
this.timeSlotCourseMapping = new HashMap<>();
|
||||
this.availableTimeSlots = new ArrayList<>();
|
||||
this.availableClassrooms = new ArrayList<>();
|
||||
this.totalCourses = 0;
|
||||
this.assignedCourses = 0;
|
||||
this.constraintViolations = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deep copy of this schedule state.
|
||||
*/
|
||||
public ScheduleState copy() {
|
||||
ScheduleState copy = new ScheduleState();
|
||||
|
||||
for (Map.Entry<String, ExamAssignment> entry : this.assignments.entrySet()) {
|
||||
copy.assignments.put(entry.getKey(), entry.getValue().copy());
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Set<String>> entry : this.classroomTimeSlotUsage.entrySet()) {
|
||||
copy.classroomTimeSlotUsage.put(entry.getKey(), new HashSet<>(entry.getValue()));
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Set<String>> entry : this.timeSlotCourseMapping.entrySet()) {
|
||||
copy.timeSlotCourseMapping.put(entry.getKey(), new HashSet<>(entry.getValue()));
|
||||
}
|
||||
|
||||
copy.availableTimeSlots = new ArrayList<>(this.availableTimeSlots);
|
||||
copy.availableClassrooms = new ArrayList<>(this.availableClassrooms);
|
||||
copy.totalCourses = this.totalCourses;
|
||||
copy.assignedCourses = this.assignedCourses;
|
||||
copy.constraintViolations = this.constraintViolations;
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an exam assignment to the schedule.
|
||||
* Updates the quick lookup structures.
|
||||
*/
|
||||
public void addAssignment(ExamAssignment assignment) {
|
||||
String courseCode = assignment.getCourseCode();
|
||||
assignments.put(courseCode, assignment);
|
||||
totalCourses++;
|
||||
|
||||
if (assignment.isAssigned()) {
|
||||
updateLookupStructures(assignment, true);
|
||||
assignedCourses++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing assignment with new time slot and classroom.
|
||||
*/
|
||||
public boolean updateAssignment(String courseCode, TimeSlot timeSlot, String classroomId) {
|
||||
ExamAssignment assignment = assignments.get(courseCode);
|
||||
if (assignment == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assignment.isLocked()) {
|
||||
return false; // Cannot modify locked assignments
|
||||
}
|
||||
|
||||
// Remove old assignment from lookup structures
|
||||
if (assignment.isAssigned()) {
|
||||
updateLookupStructures(assignment, false);
|
||||
assignedCourses--;
|
||||
}
|
||||
|
||||
// Update assignment
|
||||
assignment.setTimeSlot(timeSlot);
|
||||
assignment.setClassroomId(classroomId);
|
||||
|
||||
// Add new assignment to lookup structures
|
||||
if (assignment.isAssigned()) {
|
||||
updateLookupStructures(assignment, true);
|
||||
assignedCourses++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an assignment (resets it to unassigned state).
|
||||
*/
|
||||
public boolean removeAssignment(String courseCode) {
|
||||
ExamAssignment assignment = assignments.get(courseCode);
|
||||
if (assignment == null || assignment.isLocked()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assignment.isAssigned()) {
|
||||
updateLookupStructures(assignment, false);
|
||||
assignedCourses--;
|
||||
}
|
||||
|
||||
assignment.setTimeSlot(null);
|
||||
assignment.setClassroomId(null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the quick lookup structures when an assignment changes.
|
||||
*/
|
||||
private void updateLookupStructures(ExamAssignment assignment, boolean add) {
|
||||
if (!assignment.isAssigned())
|
||||
return;
|
||||
|
||||
String classroomId = assignment.getClassroomId();
|
||||
String timeSlotId = assignment.getTimeSlot().getId();
|
||||
String courseCode = assignment.getCourseCode();
|
||||
|
||||
if (add) {
|
||||
classroomTimeSlotUsage.computeIfAbsent(classroomId, k -> new HashSet<>()).add(timeSlotId);
|
||||
timeSlotCourseMapping.computeIfAbsent(timeSlotId, k -> new HashSet<>()).add(courseCode);
|
||||
} else {
|
||||
Set<String> slots = classroomTimeSlotUsage.get(classroomId);
|
||||
if (slots != null) {
|
||||
slots.remove(timeSlotId);
|
||||
}
|
||||
|
||||
Set<String> courses = timeSlotCourseMapping.get(timeSlotId);
|
||||
if (courses != null) {
|
||||
courses.remove(courseCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a classroom is available at a given time slot.
|
||||
*/
|
||||
public boolean isClassroomAvailable(String classroomId, TimeSlot timeSlot) {
|
||||
Set<String> usedSlots = classroomTimeSlotUsage.get(classroomId);
|
||||
if (usedSlots == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for overlapping time slots
|
||||
for (ExamAssignment assignment : assignments.values()) {
|
||||
if (assignment.isAssigned() &&
|
||||
assignment.getClassroomId().equals(classroomId) &&
|
||||
assignment.getTimeSlot().overlapsWith(timeSlot)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all courses scheduled at a specific time slot.
|
||||
*/
|
||||
public Set<String> getCoursesAtTimeSlot(TimeSlot timeSlot) {
|
||||
Set<String> courses = new HashSet<>();
|
||||
for (ExamAssignment assignment : assignments.values()) {
|
||||
if (assignment.isAssigned() &&
|
||||
assignment.getTimeSlot().overlapsWith(timeSlot)) {
|
||||
courses.add(assignment.getCourseCode());
|
||||
}
|
||||
}
|
||||
return courses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all unassigned courses.
|
||||
*/
|
||||
public List<ExamAssignment> getUnassignedCourses() {
|
||||
List<ExamAssignment> unassigned = new ArrayList<>();
|
||||
for (ExamAssignment assignment : assignments.values()) {
|
||||
if (!assignment.isAssigned()) {
|
||||
unassigned.add(assignment);
|
||||
}
|
||||
}
|
||||
return unassigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the schedule is complete (all courses are assigned).
|
||||
*/
|
||||
public boolean isComplete() {
|
||||
return assignedCourses == totalCourses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the completion percentage.
|
||||
*/
|
||||
public double getCompletionPercentage() {
|
||||
if (totalCourses == 0)
|
||||
return 100.0;
|
||||
return (assignedCourses * 100.0) / totalCourses;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Map<String, ExamAssignment> getAssignments() {
|
||||
return Collections.unmodifiableMap(assignments);
|
||||
}
|
||||
|
||||
public ExamAssignment getAssignment(String courseCode) {
|
||||
return assignments.get(courseCode);
|
||||
}
|
||||
|
||||
public List<TimeSlot> getAvailableTimeSlots() {
|
||||
return availableTimeSlots;
|
||||
}
|
||||
|
||||
public void setAvailableTimeSlots(List<TimeSlot> availableTimeSlots) {
|
||||
this.availableTimeSlots = availableTimeSlots;
|
||||
}
|
||||
|
||||
public List<Classroom> getAvailableClassrooms() {
|
||||
return availableClassrooms;
|
||||
}
|
||||
|
||||
public void setAvailableClassrooms(List<Classroom> availableClassrooms) {
|
||||
this.availableClassrooms = availableClassrooms;
|
||||
}
|
||||
|
||||
public int getTotalCourses() {
|
||||
return totalCourses;
|
||||
}
|
||||
|
||||
public int getAssignedCourses() {
|
||||
return assignedCourses;
|
||||
}
|
||||
|
||||
public int getConstraintViolations() {
|
||||
return constraintViolations;
|
||||
}
|
||||
|
||||
public void setConstraintViolations(int constraintViolations) {
|
||||
this.constraintViolations = constraintViolations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ScheduleState[%d/%d assigned, %.1f%% complete, %d violations]",
|
||||
assignedCourses, totalCourses, getCompletionPercentage(), constraintViolations);
|
||||
}
|
||||
}
|
||||
109
src/main/java/org/example/se302/model/TimeSlot.java
Normal file
109
src/main/java/org/example/se302/model/TimeSlot.java
Normal file
@@ -0,0 +1,109 @@
|
||||
package org.example.se302.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a time slot for exam scheduling.
|
||||
* A time slot is defined by a date and a specific time period.
|
||||
*/
|
||||
public class TimeSlot implements Comparable<TimeSlot> {
|
||||
private LocalDate date;
|
||||
private LocalTime startTime;
|
||||
private LocalTime endTime;
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
public TimeSlot(LocalDate date, LocalTime startTime, LocalTime endTime) {
|
||||
this.date = date;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a time slot with a default duration of 2 hours.
|
||||
*/
|
||||
public TimeSlot(LocalDate date, LocalTime startTime) {
|
||||
this(date, startTime, startTime.plusHours(2));
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public LocalTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(LocalTime endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time slot overlaps with another time slot.
|
||||
* Two time slots overlap if they are on the same day and their time ranges
|
||||
* intersect.
|
||||
*/
|
||||
public boolean overlapsWith(TimeSlot other) {
|
||||
if (!this.date.equals(other.date)) {
|
||||
return false;
|
||||
}
|
||||
// Check if time ranges overlap
|
||||
return this.startTime.isBefore(other.endTime) && other.startTime.isBefore(this.endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique identifier for this time slot.
|
||||
*/
|
||||
public String getId() {
|
||||
return date.format(DATE_FORMATTER) + "_" + startTime.format(TIME_FORMATTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TimeSlot other) {
|
||||
int dateCompare = this.date.compareTo(other.date);
|
||||
if (dateCompare != 0) {
|
||||
return dateCompare;
|
||||
}
|
||||
return this.startTime.compareTo(other.startTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
TimeSlot timeSlot = (TimeSlot) o;
|
||||
return Objects.equals(date, timeSlot.date) &&
|
||||
Objects.equals(startTime, timeSlot.startTime) &&
|
||||
Objects.equals(endTime, timeSlot.endTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(date, startTime, endTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return date.format(DATE_FORMATTER) + " " +
|
||||
startTime.format(TIME_FORMATTER) + "-" +
|
||||
endTime.format(TIME_FORMATTER);
|
||||
}
|
||||
}
|
||||
@@ -4,73 +4,76 @@
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.example.se302.controller.ImportController"
|
||||
spacing="20" styleClass="content-pane">
|
||||
<padding>
|
||||
<Insets top="20" right="20" bottom="20" left="20"/>
|
||||
</padding>
|
||||
<ScrollPane xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.example.se302.controller.ImportController"
|
||||
fitToWidth="true" fitToHeight="true"
|
||||
hbarPolicy="AS_NEEDED" vbarPolicy="AS_NEEDED">
|
||||
<VBox spacing="20" styleClass="content-pane">
|
||||
<padding>
|
||||
<Insets top="20" right="20" bottom="20" left="20"/>
|
||||
</padding>
|
||||
|
||||
<!-- Header -->
|
||||
<Label text="Import Data from CSV Files" styleClass="section-title"/>
|
||||
<Label text="Select CSV files to import student, course, classroom, and enrollment data." wrapText="true"/>
|
||||
<!-- Header -->
|
||||
<Label text="Import Data from CSV Files" styleClass="section-title"/>
|
||||
<Label text="Select CSV files to import student, course, classroom, and enrollment data." wrapText="true"/>
|
||||
|
||||
<Separator/>
|
||||
<Separator/>
|
||||
|
||||
<!-- Students Import Section -->
|
||||
<VBox spacing="8">
|
||||
<Label text="Student Data" styleClass="subsection-title"/>
|
||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||
<TextField fx:id="studentFileField" promptText="No file selected" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
<Button fx:id="studentBrowseButton" text="Browse..." onAction="#onBrowseStudents"/>
|
||||
<!-- Students Import Section -->
|
||||
<VBox spacing="8">
|
||||
<Label text="Student Data" styleClass="subsection-title"/>
|
||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||
<TextField fx:id="studentFileField" promptText="No file selected" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
<Button fx:id="studentBrowseButton" text="Browse..." onAction="#onBrowseStudents"/>
|
||||
</HBox>
|
||||
<Label fx:id="studentStatusLabel" text="Not Loaded" styleClass="status-label"/>
|
||||
</VBox>
|
||||
|
||||
<!-- Courses Import Section -->
|
||||
<VBox spacing="8">
|
||||
<Label text="Course Data" styleClass="subsection-title"/>
|
||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||
<TextField fx:id="courseFileField" promptText="No file selected" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
<Button fx:id="courseBrowseButton" text="Browse..." onAction="#onBrowseCourses"/>
|
||||
</HBox>
|
||||
<Label fx:id="courseStatusLabel" text="Not Loaded" styleClass="status-label"/>
|
||||
</VBox>
|
||||
|
||||
<!-- Classrooms Import Section -->
|
||||
<VBox spacing="8">
|
||||
<Label text="Classroom Data" styleClass="subsection-title"/>
|
||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||
<TextField fx:id="classroomFileField" promptText="No file selected" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
<Button fx:id="classroomBrowseButton" text="Browse..." onAction="#onBrowseClassrooms"/>
|
||||
</HBox>
|
||||
<Label fx:id="classroomStatusLabel" text="Not Loaded" styleClass="status-label"/>
|
||||
</VBox>
|
||||
|
||||
<!-- Enrollments Import Section -->
|
||||
<VBox spacing="8">
|
||||
<Label text="Enrollment Data" styleClass="subsection-title"/>
|
||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||
<TextField fx:id="enrollmentFileField" promptText="No file selected" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
<Button fx:id="enrollmentBrowseButton" text="Browse..." onAction="#onBrowseEnrollments"/>
|
||||
</HBox>
|
||||
<Label fx:id="enrollmentStatusLabel" text="Not Loaded" styleClass="status-label"/>
|
||||
</VBox>
|
||||
|
||||
<Separator/>
|
||||
|
||||
<!-- Import Messages Area -->
|
||||
<VBox spacing="5" VBox.vgrow="ALWAYS">
|
||||
<Label text="Import Messages" styleClass="subsection-title"/>
|
||||
<TextArea fx:id="messagesArea" editable="false" wrapText="true" VBox.vgrow="ALWAYS"
|
||||
promptText="Validation messages and import results will appear here..."/>
|
||||
</VBox>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<HBox spacing="10" alignment="CENTER_RIGHT">
|
||||
<Button fx:id="importAllButton" text="Import All" onAction="#onImportAll"
|
||||
styleClass="primary-button" disable="true"/>
|
||||
<Button text="Clear All" onAction="#onClearAll"/>
|
||||
</HBox>
|
||||
<Label fx:id="studentStatusLabel" text="Not Loaded" styleClass="status-label"/>
|
||||
</VBox>
|
||||
|
||||
<!-- Courses Import Section -->
|
||||
<VBox spacing="8">
|
||||
<Label text="Course Data" styleClass="subsection-title"/>
|
||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||
<TextField fx:id="courseFileField" promptText="No file selected" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
<Button fx:id="courseBrowseButton" text="Browse..." onAction="#onBrowseCourses"/>
|
||||
</HBox>
|
||||
<Label fx:id="courseStatusLabel" text="Not Loaded" styleClass="status-label"/>
|
||||
</VBox>
|
||||
|
||||
<!-- Classrooms Import Section -->
|
||||
<VBox spacing="8">
|
||||
<Label text="Classroom Data" styleClass="subsection-title"/>
|
||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||
<TextField fx:id="classroomFileField" promptText="No file selected" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
<Button fx:id="classroomBrowseButton" text="Browse..." onAction="#onBrowseClassrooms"/>
|
||||
</HBox>
|
||||
<Label fx:id="classroomStatusLabel" text="Not Loaded" styleClass="status-label"/>
|
||||
</VBox>
|
||||
|
||||
<!-- Enrollments Import Section -->
|
||||
<VBox spacing="8">
|
||||
<Label text="Enrollment Data" styleClass="subsection-title"/>
|
||||
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||
<TextField fx:id="enrollmentFileField" promptText="No file selected" editable="false" HBox.hgrow="ALWAYS"/>
|
||||
<Button fx:id="enrollmentBrowseButton" text="Browse..." onAction="#onBrowseEnrollments"/>
|
||||
</HBox>
|
||||
<Label fx:id="enrollmentStatusLabel" text="Not Loaded" styleClass="status-label"/>
|
||||
</VBox>
|
||||
|
||||
<Separator/>
|
||||
|
||||
<!-- Import Messages Area -->
|
||||
<VBox spacing="5" VBox.vgrow="ALWAYS">
|
||||
<Label text="Import Messages" styleClass="subsection-title"/>
|
||||
<TextArea fx:id="messagesArea" editable="false" wrapText="true" VBox.vgrow="ALWAYS"
|
||||
promptText="Validation messages and import results will appear here..."/>
|
||||
</VBox>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<HBox spacing="10" alignment="CENTER_RIGHT">
|
||||
<Button fx:id="importAllButton" text="Import All" onAction="#onImportAll"
|
||||
styleClass="primary-button" disable="true"/>
|
||||
<Button text="Clear All" onAction="#onClearAll"/>
|
||||
</HBox>
|
||||
</VBox>
|
||||
</ScrollPane>
|
||||
|
||||
Reference in New Issue
Block a user