feat: Implement instructor course and lesson management with dedicated views for quizzes, videos, and admin categories.

This commit is contained in:
Missez 2026-01-30 10:56:48 +07:00
parent 344e1e4341
commit 878a17f922
9 changed files with 903 additions and 399 deletions

View file

@ -25,6 +25,7 @@
outlined
dense
bg-color="grey-1"
hide-bottom-space
>
<template v-slot:prepend>
<q-icon name="search" />
@ -105,12 +106,16 @@
label="ชื่อ (ภาษาไทย)"
outlined
:rules="[val => !!val || 'กรุณากรอกชื่อ']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-input
v-model="form.name.en"
label="ชื่อ (English)"
outlined
:rules="[val => !!val || 'Please enter name']"
lazy-rules="ondemand"
hide-bottom-space
/>
</div>
@ -121,6 +126,8 @@
class="mb-4"
hint="ใช้สำหรับ URL เช่น web-development"
:rules="[val => !!val || 'กรุณากรอก slug']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-input

View file

@ -33,6 +33,8 @@
label="ชื่อแบบทดสอบ (ภาษาไทย) *"
outlined
:rules="[val => !!val || 'กรุณากรอกชื่อแบบทดสอบ']"
lazy-rules="ondemand"
hide-bottom-space
/>
<!-- Title English -->
@ -317,7 +319,7 @@ const savingSettings = ref(false);
const saveQuizSettings = async () => {
savingSettings.value = true;
try {
await instructorService.updateQuizSettings(courseId, chapterId, lessonId, {
const response = await instructorService.updateQuizSettings(courseId, chapterId, lessonId, {
title: form.value.title,
description: form.value.content,
passing_score: quizSettings.value.passing_score,
@ -326,10 +328,10 @@ const saveQuizSettings = async () => {
shuffle_choices: quizSettings.value.shuffle_choices,
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) {
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 {
savingSettings.value = false;
}
@ -345,14 +347,14 @@ const onDragEnd = async (event: { oldIndex: number; newIndex: number }) => {
try {
// Call API with new position (1-indexed)
await instructorService.reorderQuestion(courseId, chapterId, lessonId, question.id, newIndex + 1);
$q.notify({ type: 'positive', message: 'เรียงลำดับคำถามสำเร็จ', position: 'top' });
const response = await instructorService.reorderQuestion(courseId, chapterId, lessonId, question.id, newIndex + 1);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
} catch (error) {
console.error('Failed to reorder question:', error);
// Revert on error - swap back
const [item] = questions.value.splice(newIndex, 1);
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 () => {
saving.value = true;
try {
await instructorService.updateLesson(courseId, chapterId, lessonId, form.value);
$q.notify({ type: 'positive', message: 'บันทึกสำเร็จ', position: 'top' });
const response = await instructorService.updateLesson(courseId, chapterId, lessonId, form.value);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
navigateTo(`/instructor/courses/${courseId}/structure`);
} catch (error) {
console.error('Failed to save lesson:', error);
@ -462,13 +464,15 @@ const removeQuestion = async (index: number) => {
}).onOk(async () => {
try {
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);
$q.notify({ type: 'positive', message: 'ลบคำถามสำเร็จ', position: 'top' });
} catch (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) {
// Update existing question
await instructorService.updateQuestion(
const response = await instructorService.updateQuestion(
courseId, chapterId, lessonId,
questions.value[editingQuestionIndex.value].id!,
questionData
);
$q.notify({ type: 'positive', message: 'แก้ไขคำถามสำเร็จ', position: 'top' });
$q.notify({ type: 'positive', message: response.message, position: 'top' });
} else {
// Create new question
await instructorService.createQuestion(courseId, chapterId, lessonId, questionData);
$q.notify({ type: 'positive', message: 'เพิ่มคำถามสำเร็จ', position: 'top' });
const response = await instructorService.createQuestion(courseId, chapterId, lessonId, questionData);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
}
questionDialog.value = false;
@ -518,7 +522,7 @@ const saveQuestion = async () => {
await fetchLesson();
} catch (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 {
savingQuestion.value = false;
}

View file

@ -32,6 +32,8 @@
label="ชื่อบทเรียน (ภาษาไทย) *"
outlined
:rules="[val => !!val || 'กรุณากรอกชื่อบทเรียน']"
lazy-rules="ondemand"
hide-bottom-space
/>
<!-- Title English -->
@ -255,12 +257,12 @@ const fetchLesson = async () => {
const saveLesson = async () => {
saving.value = true;
try {
await instructorService.updateLesson(courseId, chapterId, lessonId, form.value);
$q.notify({ type: 'positive', message: 'บันทึกสำเร็จ', position: 'top' });
const response = await instructorService.updateLesson(courseId, chapterId, lessonId, form.value);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
navigateTo(`/instructor/courses/${courseId}/structure`);
} catch (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 {
saving.value = false;
}
@ -304,17 +306,18 @@ const uploadVideo = async () => {
uploadingVideo.value = true;
try {
let response;
if (lesson.value?.video_url) {
await instructorService.updateLessonVideo(courseId, chapterId, lessonId, selectedVideo.value);
response = await instructorService.updateLessonVideo(courseId, chapterId, lessonId, selectedVideo.value);
} 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;
await fetchLesson();
} catch (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 {
uploadingVideo.value = false;
}
@ -336,12 +339,12 @@ const handleAttachmentSelect = async (event: Event) => {
const files = Array.from(target.files);
uploadingAttachment.value = true;
try {
await instructorService.addAttachments(courseId, chapterId, lessonId, files);
$q.notify({ type: 'positive', message: 'อัพโหลดไฟล์แนบสำเร็จ', position: 'top' });
const response = await instructorService.addAttachments(courseId, chapterId, lessonId, files);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
await fetchLesson();
} catch (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 {
uploadingAttachment.value = false;
target.value = '';
@ -352,12 +355,12 @@ const handleAttachmentSelect = async (event: Event) => {
const deleteAttachment = async (attachmentId: number) => {
deletingAttachmentId.value = attachmentId;
try {
await instructorService.deleteAttachment(courseId, chapterId, lessonId, attachmentId);
$q.notify({ type: 'positive', message: 'ลบไฟล์แนบสำเร็จ', position: 'top' });
const response = await instructorService.deleteAttachment(courseId, chapterId, lessonId, attachmentId);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
await fetchLesson();
} catch (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 {
deletingAttachmentId.value = null;
}

View file

@ -31,12 +31,16 @@
label="ชื่อหลักสูตร (ภาษาไทย) *"
outlined
:rules="[val => !!val || 'กรุณากรอกชื่อหลักสูตร']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-input
v-model="form.title.en"
label="ชื่อหลักสูตร (English) *"
outlined
:rules="[val => !!val || 'Please enter course title']"
lazy-rules="ondemand"
hide-bottom-space
/>
</div>
@ -47,6 +51,8 @@
outlined
hint="ใช้สำหรับ URL เช่น javascript-basics"
:rules="[val => !!val || 'กรุณากรอก slug']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-select
v-model="form.category_id"
@ -99,6 +105,8 @@
outlined
prefix="฿"
:rules="[val => form.is_free || val > 0 || 'กรุณากรอกราคา']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-toggle
v-model="form.have_certificate"
@ -107,43 +115,6 @@
/>
</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 -->
<div class="flex justify-end gap-3 mt-8">
<q-btn
@ -180,8 +151,6 @@ const route = useRoute();
// Data
const initialLoading = ref(true);
const saving = ref(false);
const uploadingThumbnail = ref(false);
const thumbnailInputRef = ref<HTMLInputElement | null>(null);
const categories = ref<CategoryResponse[]>([]);
// Form
@ -251,11 +220,15 @@ const handleSubmit = async () => {
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({
type: 'positive',
message: 'บันทึกการแก้ไขสำเร็จ',
message: response.message,
position: 'top'
});
@ -263,7 +236,7 @@ const handleSubmit = async () => {
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
position: 'top'
});
} 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
onMounted(() => {
fetchData();

View file

@ -10,14 +10,42 @@
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex flex-col md:flex-row gap-6">
<!-- 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
v-if="course.thumbnail_url"
:src="course.thumbnail_url"
: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>
<!-- Info -->
@ -79,6 +107,7 @@
>
<q-tab name="structure" icon="list" 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="announcements" icon="campaign" label="ประกาศ" />
</q-tabs>
@ -157,6 +186,65 @@
</div>
</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 -->
<q-tab-panel name="quiz" class="p-6">
<div class="text-center py-10 text-gray-500">
@ -171,7 +259,7 @@
<h2 class="text-xl font-semibold text-gray-900">ประกาศ</h2>
<q-btn
color="primary"
label="+ สร้างประกาศ"
label="สร้างประกาศ"
icon="add"
@click="openAnnouncementDialog()"
/>
@ -243,6 +331,8 @@
outlined
label="หัวข้อ (ภาษาไทย) *"
:rules="[val => !!val || 'กรุณากรอกหัวข้อ']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-input
v-model="announcementForm.title.en"
@ -256,6 +346,8 @@
rows="4"
label="เนื้อหา (ภาษาไทย) *"
:rules="[val => !!val || 'กรุณากรอกเนื้อหา']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-input
v-model="announcementForm.content.en"
@ -301,9 +393,9 @@
</div>
<!-- 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
v-for="attachment in editingAnnouncement.attachments"
v-for="attachment in editingAnnouncement?.attachments"
:key="attachment.id"
class="flex items-center justify-between bg-gray-50 rounded p-2"
>
@ -366,6 +458,56 @@
</q-card-actions>
</q-card>
</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>
</div>
</template>
@ -377,8 +519,10 @@ import {
type CourseDetailResponse,
type ChapterResponse,
type AnnouncementResponse,
type CreateAnnouncementRequest
type CreateAnnouncementRequest,
type CourseInstructorResponse
} from '~/services/instructor.service';
import { adminService, type AdminUserResponse } from '~/services/admin.service';
definePageMeta({
layout: 'instructor',
@ -393,6 +537,10 @@ const course = ref<CourseDetailResponse | null>(null);
const loading = ref(true);
const activeTab = ref('structure');
// Thumbnail upload
const uploadingThumbnail = ref(false);
const thumbnailInputRef = ref<HTMLInputElement | null>(null);
// Announcements data
const announcements = ref<AnnouncementResponse[]>([]);
const loadingAnnouncements = ref(false);
@ -406,6 +554,16 @@ const announcementForm = ref<CreateAnnouncementRequest>({
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
const fileInputRef = ref<HTMLInputElement | null>(null);
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 colors: Record<string, string> = {
APPROVED: 'green',
@ -496,10 +720,10 @@ const requestApproval = () => {
}).onOk(async () => {
try {
const courseId = parseInt(route.params.id as string);
await instructorService.sendForReview(courseId);
const response = await instructorService.sendForReview(courseId);
$q.notify({
type: 'positive',
message: 'ส่งคำขออนุมัติแล้ว',
message: response.message,
position: 'top'
});
// 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
const fetchAnnouncements = async () => {
loadingAnnouncements.value = true;
@ -531,14 +884,19 @@ const fetchAnnouncements = async () => {
}
};
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('th-TH', {
const formatDate = (date: string, includeTime = true) => {
const options: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: 'short',
year: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
year: '2-digit'
};
if (includeTime) {
options.hour = '2-digit';
options.minute = '2-digit';
}
return new Date(date).toLocaleDateString('th-TH', options);
};
const openAnnouncementDialog = (announcement?: AnnouncementResponse) => {
@ -577,15 +935,15 @@ const saveAnnouncement = async () => {
try {
const courseId = parseInt(route.params.id as string);
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({
type: 'positive',
message: 'บันทึกประกาศสำเร็จ',
message: response.message,
position: 'top'
});
} else {
// Create announcement with files
await instructorService.createAnnouncement(
const response = await instructorService.createAnnouncement(
courseId,
announcementForm.value,
pendingFiles.value.length > 0 ? pendingFiles.value : undefined
@ -594,7 +952,7 @@ const saveAnnouncement = async () => {
$q.notify({
type: 'positive',
message: 'สร้างประกาศสำเร็จ',
message: response.message,
position: 'top'
});
}
@ -603,7 +961,7 @@ const saveAnnouncement = async () => {
} catch (error) {
$q.notify({
type: 'negative',
message: 'เกิดข้อผิดพลาด',
message: (error as any).data?.error?.message || (error as any).data?.message || 'เกิดข้อผิดพลาด',
position: 'top'
});
} finally {
@ -620,17 +978,17 @@ const confirmDeleteAnnouncement = (announcement: AnnouncementResponse) => {
}).onOk(async () => {
try {
const courseId = parseInt(route.params.id as string);
await instructorService.deleteAnnouncement(courseId, announcement.id);
const response = await instructorService.deleteAnnouncement(courseId, announcement.id);
$q.notify({
type: 'positive',
message: 'ลบประกาศสำเร็จ',
message: response.message,
position: 'top'
});
fetchAnnouncements();
} catch (error) {
$q.notify({
type: 'negative',
message: 'เกิดข้อผิดพลาดในการลบประกาศ',
message: (error as any).data?.error?.message || (error as any).data?.message || 'เกิดข้อผิดพลาดในการลบประกาศ',
position: 'top'
});
}
@ -657,17 +1015,26 @@ const handleFileUpload = async (event: Event) => {
editingAnnouncement.value.id,
file
);
editingAnnouncement.value = updated;
$q.notify({
type: 'positive',
message: 'อัพโหลดไฟล์สำเร็จ',
message: updated.message,
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) {
$q.notify({
type: 'negative',
message: 'เกิดข้อผิดพลาดในการอัพโหลดไฟล์',
message: (error as any).data?.error?.message || (error as any).data?.message || 'เกิดข้อผิดพลาดในการอัพโหลดไฟล์',
position: 'top'
});
} finally {
@ -691,7 +1058,7 @@ const deleteAttachment = async (attachmentId: number) => {
deletingAttachmentId.value = attachmentId;
try {
const courseId = parseInt(route.params.id as string);
await instructorService.deleteAnnouncementAttachment(
const response = await instructorService.deleteAnnouncementAttachment(
courseId,
editingAnnouncement.value.id,
attachmentId
@ -702,14 +1069,14 @@ const deleteAttachment = async (attachmentId: number) => {
);
$q.notify({
type: 'positive',
message: 'ลบไฟล์สำเร็จ',
message: response.message,
position: 'top'
});
fetchAnnouncements();
} catch (error) {
$q.notify({
type: 'negative',
message: 'เกิดข้อผิดพลาดในการลบไฟล์',
message: (error as any).data?.error?.message || (error as any).data?.message || 'เกิดข้อผิดพลาดในการลบไฟล์',
position: 'top'
});
} finally {
@ -729,6 +1096,8 @@ const formatFileSize = (bytes: number): string => {
watch(activeTab, (newTab) => {
if (newTab === 'announcements' && announcements.value.length === 0) {
fetchAnnouncements();
} else if (newTab === 'instructors' && instructors.value.length === 0) {
fetchInstructors();
}
});

View file

@ -149,7 +149,10 @@
v-model="chapterForm.title.th"
label="ชื่อบท (ภาษาไทย) *"
outlined
class="mb-4"
:rules="[val => !!val || 'กรุณากรอกชื่อบท']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-input
v-model="chapterForm.title.en"
@ -199,6 +202,8 @@
label="ชื่อบทเรียน (ภาษาไทย) *"
outlined
:rules="[val => !!val || 'กรุณากรอกชื่อบทเรียน']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-input
v-model="lessonForm.title.en"
@ -298,14 +303,14 @@ const onChapterDragEnd = async (event: { oldIndex: number; newIndex: number }) =
if (!chapter) return;
try {
await instructorService.reorderChapter(courseId.value, chapter.id, newIndex + 1);
$q.notify({ type: 'positive', message: 'เรียงลำดับบทสำเร็จ', position: 'top' });
} catch (error) {
const response = await instructorService.reorderChapter(courseId.value, chapter.id, newIndex + 1);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
} catch (error: any) {
console.error('Failed to reorder chapter:', error);
// Revert
const [item] = chapters.value.splice(newIndex, 1);
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;
try {
await instructorService.reorderLesson(courseId.value, chapter.id, lesson.id, newIndex + 1);
$q.notify({ type: 'positive', message: 'เรียงลำดับบทเรียนสำเร็จ', position: 'top' });
} catch (error) {
const response = await instructorService.reorderLesson(courseId.value, chapter.id, lesson.id, newIndex + 1);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
} catch (error: any) {
console.error('Failed to reorder lesson:', error);
// Revert
const [item] = chapter.lessons.splice(newIndex, 1);
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;
try {
if (editingChapter.value) {
await instructorService.updateChapter(courseId.value, editingChapter.value.id, chapterForm.value);
$q.notify({ type: 'positive', message: 'แก้ไขบทสำเร็จ', position: 'top' });
const response = await instructorService.updateChapter(courseId.value, editingChapter.value.id, chapterForm.value);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
} else {
// Calculate sort_order for new chapter
const sortOrder = chapters.value.length + 1;
await instructorService.createChapter(courseId.value, {
const response = await instructorService.createChapter(courseId.value, {
...chapterForm.value,
sort_order: sortOrder
});
$q.notify({ type: 'positive', message: 'เพิ่มบทสำเร็จ', position: 'top' });
$q.notify({ type: 'positive', message: response.message, position: 'top' });
}
chapterDialog.value = false;
fetchChapters();
} catch (error) {
$q.notify({ type: 'negative', message: 'เกิดข้อผิดพลาด', position: 'top' });
} catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด', position: 'top' });
} finally {
saving.value = false;
}
@ -457,11 +462,11 @@ const confirmDeleteChapter = (chapter: ChapterResponse) => {
persistent: true
}).onOk(async () => {
try {
await instructorService.deleteChapter(courseId.value, chapter.id);
$q.notify({ type: 'positive', message: 'ลบบทสำเร็จ', position: 'top' });
const response = await instructorService.deleteChapter(courseId.value, chapter.id);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
fetchChapters();
} catch (error) {
$q.notify({ type: 'negative', message: 'ไม่สามารถลบได้', position: 'top' });
} catch (error: any) {
$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,
content: lessonForm.value.content
};
await instructorService.updateLesson(
const response = await instructorService.updateLesson(
courseId.value,
selectedChapter.value.id,
editingLesson.value.id,
updateData
);
$q.notify({ type: 'positive', message: 'แก้ไขบทเรียนสำเร็จ', position: 'top' });
$q.notify({ type: 'positive', message: response.message, position: 'top' });
} else {
// Create - include type and sort_order
const createData = {
...lessonForm.value,
sort_order: (selectedChapter.value.lessons?.length || 0) + 1
};
await instructorService.createLesson(
const response = await instructorService.createLesson(
courseId.value,
selectedChapter.value.id,
createData
);
$q.notify({ type: 'positive', message: 'เพิ่มบทเรียนสำเร็จ', position: 'top' });
$q.notify({ type: 'positive', message: response.message, position: 'top' });
}
lessonDialog.value = false;
fetchChapters();
} catch (error) {
$q.notify({ type: 'negative', message: 'เกิดข้อผิดพลาด', position: 'top' });
} catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด', position: 'top' });
} finally {
saving.value = false;
}
@ -535,11 +540,11 @@ const confirmDeleteLesson = (chapter: ChapterResponse, lesson: LessonResponse) =
persistent: true
}).onOk(async () => {
try {
await instructorService.deleteLesson(courseId.value, chapter.id, lesson.id);
$q.notify({ type: 'positive', message: 'ลบบทเรียนสำเร็จ', position: 'top' });
const response = await instructorService.deleteLesson(courseId.value, chapter.id, lesson.id);
$q.notify({ type: 'positive', message: response.message, position: 'top' });
fetchChapters();
} catch (error) {
$q.notify({ type: 'negative', message: 'ไม่สามารถลบได้', position: 'top' });
} catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.error?.message || error.data?.message || 'ไม่สามารถลบได้', position: 'top' });
}
});
};

View file

@ -26,12 +26,16 @@
label="ชื่อหลักสูตร (ภาษาไทย) *"
outlined
:rules="[val => !!val || 'กรุณากรอกชื่อหลักสูตร']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-input
v-model="form.title.en"
label="ชื่อหลักสูตร (English) *"
outlined
:rules="[val => !!val || 'Please enter course title']"
lazy-rules="ondemand"
hide-bottom-space
/>
</div>
@ -42,6 +46,8 @@
outlined
hint="ใช้สำหรับ URL เช่น javascript-basics"
:rules="[val => !!val || 'กรุณากรอก slug']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-select
v-model="form.category_id"
@ -94,6 +100,8 @@
outlined
prefix="฿"
:rules="[val => form.is_free || val > 0 || 'กรุณากรอกราคา']"
lazy-rules="ondemand"
hide-bottom-space
/>
<q-toggle
v-model="form.have_certificate"
@ -102,32 +110,6 @@
/>
</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 -->
<div class="flex justify-end gap-3 mt-8">
@ -172,7 +154,6 @@ const form = ref<CreateCourseRequest>({
title: { th: '', en: '' },
slug: '',
description: { th: '', en: '' },
thumbnail_url: null,
price: 0,
is_free: true,
have_certificate: true
@ -214,19 +195,25 @@ const handleSubmit = async () => {
form.value.price = 0;
}
await instructorService.createCourse(form.value);
const response = await instructorService.createCourse(form.value);
$q.notify({
type: 'positive',
message: 'สร้างหลักสูตรสำเร็จ',
message: response.message,
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) {
$q.notify({
type: 'negative',
message: error.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่',
position: 'top'
});
} finally {

View file

@ -128,6 +128,8 @@
outlined
class="mb-4"
:rules="[val => !!val || 'กรุณากรอกชื่อ']"
lazy-rules="ondemand"
hide-bottom-space
>
<template v-slot:prepend>
<q-icon name="person" />
@ -140,6 +142,8 @@
outlined
class="mb-4"
:rules="[val => !!val || 'กรุณากรอกนามสกุล']"
lazy-rules="ondemand"
hide-bottom-space
>
<template v-slot:prepend>
<q-icon name="person" />
@ -151,6 +155,8 @@
label="เบอร์โทร"
outlined
class="mb-4"
lazy-rules="ondemand"
hide-bottom-space
>
<template v-slot:prepend>
<q-icon name="phone" />
@ -194,6 +200,8 @@
outlined
class="mb-4"
:rules="[val => !!val || 'กรุณากรอกรหัสผ่านปัจจุบัน']"
lazy-rules="ondemand"
hide-bottom-space
>
<template v-slot:prepend>
<q-icon name="lock" />
@ -217,6 +225,8 @@
val => !!val || 'กรุณากรอกรหัสผ่านใหม่',
val => val.length >= 6 || 'รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร'
]"
lazy-rules="ondemand"
hide-bottom-space
>
<template v-slot:prepend>
<q-icon name="lock" />
@ -240,6 +250,8 @@
val => !!val || 'กรุณายืนยันรหัสผ่าน',
val => val === passwordForm.newPassword || 'รหัสผ่านไม่ตรงกัน'
]"
lazy-rules="ondemand"
hide-bottom-space
>
<template v-slot:prepend>
<q-icon name="lock" />

View file

@ -25,6 +25,12 @@ export interface CourseResponse {
updated_by: number | null;
}
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
export interface CoursesListResponse {
code: number;
message: string;
@ -32,6 +38,31 @@ export interface CoursesListResponse {
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
const getAuthToken = (): string => {
const tokenCookie = useCookie('token');
@ -161,21 +192,25 @@ export const instructorService = {
return response.data;
},
async createCourse(data: CreateCourseRequest): Promise<CourseResponse> {
async createCourse(data: CreateCourseRequest): Promise<ApiResponse<CourseResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
...MOCK_COURSES[0],
id: Date.now(),
...data,
price: String(data.price), // Convert number to string to match CourseResponse type
status: 'DRAFT',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
} as CourseResponse;
code: 201,
message: 'Course created successfully (Mock)',
data: {
...MOCK_COURSES[0],
id: Date.now(),
...data,
price: String(data.price), // Convert number to string to match CourseResponse type
status: 'DRAFT',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
} as CourseResponse
};
}
// Clean data - remove empty thumbnail_url
@ -184,11 +219,10 @@ export const instructorService = {
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',
body: { data: cleanedData }
body: cleanedData
});
return response.data;
},
async getCourseById(courseId: number): Promise<CourseDetailResponse> {
@ -204,68 +238,172 @@ export const instructorService = {
return response.data;
},
async updateCourse(courseId: number, data: CreateCourseRequest): Promise<CourseResponse> {
async updateCourse(courseId: number, data: CreateCourseRequest): Promise<ApiResponse<CourseResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
...MOCK_COURSES[0],
id: courseId,
...data,
price: String(data.price)
} as CourseResponse;
code: 200,
message: 'Course updated successfully (Mock)',
data: {
...MOCK_COURSES[0],
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',
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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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();
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`,
{ 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;
},
async deleteCourse(courseId: number): Promise<void> {
async addInstructor(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;
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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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[]> {
@ -284,72 +422,86 @@ export const instructorService = {
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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
...MOCK_COURSE_DETAIL.chapters[0],
id: Date.now(),
...data,
lessons: []
code: 201,
message: 'Chapter created successfully (Mock)',
data: {
...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`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
...MOCK_COURSE_DETAIL.chapters[0],
id: chapterId,
...data
code: 200,
message: 'Chapter updated successfully (Mock)',
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}`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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}`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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`,
{ method: 'PUT', body: { sort_order: sortOrder } }
);
@ -371,208 +523,243 @@ export const instructorService = {
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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
id: Date.now(),
...data
code: 201,
message: 'Lesson created successfully (Mock)',
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`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
id: lessonId,
...data
code: 200,
message: 'Lesson updated successfully (Mock)',
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}`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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}`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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`,
{ method: 'PUT', body: { lesson_id: lessonId, sort_order: sortOrder } }
);
},
// 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
id: Date.now(),
quiz_id: 1,
question: data.question,
explanation: data.explanation || null,
question_type: data.question_type,
score: data.score,
sort_order: data.sort_order || 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
choices: data.choices.map((c, i) => ({
id: Date.now() + i,
question_id: Date.now(),
text: c.text,
is_correct: c.is_correct,
sort_order: c.sort_order || i + 1
}))
code: 201,
message: 'Question created successfully (Mock)',
data: {
id: Date.now(),
quiz_id: 1,
question: data.question,
explanation: data.explanation || null,
question_type: data.question_type,
score: data.score || 1,
sort_order: data.sort_order || 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
choices: data.choices.map((c, i) => ({
id: Date.now() + i,
question_id: Date.now(),
text: c.text,
is_correct: c.is_correct,
sort_order: c.sort_order || i + 1
}))
}
};
}
const response = await authRequest<{ code: number; data: QuizQuestionResponse }>(
return await authRequest<ApiResponse<QuizQuestionResponse>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions`,
{ method: 'POST', body: data }
);
return response.data;
},
async updateQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, data: CreateQuestionRequest): Promise<QuizQuestionResponse> {
async updateQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, data: CreateQuestionRequest): Promise<ApiResponse<QuizQuestionResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
id: questionId,
quiz_id: 1,
question: data.question,
explanation: data.explanation || null,
question_type: data.question_type,
score: data.score,
sort_order: data.sort_order || 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
choices: data.choices.map((c, i) => ({
id: Date.now() + i,
question_id: questionId,
text: c.text,
is_correct: c.is_correct,
sort_order: c.sort_order || i + 1
}))
code: 200,
message: 'Question updated successfully (Mock)',
data: {
id: questionId,
quiz_id: 1,
question: data.question,
explanation: data.explanation || null,
question_type: data.question_type,
score: data.score || 1,
sort_order: data.sort_order || 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
choices: data.choices.map((c, i) => ({
id: Date.now() + i,
question_id: questionId,
text: c.text,
is_correct: c.is_correct,
sort_order: c.sort_order || i + 1
}))
}
};
}
const response = await authRequest<{ code: number; data: QuizQuestionResponse }>(
return await authRequest<ApiResponse<QuizQuestionResponse>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
{ method: 'PUT', body: data }
);
return response.data;
},
async deleteQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number): Promise<void> {
async deleteQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
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}`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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`,
{ method: 'PUT', body: { sort_order: sortOrder } }
);
},
// 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
id: 1,
lesson_id: lessonId,
title: data.title,
description: data.description,
passing_score: data.passing_score,
time_limit: data.time_limit,
shuffle_questions: data.shuffle_questions,
shuffle_choices: data.shuffle_choices,
show_answers_after_completion: data.show_answers_after_completion
code: 200,
message: 'Quiz settings updated successfully (Mock)',
data: {
id: 1,
lesson_id: lessonId,
title: data.title,
description: data.description,
passing_score: data.passing_score,
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`,
{ method: 'PUT', body: data }
);
return response.data;
},
// 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 1000));
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
return {
code: 200,
message: 'Video uploaded successfully (Mock)',
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
};
}
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`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 1000));
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
return {
code: 200,
message: 'Video updated successfully (Mock)',
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
};
}
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`,
{ method: 'PUT', body: formData }
);
return response.data;
},
// 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse;
return {
code: 200,
message: 'Attachments added successfully (Mock)',
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
};
}
const formData = new FormData();
@ -631,23 +824,26 @@ export const instructorService = {
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`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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}`,
{ method: 'DELETE' }
);
@ -669,21 +865,25 @@ export const instructorService = {
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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
id: Date.now(),
title: data.title,
content: data.content,
status: data.status || 'DRAFT',
is_pinned: data.is_pinned || false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
attachments: []
code: 201,
message: 'Announcement created successfully (Mock)',
data: {
id: Date.now(),
title: data.title,
content: data.content,
status: data.status || 'DRAFT',
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`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
id: announcementId,
title: data.title,
content: data.content,
status: data.status || 'DRAFT',
is_pinned: data.is_pinned || false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
attachments: []
code: 200,
message: 'Announcement updated successfully (Mock)',
data: {
id: announcementId,
title: data.title,
content: data.content,
status: data.status || 'DRAFT',
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}`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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}`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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();
formData.append('file', file);
const response = await authRequest<{ code: number; data: AnnouncementResponse }>(
return await authRequest<ApiResponse<AnnouncementResponse>>(
`/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments`,
{ 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 useMockData = config.public.useMockData as boolean;
if (useMockData) {
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}`,
{ method: 'DELETE' }
);
@ -895,6 +1108,7 @@ export interface CreateQuestionRequest {
question: { th: string; en: string };
explanation?: { th: string; en: string } | null;
question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE';
score?: number;
sort_order?: number;
choices: CreateChoiceRequest[];
}