- [กราฟแสดงสถิติผู้สมัครรวม (รายเดือน)]
+
สถานะหลักสูตร
+
+
+
+
+ เผยแพร่แล้ว
+
+
{{ 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 {