2026-01-23 09:54:35 +07:00
|
|
|
// Interface สำหรับข้อมูลคอร์สเรียน (Public Course Data)
|
2026-01-16 10:03:04 +07:00
|
|
|
export interface Course {
|
|
|
|
|
id: number
|
2026-01-23 09:54:35 +07:00
|
|
|
title: string | { th: string; en: string } // รองรับ 2 ภาษา
|
2026-01-16 10:03:04 +07:00
|
|
|
slug: string
|
2026-01-16 10:26:33 +07:00
|
|
|
description: string | { th: string; en: string }
|
2026-01-16 10:03:04 +07:00
|
|
|
thumbnail_url: string
|
|
|
|
|
price: string
|
|
|
|
|
is_free: boolean
|
2026-02-09 11:40:41 +07:00
|
|
|
original_price?: string
|
2026-01-16 10:03:04 +07:00
|
|
|
have_certificate: boolean
|
2026-01-23 09:54:35 +07:00
|
|
|
status: string // DRAFT, PUBLISHED
|
2026-01-16 10:03:04 +07:00
|
|
|
category_id: number
|
|
|
|
|
created_at?: string
|
|
|
|
|
updated_at?: string
|
|
|
|
|
created_by?: number
|
|
|
|
|
updated_by?: number
|
|
|
|
|
approved_at?: string
|
|
|
|
|
approved_by?: number
|
|
|
|
|
rejection_reason?: string
|
|
|
|
|
|
2026-01-22 11:04:57 +07:00
|
|
|
|
2026-01-16 10:03:04 +07:00
|
|
|
rating?: string
|
|
|
|
|
lessons?: number | string
|
2026-01-23 09:54:35 +07:00
|
|
|
levelType?: 'neutral' | 'warning' | 'success' // ใช้สำหรับการแสดงผล Badge ระดับความยาก (Frontend Logic)
|
2026-01-22 10:46:44 +07:00
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// โครงสร้างบทเรียน (Chapters & Lessons)
|
2026-01-22 10:46:44 +07:00
|
|
|
chapters?: {
|
|
|
|
|
id: number
|
|
|
|
|
title: string | { th: string; en: string }
|
|
|
|
|
lessons: {
|
|
|
|
|
id: number
|
|
|
|
|
title: string | { th: string; en: string }
|
|
|
|
|
duration_minutes: number
|
|
|
|
|
video_url?: string
|
|
|
|
|
}[]
|
|
|
|
|
}[]
|
2026-01-16 10:03:04 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface CourseResponse {
|
|
|
|
|
code: number
|
|
|
|
|
message: string
|
|
|
|
|
data: Course[]
|
|
|
|
|
total: number
|
2026-02-09 11:40:41 +07:00
|
|
|
page?: number
|
|
|
|
|
limit?: number
|
|
|
|
|
totalPages?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SingleCourseResponse {
|
|
|
|
|
code: number
|
|
|
|
|
message: string
|
|
|
|
|
data: Course
|
2026-01-16 10:03:04 +07:00
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// Interface สำหรับคอร์สที่ผู้ใช้ลงทะเบียนเรียนแล้ว (My Course)
|
2026-01-20 15:13:02 +07:00
|
|
|
export interface EnrolledCourse {
|
2026-01-22 11:04:57 +07:00
|
|
|
id: number
|
2026-01-20 15:13:02 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 10:01:21 +07:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 14:34:45 +07:00
|
|
|
// Interface สำหรับ Certificate
|
|
|
|
|
export interface Certificate {
|
|
|
|
|
certificate_id: number
|
|
|
|
|
course_id: number
|
|
|
|
|
course_title: {
|
|
|
|
|
en: string
|
|
|
|
|
th: string
|
|
|
|
|
}
|
|
|
|
|
issued_at: string
|
|
|
|
|
download_url: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:47:32 +07:00
|
|
|
// ==========================================
|
2026-02-09 11:40:41 +07:00
|
|
|
// Composable: useCourse
|
|
|
|
|
// หน้าที่: จัดการ Logic ทุกอย่างเกี่ยวกับคอร์สเรียน
|
|
|
|
|
// - ดึงข้อมูลคอร์ส (Public & Protected)
|
|
|
|
|
// - ลงทะเบียนเรียน (Enroll)
|
|
|
|
|
// - ติดตามความคืบหน้าการเรียน (Progress tracking)
|
2026-01-16 10:03:04 +07:00
|
|
|
export const useCourse = () => {
|
|
|
|
|
const config = useRuntimeConfig()
|
|
|
|
|
const API_BASE_URL = config.public.apiBase as string
|
|
|
|
|
const { token } = useAuth()
|
|
|
|
|
|
2026-02-09 11:40:41 +07:00
|
|
|
// ใช้ useState เพื่อเก็บรายชื่อคอร์สทั้งหมดใน Memory (สำหรับกรณีดึงทั้งหมด)
|
2026-01-27 10:42:13 +07:00
|
|
|
const coursesState = useState<Course[]>('courses_cache', () => [])
|
|
|
|
|
const isCoursesLoaded = useState<boolean>('courses_loaded', () => false)
|
|
|
|
|
|
2026-02-09 11:40:41 +07:00
|
|
|
/**
|
|
|
|
|
* ดึงรายชื่อคอร์สทั้งหมด (Catalog)
|
|
|
|
|
* รองรับการกรองด้วยหมวดหมู่ และ Pagination
|
|
|
|
|
* Endpoint: GET /courses
|
|
|
|
|
*/
|
|
|
|
|
const fetchCourses = async (params: {
|
|
|
|
|
category_id?: number;
|
|
|
|
|
page?: number;
|
|
|
|
|
limit?: number;
|
|
|
|
|
random?: boolean;
|
|
|
|
|
forceRefresh?: boolean
|
|
|
|
|
} = {}) => {
|
|
|
|
|
const { forceRefresh = false, ...apiParams } = params
|
|
|
|
|
|
|
|
|
|
// ใช้ Cache เฉพาะกรณีดึง "ทั้งหมด" แบบปกติ (ไม่มี params)
|
|
|
|
|
const isRequestingAll = Object.keys(apiParams).length === 0
|
|
|
|
|
if (isRequestingAll && isCoursesLoaded.value && !forceRefresh && coursesState.value.length > 0) {
|
2026-01-27 10:42:13 +07:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
data: coursesState.value,
|
|
|
|
|
total: coursesState.value.length
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 10:03:04 +07:00
|
|
|
try {
|
2026-02-09 11:40:41 +07:00
|
|
|
// สร้าง Query String
|
|
|
|
|
const queryParams = new URLSearchParams()
|
|
|
|
|
if (apiParams.category_id) queryParams.append('category_id', apiParams.category_id.toString())
|
|
|
|
|
if (apiParams.page) queryParams.append('page', apiParams.page.toString())
|
|
|
|
|
if (apiParams.limit) queryParams.append('limit', apiParams.limit.toString())
|
|
|
|
|
if (apiParams.random !== undefined) queryParams.append('random', apiParams.random.toString())
|
|
|
|
|
|
|
|
|
|
const queryString = queryParams.toString()
|
|
|
|
|
const url = `${API_BASE_URL}/courses${queryString ? `?${queryString}` : ''}`
|
|
|
|
|
|
|
|
|
|
const data = await $fetch<CourseResponse>(url, {
|
2026-01-16 10:03:04 +07:00
|
|
|
method: 'GET',
|
|
|
|
|
headers: token.value ? {
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
|
|
|
|
} : {}
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-27 10:42:13 +07:00
|
|
|
const courses = data.data || []
|
|
|
|
|
|
2026-02-09 11:40:41 +07:00
|
|
|
// เก็บลง State เฉพาะกรณีดึง "ทั้งหมด"
|
|
|
|
|
if (isRequestingAll) {
|
|
|
|
|
coursesState.value = courses
|
|
|
|
|
isCoursesLoaded.value = true
|
|
|
|
|
}
|
2026-01-27 10:42:13 +07:00
|
|
|
|
2026-01-16 10:03:04 +07:00
|
|
|
return {
|
|
|
|
|
success: true,
|
2026-01-27 10:42:13 +07:00
|
|
|
data: courses,
|
2026-02-09 11:40:41 +07:00
|
|
|
total: data.total || 0,
|
|
|
|
|
page: data.page,
|
|
|
|
|
limit: data.limit,
|
|
|
|
|
totalPages: data.totalPages
|
2026-01-16 10:03:04 +07:00
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Fetch courses failed:', err)
|
2026-01-29 13:17:58 +07:00
|
|
|
|
2026-02-09 11:40:41 +07:00
|
|
|
// Retry logic logic for 429
|
2026-01-29 13:17:58 +07:00
|
|
|
if (err.statusCode === 429 || err.status === 429) {
|
2026-02-09 11:40:41 +07:00
|
|
|
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
|
|
|
return fetchCourses(params) // Recursive retry
|
2026-01-29 13:17:58 +07:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 10:03:04 +07:00
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: err.data?.message || err.message || 'Error fetching courses'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 11:40:41 +07:00
|
|
|
/**
|
|
|
|
|
* ดึงรายละเอียดคอร์สตาม ID
|
|
|
|
|
* Endpoint: GET /courses/:id
|
|
|
|
|
*/
|
2026-01-16 10:03:04 +07:00
|
|
|
const fetchCourseById = async (id: number) => {
|
|
|
|
|
try {
|
2026-02-09 11:40:41 +07:00
|
|
|
const data = await $fetch<SingleCourseResponse>(`${API_BASE_URL}/courses/${id}`, {
|
2026-01-16 10:03:04 +07:00
|
|
|
method: 'GET',
|
|
|
|
|
headers: token.value ? {
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
|
|
|
|
} : {}
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-09 11:40:41 +07:00
|
|
|
if (!data.data) throw new Error('Course not found')
|
2026-01-16 10:03:04 +07:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
2026-02-09 11:40:41 +07:00
|
|
|
data: data.data // ข้อมูลคอร์สตัวเดียว
|
2026-01-16 10:03:04 +07:00
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Fetch course details failed:', err)
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: err.data?.message || err.message || 'Error fetching course details'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันลงทะเบียนเรียน (Enroll)
|
2026-01-23 09:47:32 +07:00
|
|
|
// Endpoint: POST /students/courses/:id/enroll
|
2026-01-20 15:01:01 +07:00
|
|
|
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)
|
2026-01-21 13:30:32 +07:00
|
|
|
|
2026-01-29 13:17:58 +07:00
|
|
|
// เช็ค Error 409 Conflict หรือ 400 Bad Request (กรณีลงทะเบียนไปแล้ว)
|
|
|
|
|
// API ใหม่ส่ง 400 พร้อม error.code = "VALIDATION_ERROR" และ message "Already enrolled..."
|
2026-01-21 13:30:32 +07:00
|
|
|
const status = err.statusCode || err.status || err.response?.status
|
2026-01-29 13:17:58 +07:00
|
|
|
const errorData = err.data?.error || err.data
|
2026-01-21 13:30:32 +07:00
|
|
|
|
2026-01-29 13:17:58 +07:00
|
|
|
if (status === 409 || (status === 400 && errorData?.message?.includes('Already enrolled'))) {
|
2026-01-21 13:30:32 +07:00
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: 'ท่านได้ลงทะเบียนไปแล้ว',
|
2026-01-29 13:17:58 +07:00
|
|
|
code: 409 // Treat as conflict logic internally
|
2026-01-21 13:30:32 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 15:01:01 +07:00
|
|
|
return {
|
|
|
|
|
success: false,
|
2026-01-29 13:17:58 +07:00
|
|
|
error: errorData?.message || err.message || 'Error enrolling in course',
|
|
|
|
|
code: errorData?.code || err.data?.code
|
2026-01-20 15:01:01 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันดึงคอร์สที่ฉันลงทะเบียนเรียน (My Courses)
|
|
|
|
|
// รองรับ Pagination และการกรอง Status (ENROLLED, IN_PROGRESS, COMPLETED)
|
2026-01-20 15:13:02 +07:00
|
|
|
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)
|
2026-01-29 13:17:58 +07:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 15:13:02 +07:00
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: err.data?.message || err.message || 'Error fetching enrolled courses'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:47:32 +07:00
|
|
|
// ฟังก์ชันดึงข้อมูลสำหรับการเรียน (Chapters, Lessons, Progress)
|
|
|
|
|
// Endpoint: GET /students/courses/:id/learn
|
2026-01-20 15:25:18 +07:00
|
|
|
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)
|
2026-01-29 13:17:58 +07:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 15:25:18 +07:00
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: err.data?.message || err.message || 'Error fetching course learning info',
|
|
|
|
|
code: err.data?.code
|
|
|
|
|
}
|
2026-01-29 13:17:58 +07:00
|
|
|
}
|
2026-01-20 15:25:18 +07:00
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:47:32 +07:00
|
|
|
// ฟังก์ชันดึงเนื้อหาบทเรียน (Video, Content)
|
|
|
|
|
// Endpoint: GET /students/courses/:cid/lessons/:lid
|
2026-01-20 15:25:18 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันเช็คสิทธิ์การเข้าถึงบทเรียน (Access Control)
|
|
|
|
|
// ต้อง Enrolled ก่อนถึงจะเข้าได้ และต้องผ่านเงื่อนไข Prerequisites (ถ้ามี)
|
|
|
|
|
// Endpoint: GET /students/courses/:cid/lessons/:lid/access-check
|
2026-01-20 15:31:28 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:47:32 +07:00
|
|
|
// ฟังก์ชันบันทึกเวลาที่ดูวิดีโอ (Video Progress)
|
|
|
|
|
// Endpoint: POST /students/lessons/:id/progress
|
2026-01-29 13:17:58 +07:00
|
|
|
const saveVideoProgress = async (lessonId: number, progressSeconds: number, durationSeconds: number, keepalive: boolean = false) => {
|
2026-01-20 15:42:34 +07:00
|
|
|
try {
|
|
|
|
|
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: token.value ? {
|
2026-02-04 16:22:42 +07:00
|
|
|
Authorization: `Bearer ${token.value}`
|
2026-01-20 15:42:34 +07:00
|
|
|
} : {},
|
|
|
|
|
body: {
|
|
|
|
|
video_progress_seconds: progressSeconds,
|
|
|
|
|
video_duration_seconds: durationSeconds
|
2026-01-29 13:17:58 +07:00
|
|
|
},
|
|
|
|
|
keepalive: keepalive
|
2026-01-20 15:42:34 +07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันดึง Video Progress ปัจจุบันของบทเรียน
|
|
|
|
|
// Endpoint: GET /students/lessons/:id/progress
|
2026-01-20 15:42:34 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:47:32 +07:00
|
|
|
// ฟังก์ชันบันทึกว่าเรียนจบบทเรียนแล้ว (Mark Complete)
|
|
|
|
|
// Endpoint: POST /students/courses/:cid/lessons/:lid/complete
|
2026-01-20 15:42:34 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 10:01:21 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ฟังก์ชันส่งคำตอบ Quiz
|
|
|
|
|
// Endpoint: POST /students/courses/:cid/lessons/:lid/quiz/submit
|
2026-02-02 14:37:26 +07:00
|
|
|
const submitQuiz = async (courseId: number, lessonId: number, answers: QuizAnswerSubmission[], alreadyPassed: boolean = false) => {
|
2026-01-28 10:01:21 +07:00
|
|
|
try {
|
2026-02-02 14:37:26 +07:00
|
|
|
// NOTE: Backend crashes with 500 if we send extra fields like 'already_passed'.
|
|
|
|
|
// Reverting to strict body structure.
|
|
|
|
|
const body = { answers }
|
2026-01-28 10:01:21 +07:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 14:34:45 +07:00
|
|
|
|
|
|
|
|
// ฟังก์ชันสร้างใบ 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 14:37:26 +07:00
|
|
|
// ฟังก์ชันดึงประกาศของคอร์ส (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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 10:53:42 +07:00
|
|
|
// ฟังก์ชันดึงรายการใบ Certificate ทั้งหมดของผู้ใช้ (ใหม่)
|
|
|
|
|
// Endpoint: GET /certificates
|
|
|
|
|
const fetchAllCertificates = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const data = await $fetch<{ code: number; message: string; data: Certificate[] }>(`${API_BASE_URL}/certificates`, {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
headers: token.value ? {
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
|
|
|
|
} : {}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
data: data.data || []
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Fetch all certificates failed:', err)
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: err.data?.message || err.message || 'Error fetching certificates',
|
|
|
|
|
code: err.data?.code,
|
|
|
|
|
status: err.status
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 11:25:55 +07:00
|
|
|
const { locale } = useI18n()
|
|
|
|
|
|
2026-02-09 11:40:41 +07:00
|
|
|
/**
|
|
|
|
|
* Helper: แปลงข้อมูล 2 ภาษาเป็นข้อความตาม locale ปัจจุบัน หรือค่าที่มีอยู่
|
|
|
|
|
*/
|
|
|
|
|
const getLocalizedText = (text: string | { th: string; en: string } | undefined | null) => {
|
|
|
|
|
if (!text) return ''
|
|
|
|
|
if (typeof text === 'string') return text
|
2026-02-11 11:25:55 +07:00
|
|
|
|
|
|
|
|
// Return based on current locale, fallback to th then en
|
|
|
|
|
const currentLocale = locale.value as 'th' | 'en'
|
2026-02-09 11:40:41 +07:00
|
|
|
// @ts-ignore
|
2026-02-11 11:25:55 +07:00
|
|
|
return text[currentLocale] || text.th || text.en || ''
|
2026-02-09 11:40:41 +07:00
|
|
|
}
|
|
|
|
|
|
2026-01-16 10:03:04 +07:00
|
|
|
return {
|
2026-02-09 11:40:41 +07:00
|
|
|
getLocalizedText,
|
2026-01-16 10:03:04 +07:00
|
|
|
fetchCourses,
|
2026-01-20 15:01:01 +07:00
|
|
|
fetchCourseById,
|
2026-01-20 15:13:02 +07:00
|
|
|
enrollCourse,
|
2026-01-20 15:25:18 +07:00
|
|
|
fetchEnrolledCourses,
|
|
|
|
|
fetchCourseLearningInfo,
|
2026-01-20 15:31:28 +07:00
|
|
|
fetchLessonContent,
|
2026-01-20 15:42:34 +07:00
|
|
|
checkLessonAccess,
|
|
|
|
|
saveVideoProgress,
|
|
|
|
|
fetchVideoProgress,
|
2026-01-28 10:01:21 +07:00
|
|
|
markLessonComplete,
|
2026-01-30 14:34:45 +07:00
|
|
|
submitQuiz,
|
|
|
|
|
generateCertificate,
|
2026-02-02 14:37:26 +07:00
|
|
|
getCertificate,
|
2026-02-09 10:53:42 +07:00
|
|
|
fetchAllCertificates,
|
2026-02-02 14:37:26 +07:00
|
|
|
fetchCourseAnnouncements
|
2026-01-16 10:03:04 +07:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 10:26:33 +07:00
|
|
|
|