-
คำถามที่ {{ qIndex + 1 }}
-
-
-
+
+
+
+
+
+
+
+ คำถามที่ {{ qIndex + 1 }}
+
+
+
+
+
-
-
{{ question.text.th || '(ยังไม่ได้กรอกคำถาม)' }}
-
-
-
-
-
-
- {{ choice.text.th || `ตัวเลือก ${cIndex + 1}` }}
-
+
{{ question.text.th || '(ยังไม่ได้กรอกคำถาม)' }}
+
+
+
+
+
+
+ {{ choice.text.th || `ตัวเลือก ${cIndex + 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 @@
-