feat: Implement initial frontend for instructor and admin course management functionalities.
All checks were successful
Build and Deploy Frontend Management to Dev Server / Build Frontend Management Docker Image (push) Successful in 4m21s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 7s
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 4m21s
Build and Deploy Frontend Management to Dev Server / Deploy E-learning Frontend Management to Dev Server (push) Successful in 7s
Build and Deploy Frontend Management to Dev Server / Notify Deployment Status (push) Successful in 1s
This commit is contained in:
parent
c8ef372d4e
commit
03f16cf2fd
15 changed files with 197 additions and 36 deletions
|
|
@ -1,5 +1,3 @@
|
||||||
# API Configuration
|
# API Configuration
|
||||||
API_BASE_URL=http://localhost:3001/api
|
NUXT_PUBLIC_API_BASE_URL=http://localhost:3001/api
|
||||||
|
|
||||||
# Application
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
|
||||||
9
frontend_management/.gitignore
vendored
9
frontend_management/.gitignore
vendored
|
|
@ -24,3 +24,12 @@ logs
|
||||||
!.env.example
|
!.env.example
|
||||||
deploy.ps1
|
deploy.ps1
|
||||||
*.tar
|
*.tar
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
tests
|
||||||
|
tests/.auth/
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
playwright.config.ts
|
||||||
|
|
@ -342,11 +342,18 @@ const save = async () => {
|
||||||
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
|
// Convert local datetime to ISO string to preserve timezone
|
||||||
|
const payload = { ...form.value };
|
||||||
|
if (payload.published_at) {
|
||||||
|
const localDate = new Date(payload.published_at.replace(' ', 'T'));
|
||||||
|
payload.published_at = localDate.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
if (editing.value) {
|
if (editing.value) {
|
||||||
await instructorService.updateAnnouncement(props.courseId, editing.value.id, form.value);
|
await instructorService.updateAnnouncement(props.courseId, editing.value.id, payload);
|
||||||
$q.notify({ type: 'positive', message: 'บันทึกประกาศสำเร็จ', position: 'top' });
|
$q.notify({ type: 'positive', message: 'บันทึกประกาศสำเร็จ', position: 'top' });
|
||||||
} else {
|
} else {
|
||||||
const created = await instructorService.createAnnouncement(props.courseId, form.value);
|
const created = await instructorService.createAnnouncement(props.courseId, payload);
|
||||||
|
|
||||||
// Upload pending files
|
// Upload pending files
|
||||||
for (const file of pendingFiles.value) {
|
for (const file of pendingFiles.value) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
<q-item-label caption>{{ instructor.user.email }}</q-item-label>
|
<q-item-label caption>{{ instructor.user.email }}</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section side v-if="isPrimaryInstructor">
|
<q-item-section side v-if="isPrimaryInstructor && instructor.user_id !== currentUserId">
|
||||||
<q-btn flat dense round icon="more_vert">
|
<q-btn flat dense round icon="more_vert">
|
||||||
<q-menu>
|
<q-menu>
|
||||||
<q-item v-if="!instructor.is_primary" clickable v-close-popup @click="setPrimary(instructor.user_id)">
|
<q-item v-if="!instructor.is_primary" clickable v-close-popup @click="setPrimary(instructor.user_id)">
|
||||||
|
|
@ -139,6 +139,10 @@ const isPrimaryInstructor = computed(() => {
|
||||||
return myRecord?.is_primary === true;
|
return myRecord?.is_primary === true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentUserId = computed(() => {
|
||||||
|
return authStore.user?.id ? parseInt(authStore.user.id) : null;
|
||||||
|
});
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const fetchInstructors = async () => {
|
const fetchInstructors = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
|
||||||
64
frontend_management/package-lock.json
generated
64
frontend_management/package-lock.json
generated
|
|
@ -18,6 +18,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"nuxt-quasar-ui": "^3.0.0"
|
"nuxt-quasar-ui": "^3.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2773,6 +2774,22 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
|
|
@ -8783,6 +8800,53 @@
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/portfinder": {
|
"node_modules/portfinder": {
|
||||||
"version": "1.0.38",
|
"version": "1.0.38",
|
||||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
|
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"test": "playwright test",
|
||||||
|
"test:ui": "playwright test --ui",
|
||||||
|
"test:headed": "playwright test --headed",
|
||||||
|
"test:report": "playwright show-report"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
|
|
@ -21,6 +25,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"nuxt-quasar-ui": "^3.0.0"
|
"nuxt-quasar-ui": "^3.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { adminService, type PendingCourse, type AuditLog } from '~/services/admin.service';
|
import { adminService, type PendingCourse, type AuditLog } from '~/services/admin.service';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin',
|
layout: 'admin',
|
||||||
|
|
@ -190,6 +191,7 @@ definePageMeta({
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
@ -207,8 +209,23 @@ const goToProfile = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authStore.logout();
|
$q.dialog({
|
||||||
router.push('/login');
|
title: 'ยืนยันการออกจากระบบ',
|
||||||
|
message: 'คุณต้องการออกจากระบบใช่หรือไม่?',
|
||||||
|
cancel: {
|
||||||
|
label: 'ยกเลิก',
|
||||||
|
flat: true,
|
||||||
|
color: 'grey-8'
|
||||||
|
},
|
||||||
|
ok: {
|
||||||
|
label: 'ออกจากระบบ',
|
||||||
|
color: 'negative'
|
||||||
|
},
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
authStore.logout();
|
||||||
|
router.push('/login');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Data Fetching
|
// Data Fetching
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<!-- <div class="grid grid-cols-3 gap-2">
|
||||||
<div class="bg-blue-50 p-2 rounded-lg text-center">
|
<div class="bg-blue-50 p-2 rounded-lg text-center">
|
||||||
<div class="text-2xl font-bold text-blue-800">{{ selectedCourse.chapters_count || 0 }}</div>
|
<div class="text-2xl font-bold text-blue-800">{{ selectedCourse.chapters_count || 0 }}</div>
|
||||||
<div class="text-blue-600 text-sm">Chapters</div>
|
<div class="text-blue-600 text-sm">Chapters</div>
|
||||||
|
|
@ -180,7 +180,7 @@
|
||||||
<div class="text-2xl font-bold text-purple-800">{{ selectedCourse.lessons_count || 0 }}</div>
|
<div class="text-2xl font-bold text-purple-800">{{ selectedCourse.lessons_count || 0 }}</div>
|
||||||
<div class="text-purple-600 text-sm">Lessons</div>
|
<div class="text-purple-600 text-sm">Lessons</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
>
|
>
|
||||||
<q-icon name="article" color="primary" size="20px" />
|
<q-icon name="article" color="primary" size="20px" />
|
||||||
<span class="text-gray-700">{{ lessonIndex + 1 }}. {{ lesson.title.th }}</span>
|
<span class="text-gray-700">{{ lessonIndex + 1 }}. {{ lesson.title.th }}</span>
|
||||||
<span v-if="lesson.title.en" class="text-gray-400 text-xs ml-auto">{{ lesson.title.en }}</span>
|
<!-- <span v-if="lesson.title.en" class="text-gray-400 text-xs ml-auto">{{ lesson.title.en }}</span> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
<q-icon v-else name="person" color="primary" />
|
<q-icon v-else name="person" color="primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-primary-600 hover:underline cursor-pointer">
|
<div class="font-medium text-primary-600">
|
||||||
{{ getFullName(props.row) }}
|
{{ getFullName(props.row) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-500">{{ props.row.email }}</div>
|
<div class="text-sm text-gray-500">{{ props.row.email }}</div>
|
||||||
|
|
|
||||||
|
|
@ -40,28 +40,37 @@
|
||||||
<!-- Title English -->
|
<!-- Title English -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.title.en"
|
v-model="form.title.en"
|
||||||
label="ชื่อแบบทดสอบ (English)"
|
label="ชื่อแบบทดสอบ (English) *"
|
||||||
outlined
|
outlined
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกชื่อแบบทดสอบ']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Content Thai -->
|
<!-- Content Thai -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.content.th"
|
v-model="form.content.th"
|
||||||
label="คำอธิบาย (ภาษาไทย)"
|
label="คำอธิบาย (ภาษาไทย) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
rows="2"
|
rows="2"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกคำอธิบาย']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Content English -->
|
<!-- Content English -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.content.en"
|
v-model="form.content.en"
|
||||||
label="คำอธิบาย (English)"
|
label="คำอธิบาย (English) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
rows="2"
|
rows="2"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกคำอธิบาย']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
@ -607,7 +616,6 @@ const saveQuestion = async () => {
|
||||||
try {
|
try {
|
||||||
const questionData: CreateQuestionRequest = {
|
const questionData: CreateQuestionRequest = {
|
||||||
question: questionForm.value.text,
|
question: questionForm.value.text,
|
||||||
explanation: { th: '', en: '' },
|
|
||||||
question_type: 'MULTIPLE_CHOICE',
|
question_type: 'MULTIPLE_CHOICE',
|
||||||
sort_order: editingQuestionIndex.value !== null
|
sort_order: editingQuestionIndex.value !== null
|
||||||
? questions.value[editingQuestionIndex.value].id ? editingQuestionIndex.value + 1 : questions.value.length + 1
|
? questions.value[editingQuestionIndex.value].id ? editingQuestionIndex.value + 1 : questions.value.length + 1
|
||||||
|
|
|
||||||
|
|
@ -39,28 +39,37 @@
|
||||||
<!-- Title English -->
|
<!-- Title English -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.title.en"
|
v-model="form.title.en"
|
||||||
label="ชื่อบทเรียน (English)"
|
label="ชื่อบทเรียน (English) *"
|
||||||
outlined
|
outlined
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกชื่อบทเรียน']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Content Thai -->
|
<!-- Content Thai -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.content.th"
|
v-model="form.content.th"
|
||||||
label="คำอธิบาย (ภาษาไทย)"
|
label="คำอธิบาย (ภาษาไทย) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
rows="3"
|
rows="3"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกคำอธิบาย']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Content English -->
|
<!-- Content English -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.content.en"
|
v-model="form.content.en"
|
||||||
label="คำอธิบาย (English)"
|
label="คำอธิบาย (English) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
rows="3"
|
rows="3"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกคำอธิบาย']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-card-section class="flex justify-end gap-2">
|
<q-card-section class="flex justify-end gap-2">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
|
||||||
|
|
@ -68,22 +68,28 @@
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.description.th"
|
v-model="form.description.th"
|
||||||
label="คำอธิบาย (ภาษาไทย)"
|
label="คำอธิบาย (ภาษาไทย) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
rows="3"
|
rows="3"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกคำอธิบาย']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="form.description.en"
|
v-model="form.description.en"
|
||||||
label="คำอธิบาย (English)"
|
label="คำอธิบาย (English) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
rows="3"
|
rows="3"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกคำอธิบาย']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,25 +156,34 @@
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="chapterForm.title.en"
|
v-model="chapterForm.title.en"
|
||||||
label="ชื่อบท (English)"
|
label="ชื่อบท (English) *"
|
||||||
outlined
|
outlined
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกชื่อบท']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="chapterForm.description.th"
|
v-model="chapterForm.description.th"
|
||||||
label="คำอธิบาย (ภาษาไทย)"
|
label="คำอธิบาย (ภาษาไทย) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกคำอธิบาย']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="chapterForm.description.en"
|
v-model="chapterForm.description.en"
|
||||||
label="คำอธิบาย (English)"
|
label="คำอธิบาย (English) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกคำอธิบาย']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 mt-4">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
|
@ -208,7 +217,7 @@
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="lessonForm.title.en"
|
v-model="lessonForm.title.en"
|
||||||
label="ชื่อบทเรียน (English)"
|
label="ชื่อบทเรียน (English) *"
|
||||||
outlined
|
outlined
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
|
|
@ -235,22 +244,29 @@
|
||||||
<q-input
|
<q-input
|
||||||
v-if="lessonForm.type"
|
v-if="lessonForm.type"
|
||||||
v-model="lessonForm.content.th"
|
v-model="lessonForm.content.th"
|
||||||
label="เนื้อหา (ภาษาไทย)"
|
label="เนื้อหา (ภาษาไทย) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
rows="3"
|
rows="3"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกเนื้อหา']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-if="lessonForm.type"
|
v-if="lessonForm.type"
|
||||||
v-model="lessonForm.content.en"
|
v-model="lessonForm.content.en"
|
||||||
label="เนื้อหา (English)"
|
label="เนื้อหา (English) *"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
outlined
|
outlined
|
||||||
autogrow
|
autogrow
|
||||||
rows="3"
|
rows="3"
|
||||||
|
class="mb-4"
|
||||||
|
:rules="[val => !!val || 'กรุณากรอกเนื้อหา']"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
hide-bottom-space
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 mt-4">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
|
|
||||||
|
|
@ -163,13 +163,13 @@
|
||||||
<q-btn flat round dense icon="more_vert">
|
<q-btn flat round dense icon="more_vert">
|
||||||
<q-menu>
|
<q-menu>
|
||||||
<q-list style="min-width: 150px">
|
<q-list style="min-width: 150px">
|
||||||
<q-item clickable v-close-popup @click="duplicateCourse(course)">
|
<q-item v-if="course.status === 'APPROVED'" clickable v-close-popup @click="duplicateCourse(course)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="content_copy" />
|
<q-icon name="content_copy" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>ทำสำเนา</q-item-section>
|
<q-item-section>ทำสำเนา</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator />
|
<q-separator v-if="course.status === 'APPROVED'" />
|
||||||
<q-item clickable v-close-popup @click="confirmDelete(course)">
|
<q-item clickable v-close-popup @click="confirmDelete(course)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="delete" color="negative" />
|
<q-icon name="delete" color="negative" />
|
||||||
|
|
@ -250,13 +250,13 @@
|
||||||
<q-btn flat round dense icon="more_vert" size="sm">
|
<q-btn flat round dense icon="more_vert" size="sm">
|
||||||
<q-menu>
|
<q-menu>
|
||||||
<q-list style="min-width: 150px">
|
<q-list style="min-width: 150px">
|
||||||
<q-item clickable v-close-popup @click="duplicateCourse(props.row)">
|
<q-item v-if="props.row.status === 'APPROVED'" clickable v-close-popup @click="duplicateCourse(props.row)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="content_copy" />
|
<q-icon name="content_copy" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>ทำสำเนา</q-item-section>
|
<q-item-section>ทำสำเนา</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator />
|
<q-separator v-if="props.row.status === 'APPROVED'" />
|
||||||
<q-item clickable v-close-popup @click="confirmDelete(props.row)">
|
<q-item clickable v-close-popup @click="confirmDelete(props.row)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="delete" color="negative" />
|
<q-icon name="delete" color="negative" />
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'instructor',
|
layout: 'instructor',
|
||||||
middleware: 'auth'
|
middleware: 'auth'
|
||||||
|
|
@ -179,6 +181,7 @@ definePageMeta({
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const instructorStore = useInstructorStore();
|
const instructorStore = useInstructorStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
// Navigation functions
|
// Navigation functions
|
||||||
const goToProfile = () => {
|
const goToProfile = () => {
|
||||||
|
|
@ -190,8 +193,23 @@ const goToSettings = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authStore.logout();
|
$q.dialog({
|
||||||
router.push('/login');
|
title: 'ยืนยันการออกจากระบบ',
|
||||||
|
message: 'คุณต้องการออกจากระบบใช่หรือไม่?',
|
||||||
|
cancel: {
|
||||||
|
label: 'ยกเลิก',
|
||||||
|
flat: true,
|
||||||
|
color: 'grey-8'
|
||||||
|
},
|
||||||
|
ok: {
|
||||||
|
label: 'ออกจากระบบ',
|
||||||
|
color: 'negative'
|
||||||
|
},
|
||||||
|
persistent: true
|
||||||
|
}).onOk(() => {
|
||||||
|
authStore.logout();
|
||||||
|
router.push('/login');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch dashboard data on mount
|
// Fetch dashboard data on mount
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue