From 1c665830b0959d6545436fea313d837ceb90a1ad Mon Sep 17 00:00:00 2001 From: sajjadtalkhabi <97734061+sajjadtalkhabi@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:41:38 +0330 Subject: [PATCH] fix --- src/constants/fields.js | 2 +- src/enums/index.js | 17 +- .../courses/components/CoursesFilters.vue | 6 +- .../components/modals/CourseDetailsModal.vue | 10 +- .../admin/courses/pages/CourseFormPage.vue | 90 ++++---- .../admin/courses/pages/CoursesListPage.vue | 119 +--------- src/features/admin/courses/schema/index.js | 7 +- .../admin/sessions/components/SessionItem.vue | 8 +- .../components/modals/SessionDetailsModal.vue | 2 +- .../admin/sessions/pages/SessionFormPage.vue | 14 +- .../admin/users/components/UsersFilters.vue | 20 +- .../admin/users/components/UsersTable.vue | 13 +- .../admin/users/pages/UsersListPage.vue | 2 +- .../exams/components/StudentExamItem.vue | 14 +- .../lessons/components/StudentLessonItem.vue | 125 +++++----- .../pages/StudentLessonDetailsPage.vue | 136 ++++++----- .../student/profile/pages/EditProfilePage.vue | 213 ++++++++++-------- src/features/student/profile/schema/index.js | 20 +- .../components/StudentSessionItem.vue | 49 +--- .../pages/StudentSessionDetailsPage.vue | 171 ++++++-------- src/services/api/endpoints.js | 1 + src/services/api/student-exams.js | 2 + src/services/api/student-sessions.js | 2 + src/services/query/common.js | 66 +++--- src/services/query/student-exams.js | 23 +- src/services/query/student-sessions.js | 29 ++- src/utils/error-handler.js | 21 ++ 27 files changed, 565 insertions(+), 617 deletions(-) diff --git a/src/constants/fields.js b/src/constants/fields.js index 5750cd8..913180c 100644 --- a/src/constants/fields.js +++ b/src/constants/fields.js @@ -1,7 +1,7 @@ export const fields = { mobile: 'شماره موبایل', phoneNumber: 'شماره موبایل', - phone: 'تلفن ثابت', + phone: 'شماره موبایل', password: 'رمز عبور', passwordConfirmation: 'تکرار رمز عبور', newPassword: 'رمز عبور جدید', diff --git a/src/enums/index.js b/src/enums/index.js index 761e850..75f4168 100644 --- a/src/enums/index.js +++ b/src/enums/index.js @@ -5,8 +5,11 @@ export const getEnumObject = (enumArray, key = 'value', value = 'label') => }, {}) export const ROLES = Object.freeze([ - { value: 'admin', label: 'مدیر' }, - { value: 'student', label: 'دانش‌آموز' }, + { value: 'admin', label: 'مدیر', changeable: false }, + { value: 'student', label: 'دانشجو', changeable: true }, + { value: 'teacher', label: 'استاد', changeable: true }, + { value: 'counselor', label: 'مشاور', changeable: true }, + { value: 'missionary', label: 'مبلغ', changeable: true }, ]) export const LOGIN_STEPS = Object.freeze({ @@ -54,15 +57,9 @@ export const GENDER = Object.freeze({ female: 'خانم', }) -export const ROLE_LABELS = Object.freeze({ - admin: 'مدیر', - counselor: 'مشاور', - missionary: 'مبلغ', - student: 'دانشجو', - teacher: 'استاد', -}) +export const ROLE_LABELS = Object.freeze(Object.fromEntries(ROLES.map((r) => [r.value, r.label]))) -export const CHANGEABLE_ROLES = Object.freeze(['student', 'teacher', 'missionary', 'counselor']) +export const CHANGEABLE_ROLES = Object.freeze(ROLES.filter((r) => r.changeable).map((r) => r.value)) export const EDUCATION_STATUS = Object.freeze({ seminary_student: 'طلبه', diff --git a/src/features/admin/courses/components/CoursesFilters.vue b/src/features/admin/courses/components/CoursesFilters.vue index b1c56fd..7690961 100644 --- a/src/features/admin/courses/components/CoursesFilters.vue +++ b/src/features/admin/courses/components/CoursesFilters.vue @@ -7,7 +7,6 @@ ({ title: '', termId: '', status: '', fromDate: '', toDate: '' }), }, - showTermFilter: { type: Boolean, default: false }, }) const emit = defineEmits(['update:modelValue', 'apply', 'reset']) @@ -100,9 +98,7 @@ const statusOptions = [ const termFilters = ref({}) const termPagination = ref({ page: 1, perPage: 10 }) -const { data: termsResponse } = useAdminTermsListQuery(termFilters, termPagination, { - enabled: () => props.showTermFilter, -}) +const { data: termsResponse } = useAdminTermsListQuery(termFilters, termPagination) const termOptions = computed(() => termsResponse.value?.data ?? []) const hasFilters = computed(() => diff --git a/src/features/admin/courses/components/modals/CourseDetailsModal.vue b/src/features/admin/courses/components/modals/CourseDetailsModal.vue index 43e36e6..b7e1817 100644 --- a/src/features/admin/courses/components/modals/CourseDetailsModal.vue +++ b/src/features/admin/courses/components/modals/CourseDetailsModal.vue @@ -20,7 +20,7 @@
- +
@@ -99,11 +99,9 @@ const { data: course, isLoading } = useAdminCourseQuery(courseId, { enabled: () => !!courseId.value, }) -const teacherName = computed(() => { - const t = course.value?.teacher - if (!t) return '—' - return `${t.firstName || ''} ${t.lastName || ''}`.trim() || t.fullName || '—' -}) +const prerequisiteCourses = computed(() => + course.value.prerequisiteCourses.map((course_) => course_.title).join(' ,') +) const sessionsFilters = computed(() => ({ courseId: courseId.value })) const { pagination: sessionsPagination, setPage: setSessionsPage } = usePagination({ diff --git a/src/features/admin/courses/pages/CourseFormPage.vue b/src/features/admin/courses/pages/CourseFormPage.vue index bfc8372..3865ca1 100644 --- a/src/features/admin/courses/pages/CourseFormPage.vue +++ b/src/features/admin/courses/pages/CourseFormPage.vue @@ -65,28 +65,30 @@
+
+
+ -
-
-
+
({ - value, - label, -})) - const { validate, validateAt, errors } = useYup(courseSchema) const teacherSearch = ref('') @@ -203,6 +199,11 @@ const teacherOptions = computed(() => { })) }) +const termFilters = ref({}) +const termPagination = ref({ page: 1, perPage: 100 }) +const { data: termsResponse } = useAdminTermsListQuery(termFilters, termPagination) +const termOptions = computed(() => termsResponse.value?.data ?? []) + const prereqSearch = ref('') const prereqFilters = computed(() => ({ title: prereqSearch.value })) const prereqPagination = ref({ page: 1, perPage: 10 }) @@ -211,13 +212,14 @@ const selectedPrereqs = ref([]) const prerequisiteOptions = computed(() => { const base = prereqsResponse.value?.data ?? [] const extras = selectedPrereqs.value.filter((p) => !base.some((b) => b.id === p.id)) - return [...base, ...extras] + const merged = [...base, ...extras] + return courseId.value ? merged.filter((c) => c.id !== courseId.value) : merged }) const searchTeachers = useDebounce((q) => { teacherSearch.value = q || '' }, 400) -const searchPrerequisites = useDebounce((q) => { +const searchPrerequisiteCourses = useDebounce((q) => { prereqSearch.value = q || '' }, 400) @@ -225,25 +227,29 @@ const { data: existingCourse } = useAdminCourseQuery(courseId, { enabled: () => !!courseId.value, }) -watch(existingCourse, (course) => { - if (!course) return - 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) +watch( + existingCourse, + (course) => { + if (!course) return + const teacher = course.teacher + if (teacher) selectedTeacher.value = teacher + const prereqs = Array.isArray(course.prerequisiteCourseIds) ? course.prerequisiteCourseIds : [] + selectedPrereqs.value = prereqs.map((p) => p.course || p).filter((c) => c?.id) - form.value = { - title: course.title || '', - teacherId: teacher?.id || course.teacherId || '', - capacity: course.capacity ?? '', - prerequisites: prereqs.map((p) => p.courseId ?? p.id).filter(Boolean), - contentType: course.contentType || '', - description: course.description || '', - coverMediaId: course.coverMediaId || null, - termId: course.termId ?? termId.value, - } - if (course.coverUrl) image.value = { url: course.coverUrl } -}) + form.value = { + title: course.title || '', + teacherId: teacher?.id || course.teacherId || '', + capacity: course.capacity ?? '', + prerequisiteCourseIds: prereqs.map((p) => p.courseId ?? p.id).filter(Boolean), + contentType: course.contentType || '', + description: course.description || '', + coverMediaId: course.coverMediaId || null, + termId: course.termId ?? termId.value, + } + if (course.coverUrl) image.value = { url: course.coverUrl } + }, + { immediate: true } +) const uploadMutation = useUploadMediaMutation() diff --git a/src/features/admin/courses/pages/CoursesListPage.vue b/src/features/admin/courses/pages/CoursesListPage.vue index 25dd99a..4e13eb0 100644 --- a/src/features/admin/courses/pages/CoursesListPage.vue +++ b/src/features/admin/courses/pages/CoursesListPage.vue @@ -10,19 +10,14 @@ - + - - diff --git a/src/features/student/profile/pages/EditProfilePage.vue b/src/features/student/profile/pages/EditProfilePage.vue index cdbcf8b..14feeff 100644 --- a/src/features/student/profile/pages/EditProfilePage.vue +++ b/src/features/student/profile/pages/EditProfilePage.vue @@ -27,30 +27,18 @@
- - -
-
-
+
+
+ + + +
-
- -
- +
-
- - - -
+
+ + + +
+
({ value, label })) -const { data: provinces = ref([]) } = useGetProvincesQuery() - -const provinceIdRef = ref(null) -const { data: cities = ref([]) } = useGetCitiesOfProvinceQuery(provinceIdRef, { - enabled: () => !!provinceIdRef.value, -}) - const initialNationalCode = ref('') const initialPhoneNumber = ref('') const form = ref({ - firstName: '', - lastName: '', + name: '', birthDate: '', nationalCode: '', - maritalStatus: '', + marriageStatus: '', gender: '', - bio: '', - phoneNumber: '', + phone: '', provinceId: '', cityId: '', address: '', - avatarId: null, + avatarMediaId: null, +}) + +const passwordForm = ref({ password: '', passwordConfirmation: '', }) @@ -278,44 +275,47 @@ const avatar = ref(null) const schema = studentProfileSchema const { validate, validateAt, errors } = useYup(schema) +const { + validate: validatePassword, + validateAt: validateAtPassword, + errors: passwordErrors, + resetErrors: resetPasswordErrors, + setError: setPasswordError, +} = useYup(studentResetPasswordSchema, () => ({ + ...passwordForm.value, + phone: form.value.phone, +})) + +const { data: provinces = ref([]) } = useGetProvincesQuery() + +const provinceIdRef = computed(() => form.value.provinceId) +const { data: cities = ref([]) } = useGetCitiesOfProvinceQuery(provinceIdRef, { + enabled: () => !!provinceIdRef.value, +}) const onProvinceChange = () => { - provinceIdRef.value = form.value.provinceId form.value.cityId = '' } -watch( - () => form.value.provinceId, - (val) => { - provinceIdRef.value = val - } -) - const { data: profile } = useGetMeQuery() watch(profile, (user) => { if (!user) return - const nameParts = String(user.name || '') - .trim() - .split(/\s+/) - .filter(Boolean) - const firstName = user.firstName || nameParts[0] || '' - const lastName = user.lastName || nameParts.slice(1).join(' ') || '' + const fullName = + user.name || [user.firstName, user.lastName].filter(Boolean).join(' ').trim() || '' const phone = user.phone || user.phoneNumber || '' form.value = { ...form.value, - firstName, - lastName, + name: fullName, birthDate: user.profile?.birthDate || '', nationalCode: user.nationalCode || '', - maritalStatus: user.profile?.maritalStatus || '', + marriageStatus: user.profile?.marriageStatus || '', gender: user.profile?.gender || '', - bio: user.profile?.bio || '', - phoneNumber: phone, + phone, provinceId: user?.province?.id || '', cityId: user?.city?.id || '', address: user?.address || '', - avatarId: user.profile?.avatarId || null, + avatarMediaId: user.profile?.avatarMediaId || null, } initialNationalCode.value = user.nationalCode || '' initialPhoneNumber.value = phone @@ -330,7 +330,7 @@ const onAvatarCropped = async (file) => { const response = await uploadMutation.mutateAsync(formData) const payload = response?.data ?? response avatar.value = { url: payload?.url, uploadId: payload?.uploadId, ...payload } - form.value.avatarId = payload?.uploadId || payload?.id + form.value.avatarMediaId = payload?.uploadId || payload?.id } catch { /* handled globally */ } @@ -339,23 +339,20 @@ const onAvatarCropped = async (file) => { const onAvatarError = (msg) => toast.error(msg) const updateMutation = useUpdateProfileMutation() +const resetPasswordMutation = useResetPasswordMutation() const onSubmit = async () => { const { isValid, payload } = await validate(form.value) if (!isValid) return - // Backend PATCH /me accepts: name, email, phone, password (+confirmation), - // avatar_media_id. Other UI-only fields (province/city/address/birthDate/…) - // are not in the backend Postman doc and are dropped here. - const name = [payload.firstName, payload.lastName].filter(Boolean).join(' ').trim() + // Backend PATCH /me accepts: name, email, phone, avatar_media_id. + // Other UI-only fields (province/city/address/birthDate/…) are not in + // the backend Postman doc and are dropped here. Password is handled + // separately via POST /reset-password. const body = { - ...(name ? { name } : {}), - ...(payload.phoneNumber ? { phone: payload.phoneNumber } : {}), + ...(payload.name ? { name: payload.name } : {}), + ...(payload.phone ? { phone: payload.phone } : {}), ...(payload.email ? { email: payload.email } : {}), - ...(payload.avatarId ? { avatarMediaId: payload.avatarId } : {}), - } - if (payload.password) { - body.password = payload.password - body.passwordConfirmation = payload.passwordConfirmation + ...(payload.avatarMediaId ? { avatarMediaId: payload.avatarMediaId } : {}), } await updateMutation.mutateAsync(body) await queryClient.invalidateQueries({ queryKey: authKeys.me() }) @@ -363,6 +360,25 @@ const onSubmit = async () => { router.push({ name: 'student-dashboard' }) } +const onResetPassword = async () => { + resetPasswordErrors() + const { isValid, payload } = await validatePassword({ + phone: form.value.phone, + password: passwordForm.value.password, + passwordConfirmation: passwordForm.value.passwordConfirmation, + }) + if (!isValid) return + try { + await resetPasswordMutation.mutateAsync(payload) + passwordForm.value.password = '' + passwordForm.value.passwordConfirmation = '' + resetPasswordErrors() + toast.success('رمز عبور با موفقیت تغییر یافت') + } catch (error) { + applyValidationErrors(error, setPasswordError) + } +} + const onCancel = () => router.push({ name: 'student-dashboard' }) @@ -457,6 +473,13 @@ const onCancel = () => router.push({ name: 'student-dashboard' }) margin-top: 1rem; } + &__password-actions { + display: flex; + justify-content: flex-end; + margin-top: 0.5rem; + margin-bottom: 1rem; + } + &__btn-cancel { min-width: 9rem; } @@ -464,5 +487,9 @@ const onCancel = () => router.push({ name: 'student-dashboard' }) &__btn-submit { min-width: 12rem; } + + &__btn-reset { + min-width: 12rem; + } } diff --git a/src/features/student/profile/schema/index.js b/src/features/student/profile/schema/index.js index e591604..2bf594d 100644 --- a/src/features/student/profile/schema/index.js +++ b/src/features/student/profile/schema/index.js @@ -1,22 +1,24 @@ import { object, string } from 'yup' +import { passwordRule } from '@/constants/rules/passwordRule' import { phoneNumberRule } from '@/constants/rules/phoneNumberRule' import { nationalCodeRule } from '@/constants/rules/nationalCodeRule' -import { optionalPasswordRule } from '@/constants/rules/passwordRule' -import { optionalPasswordConfirmationRule } from '@/constants/rules/passwordConfirmationRule' +import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule' export const studentProfileSchema = object().shape({ - firstName: string().required().min(2), - lastName: string().required().min(2), - phoneNumber: phoneNumberRule, + name: string().required().min(2), + phone: phoneNumberRule, nationalCode: nationalCodeRule, birthDate: string().nullable().notRequired(), - maritalStatus: string().nullable().notRequired(), + marriageStatus: string().nullable().notRequired(), gender: string().nullable().notRequired(), - bio: string().nullable().notRequired(), provinceId: string().nullable().notRequired(), cityId: string().nullable().notRequired(), address: string().nullable().notRequired(), - password: optionalPasswordRule, - passwordConfirmation: optionalPasswordConfirmationRule, +}) + +export const studentResetPasswordSchema = object().shape({ + phone: phoneNumberRule, + password: passwordRule, + passwordConfirmation: passwordConfirmationRule, }) diff --git a/src/features/student/sessions/components/StudentSessionItem.vue b/src/features/student/sessions/components/StudentSessionItem.vue index b2e83b8..c85ba18 100644 --- a/src/features/student/sessions/components/StudentSessionItem.vue +++ b/src/features/student/sessions/components/StudentSessionItem.vue @@ -10,24 +10,12 @@
- - دیده شده - - - - ارسال تکلیف - - - - دارای آزمون - - - - جلسه آنلاین - + تکمیل شده + دیده شده + + دارای تکلیف + دارای آزمون + جلسه آنلاین
@@ -46,6 +34,7 @@