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
|
|
@ -136,7 +136,7 @@
|
|||
<!-- Created At Custom Column -->
|
||||
<template v-slot:body-cell-created_at="props">
|
||||
<q-td :props="props">
|
||||
{{ formatDate(props.value) }}
|
||||
{{ formatDateTime(props.value) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
|
|
@ -169,7 +169,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<div class="text-subtitle2 text-grey">Date & Time</div>
|
||||
<div>{{ formatDate(selectedLog.created_at) }}</div>
|
||||
<div>{{ formatDateTime(selectedLog.created_at) }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -241,7 +241,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useQuasar, type QTableColumn } from 'quasar';
|
||||
import { adminService, type AuditLog, type AuditLogStats } from '~/services/admin.service';
|
||||
|
||||
definePageMeta({
|
||||
|
|
@ -284,15 +284,15 @@ const pagination = ref({
|
|||
});
|
||||
|
||||
// Table setup
|
||||
const columns = [
|
||||
{ name: 'id', label: 'ID', field: 'id', align: 'left', style: 'width: 60px' },
|
||||
{ name: 'action', label: 'Action', field: 'action', align: 'left' },
|
||||
{ name: 'user', label: 'User', field: 'user', align: 'left' },
|
||||
{ name: 'entity_type', label: 'Entity Type', field: 'entity_type', align: 'left' },
|
||||
{ name: 'entity_id', label: 'Entity ID', field: 'entity_id', align: 'left' },
|
||||
const columns: QTableColumn[] = [
|
||||
{ name: 'id', label: 'ID', field: 'id', align: 'left' as const, style: 'width: 60px' },
|
||||
{ name: 'action', label: 'Action', field: 'action', align: 'left' as const },
|
||||
{ name: 'user', label: 'User', field: 'user', align: 'left' as const },
|
||||
{ name: 'entity_type', label: 'Entity Type', field: 'entity_type', align: 'left' as const },
|
||||
{ name: 'entity_id', label: 'Entity ID', field: 'entity_id', align: 'left' as const },
|
||||
|
||||
{ name: 'created_at', label: 'Date & Time', field: 'created_at', align: 'left' },
|
||||
{ name: 'actions', label: '', field: 'actions', align: 'center' }
|
||||
{ name: 'created_at', label: 'Date & Time', field: 'created_at', align: 'left' as const },
|
||||
{ name: 'actions', label: '', field: 'actions', align: 'center' as const }
|
||||
];
|
||||
|
||||
// Actions options (for filtering)
|
||||
|
|
@ -416,10 +416,7 @@ const tryFormatJson = (str: string | null) => {
|
|||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-';
|
||||
return new Date(date).toLocaleString('th-TH');
|
||||
};
|
||||
// Date formatting function is auto-imported from utils/date.ts
|
||||
|
||||
const ACTION_COLOR_MAP: Record<string, string> = {
|
||||
DELETE: 'negative',
|
||||
|
|
@ -453,10 +450,12 @@ onMounted(() => {
|
|||
:deep(input[type=number]::-webkit-outer-spin-button),
|
||||
:deep(input[type=number]::-webkit-inner-spin-button) {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(input[type=number]) {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -233,13 +233,7 @@ const fetchCategories = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('th-TH', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
};
|
||||
// Date formatting function is auto-imported from utils/date.ts
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
|
|
|
|||
|
|
@ -356,23 +356,7 @@ const getActionColor = (action: string) => {
|
|||
return colors[action] || 'grey';
|
||||
};
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const formatDateTime = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
// Date formatting functions are auto-imported from utils/date.ts
|
||||
|
||||
const confirmApprove = () => {
|
||||
if (!course.value) return;
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@
|
|||
<div v-if="course.latest_submission" class="mt-3 text-sm text-gray-500">
|
||||
<q-icon name="send" size="16px" class="mr-1" />
|
||||
ส่งโดย {{ course.latest_submission.submitter.username }}
|
||||
เมื่อ {{ formatDate(course.latest_submission.created_at) }}
|
||||
เมื่อ {{ formatDateTime(course.latest_submission.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -203,7 +203,7 @@
|
|||
<template v-slot:body-cell-submitted_at="props">
|
||||
<q-td :props="props">
|
||||
<div v-if="props.row.latest_submission">
|
||||
<div class="text-xs">{{ formatDate(props.row.latest_submission.created_at) }}</div>
|
||||
<div class="text-xs">{{ formatDateTime(props.row.latest_submission.created_at) }}</div>
|
||||
<div class="text-xs text-gray-500">โดย {{ props.row.latest_submission.submitter.username }}</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
|
|
@ -298,15 +298,7 @@ const getPrimaryInstructor = (course: PendingCourse) => {
|
|||
return primary?.user.username || course.creator.username;
|
||||
};
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
// Date formatting function is auto-imported from utils/date.ts
|
||||
|
||||
const viewCourse = (course: PendingCourse) => {
|
||||
router.push(`/admin/courses/${course.id}`);
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@
|
|||
<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) }}
|
||||
{{ formatDateStr(course.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -170,7 +170,7 @@
|
|||
<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>
|
||||
<p class="text-xs text-gray-400 mt-0.5">{{ formatDateStr(log.created_at) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -254,14 +254,7 @@ const fetchDashboardData = async () => {
|
|||
};
|
||||
|
||||
// Utilities
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('th-TH', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
const formatDateStr = (date: string) => formatDateTime(date);
|
||||
|
||||
const getActionIcon = (action: string) => {
|
||||
if (action.includes('create')) return 'add_circle';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -324,13 +324,7 @@ const getRoleBadgeColor = (roleCode: string) => {
|
|||
return colors[roleCode] || 'grey';
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
const viewUser = (user: AdminUserResponse) => {
|
||||
selectedUser.value = user;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue