diff --git a/frontend_management/layouts/admin.vue b/frontend_management/layouts/admin.vue index 956f75ca..6c45023e 100644 --- a/frontend_management/layouts/admin.vue +++ b/frontend_management/layouts/admin.vue @@ -26,6 +26,15 @@ จัดการหลักสูตร + + + คอร์สรออนุมัติ + + +
+ +
+ +
+ + +
+ +

{{ error }}

+ +
+ + +
+ +
+
+ +

{{ course.title.th }}

+

{{ course.title.en }}

+
+
+ + +
+
+ + +
+ +
+ +
+ + +
+ +
+
+ + + + + +
+ +

รายละเอียด

+

{{ course.description.th }}

+ +
+

{{ course.description.en }}

+
+
+
+ + +
+ +
+

สถิติ

+
+
+ จำนวนบท + {{ course.chapters.length }} +
+
+ จำนวนบทเรียน + {{ totalLessons }} +
+
+ วิดีโอ + {{ videoCount }} +
+
+ แบบทดสอบ + {{ quizCount }} +
+
+
+ + +
+

ผู้สอน

+
+
+
+ +
+
+
{{ instructor.user.username }}
+
{{ instructor.user.email }}
+ +
+
+
+
+ + +
+

ไทม์ไลน์

+
+
+ สร้างเมื่อ + {{ formatDate(course.created_at) }} +
+
+ อัพเดทล่าสุด + {{ formatDate(course.updated_at) }} +
+
+
+
+
+ + +
+

โครงสร้างหลักสูตร

+ +
+ +
+
+ +
+ {{ lessonIndex + 1 }}. {{ lesson.title.th }} + +
+ +
+
+
+
+
+ + +
+

ประวัติการอนุมัติ

+ + + +
+

โดย: {{ history.submitter.username }}

+

ผู้ตรวจสอบ: {{ history.reviewer.username }}

+

{{ history.comment }}

+
+
+
+
+
+ + + + + +
ปฏิเสธคอร์ส
+ + +
+ + +

+ กรุณาระบุเหตุผลในการปฏิเสธคอร์ส "{{ course?.title.th }}" +

+ +
+ + + + + +
+
+
+ + + diff --git a/frontend_management/pages/admin/courses/pending.vue b/frontend_management/pages/admin/courses/pending.vue new file mode 100644 index 00000000..b39224fc --- /dev/null +++ b/frontend_management/pages/admin/courses/pending.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/quiz.vue b/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/quiz.vue index 5646db3f..900b7d03 100644 --- a/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/quiz.vue +++ b/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/quiz.vue @@ -116,6 +116,15 @@ label="แสดงเฉลยหลังทำเสร็จ" /> +
+ +
@@ -133,44 +142,56 @@ -
- - -
- คำถามที่ {{ qIndex + 1 }} -
- - + + +
@@ -264,6 +285,7 @@ import { ref, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { useQuasar } from 'quasar'; +import draggable from 'vuedraggable'; import { instructorService, type LessonResponse, type CreateQuestionRequest } from '~/services/instructor.service'; const route = useRoute(); @@ -290,6 +312,50 @@ const quizSettings = ref({ show_answers_after_completion: true }); +const savingSettings = ref(false); + +const saveQuizSettings = async () => { + savingSettings.value = true; + try { + await instructorService.updateQuizSettings(courseId, chapterId, lessonId, { + title: form.value.title, + description: form.value.content, + passing_score: quizSettings.value.passing_score, + time_limit: quizSettings.value.time_limit, + shuffle_questions: quizSettings.value.shuffle_questions, + shuffle_choices: quizSettings.value.shuffle_choices, + show_answers_after_completion: quizSettings.value.show_answers_after_completion + }); + $q.notify({ type: 'positive', message: 'บันทึกการตั้งค่าสำเร็จ', position: 'top' }); + } catch (error) { + console.error('Failed to save quiz settings:', error); + $q.notify({ type: 'negative', message: 'ไม่สามารถบันทึกการตั้งค่าได้', position: 'top' }); + } finally { + savingSettings.value = false; + } +}; + +// Drag and Drop handler (vuedraggable) +const onDragEnd = async (event: { oldIndex: number; newIndex: number }) => { + const { oldIndex, newIndex } = event; + if (oldIndex === newIndex) return; + + const question = questions.value[newIndex]; + if (!question.id) return; + + 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' }); + } 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' }); + } +}; + interface QuizChoice { id?: number; text: { th: string; en: string }; diff --git a/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/video.vue b/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/video.vue index 6137a71a..61179a81 100644 --- a/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/video.vue +++ b/frontend_management/pages/instructor/courses/[id]/chapters/[chapterId]/lessons/[lessonId]/video.vue @@ -162,7 +162,7 @@
(null); const triggerAttachmentInput = () => { + console.log('Trigger attachment input:', attachmentInput.value); attachmentInput.value?.click(); }; diff --git a/frontend_management/pages/instructor/courses/[id]/index.vue b/frontend_management/pages/instructor/courses/[id]/index.vue index be7eae69..6413e45b 100644 --- a/frontend_management/pages/instructor/courses/[id]/index.vue +++ b/frontend_management/pages/instructor/courses/[id]/index.vue @@ -167,12 +167,205 @@ -
+
+

ประกาศ

+ +
+ +
+ +
+ +

ยังไม่มีประกาศ

+ +
+ +
+ + +
+
+
+ +

{{ announcement.title.th }}

+ + {{ announcement.status === 'PUBLISHED' ? 'เผยแพร่' : 'ฉบับร่าง' }} + +
+

{{ announcement.title.en }}

+

{{ announcement.content.th }}

+

+ สร้างเมื่อ {{ formatDate(announcement.created_at) }} +

+
+
+ + +
+
+
+
+ + + + + +
{{ editingAnnouncement ? 'แก้ไขประกาศ' : 'สร้างประกาศใหม่' }}
+ + +
+ + +
+ + + + +
+ + +
+ + +
+
+

ไฟล์แนบ

+ + +
+ + +
+
+
+ + {{ attachment.file_name }} + ({{ formatFileSize(attachment.file_size) }}) +
+ +
+
+ + +
+
+
+ + {{ file.name }} + ({{ formatFileSize(file.size) }}) + +
+ +
+
+ +
+ ยังไม่มีไฟล์แนบ +
+
+
+
+ + + + + +
+
@@ -182,7 +375,9 @@ import { useQuasar } from 'quasar'; import { instructorService, type CourseDetailResponse, - type ChapterResponse + type ChapterResponse, + type AnnouncementResponse, + type CreateAnnouncementRequest } from '~/services/instructor.service'; definePageMeta({ @@ -198,6 +393,25 @@ const course = ref(null); const loading = ref(true); const activeTab = ref('structure'); +// Announcements data +const announcements = ref([]); +const loadingAnnouncements = ref(false); +const showAnnouncementDialog = ref(false); +const editingAnnouncement = ref(null); +const savingAnnouncement = ref(false); +const announcementForm = ref({ + title: { th: '', en: '' }, + content: { th: '', en: '' }, + status: 'DRAFT', + is_pinned: false +}); + +// Attachment handling +const fileInputRef = ref(null); +const uploadingAttachment = ref(false); +const deletingAttachmentId = ref(null); +const pendingFiles = ref([]); + // Computed const totalLessons = computed(() => { if (!course.value) return 0; @@ -300,6 +514,224 @@ const requestApproval = () => { }); }; +// Announcements methods +const fetchAnnouncements = async () => { + loadingAnnouncements.value = true; + try { + const courseId = parseInt(route.params.id as string); + announcements.value = await instructorService.getAnnouncements(courseId); + } catch (error) { + $q.notify({ + type: 'negative', + message: 'ไม่สามารถโหลดข้อมูลประกาศได้', + position: 'top' + }); + } finally { + loadingAnnouncements.value = false; + } +}; + +const formatDate = (date: string) => { + return new Date(date).toLocaleDateString('th-TH', { + day: 'numeric', + month: 'short', + year: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); +}; + +const openAnnouncementDialog = (announcement?: AnnouncementResponse) => { + if (announcement) { + editingAnnouncement.value = announcement; + announcementForm.value = { + title: { ...announcement.title }, + content: { ...announcement.content }, + status: announcement.status, + is_pinned: announcement.is_pinned + }; + } else { + editingAnnouncement.value = null; + announcementForm.value = { + title: { th: '', en: '' }, + content: { th: '', en: '' }, + status: 'DRAFT', + is_pinned: false + }; + } + pendingFiles.value = []; + showAnnouncementDialog.value = true; +}; + +const saveAnnouncement = async () => { + if (!announcementForm.value.title.th || !announcementForm.value.content.th) { + $q.notify({ + type: 'warning', + message: 'กรุณากรอกหัวข้อและเนื้อหา', + position: 'top' + }); + return; + } + + savingAnnouncement.value = true; + try { + const courseId = parseInt(route.params.id as string); + if (editingAnnouncement.value) { + await instructorService.updateAnnouncement(courseId, editingAnnouncement.value.id, announcementForm.value); + $q.notify({ + type: 'positive', + message: 'บันทึกประกาศสำเร็จ', + position: 'top' + }); + } else { + // Create announcement with files + await instructorService.createAnnouncement( + courseId, + announcementForm.value, + pendingFiles.value.length > 0 ? pendingFiles.value : undefined + ); + pendingFiles.value = []; + + $q.notify({ + type: 'positive', + message: 'สร้างประกาศสำเร็จ', + position: 'top' + }); + } + showAnnouncementDialog.value = false; + fetchAnnouncements(); + } catch (error) { + $q.notify({ + type: 'negative', + message: 'เกิดข้อผิดพลาด', + position: 'top' + }); + } finally { + savingAnnouncement.value = false; + } +}; + +const confirmDeleteAnnouncement = (announcement: AnnouncementResponse) => { + $q.dialog({ + title: 'ยืนยันการลบ', + message: `คุณต้องการลบประกาศ "${announcement.title.th}" หรือไม่?`, + cancel: true, + persistent: true + }).onOk(async () => { + try { + const courseId = parseInt(route.params.id as string); + await instructorService.deleteAnnouncement(courseId, announcement.id); + $q.notify({ + type: 'positive', + message: 'ลบประกาศสำเร็จ', + position: 'top' + }); + fetchAnnouncements(); + } catch (error) { + $q.notify({ + type: 'negative', + message: 'เกิดข้อผิดพลาดในการลบประกาศ', + position: 'top' + }); + } + }); +}; + +// Attachment handling methods +const triggerFileInput = () => { + fileInputRef.value?.click(); +}; + +const handleFileUpload = async (event: Event) => { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file) return; + + // If editing existing announcement, upload immediately + if (editingAnnouncement.value) { + uploadingAttachment.value = true; + try { + const courseId = parseInt(route.params.id as string); + const updated = await instructorService.uploadAnnouncementAttachment( + courseId, + editingAnnouncement.value.id, + file + ); + editingAnnouncement.value = updated; + $q.notify({ + type: 'positive', + message: 'อัพโหลดไฟล์สำเร็จ', + position: 'top' + }); + fetchAnnouncements(); + } catch (error) { + $q.notify({ + type: 'negative', + message: 'เกิดข้อผิดพลาดในการอัพโหลดไฟล์', + position: 'top' + }); + } finally { + uploadingAttachment.value = false; + input.value = ''; + } + } else { + // If creating new announcement, add to pending files + pendingFiles.value.push(file); + input.value = ''; + } +}; + +const removePendingFile = (index: number) => { + pendingFiles.value.splice(index, 1); +}; + +const deleteAttachment = async (attachmentId: number) => { + if (!editingAnnouncement.value) return; + + deletingAttachmentId.value = attachmentId; + try { + const courseId = parseInt(route.params.id as string); + await instructorService.deleteAnnouncementAttachment( + courseId, + editingAnnouncement.value.id, + attachmentId + ); + // Remove from local state + editingAnnouncement.value.attachments = editingAnnouncement.value.attachments.filter( + a => a.id !== attachmentId + ); + $q.notify({ + type: 'positive', + message: 'ลบไฟล์สำเร็จ', + position: 'top' + }); + fetchAnnouncements(); + } catch (error) { + $q.notify({ + type: 'negative', + message: 'เกิดข้อผิดพลาดในการลบไฟล์', + position: 'top' + }); + } finally { + deletingAttachmentId.value = null; + } +}; + +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +// Watch for tab change to load announcements +watch(activeTab, (newTab) => { + if (newTab === 'announcements' && announcements.value.length === 0) { + fetchAnnouncements(); + } +}); + // Lifecycle onMounted(() => { fetchCourse(); diff --git a/frontend_management/pages/instructor/courses/[id]/structure.vue b/frontend_management/pages/instructor/courses/[id]/structure.vue index 8aad7692..47e685bc 100644 --- a/frontend_management/pages/instructor/courses/[id]/structure.vue +++ b/frontend_management/pages/instructor/courses/[id]/structure.vue @@ -38,128 +38,101 @@
-
- -
- - - -
- - -
- - -
- -
-
- บทที่ {{ chapter.sort_order }}: {{ chapter.title.th }} -
-
- {{ chapter.lessons.length }} บทเรียน + + + @@ -287,6 +260,7 @@