feat: Implement user authentication, admin user management, and role-based access control.
This commit is contained in:
parent
8a2ca592bc
commit
38648581ec
19 changed files with 1762 additions and 514 deletions
|
|
@ -5,13 +5,13 @@
|
|||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
สวัสดี, {{ authStore.user?.fullName || 'อาจารย์' }}
|
||||
สวัสดี, {{ 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?.fullName || 'อาจารย์ทดสอบ' }}</div>
|
||||
<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 || 'INSTRUCTOR' }}</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<!-- User Info Header -->
|
||||
<q-item class="bg-primary-50">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-weight-bold">{{ authStore.user?.fullName }}</q-item-label>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Profile Card -->
|
||||
<AppCard class="mb-10 ">
|
||||
<div class="mb-10 ">
|
||||
<div class="flex flex-col md:flex-row gap-8">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex flex-col items-center">
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
<div>
|
||||
<div class="text-sm text-gray-600 mb-1">ตำแหน่ง</div>
|
||||
<div class="text-lg text-gray-900">
|
||||
<q-badge color="primary">{{ getRoleLabel(profile.role) }}</q-badge>
|
||||
<q-badge color="primary">{{ profile.roleName || getRoleLabel(profile.role) }}</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mt-5">
|
||||
|
|
@ -108,11 +108,11 @@
|
|||
<q-card-section>
|
||||
<q-form @submit="handleUpdateProfile">
|
||||
<q-input
|
||||
v-model="editForm.fullName"
|
||||
label="ชื่อ-นามสกุล"
|
||||
v-model="editForm.firstName"
|
||||
label="ชื่อ"
|
||||
outlined
|
||||
class="mb-4"
|
||||
:rules="[val => !!val || 'กรุณากรอกชื่อ-นามสกุล']"
|
||||
:rules="[val => !!val || 'กรุณากรอกชื่อ']"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="person" />
|
||||
|
|
@ -120,27 +120,14 @@
|
|||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="editForm.email"
|
||||
label="อีเมล"
|
||||
type="email"
|
||||
v-model="editForm.lastName"
|
||||
label="นามสกุล"
|
||||
outlined
|
||||
class="mb-4"
|
||||
:rules="[val => !!val || 'กรุณากรอกอีเมล']"
|
||||
:rules="[val => !!val || 'กรุณากรอกนามสกุล']"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="email" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="editForm.username"
|
||||
label="Username"
|
||||
outlined
|
||||
class="mb-4"
|
||||
:rules="[val => !!val || 'กรุณากรอก username']"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="account_circle" />
|
||||
<q-icon name="person" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
|
|
@ -274,6 +261,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar';
|
||||
import { userService, type UserProfileResponse } from '~/services/user.service';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'instructor',
|
||||
|
|
@ -283,15 +271,19 @@ definePageMeta({
|
|||
const $q = useQuasar();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// Loading state
|
||||
const loading = ref(true);
|
||||
|
||||
// Profile data
|
||||
const profile = ref({
|
||||
fullName: 'อาจารย์ทดสอบ ระบบอีเลินนิ่ง',
|
||||
email: 'instructor@example.com',
|
||||
username: 'instructor_test',
|
||||
phone: '081-234-5678',
|
||||
role: 'INSTRUCTOR',
|
||||
fullName: '',
|
||||
email: '',
|
||||
username: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
roleName: '',
|
||||
avatar: '👨🏫',
|
||||
createdAt: '2024-01-01'
|
||||
createdAt: ''
|
||||
});
|
||||
|
||||
const stats = ref({
|
||||
|
|
@ -303,9 +295,8 @@ const stats = ref({
|
|||
const showEditModal = ref(false);
|
||||
const saving = ref(false);
|
||||
const editForm = ref({
|
||||
fullName: '',
|
||||
email: '',
|
||||
username: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: ''
|
||||
});
|
||||
|
||||
|
|
@ -351,11 +342,15 @@ const handleUpdateProfile = async () => {
|
|||
saving.value = true;
|
||||
|
||||
try {
|
||||
// TODO: Call API to update profile
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// Call real API to update profile
|
||||
await userService.updateProfile({
|
||||
first_name: editForm.value.firstName,
|
||||
last_name: editForm.value.lastName,
|
||||
phone: editForm.value.phone || null
|
||||
});
|
||||
|
||||
// Update local data
|
||||
profile.value = { ...profile.value, ...editForm.value };
|
||||
// Refresh profile data from API
|
||||
await fetchProfile();
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
|
|
@ -364,7 +359,7 @@ const handleUpdateProfile = async () => {
|
|||
});
|
||||
|
||||
showEditModal.value = false;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'เกิดข้อผิดพลาด กรุณาลองใหม่อีกครั้ง',
|
||||
|
|
@ -379,8 +374,11 @@ const handleChangePassword = async () => {
|
|||
changingPassword.value = true;
|
||||
|
||||
try {
|
||||
// TODO: Call API to change password
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// Call real API to change password
|
||||
await userService.changePassword(
|
||||
passwordForm.value.currentPassword,
|
||||
passwordForm.value.newPassword
|
||||
);
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
|
|
@ -394,10 +392,12 @@ const handleChangePassword = async () => {
|
|||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'รหัสผ่านปัจจุบันไม่ถูกต้อง',
|
||||
message: error.response?.status === 401
|
||||
? 'รหัสผ่านปัจจุบันไม่ถูกต้อง'
|
||||
: 'เกิดข้อผิดพลาดในการเปลี่ยนรหัสผ่าน',
|
||||
position: 'top'
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -408,12 +408,46 @@ const handleChangePassword = async () => {
|
|||
// Watch edit modal
|
||||
watch(showEditModal, (newVal) => {
|
||||
if (newVal) {
|
||||
// Split fullName into firstName and lastName for editing
|
||||
const nameParts = profile.value.fullName.split(' ');
|
||||
editForm.value = {
|
||||
fullName: profile.value.fullName,
|
||||
email: profile.value.email,
|
||||
username: profile.value.username,
|
||||
firstName: nameParts[0] || '',
|
||||
lastName: nameParts.slice(1).join(' ') || '',
|
||||
phone: profile.value.phone
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch profile from API
|
||||
const fetchProfile = async () => {
|
||||
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,
|
||||
username: data.username,
|
||||
phone: data.profile.phone || '',
|
||||
role: data.role.code,
|
||||
roleName: data.role.name.th,
|
||||
avatar: data.profile.avatar_url || '👨🏫',
|
||||
createdAt: data.created_at
|
||||
};
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'ไม่สามารถโหลดข้อมูลโปรไฟล์ได้',
|
||||
position: 'top'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load profile on mount
|
||||
onMounted(() => {
|
||||
fetchProfile();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue