2026-02-05 09:31:24 +07:00
|
|
|
<template>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="flex justify-between items-center mb-6">
|
|
|
|
|
<h2 class="text-xl font-semibold text-gray-900">ผู้สอนในรายวิชา</h2>
|
|
|
|
|
<q-btn v-if="isPrimaryInstructor" color="primary" icon="person_add" label="เพิ่มผู้สอน" @click="showAddDialog = true" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="loading" class="flex justify-center py-10">
|
|
|
|
|
<q-spinner color="primary" size="40px" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-else-if="instructors.length === 0" class="text-center py-10 text-gray-500">
|
|
|
|
|
<q-icon name="group_off" size="60px" color="grey-4" class="mb-4" />
|
|
|
|
|
<p>ยังไม่มีข้อมูลผู้สอน</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
|
|
<q-card v-for="instructor in instructors" :key="instructor.id" flat bordered class="rounded-lg">
|
|
|
|
|
<q-item>
|
|
|
|
|
<q-item-section avatar>
|
|
|
|
|
<q-avatar size="50px" color="primary" text-color="white">
|
|
|
|
|
<img v-if="instructor.user.avatar_url" :src="instructor.user.avatar_url">
|
|
|
|
|
<span v-else>{{ instructor.user.username.charAt(0).toUpperCase() }}</span>
|
|
|
|
|
</q-avatar>
|
|
|
|
|
</q-item-section>
|
|
|
|
|
|
|
|
|
|
<q-item-section>
|
|
|
|
|
<q-item-label class="text-base font-medium flex items-center gap-2">
|
|
|
|
|
{{ instructor.user.username }}
|
|
|
|
|
<q-badge v-if="instructor.is_primary" color="primary" label="หัวหน้าผู้สอน" />
|
|
|
|
|
</q-item-label>
|
|
|
|
|
<q-item-label caption>{{ instructor.user.email }}</q-item-label>
|
|
|
|
|
</q-item-section>
|
|
|
|
|
|
|
|
|
|
<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="setPrimary(instructor.user_id)">
|
|
|
|
|
<q-item-section avatar>
|
|
|
|
|
<q-icon name="verified_user" color="primary" />
|
|
|
|
|
</q-item-section>
|
|
|
|
|
<q-item-section>ตั้งเป็นหัวหน้าผู้สอน</q-item-section>
|
|
|
|
|
</q-item>
|
|
|
|
|
<q-item clickable v-close-popup @click="removeInstructor(instructor.user_id)" class="text-red">
|
|
|
|
|
<q-item-section avatar>
|
|
|
|
|
<q-icon name="person_remove" color="red" />
|
|
|
|
|
</q-item-section>
|
|
|
|
|
<q-item-section>ลบผู้สอน</q-item-section>
|
|
|
|
|
</q-item>
|
|
|
|
|
</q-menu>
|
|
|
|
|
</q-btn>
|
|
|
|
|
</q-item-section>
|
|
|
|
|
</q-item>
|
|
|
|
|
</q-card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Add Instructor Dialog -->
|
|
|
|
|
<q-dialog v-model="showAddDialog">
|
|
|
|
|
<q-card style="min-width: 400px">
|
|
|
|
|
<q-card-section>
|
|
|
|
|
<div class="text-h6">เพิ่มผู้สอน</div>
|
|
|
|
|
</q-card-section>
|
|
|
|
|
|
|
|
|
|
<q-card-section>
|
|
|
|
|
<q-select
|
|
|
|
|
v-model="selectedUser"
|
|
|
|
|
:options="searchResults"
|
|
|
|
|
:option-label="(item) => item ? `${item.username} (${item.email})` : ''"
|
|
|
|
|
label="ค้นหาผู้ใช้"
|
|
|
|
|
use-input
|
|
|
|
|
filled
|
|
|
|
|
@filter="filterUsers"
|
|
|
|
|
:loading="loadingSearch"
|
|
|
|
|
>
|
|
|
|
|
<template v-slot:option="scope">
|
|
|
|
|
<q-item v-bind="scope.itemProps">
|
|
|
|
|
<q-item-section avatar>
|
|
|
|
|
<q-avatar>
|
|
|
|
|
<img v-if="scope.opt.profile?.avatar_url" :src="scope.opt.profile.avatar_url">
|
|
|
|
|
<span v-else>{{ scope.opt.username.charAt(0).toUpperCase() }}</span>
|
|
|
|
|
</q-avatar>
|
|
|
|
|
</q-item-section>
|
|
|
|
|
<q-item-section>
|
|
|
|
|
<q-item-label>{{ scope.opt.username }}</q-item-label>
|
|
|
|
|
<q-item-label caption>{{ scope.opt.email }}</q-item-label>
|
|
|
|
|
</q-item-section>
|
|
|
|
|
</q-item>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-slot:no-option>
|
|
|
|
|
<q-item>
|
|
|
|
|
<q-item-section class="text-grey">
|
|
|
|
|
ไม่พบผู้ใช้
|
|
|
|
|
</q-item-section>
|
|
|
|
|
</q-item>
|
|
|
|
|
</template>
|
|
|
|
|
</q-select>
|
|
|
|
|
</q-card-section>
|
|
|
|
|
|
|
|
|
|
<q-card-actions align="right">
|
|
|
|
|
<q-btn flat label="ยกเลิก" color="primary" v-close-popup />
|
|
|
|
|
<q-btn flat label="เพิ่ม" color="primary" @click="addInstructor" :disable="!selectedUser" :loading="addingInstructor" />
|
|
|
|
|
</q-card-actions>
|
|
|
|
|
</q-card>
|
|
|
|
|
</q-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { useQuasar } from 'quasar';
|
|
|
|
|
import {
|
|
|
|
|
instructorService,
|
|
|
|
|
type CourseInstructorResponse,
|
|
|
|
|
type SearchInstructorResult
|
|
|
|
|
} from '~/services/instructor.service';
|
|
|
|
|
import { useAuthStore } from '~/stores/auth';
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
courseId: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
|
const $q = useQuasar();
|
|
|
|
|
const authStore = useAuthStore();
|
|
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
const instructors = ref<CourseInstructorResponse[]>([]);
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const showAddDialog = ref(false);
|
|
|
|
|
const selectedUser = ref<SearchInstructorResult | null>(null);
|
|
|
|
|
const searchResults = ref<SearchInstructorResult[]>([]);
|
|
|
|
|
const loadingSearch = ref(false);
|
|
|
|
|
const addingInstructor = ref(false);
|
|
|
|
|
|
|
|
|
|
// Computed
|
|
|
|
|
const isPrimaryInstructor = computed(() => {
|
|
|
|
|
if (!authStore.user?.id) return false;
|
|
|
|
|
const currentUserId = parseInt(authStore.user.id);
|
|
|
|
|
const myRecord = instructors.value.find(i => i.user_id === currentUserId);
|
|
|
|
|
return myRecord?.is_primary === true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Methods
|
|
|
|
|
const fetchInstructors = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
instructors.value = await instructorService.getCourseInstructors(props.courseId);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to fetch instructors:', error);
|
|
|
|
|
$q.notify({ type: 'negative', message: 'ไม่สามารถโหลดข้อมูลผู้สอนได้', position: 'top' });
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const filterUsers = async (val: string, update: (fn: () => void) => void, abort: () => void) => {
|
|
|
|
|
if (val.length < 2) {
|
|
|
|
|
abort();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
loadingSearch.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const results = await instructorService.searchInstructors(val, props.courseId);
|
|
|
|
|
update(() => {
|
|
|
|
|
searchResults.value = results;
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Search failed:', error);
|
|
|
|
|
abort();
|
|
|
|
|
} finally {
|
|
|
|
|
loadingSearch.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addInstructor = async () => {
|
|
|
|
|
if (!selectedUser.value) return;
|
|
|
|
|
addingInstructor.value = true;
|
|
|
|
|
try {
|
2026-02-05 16:39:58 +07:00
|
|
|
await instructorService.addInstructor(props.courseId, selectedUser.value.email);
|
2026-02-05 09:31:24 +07:00
|
|
|
$q.notify({ type: 'positive', message: 'เพิ่มผู้สอนสำเร็จ', position: 'top' });
|
|
|
|
|
showAddDialog.value = false;
|
|
|
|
|
selectedUser.value = null;
|
|
|
|
|
fetchInstructors();
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'negative',
|
|
|
|
|
message: error.data?.message || 'ไม่สามารถเพิ่มผู้สอนได้',
|
|
|
|
|
position: 'top'
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
addingInstructor.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const setPrimary = async (userId: number) => {
|
|
|
|
|
try {
|
|
|
|
|
await instructorService.setPrimaryInstructor(props.courseId, userId);
|
|
|
|
|
$q.notify({ type: 'positive', message: 'ตั้งเป็นหัวหน้าผู้สอนสำเร็จ', position: 'top' });
|
|
|
|
|
fetchInstructors();
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'negative',
|
|
|
|
|
message: error.data?.message || 'ไม่สามารถตั้งเป็นหัวหน้าผู้สอนได้',
|
|
|
|
|
position: 'top'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeInstructor = async (userId: number) => {
|
|
|
|
|
$q.dialog({
|
|
|
|
|
title: 'ยืนยันการลบ',
|
|
|
|
|
message: 'คุณต้องการลบผู้สอนคนนี้หรือไม่?',
|
|
|
|
|
cancel: true,
|
|
|
|
|
persistent: true
|
|
|
|
|
}).onOk(async () => {
|
|
|
|
|
try {
|
2026-02-05 16:39:58 +07:00
|
|
|
await instructorService.removeInstructor(props.courseId, userId);
|
2026-02-05 09:31:24 +07:00
|
|
|
$q.notify({ type: 'positive', message: 'ลบผู้สอนสำเร็จ', position: 'top' });
|
|
|
|
|
fetchInstructors();
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'negative',
|
|
|
|
|
message: error.data?.message || 'ไม่สามารถลบผู้สอนได้',
|
|
|
|
|
position: 'top'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Fetch on mount
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
fetchInstructors();
|
|
|
|
|
});
|
|
|
|
|
</script>
|