elearning/frontend_management/pages/instructor/courses/[id]/index.vue
Missez 941b195813
All checks were successful
Build and Deploy Frontend Management to Dev Server / Build Frontend Management Docker Image (push) Successful in 33s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 2s
Build and Deploy Frontend Management to Dev Server / Notify Deployment Status (push) Successful in 1s
feat: Introduce admin pages for pending course review and course details, and instructor pages for course management and lesson quizzes.
2026-02-10 15:40:03 +07:00

266 lines
8.7 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="course" />
</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>