feat: Implement instructor course structure management, enabling drag-and-drop reordering of chapters and lessons, and adding support for video and quiz lesson types.

This commit is contained in:
Missez 2026-01-27 09:19:53 +07:00
parent be7348c74d
commit 310a5e7dd7
4 changed files with 1085 additions and 3 deletions

View file

@ -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<QuizQuestionResponse> {
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<QuizQuestionResponse> {
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<void> {
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<LessonResponse> {
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<LessonResponse> {
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<LessonResponse> {
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<void> {
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
}
]