elearning/Frontend-Learner/composables/useCourse.ts

636 lines
24 KiB
TypeScript
Raw Normal View History

// Interface สำหรับข้อมูลคอร์สเรียน (Public Course Data)
export interface Course {
id: number
title: string | { th: string; en: string } // รองรับ 2 ภาษา
slug: string
description: string | { th: string; en: string }
thumbnail_url: string
price: string
is_free: boolean
have_certificate: boolean
status: string // DRAFT, PUBLISHED
category_id: number
created_at?: string
updated_at?: string
created_by?: number
updated_by?: number
approved_at?: string
approved_by?: number
rejection_reason?: string
rating?: string
lessons?: number | string
levelType?: 'neutral' | 'warning' | 'success' // ใช้สำหรับการแสดงผล Badge ระดับความยาก (Frontend Logic)
// โครงสร้างบทเรียน (Chapters & Lessons)
chapters?: {
id: number
title: string | { th: string; en: string }
lessons: {
id: number
title: string | { th: string; en: string }
duration_minutes: number
video_url?: string
}[]
}[]
}
interface CourseResponse {
code: number
message: string
data: Course[]
total: number
}
// Interface สำหรับคอร์สที่ผู้ใช้ลงทะเบียนเรียนแล้ว (My Course)
export interface EnrolledCourse {
id: number
course_id: number
course: Course
status: 'ENROLLED' | 'IN_PROGRESS' | 'COMPLETED' | 'DROPPED'
progress_percentage: number
enrolled_at: string
started_at?: string
completed_at?: string
last_accessed_at?: string
}
interface EnrolledCourseResponse {
code: number
message: string
data: EnrolledCourse[]
total: number
page: number
limit: number
}
// Interface สำหรับการส่งคำตอบแบบทดสอบ (Quiz Submission)
export interface QuizAnswerSubmission {
question_id: number
choice_id: number
}
export interface QuizSubmitRequest {
answers: QuizAnswerSubmission[]
}
// Interface สำหรับผลลัพธ์การสอบ (Quiz Result)
export interface QuizResult {
answers_review: {
score: number
is_correct: boolean
correct_choice_id: number
selected_choice_id: number
question_id: number
}[]
completed_at: string
started_at: string
attempt_number: number
passing_score: number
is_passed: boolean
correct_answers: number
total_questions: number
total_score: number
score: number
quiz_id: number
attempt_id: number
}
// ==========================================
// Composable: useCourse
// หน้าที่: จัดการ Logic ทุกอย่างเกี่ยวกับคอร์สเรียน
// - ดึงข้อมูลคอร์ส (Public & Protected)
// - ลงทะเบียนเรียน (Enroll)
// - ติดตามความคืบหน้าการเรียน (Progress tracking)
// Interface สำหรับ Certificate
export interface Certificate {
certificate_id: number
course_id: number
course_title: {
en: string
th: string
}
issued_at: string
download_url: string
}
// ==========================================
export const useCourse = () => {
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBase as string
const { token } = useAuth()
// ใช้ useState เพื่อเก็บรายชื่อคอร์สทั้งหมดใน Memory
const coursesState = useState<Course[]>('courses_cache', () => [])
const isCoursesLoaded = useState<boolean>('courses_loaded', () => false)
// ฟังก์ชันดึงรายชื่อคอร์สทั้งหมด (Catalog)
// ใช้สำหรับหน้า Discover/Browse
// Endpoint: GET /courses
const fetchCourses = async (forceRefresh = false) => {
// ถ้าโหลดไปแล้ว และไม่ได้บังคับ Refresh ให้ใช้ข้อมูลจาก State
if (isCoursesLoaded.value && !forceRefresh && coursesState.value.length > 0) {
return {
success: true,
data: coursesState.value,
total: coursesState.value.length
}
}
try {
const data = await $fetch<CourseResponse>(`${API_BASE_URL}/courses`, {
method: 'GET',
// ส่ง Token ไปด้วยถ้ามี
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
const courses = data.data || []
// เก็บลง State
coursesState.value = courses
isCoursesLoaded.value = true
return {
success: true,
data: courses,
total: data.total || 0
}
} catch (err: any) {
console.error('Fetch courses failed:', err)
// Retry logic for 429 Too Many Requests
if (err.statusCode === 429 || err.status === 429) {
await new Promise(resolve => setTimeout(resolve, 1500)); // Wait 1.5s
try {
const retryData = await $fetch<CourseResponse>(`${API_BASE_URL}/courses`, {
method: 'GET',
headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}
})
const courses = retryData.data || []
coursesState.value = courses
isCoursesLoaded.value = true
return { success: true, data: courses, total: retryData.total || 0 }
} catch (retryErr) {
console.error('Retry fetch courses failed:', retryErr)
}
}
return {
success: false,
error: err.data?.message || err.message || 'Error fetching courses'
}
}
}
// ฟังก์ชันดึงรายละเอียดคอร์สตาม ID
// Endpoint: GET /courses/:id
const fetchCourseById = async (id: number) => {
try {
const data = await $fetch<CourseResponse>(`${API_BASE_URL}/courses/${id}`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
// Logic จัดการข้อมูลที่ได้รับ (API อาจส่งกลับมาเป็น Array หรือ Object)
let courseData: any = null
if (Array.isArray(data.data)) {
// ถ้าเป็น Array ให้หาอันที่ ID ตรงกัน
courseData = data.data.find((c: any) => c.id == id)
// Fallback: ถ้าหาไม่เจอ แต่มีข้อมูลตัวเดียว อาจจะเป็นตัวนั้น
if (!courseData && data.data.length === 1) {
courseData = data.data[0]
}
} else {
courseData = data.data
}
if (!courseData) throw new Error('Course not found')
return {
success: true,
data: courseData
}
} catch (err: any) {
console.error('Fetch course details failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error fetching course details'
}
}
}
// ฟังก์ชันลงทะเบียนเรียน (Enroll)
// Endpoint: POST /students/courses/:id/enroll
const enrollCourse = async (courseId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/enroll`, {
method: 'POST',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data,
message: data.message
}
} catch (err: any) {
console.error('Enroll course failed:', err)
// เช็ค Error 409 Conflict หรือ 400 Bad Request (กรณีลงทะเบียนไปแล้ว)
// API ใหม่ส่ง 400 พร้อม error.code = "VALIDATION_ERROR" และ message "Already enrolled..."
const status = err.statusCode || err.status || err.response?.status
const errorData = err.data?.error || err.data
if (status === 409 || (status === 400 && errorData?.message?.includes('Already enrolled'))) {
return {
success: false,
error: 'ท่านได้ลงทะเบียนไปแล้ว',
code: 409 // Treat as conflict logic internally
}
}
return {
success: false,
error: errorData?.message || err.message || 'Error enrolling in course',
code: errorData?.code || err.data?.code
}
}
}
// ฟังก์ชันดึงคอร์สที่ฉันลงทะเบียนเรียน (My Courses)
// รองรับ Pagination และการกรอง Status (ENROLLED, IN_PROGRESS, COMPLETED)
const fetchEnrolledCourses = async (params: { page?: number; limit?: number; status?: string } = {}) => {
try {
const queryParams = new URLSearchParams()
if (params.page) queryParams.append('page', params.page.toString())
if (params.limit) queryParams.append('limit', params.limit.toString())
if (params.status && params.status !== 'ALL') queryParams.append('status', params.status)
const data = await $fetch<EnrolledCourseResponse>(`${API_BASE_URL}/students/courses?${queryParams.toString()}`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data || [],
total: data.total || 0,
page: data.page,
limit: data.limit
}
} catch (err: any) {
console.error('Fetch enrolled courses failed:', err)
// Retry for 429
if (err.statusCode === 429 || err.status === 429) {
await new Promise(resolve => setTimeout(resolve, 1500));
try {
const queryParams = new URLSearchParams()
if (params.page) queryParams.append('page', params.page.toString())
if (params.limit) queryParams.append('limit', params.limit.toString())
if (params.status && params.status !== 'ALL') queryParams.append('status', params.status)
const retryData = await $fetch<EnrolledCourseResponse>(`${API_BASE_URL}/students/courses?${queryParams.toString()}`, {
method: 'GET',
headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}
})
return { success: true, data: retryData.data || [], total: retryData.total || 0, page: retryData.page, limit: retryData.limit }
} catch (retryErr) {
console.error('Retry fetch enrolled courses failed:', retryErr)
}
}
return {
success: false,
error: err.data?.message || err.message || 'Error fetching enrolled courses'
}
}
}
// ฟังก์ชันดึงข้อมูลสำหรับการเรียน (Chapters, Lessons, Progress)
// Endpoint: GET /students/courses/:id/learn
const fetchCourseLearningInfo = async (courseId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/learn`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data
}
} catch (err: any) {
console.error('Fetch course learning info failed:', err)
// Retry for 429
if (err.statusCode === 429 || err.status === 429) {
await new Promise(resolve => setTimeout(resolve, 1500));
try {
const retryData = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/learn`, {
method: 'GET',
headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}
})
return { success: true, data: retryData.data }
} catch (retryErr) {
console.error('Retry fetch course learning info failed:', retryErr)
}
}
return {
success: false,
error: err.data?.message || err.message || 'Error fetching course learning info',
code: err.data?.code
}
}
}
// ฟังก์ชันดึงเนื้อหาบทเรียน (Video, Content)
// Endpoint: GET /students/courses/:cid/lessons/:lid
const fetchLessonContent = async (courseId: number, lessonId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any; progress?: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data,
progress: data.progress
}
} catch (err: any) {
console.error('Fetch lesson content failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error fetching lesson content',
code: err.data?.code,
status: err.status
}
}
}
// ฟังก์ชันเช็คสิทธิ์การเข้าถึงบทเรียน (Access Control)
// ต้อง Enrolled ก่อนถึงจะเข้าได้ และต้องผ่านเงื่อนไข Prerequisites (ถ้ามี)
// Endpoint: GET /students/courses/:cid/lessons/:lid/access-check
const checkLessonAccess = async (courseId: number, lessonId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/access-check`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data
}
} catch (err: any) {
console.error('Check lesson access failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error checking lesson access',
code: err.data?.code,
status: err.status
}
}
}
// ฟังก์ชันบันทึกเวลาที่ดูวิดีโอ (Video Progress)
// Endpoint: POST /students/lessons/:id/progress
const saveVideoProgress = async (lessonId: number, progressSeconds: number, durationSeconds: number, keepalive: boolean = false) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
method: 'POST',
headers: token.value ? {
// User Request: "Backend รับ header แบบ Authorization: <token> (ยังไม่ใช้ Bearer)"
// Checking existing usage, most use Bearer. I will stick to existing Bearer for consistency unless it fails,
// BUT user explicitly said "not using Bearer" in this specific prompt.
// To be safe and minimal, I will keep using `Bearer ${token}` if that's what the codebase uses globally,
// UNLESS I see strong evidence otherwise.
// The user's curl example in Step 209 DOES NOT have "Bearer".
// So I will remove it for these 2 functions as requested.
Authorization: `${token.value}`
} : {},
body: {
video_progress_seconds: progressSeconds,
video_duration_seconds: durationSeconds
},
keepalive: keepalive
})
return {
success: true,
data: data.data
}
} catch (err: any) {
console.error('Save video progress failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error saving video progress',
code: err.data?.code
}
}
}
// ฟังก์ชันดึง Video Progress ปัจจุบันของบทเรียน
// Endpoint: GET /students/lessons/:id/progress
const fetchVideoProgress = async (lessonId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data
}
} catch (err: any) {
console.error('Fetch video progress failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error fetching video progress',
code: err.data?.code,
status: err.status
}
}
}
// ฟังก์ชันบันทึกว่าเรียนจบบทเรียนแล้ว (Mark Complete)
// Endpoint: POST /students/courses/:cid/lessons/:lid/complete
const markLessonComplete = async (courseId: number, lessonId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/complete`, {
method: 'POST',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data
}
} catch (err: any) {
console.error('Mark lesson complete failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error marking lesson as complete',
code: err.data?.code,
status: err.status
}
}
}
// ฟังก์ชันส่งคำตอบ Quiz
// Endpoint: POST /students/courses/:cid/lessons/:lid/quiz/submit
const submitQuiz = async (courseId: number, lessonId: number, answers: QuizAnswerSubmission[], alreadyPassed: boolean = false) => {
try {
// NOTE: Backend crashes with 500 if we send extra fields like 'already_passed'.
// Reverting to strict body structure.
const body = { answers }
const data = await $fetch<{ code: number; message: string; data: QuizResult }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/quiz/submit`, {
method: 'POST',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {},
body: body
})
return {
success: true,
data: data.data
}
} catch (err: any) {
console.error('Submit quiz failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error submitting quiz',
code: err.data?.code,
status: err.status
}
}
}
// ฟังก์ชันสร้างใบ Certificate (Create/Generate)
// Endpoint: POST /certificates/:courseId/generate
const generateCertificate = async (courseId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: Certificate }>(`${API_BASE_URL}/certificates/${courseId}/generate`, {
method: 'POST',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data
}
} catch (err: any) {
console.error('Generate certificate failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error generating certificate',
code: err.data?.code,
status: err.status
}
}
}
// ฟังก์ชันดึงใบ Certificate ของคอร์สที่ระบุ (แบบเดี่ยว - ใหม่)
// Endpoint: GET /certificates/:courseId
const getCertificate = async (courseId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: Certificate }>(`${API_BASE_URL}/certificates/${courseId}`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data // Return single certificate object
}
} catch (err: any) {
console.error('Get certificate failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error getting certificate',
code: err.data?.code,
status: err.status
}
}
}
// ฟังก์ชันดึงประกาศของคอร์ส (Announcements)
// Endpoint: GET /student/courses/:id/announcements (Note: 'student' singular based on recent API update)
const fetchCourseAnnouncements = async (courseId: number) => {
try {
const data = await $fetch<{ code: number; message: string; data: any[] }>(`${API_BASE_URL}/student/courses/${courseId}/announcements`, {
method: 'GET',
headers: token.value ? {
Authorization: `Bearer ${token.value}`
} : {}
})
return {
success: true,
data: data.data
}
} catch (err: any) {
console.error('Fetch course announcements failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'Error fetching announcements',
code: err.data?.code,
status: err.status
}
}
}
return {
fetchCourses,
fetchCourseById,
enrollCourse,
fetchEnrolledCourses,
fetchCourseLearningInfo,
fetchLessonContent,
checkLessonAccess,
saveVideoProgress,
fetchVideoProgress,
markLessonComplete,
submitQuiz,
generateCertificate,
getCertificate,
fetchCourseAnnouncements
}
}