feat: del mock , crud add instructor , VerifyEmail

This commit is contained in:
Missez 2026-02-03 11:55:26 +07:00
parent b2365a4c6a
commit 278bc17fa0
8 changed files with 345 additions and 1566 deletions

View file

@ -29,8 +29,7 @@ export default defineNuxtConfig({
],
runtimeConfig: {
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL,
useMockData: process.env.NUXT_PUBLIC_USE_MOCK_DATA === 'true'
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL
}
},
devtools: { enabled: true },

View file

@ -193,7 +193,7 @@
<q-tab-panel name="instructors" class="p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-900">สอนในรายวชา</h2>
<q-btn color="primary" icon="person_add" label="เพิ่มผู้สอน" @click="showAddInstructorDialog = true" />
<q-btn v-if="isPrimaryInstructor" color="primary" icon="person_add" label="เพิ่มผู้สอน" @click="showAddInstructorDialog = true" />
</div>
<div v-if="loadingInstructors" class="flex justify-center py-10">
@ -223,7 +223,7 @@
<q-item-label caption>{{ instructor.user.email }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-section side v-if="isPrimaryInstructor">
<q-btn flat dense round icon="more_vert">
<q-menu>
<q-item v-if="!instructor.is_primary" clickable v-close-popup @click="setPrimaryInstructor(instructor.user_id)">
@ -469,14 +469,15 @@
<q-card-section>
<q-select
v-model="selectedUser"
:options="filteredUsers"
:options="searchResults"
option-value="id"
option-label="email"
label="ค้นหาผู้ใช้ (Email หรือ Username)"
label="ค้นหาผู้สอน (Email หรือ Username)"
hint="พิมพ์อย่างน้อย 2 ตัวอักษรเพื่อค้นหา"
use-input
filled
@filter="filterUsers"
:loading="loadingUsers"
:loading="loadingSearch"
>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
@ -521,9 +522,9 @@ import {
type ChapterResponse,
type AnnouncementResponse,
type CreateAnnouncementRequest,
type CourseInstructorResponse
type CourseInstructorResponse,
type SearchInstructorResult
} from '~/services/instructor.service';
import { adminService, type AdminUserResponse } from '~/services/admin.service';
definePageMeta({
layout: 'instructor',
@ -532,6 +533,7 @@ definePageMeta({
const $q = useQuasar();
const route = useRoute();
const authStore = useAuthStore();
// Data
const course = ref<CourseDetailResponse | null>(null);
@ -559,11 +561,11 @@ const announcementForm = ref<CreateAnnouncementRequest>({
const instructors = ref<CourseInstructorResponse[]>([]);
const loadingInstructors = ref(false);
const showAddInstructorDialog = ref(false);
const selectedUser = ref<AdminUserResponse | null>(null);
const users = ref<AdminUserResponse[]>([]);
const filteredUsers = ref<AdminUserResponse[]>([]);
const loadingUsers = ref(false);
const selectedUser = ref<SearchInstructorResult | null>(null);
const searchResults = ref<SearchInstructorResult[]>([]);
const loadingSearch = ref(false);
const addingInstructor = ref(false);
const searchQuery = ref('');
// Attachment handling
const fileInputRef = ref<HTMLInputElement | null>(null);
@ -582,6 +584,14 @@ const sortedChapters = computed(() => {
return course.value.chapters.slice().sort((a, b) => a.sort_order - b.sort_order);
});
// Check if current user is the primary instructor
const isPrimaryInstructor = computed(() => {
if (!authStore.user?.id) return false;
const currentUserId = parseInt(authStore.user.id);
const myInstructorRecord = instructors.value.find(i => i.user_id === currentUserId);
return myInstructorRecord?.is_primary === true;
});
// Methods
const fetchCourse = async () => {
loading.value = true;
@ -757,31 +767,40 @@ const fetchInstructors = async () => {
}
};
const filterUsers = async (val: string, update: (callback: () => void) => void) => {
if (users.value.length === 0) {
loadingUsers.value = true;
try {
users.value = await adminService.getUsers();
} catch (error) {
console.error('Failed to load users', error);
} finally {
loadingUsers.value = false;
}
let searchTimeout: NodeJS.Timeout | null = null;
const filterUsers = (val: string, update: (callback: () => void) => void, abort: () => void) => {
// Abort if query is too short
if (val.length < 2) {
abort();
return;
}
update(() => {
const needle = val.toLowerCase();
const existingInstructorIds = instructors.value.map(i => i.user_id);
filteredUsers.value = users.value.filter(v => {
// Exclude existing instructors
if (existingInstructorIds.includes(v.id)) return false;
// Clear previous timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Debounce the search
searchTimeout = setTimeout(async () => {
loadingSearch.value = true;
try {
const results = await instructorService.searchInstructors(val);
const existingInstructorIds = instructors.value.map(i => i.user_id);
// Filter by username or email
return v.username.toLowerCase().indexOf(needle) > -1 ||
v.email.toLowerCase().indexOf(needle) > -1;
});
});
// Filter out existing instructors
update(() => {
searchResults.value = results.filter(r => !existingInstructorIds.includes(r.id));
});
} catch (error) {
console.error('Failed to search instructors', error);
update(() => {
searchResults.value = [];
});
} finally {
loadingSearch.value = false;
}
}, 300);
};
const addInstructor = async () => {
@ -790,7 +809,7 @@ const addInstructor = async () => {
addingInstructor.value = true;
try {
const courseId = parseInt(route.params.id as string);
const response = await instructorService.addInstructor(courseId, selectedUser.value.id);
const response = await instructorService.addInstructor(courseId, selectedUser.value.email);
$q.notify({
type: 'positive',

View file

@ -50,7 +50,11 @@
<div>
<div class="text-sm text-gray-600 mb-1">เมล</div>
<div class="text-lg text-gray-900">{{ profile.email }}</div>
<div class="text-lg text-gray-900 flex items-center gap-2">
{{ profile.email }}
<q-badge v-if="profile.emailVerified" color="positive" label="ยืนยันแล้ว" />
<q-badge v-else color="warning" label="ยังไม่ยืนยัน" />
</div>
</div>
<div>
@ -75,7 +79,7 @@
</div>
<!-- Action Buttons -->
<div class="flex gap-3 mt-6">
<div class="flex flex-wrap gap-3 mt-6">
<q-btn
color="primary"
label="แก้ไขโปรไฟล์"
@ -89,6 +93,15 @@
icon="lock"
@click="showPasswordModal = true"
/>
<q-btn
v-if="!profile.emailVerified"
outline
color="orange"
label="ขอยืนยันอีเมล"
icon="mark_email_unread"
:loading="sendingVerification"
@click="handleSendVerificationEmail"
/>
</div>
</div>
</div>
@ -289,6 +302,7 @@
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { userService, type UserProfileResponse } from '~/services/user.service';
import { authService } from '~/services/auth.service';
definePageMeta({
layout: 'instructor',
@ -305,11 +319,12 @@ const loading = ref(true);
const profile = ref({
fullName: '',
email: '',
emailVerified: false,
username: '',
phone: '',
role: '',
roleName: '',
avatar: '👨‍🏫',
avatar: '',
avatarUrl: '' as string | null,
createdAt: ''
});
@ -340,6 +355,9 @@ const passwordForm = ref({
confirmPassword: ''
});
// Email verification
const sendingVerification = ref(false);
// Methods
const getRoleLabel = (role: string) => {
const labels: Record<string, string> = {
@ -495,6 +513,26 @@ const handleChangePassword = async () => {
}
};
const handleSendVerificationEmail = async () => {
sendingVerification.value = true;
try {
const response = await authService.sendVerifyEmail();
$q.notify({
type: 'positive',
message: response.message || 'ส่งอีเมลยืนยันไปยังอีเมลของคุณแล้ว',
position: 'top'
});
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.data?.message || 'เกิดข้อผิดพลาดในการส่งอีเมลยืนยัน',
position: 'top'
});
} finally {
sendingVerification.value = false;
}
};
// Watch edit modal
watch(showEditModal, (newVal) => {
if (newVal) {
@ -518,11 +556,12 @@ const fetchProfile = async () => {
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: '👨‍🏫',
avatar: '',
avatarUrl: data.profile.avatar_url,
createdAt: data.created_at
};

View file

@ -1,74 +0,0 @@
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
if (!config.public.useMockData) {
return; // ใช้ API จริง
}
// Mock Data
const mockUsers = [
{ id: '1', email: 'instructor@test.com', fullName: 'อาจารย์ทดสอบ', role: 'INSTRUCTOR' },
{ id: '2', email: 'admin@test.com', fullName: 'ผู้ดูแลระบบ', role: 'ADMIN' }
];
const mockCourses = [
{
id: '1',
title: 'Python เบื้องต้น',
description: 'เรียนรู้ Python จากพื้นฐาน',
instructorId: '1',
status: 'PUBLISHED',
thumbnail: '/images/python.jpg'
},
{
id: '2',
title: 'JavaScript สำหรับเว็บ',
description: 'พัฒนาเว็บด้วย JavaScript',
instructorId: '1',
status: 'DRAFT',
thumbnail: '/images/javascript.jpg'
}
];
// Mock API Functions
const mockApi = {
// Auth
login: async (email: string, password: string) => {
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate delay
const user = mockUsers.find(u => u.email === email);
if (user) {
return {
token: 'mock-jwt-token',
user
};
}
throw new Error('Invalid credentials');
},
// Courses
getCourses: async () => {
await new Promise(resolve => setTimeout(resolve, 300));
return mockCourses;
},
getCourse: async (id: string) => {
await new Promise(resolve => setTimeout(resolve, 300));
return mockCourses.find(c => c.id === id);
},
createCourse: async (data: any) => {
await new Promise(resolve => setTimeout(resolve, 500));
const newCourse = {
id: String(mockCourses.length + 1),
...data,
status: 'DRAFT'
};
mockCourses.push(newCourse);
return newCourse;
},
// Users
getUsers: async () => {
await new Promise(resolve => setTimeout(resolve, 300));
return mockUsers;
}
};
return {
provide: {
mockApi
}
};
});

View file

@ -158,468 +158,6 @@ export interface CourseDetailForReviewResponse {
data: CourseDetailForReview;
}
// Mock data for development
const MOCK_USERS: AdminUserResponse[] = [
{
id: 1,
username: 'admin',
email: 'admin@elearning.local',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
role: { code: 'ADMIN', name: { en: 'Administrator', th: 'ผู้ดูแลระบบ' } },
profile: {
prefix: { en: 'Mr.', th: 'นาย' },
first_name: 'Admin',
last_name: 'User',
avatar_url: null,
birth_date: null,
phone: '0812345678'
}
},
{
id: 2,
username: 'instructor',
email: 'instructor@elearning.local',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
role: { code: 'INSTRUCTOR', name: { en: 'Instructor', th: 'ผู้สอน' } },
profile: {
prefix: { en: 'Mr.', th: 'นาย' },
first_name: 'John',
last_name: 'Instructor',
avatar_url: null,
birth_date: null,
phone: '0812345679'
}
},
{
id: 3,
username: 'student',
email: 'student@elearning.local',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
role: { code: 'STUDENT', name: { en: 'Student', th: 'นักเรียน' } },
profile: {
prefix: { en: 'Ms.', th: 'นางสาว' },
first_name: 'Jane',
last_name: 'Student',
avatar_url: null,
birth_date: null,
phone: '0812345680'
}
}
];
// Mock pending courses data
const MOCK_PENDING_COURSES: PendingCourse[] = [
{
id: 1,
title: { th: 'พื้นฐาน JavaScript', en: 'JavaScript Fundamentals' },
slug: 'javascript-fundamentals',
description: { th: 'เรียนรู้ JavaScript ตั้งแต่เริ่มต้น', en: 'Learn JavaScript from scratch' },
thumbnail_url: null,
status: 'PENDING',
created_at: '2024-02-01T00:00:00Z',
updated_at: '2024-02-10T00:00:00Z',
created_by: 2,
creator: { id: 2, email: 'instructor@elearning.local', username: 'instructor' },
instructors: [
{
user_id: 2,
is_primary: true,
user: { id: 2, email: 'instructor@elearning.local', username: 'instructor' }
}
],
chapters_count: 5,
lessons_count: 15,
latest_submission: {
id: 1,
submitted_by: 2,
created_at: '2024-02-10T00:00:00Z',
submitter: { id: 2, email: 'instructor@elearning.local', username: 'instructor' }
}
},
{
id: 2,
title: { th: 'React สำหรับผู้เริ่มต้น', en: 'React for Beginners' },
slug: 'react-for-beginners',
description: { th: 'เรียนรู้ React ตั้งแต่พื้นฐาน', en: 'Learn React from basics' },
thumbnail_url: null,
status: 'PENDING',
created_at: '2024-02-05T00:00:00Z',
updated_at: '2024-02-12T00:00:00Z',
created_by: 2,
creator: { id: 2, email: 'instructor@elearning.local', username: 'instructor' },
instructors: [
{
user_id: 2,
is_primary: true,
user: { id: 2, email: 'instructor@elearning.local', username: 'instructor' }
}
],
chapters_count: 8,
lessons_count: 24,
latest_submission: {
id: 2,
submitted_by: 2,
created_at: '2024-02-12T00:00:00Z',
submitter: { id: 2, email: 'instructor@elearning.local', username: 'instructor' }
}
}
];
// Mock course detail for review
const MOCK_COURSE_DETAIL: CourseDetailForReview = {
id: 1,
title: { th: 'พื้นฐาน JavaScript', en: 'JavaScript Fundamentals' },
slug: 'javascript-fundamentals',
description: { th: 'เรียนรู้ JavaScript ตั้งแต่เริ่มต้น รวมถึง ES6+ features และ best practices', en: 'Learn JavaScript from scratch including ES6+ features and best practices' },
thumbnail_url: null,
price: 990,
is_free: false,
have_certificate: true,
status: 'PENDING',
created_at: '2024-02-01T00:00:00Z',
updated_at: '2024-02-10T00:00:00Z',
category: { id: 1, name: { th: 'การพัฒนาเว็บไซต์', en: 'Web Development' } },
creator: { id: 2, email: 'instructor@elearning.local', username: 'instructor' },
instructors: [
{
user_id: 2,
is_primary: true,
user: { id: 2, email: 'instructor@elearning.local', username: 'instructor' }
}
],
chapters: [
{
id: 1,
title: { th: 'แนะนำ JavaScript', en: 'Introduction to JavaScript' },
sort_order: 1,
is_published: true,
lessons: [
{ id: 1, title: { th: 'JavaScript คืออะไร', en: 'What is JavaScript' }, type: 'VIDEO', sort_order: 1, is_published: true },
{ id: 2, title: { th: 'ติดตั้ง Development Environment', en: 'Setup Development Environment' }, type: 'VIDEO', sort_order: 2, is_published: true },
{ id: 3, title: { th: 'แบบทดสอบบทที่ 1', en: 'Chapter 1 Quiz' }, type: 'QUIZ', sort_order: 3, is_published: true }
]
},
{
id: 2,
title: { th: 'Variables และ Data Types', en: 'Variables and Data Types' },
sort_order: 2,
is_published: true,
lessons: [
{ id: 4, title: { th: 'var, let และ const', en: 'var, let and const' }, type: 'VIDEO', sort_order: 1, is_published: true },
{ id: 5, title: { th: 'Primitive Data Types', en: 'Primitive Data Types' }, type: 'VIDEO', sort_order: 2, is_published: true }
]
}
],
approval_history: [
{
id: 1,
action: 'SUBMITTED',
comment: null,
created_at: '2024-02-10T10:00:00Z',
submitter: { id: 2, email: 'instructor@elearning.local', username: 'instructor' },
reviewer: null
}
]
};
// Helper function to get auth token from cookie or localStorage
const getAuthToken = (): string => {
const tokenCookie = useCookie('token');
return tokenCookie.value || '';
};
export const adminService = {
async getUsers(): Promise<AdminUserResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_USERS;
}
const token = getAuthToken();
const response = await $fetch<UsersListResponse>('/api/admin/usermanagement/users', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
},
async getUserById(id: number): Promise<AdminUserResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 300));
const user = MOCK_USERS.find(u => u.id === id);
if (!user) throw new Error('User not found');
return user;
}
const token = getAuthToken();
const response = await $fetch<AdminUserResponse>(`/api/admin/usermanagement/users/${id}`, {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
},
async updateUserRole(userId: number, roleId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'User role updated successfully (Mock)',
data: undefined
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/usermanagement/role/${userId}`, {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: {
id: userId,
role_id: roleId
}
});
return response;
},
async deleteUser(userId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'User deleted successfully (Mock)',
data: undefined
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/usermanagement/users/${userId}`, {
method: 'DELETE',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
},
// ============ Pending Courses ============
async getPendingCourses(): Promise<PendingCourse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_PENDING_COURSES;
}
const token = getAuthToken();
const response = await $fetch<PendingCoursesListResponse>('/api/admin/courses/pending', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
},
async getCourseForReview(courseId: number): Promise<CourseDetailForReview> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_COURSE_DETAIL;
}
const token = getAuthToken();
const response = await $fetch<CourseDetailForReviewResponse>(`/api/admin/courses/${courseId}`, {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
},
async approveCourse(courseId: number, comment?: string): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Course approved successfully (Mock)',
data: undefined
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/courses/${courseId}/approve`, {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: { comment: comment || '' }
});
return response;
},
async rejectCourse(courseId: number, comment: string): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Course rejected successfully (Mock)',
data: undefined
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/courses/${courseId}/reject`, {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: { comment }
});
return response;
},
// ============ Categories ============
async getCategories(): Promise<CategoryResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_CATEGORIES;
}
const token = getAuthToken();
const response = await $fetch<CategoriesListResponse>('/api/categories', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data.categories;
},
async createCategory(data: CreateCategoryRequest): Promise<ApiResponse<CategoryResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Category created successfully (Mock)',
data: { ...MOCK_CATEGORIES[0], id: Date.now() }
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<CategoryResponse>>('/api/admin/categories', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: data
});
return response;
},
async updateCategory(id: number, data: UpdateCategoryRequest): Promise<ApiResponse<CategoryResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Category updated successfully (Mock)',
data: { ...MOCK_CATEGORIES[0], id }
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<CategoryResponse>>(`/api/admin/categories/${id}`, {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: data
});
return response;
},
async deleteCategory(id: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Category deleted successfully (Mock)',
data: undefined
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/categories/${id}`, {
method: 'DELETE',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
}
};
// Category interfaces
export interface CategoryResponse {
id: number;
@ -675,45 +213,183 @@ export interface UpdateCategoryRequest {
};
}
// Mock categories
const MOCK_CATEGORIES: CategoryResponse[] = [
{
id: 1,
name: { en: 'Web Development', th: 'การพัฒนาเว็บไซต์' },
slug: 'web-development',
description: { en: 'Learn web development', th: 'หลักสูตรเกี่ยวกับการพัฒนาเว็บไซต์และเว็บแอปพลิเคชัน' },
icon: 'code',
sort_order: 1,
is_active: true,
created_at: '2024-01-15T00:00:00Z',
created_by: 1,
updated_at: '2024-01-15T00:00:00Z',
updated_by: null
// Helper function to get auth token from cookie
const getAuthToken = (): string => {
const tokenCookie = useCookie('token');
return tokenCookie.value || '';
};
export const adminService = {
async getUsers(): Promise<AdminUserResponse[]> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<UsersListResponse>('/api/admin/usermanagement/users', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
},
{
id: 2,
name: { en: 'Mobile Development', th: 'การพัฒนาแอปพลิเคชันมือถือ' },
slug: 'mobile-development',
description: { en: 'Learn mobile app development', th: 'หลักสูตรเกี่ยวกับการพัฒนาแอปพลิเคชันบนมือถือ' },
icon: 'smartphone',
sort_order: 2,
is_active: true,
created_at: '2024-01-20T00:00:00Z',
created_by: 1,
updated_at: '2024-01-20T00:00:00Z',
updated_by: null
async getUserById(id: number): Promise<AdminUserResponse> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<AdminUserResponse>(`/api/admin/usermanagement/users/${id}`, {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
},
{
id: 3,
name: { en: 'Database', th: 'ฐานข้อมูล' },
slug: 'database',
description: { en: 'Learn database management', th: 'หลักสูตรเกี่ยวกับการออกแบบและจัดการฐานข้อมูล' },
icon: 'storage',
sort_order: 3,
is_active: true,
created_at: '2024-02-01T00:00:00Z',
created_by: 1,
updated_at: '2024-02-01T00:00:00Z',
updated_by: null
async updateUserRole(userId: number, roleId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/usermanagement/role/${userId}`, {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: {
id: userId,
role_id: roleId
}
});
return response;
},
async deleteUser(userId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/usermanagement/users/${userId}`, {
method: 'DELETE',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
},
// ============ Pending Courses ============
async getPendingCourses(): Promise<PendingCourse[]> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<PendingCoursesListResponse>('/api/admin/courses/pending', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
},
async getCourseForReview(courseId: number): Promise<CourseDetailForReview> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<CourseDetailForReviewResponse>(`/api/admin/courses/${courseId}`, {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
},
async approveCourse(courseId: number, comment?: string): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/courses/${courseId}/approve`, {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: { comment: comment || '' }
});
return response;
},
async rejectCourse(courseId: number, comment: string): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/courses/${courseId}/reject`, {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: { comment }
});
return response;
},
// ============ Categories ============
async getCategories(): Promise<CategoryResponse[]> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<CategoriesListResponse>('/api/categories', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data.categories;
},
async createCategory(data: CreateCategoryRequest): Promise<ApiResponse<CategoryResponse>> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<ApiResponse<CategoryResponse>>('/api/admin/categories', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: data
});
return response;
},
async updateCategory(id: number, data: UpdateCategoryRequest): Promise<ApiResponse<CategoryResponse>> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<ApiResponse<CategoryResponse>>(`/api/admin/categories/${id}`, {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: data
});
return response;
},
async deleteCategory(id: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<ApiResponse<void>>(`/api/admin/categories/${id}`, {
method: 'DELETE',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
}
];
};

View file

@ -55,69 +55,12 @@ export interface ApiResponse<T> {
data: T;
}
// Mock data for development
const MOCK_USERS = [
{
email: 'admin@elearning.local',
password: 'password',
user: {
id: '1',
email: 'admin@elearning.local',
firstName: 'ผู้ดูแล',
lastName: 'ระบบ',
role: 'ADMIN'
}
},
{
email: 'instructor@elearning.local',
password: 'password',
user: {
id: '2',
email: 'instructor@elearning.local',
firstName: 'อาจารย์',
lastName: 'ทดสอบ',
role: 'INSTRUCTOR'
}
}
];
export const authService = {
async login(email: string, password: string): Promise<LoginResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
// Use Mock Data
if (useMockData) {
return this.mockLogin(email, password);
}
// Use Real API
return this.apiLogin(email, password);
},
// Mock login for development
async mockLogin(email: string, password: string): Promise<LoginResponse> {
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate delay
const mockUser = MOCK_USERS.find(u => u.email === email);
if (!mockUser || mockUser.password !== password) {
throw new Error('อีเมลหรือรหัสผ่านไม่ถูกต้อง');
}
return {
token: 'mock-jwt-token',
refreshToken: 'mock-refresh-token',
user: mockUser.user
};
},
// Real API login
async apiLogin(email: string, password: string): Promise<LoginResponse> {
const config = useRuntimeConfig();
try {
const response = await $fetch<ApiLoginResponse>('/api/auth/login', {
const response = await $fetch<ApiResponse<ApiLoginResponse>>('/api/auth/login', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
body: {
@ -126,24 +69,26 @@ export const authService = {
}
});
const loginData = response.data;
// Check if user role is STUDENT - block login
if (response.user.role.code === 'STUDENT') {
if (loginData.user.role.code === 'STUDENT') {
throw new Error('ไม่สามารถเข้าสู่ระบบได้ ระบบนี้สำหรับผู้สอนและผู้ดูแลระบบเท่านั้น');
}
// Transform API response to frontend format
return {
token: response.token,
refreshToken: response.refreshToken,
token: loginData.token,
refreshToken: loginData.refreshToken,
user: {
id: response.user.id.toString(),
email: response.user.email,
firstName: response.user.profile.first_name,
lastName: response.user.profile.last_name,
role: response.user.role.code,
avatarUrl: response.user.profile.avatar_url
id: loginData.user.id.toString(),
email: loginData.user.email,
firstName: loginData.user.profile.first_name,
lastName: loginData.user.profile.last_name,
role: loginData.user.role.code,
avatarUrl: loginData.user.profile.avatar_url
},
message: 'เข้าสู่ระบบสำเร็จ' // Note: Backend usually returns message too, but we can default it or use backend's
message: response.message || 'เข้าสู่ระบบสำเร็จ'
};
} catch (error: any) {
// Re-throw custom errors (like STUDENT role block)
@ -179,19 +124,7 @@ export const authService = {
async forgotPassword(email: string): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
// Mock: simulate sending email
await new Promise(resolve => setTimeout(resolve, 1000));
return {
code: 200,
message: 'ส่งลิงก์รีเซ็ตรหัสผ่านไปยังอีเมลของคุณแล้ว (Mock)',
data: undefined
};
}
// Real API
const response = await $fetch<ApiResponse<void>>('/api/auth/reset-request', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
@ -202,15 +135,7 @@ export const authService = {
async resetPassword(token: string, password: string): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
// Mock: simulate reset password
await new Promise(resolve => setTimeout(resolve, 1000));
return;
}
// Real API
await $fetch('/api/auth/reset-password', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
@ -220,15 +145,7 @@ export const authService = {
async registerInstructor(data: RegisterInstructorRequest): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
// Mock: simulate registration
await new Promise(resolve => setTimeout(resolve, 1000));
return;
}
// Real API
await $fetch('/api/auth/register-instructor', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
@ -238,28 +155,46 @@ export const authService = {
async refreshToken(currentRefreshToken: string): Promise<{ token: string; refreshToken: string }> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
// Mock: return new tokens
await new Promise(resolve => setTimeout(resolve, 500));
return {
token: 'mock-new-jwt-token-' + Date.now(),
refreshToken: 'mock-new-refresh-token-' + Date.now()
};
}
if (!currentRefreshToken) {
throw new Error('No refresh token available');
}
// Real API
const response = await $fetch<{ token: string; refreshToken: string }>('/api/auth/refresh', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
body: { refreshToken: currentRefreshToken }
});
return response;
},
async sendVerifyEmail(): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const token = useCookie('token').value;
const response = await $fetch<ApiResponse<void>>('/api/user/send-verify-email', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
},
async verifyEmail(verificationToken: string): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const token = useCookie('token').value;
const response = await $fetch<ApiResponse<void>>('/api/user/verify-email', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: { token: verificationToken }
});
return response;
}
};

View file

@ -63,6 +63,23 @@ export interface InstructorsListResponse {
data: CourseInstructorResponse[];
}
export interface SearchInstructorResult {
id: number;
username: string;
email: string;
profile: {
first_name: string;
last_name: string;
avatar_url: string | null;
};
}
export interface SearchInstructorsResponse {
code: number;
message: string;
data: SearchInstructorResult[];
}
// Helper function to get auth token from cookie
const getAuthToken = (): string => {
const tokenCookie = useCookie('token');
@ -108,113 +125,14 @@ const authRequest = async <T>(
}
};
// Mock courses data
const MOCK_COURSES: CourseResponse[] = [
{
id: 1,
category_id: 1,
title: { en: 'JavaScript Fundamentals', th: 'พื้นฐาน JavaScript' },
slug: 'javascript-fundamentals',
description: {
en: 'Learn JavaScript fundamentals from scratch',
th: 'เรียนรู้พื้นฐาน JavaScript ตั้งแต่เริ่มต้น'
},
thumbnail_url: null,
price: '0',
is_free: true,
have_certificate: true,
status: 'APPROVED',
approved_by: 1,
approved_at: '2024-01-15T00:00:00Z',
rejection_reason: null,
created_at: '2024-01-15T00:00:00Z',
created_by: 2,
updated_at: '2024-01-15T00:00:00Z',
updated_by: null
},
{
id: 2,
category_id: 2,
title: { en: 'React for Beginners', th: 'React สำหรับผู้เริ่มต้น' },
slug: 'react-for-beginners',
description: {
en: 'Build modern web apps with React',
th: 'สร้างเว็บแอปพลิเคชันด้วย React'
},
thumbnail_url: null,
price: '990',
is_free: false,
have_certificate: true,
status: 'PENDING',
approved_by: null,
approved_at: null,
rejection_reason: null,
created_at: '2024-02-01T00:00:00Z',
created_by: 2,
updated_at: '2024-02-01T00:00:00Z',
updated_by: null
},
{
id: 3,
category_id: 1,
title: { en: 'TypeScript Masterclass', th: 'TypeScript ขั้นสูง' },
slug: 'typescript-masterclass',
description: {
en: 'Master TypeScript for better JavaScript development',
th: 'เรียนรู้ TypeScript เพื่อพัฒนา JavaScript ได้ดียิ่งขึ้น'
},
thumbnail_url: null,
price: '1290',
is_free: false,
have_certificate: true,
status: 'DRAFT',
approved_by: null,
approved_at: null,
rejection_reason: null,
created_at: '2024-02-15T00:00:00Z',
created_by: 2,
updated_at: '2024-02-15T00:00:00Z',
updated_by: null
}
];
export const instructorService = {
async getCourses(): Promise<CourseResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_COURSES;
}
const response = await authRequest<CoursesListResponse>('/api/instructors/courses');
return response.data;
},
async createCourse(data: CreateCourseRequest): Promise<ApiResponse<CourseResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 201,
message: 'Course created successfully (Mock)',
data: {
...MOCK_COURSES[0],
id: Date.now(),
...data,
price: String(data.price), // Convert number to string to match CourseResponse type
status: 'DRAFT',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
} as CourseResponse
};
}
// Clean data - remove empty thumbnail_url
const cleanedData = { ...data };
if (!cleanedData.thumbnail_url) {
delete cleanedData.thumbnail_url;
@ -230,36 +148,11 @@ export const instructorService = {
},
async getCourseById(courseId: number): Promise<CourseDetailResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_COURSE_DETAIL;
}
const response = await authRequest<{ code: number; data: CourseDetailResponse }>(`/api/instructors/courses/${courseId}`);
return response.data;
},
async updateCourse(courseId: number, data: CreateCourseRequest): Promise<ApiResponse<CourseResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Course updated successfully (Mock)',
data: {
...MOCK_COURSES[0],
id: courseId,
...data,
price: String(data.price)
} as CourseResponse
};
}
return await authRequest<ApiResponse<CourseResponse>>(`/api/instructors/courses/${courseId}`, {
method: 'PUT',
body: { data }
@ -267,18 +160,6 @@ export const instructorService = {
},
async uploadCourseThumbnail(courseId: number, file: File): Promise<ApiResponse<{ thumbnail_url: string }>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Thumbnail uploaded successfully (Mock)',
data: { thumbnail_url: URL.createObjectURL(file) }
};
}
const formData = new FormData();
formData.append('file', file);
@ -289,70 +170,20 @@ export const instructorService = {
},
async getCourseInstructors(courseId: number): Promise<CourseInstructorResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return [
{
id: 1,
course_id: courseId,
user_id: 1,
is_primary: true,
joined_at: new Date().toISOString(),
user: {
id: 1,
username: 'instructor',
email: 'instructor@elearning.local',
role_id: 2,
email_verified_at: new Date().toISOString(),
is_deactivated: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
}
];
}
const response = await authRequest<InstructorsListResponse>(
`/api/instructors/courses/listinstructor/${courseId}`
);
return response.data;
},
async addInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Instructor added successfully (Mock)',
data: undefined
};
}
async addInstructor(courseId: number, emailOrUsername: string): Promise<ApiResponse<void>> {
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/add-instructor/${courseId}/${userId}`,
`/api/instructors/courses/add-instructor/${courseId}/${encodeURIComponent(emailOrUsername)}`,
{ method: 'POST' }
);
},
async removeInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Instructor removed successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/remove-instructor/${courseId}/${userId}`,
{ method: 'DELETE' }
@ -360,65 +191,28 @@ export const instructorService = {
},
async setPrimaryInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Primary instructor updated successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/set-primary-instructor/${courseId}/${userId}`,
{ method: 'PUT' }
);
},
async searchInstructors(query: string): Promise<SearchInstructorResult[]> {
const response = await authRequest<SearchInstructorsResponse>(
`/api/instructors/courses/search-instructors?query=${encodeURIComponent(query)}`
);
return response.data;
},
async deleteCourse(courseId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Course deleted successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(`/api/instructors/courses/${courseId}`, { method: 'DELETE' });
},
async sendForReview(courseId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Sent for review successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(`/api/instructors/courses/send-review/${courseId}`, { method: 'POST' });
},
async getChapters(courseId: number): Promise<ChapterResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_COURSE_DETAIL.chapters;
}
// Get chapters from course detail endpoint
const response = await authRequest<{ code: number; data: { chapters: ChapterResponse[] } }>(
`/api/instructors/courses/${courseId}`
@ -427,23 +221,6 @@ export const instructorService = {
},
async createChapter(courseId: number, data: CreateChapterRequest): Promise<ApiResponse<ChapterResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 201,
message: 'Chapter created successfully (Mock)',
data: {
...MOCK_COURSE_DETAIL.chapters[0],
id: Date.now(),
...data,
lessons: []
}
};
}
return await authRequest<ApiResponse<ChapterResponse>>(
`/api/instructors/courses/${courseId}/chapters`,
{ method: 'POST', body: data }
@ -451,22 +228,6 @@ export const instructorService = {
},
async updateChapter(courseId: number, chapterId: number, data: CreateChapterRequest): Promise<ApiResponse<ChapterResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Chapter updated successfully (Mock)',
data: {
...MOCK_COURSE_DETAIL.chapters[0],
id: chapterId,
...data
}
};
}
return await authRequest<ApiResponse<ChapterResponse>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
{ method: 'PUT', body: data }
@ -474,18 +235,6 @@ export const instructorService = {
},
async deleteChapter(courseId: number, chapterId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Chapter deleted successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
{ method: 'DELETE' }
@ -493,18 +242,6 @@ export const instructorService = {
},
async reorderChapter(courseId: number, chapterId: number, sortOrder: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 300));
return {
code: 200,
message: 'Chapter reordered successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder`,
{ method: 'PUT', body: { sort_order: sortOrder } }
@ -513,14 +250,6 @@ export const instructorService = {
// Lesson CRUD
async getLesson(courseId: number, chapterId: number, lessonId: number): Promise<LessonDetailResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonDetailResponse;
}
const response = await authRequest<{ code: number; data: LessonDetailResponse }>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`
);
@ -528,22 +257,6 @@ export const instructorService = {
},
async createLesson(courseId: number, chapterId: number, data: CreateLessonRequest): Promise<ApiResponse<LessonResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 201,
message: 'Lesson created successfully (Mock)',
data: {
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
id: Date.now(),
...data
}
};
}
return await authRequest<ApiResponse<LessonResponse>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons`,
{ method: 'POST', body: data }
@ -551,22 +264,6 @@ export const instructorService = {
},
async updateLesson(courseId: number, chapterId: number, lessonId: number, data: UpdateLessonRequest): Promise<ApiResponse<LessonResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Lesson updated successfully (Mock)',
data: {
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
id: lessonId,
...data
}
};
}
return await authRequest<ApiResponse<LessonResponse>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`,
{ method: 'PUT', body: data }
@ -574,18 +271,6 @@ export const instructorService = {
},
async deleteLesson(courseId: number, chapterId: number, lessonId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Lesson deleted successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`,
{ method: 'DELETE' }
@ -593,18 +278,6 @@ export const instructorService = {
},
async reorderLesson(courseId: number, chapterId: number, lessonId: number, sortOrder: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 300));
return {
code: 200,
message: 'Lesson reordered successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder-lessons`,
{ method: 'PUT', body: { lesson_id: lessonId, sort_order: sortOrder } }
@ -613,35 +286,6 @@ export const instructorService = {
// Question CRUD
async createQuestion(courseId: number, chapterId: number, lessonId: number, data: CreateQuestionRequest): Promise<ApiResponse<QuizQuestionResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 201,
message: 'Question created successfully (Mock)',
data: {
id: Date.now(),
quiz_id: 1,
question: data.question,
explanation: data.explanation || null,
question_type: data.question_type,
score: data.score || 1,
sort_order: data.sort_order || 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
choices: data.choices.map((c, i) => ({
id: Date.now() + i,
question_id: Date.now(),
text: c.text,
is_correct: c.is_correct,
sort_order: c.sort_order || i + 1
}))
}
};
}
return await authRequest<ApiResponse<QuizQuestionResponse>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions`,
{ method: 'POST', body: data }
@ -649,35 +293,6 @@ export const instructorService = {
},
async updateQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, data: CreateQuestionRequest): Promise<ApiResponse<QuizQuestionResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Question updated successfully (Mock)',
data: {
id: questionId,
quiz_id: 1,
question: data.question,
explanation: data.explanation || null,
question_type: data.question_type,
score: data.score || 1,
sort_order: data.sort_order || 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
choices: data.choices.map((c, i) => ({
id: Date.now() + i,
question_id: questionId,
text: c.text,
is_correct: c.is_correct,
sort_order: c.sort_order || i + 1
}))
}
};
}
return await authRequest<ApiResponse<QuizQuestionResponse>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
{ method: 'PUT', body: data }
@ -685,18 +300,6 @@ export const instructorService = {
},
async deleteQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Question deleted successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
{ method: 'DELETE' }
@ -704,18 +307,6 @@ export const instructorService = {
},
async reorderQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, sortOrder: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 300));
return {
code: 200,
message: 'Question reordered successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}/reorder`,
{ method: 'PUT', body: { sort_order: sortOrder } }
@ -724,28 +315,6 @@ export const instructorService = {
// Quiz Settings
async updateQuizSettings(courseId: number, chapterId: number, lessonId: number, data: UpdateQuizSettingsRequest): Promise<ApiResponse<QuizResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Quiz settings updated successfully (Mock)',
data: {
id: 1,
lesson_id: lessonId,
title: data.title,
description: data.description,
passing_score: data.passing_score,
time_limit: data.time_limit,
shuffle_questions: data.shuffle_questions,
shuffle_choices: data.shuffle_choices,
show_answers_after_completion: data.show_answers_after_completion
}
};
}
return await authRequest<ApiResponse<QuizResponse>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/quiz`,
{ method: 'PUT', body: data }
@ -754,18 +323,6 @@ export const instructorService = {
// Video Upload
async uploadLessonVideo(courseId: number, chapterId: number, lessonId: number, video: File, attachments?: File[]): Promise<ApiResponse<LessonResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
code: 200,
message: 'Video uploaded successfully (Mock)',
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
};
}
const formData = new FormData();
formData.append('video', video);
if (attachments) {
@ -781,18 +338,6 @@ export const instructorService = {
},
async updateLessonVideo(courseId: number, chapterId: number, lessonId: number, video?: File, attachments?: File[]): Promise<ApiResponse<LessonResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
code: 200,
message: 'Video updated successfully (Mock)',
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
};
}
const formData = new FormData();
if (video) {
formData.append('video', video);
@ -811,18 +356,6 @@ export const instructorService = {
// Attachments
async addAttachments(courseId: number, chapterId: number, lessonId: number, files: File[]): Promise<ApiResponse<LessonResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Attachments added successfully (Mock)',
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
};
}
const formData = new FormData();
files.forEach(file => {
formData.append('attachment', file);
@ -835,18 +368,6 @@ export const instructorService = {
},
async deleteAttachment(courseId: number, chapterId: number, lessonId: number, attachmentId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Attachment deleted successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments/${attachmentId}`,
{ method: 'DELETE' }
@ -855,14 +376,6 @@ export const instructorService = {
// Announcements
async getAnnouncements(courseId: number): Promise<AnnouncementResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_ANNOUNCEMENTS;
}
const response = await authRequest<AnnouncementsListResponse>(
`/api/instructors/courses/${courseId}/announcements`
);
@ -870,27 +383,6 @@ export const instructorService = {
},
async createAnnouncement(courseId: number, data: CreateAnnouncementRequest, files?: File[]): Promise<ApiResponse<AnnouncementResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 201,
message: 'Announcement created successfully (Mock)',
data: {
id: Date.now(),
title: data.title,
content: data.content,
status: data.status || 'DRAFT',
is_pinned: data.is_pinned || false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
attachments: []
}
};
}
const formData = new FormData();
formData.append('data', JSON.stringify(data));
@ -907,27 +399,6 @@ export const instructorService = {
},
async updateAnnouncement(courseId: number, announcementId: number, data: CreateAnnouncementRequest): Promise<ApiResponse<AnnouncementResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Announcement updated successfully (Mock)',
data: {
id: announcementId,
title: data.title,
content: data.content,
status: data.status || 'DRAFT',
is_pinned: data.is_pinned || false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
attachments: []
}
};
}
return await authRequest<ApiResponse<AnnouncementResponse>>(
`/api/instructors/courses/${courseId}/announcements/${announcementId}`,
{ method: 'PUT', body: data }
@ -935,18 +406,6 @@ export const instructorService = {
},
async deleteAnnouncement(courseId: number, announcementId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Announcement deleted successfully (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/announcements/${announcementId}`,
{ method: 'DELETE' }
@ -954,18 +413,6 @@ export const instructorService = {
},
async uploadAnnouncementAttachment(courseId: number, announcementId: number, file: File): Promise<ApiResponse<AnnouncementResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Upload attachment success (Mock)',
data: MOCK_ANNOUNCEMENTS[0]
};
}
const formData = new FormData();
formData.append('file', file);
@ -976,18 +423,6 @@ export const instructorService = {
},
async deleteAnnouncementAttachment(courseId: number, announcementId: number, attachmentId: number): Promise<ApiResponse<void>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
code: 200,
message: 'Delete attachment success (Mock)',
data: undefined
};
}
return await authRequest<ApiResponse<void>>(
`/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments/${attachmentId}`,
{ method: 'DELETE' }
@ -1192,146 +627,3 @@ export interface CreateAnnouncementRequest {
status?: 'DRAFT' | 'PUBLISHED';
is_pinned?: boolean;
}
// Mock announcements
const MOCK_ANNOUNCEMENTS: AnnouncementResponse[] = [
{
id: 1,
title: { th: 'ยินดีต้อนรับสู่คอร์ส', en: 'Welcome to the Course' },
content: {
th: 'ยินดีต้อนรับทุกคนสู่คอร์ส JavaScript Fundamentals! เราจะเริ่มเรียนในสัปดาห์หน้า',
en: 'Welcome everyone to JavaScript Fundamentals! We will start next week'
},
status: 'PUBLISHED',
is_pinned: true,
created_at: '2024-01-15T10:00:00Z',
updated_at: '2024-01-15T10:00:00Z',
attachments: []
},
{
id: 2,
title: { th: 'อัพเดทตารางเรียน', en: 'Schedule Update' },
content: {
th: 'มีการเปลี่ยนแปลงตารางเรียนสำหรับบทที่ 3',
en: 'There is a schedule change for Chapter 3'
},
status: 'PUBLISHED',
is_pinned: false,
created_at: '2024-01-20T14:00:00Z',
updated_at: '2024-01-20T14:00:00Z',
attachments: []
}
];
// Mock course detail
const MOCK_COURSE_DETAIL: CourseDetailResponse = {
...MOCK_COURSES[0],
chapters: [
{
id: 1,
course_id: 1,
title: { en: 'Chapter 1: Getting Started', th: 'บทที่ 1: เริ่มต้น' },
description: { en: 'Introduction to JavaScript', th: 'แนะนำ JavaScript' },
sort_order: 1,
is_published: true,
created_at: '2024-01-15T00:00:00Z',
updated_at: '2024-01-15T00:00:00Z',
lessons: [
{
id: 1,
chapter_id: 1,
title: { en: 'What is JavaScript', th: 'JavaScript คืออะไร' },
content: { en: 'Introduction', th: 'แนะนำ' },
type: 'VIDEO',
duration_minutes: 15,
sort_order: 1,
is_sequential: true,
prerequisite_lesson_ids: null,
require_pass_quiz: false,
is_published: true,
created_at: '2024-01-15T00:00:00Z',
updated_at: '2024-01-15T00:00:00Z',
video_url: null,
attachments: [],
quiz: null
},
{
id: 2,
chapter_id: 1,
title: { en: 'Variables', th: 'ตัวแปร' },
content: { en: 'Learn variables', th: 'เรียนรู้ตัวแปร' },
type: 'VIDEO',
duration_minutes: 20,
sort_order: 2,
is_sequential: true,
prerequisite_lesson_ids: null,
require_pass_quiz: false,
is_published: true,
created_at: '2024-01-15T00:00:00Z',
updated_at: '2024-01-15T00:00:00Z',
video_url: null,
attachments: [],
quiz: null
},
{
id: 3,
chapter_id: 1,
title: { en: 'Chapter 1 Quiz', th: 'แบบทดสอบบทที่ 1' },
content: null,
type: 'QUIZ',
duration_minutes: 10,
sort_order: 3,
is_sequential: true,
prerequisite_lesson_ids: null,
require_pass_quiz: true,
is_published: true,
created_at: '2024-01-15T00:00:00Z',
updated_at: '2024-01-15T00:00:00Z',
video_url: null,
attachments: [],
quiz: {
id: 1,
lesson_id: 3,
title: { en: 'Chapter 1 Quiz', th: 'แบบทดสอบบทที่ 1' },
description: { en: 'Test your knowledge', th: 'ทดสอบความรู้' },
passing_score: 70,
time_limit: 10,
shuffle_questions: true,
shuffle_choices: true,
show_answers_after_completion: true
}
}
]
},
{
id: 2,
course_id: 1,
title: { en: 'Chapter 2: Functions', th: 'บทที่ 2: ฟังก์ชัน' },
description: { en: 'Learn about functions', th: 'เรียนรู้เกี่ยวกับฟังก์ชัน' },
sort_order: 2,
is_published: true,
created_at: '2024-01-15T00:00:00Z',
updated_at: '2024-01-15T00:00:00Z',
lessons: [
{
id: 4,
chapter_id: 2,
title: { en: 'Creating Functions', th: 'การสร้างฟังก์ชัน' },
content: { en: 'How to create functions', th: 'วิธีสร้างฟังก์ชัน' },
type: 'VIDEO',
duration_minutes: 25,
sort_order: 1,
is_sequential: true,
prerequisite_lesson_ids: null,
require_pass_quiz: false,
is_published: true,
created_at: '2024-01-15T00:00:00Z',
updated_at: '2024-01-15T00:00:00Z',
video_url: null,
attachments: [],
quiz: null
}
]
}
]
};

View file

@ -3,6 +3,7 @@ export interface UserProfileResponse {
id: number;
username: string;
email: string;
email_verified_at: string | null;
updated_at: string;
created_at: string;
role: {
@ -44,48 +45,6 @@ export interface UpdateProfileRequest {
birth_date?: string | null;
}
// Mock profile data for development
const MOCK_PROFILES: Record<string, UserProfileResponse> = {
'admin@elearning.local': {
id: 1,
username: 'admin',
email: 'admin@elearning.local',
updated_at: '2024-01-01T00:00:00Z',
created_at: '2024-01-01T00:00:00Z',
role: {
code: 'ADMIN',
name: { en: 'Administrator', th: 'ผู้ดูแลระบบ' }
},
profile: {
prefix: { en: 'Mr.', th: 'นาย' },
first_name: 'ผู้ดูแล',
last_name: 'ระบบ',
avatar_url: null,
birth_date: null,
phone: '081-234-5678'
}
},
'instructor@elearning.local': {
id: 2,
username: 'instructor',
email: 'instructor@elearning.local',
updated_at: '2024-01-01T00:00:00Z',
created_at: '2024-01-01T00:00:00Z',
role: {
code: 'INSTRUCTOR',
name: { en: 'Instructor', th: 'ผู้สอน' }
},
profile: {
prefix: { en: 'Mr.', th: 'นาย' },
first_name: 'อาจารย์',
last_name: 'ทดสอบ',
avatar_url: null,
birth_date: null,
phone: '081-234-5678'
}
}
};
// Helper function to get auth token from cookie
const getAuthToken = (): string => {
const tokenCookie = useCookie('token');
@ -94,37 +53,6 @@ const getAuthToken = (): string => {
export const userService = {
async getProfile(): Promise<UserProfileResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
return this.mockGetProfile();
}
return this.apiGetProfile();
},
// Mock get profile
async mockGetProfile(): Promise<UserProfileResponse> {
await new Promise(resolve => setTimeout(resolve, 300));
const userCookie = useCookie('user');
if (!userCookie.value) throw new Error('User not found');
const user = typeof userCookie.value === 'string'
? JSON.parse(userCookie.value)
: userCookie.value;
const mockProfile = MOCK_PROFILES[user.email];
if (!mockProfile) {
throw new Error('Profile not found');
}
return mockProfile;
},
// Real API get profile
async apiGetProfile(): Promise<UserProfileResponse> {
const config = useRuntimeConfig();
const token = getAuthToken();
@ -140,20 +68,8 @@ export const userService = {
async updateProfile(data: UpdateProfileRequest): Promise<ApiResponse<UserProfileResponse>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
// In mock mode, just return the current profile with updates
const profile = await this.mockGetProfile();
return {
code: 200,
message: 'Update profile successfully',
data: { ...profile, profile: { ...profile.profile, ...data } }
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<UserProfileResponse>>('/api/user/me', {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
@ -168,19 +84,8 @@ export const userService = {
async changePassword(oldPassword: string, newPassword: string): Promise<ApiResponse<null>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
// In mock mode, just simulate success
return {
code: 200,
message: 'Change password successfully',
data: null
};
}
const token = getAuthToken();
const response = await $fetch<ApiResponse<null>>('/api/user/change-password', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
@ -198,18 +103,6 @@ export const userService = {
async uploadAvatar(file: File): Promise<ApiResponse<{ avatar_url: string; id: number }>> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
// Return mock URL
return {
code: 200,
message: 'Upload avatar successfully',
data: { avatar_url: URL.createObjectURL(file), id: 1 }
};
}
const token = getAuthToken();
const formData = new FormData();
formData.append('file', file);