From 127b63de49350181c7ed7a3f24666528d3160ec6 Mon Sep 17 00:00:00 2001 From: Missez Date: Fri, 6 Feb 2026 14:58:41 +0700 Subject: [PATCH] feat: Implement initial frontend for admin and instructor roles, including dashboards, course management, authentication, and core services. --- .../components/course/StructureTab.vue | 5 +- frontend_management/layouts/admin.vue | 10 +- frontend_management/layouts/instructor.vue | 6 +- .../pages/admin/audit-logs.vue | 452 ++++++++++++++ .../pages/admin/categories/index.vue | 1 + frontend_management/pages/admin/index.vue | 208 ++++++- .../pages/admin/profile/index.vue | 583 ++++++++++++++++++ .../pages/admin/users/index.vue | 5 +- frontend_management/pages/index.vue | 47 +- .../pages/instructor/courses/[id]/index.vue | 19 +- .../pages/instructor/index.vue | 36 +- frontend_management/pages/register.vue | 3 +- frontend_management/services/admin.service.ts | 126 ++++ .../services/instructor.service.ts | 2 +- frontend_management/stores/auth.ts | 23 + frontend_management/stores/instructor.ts | 81 ++- 16 files changed, 1505 insertions(+), 102 deletions(-) create mode 100644 frontend_management/pages/admin/audit-logs.vue create mode 100644 frontend_management/pages/admin/profile/index.vue diff --git a/frontend_management/components/course/StructureTab.vue b/frontend_management/components/course/StructureTab.vue index 5dd42a69..e2e6aa33 100644 --- a/frontend_management/components/course/StructureTab.vue +++ b/frontend_management/components/course/StructureTab.vue @@ -29,7 +29,7 @@ Chapter {{ chapter.sort_order }}: {{ chapter.title.th }}
- {{ chapter.lessons.length }} บทเรียน · {{ getChapterDuration(chapter) }} นาที + {{ chapter.lessons.length }} บทเรียน
@@ -53,9 +53,6 @@ Lesson {{ chapter.sort_order }}.{{ lesson.sort_order }}: {{ lesson.title.th }} - - {{ lesson.duration_minutes }} นาที - diff --git a/frontend_management/layouts/admin.vue b/frontend_management/layouts/admin.vue index 3b74e275..e3d9546e 100644 --- a/frontend_management/layouts/admin.vue +++ b/frontend_management/layouts/admin.vue @@ -54,24 +54,24 @@ - ตั้งค่าระบบ + Audit logs - + diff --git a/frontend_management/layouts/instructor.vue b/frontend_management/layouts/instructor.vue index d8320b6e..e2b8c99e 100644 --- a/frontend_management/layouts/instructor.vue +++ b/frontend_management/layouts/instructor.vue @@ -27,15 +27,15 @@ - + diff --git a/frontend_management/pages/admin/audit-logs.vue b/frontend_management/pages/admin/audit-logs.vue new file mode 100644 index 00000000..725c15d4 --- /dev/null +++ b/frontend_management/pages/admin/audit-logs.vue @@ -0,0 +1,452 @@ + + + + + diff --git a/frontend_management/pages/admin/categories/index.vue b/frontend_management/pages/admin/categories/index.vue index 7923b5cb..65bf74fd 100644 --- a/frontend_management/pages/admin/categories/index.vue +++ b/frontend_management/pages/admin/categories/index.vue @@ -40,6 +40,7 @@ row-key="id" :loading="loading" flat + :rows-per-page-options="[5, 10, 20, 50, 0]" bordered > diff --git a/frontend_management/pages/admin/index.vue b/frontend_management/pages/admin/index.vue index 36b8e361..03e35cb9 100644 --- a/frontend_management/pages/admin/index.vue +++ b/frontend_management/pages/admin/index.vue @@ -15,9 +15,15 @@
{{ authStore.user?.role || 'ADMIN' }}
- + + @@ -56,52 +62,127 @@
-
+
-

หลักสูตรทั้งหมด

-

5

+

คอร์สรออนุมัติ

+

{{ pendingCourses.length }}

+

จากทั้งหมด {{ pendingCourses.length }} รายการ

+
+
+
-
-
+ +
-

ผู้เรียนทั้งหมด

-

125

+

กิจกรรมวันนี้

+

{{ stats.todayLogs }}

+

Logs ทั้งหมดวันนี้

+
+
+
-
-
+ +
-

เรียนจบแล้ว

-

45

+

ผู้ใช้งานทั้งหมด

+

{{ totalUsers }}

+

ในระบบปัจจุบัน

+
+
+
- -
-

Dashboard

- +
+
+

คอร์สรอตรวจสอบล่าสุด

+
-
--> +
+
+ +

กำลังโหลด...

+
+
+ +

ไม่มีคอร์สรอตรวจสอบ

+
+
+
+ +
+ +
+
+
+

{{ course.title.th }}

+

โดย {{ course.creator.username }}

+
+
+ {{ formatDate(course.created_at) }} +
+
+
+
+ + +
+
+

กิจกรรมล่าสุด

+ +
+
+
+ +
+
+

ไม่มีกิจกรรมล่าสุด

+
+
+
+ +
+
+

+ {{ log.user?.username || 'Unknown' }} + {{ formatAction(log.action) }} + {{ log.entity_type }} #{{ log.entity_id }} +

+

{{ formatDate(log.created_at) }}

+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend_management/pages/admin/profile/index.vue b/frontend_management/pages/admin/profile/index.vue new file mode 100644 index 00000000..32dee306 --- /dev/null +++ b/frontend_management/pages/admin/profile/index.vue @@ -0,0 +1,583 @@ + + + diff --git a/frontend_management/pages/admin/users/index.vue b/frontend_management/pages/admin/users/index.vue index 2e636bb5..8a57af14 100644 --- a/frontend_management/pages/admin/users/index.vue +++ b/frontend_management/pages/admin/users/index.vue @@ -4,13 +4,13 @@

จัดการผู้ใช้งาน

- + /> --> diff --git a/frontend_management/pages/index.vue b/frontend_management/pages/index.vue index 21e22eae..f1e3b94b 100644 --- a/frontend_management/pages/index.vue +++ b/frontend_management/pages/index.vue @@ -1,47 +1,18 @@ diff --git a/frontend_management/pages/instructor/courses/[id]/index.vue b/frontend_management/pages/instructor/courses/[id]/index.vue index 1a59006c..deedb30f 100644 --- a/frontend_management/pages/instructor/courses/[id]/index.vue +++ b/frontend_management/pages/instructor/courses/[id]/index.vue @@ -11,7 +11,8 @@
- 0 ผู้เรียน + {{ totalStudentsCount }} ผู้เรียน
@@ -166,6 +167,7 @@ const course = ref(null); const activeTab = ref('structure'); const uploadingThumbnail = ref(false); const thumbnailInputRef = ref(null); +const totalStudentsCount = ref(0); // Computed const totalLessons = computed(() => { @@ -178,7 +180,13 @@ const fetchCourse = async () => { loading.value = true; try { const courseId = parseInt(route.params.id as string); - course.value = await instructorService.getCourseById(courseId); + 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' }); @@ -187,6 +195,7 @@ const fetchCourse = async () => { } }; + const getStatusColor = (status: string) => { const colors: Record = { APPROVED: 'green', @@ -238,8 +247,8 @@ const handleThumbnailUpload = async (event: Event) => { const requestApproval = async () => { if (!course.value) return; try { - await instructorService.submitCourseForApproval(course.value.id); - $q.notify({ type: 'positive', message: 'ส่งขออนุมัติสำเร็จ', position: 'top' }); + const response = await instructorService.submitCourseForApproval(course.value.id); + $q.notify({ type: 'positive', message: response.message || 'ส่งขออนุมัติสำเร็จ', position: 'top' }); fetchCourse(); } catch (error: any) { $q.notify({ diff --git a/frontend_management/pages/instructor/index.vue b/frontend_management/pages/instructor/index.vue index c39ecaef..8b7fb904 100644 --- a/frontend_management/pages/instructor/index.vue +++ b/frontend_management/pages/instructor/index.vue @@ -86,12 +86,39 @@
- + -

สถิติผู้สมัครรวม (รายเดือน)

-
- [กราฟแสดงสถิติผู้สมัครรวม (รายเดือน)] +

สถานะหลักสูตร

+
+
+
+ + เผยแพร่แล้ว +
+ {{ instructorStore.courseStatusCounts.approved }} +
+
+
+ + รอตรวจสอบ +
+ {{ instructorStore.courseStatusCounts.pending }} +
+
+
+ + แบบร่าง +
+ {{ instructorStore.courseStatusCounts.draft }} +
+
+
+ + ถูกปฏิเสธ +
+ {{ instructorStore.courseStatusCounts.rejected }} +
@@ -170,5 +197,6 @@ const handleLogout = () => { // Fetch dashboard data on mount onMounted(() => { instructorStore.fetchDashboardData(); + authStore.fetchUserProfile(); }); diff --git a/frontend_management/pages/register.vue b/frontend_management/pages/register.vue index 762ce9e3..24235ee9 100644 --- a/frontend_management/pages/register.vue +++ b/frontend_management/pages/register.vue @@ -239,9 +239,10 @@ const handleRegister = async () => { router.push('/login'); } catch (error: any) { + const errorMessage = error.data?.error?.message || error.data?.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่'; $q.notify({ type: 'negative', - message: error.data?.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่', + message: errorMessage, position: 'top' }); } finally { diff --git a/frontend_management/services/admin.service.ts b/frontend_management/services/admin.service.ts index 2f9db7c4..507f17c1 100644 --- a/frontend_management/services/admin.service.ts +++ b/frontend_management/services/admin.service.ts @@ -214,6 +214,46 @@ export interface UpdateCategoryRequest { }; } +// Audit Logs Interfaces +export interface AuditLog { + id: number; + user_id: number; + action: string; + entity_type: string; + entity_id: number; + old_value: string | null; + new_value: string | null; + ip_address: string | null; + user_agent: string | null; + metadata: string | null; + created_at: string; + user: { + email: string; + username: string; + id: number; + } | null; +} + +export interface AuditLogsListResponse { + data: AuditLog[]; + pagination: { + totalPages: number; + total: number; + limit: number; + page: number; + }; +} + +export interface AuditLogStats { + totalLogs: number; + todayLogs: number; + actionSummary: { + action: string; + count: number; + }[]; + recentActivity: AuditLog[]; +} + // Helper function to get auth token from cookie const getAuthToken = (): string => { const tokenCookie = useCookie('token'); @@ -391,6 +431,92 @@ export const adminService = { } }); + return response; + }, + + // ============ Audit Logs ============ + async getAuditLogs( + page: number = 1, + limit: number = 20, + filters: { + userId?: number; + action?: string; + entityType?: string; + entityId?: number; + startDate?: string; + endDate?: string; + } = {} + ): Promise { + const config = useRuntimeConfig(); + const token = getAuthToken(); + + let query: any = { page, limit }; + if (filters.userId) query.userId = filters.userId; + if (filters.action) query.action = filters.action; + if (filters.entityType) query.entityType = filters.entityType; + if (filters.entityId) query.entityId = filters.entityId; + if (filters.startDate) query.startDate = filters.startDate; + if (filters.endDate) query.endDate = filters.endDate; + + const response = await $fetch('/api/admin/audit-logs', { + baseURL: config.public.apiBaseUrl as string, + headers: { Authorization: `Bearer ${token}` }, + query + }); + + return response; + }, + + async getAuditLogById(id: number): Promise { + const config = useRuntimeConfig(); + const token = getAuthToken(); + const response = await $fetch(`/api/admin/audit-logs/${id}`, { + baseURL: config.public.apiBaseUrl as string, + headers: { Authorization: `Bearer ${token}` } + }); + return response; + }, + + async getAuditLogStats(): Promise { + const config = useRuntimeConfig(); + const token = getAuthToken(); + const response = await $fetch('/api/admin/audit-logs/stats/summary', { + baseURL: config.public.apiBaseUrl as string, + headers: { Authorization: `Bearer ${token}` } + }); + return response; + }, + + async getAuditLogsByEntity(entityType: string, entityId: number): Promise { + const config = useRuntimeConfig(); + const token = getAuthToken(); + const response = await $fetch(`/api/admin/audit-logs/entity/${entityType}/${entityId}`, { + baseURL: config.public.apiBaseUrl as string, + headers: { Authorization: `Bearer ${token}` } + }); + return response; + }, + + async getAuditLogsByUser(userId: number, limit: number = 20): Promise { + const config = useRuntimeConfig(); + const token = getAuthToken(); + const response = await $fetch(`/api/admin/audit-logs/user/${userId}/activity`, { + baseURL: config.public.apiBaseUrl as string, + headers: { Authorization: `Bearer ${token}` }, + query: { limit } + }); + return response; + }, + + async cleanupAuditLogs(days: number = 90): Promise> { + const config = useRuntimeConfig(); + const token = getAuthToken(); + const response = await $fetch>('/api/admin/audit-logs/cleanup', { + method: 'DELETE', + baseURL: config.public.apiBaseUrl as string, + headers: { Authorization: `Bearer ${token}` }, + query: { days } + }); return response; } }; diff --git a/frontend_management/services/instructor.service.ts b/frontend_management/services/instructor.service.ts index 207c5e5e..0c192def 100644 --- a/frontend_management/services/instructor.service.ts +++ b/frontend_management/services/instructor.service.ts @@ -253,7 +253,7 @@ export const instructorService = { async submitCourseForApproval(courseId: number): Promise> { return await authRequest>( - `/api/instructors/courses/${courseId}/submit`, + `/api/instructors/courses/send-review/${courseId}`, { method: 'POST' } ); }, diff --git a/frontend_management/stores/auth.ts b/frontend_management/stores/auth.ts index 392fa8ee..3858242a 100644 --- a/frontend_management/stores/auth.ts +++ b/frontend_management/stores/auth.ts @@ -1,5 +1,6 @@ import { defineStore } from 'pinia'; import { authService } from '~/services/auth.service'; +import { userService } from '~/services/user.service'; interface User { id: string; @@ -123,6 +124,28 @@ export const useAuthStore = defineStore('auth', { this.logout(); } } + }, + + async fetchUserProfile() { + try { + const response = await userService.getProfile(); + + // Update local user state + this.user = { + id: response.id.toString(), + email: response.email, + firstName: response.profile.first_name, + lastName: response.profile.last_name, + role: response.role.code as 'INSTRUCTOR' | 'ADMIN' | 'STUDENT', + avatarUrl: response.profile.avatar_url + }; + + // Update user cookie to keep it in sync + const userCookie = useCookie('user'); + userCookie.value = JSON.stringify(this.user); + } catch (error) { + console.error('Failed to fetch user profile:', error); + } } } }); diff --git a/frontend_management/stores/instructor.ts b/frontend_management/stores/instructor.ts index fea04177..57cade09 100644 --- a/frontend_management/stores/instructor.ts +++ b/frontend_management/stores/instructor.ts @@ -16,6 +16,13 @@ interface DashboardStats { completedStudents: number; } +interface CourseStatusCounts { + approved: number; + pending: number; + draft: number; + rejected: number; +} + export const useInstructorStore = defineStore('instructor', { state: () => ({ stats: { @@ -24,6 +31,13 @@ export const useInstructorStore = defineStore('instructor', { completedStudents: 0 } as DashboardStats, + courseStatusCounts: { + approved: 0, + pending: 0, + draft: 0, + rejected: 0 + } as CourseStatusCounts, + recentCourses: [] as Course[], loading: false }), @@ -40,21 +54,64 @@ export const useInstructorStore = defineStore('instructor', { // Fetch real courses from API const courses = await instructorService.getCourses(); + // Fetch student counts for each course + let totalStudents = 0; + let completedStudents = 0; + const courseDetails: Course[] = []; + + for (const course of courses.slice(0, 5)) { + try { + // Get student counts + const studentsResponse = await instructorService.getEnrolledStudents(course.id, 1, 1); + const courseStudents = studentsResponse.total || 0; + totalStudents += courseStudents; + + // Get completed count from full list (if small) or estimate + if (courseStudents > 0 && courseStudents <= 100) { + const allStudents = await instructorService.getEnrolledStudents(course.id, 1, 100); + completedStudents += allStudents.data.filter(s => s.status === 'COMPLETED').length; + } + + // Get lesson count from course detail + const courseDetail = await instructorService.getCourseById(course.id); + const lessonCount = courseDetail.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0); + + courseDetails.push({ + id: course.id, + title: course.title.th, + students: courseStudents, + lessons: lessonCount, + icon: 'book', + thumbnail: course.thumbnail_url || null + }); + } catch (e) { + // Course might not have students endpoint + courseDetails.push({ + id: course.id, + title: course.title.th, + students: 0, + lessons: 0, + icon: 'book', + thumbnail: course.thumbnail_url || null + }); + } + } + // Update stats this.stats.totalCourses = courses.length; - // TODO: Get real student counts from API when available - this.stats.totalStudents = 0; - this.stats.completedStudents = 0; + this.stats.totalStudents = totalStudents; + this.stats.completedStudents = completedStudents; - // Map to recent courses format (take first 5) - this.recentCourses = courses.slice(0, 3).map((course, index) => ({ - id: course.id, - title: course.title.th, - students: 0, // TODO: Get from API - lessons: 0, // TODO: Get from course detail API - icon: 'book', - thumbnail: course.thumbnail_url || null - })); + // Update course status counts + this.courseStatusCounts = { + approved: courses.filter(c => c.status === 'APPROVED').length, + pending: courses.filter(c => c.status === 'PENDING').length, + draft: courses.filter(c => c.status === 'DRAFT').length, + rejected: courses.filter(c => c.status === 'REJECTED').length + }; + + // Update recent courses (first 3) + this.recentCourses = courseDetails.slice(0, 3); } catch (error) { console.error('Failed to fetch dashboard data:', error); } finally {