feat: Implement initial frontend for admin and instructor roles, including dashboards, course management, authentication, and core services.
This commit is contained in:
parent
832a8f5067
commit
127b63de49
16 changed files with 1505 additions and 102 deletions
|
|
@ -15,9 +15,15 @@
|
|||
<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"
|
||||
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"
|
||||
>
|
||||
<q-icon name="person" />
|
||||
<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 -->
|
||||
|
|
@ -56,52 +62,127 @@
|
|||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="card">
|
||||
<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">5</p>
|
||||
<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>
|
||||
<q-icon name="school" size="48px" class="text-primary-200" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
|
||||
<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-500">125</p>
|
||||
<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>
|
||||
<q-icon name="people" size="48px" class="text-secondary-200" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
|
||||
<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-accent-500">45</p>
|
||||
<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>
|
||||
<!-- Recent Courses -->
|
||||
<div class="card">
|
||||
<h2 class="text-xl font-semibold mb-4">Dashboard</h2>
|
||||
<!-- <div class="space-y-4">
|
||||
<div class="flex items-center gap-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div class="w-16 h-16 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<q-icon name="code" size="32px" class="text-primary-600" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold">Python เบื้องต้น</h3>
|
||||
<p class="text-sm text-gray-600">45 ผู้เรียน • 8 บทเรียน</p>
|
||||
</div>
|
||||
<q-btn flat color="primary" label="ดูรายละเอียด" />
|
||||
|
||||
<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> -->
|
||||
<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-logs" 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'
|
||||
|
|
@ -110,6 +191,16 @@ definePageMeta({
|
|||
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');
|
||||
|
|
@ -119,4 +210,67 @@ 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue