266 lines
8.8 KiB
Vue
266 lines
8.8 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="flex justify-center py-20">
|
|
<q-spinner-dots size="50px" color="primary" />
|
|
</div>
|
|
|
|
<template v-else-if="course">
|
|
<!-- Course Header -->
|
|
<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="bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0 relative group cursor-pointer overflow-hidden border border-gray-200"
|
|
style="width: 192px; height: 128px;"
|
|
@click="triggerThumbnailUpload"
|
|
>
|
|
<img
|
|
v-if="course.thumbnail_url"
|
|
:src="course.thumbnail_url"
|
|
:alt="course.title.th"
|
|
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"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="flex-1">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900">{{ course.title.th }}</h1>
|
|
<p class="text-gray-600 mt-1">{{ course.description.th }}</p>
|
|
</div>
|
|
|
|
<!-- Status Badges -->
|
|
<div class="flex gap-2">
|
|
<q-badge v-if="course.is_free" color="purple">ฟรี</q-badge>
|
|
<q-badge v-else color="purple">เสียเงิน</q-badge>
|
|
<q-badge :color="getStatusColor(course.status)">
|
|
{{ getStatusLabel(course.status) }}
|
|
</q-badge>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="flex items-center gap-6 mt-4 text-gray-600">
|
|
<div class="flex items-center gap-1">
|
|
<q-icon name="menu_book" size="20px" />
|
|
<span>{{ totalLessons }} บทเรียน</span>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<q-icon name="people" size="20px" />
|
|
<span>{{ totalStudentsCount }} ผู้เรียน</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex flex-col gap-2">
|
|
<q-btn
|
|
outline
|
|
color="primary"
|
|
label="แก้ไข"
|
|
icon="edit"
|
|
@click="navigateTo(`/instructor/courses/${course.id}/edit`)"
|
|
/>
|
|
<q-btn
|
|
v-if="course.status === 'DRAFT'"
|
|
color="primary"
|
|
label="ขออนุมัติหลักสูตร"
|
|
@click="requestApproval"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<q-tabs
|
|
v-model="activeTab"
|
|
class="bg-white rounded-t-xl shadow-sm text-primary-600"
|
|
active-color="primary"
|
|
indicator-color="primary"
|
|
align="left"
|
|
>
|
|
<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>
|
|
|
|
<!-- Tab Panels -->
|
|
<q-tab-panels v-model="activeTab" class="bg-white rounded-b-xl shadow-sm">
|
|
<!-- Structure Tab -->
|
|
<q-tab-panel name="structure" class="p-6">
|
|
<CourseStructureTab :course-id="course.id" :chapters="course.chapters" />
|
|
</q-tab-panel>
|
|
|
|
<!-- Students Tab -->
|
|
<q-tab-panel name="students" class="p-6">
|
|
<CourseStudentsTab :course-id="course.id" />
|
|
</q-tab-panel>
|
|
|
|
<!-- Instructors Tab -->
|
|
<q-tab-panel name="instructors" class="p-6">
|
|
<CourseInstructorsTab :course-id="course.id" />
|
|
</q-tab-panel>
|
|
|
|
<!-- Quiz Results Tab -->
|
|
<q-tab-panel name="quiz" class="p-6">
|
|
<CourseQuizResultsTab :course-id="course.id" :chapters="course.chapters" />
|
|
</q-tab-panel>
|
|
|
|
<!-- Announcements Tab -->
|
|
<q-tab-panel name="announcements" class="p-6">
|
|
<CourseAnnouncementsTab :course-id="course.id" />
|
|
</q-tab-panel>
|
|
</q-tab-panels>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useQuasar } from 'quasar';
|
|
import {
|
|
instructorService,
|
|
type CourseDetailResponse
|
|
} from '~/services/instructor.service';
|
|
|
|
|
|
|
|
definePageMeta({
|
|
layout: 'instructor',
|
|
middleware: ['auth']
|
|
});
|
|
|
|
const $q = useQuasar();
|
|
const route = useRoute();
|
|
|
|
// State
|
|
const loading = ref(true);
|
|
const course = ref<CourseDetailResponse | null>(null);
|
|
const activeTab = ref('structure');
|
|
const uploadingThumbnail = ref(false);
|
|
const thumbnailInputRef = ref<HTMLInputElement | null>(null);
|
|
const totalStudentsCount = ref(0);
|
|
|
|
// Computed
|
|
const totalLessons = computed(() => {
|
|
if (!course.value) return 0;
|
|
return course.value.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0);
|
|
});
|
|
|
|
// Methods
|
|
const fetchCourse = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const courseId = parseInt(route.params.id as string);
|
|
const [courseData, studentsData] = await Promise.all([
|
|
instructorService.getCourseById(courseId),
|
|
instructorService.getEnrolledStudents(courseId, 1, 1)
|
|
]);
|
|
|
|
course.value = courseData;
|
|
totalStudentsCount.value = studentsData.total;
|
|
} catch (error) {
|
|
console.error('Failed to fetch course:', error);
|
|
$q.notify({ type: 'negative', message: 'ไม่สามารถโหลดข้อมูลหลักสูตรได้', position: 'top' });
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
|
|
const getStatusColor = (status: string) => {
|
|
const colors: Record<string, string> = {
|
|
APPROVED: 'green',
|
|
PENDING: 'orange',
|
|
DRAFT: 'grey',
|
|
REJECTED: 'red'
|
|
};
|
|
return colors[status] || 'grey';
|
|
};
|
|
|
|
const getStatusLabel = (status: string) => {
|
|
const labels: Record<string, string> = {
|
|
APPROVED: 'อนุมัติแล้ว',
|
|
PENDING: 'รอตรวจสอบ',
|
|
DRAFT: 'ฉบับร่าง',
|
|
REJECTED: 'ไม่อนุมัติ'
|
|
};
|
|
return labels[status] || status;
|
|
};
|
|
|
|
const triggerThumbnailUpload = () => {
|
|
thumbnailInputRef.value?.click();
|
|
};
|
|
|
|
const handleThumbnailUpload = async (event: Event) => {
|
|
const input = event.target as HTMLInputElement;
|
|
const files = input.files;
|
|
if (!files || files.length === 0 || !course.value) return;
|
|
|
|
uploadingThumbnail.value = true;
|
|
try {
|
|
const response = await instructorService.uploadCourseThumbnail(course.value.id, files[0]);
|
|
if (response.data?.thumbnail_url) {
|
|
course.value.thumbnail_url = response.data.thumbnail_url;
|
|
}
|
|
$q.notify({ type: 'positive', message: response.message, position: 'top' });
|
|
input.value = '';
|
|
} catch (error: any) {
|
|
$q.notify({
|
|
type: 'negative',
|
|
message: error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาดในการอัพโหลดรูปภาพ',
|
|
position: 'top'
|
|
});
|
|
} finally {
|
|
uploadingThumbnail.value = false;
|
|
}
|
|
};
|
|
|
|
const requestApproval = async () => {
|
|
if (!course.value) return;
|
|
try {
|
|
const response = await instructorService.submitCourseForApproval(course.value.id);
|
|
$q.notify({ type: 'positive', message: response.message || 'ส่งขออนุมัติสำเร็จ', position: 'top' });
|
|
fetchCourse();
|
|
} catch (error: any) {
|
|
$q.notify({
|
|
type: 'negative',
|
|
message: error.data?.message || 'ไม่สามารถส่งขออนุมัติได้',
|
|
position: 'top'
|
|
});
|
|
}
|
|
};
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
fetchCourse();
|
|
});
|
|
</script>
|