export type QuestionStatus = 'not_started' | 'in_progress' | 'completed' | 'skipped'; export interface QuizQuestion { id: number; question: string | { th: string; en: string }; is_skippable: boolean; type: string; choices?: { id: number; text: string | { th: string; en: string } }[]; } export interface AnswerState { questionId: number; value: any; is_saved: boolean; status: QuestionStatus; touched: boolean; last_saved_at?: string; } /** * @composable useQuizRunner * @description จัดการ State (สถานะ) และ Logic สำหรับการทำแบบทดสอบ (Quiz) * ครอบคลุมการแสดงคำถาม, บันทึกคำตอบ, และตรวจสอบการข้ามคำถาม */ export const useQuizRunner = () => { // ================= State (สถานะเก็บค่าต่างๆ ของข้อสอบ) ================= const questions = useState('quiz-questions', () => []); // เก็บรายการคำถามทั้งหมด const answers = useState>('quiz-answers', () => ({})); // เก็บคำตอบที่ผู้ใช้ตอบ แยกตาม ID คำถาม const currentQuestionIndex = useState('quiz-current-index', () => 0); // ลำดับคำถามที่กำลังทำอยู่ปัจจุบัน (เริ่มที่ 0) const loading = useState('quiz-loading', () => false); // สถานะตอนกำลังกดเซฟหรือโหลดข้อมูล const lastError = useState('quiz-error', () => null); // เก็บข้อความแจ้งเตือนข้อผิดพลาดล่าสุด // ================= Getters (ดึงค่าที่ถูกประมวลผลแล้ว) ================= const currentQuestion = computed(() => questions.value[currentQuestionIndex.value]); // ดึงคำถามข้อปัจจุบัน const currentAnswer = computed(() => { // ดึงคำตอบในข้อปัจจุบัน if (!currentQuestion.value) return null; return answers.value[currentQuestion.value.id]; }); const totalQuestions = computed(() => questions.value.length); // จำนวนคำถามทั้งหมดในแบบทดสอบ const isLastQuestion = computed(() => currentQuestionIndex.value === questions.value.length - 1); // เช็คว่าใช่คำถามข้อสุดท้ายหรือไม่ const isFirstQuestion = computed(() => currentQuestionIndex.value === 0); // เช็คว่าใช่คำถามข้อแรกหรือไม่ // ================= Actions (ฟังก์ชันหลักสำหรับการทำงาน) ================= // ฟังก์ชันเริ่มต้นสร้าง/โหลดข้อสอบ (กำหนดโครงสร้างพื้นฐาน) function initQuiz(quizData: any) { if (!quizData || !quizData.questions) return; questions.value = quizData.questions; currentQuestionIndex.value = 0; // รีเซ็ตไปที่ข้อ 1 ใหม่ answers.value = {}; lastError.value = null; // เตรียมโครงสร้างคำตอบรองรับทุกข้อ questions.value.forEach(q => { answers.value[q.id] = { questionId: q.id, value: null, is_saved: false, // บันทึกและส่ง API เรียบร้อยหรือยัง status: 'not_started', // สถานะเริ่มต้นของคำถาม touched: false, // ผู้ใช้เคยเปิดเข้ามาดูข้อนีัหรือยัง }; }); // เริ่มต้นบันทึกเวลา/เข้าสู่ข้อที่ 1 ทันทีเมื่ออธิบายเสร็จ if (questions.value.length > 0) { enterQuestion(questions.value[0].id); } } // ฟังก์ชันสลับสถานะเมื่อกดเข้ามาที่คำถามนั้นๆ function enterQuestion(qId: number) { const ans = answers.value[qId]; if (ans) { ans.touched = true; if (ans.status === 'not_started' || ans.status === 'skipped') { ans.status = 'in_progress'; // เปลี่ยนสถานะเป็น 'กำลังทำ' } } } // ตรวจสอบเงื่อนไขว่าผู้ใช้สามารถออกจากคำถามปัจจุบันไปยังข้ออื่นได้หรือไม่ function canLeaveCurrent(): { allowed: boolean; reason?: string } { if (!currentQuestion.value) return { allowed: true }; const q = currentQuestion.value; const a = answers.value[q.id]; // สามารถออกได้ถ้าคำถามทำถูกหรือโจทย์อนุญาตให้ข้ามได้ if (a.status === 'completed' || a.is_saved) return { allowed: true }; if (q.is_skippable) return { allowed: true }; // บังคับให้ตอบถ้าไม่ได้อนุญาตให้ข้าม และไม่ได้ตอบ if (!a.is_saved && a.value === null) { return { allowed: false, reason: 'ต้องการคำตอบสำหรับข้อบังคับนี้' }; } return { allowed: true }; } // ฟังก์ชันอัปเดตค่าตัวเลือกที่ผู้ใช้กดเลือกในข้อปัจจุบัน function updateAnswer(val: any) { if (!currentQuestion.value) return; const qId = currentQuestion.value.id; answers.value[qId].value = val; // หากมีแก้ไขคำตอบหลังจากกดเซฟไปแล้ว ให้เปลี่ยนสถานะให้ระบบรู้ว่าต้องเซฟใหม่ if (answers.value[qId].is_saved) { answers.value[qId].is_saved = false; answers.value[qId].status = 'in_progress'; } } // ล็อกและบันทึกข้อสอบเมื่อกดปุ่ม "ตกลง/ส่งคำตอบ" สำหรับข้อนั้นๆ async function saveCurrentAnswer() { if (!currentQuestion.value) return; const qId = currentQuestion.value.id; const ans = answers.value[qId]; if (ans.value === null) { lastError.value = "กรุณาเลือกคำตอบอย่างน้อย 1 ตัวเลือก"; return false; } loading.value = true; lastError.value = null; try { // หมายเหตุ: การเชื่อมต่อ API หลักต้องทำที่ไฟล์ component, ตัวนี้จัดการแค่เรื่อง State ans.is_saved = true; ans.status = 'completed'; // มาร์คว่าเป็นข้อที่ทำเสร็จแล้ว ans.last_saved_at = new Date().toISOString(); return true; } catch (e) { lastError.value = "เกิดข้อผิดพลาดในการบันทึกคำตอบ"; return false; } finally { loading.value = false; } } // วินิจฉัยก่อนสั่งผู้ใช้ย้ายไปยังคำถามอื่นหน้าอื่นตามดัชนีระบุ function handleLeaveLogic(targetIndex: number) { if (targetIndex === currentQuestionIndex.value) return; // ตรวจสอบขั้นสุดท้าย ป้องกันคนคลิกแอบหนีข้อที่บังคับทำ const check = canLeaveCurrent(); if (!check.allowed) { lastError.value = check.reason || "จำเป็นต้องตอบข้อนี้ก่อนข้าม"; return false; } const currQ = currentQuestion.value; if (currQ) { const currAns = answers.value[currQ.id]; // หากผู้ใช้ทิ้งขว้างโดยที่ไม่บังคับ ให้ทิ้งสถานะเป็นข้าม ('skipped') if (currAns.status !== 'completed' && !currAns.is_saved) { currAns.status = 'skipped'; } } currentQuestionIndex.value = targetIndex; lastError.value = null; // ติดตามสถานะ 'touched' ในข้อใหม่ที่เข้าไปล่าสุด if (questions.value[targetIndex]) { enterQuestion(questions.value[targetIndex].id); } return true; } return { questions, answers, currentQuestionIndex, loading, lastError, currentQuestion, currentAnswer, totalQuestions, isFirstQuestion, isLastQuestion, initQuiz, updateAnswer, saveCurrentAnswer, nextQuestion: () => handleLeaveLogic(currentQuestionIndex.value + 1), prevQuestion: () => handleLeaveLogic(currentQuestionIndex.value - 1), goToQuestion: (index: number) => handleLeaveLogic(index) }; };