File "examSlice.ts"

Full Path: /home/trinadezambia/public_html/student_panel/src/components/store/slices/examSlice.ts
File size: 11.32 KB
MIME-type: text/x-java
Charset: utf-8

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

/**
 * Offline Exam interface for Redux store
 * Based on offline exam API response structure
 */
export interface OfflineExamData {
  id: number;
  name: string;
  description: string;
  exam_starting_date: string;
  exam_ending_date: string;
  publish: number;
  session_year: string;
  exam_status: string;
}

/**
 * Online Exam interface for Redux store
 * Based on online exam API response structure
 */
export interface OnlineExamData {
  id: number;
  title: string;
  subject_with_name: string;
  start_date: string;
  end_date: string;
  total_marks: number;
  exam_key: number;
  duration: number;
  exam_status_name: string;
}

/**
 * Online Exam Question Option Interface
 * Represents a single option for a question
 */
export interface OnlineExamQuestionOption {
  id: number;
  option: string;
  is_answer: number; // 1 = correct answer, 0 = wrong answer
}

/**
 * Online Exam Question Interface
 * Represents a single question in the exam
 */
export interface OnlineExamQuestion {
  id: number;
  question: string;
  options: OnlineExamQuestionOption[];
  marks: number;
  image: string | null;
  note: string | null;
}

/**
 * Student Answer Interface
 * Represents a student's answer to a question
 */
export interface StudentAnswer {
  questionId: number;
  optionId: number;
  isCorrect: boolean;
}

/**
 * Union type for exam data
 */
export type ExamData = OfflineExamData | OnlineExamData;

/**
 * Exam state interface
 * Defines the structure of the exam slice state
 */
interface ExamState {
  // Store all exams data
  exams: ExamData[];

  // Store currently selected exam for exam key validation
  selectedExam: ExamData | null;

  // Store exam keys for validation (examId -> examKey mapping)
  examKeys: Record<number, number>;

  // Store exam questions and answers
  examQuestions: OnlineExamQuestion[];
  studentAnswers: StudentAnswer[];
  currentQuestionIndex: number;
  examStarted: boolean;
  examCompleted: boolean;

  // Exam metadata
  totalQuestions: number;
  totalMarks: number;
  examKey: string;

  // Loading states
  loading: boolean;

  // Error handling
  error: string | null;
}

// Initial state
const initialState: ExamState = {
  exams: [],
  selectedExam: null,
  examKeys: {},
  examQuestions: [],
  studentAnswers: [],
  currentQuestionIndex: 0,
  examStarted: false,
  examCompleted: false,
  totalQuestions: 0,
  totalMarks: 0,
  examKey: '',
  loading: false,
  error: null,
};

/**
 * Exam Redux Slice
 *
 * Manages exam data, selected exam, and exam key validation
 * Provides actions for storing exam data and validating exam keys
 */
const examSlice = createSlice({
  name: 'exam',
  initialState,
  reducers: {
    /**
     * Set loading state
     */
    setLoading: (state, action: PayloadAction<boolean>) => {
      state.loading = action.payload;
    },

    /**
     * Set error message
     */
    setError: (state, action: PayloadAction<string | null>) => {
      state.error = action.payload;
    },

    /**
     * Clear error message
     */
    clearError: (state) => {
      state.error = null;
    },

    /**
     * Store exams data from API
     * Replaces existing exams with new data
     */
    setExams: (state, action: PayloadAction<ExamData[]>) => {
      state.exams = action.payload;
      state.loading = false;
      state.error = null;
    },

    /**
     * Add or update a single exam
     * Useful for adding new exams or updating existing ones
     */
    setExam: (state, action: PayloadAction<ExamData>) => {
      const examIndex = state.exams.findIndex(
        (exam) => exam.id === action.payload.id
      );

      if (examIndex >= 0) {
        // Update existing exam
        state.exams[examIndex] = action.payload;
      } else {
        // Add new exam
        state.exams.push(action.payload);
      }
    },

    /**
     * Set selected exam for exam key validation
     * This is called when user clicks "Start Exam" button
     */
    setSelectedExam: (state, action: PayloadAction<ExamData | null>) => {
      state.selectedExam = action.payload;
    },

    /**
     * Store exam key for an exam
     * Maps exam ID to its corresponding exam key for validation
     */
    setExamKey: (
      state,
      action: PayloadAction<{ examId: number; examKey: number }>
    ) => {
      const { examId, examKey } = action.payload;
      state.examKeys[examId] = examKey;
    },

    /**
     * Validate exam key against stored exam data
     * Returns true if the provided exam key matches the stored key for the selected exam
     */
    validateExamKey: (state, action: PayloadAction<string>) => {
      const providedKey = parseInt(action.payload);

      if (!state.selectedExam) {
        state.error = 'No exam selected for validation';
        return;
      }

      const storedKey = state.examKeys[state.selectedExam.id];

      if (!storedKey) {
        state.error = 'Exam key not found for this exam';
        return;
      }

      if (providedKey !== storedKey) {
        state.error = 'Invalid exam key. Please check and try again.';
        return;
      }

      // If validation passes, clear any previous errors
      state.error = null;
    },

    /**
     * Clear selected exam and reset validation state
     * Called when modal is closed or exam is completed
     */
    clearSelectedExam: (state) => {
      state.selectedExam = null;
      state.error = null;
    },

    /**
     * Set exam questions from API response
     * Called when exam questions are successfully fetched
     */
    setExamQuestions: (
      state,
      action: PayloadAction<{
        questions: OnlineExamQuestion[];
        totalQuestions: number;
        totalMarks: number;
        examKey: string;
      }>
    ) => {
      const { questions, totalQuestions, totalMarks, examKey } = action.payload;
      state.examQuestions = questions;
      state.totalQuestions = totalQuestions;
      state.totalMarks = totalMarks;
      state.examKey = examKey;
      state.currentQuestionIndex = 0;
      state.studentAnswers = [];
      state.examStarted = true;
      state.examCompleted = false;
      state.loading = false;
      state.error = null;
    },

    /**
     * Set student answer for a question
     * Called when student selects an option
     */
    setStudentAnswer: (
      state,
      action: PayloadAction<{
        questionId: number;
        optionId: number;
        isCorrect: boolean;
      }>
    ) => {
      const { questionId, optionId, isCorrect } = action.payload;

      // Find existing answer for this question
      const existingAnswerIndex = state.studentAnswers.findIndex(
        (answer) => answer.questionId === questionId
      );

      const newAnswer: StudentAnswer = {
        questionId,
        optionId,
        isCorrect,
      };

      if (existingAnswerIndex >= 0) {
        // Update existing answer
        state.studentAnswers[existingAnswerIndex] = newAnswer;
      } else {
        // Add new answer
        state.studentAnswers.push(newAnswer);
      }
    },

    /**
     * Set multiple student answers at once
     * Useful for restoring state from storage
     */
    setStudentAnswers: (state, action: PayloadAction<StudentAnswer[]>) => {
      state.studentAnswers = action.payload;
    },

    /**
     * Navigate to next question
     */
    nextQuestion: (state) => {
      if (state.currentQuestionIndex < state.totalQuestions - 1) {
        state.currentQuestionIndex += 1;
      }
    },

    /**
     * Navigate to previous question
     */
    previousQuestion: (state) => {
      if (state.currentQuestionIndex > 0) {
        state.currentQuestionIndex -= 1;
      }
    },

    /**
     * Navigate to specific question by index
     */
    goToQuestion: (state, action: PayloadAction<number>) => {
      const index = action.payload;
      if (index >= 0 && index < state.totalQuestions) {
        state.currentQuestionIndex = index;
      }
    },

    /**
     * Complete the exam
     * Called when exam is submitted or timer expires
     */
    completeExam: (state) => {
      state.examCompleted = true;
      state.examStarted = false;
    },

    /**
     * Reset exam state
     * Called when starting a new exam or closing exam
     */
    resetExam: (state) => {
      state.examQuestions = [];
      state.studentAnswers = [];
      state.currentQuestionIndex = 0;
      state.examStarted = false;
      state.examCompleted = false;
      state.totalQuestions = 0;
      state.totalMarks = 0;
      state.examKey = '';
      state.error = null;
    },

    /**
     * Clear all exam data
     * Useful for logout or reset functionality
     */
    clearAllExams: (state) => {
      state.exams = [];
      state.selectedExam = null;
      state.examKeys = {};
      state.examQuestions = [];
      state.studentAnswers = [];
      state.currentQuestionIndex = 0;
      state.examStarted = false;
      state.examCompleted = false;
      state.totalQuestions = 0;
      state.totalMarks = 0;
      state.examKey = '';
      state.loading = false;
      state.error = null;
    },
  },
});

// Export actions
export const {
  setLoading,
  setError,
  clearError,
  setExams,
  setExam,
  setSelectedExam,
  setExamKey,
  validateExamKey,
  clearSelectedExam,
  setExamQuestions,
  setStudentAnswer,
  setStudentAnswers,
  nextQuestion,
  previousQuestion,
  goToQuestion,
  completeExam,
  resetExam,
  clearAllExams,
} = examSlice.actions;

// Export reducer
export default examSlice.reducer;

// Selector functions for easy access to state
export const selectExams = (state: { exam: ExamState }) => state.exam.exams;
export const selectSelectedExam = (state: { exam: ExamState }) =>
  state.exam.selectedExam;
export const selectExamKeys = (state: { exam: ExamState }) =>
  state.exam.examKeys;
export const selectExamLoading = (state: { exam: ExamState }) =>
  state.exam.loading;
export const selectExamError = (state: { exam: ExamState }) => state.exam.error;

// New selectors for exam questions and answers
export const selectExamQuestions = (state: { exam: ExamState }) =>
  state.exam.examQuestions;
export const selectStudentAnswers = (state: { exam: ExamState }) =>
  state.exam.studentAnswers;
export const selectCurrentQuestionIndex = (state: { exam: ExamState }) =>
  state.exam.currentQuestionIndex;
export const selectExamStarted = (state: { exam: ExamState }) =>
  state.exam.examStarted;
export const selectExamCompleted = (state: { exam: ExamState }) =>
  state.exam.examCompleted;
export const selectTotalQuestions = (state: { exam: ExamState }) =>
  state.exam.totalQuestions;
export const selectTotalMarks = (state: { exam: ExamState }) =>
  state.exam.totalMarks;
export const selectExamKey = (state: { exam: ExamState }) => state.exam.examKey;

// Computed selectors
export const selectCurrentQuestion = (state: { exam: ExamState }) => {
  const questions = state.exam.examQuestions;
  const currentIndex = state.exam.currentQuestionIndex;
  return questions[currentIndex] || null;
};

export const selectStudentAnswerForQuestion =
  (questionId: number) => (state: { exam: ExamState }) => {
    return (
      state.exam.studentAnswers.find(
        (answer) => answer.questionId === questionId
      ) || null
    );
  };

export const selectAnsweredQuestionsCount = (state: { exam: ExamState }) => {
  return state.exam.studentAnswers.length;
};

export const selectCorrectAnswersCount = (state: { exam: ExamState }) => {
  return state.exam.studentAnswers.filter((answer) => answer.isCorrect).length;
};