fix
This commit is contained in:
@@ -21,18 +21,13 @@
|
||||
</div>
|
||||
|
||||
<div class="course-item__meta">
|
||||
<div v-if="course.term?.title" class="course-item__pill">
|
||||
<span class="course-item__pill-label">مختص به:</span>
|
||||
<span class="course-item__pill-value">{{ course.term.title }}</span>
|
||||
</div>
|
||||
<div class="course-item__pill">
|
||||
<span class="course-item__pill-label">ظرفیت:</span>
|
||||
<span class="course-item__pill-value">{{ capacity || '—' }}</span>
|
||||
</div>
|
||||
<div v-if="course.prerequisitesCount" class="course-item__pill">
|
||||
<span class="course-item__pill-label">پیشنیاز:</span>
|
||||
<span class="course-item__pill-value">{{ course.prerequisitesCount }}</span>
|
||||
</div>
|
||||
<Badge v-if="course.term?.title" label="مختص به:" :value="course.term.title" />
|
||||
<Badge label="ظرفیت:" :value="capacity || '—'" />
|
||||
<Badge
|
||||
v-if="course.prerequisitesCount"
|
||||
label="پیشنیاز:"
|
||||
:value="course.prerequisitesCount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!isActive" class="course-item__status">
|
||||
@@ -82,6 +77,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Badge from '@/components/Badge.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import CircleButton from '@/components/CircleButton.vue'
|
||||
@@ -94,14 +90,14 @@ const props = defineProps({
|
||||
const emit = defineEmits(['edit', 'delete', 'change-status', 'show-details'])
|
||||
|
||||
const teacherName = computed(() => {
|
||||
const t = props.course.teacher || props.course.defaultTeacher
|
||||
const t = props.course.teacher
|
||||
if (!t) return '—'
|
||||
return `${t.firstName || ''} ${t.lastName || ''}`.trim() || t.fullName || '—'
|
||||
return t.name
|
||||
})
|
||||
|
||||
const capacity = computed(() => props.course.capacity ?? props.course.defaultCapacity ?? '')
|
||||
const capacity = computed(() => props.course.capacity ?? '')
|
||||
|
||||
const isActive = computed(() => props.course.isActive ?? props.course.isActiveByDefault ?? false)
|
||||
const isActive = computed(() => props.course.isActive ?? false)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -193,28 +189,7 @@ const isActive = computed(() => props.course.isActive ?? props.course.isActiveBy
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
flex: 1 1 33%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__pill {
|
||||
background: rgba(107, 107, 107, 5%);
|
||||
padding: 0.25rem 1rem;
|
||||
border-radius: 0.875rem;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__pill-label {
|
||||
font-family: var(--font-family-fa);
|
||||
font-weight: 300;
|
||||
font-size: 0.75rem;
|
||||
color: #848484;
|
||||
margin-inline-end: 0.25rem;
|
||||
}
|
||||
|
||||
&__pill-value {
|
||||
font-family: var(--font-family-en);
|
||||
font-size: 0.75rem;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__status {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<BasicModal width="95%" max-width="42rem" min-width="auto" :show-close-button="true">
|
||||
<BasicModal
|
||||
title="افزودن دانشجو"
|
||||
title-en="Add Student"
|
||||
width="95%"
|
||||
max-width="42rem"
|
||||
min-width="auto"
|
||||
:show-close-button="true"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<div class="add-course-student">
|
||||
<LineTitleBlock title="افزودن دانشجو" title-en="Add Student" />
|
||||
|
||||
<div class="add-course-student__search">
|
||||
<SvgIcon name="user" :size="18" color="var(--color-thd-gray)" />
|
||||
<input
|
||||
@@ -21,7 +26,7 @@
|
||||
<div v-for="user in users" :key="user.id" class="add-course-student__row">
|
||||
<div class="add-course-student__main">
|
||||
<div v-if="user.avatarUrl" class="add-course-student__avatar">
|
||||
<img :src="user.avatarUrl" :alt="userLabel(user)" />
|
||||
<img :src="user.avatarUrl" :alt="user?.name" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
@@ -30,7 +35,7 @@
|
||||
<SvgIcon name="user" :size="20" color="#bcbcbc" />
|
||||
</div>
|
||||
<div class="add-course-student__text">
|
||||
<p class="add-course-student__name">{{ userLabel(user) }}</p>
|
||||
<p class="add-course-student__name">{{ user?.name }}</p>
|
||||
<p class="add-course-student__meta">
|
||||
<span>{{ user.address?.province?.name || '—' }}</span>
|
||||
<span class="add-course-student__sep">،</span>
|
||||
@@ -98,11 +103,11 @@ import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||
import { useAdminUsersListQuery } from '@/services/query/admin-users'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import {
|
||||
adminCourseTemplatesKeys,
|
||||
useAddAdminTemplateStudentMutation,
|
||||
useAdminTemplateStudentsQuery,
|
||||
useRemoveAdminTemplateStudentMutation,
|
||||
} from '@/services/query/admin-course-templates'
|
||||
adminCoursesKeys,
|
||||
useAddAdminCourseStudentMutation,
|
||||
useAdminCourseStudentsQuery,
|
||||
useRemoveAdminCourseStudentMutation,
|
||||
} from '@/services/query/admin-courses'
|
||||
|
||||
defineOptions({ name: 'AddCourseStudentModal' })
|
||||
|
||||
@@ -110,7 +115,7 @@ const queryClient = useQueryClient()
|
||||
const { getModal } = useModal()
|
||||
|
||||
const modalData = computed(() => getModal('AddCourseStudentModal')?.data ?? {})
|
||||
const templateId = computed(() => modalData.value.templateId ?? null)
|
||||
const courseId = computed(() => modalData.value.courseId ?? null)
|
||||
|
||||
const searchInput = ref('')
|
||||
const searchQuery = ref('')
|
||||
@@ -122,22 +127,16 @@ const users = computed(() => usersResponse.value?.data ?? [])
|
||||
|
||||
const attachedFilters = computed(() => ({}))
|
||||
const attachedPagination = ref({ page: 1, perPage: 200 })
|
||||
const { data: attachedResponse } = useAdminTemplateStudentsQuery(
|
||||
templateId,
|
||||
const { data: attachedResponse } = useAdminCourseStudentsQuery(
|
||||
courseId,
|
||||
attachedFilters,
|
||||
attachedPagination,
|
||||
{ enabled: () => !!templateId.value }
|
||||
{ enabled: () => !!courseId.value }
|
||||
)
|
||||
const attachedIds = computed(() => new Set((attachedResponse.value?.data ?? []).map((u) => u.id)))
|
||||
|
||||
const isAttached = (id) => attachedIds.value.has(id)
|
||||
|
||||
const userLabel = (user) =>
|
||||
`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
user.fullName ||
|
||||
user.phoneNumber ||
|
||||
'—'
|
||||
|
||||
const onSearchInput = useDebounce(() => {
|
||||
searchQuery.value = searchInput.value || ''
|
||||
userPagination.value = { ...userPagination.value, page: 1 }
|
||||
@@ -145,17 +144,17 @@ const onSearchInput = useDebounce(() => {
|
||||
|
||||
const pendingId = ref(null)
|
||||
|
||||
const addMutation = useAddAdminTemplateStudentMutation()
|
||||
const removeMutation = useRemoveAdminTemplateStudentMutation()
|
||||
const addMutation = useAddAdminCourseStudentMutation()
|
||||
const removeMutation = useRemoveAdminCourseStudentMutation()
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: adminCourseTemplatesKeys.all })
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: adminCoursesKeys.all })
|
||||
|
||||
const onAttach = async (user) => {
|
||||
if (!templateId.value) return
|
||||
if (!courseId.value) return
|
||||
pendingId.value = user.id
|
||||
try {
|
||||
await addMutation.mutateAsync({
|
||||
templateId: templateId.value,
|
||||
courseId: courseId.value,
|
||||
payload: { userIds: [user.id] },
|
||||
})
|
||||
invalidate()
|
||||
@@ -165,17 +164,17 @@ const onAttach = async (user) => {
|
||||
}
|
||||
|
||||
const onDetach = async (user) => {
|
||||
if (!templateId.value) return
|
||||
if (!courseId.value) return
|
||||
pendingId.value = user.id
|
||||
try {
|
||||
await removeMutation.mutateAsync({ templateId: templateId.value, userId: user.id })
|
||||
await removeMutation.mutateAsync({ courseId: courseId.value, userId: user.id })
|
||||
invalidate()
|
||||
} finally {
|
||||
pendingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(templateId, () => {
|
||||
watch(courseId, () => {
|
||||
searchInput.value = ''
|
||||
searchQuery.value = ''
|
||||
pendingId.value = null
|
||||
|
||||
@@ -122,11 +122,11 @@ import { objectToFormData } from '@/utils/object-to-formdata'
|
||||
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
||||
import { offeredCourseSchema } from '@/features/admin/courses/schema'
|
||||
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
||||
import {
|
||||
adminCoursesKeys,
|
||||
useAddAdminCourseMutation,
|
||||
useAdminCourseQuery,
|
||||
useAdminCoursesListQuery,
|
||||
useUpdateAdminCourseMutation,
|
||||
} from '@/services/query/admin-courses'
|
||||
|
||||
@@ -139,19 +139,24 @@ const modalData = computed(() => getModal('AddOfferedCourseModal')?.data ?? {})
|
||||
const mode = computed(() => modalData.value.mode || 'add')
|
||||
const courseId = computed(() => modalData.value.courseId ?? null)
|
||||
const isEditMode = computed(() => mode.value === 'edit')
|
||||
const presetTermId = computed(() => modalData.value.termId ?? '')
|
||||
|
||||
const modeTitle = computed(() =>
|
||||
isEditMode.value ? 'ویرایش دوره ارائه شده' : 'افزودن دوره ارائه شده'
|
||||
)
|
||||
|
||||
const form = ref({
|
||||
termId: '',
|
||||
termId: presetTermId.value,
|
||||
templateId: '',
|
||||
title: '',
|
||||
capacity: '',
|
||||
imageId: null,
|
||||
isActive: false,
|
||||
})
|
||||
|
||||
watch(presetTermId, (val) => {
|
||||
if (val && !form.value.termId) form.value.termId = val
|
||||
})
|
||||
const image = ref(null)
|
||||
|
||||
const schema = offeredCourseSchema
|
||||
@@ -172,12 +177,9 @@ const termOptions = computed(() => {
|
||||
})
|
||||
|
||||
const templateSearch = ref('')
|
||||
const templateFilters = computed(() => ({ title: templateSearch.value }))
|
||||
const templateFilters = computed(() => ({ title: templateSearch.value, termId: 'null' }))
|
||||
const templatePagination = ref({ page: 1, perPage: 30 })
|
||||
const { data: templatesResponse } = useAdminCourseTemplatesListQuery(
|
||||
templateFilters,
|
||||
templatePagination
|
||||
)
|
||||
const { data: templatesResponse } = useAdminCoursesListQuery(templateFilters, templatePagination)
|
||||
const selectedTemplate = ref(null)
|
||||
const templateOptions = computed(() => {
|
||||
const base = templatesResponse.value?.data ?? []
|
||||
@@ -201,10 +203,9 @@ const { data: existingCourse } = useAdminCourseQuery(courseId, {
|
||||
watch(existingCourse, (course) => {
|
||||
if (!course) return
|
||||
if (course.term) selectedTerm.value = course.term
|
||||
if (course.template) selectedTemplate.value = course.template
|
||||
form.value = {
|
||||
termId: course.term?.id || course.termId || '',
|
||||
templateId: course.template?.id || course.templateId || '',
|
||||
templateId: '',
|
||||
title: course.title || '',
|
||||
capacity: course.capacity ?? '',
|
||||
imageId: course.imageId || null,
|
||||
@@ -237,10 +238,11 @@ const submitting = computed(() => addMutation.isPending.value || updateMutation.
|
||||
const onSubmit = async (close) => {
|
||||
const { isValid, payload } = await validate(form.value)
|
||||
if (!isValid) return
|
||||
const { templateId: _ignored, ...submitPayload } = payload
|
||||
if (isEditMode.value) {
|
||||
await updateMutation.mutateAsync({ id: courseId.value, payload })
|
||||
await updateMutation.mutateAsync({ id: courseId.value, payload: submitPayload })
|
||||
} else {
|
||||
await addMutation.mutateAsync(payload)
|
||||
await addMutation.mutateAsync(submitPayload)
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: adminCoursesKeys.all })
|
||||
resetErrors()
|
||||
|
||||
@@ -95,11 +95,11 @@ import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import {
|
||||
adminCourseTemplatesKeys,
|
||||
useAdminTemplateSessionsQuery,
|
||||
useAttachAdminTemplateSessionMutation,
|
||||
useDetachAdminTemplateSessionMutation,
|
||||
} from '@/services/query/admin-course-templates'
|
||||
adminCoursesKeys,
|
||||
useAdminCourseSessionsQuery,
|
||||
useAttachAdminCourseSessionMutation,
|
||||
useDetachAdminCourseSessionMutation,
|
||||
} from '@/services/query/admin-courses'
|
||||
|
||||
defineOptions({ name: 'AddSessionToCourseModal' })
|
||||
|
||||
@@ -107,7 +107,7 @@ const queryClient = useQueryClient()
|
||||
const { getModal } = useModal()
|
||||
|
||||
const modalData = computed(() => getModal('AddSessionToCourseModal')?.data ?? {})
|
||||
const templateId = computed(() => modalData.value.templateId ?? null)
|
||||
const courseId = computed(() => modalData.value.courseId ?? null)
|
||||
|
||||
const searchInput = ref('')
|
||||
const searchQuery = ref('')
|
||||
@@ -122,18 +122,18 @@ const sessions = computed(() => sessionsResponse.value?.data ?? [])
|
||||
|
||||
const attachedFilters = computed(() => ({}))
|
||||
const attachedPagination = ref({ page: 1, perPage: 200 })
|
||||
const { data: attachedResponse } = useAdminTemplateSessionsQuery(
|
||||
templateId,
|
||||
const { data: attachedResponse } = useAdminCourseSessionsQuery(
|
||||
courseId,
|
||||
attachedFilters,
|
||||
attachedPagination,
|
||||
{ enabled: () => !!templateId.value }
|
||||
{ enabled: () => !!courseId.value }
|
||||
)
|
||||
const attachedIds = computed(() => new Set((attachedResponse.value?.data ?? []).map((s) => s.id)))
|
||||
|
||||
const isAttached = (id) => attachedIds.value.has(id)
|
||||
|
||||
const teacherName = (session) => {
|
||||
const t = session.teacher || session.defaultTeacher
|
||||
const t = session.teacher
|
||||
if (!t) return '—'
|
||||
return `${t.firstName || ''} ${t.lastName || ''}`.trim() || t.fullName || '—'
|
||||
}
|
||||
@@ -145,17 +145,17 @@ const onSearchInput = useDebounce(() => {
|
||||
|
||||
const pendingId = ref(null)
|
||||
|
||||
const attachMutation = useAttachAdminTemplateSessionMutation()
|
||||
const detachMutation = useDetachAdminTemplateSessionMutation()
|
||||
const attachMutation = useAttachAdminCourseSessionMutation()
|
||||
const detachMutation = useDetachAdminCourseSessionMutation()
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: adminCourseTemplatesKeys.all })
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: adminCoursesKeys.all })
|
||||
|
||||
const onAttach = async (session) => {
|
||||
if (!templateId.value) return
|
||||
if (!courseId.value) return
|
||||
pendingId.value = session.id
|
||||
try {
|
||||
await attachMutation.mutateAsync({
|
||||
templateId: templateId.value,
|
||||
courseId: courseId.value,
|
||||
payload: { sessionIds: [session.id] },
|
||||
})
|
||||
invalidate()
|
||||
@@ -165,17 +165,17 @@ const onAttach = async (session) => {
|
||||
}
|
||||
|
||||
const onDetach = async (session) => {
|
||||
if (!templateId.value) return
|
||||
if (!courseId.value) return
|
||||
pendingId.value = session.id
|
||||
try {
|
||||
await detachMutation.mutateAsync({ templateId: templateId.value, sessionId: session.id })
|
||||
await detachMutation.mutateAsync({ courseId: courseId.value, sessionId: session.id })
|
||||
invalidate()
|
||||
} finally {
|
||||
pendingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(templateId, () => {
|
||||
watch(courseId, () => {
|
||||
searchInput.value = ''
|
||||
searchQuery.value = ''
|
||||
pendingId.value = null
|
||||
|
||||
@@ -166,12 +166,12 @@ import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import CourseSessionItem from '@/features/admin/courses/components/CourseSessionItem.vue'
|
||||
import {
|
||||
adminCourseTemplatesKeys,
|
||||
useAdminCourseTemplateQuery,
|
||||
useAdminTemplateSessionsQuery,
|
||||
useAdminTemplateStudentsQuery,
|
||||
useRemoveAdminTemplateStudentMutation,
|
||||
} from '@/services/query/admin-course-templates'
|
||||
adminCoursesKeys,
|
||||
useAdminCourseQuery,
|
||||
useAdminCourseSessionsQuery,
|
||||
useAdminCourseStudentsQuery,
|
||||
useRemoveAdminCourseStudentMutation,
|
||||
} from '@/services/query/admin-courses'
|
||||
|
||||
defineOptions({ name: 'CourseDetailsModal' })
|
||||
|
||||
@@ -179,14 +179,14 @@ const queryClient = useQueryClient()
|
||||
const { openModal, getModal } = useModal()
|
||||
|
||||
const modalData = computed(() => getModal('CourseDetailsModal')?.data ?? {})
|
||||
const templateId = computed(() => modalData.value.id ?? null)
|
||||
const courseId = computed(() => modalData.value.id ?? null)
|
||||
|
||||
const { data: course, isLoading } = useAdminCourseTemplateQuery(templateId, {
|
||||
enabled: () => !!templateId.value,
|
||||
const { data: course, isLoading } = useAdminCourseQuery(courseId, {
|
||||
enabled: () => !!courseId.value,
|
||||
})
|
||||
|
||||
const teacherName = computed(() => {
|
||||
const t = course.value?.defaultTeacher || course.value?.teacher
|
||||
const t = course.value?.teacher
|
||||
if (!t) return '—'
|
||||
return `${t.firstName || ''} ${t.lastName || ''}`.trim() || t.fullName || '—'
|
||||
})
|
||||
@@ -203,12 +203,12 @@ const { pagination: sessionsPagination, setPage: setSessionsPage } = usePaginati
|
||||
perPage: 10,
|
||||
})
|
||||
|
||||
const { data: sessionsData, isLoading: sessionsPending } = useAdminTemplateSessionsQuery(
|
||||
templateId,
|
||||
const { data: sessionsData, isLoading: sessionsPending } = useAdminCourseSessionsQuery(
|
||||
courseId,
|
||||
sessionsFilters,
|
||||
sessionsPagination,
|
||||
{
|
||||
enabled: () => !!templateId.value && activeTab.value === 'sessions',
|
||||
enabled: () => !!courseId.value && activeTab.value === 'sessions',
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
@@ -226,12 +226,12 @@ const { pagination: studentsPagination, setPage: setStudentsPage } = usePaginati
|
||||
perPage: 10,
|
||||
})
|
||||
|
||||
const { data: studentsData, isLoading: studentsPending } = useAdminTemplateStudentsQuery(
|
||||
templateId,
|
||||
const { data: studentsData, isLoading: studentsPending } = useAdminCourseStudentsQuery(
|
||||
courseId,
|
||||
studentsFilters,
|
||||
studentsPagination,
|
||||
{
|
||||
enabled: () => !!templateId.value && activeTab.value === 'students',
|
||||
enabled: () => !!courseId.value && activeTab.value === 'students',
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
@@ -249,16 +249,16 @@ const studentName = (student) =>
|
||||
student.phoneNumber ||
|
||||
'—'
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: adminCourseTemplatesKeys.all })
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: adminCoursesKeys.all })
|
||||
|
||||
const removeStudentMutation = useRemoveAdminTemplateStudentMutation()
|
||||
const removeStudentMutation = useRemoveAdminCourseStudentMutation()
|
||||
|
||||
const onOpenAddSession = () => {
|
||||
openModal('AddSessionToCourseModal', { templateId: templateId.value })
|
||||
openModal('AddSessionToCourseModal', { courseId: courseId.value })
|
||||
}
|
||||
|
||||
const onOpenAddStudent = () => {
|
||||
openModal('AddCourseStudentModal', { templateId: templateId.value })
|
||||
openModal('AddCourseStudentModal', { courseId: courseId.value })
|
||||
}
|
||||
|
||||
const onAskRemoveStudent = (student) => {
|
||||
@@ -267,7 +267,7 @@ const onAskRemoveStudent = (student) => {
|
||||
message: `آیا از حذف <strong>${studentName(student)}</strong> از این دوره اطمینان دارید؟`,
|
||||
onConfirm: () =>
|
||||
removeStudentMutation.mutate(
|
||||
{ templateId: templateId.value, userId: student.id },
|
||||
{ courseId: courseId.value, userId: student.id },
|
||||
{ onSuccess: invalidate }
|
||||
),
|
||||
})
|
||||
|
||||
+30
-31
@@ -2,11 +2,9 @@
|
||||
<div class="course-form">
|
||||
<BoxedIconTitleBlock
|
||||
class="course-form__heading"
|
||||
:title="isEditMode ? 'ویرایش دوره الگو' : 'افزودن دوره الگوی جدید'"
|
||||
:title="isEditMode ? 'ویرایش دوره' : 'افزودن دوره جدید'"
|
||||
:desc="
|
||||
isEditMode
|
||||
? 'اطلاعات دوره الگو را بهروز کنید'
|
||||
: 'در این قسمت میتوانید دوره الگوی جدید اضافه کنید'
|
||||
isEditMode ? 'اطلاعات دوره را بهروز کنید' : 'در این قسمت میتوانید دوره جدید اضافه کنید'
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -44,26 +42,26 @@
|
||||
</div>
|
||||
<div class="course-form__cell course-form__cell--third">
|
||||
<SelectField
|
||||
v-model="form.defaultTeacherId"
|
||||
name="defaultTeacherId"
|
||||
v-model="form.teacherId"
|
||||
name="teacherId"
|
||||
label="استاد"
|
||||
:options="teacherOptions"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
:searchable="true"
|
||||
:on-search="searchTeachers"
|
||||
:error="errors.defaultTeacherId"
|
||||
:error="errors.teacherId"
|
||||
/>
|
||||
</div>
|
||||
<div class="course-form__cell course-form__cell--third">
|
||||
<TextField
|
||||
v-model="form.defaultCapacity"
|
||||
name="defaultCapacity"
|
||||
v-model="form.capacity"
|
||||
name="capacity"
|
||||
label="ظرفیت (نفر)"
|
||||
inputmode="numeric"
|
||||
:convert-digits="true"
|
||||
:error="errors.defaultCapacity"
|
||||
@blur="validateAt('defaultCapacity', form.defaultCapacity)"
|
||||
:error="errors.capacity"
|
||||
@blur="validateAt('capacity', form.capacity)"
|
||||
/>
|
||||
</div>
|
||||
<div class="course-form__cell course-form__cell--third">
|
||||
@@ -171,18 +169,18 @@ import FileUploader from '@/components/form/FileUploader.vue'
|
||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||
import { courseSchema } from '@/features/admin/courses/schema'
|
||||
import TextareaField from '@/components/form/TextareaField.vue'
|
||||
import { useAdminUsersListQuery } from '@/services/query/admin-users'
|
||||
import { courseTemplateSchema } from '@/features/admin/courses/schema'
|
||||
import { COURSE_CONTENT_TYPE, COURSE_CONTENT_TYPE_ACCEPT } from '@/enums'
|
||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||
import {
|
||||
adminCourseTemplatesKeys,
|
||||
useAddAdminCourseTemplateMutation,
|
||||
useAdminCourseTemplateQuery,
|
||||
useAdminCourseTemplatesListQuery,
|
||||
useUpdateAdminCourseTemplateMutation,
|
||||
} from '@/services/query/admin-course-templates'
|
||||
adminCoursesKeys,
|
||||
useAddAdminCourseMutation,
|
||||
useAdminCourseQuery,
|
||||
useAdminCoursesListQuery,
|
||||
useUpdateAdminCourseMutation,
|
||||
} from '@/services/query/admin-courses'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -194,8 +192,8 @@ const isEditMode = computed(() => !!courseId.value)
|
||||
|
||||
const form = ref({
|
||||
title: '',
|
||||
defaultTeacherId: '',
|
||||
defaultCapacity: '',
|
||||
teacherId: '',
|
||||
capacity: '',
|
||||
sessionsCount: '',
|
||||
prerequisites: [],
|
||||
contentType: '',
|
||||
@@ -215,7 +213,7 @@ const contentTypeOptions = Object.entries(COURSE_CONTENT_TYPE).map(([value, labe
|
||||
|
||||
const contentAccept = computed(() => COURSE_CONTENT_TYPE_ACCEPT[form.value.contentType] || '*')
|
||||
|
||||
const { validate, validateAt, errors } = useYup(courseTemplateSchema)
|
||||
const { validate, validateAt, errors } = useYup(courseSchema)
|
||||
|
||||
const teacherSearch = ref('')
|
||||
const teacherFilters = computed(() => ({ name: teacherSearch.value }))
|
||||
@@ -237,7 +235,7 @@ const teacherOptions = computed(() => {
|
||||
const prereqSearch = ref('')
|
||||
const prereqFilters = computed(() => ({ title: prereqSearch.value }))
|
||||
const prereqPagination = ref({ page: 1, perPage: 30 })
|
||||
const { data: prereqsResponse } = useAdminCourseTemplatesListQuery(prereqFilters, prereqPagination)
|
||||
const { data: prereqsResponse } = useAdminCoursesListQuery(prereqFilters, prereqPagination)
|
||||
const selectedPrereqs = ref([])
|
||||
const prerequisiteOptions = computed(() => {
|
||||
const base = prereqsResponse.value?.data ?? []
|
||||
@@ -252,28 +250,28 @@ const searchPrerequisites = useDebounce((q) => {
|
||||
prereqSearch.value = q || ''
|
||||
}, 400)
|
||||
|
||||
const { data: existingCourse } = useAdminCourseTemplateQuery(courseId, {
|
||||
const { data: existingCourse } = useAdminCourseQuery(courseId, {
|
||||
enabled: () => !!courseId.value,
|
||||
})
|
||||
|
||||
watch(existingCourse, (course) => {
|
||||
if (!course) return
|
||||
const teacher = course.defaultTeacher || course.teacher
|
||||
const teacher = course.teacher
|
||||
if (teacher) selectedTeacher.value = teacher
|
||||
const prereqs = Array.isArray(course.prerequisites) ? course.prerequisites : []
|
||||
selectedPrereqs.value = prereqs.map((p) => p.course || p).filter((c) => c?.id)
|
||||
|
||||
form.value = {
|
||||
title: course.title || '',
|
||||
defaultTeacherId: teacher?.id || course.defaultTeacherId || '',
|
||||
defaultCapacity: course.defaultCapacity ?? course.capacity ?? '',
|
||||
teacherId: teacher?.id || course.teacherId || '',
|
||||
capacity: course.capacity ?? '',
|
||||
sessionsCount: course.sessionsCount ?? '',
|
||||
prerequisites: prereqs.map((p) => p.courseId ?? p.id).filter(Boolean),
|
||||
contentType: course.contentType || '',
|
||||
contentMediaId: course.contentMediaId || null,
|
||||
description: course.description || '',
|
||||
coverMediaId: course.coverMediaId || null,
|
||||
termId: termId.value,
|
||||
termId: course.termId ?? termId.value,
|
||||
}
|
||||
if (course.coverUrl) image.value = { url: course.coverUrl }
|
||||
if (course.contentMedia) {
|
||||
@@ -315,7 +313,7 @@ const onContentSelect = async (files) => {
|
||||
try {
|
||||
const formData = objectToFormData({
|
||||
file,
|
||||
purpose: 'content',
|
||||
purpose: 'voice',
|
||||
context: 'course',
|
||||
type: form.value.contentType,
|
||||
})
|
||||
@@ -336,8 +334,8 @@ const onContentRemove = () => {
|
||||
|
||||
const onContentError = (msg) => toast.error(msg)
|
||||
|
||||
const addMutation = useAddAdminCourseTemplateMutation()
|
||||
const updateMutation = useUpdateAdminCourseTemplateMutation()
|
||||
const addMutation = useAddAdminCourseMutation()
|
||||
const updateMutation = useUpdateAdminCourseMutation()
|
||||
|
||||
const submitting = computed(() => addMutation.isPending.value || updateMutation.isPending.value)
|
||||
|
||||
@@ -349,7 +347,7 @@ const onSubmit = async () => {
|
||||
} else {
|
||||
await addMutation.mutateAsync(payload)
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: adminCourseTemplatesKeys.all })
|
||||
await queryClient.invalidateQueries({ queryKey: adminCoursesKeys.all })
|
||||
router.push({ name: 'admin-courses' })
|
||||
}
|
||||
|
||||
@@ -452,6 +450,7 @@ const onCancel = () => router.push({ name: 'admin-courses' })
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
v-for="course in templates"
|
||||
:key="course.id"
|
||||
:course="course"
|
||||
@edit="onEditTemplate"
|
||||
@delete="onAskDeleteTemplate"
|
||||
@change-status="onChangeTemplateStatus"
|
||||
@edit="onEditCourse"
|
||||
@delete="onAskDelete"
|
||||
@change-status="onChangeStatus"
|
||||
@show-details="onShowDetails"
|
||||
/>
|
||||
</div>
|
||||
@@ -36,20 +36,27 @@
|
||||
</template>
|
||||
|
||||
<template #offered>
|
||||
<SkeletonLoaderBlock v-if="offeredPending" :rows="6" :cols-per-row="1" />
|
||||
<div v-else-if="offered.length > 0">
|
||||
<CourseItem
|
||||
v-for="course in offered"
|
||||
:key="course.id"
|
||||
:course="course"
|
||||
@edit="onEditOffered"
|
||||
@delete="onAskDeleteOffered"
|
||||
@change-status="onChangeOfferedStatus"
|
||||
@show-details="onShowDetails"
|
||||
/>
|
||||
</div>
|
||||
<NoItems v-else title="متاسفیم" desc="دورهای برای نمایش وجود ندارد." />
|
||||
<PaginationBlock :pagination="offeredPaginationMeta" @update:page="setOfferedPage" />
|
||||
<NoItems
|
||||
v-if="!hasOfferedTerm"
|
||||
title="ترم را انتخاب کنید"
|
||||
desc="برای نمایش دورههای ارائه شده، ابتدا ترم را از فیلترها انتخاب کنید."
|
||||
/>
|
||||
<template v-else>
|
||||
<SkeletonLoaderBlock v-if="offeredPending" :rows="6" :cols-per-row="1" />
|
||||
<div v-else-if="offered.length > 0">
|
||||
<CourseItem
|
||||
v-for="course in offered"
|
||||
:key="course.id"
|
||||
:course="course"
|
||||
@edit="onEditCourse"
|
||||
@delete="onAskDelete"
|
||||
@change-status="onChangeStatus"
|
||||
@show-details="onShowDetails"
|
||||
/>
|
||||
</div>
|
||||
<NoItems v-else title="متاسفیم" desc="دورهای برای نمایش وجود ندارد." />
|
||||
<PaginationBlock :pagination="offeredPaginationMeta" @update:page="setOfferedPage" />
|
||||
</template>
|
||||
</template>
|
||||
</TabsBlock>
|
||||
|
||||
@@ -61,11 +68,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import useModal from '@/composables/useModal'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import NoItems from '@/components/blocks/NoItems.vue'
|
||||
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
||||
import { usePagination } from '@/composables/usePagination'
|
||||
@@ -81,25 +88,25 @@ import AddSessionToCourseModal from '@/features/admin/courses/components/modals/
|
||||
import {
|
||||
adminCoursesKeys,
|
||||
useAdminCoursesListQuery,
|
||||
useChangeAdminCourseStatusMutation,
|
||||
useDeleteAdminCourseMutation,
|
||||
useUpdateAdminCourseMutation,
|
||||
} from '@/services/query/admin-courses'
|
||||
import {
|
||||
adminCourseTemplatesKeys,
|
||||
useAdminCourseTemplatesListQuery,
|
||||
useChangeAdminCourseTemplateStatusMutation,
|
||||
useDeleteAdminCourseTemplateMutation,
|
||||
} from '@/services/query/admin-course-templates'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const { openModal, isModal } = useModal()
|
||||
|
||||
const routeTermId = computed(() => (route.params.termId ? Number(route.params.termId) : null))
|
||||
|
||||
const onAdd = () => {
|
||||
if (activeTab.value === 'templates') {
|
||||
router.push({ name: 'admin-add-course-template' }).catch(() => {})
|
||||
router.push({ name: 'admin-add-course' }).catch(() => {})
|
||||
} else {
|
||||
openModal('AddOfferedCourseModal', { mode: 'add' })
|
||||
openModal('AddOfferedCourseModal', {
|
||||
mode: 'add',
|
||||
termId: routeTermId.value ?? undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +128,16 @@ const tabs = [
|
||||
buttonAction: onAdd,
|
||||
},
|
||||
]
|
||||
const activeTab = ref('templates')
|
||||
const activeTab = ref(routeTermId.value ? 'offered' : 'templates')
|
||||
|
||||
const templateFilters = ref({ title: '', status: '', fromDate: '', toDate: '' })
|
||||
const offeredFilters = ref({ title: '', termId: '', status: '', fromDate: '', toDate: '' })
|
||||
const offeredFilters = ref({
|
||||
title: '',
|
||||
termId: routeTermId.value ?? '',
|
||||
status: '',
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
})
|
||||
|
||||
const currentFilters = computed({
|
||||
get: () => (activeTab.value === 'templates' ? templateFilters.value : offeredFilters.value),
|
||||
@@ -146,7 +159,7 @@ const {
|
||||
reset: resetOfferedPagination,
|
||||
} = usePagination({ page: 1, perPage: 10 })
|
||||
|
||||
const { data: templatesData, isLoading: templatesPending } = useAdminCourseTemplatesListQuery(
|
||||
const { data: templatesData, isLoading: templatesPending } = useAdminCoursesListQuery(
|
||||
templateFilters,
|
||||
templatesPagination,
|
||||
{
|
||||
@@ -155,16 +168,18 @@ const { data: templatesData, isLoading: templatesPending } = useAdminCourseTempl
|
||||
}
|
||||
)
|
||||
|
||||
const hasOfferedTerm = computed(() => !!offeredFilters.value.termId)
|
||||
|
||||
const { data: offeredData, isLoading: offeredPending } = useAdminCoursesListQuery(
|
||||
offeredFilters,
|
||||
offeredPagination,
|
||||
{
|
||||
enabled: () => activeTab.value === 'offered',
|
||||
enabled: () => activeTab.value === 'offered' && hasOfferedTerm.value,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const templates = computed(() => templatesData.value?.data?.items ?? [])
|
||||
const templates = computed(() => templatesData.value?.data ?? [])
|
||||
const templatesPaginationMeta = computed(() => ({
|
||||
page: templatesPagination.value.page,
|
||||
perPage: templatesPagination.value.perPage,
|
||||
@@ -188,12 +203,8 @@ const onFilterApply = () => {
|
||||
}
|
||||
const onFilterReset = onFilterApply
|
||||
|
||||
const onEditTemplate = (course) => {
|
||||
router.push({ name: 'admin-edit-course-template', params: { id: course.id } }).catch(() => {})
|
||||
}
|
||||
|
||||
const onEditOffered = (course) => {
|
||||
openModal('AddOfferedCourseModal', { mode: 'edit', courseId: course.id })
|
||||
const onEditCourse = (course) => {
|
||||
router.push({ name: 'admin-edit-course', params: { id: course.id } }).catch(() => {})
|
||||
}
|
||||
|
||||
const onShowDetails = (course) => {
|
||||
@@ -203,45 +214,33 @@ const onShowDetails = (course) => {
|
||||
})
|
||||
}
|
||||
|
||||
const invalidateTemplates = () =>
|
||||
queryClient.invalidateQueries({ queryKey: adminCourseTemplatesKeys.all })
|
||||
const invalidateOffered = () => queryClient.invalidateQueries({ queryKey: adminCoursesKeys.all })
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: adminCoursesKeys.all })
|
||||
|
||||
const deleteTemplateMutation = useDeleteAdminCourseTemplateMutation()
|
||||
const changeTemplateStatusMutation = useChangeAdminCourseTemplateStatusMutation()
|
||||
const deleteMutation = useDeleteAdminCourseMutation()
|
||||
// Backend has no dedicated status endpoint — PATCH /courses/:id with { isActive }.
|
||||
const updateMutation = useUpdateAdminCourseMutation()
|
||||
|
||||
const deleteOfferedMutation = useDeleteAdminCourseMutation()
|
||||
const changeOfferedStatusMutation = useChangeAdminCourseStatusMutation()
|
||||
|
||||
const onAskDeleteTemplate = (course) => {
|
||||
const onAskDelete = (course) => {
|
||||
openModal('ConfirmModal', {
|
||||
title: `حذف ${course.title}`,
|
||||
message: `بعد از حذف امکان بازگشت وجود ندارد، آیا مطمئن هستید که میخواهید <strong>${course.title}</strong> را حذف کنید؟`,
|
||||
onConfirm: () => deleteTemplateMutation.mutate(course.id, { onSuccess: invalidateTemplates }),
|
||||
onConfirm: () => deleteMutation.mutate(course.id, { onSuccess: invalidate }),
|
||||
})
|
||||
}
|
||||
|
||||
const onAskDeleteOffered = (course) => {
|
||||
openModal('ConfirmModal', {
|
||||
title: `حذف ${course.title}`,
|
||||
message: `بعد از حذف امکان بازگشت وجود ندارد، آیا مطمئن هستید که میخواهید <strong>${course.title}</strong> را حذف کنید؟`,
|
||||
onConfirm: () => deleteOfferedMutation.mutate(course.id, { onSuccess: invalidateOffered }),
|
||||
})
|
||||
const onChangeStatus = ({ id, isActive }) => {
|
||||
updateMutation.mutate({ id, payload: { isActive } }, { onSuccess: invalidate })
|
||||
}
|
||||
|
||||
const onChangeTemplateStatus = ({ id, isActive }) => {
|
||||
changeTemplateStatusMutation.mutate(
|
||||
{ id, payload: { isActiveByDefault: isActive } },
|
||||
{ onSuccess: invalidateTemplates }
|
||||
)
|
||||
const syncRouteTermId = (termId) => {
|
||||
if (!termId) return
|
||||
activeTab.value = 'offered'
|
||||
offeredFilters.value = { ...offeredFilters.value, termId }
|
||||
resetOfferedPagination()
|
||||
}
|
||||
|
||||
const onChangeOfferedStatus = ({ id, isActive }) => {
|
||||
changeOfferedStatusMutation.mutate(
|
||||
{ id, payload: { isActive } },
|
||||
{ onSuccess: invalidateOffered }
|
||||
)
|
||||
}
|
||||
onMounted(() => syncRouteTermId(routeTermId.value))
|
||||
watch(routeTermId, (val) => syncRouteTermId(val))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { array, boolean, mixed, number, object, string } from 'yup'
|
||||
|
||||
export const courseTemplateSchema = object().shape({
|
||||
export const courseSchema = object().shape({
|
||||
title: string().required().min(3).max(255),
|
||||
defaultTeacherId: mixed().required(),
|
||||
defaultCapacity: number().required().min(1),
|
||||
teacherId: mixed().required(),
|
||||
capacity: number().required().min(1),
|
||||
sessionsCount: number().required().min(1),
|
||||
prerequisites: array().nullable().default([]),
|
||||
contentType: string().oneOf(['video', 'voice', 'text']).required(),
|
||||
contentMediaId: number().nullable().notRequired(),
|
||||
description: string().nullable().notRequired(),
|
||||
termId: string().nullable(),
|
||||
termId: mixed().nullable(),
|
||||
isActive: boolean().nullable().notRequired(),
|
||||
})
|
||||
|
||||
export const offeredCourseSchema = object().shape({
|
||||
termId: string().required(),
|
||||
templateId: string().required(),
|
||||
title: string().required(),
|
||||
capacity: string().required(),
|
||||
imageId: string().nullable().notRequired(),
|
||||
|
||||
Reference in New Issue
Block a user