elearning/frontend_management/pages/admin/index.vue
Missez 031ca5c984
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
feat: Add initial e-learning frontend setup including admin and instructor services, layouts, and pages.
2026-02-24 09:25:02 +07:00

276 lines
No EOL
11 KiB
Vue

<template>
<div>
<!-- Header -->
<div class="mb-8">
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-gray-900">
สวสด, {{ authStore.user?.firstName }} {{ authStore.user?.lastName }}
</h1>
<p class="text-gray-600 mt-2">นดอนรบกลบสระบบ</p>
</div>
<div class="flex items-center gap-4">
<div class="text-right">
<div class="text-sm font-semibold text-gray-900">{{ authStore.user?.firstName }} {{ authStore.user?.lastName }}</div>
<div class="text-xs text-gray-500">{{ authStore.user?.role || 'ADMIN' }}</div>
</div>
<div
class="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center text-2xl cursor-pointer hover:bg-primary-200 transition overflow-hidden"
>
<img
v-if="authStore.user?.avatarUrl"
:src="authStore.user.avatarUrl"
alt=""
class="w-full h-full object-cover"
/>
<q-icon v-else name="person" />
<q-menu>
<q-list style="min-width: 200px">
<!-- User Info Header -->
<q-item class="bg-primary-50">
<q-item-section>
<q-item-label class="text-weight-bold">{{ authStore.user?.firstName }} {{ authStore.user?.lastName }}</q-item-label>
<q-item-label caption>{{ authStore.user?.email }}</q-item-label>
</q-item-section>
</q-item>
<q-separator />
<!-- Profile -->
<q-item clickable v-close-popup @click="goToProfile">
<q-item-section avatar>
<q-icon name="person" />
</q-item-section>
<q-item-section>โปรไฟล</q-item-section>
</q-item>
<q-separator />
<!-- Logout -->
<q-item clickable v-close-popup @click="handleLogout">
<q-item-section avatar>
<q-icon name="logout" color="negative" />
</q-item-section>
<q-item-section class="text-negative">ออกจากระบบ</q-item-section>
</q-item>
</q-list>
</q-menu>
</div>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="card bg-white p-6 rounded-lg shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-600 text-sm">คอรสรออน</p>
<p class="text-3xl font-bold text-orange-600">{{ pendingCourses.length }}</p>
<p class="text-xs text-gray-500 mt-1">จากทงหมด {{ pendingCourses.length }} รายการ</p>
</div>
<div class="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center">
<q-icon name="pending_actions" size="28px" class="text-orange-600" />
</div>
</div>
</div>
<div class="card bg-white p-6 rounded-lg shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-600 text-sm">จกรรมวนน</p>
<p class="text-3xl font-bold text-primary-600">{{ stats.todayLogs }}</p>
<p class="text-xs text-gray-500 mt-1">Logs งหมดวนน</p>
</div>
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center">
<q-icon name="today" size="28px" class="text-primary-600" />
</div>
</div>
</div>
<div class="card bg-white p-6 rounded-lg shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-600 text-sm">ใชงานทงหมด</p>
<p class="text-3xl font-bold text-secondary-600">{{ totalUsers }}</p>
<p class="text-xs text-gray-500 mt-1">ในระบบปจจ</p>
</div>
<div class="w-12 h-12 bg-secondary-100 rounded-lg flex items-center justify-center">
<q-icon name="people" size="28px" class="text-secondary-600" />
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Pending Courses List -->
<div class="card bg-white rounded-lg shadow-sm">
<div class="p-6 border-b flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-900">คอรสรอตรวจสอบลาส</h2>
<q-btn flat color="primary" label="ดูทั้งหมด" to="/admin/courses/pending" size="sm" />
</div>
<div class="divide-y">
<div v-if="loading" class="p-8 text-center text-gray-500">
<q-spinner color="primary" size="2em" />
<p class="mt-2">กำลงโหลด...</p>
</div>
<div v-else-if="pendingCourses.length === 0" class="p-8 text-center text-gray-500">
<q-icon name="check_circle" size="48px" color="green-2" class="mb-2" />
<p>ไมคอรสรอตรวจสอบ</p>
</div>
<div
v-else
v-for="course in pendingCourses.slice(0, 5)"
:key="course.id"
class="p-4 flex items-center gap-4 hover:bg-gray-50 transition cursor-pointer"
@click="router.push(`/admin/courses/${course.id}`)"
>
<div class="w-12 h-12 bg-gray-200 rounded-lg flex-shrink-0 overflow-hidden">
<img v-if="course.thumbnail_url" :src="course.thumbnail_url" class="w-full h-full object-cover" />
<div v-else class="w-full h-full flex items-center justify-center text-gray-400">
<q-icon name="image" />
</div>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-sm font-medium text-gray-900 truncate">{{ course.title.th }}</h3>
<p class="text-xs text-gray-500 truncate">โดย {{ course.creator.username }}</p>
</div>
<div class="text-xs text-gray-400 whitespace-nowrap">
{{ formatDate(course.created_at) }}
</div>
</div>
</div>
</div>
<!-- Recent Activity List -->
<div class="card bg-white rounded-lg shadow-sm">
<div class="p-6 border-b flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-900">จกรรมลาส</h2>
<q-btn flat color="primary" label="ดูทั้งหมด" to="/admin/audit-log" size="sm" />
</div>
<div class="divide-y">
<div v-if="loading" class="p-8 text-center text-gray-500">
<q-spinner color="primary" size="2em" />
</div>
<div v-else-if="recentLogs.length === 0" class="p-8 text-center text-gray-500">
<p>ไมจกรรมลาส</p>
</div>
<div
v-else
v-for="log in recentLogs.slice(0, 5)"
:key="log.id"
class="p-4 flex items-start gap-3 text-sm"
>
<div class="mt-0.5">
<q-icon :name="getActionIcon(log.action)" :color="getActionColor(log.action)" size="20px" />
</div>
<div class="flex-1">
<p class="text-gray-900">
<span class="font-medium">{{ log.user?.username || 'Unknown' }}</span>
<span class="text-gray-600 mx-1">{{ formatAction(log.action) }}</span>
<span class="text-primary-700 font-medium">{{ log.entity_type }} #{{ log.entity_id }}</span>
</p>
<p class="text-xs text-gray-400 mt-0.5">{{ formatDate(log.created_at) }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { adminService, type PendingCourse, type AuditLog } from '~/services/admin.service';
definePageMeta({
layout: 'admin',
middleware: 'auth'
});
const authStore = useAuthStore();
const router = useRouter();
// State
const loading = ref(true);
const pendingCourses = ref<PendingCourse[]>([]);
const recentLogs = ref<AuditLog[]>([]);
const totalUsers = ref(0);
const stats = ref({
todayLogs: 0,
totalLogs: 0
});
// Navigation functions
const goToProfile = () => {
router.push('/admin/profile');
};
const handleLogout = () => {
authStore.logout();
router.push('/login');
};
// Data Fetching
const fetchDashboardData = async () => {
loading.value = true;
try {
const [pending, logStats, users] = await Promise.all([
adminService.getPendingCourses(),
adminService.getAuditLogStats(),
adminService.getUsers()
]);
pendingCourses.value = pending;
stats.value = logStats;
recentLogs.value = logStats.recentActivity;
// This might be heavy if lots of users, practically simpler analytics endpoint is better
// but for now this works as requested to use existing API
totalUsers.value = users.length;
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
} finally {
loading.value = false;
}
};
// Utilities
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('th-TH', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
};
const getActionIcon = (action: string) => {
if (action.includes('create')) return 'add_circle';
if (action.includes('update')) return 'edit';
if (action.includes('delete')) return 'delete';
if (action.includes('login')) return 'login';
if (action.includes('approve')) return 'check_circle';
if (action.includes('reject')) return 'cancel';
return 'info';
};
const getActionColor = (action: string) => {
action = action.toLowerCase();
if (action.includes('delete') || action.includes('reject')) return 'negative';
if (action.includes('update')) return 'warning';
if (action.includes('approve') || action.includes('create')) return 'positive';
return 'grey';
};
const formatAction = (action: string) => {
return action.toLowerCase().replace(/_/g, ' ');
};
onMounted(() => {
authStore.fetchUserProfile();
fetchDashboardData();
});
</script>