feat: add utils/date.ts and stores api/user/me
All checks were successful
Build and Deploy Frontend Management to Dev Server / Build Frontend Management Docker Image (push) Successful in 56s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 4s
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 56s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 4s
Build and Deploy Frontend Management to Dev Server / Notify Deployment Status (push) Successful in 1s
This commit is contained in:
parent
ea442d7815
commit
ae32cfebe4
17 changed files with 199 additions and 275 deletions
|
|
@ -449,13 +449,7 @@ const getStatusLabel = (status: string) => {
|
|||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: '2-digit'
|
||||
});
|
||||
};
|
||||
// Date formatting function is auto-imported from utils/date.ts
|
||||
// Clone Dialog
|
||||
const cloneDialog = ref(false);
|
||||
const cloneLoading = ref(false);
|
||||
|
|
|
|||
|
|
@ -64,21 +64,21 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<q-card class="p-6 text-center">
|
||||
<div class="text-4xl font-bold text-primary-600 mb-2">
|
||||
{{ instructorStore.stats.totalCourses }}
|
||||
{{ stats.totalCourses }}
|
||||
</div>
|
||||
<div class="text-gray-600">หลักสูตรทั้งหมด</div>
|
||||
</q-card>
|
||||
|
||||
<q-card class="p-6 text-center">
|
||||
<div class="text-4xl font-bold text-secondary-600 mb-2">
|
||||
{{ instructorStore.stats.totalStudents }}
|
||||
{{ stats.totalStudents }}
|
||||
</div>
|
||||
<div class="text-gray-600">ผู้เรียนทั้งหมด</div>
|
||||
</q-card>
|
||||
|
||||
<q-card class="p-6 text-center">
|
||||
<div class="text-4xl font-bold text-accent-600 mb-2">
|
||||
{{ instructorStore.stats.completedStudents }}
|
||||
{{ stats.completedStudents }}
|
||||
</div>
|
||||
<div class="text-gray-600">เรียนจบแล้ว</div>
|
||||
</q-card>
|
||||
|
|
@ -96,28 +96,28 @@
|
|||
<q-icon name="check_circle" color="green" size="24px" />
|
||||
<span class="font-medium text-gray-700">เผยแพร่แล้ว</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-green-600">{{ instructorStore.courseStatusCounts.approved }}</span>
|
||||
<span class="text-2xl font-bold text-green-600">{{ courseStatusCounts.approved }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-orange-50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<q-icon name="hourglass_empty" color="orange" size="24px" />
|
||||
<span class="font-medium text-gray-700">รอตรวจสอบ</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-orange-600">{{ instructorStore.courseStatusCounts.pending }}</span>
|
||||
<span class="text-2xl font-bold text-orange-600">{{ courseStatusCounts.pending }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<q-icon name="edit_note" color="grey" size="24px" />
|
||||
<span class="font-medium text-gray-700">แบบร่าง</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-gray-600">{{ instructorStore.courseStatusCounts.draft }}</span>
|
||||
<span class="text-2xl font-bold text-gray-600">{{ courseStatusCounts.draft }}</span>
|
||||
</div>
|
||||
<div v-if="instructorStore.courseStatusCounts.rejected > 0" class="flex items-center justify-between p-3 bg-red-50 rounded-lg">
|
||||
<div v-if="courseStatusCounts.rejected > 0" class="flex items-center justify-between p-3 bg-red-50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<q-icon name="cancel" color="red" size="24px" />
|
||||
<span class="font-medium text-gray-700">ถูกปฏิเสธ</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-red-600">{{ instructorStore.courseStatusCounts.rejected }}</span>
|
||||
<span class="text-2xl font-bold text-red-600">{{ courseStatusCounts.rejected }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
|
||||
<div class="space-y-4">
|
||||
<q-card
|
||||
v-for="course in instructorStore.recentCourses"
|
||||
v-for="course in recentCourses"
|
||||
:key="course.id"
|
||||
class="cursor-pointer hover:shadow-md transition"
|
||||
@click="router.push(`/instructor/courses/${course.id}`)"
|
||||
|
|
@ -172,6 +172,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar';
|
||||
import { instructorService } from '~/services/instructor.service';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'instructor',
|
||||
|
|
@ -179,10 +180,32 @@ definePageMeta({
|
|||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const instructorStore = useInstructorStore();
|
||||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
|
||||
// Dashboard local state
|
||||
const stats = ref({
|
||||
totalCourses: 0,
|
||||
totalStudents: 0,
|
||||
completedStudents: 0
|
||||
});
|
||||
|
||||
const courseStatusCounts = ref({
|
||||
approved: 0,
|
||||
pending: 0,
|
||||
draft: 0,
|
||||
rejected: 0
|
||||
});
|
||||
|
||||
const recentCourses = ref<{
|
||||
id: number;
|
||||
title: string;
|
||||
students: number;
|
||||
lessons: number;
|
||||
icon: string;
|
||||
thumbnail: string | null;
|
||||
}[]>([]);
|
||||
|
||||
// Navigation functions
|
||||
const goToProfile = () => {
|
||||
router.push('/instructor/profile');
|
||||
|
|
@ -212,9 +235,41 @@ const handleLogout = () => {
|
|||
});
|
||||
};
|
||||
|
||||
// Fetch dashboard data on mount
|
||||
// Fetch dashboard data
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const [courses, studentStats] = await Promise.all([
|
||||
instructorService.getCourses(),
|
||||
instructorService.getMyStudentsStats()
|
||||
]);
|
||||
|
||||
stats.value.totalCourses = courses.length;
|
||||
stats.value.totalStudents = studentStats.total_students;
|
||||
stats.value.completedStudents = studentStats.total_completed;
|
||||
|
||||
courseStatusCounts.value = {
|
||||
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
|
||||
};
|
||||
|
||||
recentCourses.value = courses.slice(0, 3).map(course => ({
|
||||
id: course.id,
|
||||
title: course.title.th,
|
||||
students: 0,
|
||||
lessons: 0,
|
||||
icon: 'book',
|
||||
thumbnail: course.thumbnail_url || null
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dashboard data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch data on mount
|
||||
onMounted(() => {
|
||||
instructorStore.fetchDashboardData();
|
||||
authStore.fetchUserProfile();
|
||||
fetchDashboardData();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar';
|
||||
import { userService, type UserProfileResponse } from '~/services/user.service';
|
||||
import { userService } from '~/services/user.service';
|
||||
import { authService } from '~/services/auth.service';
|
||||
|
||||
definePageMeta({
|
||||
|
|
@ -368,20 +368,8 @@ const getRoleLabel = (role: string) => {
|
|||
return labels[role] || role;
|
||||
};
|
||||
|
||||
const formatDate = (date: string, includeTime = true) => {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: '2-digit'
|
||||
};
|
||||
|
||||
if (includeTime) {
|
||||
options.hour = '2-digit';
|
||||
options.minute = '2-digit';
|
||||
}
|
||||
|
||||
return new Date(date).toLocaleDateString('th-TH', options);
|
||||
};
|
||||
// Use formatting utilities from utils/date.ts
|
||||
// Format functions are auto-imported
|
||||
|
||||
// Avatar upload
|
||||
const avatarInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
|
@ -425,8 +413,8 @@ const handleAvatarUpload = async (event: Event) => {
|
|||
try {
|
||||
const response = await userService.uploadAvatar(file);
|
||||
|
||||
// Re-fetch profile to get presigned URL from backend
|
||||
await fetchProfile();
|
||||
// Force refresh profile cache and update local state
|
||||
await fetchProfile(true);
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
|
|
@ -457,8 +445,8 @@ const handleUpdateProfile = async () => {
|
|||
phone: editForm.value.phone || null
|
||||
});
|
||||
|
||||
// Refresh profile data from API
|
||||
await fetchProfile();
|
||||
// Force refresh profile cache and update local state
|
||||
await fetchProfile(true);
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
|
|
@ -546,25 +534,29 @@ watch(showEditModal, (newVal) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Fetch profile from API
|
||||
const fetchProfile = async () => {
|
||||
// Helper to map fullProfile to local profile state
|
||||
const mapProfileData = (data: typeof authStore.fullProfile) => {
|
||||
if (!data) return;
|
||||
profile.value = {
|
||||
fullName: `${data.profile.first_name} ${data.profile.last_name}`,
|
||||
email: data.email,
|
||||
emailVerified: !!data.email_verified_at,
|
||||
username: data.username,
|
||||
phone: data.profile.phone || '',
|
||||
role: data.role.code,
|
||||
roleName: data.role.name.th,
|
||||
avatar: '',
|
||||
avatarUrl: data.profile.avatar_url,
|
||||
createdAt: data.created_at
|
||||
};
|
||||
};
|
||||
|
||||
// Fetch profile — uses auth store cache, force=true to refresh
|
||||
const fetchProfile = async (force = false) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await userService.getProfile();
|
||||
|
||||
// Map API response to profile
|
||||
profile.value = {
|
||||
fullName: `${data.profile.first_name} ${data.profile.last_name}`,
|
||||
email: data.email,
|
||||
emailVerified: !!data.email_verified_at,
|
||||
username: data.username,
|
||||
phone: data.profile.phone || '',
|
||||
role: data.role.code,
|
||||
roleName: data.role.name.th,
|
||||
avatar: '',
|
||||
avatarUrl: data.profile.avatar_url,
|
||||
createdAt: data.created_at
|
||||
};
|
||||
await authStore.fetchUserProfile(force);
|
||||
mapProfileData(authStore.fullProfile);
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
|
|
@ -576,7 +568,7 @@ const fetchProfile = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Load profile on mount
|
||||
// Load profile on mount (uses cache if available)
|
||||
onMounted(() => {
|
||||
fetchProfile();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue