package student;
import db.DBConnection;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableRowSorter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.Vector;
import java.util.List;
import java.util.ArrayList;
public class SelectExamPanel extends JPanel {
private final int studentId;
private final StudentDashboard parentDashboard;
private JTable examTable;
private DefaultTableModel tableModel;
private JButton takeExamButton;
private JButton requestRetakeButton;
private JLabel statusLabel;
private ScheduledExecutorService scheduler;
// --- Consistent Color Palette ---
private static final Color MAIN_BG = new Color(240, 242, 245);
private static final Color HEADER_TEXT = new Color(52, 73, 94);
private static final Color TABLE_HEADER_BG = new Color(66, 133, 244);
private static final Color TABLE_HEADER_TEXT = Color.WHITE;
private static final Color BUTTON_PRIMARY_NORMAL = new Color(46, 139, 87);
private static final Color BUTTON_PRIMARY_HOVER = new Color(34, 102, 68);
private static final Color BUTTON_REQUEST_NORMAL = new Color(255, 165, 0);
private static final Color BUTTON_REQUEST_HOVER = new Color(204, 133, 0);
private static final Color BUTTON_DISABLED = new Color(150, 150, 150);
private static final Color STATUS_READY = new Color(200, 255, 200);
private static final Color STATUS_TAKEN = new Color(255, 220, 150);
private static final Color STATUS_NOT_ALLOWED = new Color(255, 180, 180);
private static final Color STATUS_PENDING_REQUEST = new Color(173, 216, 230);
public SelectExamPanel(int studentId, StudentDashboard parentDashboard) {
this.studentId = studentId;
this.parentDashboard = parentDashboard;
setLayout(new BorderLayout(20, 20));
setBorder(new EmptyBorder(30, 30, 30, 30));
setBackground(MAIN_BG);
// --- Heading ---
JLabel heading = new JLabel("📚 Available Exams for " +
getStudentName(studentId), JLabel.CENTER);
heading.setFont(new Font("Segoe UI", Font.BOLD, 32));
heading.setForeground(HEADER_TEXT);
add(heading, BorderLayout.NORTH);
// --- Table Setup ---
String[] columnNames = {"Exam ID", "Exam Title", "Duration (mins)", "Your
Attempts", "Max Attempts", "Status", "Actions"};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return switch (columnIndex) {
case 0, 2, 3, 4 -> Integer.class;
default -> String.class;
};
}
};
examTable = new JTable(tableModel);
examTable.setFillsViewportHeight(true);
examTable.setRowHeight(35);
examTable.getTableHeader().setFont(new Font("Segoe UI", Font.BOLD, 15));
examTable.getTableHeader().setBackground(TABLE_HEADER_BG);
examTable.getTableHeader().setForeground(TABLE_HEADER_TEXT);
examTable.getTableHeader().setReorderingAllowed(false);
examTable.setFont(new Font("Segoe UI", Font.PLAIN, 14));
examTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
examTable.setAutoCreateRowSorter(true);
examTable.setSelectionBackground(new Color(190, 220, 255));
examTable.setSelectionForeground(Color.BLACK);
DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
centerRenderer.setHorizontalAlignment(JLabel.CENTER);
examTable.getColumnModel().getColumn(0).setCellRenderer(centerRenderer);
examTable.getColumnModel().getColumn(2).setCellRenderer(centerRenderer);
examTable.getColumnModel().getColumn(3).setCellRenderer(centerRenderer);
examTable.getColumnModel().getColumn(4).setCellRenderer(centerRenderer);
examTable.getColumnModel().getColumn(5).setCellRenderer(new
DefaultTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object
value, boolean isSelected, boolean hasFocus, int row, int column) {
JLabel label = (JLabel) super.getTableCellRendererComponent(table,
value, isSelected, hasFocus, row, column);
if (value != null) {
String status = value.toString();
switch (status) {
case "Ready":
label.setBackground(STATUS_READY);
break;
case "Taken":
label.setBackground(STATUS_TAKEN);
break;
case "Not Allowed":
label.setBackground(STATUS_NOT_ALLOWED);
break;
case "Request Pending":
label.setBackground(STATUS_PENDING_REQUEST);
break;
default:
label.setBackground(table.getBackground());
}
}
label.setHorizontalAlignment(SwingConstants.CENTER);
return label;
}
});
JScrollPane scrollPane = new JScrollPane(examTable);
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(180, 180,
180), 1));
scrollPane.getViewport().setBackground(Color.WHITE);
add(scrollPane, BorderLayout.CENTER);
// --- Button Panel ---
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 10));
buttonPanel.setOpaque(false);
takeExamButton = new JButton("Start Exam");
styleButton(takeExamButton, BUTTON_PRIMARY_NORMAL);
takeExamButton.setEnabled(false);
buttonPanel.add(takeExamButton);
requestRetakeButton = new JButton("Request Retake");
styleButton(requestRetakeButton, BUTTON_REQUEST_NORMAL);
requestRetakeButton.setEnabled(false);
buttonPanel.add(requestRetakeButton);
add(buttonPanel, BorderLayout.SOUTH);
// --- Status Label (bottom left) ---
statusLabel = new JLabel("Loading exams...", JLabel.LEFT);
statusLabel.setFont(new Font("Segoe UI", Font.ITALIC, 12));
statusLabel.setForeground(Color.DARK_GRAY);
JPanel statusPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
statusPanel.setOpaque(false);
statusPanel.add(statusLabel);
// Add action listeners
examTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
updateButtonStates();
}
});
takeExamButton.addActionListener(e -> startExam());
requestRetakeButton.addActionListener(e -> requestRetake());
loadAvailableExams(false);
setupAccessMonitor();
}
private String getStudentName(int studentId) {
String studentName = "Student";
try (Connection con = DBConnection.getConnection();
PreparedStatement pst = con.prepareStatement("SELECT name FROM
students WHERE student_id = ?")) {
pst.setInt(1, studentId);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
studentName = rs.getString("name");
}
}
} catch (SQLException e) {
System.err.println("Error fetching student name: " + e.getMessage());
}
return studentName;
}
private void styleButton(JButton button, Color normalColor) {
button.setFont(new Font("Segoe UI", Font.BOLD, 16));
button.setBackground(normalColor);
button.setForeground(Color.WHITE);
button.setFocusPainted(false);
button.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(normalColor.darker(), 1),
new EmptyBorder(10, 25, 10, 25)
));
button.setCursor(new Cursor(Cursor.HAND_CURSOR));
button.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseEntered(java.awt.event.MouseEvent evt) {
if (button.isEnabled()) {
if (button == takeExamButton)
button.setBackground(BUTTON_PRIMARY_HOVER);
else if (button == requestRetakeButton)
button.setBackground(BUTTON_REQUEST_HOVER);
}
}
public void mouseExited(java.awt.event.MouseEvent evt) {
if (button.isEnabled()) {
button.setBackground(normalColor);
} else {
button.setBackground(BUTTON_DISABLED);
}
}
});
}
public void loadAvailableExams(boolean silent) {
tableModel.setRowCount(0);
takeExamButton.setEnabled(false);
requestRetakeButton.setEnabled(false);
statusLabel.setText("Refreshing exam list...");
try (Connection con = DBConnection.getConnection()) {
String query = """
SELECT
e.exam_id,
e.title,
e.duration,
COALESCE(sea.is_allowed, FALSE) AS is_allowed,
COALESCE(sea.attempts_taken, 0) AS attempts_taken,
COALESCE(e.max_attempts, 1) AS max_attempts,
(SELECT status FROM exam_retake_requests err WHERE
err.student_id = ? AND err.exam_id = e.exam_id AND err.status = 'Pending' LIMIT 1)
AS pending_request_status
FROM
exams e
LEFT JOIN
student_exam_access sea ON e.exam_id = sea.exam_id AND
sea.student_id = ?
WHERE
e.is_enabled = TRUE AND e.status = 'enabled'
ORDER BY
e.title ASC;
""";
PreparedStatement pst = con.prepareStatement(query);
pst.setInt(1, studentId);
pst.setInt(2, studentId);
ResultSet rs = pst.executeQuery();
boolean examsFound = false;
while (rs.next()) {
examsFound = true;
int examId = rs.getInt("exam_id");
String title = rs.getString("title");
int duration = rs.getInt("duration");
boolean isAllowed = rs.getBoolean("is_allowed");
int attemptsTaken = rs.getInt("attempts_taken");
int maxAttempts = rs.getInt("max_attempts");
String pendingRequestStatus =
rs.getString("pending_request_status");
// --- START DEBUG PRINTS ---
System.out.println("--- Debug Info for Exam ID: " + examId + "
---");
System.out.println("isAllowed (from DB): " + isAllowed);
System.out.println("attemptsTaken (from DB): " + attemptsTaken);
System.out.println("maxAttempts (from DB): " + maxAttempts);
System.out.println("pendingRequestStatus (from DB): " +
pendingRequestStatus);
// --- END DEBUG PRINTS ---
String status;
if (pendingRequestStatus != null &&
pendingRequestStatus.equals("Pending")) {
status = "Request Pending";
} else if (isAllowed && (attemptsTaken < maxAttempts || maxAttempts
== 0)) {
status = "Ready";
} else if (!isAllowed && attemptsTaken == maxAttempts) {
status = "Taken";
} else if (!isAllowed && attemptsTaken < maxAttempts) {
status = "Not Allowed";
} else {
status = "Taken";
}
// --- START DEBUG PRINTS ---
System.out.println("Calculated Status: " + status);
System.out.println("------------------------------------");
// --- END DEBUG PRINTS ---
tableModel.addRow(new Object[]{
examId,
title,
duration,
attemptsTaken,
maxAttempts,
status
});
}
// --- START OF NEW CODE TO AUTOMATICALLY SELECT APPROVED EXAM ---
int readyExamRowIndex = -1;
int readyExamCount = 0;
for (int i = 0; i < tableModel.getRowCount(); i++) {
// Ensure to convert row index from view to model if sorting is
active
int modelRow = examTable.convertRowIndexToModel(i);
String status = (String) tableModel.getValueAt(modelRow, 5); //
Assuming status is at column index 5
if ("Ready".equals(status)) {
readyExamCount++;
readyExamRowIndex = i; // Store the view row index
}
}
if (readyExamCount == 1) {
// Automatically select the row if there's exactly one "Ready" exam
final int finalReadyExamRowIndex = readyExamRowIndex; // For use in
SwingUtilities.invokeLater
SwingUtilities.invokeLater(() -> {
examTable.setRowSelectionInterval(finalReadyExamRowIndex,
finalReadyExamRowIndex);
// updateButtonStates() will be called in the finally block,
which will enable the button.
});
statusLabel.setText("Your approved exam is ready to start. Click
'Start Exam'!");
} else if (examsFound) {
statusLabel.setText("Exams loaded. Select an exam to view
options.");
} else {
statusLabel.setText("No exams available at the moment.");
}
// --- END OF NEW CODE TO AUTOMATICALLY SELECT APPROVED EXAM ---
} catch (SQLException e) {
if (!silent) {
JOptionPane.showMessageDialog(this, "Error loading exams: " +
e.getMessage(), "Database Error", JOptionPane.ERROR_MESSAGE);
}
statusLabel.setText("Error loading exams.");
e.printStackTrace();
} catch (Exception e) {
if (!silent) {
JOptionPane.showMessageDialog(this, "An unexpected error occurred:
" + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
statusLabel.setText("Error loading exams.");
e.printStackTrace();
} finally {
updateButtonStates();
}
}
private void updateButtonStates() {
int selectedRow = examTable.getSelectedRow();
boolean enableTakeExam = false;
boolean enableRequestRetake = false;
if (selectedRow != -1) {
int modelRow = examTable.convertRowIndexToModel(selectedRow);
String status = (String) tableModel.getValueAt(modelRow, 5);
int attemptsTaken = (int) tableModel.getValueAt(modelRow, 3);
int maxAttempts = (int) tableModel.getValueAt(modelRow, 4);
if ("Ready".equals(status)) {
enableTakeExam = true;
} else if ("Taken".equals(status) || "Not Allowed".equals(status)) {
if (attemptsTaken >= maxAttempts) {
enableRequestRetake = true;
}
}
}
takeExamButton.setEnabled(enableTakeExam);
requestRetakeButton.setEnabled(enableRequestRetake);
styleButton(takeExamButton, enableTakeExam ? BUTTON_PRIMARY_NORMAL :
BUTTON_DISABLED);
styleButton(requestRetakeButton, enableRequestRetake ?
BUTTON_REQUEST_NORMAL : BUTTON_DISABLED);
}
private void startExam() {
int selectedRow = examTable.getSelectedRow();
if (selectedRow == -1) {
JOptionPane.showMessageDialog(this, "Please select an exam to start.",
"No Exam Selected", JOptionPane.WARNING_MESSAGE);
return;
}
int modelRow = examTable.convertRowIndexToModel(selectedRow);
int examId = (int) tableModel.getValueAt(modelRow, 0);
String status = (String) tableModel.getValueAt(modelRow, 5);
if (!"Ready".equals(status)) {
JOptionPane.showMessageDialog(this, "This exam is not currently
available for taking. Status: " + status, "Exam Not Ready",
JOptionPane.WARNING_MESSAGE);
loadAvailableExams(false);
return;
}
List<Integer> examIdsInSession = new ArrayList<>();
examIdsInSession.add(examId);
int confirm = JOptionPane.showConfirmDialog(this,
"Are you sure you want to start the selected exam?",
"Confirm Start Exam", JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) {
stopAccessMonitor();
TakeExamPanel takeExamPanel = new TakeExamPanel(studentId,
examIdsInSession, parentDashboard);
parentDashboard.setMainContent(takeExamPanel, "Exam Session");
}
}
private void requestRetake() {
int selectedRow = examTable.getSelectedRow();
if (selectedRow == -1) {
JOptionPane.showMessageDialog(this, "Please select an exam to request a
retake for.", "No Exam Selected", JOptionPane.WARNING_MESSAGE);
return;
}
int modelRow = examTable.convertRowIndexToModel(selectedRow);
int examId = (int) tableModel.getValueAt(modelRow, 0);
String examTitle = (String) tableModel.getValueAt(modelRow, 1);
String currentStatus = (String) tableModel.getValueAt(modelRow, 5);
if (currentStatus.equals("Request Pending")) {
JOptionPane.showMessageDialog(this, "A retake request for '" +
examTitle + "' is already pending review.", "Request Already Pending",
JOptionPane.INFORMATION_MESSAGE);
return;
}
if (currentStatus.equals("Ready")) {
JOptionPane.showMessageDialog(this, "You are already allowed to take '"
+ examTitle + "'. No retake request needed.", "Exam Already Ready",
JOptionPane.INFORMATION_MESSAGE);
return;
}
int confirm = JOptionPane.showConfirmDialog(this,
"<html>Are you sure you want to request a retake for **" +
examTitle + "**?<br>" +
"Your request will be sent to the examiner for
review.</html>",
"Confirm Retake Request", JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (confirm == JOptionPane.YES_OPTION) {
try (Connection con = DBConnection.getConnection()) {
String checkQuery = "SELECT request_id, status FROM
exam_retake_requests WHERE student_id = ? AND exam_id = ? LIMIT 1";
PreparedStatement checkPst = con.prepareStatement(checkQuery);
checkPst.setInt(1, studentId);
checkPst.setInt(2, examId);
ResultSet checkRs = checkPst.executeQuery();
if (checkRs.next()) {
String existingStatus = checkRs.getString("status");
int existingRequestId = checkRs.getInt("request_id");
if ("Denied".equals(existingStatus)) {
String updateQuery = "UPDATE exam_retake_requests SET
status = 'Pending', request_date = NOW(), response_date = NULL, examiner_notes =
NULL WHERE request_id = ?";
PreparedStatement updatePst =
con.prepareStatement(updateQuery);
updatePst.setInt(1, existingRequestId);
updatePst.executeUpdate();
JOptionPane.showMessageDialog(this, "Your retake request
for '" + examTitle + "' has been re-submitted to the examiner.", "Request Re-
submitted", JOptionPane.INFORMATION_MESSAGE);
} else if ("Granted".equals(existingStatus)) {
JOptionPane.showMessageDialog(this, "You have already been
granted access for '" + examTitle + "'. Please check the 'Ready' status.", "Access
Already Granted", JOptionPane.INFORMATION_MESSAGE);
} else if ("Pending".equals(existingStatus)) {
JOptionPane.showMessageDialog(this, "A retake request for
'" + examTitle + "' is already pending review.", "Request Already Pending",
JOptionPane.INFORMATION_MESSAGE);
}
} else {
String insertQuery = "INSERT INTO exam_retake_requests
(student_id, exam_id, request_date, status) VALUES (?, ?, NOW(), 'Pending')";
PreparedStatement insertPst =
con.prepareStatement(insertQuery);
insertPst.setInt(1, studentId);
insertPst.setInt(2, examId);
insertPst.executeUpdate();
JOptionPane.showMessageDialog(this, "Your retake request for '"
+ examTitle + "' has been submitted to the examiner.", "Request Submitted",
JOptionPane.INFORMATION_MESSAGE);
}
loadAvailableExams(false);
} catch (SQLException e) {
JOptionPane.showMessageDialog(this, "Error submitting retake
request: " + e.getMessage(), "Database Error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
}
public void setupAccessMonitor() {
if (scheduler != null && !scheduler.isShutdown()) {
return;
}
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> SwingUtilities.invokeLater(() -> {
loadAvailableExams(true);
}), 0, 60, TimeUnit.SECONDS);
}
public void stopAccessMonitor() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdownNow();
scheduler = null;
}
}
}