476 lines
17 KiB
TypeScript
476 lines
17 KiB
TypeScript
// 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)
|
|
// ==========================================
|
|
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)
|
|
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 (กรณีลงทะเบียนไปแล้ว)
|
|
const status = err.statusCode || err.status || err.response?.status
|
|
|
|
if (status === 409) {
|
|
return {
|
|
success: false,
|
|
error: 'ท่านได้ลงทะเบียนไปแล้ว',
|
|
code: 409
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: err.data?.message || err.message || 'Error enrolling in course',
|
|
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)
|
|
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)
|
|
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) => {
|
|
try {
|
|
const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, {
|
|
method: 'POST',
|
|
headers: token.value ? {
|
|
Authorization: `Bearer ${token.value}`
|
|
} : {},
|
|
body: {
|
|
video_progress_seconds: progressSeconds,
|
|
video_duration_seconds: durationSeconds
|
|
}
|
|
})
|
|
|
|
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[]) => {
|
|
try {
|
|
const body: QuizSubmitRequest = { 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
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return {
|
|
fetchCourses,
|
|
fetchCourseById,
|
|
enrollCourse,
|
|
fetchEnrolledCourses,
|
|
fetchCourseLearningInfo,
|
|
fetchLessonContent,
|
|
checkLessonAccess,
|
|
saveVideoProgress,
|
|
fetchVideoProgress,
|
|
markLessonComplete,
|
|
submitQuiz
|
|
}
|
|
}
|
|
|