-
{{ $t('course.price', 'ราคาคอร์ส') }}
+
{{ $t('course.price') }}
{{ formatPrice(course.price) }}
@@ -180,13 +180,13 @@ const handleEnroll = () => {
-
{{ $t('course.includes', 'คอร์สนี้รวมอะไรบ้าง') }}
+
{{ $t('course.includes') }}
- {{ $t('course.fullLifetimeAccess', 'เข้าเรียนได้ตลอดชีพ') }}
+ {{ $t('course.fullLifetimeAccess') }}
@@ -200,7 +200,7 @@ const handleEnroll = () => {
- {{ $t('course.accessOnMobile', 'เข้าเรียนได้ทุกอุปกรณ์') }}
+ {{ $t('course.accessOnMobile') }}
diff --git a/Frontend-Learner/i18n/locales/en.json b/Frontend-Learner/i18n/locales/en.json
index 768ef6ca..c648a957 100644
--- a/Frontend-Learner/i18n/locales/en.json
+++ b/Frontend-Learner/i18n/locales/en.json
@@ -212,6 +212,15 @@
"instructionTitle": "Instructions",
"instruction1": "Pay attention to the questions to measure your learning progress.",
"startBtn": "Start Quiz",
+ "warningTitle": "Warning",
+ "singleAttemptWarning": "This quiz can only be taken once. If you fail, you will not be able to try again. Do you want to continue?",
+ "continue": "Continue",
+ "alreadyPassed": "You have already passed",
+ "latestScore": "Latest Score",
+ "retryMaybe": "Try Again",
+ "passedStatus": "Passed",
+ "failedStatus": "Failed",
+ "passingScore": "Passing Score",
"nextBtn": "Next Question",
"prevBtn": "Previous Question",
"submitBtn": "Submit Answers",
diff --git a/Frontend-Learner/i18n/locales/th.json b/Frontend-Learner/i18n/locales/th.json
index 5461bfd9..d9e41426 100644
--- a/Frontend-Learner/i18n/locales/th.json
+++ b/Frontend-Learner/i18n/locales/th.json
@@ -212,6 +212,15 @@
"instructionTitle": "คำแนะนำ",
"instruction1": "แบบทดสอบนี้มีไว้เพื่อวัดความรู้ความเข้าใจของคุณในบทเรียนนี้",
"startBtn": "เริ่มทำแบบทดสอบ",
+ "warningTitle": "คำเตือน",
+ "singleAttemptWarning": "แบบทดสอบนี้สามารถทำได้เพียงครั้งเดียวเท่านั้น หากไม่ผ่านคุณจะไม่สามารถทำใหม่ได้อีก คุณต้องการดำเนินการต่อหรือไม่?",
+ "continue": "ดำเนินการต่อ",
+ "alreadyPassed": "คุณสอบผ่านเกณฑ์แล้ว",
+ "latestScore": "คะแนนล่าสุด",
+ "retryMaybe": "ลองใหม่อีกครั้ง",
+ "passedStatus": "ผ่านเกณฑ์",
+ "failedStatus": "ไม่ผ่านเกณฑ์",
+ "passingScore": "เกณฑ์การผ่าน",
"exitTitle": "ออกจากแบบทดสอบ",
"timeLeft": "เวลาที่เหลือ",
"submitConfirm": "คุณต้องการส่งคำตอบหรือไม่?",
diff --git a/Frontend-Learner/pages/classroom/learning.vue b/Frontend-Learner/pages/classroom/learning.vue
index 2d944ea9..f6c06675 100644
--- a/Frontend-Learner/pages/classroom/learning.vue
+++ b/Frontend-Learner/pages/classroom/learning.vue
@@ -6,9 +6,8 @@
* ออกแบบให้เหมือนระบบ LMS มาตรฐาน
*/
-
definePageMeta({
- layout: false, // Custom layout defined within this component
+ layout: false,
middleware: 'auth'
})
@@ -21,10 +20,9 @@ const router = useRouter()
const { t } = useI18n()
const { user } = useAuth()
const { fetchCourseLearningInfo, fetchLessonContent, saveVideoProgress, checkLessonAccess, fetchVideoProgress, fetchCourseAnnouncements, markLessonComplete, getLocalizedText } = useCourse()
-// Media Prefs (Global Volume)
-const { volume, muted: isMuted, setVolume, setMuted, applyTo } = useMediaPrefs()
+const $q = useQuasar()
-// State
+// State management
const sidebarOpen = ref(false)
const courseId = computed(() => Number(route.query.course_id))
@@ -99,6 +97,83 @@ const toggleSidebar = () => {
sidebarOpen.value = !sidebarOpen.value
}
+// Logic Quiz Attempt Management
+const quizStatus = computed(() => {
+ if (!currentLesson.value || currentLesson.value.type !== 'QUIZ' || !currentLesson.value.quiz) return null
+
+ const quiz = currentLesson.value.quiz
+ const latestAttempt = quiz.latest_attempt
+ const allowMultiple = quiz.allow_multiple_attempts
+
+ // If never attempted
+ if (!latestAttempt) {
+ return {
+ canStart: true,
+ label: t('quiz.startBtn'),
+ icon: 'play_arrow',
+ showScore: false
+ }
+ }
+
+ // If multiple attempts allowed
+ if (allowMultiple) {
+ return {
+ canStart: true,
+ label: t('quiz.retryBtn'),
+ icon: 'refresh',
+ showScore: true,
+ score: latestAttempt.score,
+ isPassed: latestAttempt.is_passed
+ }
+ }
+
+ // allowMultiple is false (Single attempt only)
+ // Lock the quiz regardless of pass/fail once attempted
+ return {
+ canStart: false,
+ label: latestAttempt.is_passed ? t('quiz.passedStatus') : t('quiz.failedStatus'),
+ icon: latestAttempt.is_passed ? 'check_circle' : 'cancel',
+ showScore: true,
+ score: latestAttempt.score,
+ isPassed: latestAttempt.is_passed
+ }
+})
+
+const handleStartQuiz = () => {
+ if (!currentLesson.value || !currentLesson.value.quiz) return
+
+ const quiz = currentLesson.value.quiz
+
+ // If multiple attempts are disabled and it's the first time
+ if (!quiz.allow_multiple_attempts && !quiz.latest_attempt) {
+ $q.dialog({
+ title: `
${t('quiz.warningTitle')}
`,
+ message: `
${t('quiz.singleAttemptWarning')}
`,
+ html: true,
+ persistent: true,
+ class: 'rounded-[24px]',
+ ok: {
+ label: t('quiz.continue'),
+ color: 'primary',
+ unelevated: true,
+ rounded: true,
+ class: 'px-8 font-black'
+ },
+ cancel: {
+ label: t('common.cancel'),
+ color: 'grey-7',
+ flat: true,
+ rounded: true,
+ class: 'font-bold'
+ }
+ }).onOk(() => {
+ router.push(`/classroom/quiz?course_id=${courseId.value}&lesson_id=${currentLesson.value.id}`)
+ })
+ } else {
+ router.push(`/classroom/quiz?course_id=${courseId.value}&lesson_id=${currentLesson.value.id}`)
+ }
+}
+
// Helper สำหรับรีเซ็ตข้อมูลและย้ายหน้า (Hard Reload)
const resetAndNavigate = (path: string) => {
if (import.meta.client) {
@@ -117,18 +192,18 @@ const resetAndNavigate = (path: string) => {
}
}
- // 2. ล้างข้อมูลใน localStorage ทั้งหมด
+ // 2. Clear all localStorage
localStorage.clear()
- // 3. นำข้อมูลที่ยกเว้นกลับมาใส่คืน
+ // 3. Restore whitelisted keys
Object.entries(whitelist).forEach(([key, value]) => {
localStorage.setItem(key, value)
})
- // 4. บังคับโหลดหน้าใหม่ทั้งหมด (Hard Reload) ไปที่ path ใหม่
+ // 4. Force hard reload to the new path
window.location.href = path
} else {
- // Fallback สำหรับ SSR
+ // SSR Fallback
router.push(path)
}
}
@@ -137,13 +212,13 @@ const resetAndNavigate = (path: string) => {
const handleLessonSelect = (lessonId: number) => {
if (currentLesson.value?.id === lessonId) return
- // 1. เปลี่ยน URL แบบนุ่มนวล
+ // 1. Update URL query params
router.push({ query: { ...route.query, lesson_id: lessonId.toString() } })
- // 2. โหลดเนื้อหาใหม่โดยไม่ Refresh หน้า
+ // 2. Load content without refresh
loadLesson(lessonId)
- // ปิด Sidebar บนมือถือเมื่อเลือกบทเรียน
+ // Close sidebar on mobile
if (sidebarOpen.value) {
sidebarOpen.value = false
}
@@ -169,14 +244,12 @@ const loadCourseData = async () => {
if (res.success) {
courseData.value = res.data
- // Auto-load logic: เช็คจาก URL ก่อน ถ้าไม่มีค่อยหาบทแรก
+ // Auto-load logic: Check URL first, fallback to first available lesson
const urlLessonId = route.query.lesson_id ? Number(route.query.lesson_id) : null
if (urlLessonId) {
- // ถ้ามีใน URL ให้โหลดบทนั้นเลย
loadLesson(urlLessonId)
} else if (!currentLesson.value) {
- // ถ้าไม่มีใน URL ให้หาบทแรกที่ไม่ล็อค
const firstChapter = res.data.chapters[0]
if (firstChapter && firstChapter.lessons.length > 0) {
const availableLesson = firstChapter.lessons.find((l: any) => !l.is_locked) || firstChapter.lessons[0]
@@ -184,7 +257,7 @@ const loadCourseData = async () => {
}
}
- // Fetch Announcements
+ // Fetch Course Announcements
const annRes = await fetchCourseAnnouncements(courseId.value)
if (annRes.success) {
announcements.value = annRes.data || []
@@ -192,7 +265,7 @@ const loadCourseData = async () => {
}
}
} catch (error) {
- console.error('Error loading course:', error)
+ console.error('Error loading course data:', error)
} finally {
isLoading.value = false
}
@@ -483,14 +556,13 @@ const videoSrc = computed(() => {
// เมื่อวิดีโอจบ ให้บันทึกว่าเรียนจบ (Complete)
const onVideoEnded = async () => {
- // Safety check BEFORE trying to save
+ // Safety check before saving
const lesson = currentLesson.value
if (!lesson || !lesson.progress || lesson.progress.is_completed || isCompleting.value) return
isCompleting.value = true
try {
- // 1. Force save progress at 100%
- // This will trigger the backend's auto-complete logic
+ // Force save progress at 100% to trigger backend completion
await performSaveProgress(true, false)
} catch (err) {
console.error('Failed to save progress on end:', err)
@@ -539,7 +611,7 @@ onBeforeUnmount(() => {
@click="toggleSidebar"
>
-
{{ $t('classroom.curriculum', 'เนื้อหาหลักสูตร') }}
+
{{ $t('classroom.curriculum') }}
@@ -557,7 +629,7 @@ onBeforeUnmount(() => {
class="text-slate-600 dark:text-slate-300 hover:text-blue-600 transition-colors"
>
- {{ $t('classroom.announcements', 'ประกาศในคอร์ส') }}
+ {{ $t('classroom.announcements') }}
diff --git a/Frontend-Learner/pages/classroom/quiz.vue b/Frontend-Learner/pages/classroom/quiz.vue
index 55d3cbf1..9d9b6d86 100644
--- a/Frontend-Learner/pages/classroom/quiz.vue
+++ b/Frontend-Learner/pages/classroom/quiz.vue
@@ -83,7 +83,7 @@ const jumpToQuestion = (targetIndex: number) => {
if (!isAnswered && !isSkippable) {
$q.notify({
type: 'warning',
- message: t('quiz.pleaseSelectAnswer', 'กรุณาเลือกคำตอบ'),
+ message: t('quiz.pleaseSelectAnswer'),
position: 'top',
timeout: 2000
})
@@ -106,6 +106,8 @@ const totalQuestions = computed(() => {
return quizData.value?.questions?.length || 0
})
+const hasQuestions = computed(() => totalQuestions.value > 0)
+
const showQuestionMap = computed(() => $q.screen.gt.sm)
const timerDisplay = computed(() => {
@@ -252,12 +254,37 @@ const submitQuiz = async (auto = false) => {
return
}
- // Confirmation before submission
- if (!confirm(t('quiz.submitConfirm', 'ยืนยันการส่งคำตอบ?'))) {
- return
- }
+ // Premium Confirmation before submission
+ $q.dialog({
+ title: `
`,
+ html: true,
+ persistent: true,
+ class: 'rounded-[24px]',
+ ok: {
+ label: t('common.ok'),
+ color: 'primary',
+ unelevated: true,
+ rounded: true,
+ class: 'px-8 font-black'
+ },
+ cancel: {
+ label: t('common.cancel'),
+ color: 'grey-7',
+ flat: true,
+ rounded: true,
+ class: 'font-bold'
+ }
+ }).onOk(() => {
+ processSubmitQuiz(auto)
+ })
+ return
}
+ processSubmitQuiz(auto)
+}
+
+const processSubmitQuiz = async (auto = false) => {
// 2. Start Submission Process
if (timerInterval) clearInterval(timerInterval)
@@ -285,12 +312,13 @@ const submitQuiz = async (auto = false) => {
}
} else {
// Fallback error handling
- alert(res.error || 'Failed to submit quiz')
- // Maybe go back to taking?
+ $q.notify({
+ type: 'negative',
+ message: res.error || 'Failed to submit quiz'
+ })
}
} catch (err) {
console.error('Submit quiz error:', err)
- alert('An unexpected error occurred.')
} finally {
isSubmitting.value = false
}
@@ -349,7 +377,7 @@ const getCorrectChoiceId = (questionId: number) => {