diff --git a/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/quiz.vue b/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/quiz.vue new file mode 100644 index 00000000..5646db3f --- /dev/null +++ b/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/quiz.vue @@ -0,0 +1,486 @@ + + + diff --git a/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/video.vue b/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/video.vue new file mode 100644 index 00000000..6137a71a --- /dev/null +++ b/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/video.vue @@ -0,0 +1,368 @@ + + + diff --git a/frontend_management/pages/instructor/courses/[id]/structure.vue b/frontend_management/pages/instructor/courses/[id]/structure.vue index 0b24ebbf..8aad7692 100644 --- a/frontend_management/pages/instructor/courses/[id]/structure.vue +++ b/frontend_management/pages/instructor/courses/[id]/structure.vue @@ -105,7 +105,7 @@
- + แก้ไข @@ -486,6 +486,12 @@ const fetchChapters = async () => { } }; +// Navigate to lesson edit page based on type +const navigateToLessonEdit = (chapter: ChapterResponse, lesson: LessonResponse) => { + const lessonType = lesson.type === 'QUIZ' ? 'quiz' : 'video'; + navigateTo(`/instructor/courses/${courseId.value}/chapters/${chapter.id}/lessons/${lesson.id}/${lessonType}`); +}; + const getLessonIcon = (type: string) => { const icons: Record = { VIDEO: 'play_circle', @@ -513,6 +519,10 @@ const getLessonTypeLabel = (type: string) => { return labels[type] || type; }; +const getSortedLessons = (lessons: LessonResponse[]) => { + return [...lessons].sort((a, b) => a.sort_order - b.sort_order); +}; + // Chapter CRUD const openChapterDialog = (chapter?: ChapterResponse) => { if (chapter) { diff --git a/frontend_management/services/instructor.service.ts b/frontend_management/services/instructor.service.ts index 838d6197..1405e19d 100644 --- a/frontend_management/services/instructor.service.ts +++ b/frontend_management/services/instructor.service.ts @@ -417,9 +417,179 @@ export const instructorService = { } await authRequest( - `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/reorder`, + `/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder-lessons`, { method: 'PUT', body: { lesson_id: lessonId, sort_order: sortOrder } } ); + }, + + // Question CRUD + async createQuestion(courseId: number, chapterId: number, lessonId: number, data: CreateQuestionRequest): Promise { + const config = useRuntimeConfig(); + const useMockData = config.public.useMockData as boolean; + + if (useMockData) { + await new Promise(resolve => setTimeout(resolve, 500)); + return { + id: Date.now(), + quiz_id: 1, + question: data.question, + explanation: data.explanation || null, + question_type: data.question_type, + score: data.score, + sort_order: data.sort_order || 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + choices: data.choices.map((c, i) => ({ + id: Date.now() + i, + question_id: Date.now(), + text: c.text, + is_correct: c.is_correct, + sort_order: c.sort_order || i + 1 + })) + }; + } + + const response = await authRequest<{ code: number; data: QuizQuestionResponse }>( + `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions`, + { method: 'POST', body: data } + ); + return response.data; + }, + + async updateQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, data: CreateQuestionRequest): Promise { + const config = useRuntimeConfig(); + const useMockData = config.public.useMockData as boolean; + + if (useMockData) { + await new Promise(resolve => setTimeout(resolve, 500)); + return { + id: questionId, + quiz_id: 1, + question: data.question, + explanation: data.explanation || null, + question_type: data.question_type, + score: data.score, + sort_order: data.sort_order || 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + choices: data.choices.map((c, i) => ({ + id: Date.now() + i, + question_id: questionId, + text: c.text, + is_correct: c.is_correct, + sort_order: c.sort_order || i + 1 + })) + }; + } + + const response = await authRequest<{ code: number; data: QuizQuestionResponse }>( + `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`, + { method: 'PUT', body: data } + ); + return response.data; + }, + + async deleteQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number): Promise { + const config = useRuntimeConfig(); + const useMockData = config.public.useMockData as boolean; + + if (useMockData) { + await new Promise(resolve => setTimeout(resolve, 500)); + return; + } + + await authRequest( + `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`, + { method: 'DELETE' } + ); + }, + + // Video Upload + async uploadLessonVideo(courseId: number, chapterId: number, lessonId: number, video: File, attachments?: File[]): Promise { + const config = useRuntimeConfig(); + const useMockData = config.public.useMockData as boolean; + + if (useMockData) { + await new Promise(resolve => setTimeout(resolve, 1000)); + return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse; + } + + const formData = new FormData(); + formData.append('video', video); + if (attachments) { + attachments.forEach(file => { + formData.append('attachments', file); + }); + } + + const response = await authRequest<{ code: number; data: LessonResponse }>( + `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`, + { method: 'POST', body: formData } + ); + return response.data; + }, + + async updateLessonVideo(courseId: number, chapterId: number, lessonId: number, video?: File, attachments?: File[]): Promise { + const config = useRuntimeConfig(); + const useMockData = config.public.useMockData as boolean; + + if (useMockData) { + await new Promise(resolve => setTimeout(resolve, 1000)); + return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse; + } + + const formData = new FormData(); + if (video) { + formData.append('video', video); + } + if (attachments) { + attachments.forEach(file => { + formData.append('attachments', file); + }); + } + + const response = await authRequest<{ code: number; data: LessonResponse }>( + `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`, + { method: 'PUT', body: formData } + ); + return response.data; + }, + + // Attachments + async addAttachments(courseId: number, chapterId: number, lessonId: number, files: File[]): Promise { + const config = useRuntimeConfig(); + const useMockData = config.public.useMockData as boolean; + + if (useMockData) { + await new Promise(resolve => setTimeout(resolve, 500)); + return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse; + } + + const formData = new FormData(); + files.forEach(file => { + formData.append('attachments', file); + }); + + const response = await authRequest<{ code: number; data: LessonResponse }>( + `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments`, + { method: 'POST', body: formData } + ); + return response.data; + }, + + async deleteAttachment(courseId: number, chapterId: number, lessonId: number, attachmentId: number): Promise { + const config = useRuntimeConfig(); + const useMockData = config.public.useMockData as boolean; + + if (useMockData) { + await new Promise(resolve => setTimeout(resolve, 500)); + return; + } + + await authRequest( + `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments/${attachmentId}`, + { method: 'DELETE' } + ); } }; @@ -472,9 +642,32 @@ export interface LessonResponse { is_published: boolean; created_at: string; updated_at: string; + video_url: string | null; + attachments: AttachmentResponse[]; quiz: QuizResponse | null; } +export interface QuizChoiceResponse { + id: number; + question_id: number; + text: { en: string; th: string }; + is_correct: boolean; + sort_order: number; +} + +export interface QuizQuestionResponse { + id: number; + quiz_id: number; + question: { en: string; th: string }; + explanation: { en: string; th: string } | null; + question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE'; + score: number; + sort_order: number; + created_at: string; + updated_at: string; + choices: QuizChoiceResponse[]; +} + export interface QuizResponse { id: number; lesson_id: number; @@ -485,6 +678,9 @@ export interface QuizResponse { shuffle_questions: boolean; shuffle_choices: boolean; show_answers_after_completion: boolean; + created_at?: string; + updated_at?: string; + questions?: QuizQuestionResponse[]; } export interface CreateChapterRequest { @@ -494,6 +690,20 @@ export interface CreateChapterRequest { is_published?: boolean; } +export interface CreateChoiceRequest { + text: { th: string; en: string }; + is_correct: boolean; + sort_order?: number; +} + +export interface CreateQuestionRequest { + question: { th: string; en: string }; + explanation?: { th: string; en: string } | null; + question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE'; + sort_order?: number; + choices: CreateChoiceRequest[]; +} + export interface CreateLessonRequest { title: { th: string; en: string }; content?: { th: string; en: string } | null; @@ -550,6 +760,8 @@ const MOCK_COURSE_DETAIL: CourseDetailResponse = { is_published: true, created_at: '2024-01-15T00:00:00Z', updated_at: '2024-01-15T00:00:00Z', + video_url: null, + attachments: [], quiz: null }, { @@ -566,6 +778,8 @@ const MOCK_COURSE_DETAIL: CourseDetailResponse = { is_published: true, created_at: '2024-01-15T00:00:00Z', updated_at: '2024-01-15T00:00:00Z', + video_url: null, + attachments: [], quiz: null }, { @@ -582,6 +796,8 @@ const MOCK_COURSE_DETAIL: CourseDetailResponse = { is_published: true, created_at: '2024-01-15T00:00:00Z', updated_at: '2024-01-15T00:00:00Z', + video_url: null, + attachments: [], quiz: { id: 1, lesson_id: 3, @@ -620,6 +836,8 @@ const MOCK_COURSE_DETAIL: CourseDetailResponse = { is_published: true, created_at: '2024-01-15T00:00:00Z', updated_at: '2024-01-15T00:00:00Z', + video_url: null, + attachments: [], quiz: null } ]