307 lines
10 KiB
Vue
307 lines
10 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="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">
|
|
<img
|
|
v-if="course.thumbnail_url"
|
|
:src="course.thumbnail_url"
|
|
:alt="course.title.th"
|
|
class="w-full h-full object-cover rounded-lg"
|
|
/>
|
|
<span v-else class="text-white text-sm">รูปหลักสูตร</span>
|
|
</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 :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>0 ผู้เรียน</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="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">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-xl font-semibold text-gray-900">โครงสร้างบทเรียน</h2>
|
|
<q-btn
|
|
color="primary"
|
|
label="จัดการโครงสร้าง"
|
|
@click="navigateTo(`/instructor/courses/${course.id}/structure`)"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Chapters -->
|
|
<div v-if="course.chapters.length === 0" class="text-center py-10 text-gray-500">
|
|
ยังไม่มีบทเรียน
|
|
</div>
|
|
|
|
<div v-else class="space-y-4">
|
|
<q-card
|
|
v-for="chapter in sortedChapters"
|
|
:key="chapter.id"
|
|
flat
|
|
bordered
|
|
class="rounded-lg"
|
|
>
|
|
<q-card-section>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="font-semibold text-gray-900">
|
|
Chapter {{ chapter.sort_order }}: {{ chapter.title.th }}
|
|
</div>
|
|
<div class="text-sm text-gray-500 mt-1">
|
|
{{ chapter.lessons.length }} บทเรียน · {{ getChapterDuration(chapter) }} นาที
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
|
|
<!-- Lessons -->
|
|
<q-list separator class="border-t">
|
|
<q-item
|
|
v-for="lesson in getSortedLessons(chapter)"
|
|
:key="lesson.id"
|
|
class="py-3"
|
|
>
|
|
<q-item-section avatar>
|
|
<q-icon
|
|
:name="getLessonIcon(lesson.type)"
|
|
:color="getLessonIconColor(lesson.type)"
|
|
/>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label>
|
|
Lesson {{ chapter.sort_order }}.{{ lesson.sort_order }}: {{ lesson.title.th }}
|
|
</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<span class="text-sm text-gray-500">{{ lesson.duration_minutes }} นาที</span>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-card>
|
|
</div>
|
|
</q-tab-panel>
|
|
|
|
<!-- Students Tab -->
|
|
<q-tab-panel name="students" class="p-6">
|
|
<div class="text-center py-10 text-gray-500">
|
|
<q-icon name="people" size="60px" color="grey-4" class="mb-4" />
|
|
<p>ยังไม่มีผู้เรียนในหลักสูตรนี้</p>
|
|
</div>
|
|
</q-tab-panel>
|
|
|
|
<!-- Quiz Results Tab -->
|
|
<q-tab-panel name="quiz" class="p-6">
|
|
<div class="text-center py-10 text-gray-500">
|
|
<q-icon name="quiz" size="60px" color="grey-4" class="mb-4" />
|
|
<p>ยังไม่มีผลการทดสอบ</p>
|
|
</div>
|
|
</q-tab-panel>
|
|
|
|
<!-- Announcements Tab -->
|
|
<q-tab-panel name="announcements" class="p-6">
|
|
<div class="text-center py-10 text-gray-500">
|
|
<q-icon name="campaign" size="60px" color="grey-4" class="mb-4" />
|
|
<p>ยังไม่มีประกาศ</p>
|
|
</div>
|
|
</q-tab-panel>
|
|
</q-tab-panels>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useQuasar } from 'quasar';
|
|
import {
|
|
instructorService,
|
|
type CourseDetailResponse,
|
|
type ChapterResponse
|
|
} from '~/services/instructor.service';
|
|
|
|
definePageMeta({
|
|
layout: 'instructor',
|
|
middleware: ['auth']
|
|
});
|
|
|
|
const $q = useQuasar();
|
|
const route = useRoute();
|
|
|
|
// Data
|
|
const course = ref<CourseDetailResponse | null>(null);
|
|
const loading = ref(true);
|
|
const activeTab = ref('structure');
|
|
|
|
// Computed
|
|
const totalLessons = computed(() => {
|
|
if (!course.value) return 0;
|
|
return course.value.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0);
|
|
});
|
|
|
|
const sortedChapters = computed(() => {
|
|
if (!course.value) return [];
|
|
return course.value.chapters.slice().sort((a, b) => a.sort_order - b.sort_order);
|
|
});
|
|
|
|
// Methods
|
|
const fetchCourse = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const courseId = parseInt(route.params.id as string);
|
|
course.value = await instructorService.getCourseById(courseId);
|
|
} catch (error) {
|
|
$q.notify({
|
|
type: 'negative',
|
|
message: 'ไม่สามารถโหลดข้อมูลหลักสูตรได้',
|
|
position: 'top'
|
|
});
|
|
navigateTo('/instructor/courses');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
const colors: Record<string, string> = {
|
|
APPROVED: 'green',
|
|
PENDING: 'yellow',
|
|
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 getChapterDuration = (chapter: ChapterResponse) => {
|
|
return chapter.lessons.reduce((sum, l) => sum + l.duration_minutes, 0);
|
|
};
|
|
|
|
const getSortedLessons = (chapter: ChapterResponse) => {
|
|
return chapter.lessons.slice().sort((a, b) => a.sort_order - b.sort_order);
|
|
};
|
|
|
|
const getLessonIcon = (type: string) => {
|
|
const icons: Record<string, string> = {
|
|
VIDEO: 'play_circle',
|
|
QUIZ: 'quiz',
|
|
DOCUMENT: 'description'
|
|
};
|
|
return icons[type] || 'article';
|
|
};
|
|
|
|
const getLessonIconColor = (type: string) => {
|
|
const colors: Record<string, string> = {
|
|
VIDEO: 'primary',
|
|
QUIZ: 'orange',
|
|
DOCUMENT: 'grey'
|
|
};
|
|
return colors[type] || 'grey';
|
|
};
|
|
|
|
const requestApproval = () => {
|
|
$q.dialog({
|
|
title: 'ขออนุมัติหลักสูตร',
|
|
message: 'ยืนยันการขออนุมัติหลักสูตรนี้?',
|
|
cancel: true,
|
|
persistent: true
|
|
}).onOk(async () => {
|
|
try {
|
|
const courseId = parseInt(route.params.id as string);
|
|
await instructorService.sendForReview(courseId);
|
|
$q.notify({
|
|
type: 'positive',
|
|
message: 'ส่งคำขออนุมัติแล้ว',
|
|
position: 'top'
|
|
});
|
|
// Refresh course data
|
|
fetchCourse();
|
|
} catch (error: any) {
|
|
$q.notify({
|
|
type: 'negative',
|
|
message: error.data?.error?.message || 'ไม่สามารถส่งคำขอได้',
|
|
position: 'top'
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
fetchCourse();
|
|
});
|
|
</script>
|