feat: Add initial e-learning frontend setup including admin and instructor services, layouts, and pages.
All checks were successful
Build and Deploy Frontend Management to Dev Server / Build Frontend Management Docker Image (push) Successful in 46s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 6s
Build and Deploy Frontend Management to Dev Server / Notify Deployment Status (push) Successful in 1s
All checks were successful
Build and Deploy Frontend Management to Dev Server / Build Frontend Management Docker Image (push) Successful in 46s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 6s
Build and Deploy Frontend Management to Dev Server / Notify Deployment Status (push) Successful in 1s
This commit is contained in:
parent
01d249c19a
commit
031ca5c984
12 changed files with 135 additions and 36 deletions
|
|
@ -83,6 +83,28 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quiz Settings -->
|
||||||
|
<div class="mt-4 p-4 bg-white rounded-lg border border-blue-100">
|
||||||
|
<div class="font-semibold text-gray-700 mb-3">การตั้งค่าเพิ่มเติม</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<q-chip :color="lessonDetail.quiz.shuffle_questions ? 'positive' : 'grey-4'" :text-color="lessonDetail.quiz.shuffle_questions ? 'white' : 'grey-8'" size="m" :icon="lessonDetail.quiz.shuffle_questions ? 'check' : 'close'">
|
||||||
|
สุ่มคำถาม
|
||||||
|
</q-chip>
|
||||||
|
<q-chip :color="lessonDetail.quiz.shuffle_choices ? 'positive' : 'grey-4'" :text-color="lessonDetail.quiz.shuffle_choices ? 'white' : 'grey-8'" size="m" :icon="lessonDetail.quiz.shuffle_choices ? 'check' : 'close'">
|
||||||
|
สุ่มตัวเลือก
|
||||||
|
</q-chip>
|
||||||
|
<q-chip :color="lessonDetail.quiz.show_answers_after_completion ? 'positive' : 'grey-4'" :text-color="lessonDetail.quiz.show_answers_after_completion ? 'white' : 'grey-8'" size="m" :icon="lessonDetail.quiz.show_answers_after_completion ? 'check' : 'close'">
|
||||||
|
เฉลยหลังทำเสร็จ
|
||||||
|
</q-chip>
|
||||||
|
<q-chip :color="lessonDetail.quiz.is_skippable ? 'positive' : 'grey-4'" :text-color="lessonDetail.quiz.is_skippable ? 'white' : 'grey-8'" size="m" :icon="lessonDetail.quiz.is_skippable ? 'check' : 'close'">
|
||||||
|
ข้ามข้อได้
|
||||||
|
</q-chip>
|
||||||
|
<q-chip :color="lessonDetail.quiz.allow_multiple_attempts ? 'positive' : 'grey-4'" :text-color="lessonDetail.quiz.allow_multiple_attempts ? 'white' : 'grey-8'" size="m" :icon="lessonDetail.quiz.allow_multiple_attempts ? 'check' : 'close'">
|
||||||
|
ทำซ้ำได้
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Questions List -->
|
<!-- Questions List -->
|
||||||
<div v-if="lessonDetail.quiz.questions && lessonDetail.quiz.questions.length > 0" class="mt-6 space-y-6">
|
<div v-if="lessonDetail.quiz.questions && lessonDetail.quiz.questions.length > 0" class="mt-6 space-y-6">
|
||||||
<!-- ... (questions rendering code unchanged) ... -->
|
<!-- ... (questions rendering code unchanged) ... -->
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/admin/audit-logs"
|
to="/admin/audit-log"
|
||||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-primary-100 transition mb-2"
|
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-primary-100 transition mb-2"
|
||||||
active-class="bg-primary-500 text-white hover:bg-primary-600"
|
active-class="bg-primary-500 text-white hover:bg-primary-600"
|
||||||
>
|
>
|
||||||
|
|
@ -91,11 +91,29 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authStore.logout();
|
$q.dialog({
|
||||||
router.push('/login');
|
title: 'ยืนยันออกจากระบบ',
|
||||||
|
message: 'คุณต้องการออกจากระบบหรือไม่?',
|
||||||
|
persistent: true,
|
||||||
|
ok: {
|
||||||
|
label: 'ออกจากระบบ',
|
||||||
|
color: 'negative',
|
||||||
|
flat: false
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: 'ยกเลิก',
|
||||||
|
flat: true
|
||||||
|
}
|
||||||
|
}).onOk(() => {
|
||||||
|
authStore.logout();
|
||||||
|
router.push('/login');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="fixed left-0 top-0 h-screen w-64 bg-white shadow-lg">
|
<aside class="fixed left-0 top-0 h-screen w-64 bg-white shadow-lg">
|
||||||
<div class="p-6">
|
<div class="py-6 px-8">
|
||||||
<h2 class="text-xl font-bold text-primary-600">E-Learning</h2>
|
<h2 class="text-xl font-bold text-primary-600">E-Learning</h2>
|
||||||
<p class="text-sm text-gray-500">Instructor Panel</p>
|
<p class="text-sm text-gray-500">Instructor Panel</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,11 +46,29 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authStore.logout();
|
$q.dialog({
|
||||||
router.push('/login');
|
title: 'ยืนยันออกจากระบบ',
|
||||||
|
message: 'คุณต้องการออกจากระบบหรือไม่?',
|
||||||
|
persistent: true,
|
||||||
|
ok: {
|
||||||
|
label: 'ออกจากระบบ',
|
||||||
|
color: 'negative',
|
||||||
|
flat: false
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: 'ยกเลิก',
|
||||||
|
flat: true
|
||||||
|
}
|
||||||
|
}).onOk(() => {
|
||||||
|
authStore.logout();
|
||||||
|
router.push('/login');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,13 @@ export default defineNuxtConfig({
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
title: 'E-Learning System',
|
title: 'E-Learning-Management',
|
||||||
meta: [
|
meta: [
|
||||||
{ charset: 'utf-8' },
|
{ charset: 'utf-8' },
|
||||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', type: 'image/png', href: '/icon.png' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@
|
||||||
<p>เลือกระยะเวลาที่ต้องการเก็บไว้ (ลบข้อมูลที่เก่ากว่ากำหนด):</p>
|
<p>เลือกระยะเวลาที่ต้องการเก็บไว้ (ลบข้อมูลที่เก่ากว่ากำหนด):</p>
|
||||||
<q-select
|
<q-select
|
||||||
v-model="cleanupDays"
|
v-model="cleanupDays"
|
||||||
:options="[30, 60, 90, 180, 365]"
|
:options="[7, 15, 30, 60, 90, 180, 365]"
|
||||||
label="จำนวนวัน"
|
label="จำนวนวัน"
|
||||||
suffix="วัน"
|
suffix="วัน"
|
||||||
outlined
|
outlined
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
<div class="bg-white rounded-xl shadow-sm p-6">
|
||||||
<h3 class="font-semibold text-gray-700 mb-4">สถิติ</h3>
|
<h3 class="font-semibold text-gray-700 mb-4">รายละเอียด</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-gray-500">จำนวนบท</span>
|
<span class="text-gray-500">จำนวนบท</span>
|
||||||
|
|
@ -93,6 +93,14 @@
|
||||||
<span class="text-gray-500">แบบทดสอบ</span>
|
<span class="text-gray-500">แบบทดสอบ</span>
|
||||||
<span class="font-medium">{{ quizCount }}</span>
|
<span class="font-medium">{{ quizCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-500">สร้างเมื่อ</span>
|
||||||
|
<span>{{ formatDate(course.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-500">อัพเดทล่าสุด</span>
|
||||||
|
<span>{{ formatDate(course.updated_at) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -116,21 +124,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Timeline -->
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<h3 class="font-semibold text-gray-700 mb-4">ข้อมูลระบบ</h3>
|
|
||||||
<div class="space-y-3 text-sm">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-gray-500">สร้างเมื่อ</span>
|
|
||||||
<span>{{ formatDate(course.created_at) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-gray-500">อัพเดทล่าสุด</span>
|
|
||||||
<span>{{ formatDate(course.updated_at) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@
|
||||||
<div class="card bg-white rounded-lg shadow-sm">
|
<div class="card bg-white rounded-lg shadow-sm">
|
||||||
<div class="p-6 border-b flex justify-between items-center">
|
<div class="p-6 border-b flex justify-between items-center">
|
||||||
<h2 class="text-lg font-semibold text-gray-900">กิจกรรมล่าสุด</h2>
|
<h2 class="text-lg font-semibold text-gray-900">กิจกรรมล่าสุด</h2>
|
||||||
<q-btn flat color="primary" label="ดูทั้งหมด" to="/admin/audit-logs" size="sm" />
|
<q-btn flat color="primary" label="ดูทั้งหมด" to="/admin/audit-log" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div class="divide-y">
|
<div class="divide-y">
|
||||||
<div v-if="loading" class="p-8 text-center text-gray-500">
|
<div v-if="loading" class="p-8 text-center text-gray-500">
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,34 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Course Structure -->
|
||||||
|
<div v-if="selectedCourse.chapters && selectedCourse.chapters.length > 0" class="mt-6">
|
||||||
|
<div class="font-bold text-lg mb-3">โครงสร้างหลักสูตร (Course Structure)</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<q-expansion-item
|
||||||
|
v-for="(chapter, index) in selectedCourse.chapters"
|
||||||
|
:key="chapter.id"
|
||||||
|
:label="`บทที่ ${index + 1}: ${chapter.title.th}`"
|
||||||
|
:caption="`${chapter.lessons.length} บทเรียน`"
|
||||||
|
header-class="bg-gray-50 rounded-lg"
|
||||||
|
expand-icon-class="text-primary"
|
||||||
|
>
|
||||||
|
<div class="pl-4 pt-2">
|
||||||
|
<div
|
||||||
|
v-for="(lesson, lessonIndex) in chapter.lessons"
|
||||||
|
:key="lesson.id"
|
||||||
|
class="flex items-center gap-3 py-2 border-b last:border-b-0"
|
||||||
|
>
|
||||||
|
<q-icon name="article" color="primary" size="20px" />
|
||||||
|
<span class="text-gray-700">{{ lessonIndex + 1 }}. {{ lesson.title.th }}</span>
|
||||||
|
<span v-if="lesson.title.en" class="text-gray-400 text-xs ml-auto">{{ lesson.title.en }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-expansion-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<!-- Inner Loading -->
|
<!-- Inner Loading -->
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,8 @@ const statusOptions = [
|
||||||
{ label: 'เผยแพร่แล้ว', value: 'APPROVED' },
|
{ label: 'เผยแพร่แล้ว', value: 'APPROVED' },
|
||||||
{ label: 'รอตรวจสอบ', value: 'PENDING' },
|
{ label: 'รอตรวจสอบ', value: 'PENDING' },
|
||||||
{ label: 'แบบร่าง', value: 'DRAFT' },
|
{ label: 'แบบร่าง', value: 'DRAFT' },
|
||||||
{ label: 'ถูกปฏิเสธ', value: 'REJECTED' }
|
{ label: 'ถูกปฏิเสธ', value: 'REJECTED' },
|
||||||
|
//{ label: 'เก็บถาวร', value: 'ARCHIVED' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
|
|
@ -275,7 +276,7 @@ const stats = computed(() => ({
|
||||||
rejected: courses.value.filter(c => c.status === 'REJECTED').length
|
rejected: courses.value.filter(c => c.status === 'REJECTED').length
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Filtered courses
|
// Filtered courses (search only, status is handled server-side)
|
||||||
const filteredCourses = computed(() => {
|
const filteredCourses = computed(() => {
|
||||||
let result = courses.value;
|
let result = courses.value;
|
||||||
|
|
||||||
|
|
@ -287,10 +288,6 @@ const filteredCourses = computed(() => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterStatus.value) {
|
|
||||||
result = result.filter(course => course.status === filterStatus.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -298,7 +295,7 @@ const filteredCourses = computed(() => {
|
||||||
const fetchCourses = async () => {
|
const fetchCourses = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
courses.value = await instructorService.getCourses();
|
courses.value = await instructorService.getCourses(filterStatus.value || undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
|
|
@ -310,12 +307,18 @@ const fetchCourses = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Re-fetch when status filter changes
|
||||||
|
watch(filterStatus, () => {
|
||||||
|
fetchCourses();
|
||||||
|
});
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
const colors: Record<string, string> = {
|
const colors: Record<string, string> = {
|
||||||
APPROVED: 'green',
|
APPROVED: 'green',
|
||||||
PENDING: 'orange',
|
PENDING: 'orange',
|
||||||
DRAFT: 'grey',
|
DRAFT: 'grey',
|
||||||
REJECTED: 'red'
|
REJECTED: 'red',
|
||||||
|
ARCHIVED: 'blue-grey'
|
||||||
};
|
};
|
||||||
return colors[status] || 'grey';
|
return colors[status] || 'grey';
|
||||||
};
|
};
|
||||||
|
|
@ -325,7 +328,8 @@ const getStatusLabel = (status: string) => {
|
||||||
APPROVED: 'เผยแพร่แล้ว',
|
APPROVED: 'เผยแพร่แล้ว',
|
||||||
PENDING: 'รอตรวจสอบ',
|
PENDING: 'รอตรวจสอบ',
|
||||||
DRAFT: 'แบบร่าง',
|
DRAFT: 'แบบร่าง',
|
||||||
REJECTED: 'ถูกปฏิเสธ'
|
REJECTED: 'ถูกปฏิเสธ',
|
||||||
|
ARCHIVED: 'เก็บถาวร'
|
||||||
};
|
};
|
||||||
return labels[status] || status;
|
return labels[status] || status;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
BIN
frontend_management/public/icon.png
Normal file
BIN
frontend_management/public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
|
|
@ -296,6 +296,15 @@ export interface RecommendedCourse {
|
||||||
};
|
};
|
||||||
chapters_count: number;
|
chapters_count: number;
|
||||||
lessons_count: number;
|
lessons_count: number;
|
||||||
|
chapters?: {
|
||||||
|
id: number;
|
||||||
|
title: { th: string; en: string };
|
||||||
|
sort_order: number;
|
||||||
|
lessons: {
|
||||||
|
id: number;
|
||||||
|
title: { th: string; en: string };
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecommendedCoursesListResponse {
|
export interface RecommendedCoursesListResponse {
|
||||||
|
|
|
||||||
|
|
@ -208,8 +208,12 @@ const authRequest = async <T>(
|
||||||
};
|
};
|
||||||
|
|
||||||
export const instructorService = {
|
export const instructorService = {
|
||||||
async getCourses(): Promise<CourseResponse[]> {
|
async getCourses(status?: string): Promise<CourseResponse[]> {
|
||||||
const response = await authRequest<CoursesListResponse>('/api/instructors/courses');
|
let url = '/api/instructors/courses';
|
||||||
|
if (status) {
|
||||||
|
url += `?status=${status}`;
|
||||||
|
}
|
||||||
|
const response = await authRequest<CoursesListResponse>(url);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue