From 3b47b8372e3219957c63cd807f5b592e94526646 Mon Sep 17 00:00:00 2001 From: Abdoalrahmankhedr Date: Sun, 23 Nov 2025 18:22:24 +0200 Subject: [PATCH 1/6] Edit the view of the quiz button --- src/pages/StudentLessons.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/StudentLessons.java b/src/pages/StudentLessons.java index ee8ef92..40c673e 100644 --- a/src/pages/StudentLessons.java +++ b/src/pages/StudentLessons.java @@ -306,7 +306,8 @@ public static void start(Course course, int ID) { if (progressObj != null) { attemptsUsed = progressObj.getAttempts().size(); hasAttempts = attemptsUsed > 0; - allAttemptsUsed = attemptsUsed >= maxAttempts; + if(attemptsUsed >= maxAttempts) allAttemptsUsed=true; + else allAttemptsUsed=false; lessonComplete = progressObj.isLessonComplete(); } } else { From e52a066cc820771acdb840271f8aa97825f79db3 Mon Sep 17 00:00:00 2001 From: Andrew Sameh Adel Date: Sun, 23 Nov 2025 18:31:34 +0200 Subject: [PATCH 2/6] fixes --- src/models/User.java | 5 +---- src/pages/AdminDashboard.java | 8 -------- src/resources/users.json | 4 ++-- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/models/User.java b/src/models/User.java index 08a364e..6208e69 100644 --- a/src/models/User.java +++ b/src/models/User.java @@ -11,15 +11,12 @@ public class User implements Model { private String email; private String name; private String role; - private List certificates; + private List certificates = new ArrayList<>(); /* This constructor is necessary for JSON parsing */ public User() {} public void addCertificate(Certificate certificate) { - if (this.certificates == null) { - this.certificates = new ArrayList<>(); - } this.certificates.add(certificate); } diff --git a/src/pages/AdminDashboard.java b/src/pages/AdminDashboard.java index 7bbc9d4..adda041 100644 --- a/src/pages/AdminDashboard.java +++ b/src/pages/AdminDashboard.java @@ -27,14 +27,6 @@ public AdminDashboard() { initComponents(); } - public static void setAdmin(int adminId, String adminName) { - loggedInAdminId = adminId; - loggedInAdminName = adminName; - if (adminLabel != null) { - adminLabel.setText("Admin: " + loggedInAdminName); - } - } - private void initComponents() { setLayout(new BorderLayout()); diff --git a/src/resources/users.json b/src/resources/users.json index 2c391c3..a3a5733 100644 --- a/src/resources/users.json +++ b/src/resources/users.json @@ -30,14 +30,14 @@ "password" : "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", "role" : "Admin" }, { - "certificates" : null, + "certificates" : [ ], "email" : "ahmed@gmail.com", "id" : 4, "name" : "ahmed sherif", "password" : "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", "role" : "Instructor" }, { - "certificates" : null, + "certificates" : [ ], "email" : "malek@gmail.com", "id" : 5, "name" : "malek", From 4184b8a1a9cf84e759febd01072b12f640adca60 Mon Sep 17 00:00:00 2001 From: Ahmed_Sherif Date: Sun, 23 Nov 2025 18:35:20 +0200 Subject: [PATCH 3/6] QuizDialog --- src/pages/AddQuizDialog.java | 512 +++++++++++++++++++++++++++ src/pages/AdminDashboard.java | 21 +- src/pages/EditQuizDialog.java | 580 +++++++++++++++++++++++++++++++ src/pages/ManageCourseFrame.java | 50 ++- src/resources/courses.json | 143 ++++++-- 5 files changed, 1257 insertions(+), 49 deletions(-) create mode 100644 src/pages/AddQuizDialog.java create mode 100644 src/pages/EditQuizDialog.java diff --git a/src/pages/AddQuizDialog.java b/src/pages/AddQuizDialog.java new file mode 100644 index 0000000..7b1d1bd --- /dev/null +++ b/src/pages/AddQuizDialog.java @@ -0,0 +1,512 @@ +package pages; + +import databases.CourseDatabase; +import models.Course; +import models.Lesson; +import models.Question; +import services.InstructorService; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AddQuizDialog extends JDialog { + private Lesson lesson; + private Course course; + private int instructorId; + + private JSpinner retriesSpinner; + private JSpinner passingScoreSpinner; + private JPanel questionsPanel; + private List questionPanels; + private JScrollPane questionsScrollPane; + + public AddQuizDialog(Frame parent, Lesson lesson, Course course, int instructorId) { + super(parent, "Add Quiz to Lesson", true); + this.lesson = lesson; + this.course = course; + this.instructorId = instructorId; + this.questionPanels = new ArrayList<>(); + + initComponents(); + setResizable(true); + setLocationRelativeTo(parent); + } + + private void initComponents() { + setLayout(new BorderLayout(10, 10)); + setSize(800, 600); + + // Header Panel + JPanel headerPanel = new JPanel(); + headerPanel.setBackground(new Color(0, 153, 51)); + headerPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); + + JLabel headerLabel = new JLabel("Create Quiz for: " + lesson.getTitle()); + headerLabel.setFont(new Font("Segoe UI", Font.BOLD, 20)); + headerLabel.setForeground(Color.WHITE); + headerPanel.add(headerLabel); + + add(headerPanel, BorderLayout.NORTH); + + // Settings Panel + JPanel settingsPanel = new JPanel(new GridBagLayout()); + settingsPanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.GRAY, 1), + "Quiz Settings", + 0, + 0, + new Font("Segoe UI", Font.BOLD, 14) + )); + settingsPanel.setBackground(Color.WHITE); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.anchor = GridBagConstraints.WEST; + + // Number of Retries + gbc.gridx = 0; + gbc.gridy = 0; + JLabel retriesLabel = new JLabel("Number of Attempts:"); + retriesLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + settingsPanel.add(retriesLabel, gbc); + + gbc.gridx = 1; + retriesSpinner = new JSpinner(new SpinnerNumberModel(3, 1, 10, 1)); + retriesSpinner.setPreferredSize(new Dimension(100, 30)); + ((JSpinner.DefaultEditor) retriesSpinner.getEditor()).getTextField().setFont( + new Font("Segoe UI", Font.PLAIN, 14) + ); + settingsPanel.add(retriesSpinner, gbc); + + // Passing Score + gbc.gridx = 2; + gbc.insets = new Insets(10, 30, 10, 10); + JLabel passingLabel = new JLabel("Passing Score (%):"); + passingLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + settingsPanel.add(passingLabel, gbc); + + gbc.gridx = 3; + gbc.insets = new Insets(10, 10, 10, 10); + passingScoreSpinner = new JSpinner(new SpinnerNumberModel(75.0, 0.0, 100.0, 5.0)); + passingScoreSpinner.setPreferredSize(new Dimension(100, 30)); + ((JSpinner.DefaultEditor) passingScoreSpinner.getEditor()).getTextField().setFont( + new Font("Segoe UI", Font.PLAIN, 14) + ); + settingsPanel.add(passingScoreSpinner, gbc); + + // Questions Panel + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + mainPanel.setBackground(Color.WHITE); + + mainPanel.add(settingsPanel, BorderLayout.NORTH); + + // Questions Container + JPanel questionsContainer = new JPanel(new BorderLayout()); + questionsContainer.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.GRAY, 1), + "Questions", + 0, + 0, + new Font("Segoe UI", Font.BOLD, 14) + )); + questionsContainer.setBackground(Color.WHITE); + + questionsPanel = new JPanel(); + questionsPanel.setLayout(new BoxLayout(questionsPanel, BoxLayout.Y_AXIS)); + questionsPanel.setBackground(Color.WHITE); + + questionsScrollPane = new JScrollPane(questionsPanel); + questionsScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + questionsScrollPane.getVerticalScrollBar().setUnitIncrement(16); + questionsScrollPane.setBorder(null); + + questionsContainer.add(questionsScrollPane, BorderLayout.CENTER); + + // Add Question Button + JPanel addQuestionBtnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + addQuestionBtnPanel.setBackground(Color.WHITE); + + JButton addQuestionBtn = new JButton("+ Add Question"); + addQuestionBtn.setFont(new Font("Segoe UI", Font.BOLD, 14)); + addQuestionBtn.setBackground(new Color(0, 102, 204)); + addQuestionBtn.setForeground(Color.WHITE); + addQuestionBtn.setFocusPainted(false); + addQuestionBtn.addActionListener(e -> addQuestion()); + + addQuestionBtnPanel.add(addQuestionBtn); + questionsContainer.add(addQuestionBtnPanel, BorderLayout.SOUTH); + + mainPanel.add(questionsContainer, BorderLayout.CENTER); + + add(mainPanel, BorderLayout.CENTER); + + // Bottom Buttons + JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + buttonsPanel.setBackground(Color.WHITE); + + JButton saveButton = new JButton("Create Quiz"); + saveButton.setFont(new Font("Segoe UI", Font.BOLD, 16)); + saveButton.setBackground(new Color(0, 153, 51)); + saveButton.setForeground(Color.WHITE); + saveButton.setFocusPainted(false); + saveButton.setPreferredSize(new Dimension(150, 40)); + saveButton.addActionListener(e -> saveQuiz()); + + JButton cancelButton = new JButton("Cancel"); + cancelButton.setFont(new Font("Segoe UI", Font.PLAIN, 16)); + cancelButton.setBackground(new Color(204, 204, 204)); + cancelButton.setFocusPainted(false); + cancelButton.setPreferredSize(new Dimension(100, 40)); + cancelButton.addActionListener(e -> dispose()); + + buttonsPanel.add(cancelButton); + buttonsPanel.add(saveButton); + + add(buttonsPanel, BorderLayout.SOUTH); + + // Add first question by default + addQuestion(); + } + + private void addQuestion() { + QuestionPanel qPanel = new QuestionPanel(questionPanels.size() + 1); + questionPanels.add(qPanel); + questionsPanel.add(qPanel); + questionsPanel.add(Box.createVerticalStrut(10)); + questionsPanel.revalidate(); + questionsPanel.repaint(); + + // Scroll to bottom + SwingUtilities.invokeLater(() -> { + JScrollBar vertical = questionsScrollPane.getVerticalScrollBar(); + vertical.setValue(vertical.getMaximum()); + }); + } + + private void saveQuiz() { + // Validate + if (questionPanels.isEmpty()) { + JOptionPane.showMessageDialog(this, + "Please add at least one question", + "No Questions", + JOptionPane.WARNING_MESSAGE); + return; + } + + // Collect questions + List questions = new ArrayList<>(); + + for (int i = 0; i < questionPanels.size(); i++) { + QuestionPanel qPanel = questionPanels.get(i); + + String questionText = qPanel.getQuestionText(); + if (questionText.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, + "Question " + (i + 1) + " cannot be empty", + "Invalid Question", + JOptionPane.WARNING_MESSAGE); + return; + } + + Map choices = qPanel.getChoices(); + if (choices.size() < 2) { + JOptionPane.showMessageDialog(this, + "Question " + (i + 1) + " must have at least 2 choices", + "Invalid Question", + JOptionPane.WARNING_MESSAGE); + return; + } + + long correctCount = choices.values().stream().filter(b -> b).count(); + if (correctCount != 1) { + JOptionPane.showMessageDialog(this, + "Question " + (i + 1) + " must have exactly one correct answer", + "Invalid Question", + JOptionPane.WARNING_MESSAGE); + return; + } + + Question question = InstructorService.createQuestion(questionText, choices); + if (question != null) { + questions.add(question); + } + } + + if (questions.isEmpty()) { + JOptionPane.showMessageDialog(this, + "No valid questions created", + "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + + try { + int retries = (Integer) retriesSpinner.getValue(); + double passingScore = (Double) passingScoreSpinner.getValue(); + + // Create quiz + CourseDatabase courseDb = CourseDatabase.getInstance(); + courseDb.addQuiz(lesson.getId(), retries, passingScore, questions); + + JOptionPane.showMessageDialog(this, + "Quiz created successfully!", + "Success", + JOptionPane.INFORMATION_MESSAGE); + + dispose(); + + // Refresh the course view + Course updatedCourse = courseDb.getCourseById(course.getId()); + ManageCourseFrame.start(updatedCourse, instructorId); + + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, + "Error creating quiz: " + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); + } + } + + // Inner class for Question Panel + private class QuestionPanel extends JPanel { + private int questionNumber; + private JTextArea questionTextArea; + private List choicePanels; + private JPanel choicesContainer; + + public QuestionPanel(int number) { + this.questionNumber = number; + this.choicePanels = new ArrayList<>(); + + setLayout(new BorderLayout(10, 10)); + setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(0, 102, 204), 2), + BorderFactory.createEmptyBorder(15, 15, 15, 15) + )); + setBackground(new Color(240, 248, 255)); + setMaximumSize(new Dimension(Integer.MAX_VALUE, 400)); + + // Header with question number and delete button + JPanel headerPanel = new JPanel(new BorderLayout()); + headerPanel.setOpaque(false); + + JLabel numberLabel = new JLabel("Question " + questionNumber); + numberLabel.setFont(new Font("Segoe UI", Font.BOLD, 16)); + numberLabel.setForeground(new Color(0, 102, 204)); + + JButton deleteBtn = new JButton("✖"); + deleteBtn.setFont(new Font("Arial", Font.BOLD, 14)); + deleteBtn.setBackground(new Color(204, 0, 0)); + deleteBtn.setForeground(Color.WHITE); + deleteBtn.setFocusPainted(false); + deleteBtn.setPreferredSize(new Dimension(40, 30)); + deleteBtn.addActionListener(e -> deleteQuestion()); + + headerPanel.add(numberLabel, BorderLayout.WEST); + headerPanel.add(deleteBtn, BorderLayout.EAST); + + // Question text + JPanel questionPanel = new JPanel(new BorderLayout(5, 5)); + questionPanel.setOpaque(false); + + JLabel questionLabel = new JLabel("Question Text:"); + questionLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); + + questionTextArea = new JTextArea(3, 40); + questionTextArea.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + questionTextArea.setLineWrap(true); + questionTextArea.setWrapStyleWord(true); + JScrollPane questionScroll = new JScrollPane(questionTextArea); + + questionPanel.add(questionLabel, BorderLayout.NORTH); + questionPanel.add(questionScroll, BorderLayout.CENTER); + + // Choices + JPanel choicesPanel = new JPanel(new BorderLayout(5, 5)); + choicesPanel.setOpaque(false); + + JLabel choicesLabel = new JLabel("Choices (mark one as correct):"); + choicesLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); + + choicesContainer = new JPanel(); + choicesContainer.setLayout(new BoxLayout(choicesContainer, BoxLayout.Y_AXIS)); + choicesContainer.setOpaque(false); + + // Add 4 choices by default + for (int i = 0; i < 4; i++) { + addChoice(); + } + + JButton addChoiceBtn = new JButton("+ Add Choice"); + addChoiceBtn.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + addChoiceBtn.setBackground(new Color(0, 153, 51)); + addChoiceBtn.setForeground(Color.WHITE); + addChoiceBtn.setFocusPainted(false); + addChoiceBtn.addActionListener(e -> addChoice()); + + choicesPanel.add(choicesLabel, BorderLayout.NORTH); + choicesPanel.add(choicesContainer, BorderLayout.CENTER); + choicesPanel.add(addChoiceBtn, BorderLayout.SOUTH); + + // Add all components + add(headerPanel, BorderLayout.NORTH); + + JPanel contentPanel = new JPanel(); + contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); + contentPanel.setOpaque(false); + contentPanel.add(questionPanel); + contentPanel.add(Box.createVerticalStrut(10)); + contentPanel.add(choicesPanel); + + add(contentPanel, BorderLayout.CENTER); + } + + private void addChoice() { + ChoicePanel choicePanel = new ChoicePanel((char)('A' + choicePanels.size())); + choicePanels.add(choicePanel); + choicesContainer.add(choicePanel); + choicesContainer.add(Box.createVerticalStrut(5)); + revalidate(); + repaint(); + } + + private void deleteQuestion() { + int result = JOptionPane.showConfirmDialog( + AddQuizDialog.this, + "Are you sure you want to delete this question?", + "Confirm Delete", + JOptionPane.YES_NO_OPTION + ); + + if (result == JOptionPane.YES_OPTION) { + questionPanels.remove(this); + questionsPanel.remove(this); + + // Renumber remaining questions + for (int i = 0; i < questionPanels.size(); i++) { + questionPanels.get(i).questionNumber = i + 1; + questionPanels.get(i).updateNumber(); + } + + questionsPanel.revalidate(); + questionsPanel.repaint(); + } + } + + private void updateNumber() { + Component[] components = ((JPanel)getComponent(0)).getComponents(); + if (components.length > 0 && components[0] instanceof JLabel) { + ((JLabel)components[0]).setText("Question " + questionNumber); + } + } + + public String getQuestionText() { + return questionTextArea.getText().trim(); + } + + public Map getChoices() { + Map choices = new HashMap<>(); + for (ChoicePanel cp : choicePanels) { + String text = cp.getChoiceText(); + if (!text.isEmpty()) { + choices.put(text, cp.isCorrect()); + } + } + return choices; + } + + // Inner class for Choice Panel + private class ChoicePanel extends JPanel { + private char letter; + private JTextField choiceField; + private JCheckBox correctCheckbox; + + public ChoicePanel(char letter) { + this.letter = letter; + + setLayout(new BorderLayout(10, 0)); + setOpaque(false); + setMaximumSize(new Dimension(Integer.MAX_VALUE, 35)); + + JLabel letterLabel = new JLabel(letter + ")"); + letterLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); + letterLabel.setPreferredSize(new Dimension(30, 30)); + + choiceField = new JTextField(); + choiceField.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + + correctCheckbox = new JCheckBox("Correct"); + correctCheckbox.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + correctCheckbox.setOpaque(false); + + JButton deleteBtn = new JButton("✖"); + deleteBtn.setFont(new Font("Arial", Font.BOLD, 12)); + deleteBtn.setBackground(new Color(204, 0, 0)); + deleteBtn.setForeground(Color.WHITE); + deleteBtn.setFocusPainted(false); + deleteBtn.setPreferredSize(new Dimension(30, 30)); + deleteBtn.addActionListener(e -> deleteChoice()); + + JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); + rightPanel.setOpaque(false); + rightPanel.add(correctCheckbox); + rightPanel.add(deleteBtn); + + add(letterLabel, BorderLayout.WEST); + add(choiceField, BorderLayout.CENTER); + add(rightPanel, BorderLayout.EAST); + } + + private void deleteChoice() { + if (choicePanels.size() <= 2) { + JOptionPane.showMessageDialog( + AddQuizDialog.this, + "A question must have at least 2 choices", + "Cannot Delete", + JOptionPane.WARNING_MESSAGE + ); + return; + } + + choicePanels.remove(this); + choicesContainer.remove(this); + + // Reletter remaining choices + for (int i = 0; i < choicePanels.size(); i++) { + choicePanels.get(i).letter = (char)('A' + i); + choicePanels.get(i).updateLetter(); + } + + choicesContainer.revalidate(); + choicesContainer.repaint(); + } + + private void updateLetter() { + Component[] components = getComponents(); + if (components.length > 0 && components[0] instanceof JLabel) { + ((JLabel)components[0]).setText(letter + ")"); + } + } + + public String getChoiceText() { + return choiceField.getText().trim(); + } + + public boolean isCorrect() { + return correctCheckbox.isSelected(); + } + } + } + + public static void showDialog(Lesson lesson, Course course, int instructorId, Frame parent) { + AddQuizDialog dialog = new AddQuizDialog(parent, lesson, course, instructorId); + dialog.setVisible(true); + } +} \ No newline at end of file diff --git a/src/pages/AdminDashboard.java b/src/pages/AdminDashboard.java index 7bbc9d4..e902635 100644 --- a/src/pages/AdminDashboard.java +++ b/src/pages/AdminDashboard.java @@ -13,6 +13,7 @@ public class AdminDashboard extends JPanel { private static int adminId; + private static String adminName; private static JLabel adminLabel; private static JPanel coursesPanel; private static JButton logoutButton; @@ -27,22 +28,20 @@ public AdminDashboard() { initComponents(); } - public static void setAdmin(int adminId, String adminName) { - loggedInAdminId = adminId; - loggedInAdminName = adminName; + public static void setAdmin(int id, String name) { + adminId = id; + adminName = name; if (adminLabel != null) { - adminLabel.setText("Admin: " + loggedInAdminName); + adminLabel.setText("Admin: " + adminName); } } private void initComponents() { setLayout(new BorderLayout()); - // Top panel with header and info JPanel topPanel = new JPanel(new BorderLayout()); topPanel.setBackground(Color.WHITE); - // Header JPanel headerPanel = new JPanel(); headerPanel.setBackground(new Color(204, 204, 204)); JLabel titleLabel = new JLabel("Admin Dashboard"); @@ -83,7 +82,6 @@ private void initComponents() { .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); - // Admin info & buttons JPanel infoPanel = new JPanel(); infoPanel.setBackground(new Color(51, 153, 255)); @@ -115,7 +113,6 @@ private void initComponents() { infoPanel.add(viewApprovedBtn); infoPanel.add(viewRejectedBtn); - // Courses panel coursesPanel = new JPanel(); coursesPanel.setBackground(new Color(204, 204, 204)); coursesPanel.setLayout(new BorderLayout()); @@ -126,7 +123,6 @@ private void initComponents() { add(topPanel, BorderLayout.NORTH); add(coursesPanel, BorderLayout.CENTER); - // Button actions logoutButton.addActionListener(e -> { MainWindow.goTo("login"); }); @@ -147,7 +143,7 @@ private void initComponents() { } }); - showPendingCourses(); // initial view + showPendingCourses(); } private static void showPendingCourses() { @@ -254,7 +250,8 @@ public static void start(int id) { User admin = AdminService.getAdmin(id); if (admin != null) { - adminLabel.setText("Admin: " + admin.getName()); + adminName = admin.getName(); + adminLabel.setText("Admin: " + adminName); } } -} +} \ No newline at end of file diff --git a/src/pages/EditQuizDialog.java b/src/pages/EditQuizDialog.java new file mode 100644 index 0000000..9a7f64e --- /dev/null +++ b/src/pages/EditQuizDialog.java @@ -0,0 +1,580 @@ +package pages; + +import databases.CourseDatabase; +import models.Course; +import models.Lesson; +import models.Question; +import models.Quiz; +import services.InstructorService; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class EditQuizDialog extends JDialog { + private Lesson lesson; + private Quiz quiz; + private Course course; + private int instructorId; + + private JSpinner retriesSpinner; + private JSpinner passingScoreSpinner; + private JPanel questionsPanel; + private List questionPanels; + private JScrollPane questionsScrollPane; + + public EditQuizDialog(Frame parent, Lesson lesson, Course course, int instructorId) { + super(parent, "Edit Quiz", true); + this.lesson = lesson; + this.quiz = lesson.getQuiz(); + this.course = course; + this.instructorId = instructorId; + this.questionPanels = new ArrayList<>(); + + initComponents(); + loadQuizData(); + setResizable(true); + setLocationRelativeTo(parent); + } + + private void initComponents() { + setLayout(new BorderLayout(10, 10)); + setSize(800, 600); + + // Header Panel + JPanel headerPanel = new JPanel(); + headerPanel.setBackground(new Color(255, 153, 0)); + headerPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); + + JLabel headerLabel = new JLabel("Edit Quiz for: " + lesson.getTitle()); + headerLabel.setFont(new Font("Segoe UI", Font.BOLD, 20)); + headerLabel.setForeground(Color.WHITE); + headerPanel.add(headerLabel); + + add(headerPanel, BorderLayout.NORTH); + + // Settings Panel + JPanel settingsPanel = new JPanel(new GridBagLayout()); + settingsPanel.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.GRAY, 1), + "Quiz Settings", + 0, + 0, + new Font("Segoe UI", Font.BOLD, 14) + )); + settingsPanel.setBackground(Color.WHITE); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(10, 10, 10, 10); + gbc.anchor = GridBagConstraints.WEST; + + // Number of Retries + gbc.gridx = 0; + gbc.gridy = 0; + JLabel retriesLabel = new JLabel("Number of Attempts:"); + retriesLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + settingsPanel.add(retriesLabel, gbc); + + gbc.gridx = 1; + retriesSpinner = new JSpinner(new SpinnerNumberModel(3, 1, 10, 1)); + retriesSpinner.setPreferredSize(new Dimension(100, 30)); + ((JSpinner.DefaultEditor) retriesSpinner.getEditor()).getTextField().setFont( + new Font("Segoe UI", Font.PLAIN, 14) + ); + settingsPanel.add(retriesSpinner, gbc); + + // Passing Score + gbc.gridx = 2; + gbc.insets = new Insets(10, 30, 10, 10); + JLabel passingLabel = new JLabel("Passing Score (%):"); + passingLabel.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + settingsPanel.add(passingLabel, gbc); + + gbc.gridx = 3; + gbc.insets = new Insets(10, 10, 10, 10); + passingScoreSpinner = new JSpinner(new SpinnerNumberModel(75.0, 0.0, 100.0, 5.0)); + passingScoreSpinner.setPreferredSize(new Dimension(100, 30)); + ((JSpinner.DefaultEditor) passingScoreSpinner.getEditor()).getTextField().setFont( + new Font("Segoe UI", Font.PLAIN, 14) + ); + settingsPanel.add(passingScoreSpinner, gbc); + + // Delete Quiz Button + gbc.gridx = 4; + gbc.insets = new Insets(10, 30, 10, 10); + JButton deleteQuizBtn = new JButton("Delete Quiz"); + deleteQuizBtn.setBackground(new Color(204, 0, 0)); + deleteQuizBtn.setForeground(Color.WHITE); + deleteQuizBtn.setFont(new Font("Segoe UI", Font.BOLD, 12)); + deleteQuizBtn.setFocusPainted(false); + deleteQuizBtn.addActionListener(e -> deleteQuiz()); + settingsPanel.add(deleteQuizBtn, gbc); + + // Questions Panel + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + mainPanel.setBackground(Color.WHITE); + + mainPanel.add(settingsPanel, BorderLayout.NORTH); + + // Questions Container + JPanel questionsContainer = new JPanel(new BorderLayout()); + questionsContainer.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(Color.GRAY, 1), + "Questions", + 0, + 0, + new Font("Segoe UI", Font.BOLD, 14) + )); + questionsContainer.setBackground(Color.WHITE); + + questionsPanel = new JPanel(); + questionsPanel.setLayout(new BoxLayout(questionsPanel, BoxLayout.Y_AXIS)); + questionsPanel.setBackground(Color.WHITE); + + questionsScrollPane = new JScrollPane(questionsPanel); + questionsScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + questionsScrollPane.getVerticalScrollBar().setUnitIncrement(16); + questionsScrollPane.setBorder(null); + + questionsContainer.add(questionsScrollPane, BorderLayout.CENTER); + + // Add Question Button + JPanel addQuestionBtnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + addQuestionBtnPanel.setBackground(Color.WHITE); + + JButton addQuestionBtn = new JButton("+ Add Question"); + addQuestionBtn.setFont(new Font("Segoe UI", Font.BOLD, 14)); + addQuestionBtn.setBackground(new Color(0, 102, 204)); + addQuestionBtn.setForeground(Color.WHITE); + addQuestionBtn.setFocusPainted(false); + addQuestionBtn.addActionListener(e -> addQuestion(null)); + + addQuestionBtnPanel.add(addQuestionBtn); + questionsContainer.add(addQuestionBtnPanel, BorderLayout.SOUTH); + + mainPanel.add(questionsContainer, BorderLayout.CENTER); + + add(mainPanel, BorderLayout.CENTER); + + // Bottom Buttons + JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + buttonsPanel.setBackground(Color.WHITE); + + JButton saveButton = new JButton("Save Changes"); + saveButton.setFont(new Font("Segoe UI", Font.BOLD, 16)); + saveButton.setBackground(new Color(255, 153, 0)); + saveButton.setForeground(Color.WHITE); + saveButton.setFocusPainted(false); + saveButton.setPreferredSize(new Dimension(150, 40)); + saveButton.addActionListener(e -> saveQuiz()); + + JButton cancelButton = new JButton("Cancel"); + cancelButton.setFont(new Font("Segoe UI", Font.PLAIN, 16)); + cancelButton.setBackground(new Color(204, 204, 204)); + cancelButton.setFocusPainted(false); + cancelButton.setPreferredSize(new Dimension(100, 40)); + cancelButton.addActionListener(e -> dispose()); + + buttonsPanel.add(cancelButton); + buttonsPanel.add(saveButton); + + add(buttonsPanel, BorderLayout.SOUTH); + } + + private void loadQuizData() { + // Load quiz settings + retriesSpinner.setValue(quiz.getRetries()); + passingScoreSpinner.setValue(quiz.getPassingScore()); + + // Load questions + for (Question question : quiz.getQuestions()) { + addQuestion(question); + } + } + + private void addQuestion(Question existingQuestion) { + QuestionPanel qPanel = new QuestionPanel(questionPanels.size() + 1, existingQuestion); + questionPanels.add(qPanel); + questionsPanel.add(qPanel); + questionsPanel.add(Box.createVerticalStrut(10)); + questionsPanel.revalidate(); + questionsPanel.repaint(); + + // Scroll to bottom + SwingUtilities.invokeLater(() -> { + JScrollBar vertical = questionsScrollPane.getVerticalScrollBar(); + vertical.setValue(vertical.getMaximum()); + }); + } + + private void deleteQuiz() { + int result = JOptionPane.showConfirmDialog(this, + "Are you sure you want to delete this quiz?\nAll student attempts will be lost!", + "Confirm Delete Quiz", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + + if (result == JOptionPane.YES_OPTION) { + try { + CourseDatabase.getInstance().deleteQuiz(quiz.getId()); + + JOptionPane.showMessageDialog(this, + "Quiz deleted successfully!", + "Success", + JOptionPane.INFORMATION_MESSAGE); + + dispose(); + + // Refresh the course view + Course updatedCourse = CourseDatabase.getInstance().getCourseById(course.getId()); + ManageCourseFrame.start(updatedCourse, instructorId); + + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, + "Error deleting quiz: " + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + } + } + } + + private void saveQuiz() { + // Validate + if (questionPanels.isEmpty()) { + JOptionPane.showMessageDialog(this, + "Please add at least one question", + "No Questions", + JOptionPane.WARNING_MESSAGE); + return; + } + + // Collect questions + List questions = new ArrayList<>(); + + for (int i = 0; i < questionPanels.size(); i++) { + QuestionPanel qPanel = questionPanels.get(i); + + String questionText = qPanel.getQuestionText(); + if (questionText.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, + "Question " + (i + 1) + " cannot be empty", + "Invalid Question", + JOptionPane.WARNING_MESSAGE); + return; + } + + Map choices = qPanel.getChoices(); + if (choices.size() < 2) { + JOptionPane.showMessageDialog(this, + "Question " + (i + 1) + " must have at least 2 choices", + "Invalid Question", + JOptionPane.WARNING_MESSAGE); + return; + } + + long correctCount = choices.values().stream().filter(b -> b).count(); + if (correctCount != 1) { + JOptionPane.showMessageDialog(this, + "Question " + (i + 1) + " must have exactly one correct answer", + "Invalid Question", + JOptionPane.WARNING_MESSAGE); + return; + } + + Question question = InstructorService.createQuestion(questionText, choices); + if (question != null) { + questions.add(question); + } + } + + if (questions.isEmpty()) { + JOptionPane.showMessageDialog(this, + "No valid questions created", + "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + + try { + int retries = (Integer) retriesSpinner.getValue(); + double passingScore = (Double) passingScoreSpinner.getValue(); + + // Update quiz using InstructorService + InstructorService.updateQuiz(quiz.getId(), retries, (int)passingScore, questions); + + JOptionPane.showMessageDialog(this, + "Quiz updated successfully!", + "Success", + JOptionPane.INFORMATION_MESSAGE); + + dispose(); + + // Refresh the course view + Course updatedCourse = CourseDatabase.getInstance().getCourseById(course.getId()); + ManageCourseFrame.start(updatedCourse, instructorId); + + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, + "Error updating quiz: " + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); + } + } + + // Reuse QuestionPanel class from AddQuizDialog with modification + private class QuestionPanel extends JPanel { + private int questionNumber; + private JTextArea questionTextArea; + private List choicePanels; + private JPanel choicesContainer; + + public QuestionPanel(int number, Question existingQuestion) { + this.questionNumber = number; + this.choicePanels = new ArrayList<>(); + + setLayout(new BorderLayout(10, 10)); + setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(255, 153, 0), 2), + BorderFactory.createEmptyBorder(15, 15, 15, 15) + )); + setBackground(new Color(255, 248, 240)); + setMaximumSize(new Dimension(Integer.MAX_VALUE, 400)); + + // Header + JPanel headerPanel = new JPanel(new BorderLayout()); + headerPanel.setOpaque(false); + + JLabel numberLabel = new JLabel("Question " + questionNumber); + numberLabel.setFont(new Font("Segoe UI", Font.BOLD, 16)); + numberLabel.setForeground(new Color(255, 153, 0)); + + JButton deleteBtn = new JButton("✖"); + deleteBtn.setFont(new Font("Arial", Font.BOLD, 14)); + deleteBtn.setBackground(new Color(204, 0, 0)); + deleteBtn.setForeground(Color.WHITE); + deleteBtn.setFocusPainted(false); + deleteBtn.setPreferredSize(new Dimension(40, 30)); + deleteBtn.addActionListener(e -> deleteQuestion()); + + headerPanel.add(numberLabel, BorderLayout.WEST); + headerPanel.add(deleteBtn, BorderLayout.EAST); + + // Question text + JPanel questionPanel = new JPanel(new BorderLayout(5, 5)); + questionPanel.setOpaque(false); + + JLabel questionLabel = new JLabel("Question Text:"); + questionLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); + + questionTextArea = new JTextArea(3, 40); + questionTextArea.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + questionTextArea.setLineWrap(true); + questionTextArea.setWrapStyleWord(true); + if (existingQuestion != null) { + questionTextArea.setText(existingQuestion.getHeader()); + } + JScrollPane questionScroll = new JScrollPane(questionTextArea); + + questionPanel.add(questionLabel, BorderLayout.NORTH); + questionPanel.add(questionScroll, BorderLayout.CENTER); + + // Choices + JPanel choicesPanel = new JPanel(new BorderLayout(5, 5)); + choicesPanel.setOpaque(false); + + JLabel choicesLabel = new JLabel("Choices (mark one as correct):"); + choicesLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); + + choicesContainer = new JPanel(); + choicesContainer.setLayout(new BoxLayout(choicesContainer, BoxLayout.Y_AXIS)); + choicesContainer.setOpaque(false); + + // Load existing choices or add defaults + if (existingQuestion != null && existingQuestion.getChoices() != null) { + for (Question.Choice choice : existingQuestion.getChoices()) { + addChoice(choice); + } + } else { + for (int i = 0; i < 4; i++) { + addChoice(null); + } + } + + JButton addChoiceBtn = new JButton("+ Add Choice"); + addChoiceBtn.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + addChoiceBtn.setBackground(new Color(0, 153, 51)); + addChoiceBtn.setForeground(Color.WHITE); + addChoiceBtn.setFocusPainted(false); + addChoiceBtn.addActionListener(e -> addChoice(null)); + + choicesPanel.add(choicesLabel, BorderLayout.NORTH); + choicesPanel.add(choicesContainer, BorderLayout.CENTER); + choicesPanel.add(addChoiceBtn, BorderLayout.SOUTH); + + // Add all components + add(headerPanel, BorderLayout.NORTH); + + JPanel contentPanel = new JPanel(); + contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); + contentPanel.setOpaque(false); + contentPanel.add(questionPanel); + contentPanel.add(Box.createVerticalStrut(10)); + contentPanel.add(choicesPanel); + + add(contentPanel, BorderLayout.CENTER); + } + + private void addChoice(Question.Choice existingChoice) { + ChoicePanel choicePanel = new ChoicePanel( + (char)('A' + choicePanels.size()), + existingChoice + ); + choicePanels.add(choicePanel); + choicesContainer.add(choicePanel); + choicesContainer.add(Box.createVerticalStrut(5)); + revalidate(); + repaint(); + } + + private void deleteQuestion() { + int result = JOptionPane.showConfirmDialog( + EditQuizDialog.this, + "Are you sure you want to delete this question?", + "Confirm Delete", + JOptionPane.YES_NO_OPTION + ); + + if (result == JOptionPane.YES_OPTION) { + questionPanels.remove(this); + questionsPanel.remove(this); + + for (int i = 0; i < questionPanels.size(); i++) { + questionPanels.get(i).questionNumber = i + 1; + questionPanels.get(i).updateNumber(); + } + + questionsPanel.revalidate(); + questionsPanel.repaint(); + } + } + + private void updateNumber() { + Component[] components = ((JPanel)getComponent(0)).getComponents(); + if (components.length > 0 && components[0] instanceof JLabel) { + ((JLabel)components[0]).setText("Question " + questionNumber); + } + } + + public String getQuestionText() { + return questionTextArea.getText().trim(); + } + + public Map getChoices() { + Map choices = new HashMap<>(); + for (ChoicePanel cp : choicePanels) { + String text = cp.getChoiceText(); + if (!text.isEmpty()) { + choices.put(text, cp.isCorrect()); + } + } + return choices; + } + + private class ChoicePanel extends JPanel { + private char letter; + private JTextField choiceField; + private JCheckBox correctCheckbox; + + public ChoicePanel(char letter, Question.Choice existingChoice) { + this.letter = letter; + + setLayout(new BorderLayout(10, 0)); + setOpaque(false); + setMaximumSize(new Dimension(Integer.MAX_VALUE, 35)); + + JLabel letterLabel = new JLabel(letter + ")"); + letterLabel.setFont(new Font("Segoe UI", Font.BOLD, 14)); + letterLabel.setPreferredSize(new Dimension(30, 30)); + + choiceField = new JTextField(); + choiceField.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + if (existingChoice != null) { + choiceField.setText(existingChoice.getText()); + } + + correctCheckbox = new JCheckBox("Correct"); + correctCheckbox.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + correctCheckbox.setOpaque(false); + if (existingChoice != null) { + correctCheckbox.setSelected(existingChoice.isCorrect()); + } + + JButton deleteBtn = new JButton("✖"); + deleteBtn.setFont(new Font("Arial", Font.BOLD, 12)); + deleteBtn.setBackground(new Color(204, 0, 0)); + deleteBtn.setForeground(Color.WHITE); + deleteBtn.setFocusPainted(false); + deleteBtn.setPreferredSize(new Dimension(30, 30)); + deleteBtn.addActionListener(e -> deleteChoice()); + + JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); + rightPanel.setOpaque(false); + rightPanel.add(correctCheckbox); + rightPanel.add(deleteBtn); + + add(letterLabel, BorderLayout.WEST); + add(choiceField, BorderLayout.CENTER); + add(rightPanel, BorderLayout.EAST); + } + + private void deleteChoice() { + if (choicePanels.size() <= 2) { + JOptionPane.showMessageDialog( + EditQuizDialog.this, + "A question must have at least 2 choices", + "Cannot Delete", + JOptionPane.WARNING_MESSAGE + ); + return; + } + + choicePanels.remove(this); + choicesContainer.remove(this); + + for (int i = 0; i < choicePanels.size(); i++) { + choicePanels.get(i).letter = (char)('A' + i); + choicePanels.get(i).updateLetter(); + } + + choicesContainer.revalidate(); + choicesContainer.repaint(); + } + + private void updateLetter() { + Component[] components = getComponents(); + if (components.length > 0 && components[0] instanceof JLabel) { + ((JLabel)components[0]).setText(letter + ")"); + } + } + + public String getChoiceText() { + return choiceField.getText().trim(); + } + + public boolean isCorrect() { + return correctCheckbox.isSelected(); + } + } + } + + public static void showDialog(Lesson lesson, Course course, int instructorId, Frame parent) { + EditQuizDialog dialog = new EditQuizDialog(parent, lesson, course, instructorId); + dialog.setVisible(true); + } +} \ No newline at end of file diff --git a/src/pages/ManageCourseFrame.java b/src/pages/ManageCourseFrame.java index a57bc47..15660b1 100644 --- a/src/pages/ManageCourseFrame.java +++ b/src/pages/ManageCourseFrame.java @@ -150,27 +150,34 @@ private static void refreshLessons() { lessonItemPanel.setLayout(new BorderLayout(10, 0)); lessonItemPanel.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2)); lessonItemPanel.setBackground(Color.WHITE); - lessonItemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 100)); + lessonItemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 120)); LessonCard card = new LessonCard(); card.setData(false, lesson.getTitle(), lesson.getContent(), false, e -> {}); JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS)); buttonPanel.setOpaque(false); + buttonPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - JButton editBtn = new JButton("Edit"); + // Edit Lesson Button + JButton editBtn = new JButton("Edit Lesson"); editBtn.setBackground(new Color(255, 153, 0)); editBtn.setForeground(Color.WHITE); editBtn.setFont(new Font("Arial", Font.BOLD, 14)); editBtn.setFocusPainted(false); + editBtn.setAlignmentX(Component.CENTER_ALIGNMENT); + editBtn.setMaximumSize(new Dimension(150, 30)); editBtn.addActionListener(e -> EditLessonDialog.showDialog(currentCourse, lesson, instance)); - JButton deleteBtn = new JButton("Delete"); + // Delete Lesson Button + JButton deleteBtn = new JButton("Delete Lesson"); deleteBtn.setBackground(new Color(204, 0, 0)); deleteBtn.setForeground(Color.WHITE); deleteBtn.setFont(new Font("Arial", Font.BOLD, 14)); deleteBtn.setFocusPainted(false); + deleteBtn.setAlignmentX(Component.CENTER_ALIGNMENT); + deleteBtn.setMaximumSize(new Dimension(150, 30)); deleteBtn.addActionListener(e -> { int result = JOptionPane.showConfirmDialog(instance, "Are you sure you want to delete this lesson?", @@ -185,7 +192,42 @@ private static void refreshLessons() { } }); + // Add/Edit Quiz Button + JButton quizBtn = new JButton(lesson.getQuiz() == null ? "Add Quiz" : "Edit Quiz"); + quizBtn.setBackground(lesson.getQuiz() == null ? new Color(0, 153, 51) : new Color(0, 102, 204)); + quizBtn.setForeground(Color.WHITE); + quizBtn.setFont(new Font("Arial", Font.BOLD, 14)); + quizBtn.setFocusPainted(false); + quizBtn.setAlignmentX(Component.CENTER_ALIGNMENT); + quizBtn.setMaximumSize(new Dimension(150, 30)); + quizBtn.addActionListener(e -> { + if (lesson.getQuiz() == null) { + // Add new quiz + AddQuizDialog.showDialog(lesson, currentCourse, currentInstructorId, instance); + } else { + // Edit existing quiz + EditQuizDialog.showDialog(lesson, currentCourse, currentInstructorId, instance); + } + }); + + // Quiz Info Label (if quiz exists) + if (lesson.getQuiz() != null) { + JLabel quizInfoLabel = new JLabel(String.format( + "
Quiz: %d questions
Pass: %.0f%%
", + lesson.getQuiz().getQuestions().size(), + lesson.getQuiz().getPassingScore() + )); + quizInfoLabel.setFont(new Font("Arial", Font.PLAIN, 11)); + quizInfoLabel.setForeground(new Color(0, 102, 204)); + quizInfoLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + buttonPanel.add(quizInfoLabel); + buttonPanel.add(Box.createVerticalStrut(5)); + } + buttonPanel.add(editBtn); + buttonPanel.add(Box.createVerticalStrut(5)); + buttonPanel.add(quizBtn); + buttonPanel.add(Box.createVerticalStrut(5)); buttonPanel.add(deleteBtn); lessonItemPanel.add(card, BorderLayout.CENTER); diff --git a/src/resources/courses.json b/src/resources/courses.json index 0030118..3d70081 100644 --- a/src/resources/courses.json +++ b/src/resources/courses.json @@ -520,39 +520,6 @@ } ], "pending" : true, "title" : "Mobile App Development" -}, { - "approved" : false, - "approvedBy" : null, - "enrolledStudents" : [ 5 ], - "id" : 7, - "instructorId" : 4, - "lessons" : [ { - "content" : "l;fja;df", - "id" : 14, - "quiz" : null, - "studentProgress" : { - "5" : { - "attempts" : [ ], - "lessonComplete" : false, - "userId" : 5 - } - }, - "title" : "lesson1" - }, { - "content" : "dgsfg", - "id" : 15, - "quiz" : null, - "studentProgress" : { - "5" : { - "attempts" : [ ], - "lessonComplete" : false, - "userId" : 5 - } - }, - "title" : "lesson2" - } ], - "pending" : true, - "title" : "prog" }, { "approved" : true, "approvedBy" : 3, @@ -725,4 +692,114 @@ } ], "pending" : false, "title" : "Advanced Mathematics" +}, { + "approved" : false, + "approvedBy" : null, + "enrolledStudents" : [ 5 ], + "id" : 7, + "instructorId" : 4, + "lessons" : [ { + "content" : "l;fja;df", + "id" : 14, + "quiz" : { + "id" : 9, + "passingScore" : 75.0, + "questions" : [ { + "choices" : [ { + "correct" : false, + "text" : "Liverpool" + }, { + "correct" : true, + "text" : "Zamalek" + }, { + "correct" : false, + "text" : "Bayern Munich" + }, { + "correct" : false, + "text" : "Real Madrid" + } ], + "header" : "Which football club does Abdo support?" + }, { + "choices" : [ { + "correct" : true, + "text" : "Grilled Chicken" + }, { + "correct" : false, + "text" : "Grilled Fish" + }, { + "correct" : false, + "text" : "Koshary" + }, { + "correct" : false, + "text" : "Molokhia" + } ], + "header" : "What is Abdo's favorite dish" + } ], + "retries" : 3 + }, + "studentProgress" : { + "5" : { + "attempts" : [ { + "correctQuestions" : 1, + "finishTime" : "2025-11-23T18:32:17.2047075", + "passed" : false, + "questionAnswers" : { + "0" : 1, + "1" : 2 + }, + "quizId" : 9, + "score" : 50.0, + "startTime" : "2025-11-23T18:32:02.9845366", + "status" : "finished", + "userId" : 5, + "wrongQuestions" : 1 + }, { + "correctQuestions" : 0, + "finishTime" : "2025-11-23T18:32:55.9808412", + "passed" : false, + "questionAnswers" : { + "0" : 0, + "1" : 2 + }, + "quizId" : 9, + "score" : 0.0, + "startTime" : "2025-11-23T18:32:48.7736041", + "status" : "finished", + "userId" : 5, + "wrongQuestions" : 2 + }, { + "correctQuestions" : 2, + "finishTime" : "2025-11-23T18:33:10.8136471", + "passed" : true, + "questionAnswers" : { + "0" : 1, + "1" : 0 + }, + "quizId" : 9, + "score" : 100.0, + "startTime" : "2025-11-23T18:33:04.3553791", + "status" : "finished", + "userId" : 5, + "wrongQuestions" : 0 + } ], + "lessonComplete" : true, + "userId" : 5 + } + }, + "title" : "lesson1" + }, { + "content" : "dgsfg", + "id" : 15, + "quiz" : null, + "studentProgress" : { + "5" : { + "attempts" : [ ], + "lessonComplete" : false, + "userId" : 5 + } + }, + "title" : "lesson2" + } ], + "pending" : true, + "title" : "prog" } ] \ No newline at end of file From e6a39700e904e95f25e7ec2cee11a2ff91550407 Mon Sep 17 00:00:00 2001 From: Andrew Sameh Adel Date: Sun, 23 Nov 2025 18:35:31 +0200 Subject: [PATCH 4/6] fix courses.json attempts --- src/resources/courses.json | 102 +++++++++---------------------------- 1 file changed, 25 insertions(+), 77 deletions(-) diff --git a/src/resources/courses.json b/src/resources/courses.json index 0030118..04d8b0c 100644 --- a/src/resources/courses.json +++ b/src/resources/courses.json @@ -96,19 +96,8 @@ }, "studentProgress" : { "1" : { - "attempts" : [ { - "correctQuestions" : 4, - "finishTime" : null, - "passed" : true, - "questionAnswers" : { }, - "quizId" : 1, - "score" : 80.0, - "startTime" : null, - "status" : null, - "userId" : 1, - "wrongQuestions" : 0 - } ], - "lessonComplete" : true, + "attempts" : [ ], + "lessonComplete" : false, "userId" : 1 } }, @@ -196,18 +185,7 @@ }, "studentProgress" : { "1" : { - "attempts" : [ { - "correctQuestions" : 3, - "finishTime" : null, - "passed" : false, - "questionAnswers" : { }, - "quizId" : 2, - "score" : 60.0, - "startTime" : null, - "status" : null, - "userId" : 1, - "wrongQuestions" : 0 - } ], + "attempts" : [ ], "lessonComplete" : false, "userId" : 1 } @@ -293,19 +271,8 @@ }, "studentProgress" : { "1" : { - "attempts" : [ { - "correctQuestions" : 4, - "finishTime" : null, - "passed" : true, - "questionAnswers" : { }, - "quizId" : 3, - "score" : 100.0, - "startTime" : null, - "status" : null, - "userId" : 1, - "wrongQuestions" : 0 - } ], - "lessonComplete" : true, + "attempts" : [ ], + "lessonComplete" : false, "userId" : 1 } }, @@ -441,30 +408,8 @@ }, "studentProgress" : { "1" : { - "attempts" : [ { - "correctQuestions" : 7, - "finishTime" : null, - "passed" : true, - "questionAnswers" : { }, - "quizId" : 4, - "score" : 87.5, - "startTime" : null, - "status" : null, - "userId" : 1, - "wrongQuestions" : 0 - }, { - "correctQuestions" : 6, - "finishTime" : null, - "passed" : true, - "questionAnswers" : { }, - "quizId" : 4, - "score" : 75.0, - "startTime" : null, - "status" : null, - "userId" : 1, - "wrongQuestions" : 0 - } ], - "lessonComplete" : true, + "attempts" : [ ], + "lessonComplete" : false, "userId" : 1 } }, @@ -600,8 +545,22 @@ }, "studentProgress" : { "1" : { - "attempts" : [ ], - "lessonComplete" : false, + "attempts" : [ { + "correctQuestions" : 2, + "finishTime" : "2025-11-23T18:29:11.484621", + "passed" : true, + "questionAnswers" : { + "0" : 0, + "1" : 0 + }, + "quizId" : 5, + "score" : 100.0, + "startTime" : "2025-11-23T18:29:05.034621", + "status" : "finished", + "userId" : 1, + "wrongQuestions" : 0 + } ], + "lessonComplete" : true, "userId" : 1 }, "5" : { @@ -700,19 +659,8 @@ }, "studentProgress" : { "1" : { - "attempts" : [ { - "correctQuestions" : 17, - "finishTime" : null, - "passed" : true, - "questionAnswers" : { }, - "quizId" : 8, - "score" : 85.0, - "startTime" : null, - "status" : null, - "userId" : 1, - "wrongQuestions" : 0 - } ], - "lessonComplete" : true, + "attempts" : [ ], + "lessonComplete" : false, "userId" : 1 }, "5" : { From 56542193cce12a34b387de30a6cbfb3a27594b48 Mon Sep 17 00:00:00 2001 From: Andrew Sameh Adel Date: Sun, 23 Nov 2025 19:09:20 +0200 Subject: [PATCH 5/6] fix quiz delete --- src/databases/CourseDatabase.java | 8 ++++++++ src/models/Progress.java | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/src/databases/CourseDatabase.java b/src/databases/CourseDatabase.java index be1536a..6808cce 100644 --- a/src/databases/CourseDatabase.java +++ b/src/databases/CourseDatabase.java @@ -60,14 +60,22 @@ public void deleteLesson(int id) { for (Course c : getRecords()) { c.getLessons().removeIf((t) -> t.getId() == id); } + saveToFile(); } public void deleteQuiz(int id) { for (Course c : getRecords()) { for (Lesson l : c.getLessons()) { if (l.getQuiz() != null && l.getQuiz().getId() == id) l.setQuiz(null); + + // Delete all student attempts + for (Map.Entry entry : l.getStudentProgress().entrySet()) { + entry.getValue().removeAttempts(id); + } } } + + saveToFile(); } public Course getCourseById(int id) {return getRecordById(id);} diff --git a/src/models/Progress.java b/src/models/Progress.java index e340914..29efd9c 100644 --- a/src/models/Progress.java +++ b/src/models/Progress.java @@ -20,6 +20,10 @@ public void addAttempt(QuizAttempt attempt) { if (attempt.isPassed()) this.lessonComplete = true; } + public void removeAttempts(int quizId) { + this.attempts.removeIf((attempt -> attempt.getQuizId() == quizId)); + } + /* Getters & Setters */ public int getUserId() {return userId;} public void setUserId(int userId) {this.userId = userId;} From 61f69321711eb644be6fb7795d723665c88cf6cc Mon Sep 17 00:00:00 2001 From: Andrew Sameh Adel Date: Sun, 23 Nov 2025 20:34:12 +0200 Subject: [PATCH 6/6] remove md --- IMPLEMENTATION_SUMMARY.md | 249 -------------------------------------- 1 file changed, 249 deletions(-) delete mode 100644 IMPLEMENTATION_SUMMARY.md diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index d9b7729..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,249 +0,0 @@ -# Implementation Summary - -## Overview -This document summarizes all changes made to implement the quiz functionality, certificate download, and enhanced user experience features. - ---- - -## 1. Files Modified - -### Core Model Classes -- `src/models/QuizAttempt.java` - Enhanced with timestamp, status, question answers, and wrong question count - -### Database Classes -- `src/databases/CourseDatabase.java` - Added methods for starting, updating, finishing, and abandoning quiz attempts - -### Service Classes -- `src/services/CourseService.java` - Added null safety checks in getCertificate method -- `src/services/CertificateService.java` - NEW: Complete PDF certificate generation service - -### Page Classes -- `src/pages/StudentDashBoard.java` - Added Download Certificate button for completed courses -- `src/pages/StudentLessons.java` - Updated to use new quiz-based lesson card system -- `src/pages/components/LessonCard.java` - Completely redesigned to show "Take Quiz" button and attempts history icon - -### NEW Page Classes -- `src/pages/QuizView.java` - Complete quiz taking interface with Back/Finish buttons, question navigation, and review mode -- `src/pages/AttemptsSummaryDialog.java` - Dialog showing attempt history with View Details button -- `src/pages/AttemptDetailsDialog.java` - Dialog showing detailed quiz attempt results with color-coded answers - -### Configuration Files -- `pom.xml` - Added Apache PDFBox dependency for PDF certificate generation - ---- - -## 2. New Services/Endpoints Added - -### CertificateService -- `generateCertificatePDF(int studentId, int courseId)` - Generates professional PDF certificate -- `downloadCertificate(String filepath)` - Triggers file download -- `getOrCreateCertificate(int studentId, int courseId)` - Gets or creates certificate for student -- `isCourseCompleted(int studentId, int courseId)` - Checks if course is completed -- `generateCertificateCode(int certId, int studentId, int courseId)` - Generates unique certificate code -- `sanitizeFilename(String name)` - Sanitizes filenames for safe storage - -### CourseDatabase (Extended Methods) -- `startQuizAttempt(int studentId, int quizId)` - Creates new quiz attempt with "started" status -- `updateQuizAttemptAnswers(int studentId, int quizId, int attemptIndex, Map answers)` - Updates attempt with student answers -- `finishQuizAttempt(int studentId, int quizId, int attemptIndex, Map answers)` - Finishes attempt, grades it, and marks as "finished" -- `abandonQuizAttempt(int studentId, int quizId, int attemptIndex)` - Marks attempt as "abandoned" - ---- - -## 3. Database Schema Changes - -### QuizAttempt Model Enhanced -Added fields: -- `startTime` (LocalDateTime) - When attempt was started -- `finishTime` (LocalDateTime) - When attempt was finished or abandoned -- `status` (String) - "started", "finished", or "abandoned" -- `questionAnswers` (Map) - Maps question index to selected choice index -- `wrongQuestions` (int) - Count of wrong answers - -### Certificate Model -No changes needed - existing model supports certificate functionality - ---- - -## 4. UI Flow Notes - -### Student Dashboard (Course Cards) -- **Location**: Student Dashboard → My Courses section -- **New Feature**: "Download Certificate" button appears below "View Course Lessons" button -- **Visibility**: Only shown when course is completed -- **Action**: Generates and downloads PDF certificate - -### Student Lessons Panel (Lesson Cards) -- **Location**: Student Dashboard → View Course Lessons → Lessons list -- **New Features**: - - "Take Quiz" button replaces "Complete" button for lessons with quizzes - - "Completed" button (green, disabled) shown when all attempts used or lesson completed - - Attempts history icon (📋) appears to the left of "Take Quiz" when student has previous attempts -- **Actions**: - - Click "Take Quiz" → Confirmation dialog → QuizView opens - - Click attempts history icon → AttemptsSummaryDialog opens - -### Quiz View Panel -- **Location**: Started from Lesson Card → Take Quiz button -- **Features**: - - Back button (top left) - Shows warning if leaving before finish - - Attempt info (top center) - Shows attempt number and answered count - - Question display (center) - Shows current question with multiple choice options - - Previous/Next buttons (bottom) - Navigate between questions - - Finish button (bottom right) - Validates all questions answered, confirms, grades, and shows review -- **Review Mode**: - - Highlights correct answers in green with ✓ - - Highlights wrong answers in red with ✗ - - Shows "Your Answer" for selected incorrect answers - - Displays overall score and pass/fail status - -### Attempts Summary Dialog -- **Location**: Lesson Card → Attempts history icon (📋) -- **Features**: - - Shows max attempts allowed - - Shows attempts used - - Table listing all attempts with: - - Attempt number - - Date/Time - - Status (started/finished/abandoned) - - Score - - Correct/Wrong counts - - "View Details" button for each attempt - -### Attempt Details Dialog -- **Location**: Attempts Summary Dialog → View Details button -- **Features**: - - Shows attempt summary (start/finish time, status, score) - - Lists all questions with: - - Student's selected answer - - Correct answer - - Color coding (green for correct, red for wrong) - - Visual indicators (✓ and ✗) - ---- - -## 5. Configuration & Third-Party Libraries - -### Dependencies Added -- **Apache PDFBox 2.0.29** - For PDF certificate generation - - Used for creating professional landscape certificate PDFs - - Supports fonts, colors, borders, and formatting - -### Configuration Notes -- Certificate PDFs are stored in `certificates/` directory (created automatically) -- Filename format: `certificate_[StudentName]_[CourseTitle]_[IssueDate]_[CertificateCode].pdf` - ---- - -## 6. Manual Test Steps - -### Test 1: Certificate Download for Completed Course -1. Log in as a student -2. Complete a course (all lessons with quizzes passed) -3. Go to Student Dashboard -4. Verify "Download Certificate" button appears on completed course card -5. Click "Download Certificate" -6. Verify toast message appears: "Generating certificate — preparing your download..." -7. Verify PDF opens/downloads successfully -8. Verify PDF contains correct student name, course title, issue date, and certificate code - -### Test 2: Lesson Card Quiz Button Behavior -1. Log in as a student -2. Navigate to a course with lessons containing quizzes -3. Verify lesson cards show "Take Quiz" button (not "Complete") -4. If student has previous attempts, verify attempts history icon (📋) appears -5. If all attempts used, verify "Completed" button (green, disabled) appears - -### Test 3: Take Quiz Flow -1. Click "Take Quiz" on a lesson card -2. Verify confirmation dialog appears with message: "Are you sure you want to start the quiz for '[Lesson Title]'? This will use one attempt." -3. Click "Yes" -4. Verify QuizView opens -5. Answer all questions (navigate with Previous/Next buttons) -6. Verify "Answered: X/Y" count updates -7. Click "Finish" -8. Verify warning if not all questions answered: "You must finish the quiz before clicking Finish. Answer all required questions." -9. Answer all questions and click "Finish" again -10. Verify confirmation: "Are you sure you want to finish your attempt now? You will not be able to change your answers." -11. Click "Yes" -12. Verify review mode shows with correct answers in green, wrong in red -13. Verify score and pass/fail status displayed - -### Test 4: Back Button Warning -1. Start a quiz attempt -2. Answer some questions -3. Click "Back" button -4. Verify warning appears: "IF you leave now you will lose this attempt in this quiz." -5. Click "Yes" (Leave) -6. Verify attempt is marked as "abandoned" and returns to Student Lessons panel - -### Test 5: Attempts Summary Dialog -1. Click attempts history icon (📋) on a lesson card with previous attempts -2. Verify AttemptsSummaryDialog opens -3. Verify it shows: - - Max attempts allowed - - Attempts used count - - Table with all attempts -4. Click "View Details" on an attempt -5. Verify AttemptDetailsDialog opens showing detailed results - -### Test 6: Attempt Details Dialog -1. Open Attempt Details from Attempts Summary -2. Verify it shows: - - Start and finish times - - Status - - Score (with color coding) - - Correct/Wrong counts - - All questions with: - - Student's answer highlighted - - Correct answer highlighted - - Color coding (green/red) - -### Test 7: Course Completion Check -1. Complete all lessons in a course (pass all quizzes) -2. Verify course completion status updates -3. Return to Student Dashboard -4. Verify "Download Certificate" button appears on course card -5. Verify certificate can be generated and downloaded - ---- - -## 7. Known Issues & Considerations - -### Notes -- Certificate generation requires file system write permissions -- PDFBox library must be downloaded via Maven (check `pom.xml` dependencies) -- Certificate PDFs are stored in project root `certificates/` directory -- Quiz attempts are stored in JSON database files -- All attempts are tracked even if abandoned - -### Recommendations -- Consider adding file size limits for certificate storage -- Consider adding certificate expiration dates if needed -- Consider adding attempt time limits for quizzes -- Consider adding quiz retake policies (wait period between attempts) - ---- - -## 8. Acceptance Criteria Verification - -✅ **Certificate Download**: Course card shows Download Certificate only for completed courses; clicking downloads correctly populated PDF - -✅ **Lesson Card Quiz Button**: Displays Take Quiz, Attempts icon (when applicable), or Completed (green, disabled) depending on student state - -✅ **Take Quiz Confirmation**: Requires confirmation, creates attempt, and navigates to Quiz Panel - -✅ **Back Before Finish Warning**: Warns and abandons attempt when confirmed - -✅ **Finish Validation**: Enforces answering all required questions, confirms finish, grades attempt, and shows review mode with red/green highlights - -✅ **Attempts Summary Dialog**: Shows correct attempt metadata and detailed per-question views - -✅ **Certificate Generation**: Generation and storage work; certificates can be verified/downloaded by student - ---- - -## Implementation Complete ✓ - -All features have been implemented and integrated according to specifications. -