feat: Implement instructor course and lesson management with dedicated views for quizzes, videos, and admin categories.
This commit is contained in:
parent
344e1e4341
commit
878a17f922
9 changed files with 903 additions and 399 deletions
|
|
@ -25,6 +25,7 @@
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
bg-color="grey-1"
|
bg-color="grey-1"
|
||||||
|
hide-bottom-space
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="search" />
|
<q-icon name="search" />
|
||||||
|
|
@ -105,12 +106,16 @@
|
||||||
label="ชื่อ (ภาษาไทย)"
|
label="ชื่อ (ภาษาไทย)"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'กรุณากรอกชื่อ']"
|
:rules="[val => !!val || 'กรุณากรอกชื่อ']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.name.en"
|
v-model="form.name.en"
|
||||||
label="ชื่อ (English)"
|
label="ชื่อ (English)"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'Please enter name']"
|
:rules="[val => !!val || 'Please enter name']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -121,6 +126,8 @@
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
hint="ใช้สำหรับ URL เช่น web-development"
|
hint="ใช้สำหรับ URL เช่น web-development"
|
||||||
:rules="[val => !!val || 'กรุณากรอก slug']"
|
:rules="[val => !!val || 'กรุณากรอก slug']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@
|
||||||
label="ชื่อแบบทดสอบ (ภาษาไทย) *"
|
label="ชื่อแบบทดสอบ (ภาษาไทย) *"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'กรุณากรอกชื่อแบบทดสอบ']"
|
:rules="[val => !!val || 'กรุณากรอกชื่อแบบทดสอบ']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Title English -->
|
<!-- Title English -->
|
||||||
|
|
@ -317,7 +319,7 @@ const savingSettings = ref(false);
|
||||||
const saveQuizSettings = async () => {
|
const saveQuizSettings = async () => {
|
||||||
savingSettings.value = true;
|
savingSettings.value = true;
|
||||||
try {
|
try {
|
||||||
await instructorService.updateQuizSettings(courseId, chapterId, lessonId, {
|
const response = await instructorService.updateQuizSettings(courseId, chapterId, lessonId, {
|
||||||
title: form.value.title,
|
title: form.value.title,
|
||||||
description: form.value.content,
|
description: form.value.content,
|
||||||
passing_score: quizSettings.value.passing_score,
|
passing_score: quizSettings.value.passing_score,
|
||||||
|
|
@ -326,10 +328,10 @@ const saveQuizSettings = async () => {
|
||||||
shuffle_choices: quizSettings.value.shuffle_choices,
|
shuffle_choices: quizSettings.value.shuffle_choices,
|
||||||
show_answers_after_completion: quizSettings.value.show_answers_after_completion
|
show_answers_after_completion: quizSettings.value.show_answers_after_completion
|
||||||
});
|
});
|
||||||
$q.notify({ type: 'positive', message: 'บันทึกการตั้งค่าสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save quiz settings:', error);
|
console.error('Failed to save quiz settings:', error);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถบันทึกการตั้งค่าได้', position: 'top' });
|
$q.notify({ type: 'negative', message: (error as any).data?.error?.message || (error as any).data?.message || 'ไม่สามารถบันทึกการตั้งค่าได้', position: 'top' });
|
||||||
} finally {
|
} finally {
|
||||||
savingSettings.value = false;
|
savingSettings.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -345,14 +347,14 @@ const onDragEnd = async (event: { oldIndex: number; newIndex: number }) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call API with new position (1-indexed)
|
// Call API with new position (1-indexed)
|
||||||
await instructorService.reorderQuestion(courseId, chapterId, lessonId, question.id, newIndex + 1);
|
const response = await instructorService.reorderQuestion(courseId, chapterId, lessonId, question.id, newIndex + 1);
|
||||||
$q.notify({ type: 'positive', message: 'เรียงลำดับคำถามสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reorder question:', error);
|
console.error('Failed to reorder question:', error);
|
||||||
// Revert on error - swap back
|
// Revert on error - swap back
|
||||||
const [item] = questions.value.splice(newIndex, 1);
|
const [item] = questions.value.splice(newIndex, 1);
|
||||||
questions.value.splice(oldIndex, 0, item);
|
questions.value.splice(oldIndex, 0, item);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถเรียงลำดับได้', position: 'top' });
|
$q.notify({ type: 'negative', message: (error as any).data?.error?.message || (error as any).data?.message || 'ไม่สามารถเรียงลำดับได้', position: 'top' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -423,8 +425,8 @@ const fetchLesson = async () => {
|
||||||
const saveLesson = async () => {
|
const saveLesson = async () => {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
await instructorService.updateLesson(courseId, chapterId, lessonId, form.value);
|
const response = await instructorService.updateLesson(courseId, chapterId, lessonId, form.value);
|
||||||
$q.notify({ type: 'positive', message: 'บันทึกสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
navigateTo(`/instructor/courses/${courseId}/structure`);
|
navigateTo(`/instructor/courses/${courseId}/structure`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save lesson:', error);
|
console.error('Failed to save lesson:', error);
|
||||||
|
|
@ -462,13 +464,15 @@ const removeQuestion = async (index: number) => {
|
||||||
}).onOk(async () => {
|
}).onOk(async () => {
|
||||||
try {
|
try {
|
||||||
if (question.id) {
|
if (question.id) {
|
||||||
await instructorService.deleteQuestion(courseId, chapterId, lessonId, question.id);
|
const response = await instructorService.deleteQuestion(courseId, chapterId, lessonId, question.id);
|
||||||
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
|
} else {
|
||||||
|
$q.notify({ type: 'positive', message: 'ลบคำถามสำเร็จ', position: 'top' });
|
||||||
}
|
}
|
||||||
questions.value.splice(index, 1);
|
questions.value.splice(index, 1);
|
||||||
$q.notify({ type: 'positive', message: 'ลบคำถามสำเร็จ', position: 'top' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete question:', error);
|
console.error('Failed to delete question:', error);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถลบคำถามได้', position: 'top' });
|
$q.notify({ type: 'negative', message: (error as any).data?.error?.message || (error as any).data?.message || 'ไม่สามารถลบคำถามได้', position: 'top' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -501,16 +505,16 @@ const saveQuestion = async () => {
|
||||||
|
|
||||||
if (editingQuestionIndex.value !== null && questions.value[editingQuestionIndex.value].id) {
|
if (editingQuestionIndex.value !== null && questions.value[editingQuestionIndex.value].id) {
|
||||||
// Update existing question
|
// Update existing question
|
||||||
await instructorService.updateQuestion(
|
const response = await instructorService.updateQuestion(
|
||||||
courseId, chapterId, lessonId,
|
courseId, chapterId, lessonId,
|
||||||
questions.value[editingQuestionIndex.value].id!,
|
questions.value[editingQuestionIndex.value].id!,
|
||||||
questionData
|
questionData
|
||||||
);
|
);
|
||||||
$q.notify({ type: 'positive', message: 'แก้ไขคำถามสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
} else {
|
} else {
|
||||||
// Create new question
|
// Create new question
|
||||||
await instructorService.createQuestion(courseId, chapterId, lessonId, questionData);
|
const response = await instructorService.createQuestion(courseId, chapterId, lessonId, questionData);
|
||||||
$q.notify({ type: 'positive', message: 'เพิ่มคำถามสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
}
|
}
|
||||||
|
|
||||||
questionDialog.value = false;
|
questionDialog.value = false;
|
||||||
|
|
@ -518,7 +522,7 @@ const saveQuestion = async () => {
|
||||||
await fetchLesson();
|
await fetchLesson();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save question:', error);
|
console.error('Failed to save question:', error);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถบันทึกคำถามได้', position: 'top' });
|
$q.notify({ type: 'negative', message: (error as any).data?.error?.message || (error as any).data?.message || 'ไม่สามารถบันทึกคำถามได้', position: 'top' });
|
||||||
} finally {
|
} finally {
|
||||||
savingQuestion.value = false;
|
savingQuestion.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@
|
||||||
label="ชื่อบทเรียน (ภาษาไทย) *"
|
label="ชื่อบทเรียน (ภาษาไทย) *"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'กรุณากรอกชื่อบทเรียน']"
|
:rules="[val => !!val || 'กรุณากรอกชื่อบทเรียน']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Title English -->
|
<!-- Title English -->
|
||||||
|
|
@ -255,12 +257,12 @@ const fetchLesson = async () => {
|
||||||
const saveLesson = async () => {
|
const saveLesson = async () => {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
await instructorService.updateLesson(courseId, chapterId, lessonId, form.value);
|
const response = await instructorService.updateLesson(courseId, chapterId, lessonId, form.value);
|
||||||
$q.notify({ type: 'positive', message: 'บันทึกสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
navigateTo(`/instructor/courses/${courseId}/structure`);
|
navigateTo(`/instructor/courses/${courseId}/structure`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save lesson:', error);
|
console.error('Failed to save lesson:', error);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถบันทึกได้', position: 'top' });
|
$q.notify({ type: 'negative', message: (error as any).data?.error?.message || (error as any).data?.message || 'ไม่สามารถบันทึกได้', position: 'top' });
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -304,17 +306,18 @@ const uploadVideo = async () => {
|
||||||
|
|
||||||
uploadingVideo.value = true;
|
uploadingVideo.value = true;
|
||||||
try {
|
try {
|
||||||
|
let response;
|
||||||
if (lesson.value?.video_url) {
|
if (lesson.value?.video_url) {
|
||||||
await instructorService.updateLessonVideo(courseId, chapterId, lessonId, selectedVideo.value);
|
response = await instructorService.updateLessonVideo(courseId, chapterId, lessonId, selectedVideo.value);
|
||||||
} else {
|
} else {
|
||||||
await instructorService.uploadLessonVideo(courseId, chapterId, lessonId, selectedVideo.value);
|
response = await instructorService.uploadLessonVideo(courseId, chapterId, lessonId, selectedVideo.value);
|
||||||
}
|
}
|
||||||
$q.notify({ type: 'positive', message: 'อัพโหลดวิดีโอสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
selectedVideo.value = null;
|
selectedVideo.value = null;
|
||||||
await fetchLesson();
|
await fetchLesson();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to upload video:', error);
|
console.error('Failed to upload video:', error);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถอัพโหลดวิดีโอได้', position: 'top' });
|
$q.notify({ type: 'negative', message: (error as any).data?.error?.message || (error as any).data?.message || 'ไม่สามารถอัพโหลดวิดีโอได้', position: 'top' });
|
||||||
} finally {
|
} finally {
|
||||||
uploadingVideo.value = false;
|
uploadingVideo.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -336,12 +339,12 @@ const handleAttachmentSelect = async (event: Event) => {
|
||||||
const files = Array.from(target.files);
|
const files = Array.from(target.files);
|
||||||
uploadingAttachment.value = true;
|
uploadingAttachment.value = true;
|
||||||
try {
|
try {
|
||||||
await instructorService.addAttachments(courseId, chapterId, lessonId, files);
|
const response = await instructorService.addAttachments(courseId, chapterId, lessonId, files);
|
||||||
$q.notify({ type: 'positive', message: 'อัพโหลดไฟล์แนบสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
await fetchLesson();
|
await fetchLesson();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to upload attachments:', error);
|
console.error('Failed to upload attachments:', error);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถอัพโหลดไฟล์แนบได้', position: 'top' });
|
$q.notify({ type: 'negative', message: (error as any).data?.error?.message || (error as any).data?.message || 'ไม่สามารถอัพโหลดไฟล์แนบได้', position: 'top' });
|
||||||
} finally {
|
} finally {
|
||||||
uploadingAttachment.value = false;
|
uploadingAttachment.value = false;
|
||||||
target.value = '';
|
target.value = '';
|
||||||
|
|
@ -352,12 +355,12 @@ const handleAttachmentSelect = async (event: Event) => {
|
||||||
const deleteAttachment = async (attachmentId: number) => {
|
const deleteAttachment = async (attachmentId: number) => {
|
||||||
deletingAttachmentId.value = attachmentId;
|
deletingAttachmentId.value = attachmentId;
|
||||||
try {
|
try {
|
||||||
await instructorService.deleteAttachment(courseId, chapterId, lessonId, attachmentId);
|
const response = await instructorService.deleteAttachment(courseId, chapterId, lessonId, attachmentId);
|
||||||
$q.notify({ type: 'positive', message: 'ลบไฟล์แนบสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
await fetchLesson();
|
await fetchLesson();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete attachment:', error);
|
console.error('Failed to delete attachment:', error);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถลบไฟล์แนบได้', position: 'top' });
|
$q.notify({ type: 'negative', message: (error as any).data?.error?.message || (error as any).data?.message || 'ไม่สามารถลบไฟล์แนบได้', position: 'top' });
|
||||||
} finally {
|
} finally {
|
||||||
deletingAttachmentId.value = null;
|
deletingAttachmentId.value = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,16 @@
|
||||||
label="ชื่อหลักสูตร (ภาษาไทย) *"
|
label="ชื่อหลักสูตร (ภาษาไทย) *"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'กรุณากรอกชื่อหลักสูตร']"
|
:rules="[val => !!val || 'กรุณากรอกชื่อหลักสูตร']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.title.en"
|
v-model="form.title.en"
|
||||||
label="ชื่อหลักสูตร (English) *"
|
label="ชื่อหลักสูตร (English) *"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'Please enter course title']"
|
:rules="[val => !!val || 'Please enter course title']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -47,6 +51,8 @@
|
||||||
outlined
|
outlined
|
||||||
hint="ใช้สำหรับ URL เช่น javascript-basics"
|
hint="ใช้สำหรับ URL เช่น javascript-basics"
|
||||||
:rules="[val => !!val || 'กรุณากรอก slug']"
|
:rules="[val => !!val || 'กรุณากรอก slug']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-select
|
<q-select
|
||||||
v-model="form.category_id"
|
v-model="form.category_id"
|
||||||
|
|
@ -99,6 +105,8 @@
|
||||||
outlined
|
outlined
|
||||||
prefix="฿"
|
prefix="฿"
|
||||||
:rules="[val => form.is_free || val > 0 || 'กรุณากรอกราคา']"
|
:rules="[val => form.is_free || val > 0 || 'กรุณากรอกราคา']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="form.have_certificate"
|
v-model="form.have_certificate"
|
||||||
|
|
@ -107,43 +115,6 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Thumbnail -->
|
|
||||||
<q-separator class="my-6" />
|
|
||||||
<h2 class="text-lg font-semibold text-primary-600 mb-4">รูปภาพปก</h2>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<div class="flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer" @click="triggerThumbnailUpload">
|
|
||||||
<div v-if="form.thumbnail_url" class="relative group w-full max-w-md aspect-video">
|
|
||||||
<img
|
|
||||||
:src="form.thumbnail_url"
|
|
||||||
:key="form.thumbnail_url"
|
|
||||||
alt="Thumbnail preview"
|
|
||||||
class="w-full h-full object-cover rounded-lg shadow-sm"
|
|
||||||
@error="form.thumbnail_url = ''"
|
|
||||||
/>
|
|
||||||
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center rounded-lg">
|
|
||||||
<q-icon name="photo_camera" color="white" size="40px" />
|
|
||||||
<span class="text-white ml-2">เปลี่ยนรูปภาพ</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="uploadingThumbnail" class="absolute inset-0 bg-white/80 flex items-center justify-center rounded-lg">
|
|
||||||
<q-spinner color="primary" size="2em" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-center py-8">
|
|
||||||
<q-icon name="add_photo_alternate" size="48px" class="text-gray-400 mb-2" />
|
|
||||||
<div class="text-gray-600 font-medium">คลิกเพื่ออัพโหลดรูปภาพปก</div>
|
|
||||||
<div class="text-xs text-gray-400 mt-1">ขนาดแนะนำ 1280x720 (16:9) สูงสุด 5MB</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref="thumbnailInputRef"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
class="hidden"
|
|
||||||
@change="handleThumbnailUpload"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex justify-end gap-3 mt-8">
|
<div class="flex justify-end gap-3 mt-8">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -180,8 +151,6 @@ const route = useRoute();
|
||||||
// Data
|
// Data
|
||||||
const initialLoading = ref(true);
|
const initialLoading = ref(true);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const uploadingThumbnail = ref(false);
|
|
||||||
const thumbnailInputRef = ref<HTMLInputElement | null>(null);
|
|
||||||
const categories = ref<CategoryResponse[]>([]);
|
const categories = ref<CategoryResponse[]>([]);
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
|
|
@ -251,11 +220,15 @@ const handleSubmit = async () => {
|
||||||
form.value.price = 0;
|
form.value.price = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await instructorService.updateCourse(courseId, form.value);
|
// Create payload and remove thumbnail_url to avoid overwriting it
|
||||||
|
const payload = { ...form.value };
|
||||||
|
delete (payload as any).thumbnail_url;
|
||||||
|
|
||||||
|
const response = await instructorService.updateCourse(courseId, payload);
|
||||||
|
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'บันทึกการแก้ไขสำเร็จ',
|
message: response.message,
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -263,7 +236,7 @@ const handleSubmit = async () => {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: error.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
|
message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -271,76 +244,6 @@ const handleSubmit = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerThumbnailUpload = () => {
|
|
||||||
thumbnailInputRef.value?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleThumbnailUpload = async (event: Event) => {
|
|
||||||
const input = event.target as HTMLInputElement;
|
|
||||||
const file = input.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
// Validate file type
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
$q.notify({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'กรุณาเลือกไฟล์รูปภาพเท่านั้น',
|
|
||||||
position: 'top'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file size (max 5MB)
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
$q.notify({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'ไฟล์มีขนาดใหญ่เกิน 5MB',
|
|
||||||
position: 'top'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadingThumbnail.value = true;
|
|
||||||
try {
|
|
||||||
const courseId = parseInt(route.params.id as string);
|
|
||||||
// Clear current thumbnail first to force complete re-mount
|
|
||||||
form.value.thumbnail_url = null;
|
|
||||||
|
|
||||||
// Upload the file
|
|
||||||
await instructorService.uploadCourseThumbnail(courseId, file);
|
|
||||||
|
|
||||||
// Wait for Vue to unmount old img
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Re-fetch course data to get fresh presigned URL from backend
|
|
||||||
const updatedCourse = await instructorService.getCourseById(courseId);
|
|
||||||
|
|
||||||
// Add cache buster to force reload
|
|
||||||
if (updatedCourse.thumbnail_url) {
|
|
||||||
// For presigned URLs, we cannot append query parameters as it invalidates the signature
|
|
||||||
// The presigned URL itself should be unique enough or handle caching differently
|
|
||||||
form.value.thumbnail_url = updatedCourse.thumbnail_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'อัพโหลดรูปภาพปกเรียบร้อย',
|
|
||||||
position: 'top'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear input value to allow re-uploading same file if needed
|
|
||||||
input.value = '';
|
|
||||||
} catch (error: any) {
|
|
||||||
$q.notify({
|
|
||||||
type: 'negative',
|
|
||||||
message: error.message || 'เกิดข้อผิดพลาดในการอัพโหลดรูปภาพ',
|
|
||||||
position: 'top'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
uploadingThumbnail.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,42 @@
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
<div class="flex flex-col md:flex-row gap-6">
|
<div class="flex flex-col md:flex-row gap-6">
|
||||||
<!-- Thumbnail -->
|
<!-- Thumbnail -->
|
||||||
<div class="w-full md:w-48 h-32 bg-gradient-to-br from-primary-400 to-primary-600 rounded-lg flex items-center justify-center flex-shrink-0">
|
<!-- Thumbnail -->
|
||||||
|
<div
|
||||||
|
class="w-full md:w-48 h-32 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0 relative group cursor-pointer overflow-hidden border border-gray-200"
|
||||||
|
@click="triggerThumbnailUpload"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="course.thumbnail_url"
|
v-if="course.thumbnail_url"
|
||||||
:src="course.thumbnail_url"
|
:src="course.thumbnail_url"
|
||||||
:alt="course.title.th"
|
:alt="course.title.th"
|
||||||
class="w-full h-full object-cover rounded-lg"
|
class="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div v-else class="flex flex-col items-center">
|
||||||
|
<q-icon name="image" size="30px" color="grey-5" />
|
||||||
|
<span class="text-grey-6 text-xs mt-1">อัพโหลดรูป</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center">
|
||||||
|
<q-icon name="photo_camera" color="white" size="24px" />
|
||||||
|
<span class="text-white text-xs mt-1">เปลี่ยนรูป</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="uploadingThumbnail" class="absolute inset-0 bg-white/80 flex items-center justify-center">
|
||||||
|
<q-spinner color="primary" size="2em" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden Input -->
|
||||||
|
<input
|
||||||
|
ref="thumbnailInputRef"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@click.stop
|
||||||
|
@change="handleThumbnailUpload"
|
||||||
/>
|
/>
|
||||||
<span v-else class="text-white text-sm">รูปหลักสูตร</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
|
|
@ -79,6 +107,7 @@
|
||||||
>
|
>
|
||||||
<q-tab name="structure" icon="list" label="โครงสร้าง" />
|
<q-tab name="structure" icon="list" label="โครงสร้าง" />
|
||||||
<q-tab name="students" icon="people" label="ผู้เรียน" />
|
<q-tab name="students" icon="people" label="ผู้เรียน" />
|
||||||
|
<q-tab name="instructors" icon="manage_accounts" label="ผู้สอน" />
|
||||||
<q-tab name="quiz" icon="quiz" label="ผลการทดสอบ" />
|
<q-tab name="quiz" icon="quiz" label="ผลการทดสอบ" />
|
||||||
<q-tab name="announcements" icon="campaign" label="ประกาศ" />
|
<q-tab name="announcements" icon="campaign" label="ประกาศ" />
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
|
|
@ -157,6 +186,65 @@
|
||||||
</div>
|
</div>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Instructors Tab -->
|
||||||
|
<q-tab-panel name="instructors" class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">ผู้สอนในรายวิชา</h2>
|
||||||
|
<q-btn color="primary" icon="person_add" label="เพิ่มผู้สอน" @click="showAddInstructorDialog = true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loadingInstructors" class="flex justify-center py-10">
|
||||||
|
<q-spinner color="primary" size="40px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="instructors.length === 0" class="text-center py-10 text-gray-500">
|
||||||
|
<q-icon name="group_off" size="60px" color="grey-4" class="mb-4" />
|
||||||
|
<p>ยังไม่มีข้อมูลผู้สอน</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<q-card v-for="instructor in instructors" :key="instructor.id" flat bordered class="rounded-lg">
|
||||||
|
<q-item>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar size="50px" color="primary" text-color="white">
|
||||||
|
<img v-if="instructor.user.avatar_url" :src="instructor.user.avatar_url">
|
||||||
|
<span v-else>{{ instructor.user.username.charAt(0).toUpperCase() }}</span>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-base font-medium flex items-center gap-2">
|
||||||
|
{{ instructor.user.username }}
|
||||||
|
<q-badge v-if="instructor.is_primary" color="primary" label="หัวหน้าผู้สอน" />
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>{{ instructor.user.email }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn flat dense round icon="more_vert">
|
||||||
|
<q-menu>
|
||||||
|
<q-item v-if="!instructor.is_primary" clickable v-close-popup @click="setPrimaryInstructor(instructor.user_id)">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="verified_user" color="primary" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>ตั้งเป็นหัวหน้าผู้สอน</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="removeInstructor(instructor.user_id)" class="text-red">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="person_remove" color="red" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>ลบผู้สอน</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
<!-- Quiz Results Tab -->
|
<!-- Quiz Results Tab -->
|
||||||
<q-tab-panel name="quiz" class="p-6">
|
<q-tab-panel name="quiz" class="p-6">
|
||||||
<div class="text-center py-10 text-gray-500">
|
<div class="text-center py-10 text-gray-500">
|
||||||
|
|
@ -171,7 +259,7 @@
|
||||||
<h2 class="text-xl font-semibold text-gray-900">ประกาศ</h2>
|
<h2 class="text-xl font-semibold text-gray-900">ประกาศ</h2>
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
label="+ สร้างประกาศ"
|
label="สร้างประกาศ"
|
||||||
icon="add"
|
icon="add"
|
||||||
@click="openAnnouncementDialog()"
|
@click="openAnnouncementDialog()"
|
||||||
/>
|
/>
|
||||||
|
|
@ -243,6 +331,8 @@
|
||||||
outlined
|
outlined
|
||||||
label="หัวข้อ (ภาษาไทย) *"
|
label="หัวข้อ (ภาษาไทย) *"
|
||||||
:rules="[val => !!val || 'กรุณากรอกหัวข้อ']"
|
:rules="[val => !!val || 'กรุณากรอกหัวข้อ']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="announcementForm.title.en"
|
v-model="announcementForm.title.en"
|
||||||
|
|
@ -256,6 +346,8 @@
|
||||||
rows="4"
|
rows="4"
|
||||||
label="เนื้อหา (ภาษาไทย) *"
|
label="เนื้อหา (ภาษาไทย) *"
|
||||||
:rules="[val => !!val || 'กรุณากรอกเนื้อหา']"
|
:rules="[val => !!val || 'กรุณากรอกเนื้อหา']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="announcementForm.content.en"
|
v-model="announcementForm.content.en"
|
||||||
|
|
@ -301,9 +393,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Existing Attachments (edit mode) -->
|
<!-- Existing Attachments (edit mode) -->
|
||||||
<div v-if="editingAnnouncement?.attachments?.length > 0" class="space-y-2 mb-2">
|
<div v-if="editingAnnouncement?.attachments && editingAnnouncement.attachments.length > 0" class="space-y-2 mb-2">
|
||||||
<div
|
<div
|
||||||
v-for="attachment in editingAnnouncement.attachments"
|
v-for="attachment in editingAnnouncement?.attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
class="flex items-center justify-between bg-gray-50 rounded p-2"
|
class="flex items-center justify-between bg-gray-50 rounded p-2"
|
||||||
>
|
>
|
||||||
|
|
@ -366,6 +458,56 @@
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
<!-- Add Instructor Dialog -->
|
||||||
|
<q-dialog v-model="showAddInstructorDialog">
|
||||||
|
<q-card style="min-width: 400px">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">เพิ่มผู้สอน</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<q-select
|
||||||
|
v-model="selectedUser"
|
||||||
|
:options="filteredUsers"
|
||||||
|
option-value="id"
|
||||||
|
option-label="email"
|
||||||
|
label="ค้นหาผู้ใช้ (Email หรือ Username)"
|
||||||
|
use-input
|
||||||
|
filled
|
||||||
|
@filter="filterUsers"
|
||||||
|
:loading="loadingUsers"
|
||||||
|
>
|
||||||
|
<template v-slot:option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar>
|
||||||
|
<img v-if="scope.opt.profile?.avatar_url" :src="scope.opt.profile.avatar_url">
|
||||||
|
<span v-else>{{ scope.opt.username.charAt(0).toUpperCase() }}</span>
|
||||||
|
</q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt.username }}</q-item-label>
|
||||||
|
<q-item-label caption>{{ scope.opt.email }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
<template v-slot:no-option>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section class="text-grey">
|
||||||
|
ไม่พบผู้ใช้
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="ยกเลิก" color="primary" v-close-popup />
|
||||||
|
<q-btn flat label="เพิ่ม" color="primary" @click="addInstructor" :disable="!selectedUser" :loading="addingInstructor" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -377,8 +519,10 @@ import {
|
||||||
type CourseDetailResponse,
|
type CourseDetailResponse,
|
||||||
type ChapterResponse,
|
type ChapterResponse,
|
||||||
type AnnouncementResponse,
|
type AnnouncementResponse,
|
||||||
type CreateAnnouncementRequest
|
type CreateAnnouncementRequest,
|
||||||
|
type CourseInstructorResponse
|
||||||
} from '~/services/instructor.service';
|
} from '~/services/instructor.service';
|
||||||
|
import { adminService, type AdminUserResponse } from '~/services/admin.service';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'instructor',
|
layout: 'instructor',
|
||||||
|
|
@ -393,6 +537,10 @@ const course = ref<CourseDetailResponse | null>(null);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const activeTab = ref('structure');
|
const activeTab = ref('structure');
|
||||||
|
|
||||||
|
// Thumbnail upload
|
||||||
|
const uploadingThumbnail = ref(false);
|
||||||
|
const thumbnailInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
// Announcements data
|
// Announcements data
|
||||||
const announcements = ref<AnnouncementResponse[]>([]);
|
const announcements = ref<AnnouncementResponse[]>([]);
|
||||||
const loadingAnnouncements = ref(false);
|
const loadingAnnouncements = ref(false);
|
||||||
|
|
@ -406,6 +554,16 @@ const announcementForm = ref<CreateAnnouncementRequest>({
|
||||||
is_pinned: false
|
is_pinned: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Instructors data
|
||||||
|
const instructors = ref<CourseInstructorResponse[]>([]);
|
||||||
|
const loadingInstructors = ref(false);
|
||||||
|
const showAddInstructorDialog = ref(false);
|
||||||
|
const selectedUser = ref<AdminUserResponse | null>(null);
|
||||||
|
const users = ref<AdminUserResponse[]>([]);
|
||||||
|
const filteredUsers = ref<AdminUserResponse[]>([]);
|
||||||
|
const loadingUsers = ref(false);
|
||||||
|
const addingInstructor = ref(false);
|
||||||
|
|
||||||
// Attachment handling
|
// Attachment handling
|
||||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||||
const uploadingAttachment = ref(false);
|
const uploadingAttachment = ref(false);
|
||||||
|
|
@ -441,6 +599,72 @@ const fetchCourse = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const triggerThumbnailUpload = () => {
|
||||||
|
thumbnailInputRef.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailUpload = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'กรุณาเลือกไฟล์รูปภาพเท่านั้น',
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (max 5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'ไฟล์มีขนาดใหญ่เกิน 5MB',
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadingThumbnail.value = true;
|
||||||
|
try {
|
||||||
|
const courseId = parseInt(route.params.id as string);
|
||||||
|
|
||||||
|
// Upload the file
|
||||||
|
const response = await instructorService.uploadCourseThumbnail(courseId, file);
|
||||||
|
|
||||||
|
// Wait for Vue to unmount old img
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Re-fetch course data to get fresh presigned URL from backend
|
||||||
|
const updatedCourse = await instructorService.getCourseById(courseId);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
if (course.value) {
|
||||||
|
course.value.thumbnail_url = updatedCourse.thumbnail_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: response.message,
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear input value to allow re-uploading same file if needed
|
||||||
|
input.value = '';
|
||||||
|
} catch (error: any) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาดในการอัพโหลดรูปภาพ',
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
uploadingThumbnail.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
const colors: Record<string, string> = {
|
const colors: Record<string, string> = {
|
||||||
APPROVED: 'green',
|
APPROVED: 'green',
|
||||||
|
|
@ -496,10 +720,10 @@ const requestApproval = () => {
|
||||||
}).onOk(async () => {
|
}).onOk(async () => {
|
||||||
try {
|
try {
|
||||||
const courseId = parseInt(route.params.id as string);
|
const courseId = parseInt(route.params.id as string);
|
||||||
await instructorService.sendForReview(courseId);
|
const response = await instructorService.sendForReview(courseId);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'ส่งคำขออนุมัติแล้ว',
|
message: response.message,
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
// Refresh course data
|
// Refresh course data
|
||||||
|
|
@ -514,6 +738,135 @@ const requestApproval = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const fetchInstructors = async () => {
|
||||||
|
loadingInstructors.value = true;
|
||||||
|
try {
|
||||||
|
const courseId = parseInt(route.params.id as string);
|
||||||
|
instructors.value = await instructorService.getCourseInstructors(courseId);
|
||||||
|
} catch (error) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'ไม่สามารถโหลดข้อมูลผู้สอนได้',
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loadingInstructors.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterUsers = async (val: string, update: (callback: () => void) => void) => {
|
||||||
|
if (users.value.length === 0) {
|
||||||
|
loadingUsers.value = true;
|
||||||
|
try {
|
||||||
|
users.value = await adminService.getUsers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load users', error);
|
||||||
|
} finally {
|
||||||
|
loadingUsers.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(() => {
|
||||||
|
const needle = val.toLowerCase();
|
||||||
|
const existingInstructorIds = instructors.value.map(i => i.user_id);
|
||||||
|
|
||||||
|
filteredUsers.value = users.value.filter(v => {
|
||||||
|
// Exclude existing instructors
|
||||||
|
if (existingInstructorIds.includes(v.id)) return false;
|
||||||
|
|
||||||
|
// Filter by username or email
|
||||||
|
return v.username.toLowerCase().indexOf(needle) > -1 ||
|
||||||
|
v.email.toLowerCase().indexOf(needle) > -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addInstructor = async () => {
|
||||||
|
if (!selectedUser.value) return;
|
||||||
|
|
||||||
|
addingInstructor.value = true;
|
||||||
|
try {
|
||||||
|
const courseId = parseInt(route.params.id as string);
|
||||||
|
const response = await instructorService.addInstructor(courseId, selectedUser.value.id);
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: response.message,
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
|
||||||
|
showAddInstructorDialog.value = false;
|
||||||
|
selectedUser.value = null;
|
||||||
|
fetchInstructors();
|
||||||
|
} catch (error: any) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.data?.error?.message || error.data?.message || 'ไม่สามารถเพิ่มผู้สอนได้',
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
addingInstructor.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeInstructor = async (userId: number) => {
|
||||||
|
$q.dialog({
|
||||||
|
title: 'ยืนยันการลบ',
|
||||||
|
message: 'คุณต้องการลบผู้สอนท่านนี้ใช่หรือไม่?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(async () => {
|
||||||
|
try {
|
||||||
|
const courseId = parseInt(route.params.id as string);
|
||||||
|
const response = await instructorService.removeInstructor(courseId, userId);
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: response.message,
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchInstructors();
|
||||||
|
} catch (error: any) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.data?.error?.message || error.data?.message || 'ไม่สามารถลบผู้สอนได้',
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPrimaryInstructor = async (userId: number) => {
|
||||||
|
$q.dialog({
|
||||||
|
title: 'ยืนยันการเปลี่ยนหัวหน้าผู้สอน',
|
||||||
|
message: 'คุณต้องการตั้งให้ผู้สอนท่านนี้เป็นหัวหน้าผู้สอนใช่หรือไม่?',
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
}).onOk(async () => {
|
||||||
|
try {
|
||||||
|
const courseId = parseInt(route.params.id as string);
|
||||||
|
const response = await instructorService.setPrimaryInstructor(courseId, userId);
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: response.message,
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchInstructors();
|
||||||
|
} catch (error: any) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.data?.error?.message || error.data?.message || 'ไม่สามารถเปลี่ยนหัวหน้าผู้สอนได้',
|
||||||
|
position: 'top'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Announcements methods
|
// Announcements methods
|
||||||
const fetchAnnouncements = async () => {
|
const fetchAnnouncements = async () => {
|
||||||
loadingAnnouncements.value = true;
|
loadingAnnouncements.value = true;
|
||||||
|
|
@ -531,14 +884,19 @@ const fetchAnnouncements = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (date: string) => {
|
const formatDate = (date: string, includeTime = true) => {
|
||||||
return new Date(date).toLocaleDateString('th-TH', {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
year: '2-digit',
|
year: '2-digit'
|
||||||
hour: '2-digit',
|
};
|
||||||
minute: '2-digit'
|
|
||||||
});
|
if (includeTime) {
|
||||||
|
options.hour = '2-digit';
|
||||||
|
options.minute = '2-digit';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(date).toLocaleDateString('th-TH', options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openAnnouncementDialog = (announcement?: AnnouncementResponse) => {
|
const openAnnouncementDialog = (announcement?: AnnouncementResponse) => {
|
||||||
|
|
@ -577,15 +935,15 @@ const saveAnnouncement = async () => {
|
||||||
try {
|
try {
|
||||||
const courseId = parseInt(route.params.id as string);
|
const courseId = parseInt(route.params.id as string);
|
||||||
if (editingAnnouncement.value) {
|
if (editingAnnouncement.value) {
|
||||||
await instructorService.updateAnnouncement(courseId, editingAnnouncement.value.id, announcementForm.value);
|
const response = await instructorService.updateAnnouncement(courseId, editingAnnouncement.value.id, announcementForm.value);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'บันทึกประกาศสำเร็จ',
|
message: response.message,
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Create announcement with files
|
// Create announcement with files
|
||||||
await instructorService.createAnnouncement(
|
const response = await instructorService.createAnnouncement(
|
||||||
courseId,
|
courseId,
|
||||||
announcementForm.value,
|
announcementForm.value,
|
||||||
pendingFiles.value.length > 0 ? pendingFiles.value : undefined
|
pendingFiles.value.length > 0 ? pendingFiles.value : undefined
|
||||||
|
|
@ -594,7 +952,7 @@ const saveAnnouncement = async () => {
|
||||||
|
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'สร้างประกาศสำเร็จ',
|
message: response.message,
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -603,7 +961,7 @@ const saveAnnouncement = async () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: 'เกิดข้อผิดพลาด',
|
message: (error as any).data?.error?.message || (error as any).data?.message || 'เกิดข้อผิดพลาด',
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -620,17 +978,17 @@ const confirmDeleteAnnouncement = (announcement: AnnouncementResponse) => {
|
||||||
}).onOk(async () => {
|
}).onOk(async () => {
|
||||||
try {
|
try {
|
||||||
const courseId = parseInt(route.params.id as string);
|
const courseId = parseInt(route.params.id as string);
|
||||||
await instructorService.deleteAnnouncement(courseId, announcement.id);
|
const response = await instructorService.deleteAnnouncement(courseId, announcement.id);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'ลบประกาศสำเร็จ',
|
message: response.message,
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
fetchAnnouncements();
|
fetchAnnouncements();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: 'เกิดข้อผิดพลาดในการลบประกาศ',
|
message: (error as any).data?.error?.message || (error as any).data?.message || 'เกิดข้อผิดพลาดในการลบประกาศ',
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -657,17 +1015,26 @@ const handleFileUpload = async (event: Event) => {
|
||||||
editingAnnouncement.value.id,
|
editingAnnouncement.value.id,
|
||||||
file
|
file
|
||||||
);
|
);
|
||||||
editingAnnouncement.value = updated;
|
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'อัพโหลดไฟล์สำเร็จ',
|
message: updated.message,
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
fetchAnnouncements();
|
|
||||||
|
// Refresh list to get complete object including attachments
|
||||||
|
await fetchAnnouncements();
|
||||||
|
|
||||||
|
// Update the editing object from the fresh list
|
||||||
|
if (editingAnnouncement.value) {
|
||||||
|
const fresh = announcements.value.find(a => a.id === editingAnnouncement.value!.id);
|
||||||
|
if (fresh) {
|
||||||
|
editingAnnouncement.value = fresh;
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: 'เกิดข้อผิดพลาดในการอัพโหลดไฟล์',
|
message: (error as any).data?.error?.message || (error as any).data?.message || 'เกิดข้อผิดพลาดในการอัพโหลดไฟล์',
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -691,7 +1058,7 @@ const deleteAttachment = async (attachmentId: number) => {
|
||||||
deletingAttachmentId.value = attachmentId;
|
deletingAttachmentId.value = attachmentId;
|
||||||
try {
|
try {
|
||||||
const courseId = parseInt(route.params.id as string);
|
const courseId = parseInt(route.params.id as string);
|
||||||
await instructorService.deleteAnnouncementAttachment(
|
const response = await instructorService.deleteAnnouncementAttachment(
|
||||||
courseId,
|
courseId,
|
||||||
editingAnnouncement.value.id,
|
editingAnnouncement.value.id,
|
||||||
attachmentId
|
attachmentId
|
||||||
|
|
@ -702,14 +1069,14 @@ const deleteAttachment = async (attachmentId: number) => {
|
||||||
);
|
);
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'ลบไฟล์สำเร็จ',
|
message: response.message,
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
fetchAnnouncements();
|
fetchAnnouncements();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: 'เกิดข้อผิดพลาดในการลบไฟล์',
|
message: (error as any).data?.error?.message || (error as any).data?.message || 'เกิดข้อผิดพลาดในการลบไฟล์',
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -729,6 +1096,8 @@ const formatFileSize = (bytes: number): string => {
|
||||||
watch(activeTab, (newTab) => {
|
watch(activeTab, (newTab) => {
|
||||||
if (newTab === 'announcements' && announcements.value.length === 0) {
|
if (newTab === 'announcements' && announcements.value.length === 0) {
|
||||||
fetchAnnouncements();
|
fetchAnnouncements();
|
||||||
|
} else if (newTab === 'instructors' && instructors.value.length === 0) {
|
||||||
|
fetchInstructors();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,10 @@
|
||||||
v-model="chapterForm.title.th"
|
v-model="chapterForm.title.th"
|
||||||
label="ชื่อบท (ภาษาไทย) *"
|
label="ชื่อบท (ภาษาไทย) *"
|
||||||
outlined
|
outlined
|
||||||
|
class="mb-4"
|
||||||
:rules="[val => !!val || 'กรุณากรอกชื่อบท']"
|
:rules="[val => !!val || 'กรุณากรอกชื่อบท']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="chapterForm.title.en"
|
v-model="chapterForm.title.en"
|
||||||
|
|
@ -199,6 +202,8 @@
|
||||||
label="ชื่อบทเรียน (ภาษาไทย) *"
|
label="ชื่อบทเรียน (ภาษาไทย) *"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'กรุณากรอกชื่อบทเรียน']"
|
:rules="[val => !!val || 'กรุณากรอกชื่อบทเรียน']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="lessonForm.title.en"
|
v-model="lessonForm.title.en"
|
||||||
|
|
@ -298,14 +303,14 @@ const onChapterDragEnd = async (event: { oldIndex: number; newIndex: number }) =
|
||||||
if (!chapter) return;
|
if (!chapter) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await instructorService.reorderChapter(courseId.value, chapter.id, newIndex + 1);
|
const response = await instructorService.reorderChapter(courseId.value, chapter.id, newIndex + 1);
|
||||||
$q.notify({ type: 'positive', message: 'เรียงลำดับบทสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Failed to reorder chapter:', error);
|
console.error('Failed to reorder chapter:', error);
|
||||||
// Revert
|
// Revert
|
||||||
const [item] = chapters.value.splice(newIndex, 1);
|
const [item] = chapters.value.splice(newIndex, 1);
|
||||||
chapters.value.splice(oldIndex, 0, item);
|
chapters.value.splice(oldIndex, 0, item);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถเรียงลำดับได้', position: 'top' });
|
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'ไม่สามารถเรียงลำดับได้', position: 'top' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -317,14 +322,14 @@ const onLessonDragEnd = async (chapter: ChapterResponse & { expanded?: boolean }
|
||||||
if (!lesson) return;
|
if (!lesson) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await instructorService.reorderLesson(courseId.value, chapter.id, lesson.id, newIndex + 1);
|
const response = await instructorService.reorderLesson(courseId.value, chapter.id, lesson.id, newIndex + 1);
|
||||||
$q.notify({ type: 'positive', message: 'เรียงลำดับบทเรียนสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Failed to reorder lesson:', error);
|
console.error('Failed to reorder lesson:', error);
|
||||||
// Revert
|
// Revert
|
||||||
const [item] = chapter.lessons.splice(newIndex, 1);
|
const [item] = chapter.lessons.splice(newIndex, 1);
|
||||||
chapter.lessons.splice(oldIndex, 0, item);
|
chapter.lessons.splice(oldIndex, 0, item);
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถเรียงลำดับได้', position: 'top' });
|
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'ไม่สามารถเรียงลำดับได้', position: 'top' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -429,21 +434,21 @@ const saveChapter = async () => {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
if (editingChapter.value) {
|
if (editingChapter.value) {
|
||||||
await instructorService.updateChapter(courseId.value, editingChapter.value.id, chapterForm.value);
|
const response = await instructorService.updateChapter(courseId.value, editingChapter.value.id, chapterForm.value);
|
||||||
$q.notify({ type: 'positive', message: 'แก้ไขบทสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
} else {
|
} else {
|
||||||
// Calculate sort_order for new chapter
|
// Calculate sort_order for new chapter
|
||||||
const sortOrder = chapters.value.length + 1;
|
const sortOrder = chapters.value.length + 1;
|
||||||
await instructorService.createChapter(courseId.value, {
|
const response = await instructorService.createChapter(courseId.value, {
|
||||||
...chapterForm.value,
|
...chapterForm.value,
|
||||||
sort_order: sortOrder
|
sort_order: sortOrder
|
||||||
});
|
});
|
||||||
$q.notify({ type: 'positive', message: 'เพิ่มบทสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
}
|
}
|
||||||
chapterDialog.value = false;
|
chapterDialog.value = false;
|
||||||
fetchChapters();
|
fetchChapters();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
$q.notify({ type: 'negative', message: 'เกิดข้อผิดพลาด', position: 'top' });
|
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด', position: 'top' });
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -457,11 +462,11 @@ const confirmDeleteChapter = (chapter: ChapterResponse) => {
|
||||||
persistent: true
|
persistent: true
|
||||||
}).onOk(async () => {
|
}).onOk(async () => {
|
||||||
try {
|
try {
|
||||||
await instructorService.deleteChapter(courseId.value, chapter.id);
|
const response = await instructorService.deleteChapter(courseId.value, chapter.id);
|
||||||
$q.notify({ type: 'positive', message: 'ลบบทสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
fetchChapters();
|
fetchChapters();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถลบได้', position: 'top' });
|
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'ไม่สามารถลบได้', position: 'top' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -498,30 +503,30 @@ const saveLesson = async () => {
|
||||||
title: lessonForm.value.title,
|
title: lessonForm.value.title,
|
||||||
content: lessonForm.value.content
|
content: lessonForm.value.content
|
||||||
};
|
};
|
||||||
await instructorService.updateLesson(
|
const response = await instructorService.updateLesson(
|
||||||
courseId.value,
|
courseId.value,
|
||||||
selectedChapter.value.id,
|
selectedChapter.value.id,
|
||||||
editingLesson.value.id,
|
editingLesson.value.id,
|
||||||
updateData
|
updateData
|
||||||
);
|
);
|
||||||
$q.notify({ type: 'positive', message: 'แก้ไขบทเรียนสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
} else {
|
} else {
|
||||||
// Create - include type and sort_order
|
// Create - include type and sort_order
|
||||||
const createData = {
|
const createData = {
|
||||||
...lessonForm.value,
|
...lessonForm.value,
|
||||||
sort_order: (selectedChapter.value.lessons?.length || 0) + 1
|
sort_order: (selectedChapter.value.lessons?.length || 0) + 1
|
||||||
};
|
};
|
||||||
await instructorService.createLesson(
|
const response = await instructorService.createLesson(
|
||||||
courseId.value,
|
courseId.value,
|
||||||
selectedChapter.value.id,
|
selectedChapter.value.id,
|
||||||
createData
|
createData
|
||||||
);
|
);
|
||||||
$q.notify({ type: 'positive', message: 'เพิ่มบทเรียนสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
}
|
}
|
||||||
lessonDialog.value = false;
|
lessonDialog.value = false;
|
||||||
fetchChapters();
|
fetchChapters();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
$q.notify({ type: 'negative', message: 'เกิดข้อผิดพลาด', position: 'top' });
|
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด', position: 'top' });
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -535,11 +540,11 @@ const confirmDeleteLesson = (chapter: ChapterResponse, lesson: LessonResponse) =
|
||||||
persistent: true
|
persistent: true
|
||||||
}).onOk(async () => {
|
}).onOk(async () => {
|
||||||
try {
|
try {
|
||||||
await instructorService.deleteLesson(courseId.value, chapter.id, lesson.id);
|
const response = await instructorService.deleteLesson(courseId.value, chapter.id, lesson.id);
|
||||||
$q.notify({ type: 'positive', message: 'ลบบทเรียนสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
||||||
fetchChapters();
|
fetchChapters();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
$q.notify({ type: 'negative', message: 'ไม่สามารถลบได้', position: 'top' });
|
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'ไม่สามารถลบได้', position: 'top' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,16 @@
|
||||||
label="ชื่อหลักสูตร (ภาษาไทย) *"
|
label="ชื่อหลักสูตร (ภาษาไทย) *"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'กรุณากรอกชื่อหลักสูตร']"
|
:rules="[val => !!val || 'กรุณากรอกชื่อหลักสูตร']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.title.en"
|
v-model="form.title.en"
|
||||||
label="ชื่อหลักสูตร (English) *"
|
label="ชื่อหลักสูตร (English) *"
|
||||||
outlined
|
outlined
|
||||||
:rules="[val => !!val || 'Please enter course title']"
|
:rules="[val => !!val || 'Please enter course title']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -42,6 +46,8 @@
|
||||||
outlined
|
outlined
|
||||||
hint="ใช้สำหรับ URL เช่น javascript-basics"
|
hint="ใช้สำหรับ URL เช่น javascript-basics"
|
||||||
:rules="[val => !!val || 'กรุณากรอก slug']"
|
:rules="[val => !!val || 'กรุณากรอก slug']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-select
|
<q-select
|
||||||
v-model="form.category_id"
|
v-model="form.category_id"
|
||||||
|
|
@ -94,6 +100,8 @@
|
||||||
outlined
|
outlined
|
||||||
prefix="฿"
|
prefix="฿"
|
||||||
:rules="[val => form.is_free || val > 0 || 'กรุณากรอกราคา']"
|
:rules="[val => form.is_free || val > 0 || 'กรุณากรอกราคา']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="form.have_certificate"
|
v-model="form.have_certificate"
|
||||||
|
|
@ -102,32 +110,6 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Thumbnail -->
|
|
||||||
<q-separator class="my-6" />
|
|
||||||
<h2 class="text-lg font-semibold text-primary-600 mb-4">รูปภาพปก</h2>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<q-input
|
|
||||||
v-model="form.thumbnail_url"
|
|
||||||
label="URL รูปภาพปก *"
|
|
||||||
outlined
|
|
||||||
:rules="[val => !!val || 'กรุณากรอก URL รูปภาพปก']"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<q-icon name="image" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preview thumbnail if exists -->
|
|
||||||
<div v-if="form.thumbnail_url" class="mb-6">
|
|
||||||
<img
|
|
||||||
:src="form.thumbnail_url"
|
|
||||||
alt="Thumbnail preview"
|
|
||||||
class="max-w-xs rounded-lg shadow"
|
|
||||||
@error="form.thumbnail_url = ''"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex justify-end gap-3 mt-8">
|
<div class="flex justify-end gap-3 mt-8">
|
||||||
|
|
@ -172,7 +154,6 @@ const form = ref<CreateCourseRequest>({
|
||||||
title: { th: '', en: '' },
|
title: { th: '', en: '' },
|
||||||
slug: '',
|
slug: '',
|
||||||
description: { th: '', en: '' },
|
description: { th: '', en: '' },
|
||||||
thumbnail_url: null,
|
|
||||||
price: 0,
|
price: 0,
|
||||||
is_free: true,
|
is_free: true,
|
||||||
have_certificate: true
|
have_certificate: true
|
||||||
|
|
@ -214,19 +195,25 @@ const handleSubmit = async () => {
|
||||||
form.value.price = 0;
|
form.value.price = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await instructorService.createCourse(form.value);
|
const response = await instructorService.createCourse(form.value);
|
||||||
|
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'สร้างหลักสูตรสำเร็จ',
|
message: response.message,
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
|
|
||||||
navigateTo('/instructor/courses');
|
// Redirect to course edit page
|
||||||
|
// Note: Assuming response.data contains the created course with ID
|
||||||
|
if (response.data && response.data.id) {
|
||||||
|
router.push(`/instructor/courses/${response.data.id}/edit`);
|
||||||
|
} else {
|
||||||
|
navigateTo('/instructor/courses');
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: error.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
|
message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,8 @@
|
||||||
outlined
|
outlined
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:rules="[val => !!val || 'กรุณากรอกชื่อ']"
|
:rules="[val => !!val || 'กรุณากรอกชื่อ']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="person" />
|
<q-icon name="person" />
|
||||||
|
|
@ -140,6 +142,8 @@
|
||||||
outlined
|
outlined
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:rules="[val => !!val || 'กรุณากรอกนามสกุล']"
|
:rules="[val => !!val || 'กรุณากรอกนามสกุล']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="person" />
|
<q-icon name="person" />
|
||||||
|
|
@ -151,6 +155,8 @@
|
||||||
label="เบอร์โทร"
|
label="เบอร์โทร"
|
||||||
outlined
|
outlined
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="phone" />
|
<q-icon name="phone" />
|
||||||
|
|
@ -194,6 +200,8 @@
|
||||||
outlined
|
outlined
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:rules="[val => !!val || 'กรุณากรอกรหัสผ่านปัจจุบัน']"
|
:rules="[val => !!val || 'กรุณากรอกรหัสผ่านปัจจุบัน']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="lock" />
|
<q-icon name="lock" />
|
||||||
|
|
@ -217,6 +225,8 @@
|
||||||
val => !!val || 'กรุณากรอกรหัสผ่านใหม่',
|
val => !!val || 'กรุณากรอกรหัสผ่านใหม่',
|
||||||
val => val.length >= 6 || 'รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร'
|
val => val.length >= 6 || 'รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร'
|
||||||
]"
|
]"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="lock" />
|
<q-icon name="lock" />
|
||||||
|
|
@ -240,6 +250,8 @@
|
||||||
val => !!val || 'กรุณายืนยันรหัสผ่าน',
|
val => !!val || 'กรุณายืนยันรหัสผ่าน',
|
||||||
val => val === passwordForm.newPassword || 'รหัสผ่านไม่ตรงกัน'
|
val => val === passwordForm.newPassword || 'รหัสผ่านไม่ตรงกัน'
|
||||||
]"
|
]"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="lock" />
|
<q-icon name="lock" />
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ export interface CourseResponse {
|
||||||
updated_by: number | null;
|
updated_by: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CoursesListResponse {
|
export interface CoursesListResponse {
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
|
|
@ -32,6 +38,31 @@ export interface CoursesListResponse {
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CourseInstructorResponse {
|
||||||
|
id: number;
|
||||||
|
course_id: number;
|
||||||
|
user_id: number;
|
||||||
|
is_primary: boolean;
|
||||||
|
joined_at: string;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
role_id: number;
|
||||||
|
email_verified_at: string | null;
|
||||||
|
is_deactivated: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstructorsListResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: CourseInstructorResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to get auth token from cookie
|
// Helper function to get auth token from cookie
|
||||||
const getAuthToken = (): string => {
|
const getAuthToken = (): string => {
|
||||||
const tokenCookie = useCookie('token');
|
const tokenCookie = useCookie('token');
|
||||||
|
|
@ -161,21 +192,25 @@ export const instructorService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createCourse(data: CreateCourseRequest): Promise<CourseResponse> {
|
async createCourse(data: CreateCourseRequest): Promise<ApiResponse<CourseResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
...MOCK_COURSES[0],
|
code: 201,
|
||||||
id: Date.now(),
|
message: 'Course created successfully (Mock)',
|
||||||
...data,
|
data: {
|
||||||
price: String(data.price), // Convert number to string to match CourseResponse type
|
...MOCK_COURSES[0],
|
||||||
status: 'DRAFT',
|
id: Date.now(),
|
||||||
created_at: new Date().toISOString(),
|
...data,
|
||||||
updated_at: new Date().toISOString()
|
price: String(data.price), // Convert number to string to match CourseResponse type
|
||||||
} as CourseResponse;
|
status: 'DRAFT',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
} as CourseResponse
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean data - remove empty thumbnail_url
|
// Clean data - remove empty thumbnail_url
|
||||||
|
|
@ -184,11 +219,10 @@ export const instructorService = {
|
||||||
delete cleanedData.thumbnail_url;
|
delete cleanedData.thumbnail_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: CourseResponse }>('/api/instructors/courses', {
|
return await authRequest<ApiResponse<CourseResponse>>('/api/instructors/courses', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { data: cleanedData }
|
body: cleanedData
|
||||||
});
|
});
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getCourseById(courseId: number): Promise<CourseDetailResponse> {
|
async getCourseById(courseId: number): Promise<CourseDetailResponse> {
|
||||||
|
|
@ -204,68 +238,172 @@ export const instructorService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateCourse(courseId: number, data: CreateCourseRequest): Promise<CourseResponse> {
|
async updateCourse(courseId: number, data: CreateCourseRequest): Promise<ApiResponse<CourseResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
...MOCK_COURSES[0],
|
code: 200,
|
||||||
id: courseId,
|
message: 'Course updated successfully (Mock)',
|
||||||
...data,
|
data: {
|
||||||
price: String(data.price)
|
...MOCK_COURSES[0],
|
||||||
} as CourseResponse;
|
id: courseId,
|
||||||
|
...data,
|
||||||
|
price: String(data.price)
|
||||||
|
} as CourseResponse
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: CourseResponse }>(`/api/instructors/courses/${courseId}`, {
|
return await authRequest<ApiResponse<CourseResponse>>(`/api/instructors/courses/${courseId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: { data }
|
body: { data }
|
||||||
});
|
});
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async uploadCourseThumbnail(courseId: number, file: File): Promise<{ thumbnail_url: string }> {
|
async uploadCourseThumbnail(courseId: number, file: File): Promise<ApiResponse<{ thumbnail_url: string }>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return { thumbnail_url: URL.createObjectURL(file) };
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Thumbnail uploaded successfully (Mock)',
|
||||||
|
data: { thumbnail_url: URL.createObjectURL(file) }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: { thumbnail_url: string } }>(
|
return await authRequest<ApiResponse<{ thumbnail_url: string }>>(
|
||||||
`/api/instructors/courses/${courseId}/thumbnail`,
|
`/api/instructors/courses/${courseId}/thumbnail`,
|
||||||
{ method: 'POST', body: formData }
|
{ method: 'POST', body: formData }
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getCourseInstructors(courseId: number): Promise<CourseInstructorResponse[]> {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
|
if (useMockData) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
user_id: 1,
|
||||||
|
is_primary: true,
|
||||||
|
joined_at: new Date().toISOString(),
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'instructor',
|
||||||
|
email: 'instructor@elearning.local',
|
||||||
|
role_id: 2,
|
||||||
|
email_verified_at: new Date().toISOString(),
|
||||||
|
is_deactivated: false,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authRequest<InstructorsListResponse>(
|
||||||
|
`/api/instructors/courses/listinstructor/${courseId}`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteCourse(courseId: number): Promise<void> {
|
async addInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Instructor added successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(`/api/instructors/courses/${courseId}`, { method: 'DELETE' });
|
return await authRequest<ApiResponse<void>>(
|
||||||
|
`/api/instructors/courses/add-instructor/${courseId}/${userId}`,
|
||||||
|
{ method: 'POST' }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendForReview(courseId: number): Promise<void> {
|
async removeInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Instructor removed successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(`/api/instructors/courses/send-review/${courseId}`, { method: 'POST' });
|
return await authRequest<ApiResponse<void>>(
|
||||||
|
`/api/instructors/courses/remove-instructor/${courseId}/${userId}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async setPrimaryInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
|
if (useMockData) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Primary instructor updated successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await authRequest<ApiResponse<void>>(
|
||||||
|
`/api/instructors/courses/set-primary-instructor/${courseId}/${userId}`,
|
||||||
|
{ method: 'PUT' }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteCourse(courseId: number): Promise<ApiResponse<void>> {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
|
if (useMockData) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Course deleted successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await authRequest<ApiResponse<void>>(`/api/instructors/courses/${courseId}`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendForReview(courseId: number): Promise<ApiResponse<void>> {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
|
if (useMockData) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Sent for review successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await authRequest<ApiResponse<void>>(`/api/instructors/courses/send-review/${courseId}`, { method: 'POST' });
|
||||||
},
|
},
|
||||||
|
|
||||||
async getChapters(courseId: number): Promise<ChapterResponse[]> {
|
async getChapters(courseId: number): Promise<ChapterResponse[]> {
|
||||||
|
|
@ -284,72 +422,86 @@ export const instructorService = {
|
||||||
return response.data.chapters || [];
|
return response.data.chapters || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
async createChapter(courseId: number, data: CreateChapterRequest): Promise<ChapterResponse> {
|
async createChapter(courseId: number, data: CreateChapterRequest): Promise<ApiResponse<ChapterResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
...MOCK_COURSE_DETAIL.chapters[0],
|
code: 201,
|
||||||
id: Date.now(),
|
message: 'Chapter created successfully (Mock)',
|
||||||
...data,
|
data: {
|
||||||
lessons: []
|
...MOCK_COURSE_DETAIL.chapters[0],
|
||||||
|
id: Date.now(),
|
||||||
|
...data,
|
||||||
|
lessons: []
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: ChapterResponse }>(
|
return await authRequest<ApiResponse<ChapterResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters`,
|
`/api/instructors/courses/${courseId}/chapters`,
|
||||||
{ method: 'POST', body: data }
|
{ method: 'POST', body: data }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateChapter(courseId: number, chapterId: number, data: CreateChapterRequest): Promise<ChapterResponse> {
|
async updateChapter(courseId: number, chapterId: number, data: CreateChapterRequest): Promise<ApiResponse<ChapterResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
...MOCK_COURSE_DETAIL.chapters[0],
|
code: 200,
|
||||||
id: chapterId,
|
message: 'Chapter updated successfully (Mock)',
|
||||||
...data
|
data: {
|
||||||
|
...MOCK_COURSE_DETAIL.chapters[0],
|
||||||
|
id: chapterId,
|
||||||
|
...data
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: ChapterResponse }>(
|
return await authRequest<ApiResponse<ChapterResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
|
||||||
{ method: 'PUT', body: data }
|
{ method: 'PUT', body: data }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteChapter(courseId: number, chapterId: number): Promise<void> {
|
async deleteChapter(courseId: number, chapterId: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Chapter deleted successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async reorderChapter(courseId: number, chapterId: number, sortOrder: number): Promise<void> {
|
async reorderChapter(courseId: number, chapterId: number, sortOrder: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Chapter reordered successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder`,
|
||||||
{ method: 'PUT', body: { sort_order: sortOrder } }
|
{ method: 'PUT', body: { sort_order: sortOrder } }
|
||||||
);
|
);
|
||||||
|
|
@ -371,208 +523,243 @@ export const instructorService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createLesson(courseId: number, chapterId: number, data: CreateLessonRequest): Promise<LessonResponse> {
|
async createLesson(courseId: number, chapterId: number, data: CreateLessonRequest): Promise<ApiResponse<LessonResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
|
code: 201,
|
||||||
id: Date.now(),
|
message: 'Lesson created successfully (Mock)',
|
||||||
...data
|
data: {
|
||||||
|
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
|
||||||
|
id: Date.now(),
|
||||||
|
...data
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: LessonResponse }>(
|
return await authRequest<ApiResponse<LessonResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons`,
|
||||||
{ method: 'POST', body: data }
|
{ method: 'POST', body: data }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateLesson(courseId: number, chapterId: number, lessonId: number, data: UpdateLessonRequest): Promise<LessonResponse> {
|
async updateLesson(courseId: number, chapterId: number, lessonId: number, data: UpdateLessonRequest): Promise<ApiResponse<LessonResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
|
code: 200,
|
||||||
id: lessonId,
|
message: 'Lesson updated successfully (Mock)',
|
||||||
...data
|
data: {
|
||||||
|
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
|
||||||
|
id: lessonId,
|
||||||
|
...data
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: LessonResponse }>(
|
return await authRequest<ApiResponse<LessonResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`,
|
||||||
{ method: 'PUT', body: data }
|
{ method: 'PUT', body: data }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteLesson(courseId: number, chapterId: number, lessonId: number): Promise<void> {
|
async deleteLesson(courseId: number, chapterId: number, lessonId: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Lesson deleted successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async reorderLesson(courseId: number, chapterId: number, lessonId: number, sortOrder: number): Promise<void> {
|
async reorderLesson(courseId: number, chapterId: number, lessonId: number, sortOrder: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Lesson reordered successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder-lessons`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder-lessons`,
|
||||||
{ method: 'PUT', body: { lesson_id: lessonId, sort_order: sortOrder } }
|
{ method: 'PUT', body: { lesson_id: lessonId, sort_order: sortOrder } }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Question CRUD
|
// Question CRUD
|
||||||
async createQuestion(courseId: number, chapterId: number, lessonId: number, data: CreateQuestionRequest): Promise<QuizQuestionResponse> {
|
async createQuestion(courseId: number, chapterId: number, lessonId: number, data: CreateQuestionRequest): Promise<ApiResponse<QuizQuestionResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
id: Date.now(),
|
code: 201,
|
||||||
quiz_id: 1,
|
message: 'Question created successfully (Mock)',
|
||||||
question: data.question,
|
data: {
|
||||||
explanation: data.explanation || null,
|
id: Date.now(),
|
||||||
question_type: data.question_type,
|
quiz_id: 1,
|
||||||
score: data.score,
|
question: data.question,
|
||||||
sort_order: data.sort_order || 1,
|
explanation: data.explanation || null,
|
||||||
created_at: new Date().toISOString(),
|
question_type: data.question_type,
|
||||||
updated_at: new Date().toISOString(),
|
score: data.score || 1,
|
||||||
choices: data.choices.map((c, i) => ({
|
sort_order: data.sort_order || 1,
|
||||||
id: Date.now() + i,
|
created_at: new Date().toISOString(),
|
||||||
question_id: Date.now(),
|
updated_at: new Date().toISOString(),
|
||||||
text: c.text,
|
choices: data.choices.map((c, i) => ({
|
||||||
is_correct: c.is_correct,
|
id: Date.now() + i,
|
||||||
sort_order: c.sort_order || i + 1
|
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 }>(
|
return await authRequest<ApiResponse<QuizQuestionResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions`,
|
||||||
{ method: 'POST', body: data }
|
{ method: 'POST', body: data }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, data: CreateQuestionRequest): Promise<QuizQuestionResponse> {
|
async updateQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, data: CreateQuestionRequest): Promise<ApiResponse<QuizQuestionResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
id: questionId,
|
code: 200,
|
||||||
quiz_id: 1,
|
message: 'Question updated successfully (Mock)',
|
||||||
question: data.question,
|
data: {
|
||||||
explanation: data.explanation || null,
|
id: questionId,
|
||||||
question_type: data.question_type,
|
quiz_id: 1,
|
||||||
score: data.score,
|
question: data.question,
|
||||||
sort_order: data.sort_order || 1,
|
explanation: data.explanation || null,
|
||||||
created_at: new Date().toISOString(),
|
question_type: data.question_type,
|
||||||
updated_at: new Date().toISOString(),
|
score: data.score || 1,
|
||||||
choices: data.choices.map((c, i) => ({
|
sort_order: data.sort_order || 1,
|
||||||
id: Date.now() + i,
|
created_at: new Date().toISOString(),
|
||||||
question_id: questionId,
|
updated_at: new Date().toISOString(),
|
||||||
text: c.text,
|
choices: data.choices.map((c, i) => ({
|
||||||
is_correct: c.is_correct,
|
id: Date.now() + i,
|
||||||
sort_order: c.sort_order || i + 1
|
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 }>(
|
return await authRequest<ApiResponse<QuizQuestionResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
|
||||||
{ method: 'PUT', body: data }
|
{ method: 'PUT', body: data }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number): Promise<void> {
|
async deleteQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Question deleted successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async reorderQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, sortOrder: number): Promise<void> {
|
async reorderQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, sortOrder: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Question reordered successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}/reorder`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}/reorder`,
|
||||||
{ method: 'PUT', body: { sort_order: sortOrder } }
|
{ method: 'PUT', body: { sort_order: sortOrder } }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Quiz Settings
|
// Quiz Settings
|
||||||
async updateQuizSettings(courseId: number, chapterId: number, lessonId: number, data: UpdateQuizSettingsRequest): Promise<QuizResponse> {
|
async updateQuizSettings(courseId: number, chapterId: number, lessonId: number, data: UpdateQuizSettingsRequest): Promise<ApiResponse<QuizResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
id: 1,
|
code: 200,
|
||||||
lesson_id: lessonId,
|
message: 'Quiz settings updated successfully (Mock)',
|
||||||
title: data.title,
|
data: {
|
||||||
description: data.description,
|
id: 1,
|
||||||
passing_score: data.passing_score,
|
lesson_id: lessonId,
|
||||||
time_limit: data.time_limit,
|
title: data.title,
|
||||||
shuffle_questions: data.shuffle_questions,
|
description: data.description,
|
||||||
shuffle_choices: data.shuffle_choices,
|
passing_score: data.passing_score,
|
||||||
show_answers_after_completion: data.show_answers_after_completion
|
time_limit: data.time_limit,
|
||||||
|
shuffle_questions: data.shuffle_questions,
|
||||||
|
shuffle_choices: data.shuffle_choices,
|
||||||
|
show_answers_after_completion: data.show_answers_after_completion
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: QuizResponse }>(
|
return await authRequest<ApiResponse<QuizResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/quiz`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/quiz`,
|
||||||
{ method: 'PUT', body: data }
|
{ method: 'PUT', body: data }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Video Upload
|
// Video Upload
|
||||||
async uploadLessonVideo(courseId: number, chapterId: number, lessonId: number, video: File, attachments?: File[]): Promise<LessonResponse> {
|
async uploadLessonVideo(courseId: number, chapterId: number, lessonId: number, video: File, attachments?: File[]): Promise<ApiResponse<LessonResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Video uploaded successfully (Mock)',
|
||||||
|
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
@ -583,20 +770,23 @@ export const instructorService = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: LessonResponse }>(
|
return await authRequest<ApiResponse<LessonResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`,
|
||||||
{ method: 'POST', body: formData }
|
{ method: 'POST', body: formData }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateLessonVideo(courseId: number, chapterId: number, lessonId: number, video?: File, attachments?: File[]): Promise<LessonResponse> {
|
async updateLessonVideo(courseId: number, chapterId: number, lessonId: number, video?: File, attachments?: File[]): Promise<ApiResponse<LessonResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Video updated successfully (Mock)',
|
||||||
|
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
@ -609,21 +799,24 @@ export const instructorService = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: LessonResponse }>(
|
return await authRequest<ApiResponse<LessonResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`,
|
||||||
{ method: 'PUT', body: formData }
|
{ method: 'PUT', body: formData }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
async addAttachments(courseId: number, chapterId: number, lessonId: number, files: File[]): Promise<LessonResponse> {
|
async addAttachments(courseId: number, chapterId: number, lessonId: number, files: File[]): Promise<ApiResponse<LessonResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Attachments added successfully (Mock)',
|
||||||
|
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
@ -631,23 +824,26 @@ export const instructorService = {
|
||||||
formData.append('attachment', file);
|
formData.append('attachment', file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: LessonResponse }>(
|
return await authRequest<ApiResponse<LessonResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments`,
|
||||||
{ method: 'POST', body: formData }
|
{ method: 'POST', body: formData }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteAttachment(courseId: number, chapterId: number, lessonId: number, attachmentId: number): Promise<void> {
|
async deleteAttachment(courseId: number, chapterId: number, lessonId: number, attachmentId: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Attachment deleted successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments/${attachmentId}`,
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments/${attachmentId}`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
|
|
@ -669,21 +865,25 @@ export const instructorService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async createAnnouncement(courseId: number, data: CreateAnnouncementRequest, files?: File[]): Promise<AnnouncementResponse> {
|
async createAnnouncement(courseId: number, data: CreateAnnouncementRequest, files?: File[]): Promise<ApiResponse<AnnouncementResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
id: Date.now(),
|
code: 201,
|
||||||
title: data.title,
|
message: 'Announcement created successfully (Mock)',
|
||||||
content: data.content,
|
data: {
|
||||||
status: data.status || 'DRAFT',
|
id: Date.now(),
|
||||||
is_pinned: data.is_pinned || false,
|
title: data.title,
|
||||||
created_at: new Date().toISOString(),
|
content: data.content,
|
||||||
updated_at: new Date().toISOString(),
|
status: data.status || 'DRAFT',
|
||||||
attachments: []
|
is_pinned: data.is_pinned || false,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
attachments: []
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -696,82 +896,95 @@ export const instructorService = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: AnnouncementResponse }>(
|
return await authRequest<ApiResponse<AnnouncementResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/announcements`,
|
`/api/instructors/courses/${courseId}/announcements`,
|
||||||
{ method: 'POST', body: formData }
|
{ method: 'POST', body: formData }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateAnnouncement(courseId: number, announcementId: number, data: CreateAnnouncementRequest): Promise<AnnouncementResponse> {
|
async updateAnnouncement(courseId: number, announcementId: number, data: CreateAnnouncementRequest): Promise<ApiResponse<AnnouncementResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return {
|
return {
|
||||||
id: announcementId,
|
code: 200,
|
||||||
title: data.title,
|
message: 'Announcement updated successfully (Mock)',
|
||||||
content: data.content,
|
data: {
|
||||||
status: data.status || 'DRAFT',
|
id: announcementId,
|
||||||
is_pinned: data.is_pinned || false,
|
title: data.title,
|
||||||
created_at: new Date().toISOString(),
|
content: data.content,
|
||||||
updated_at: new Date().toISOString(),
|
status: data.status || 'DRAFT',
|
||||||
attachments: []
|
is_pinned: data.is_pinned || false,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
attachments: []
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: AnnouncementResponse }>(
|
return await authRequest<ApiResponse<AnnouncementResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/announcements/${announcementId}`,
|
`/api/instructors/courses/${courseId}/announcements/${announcementId}`,
|
||||||
{ method: 'PUT', body: data }
|
{ method: 'PUT', body: data }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteAnnouncement(courseId: number, announcementId: number): Promise<void> {
|
async deleteAnnouncement(courseId: number, announcementId: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Announcement deleted successfully (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/announcements/${announcementId}`,
|
`/api/instructors/courses/${courseId}/announcements/${announcementId}`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async uploadAnnouncementAttachment(courseId: number, announcementId: number, file: File): Promise<AnnouncementResponse> {
|
async uploadAnnouncementAttachment(courseId: number, announcementId: number, file: File): Promise<ApiResponse<AnnouncementResponse>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return MOCK_ANNOUNCEMENTS[0];
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Upload attachment success (Mock)',
|
||||||
|
data: MOCK_ANNOUNCEMENTS[0]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const response = await authRequest<{ code: number; data: AnnouncementResponse }>(
|
return await authRequest<ApiResponse<AnnouncementResponse>>(
|
||||||
`/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments`,
|
`/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments`,
|
||||||
{ method: 'POST', body: formData }
|
{ method: 'POST', body: formData }
|
||||||
);
|
);
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteAnnouncementAttachment(courseId: number, announcementId: number, attachmentId: number): Promise<void> {
|
async deleteAnnouncementAttachment(courseId: number, announcementId: number, attachmentId: number): Promise<ApiResponse<void>> {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const useMockData = config.public.useMockData as boolean;
|
const useMockData = config.public.useMockData as boolean;
|
||||||
|
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
return;
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Delete attachment success (Mock)',
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await authRequest(
|
return await authRequest<ApiResponse<void>>(
|
||||||
`/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments/${attachmentId}`,
|
`/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments/${attachmentId}`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
|
|
@ -895,6 +1108,7 @@ export interface CreateQuestionRequest {
|
||||||
question: { th: string; en: string };
|
question: { th: string; en: string };
|
||||||
explanation?: { th: string; en: string } | null;
|
explanation?: { th: string; en: string } | null;
|
||||||
question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE';
|
question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE';
|
||||||
|
score?: number;
|
||||||
sort_order?: number;
|
sort_order?: number;
|
||||||
choices: CreateChoiceRequest[];
|
choices: CreateChoiceRequest[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue