diff --git a/src/components/form/SelectField.vue b/src/components/form/SelectField.vue
index d7305a8..30e93ab 100644
--- a/src/components/form/SelectField.vue
+++ b/src/components/form/SelectField.vue
@@ -371,12 +371,15 @@ onBeforeUnmount(() => unmountFloating())
align-items: center;
gap: 0.5rem;
width: 100%;
- padding: 0.25rem 0.75rem;
+ min-height: 2.5rem;
+ padding: 0.5rem 0.75rem;
border: none;
border-radius: 1.5rem;
background: transparent;
text-align: right;
font-family: var(--font-family-fa);
+ font-weight: 400;
+ line-height: 1.5;
cursor: pointer;
transition: background-color 0.15s;
diff --git a/src/enums/index.js b/src/enums/index.js
index b71dc3f..21f7767 100644
--- a/src/enums/index.js
+++ b/src/enums/index.js
@@ -53,6 +53,8 @@ export const USER_STATUS = Object.freeze({
export const MARITAL_STATUS = Object.freeze({
single: 'مجرد',
married: 'متاهل',
+ divorced: 'مطلقه',
+ widowed: 'بیوه',
})
export const GENDER = Object.freeze({
diff --git a/src/features/admin/terms/components/modals/AttachCourseToTermModal.vue b/src/features/admin/terms/components/modals/AttachCourseToTermModal.vue
index 933261f..ec03c25 100644
--- a/src/features/admin/terms/components/modals/AttachCourseToTermModal.vue
+++ b/src/features/admin/terms/components/modals/AttachCourseToTermModal.vue
@@ -163,8 +163,6 @@ const invalidate = () => {
}
const onAttach = async (template) => {
- console.log(template)
-
if (!termId.value) return
pendingId.value = template.id
try {
diff --git a/src/features/auth/components/studentRegister/EducationalInformation.vue b/src/features/auth/components/studentRegister/EducationalInformation.vue
index fd8a942..beb5a71 100644
--- a/src/features/auth/components/studentRegister/EducationalInformation.vue
+++ b/src/features/auth/components/studentRegister/EducationalInformation.vue
@@ -89,9 +89,9 @@
diff --git a/src/features/auth/pages/StudentRegisterPage.vue b/src/features/auth/pages/StudentRegisterPage.vue
index c2b7a9d..c5baec6 100644
--- a/src/features/auth/pages/StudentRegisterPage.vue
+++ b/src/features/auth/pages/StudentRegisterPage.vue
@@ -2,16 +2,26 @@
-
+
+ در حال بازیابی اطلاعات ثبتنام…
+
+
@@ -23,10 +33,10 @@
import useModal from '@/composables/useModal'
import { computed, markRaw, watch } from 'vue'
import { STUDENT_REGISTRATION } from '@/enums'
-import { useGetRegisterDataQuery } from '@/services/query/auth'
import SideBarSteps from '@/features/auth/components/studentRegister/SideBarSteps.vue'
import SuccessModal from '@/features/auth/components/studentRegister/SuccessModal.vue'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
+import { useGetRegisterDataQuery, useGetRegistrationVideoQuery } from '@/services/query/auth'
import PersonalInformation from '@/features/auth/components/studentRegister/PersonalInformation.vue'
import SkillAssessmentPart1 from '@/features/auth/components/studentRegister/SkillAssessmentPart1.vue'
import SkillAssessmentPart2 from '@/features/auth/components/studentRegister/SkillAssessmentPart2.vue'
@@ -41,15 +51,21 @@ import ProfessionalInformation from '@/features/auth/components/studentRegister/
const store = useStudentRegistrationStore()
const { isModal, openModal } = useModal()
-const { data: registerData } = useGetRegisterDataQuery()
+const { data: registerData, isPending: isRegistrationDataPending } = useGetRegisterDataQuery()
watch(
registerData,
(value) => {
if (value && Object.keys(value).length > 0) store.hydrateFromRegisterData(value)
},
- { immediate: true }
+ { immediate: true, flush: 'sync' }
)
+const { data: registrationMedia } = useGetRegistrationVideoQuery({ retry: false })
+watch(registrationMedia, (value) => store.setRegistrationMedia(value), {
+ immediate: true,
+ flush: 'sync',
+})
+
const STEP_TO_COMPONENT = {
[STUDENT_REGISTRATION.PERSONAL_INFORMATION]: markRaw(PersonalInformation),
[STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION]: markRaw(EducationalInformation),
@@ -63,26 +79,6 @@ const STEP_TO_COMPONENT = {
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7]: markRaw(SkillAssessmentPart7),
}
-const STEP_TO_TAB = {
- [STUDENT_REGISTRATION.PERSONAL_INFORMATION]: 'personal',
- [STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION]: 'educational',
- [STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION]: 'professional',
- [STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1]: 'skill',
- [STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_2]: 'skill',
- [STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_3]: 'skill',
- [STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_4]: 'skill',
- [STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_5]: 'skill',
- [STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6]: 'skill',
- [STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7]: 'skill',
-}
-
-const TAB_TO_STEP = {
- personal: STUDENT_REGISTRATION.PERSONAL_INFORMATION,
- educational: STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION,
- professional: STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION,
- skill: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1,
-}
-
const STEP_LABELS = {
[STUDENT_REGISTRATION.PERSONAL_INFORMATION]: {
title: 'اطلاعـــــات شخصـــــی',
@@ -126,14 +122,33 @@ const STEP_LABELS = {
},
}
-const tabs = [
- { key: 'personal', icon: 'user' },
- { key: 'educational', icon: 'book' },
- { key: 'professional', icon: 'file' },
- { key: 'skill', icon: 'check-square' },
+const STEP_NAVIGATION = [
+ { step: STUDENT_REGISTRATION.PERSONAL_INFORMATION, title: 'مشخصات فردی', icon: 'user' },
+ { step: STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION, title: 'اطلاعات تحصیلی', icon: 'book' },
+ { step: STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION, title: 'فعالیتهای تبلیغی', icon: 'file' },
+ { step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1, title: 'مهارت ۱', icon: 'check-square' },
+ { step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_2, title: 'مهارت ۲', icon: 'check-square' },
+ { step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_3, title: 'مهارت ۳', icon: 'check-square' },
+ { step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_4, title: 'مهارت ۴', icon: 'check-square' },
+ { step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_5, title: 'مهارت ۵', icon: 'check-square' },
+ { step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6, title: 'مهارت ۶', icon: 'check-square' },
+ { step: STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7, title: 'مهارت ۷', icon: 'check-square' },
]
-const activeTab = computed(() => STEP_TO_TAB[store.currentStep] || 'personal')
+const tabs = computed(() =>
+ STEP_NAVIGATION.map((tab) => ({
+ ...tab,
+ completed: store.completedSteps.includes(tab.step),
+ disabled: !store.canGoToStep(tab.step),
+ }))
+)
+
+const totalSteps = STEP_NAVIGATION.length
+const currentStepNumber = computed(() => {
+ const index = STEP_NAVIGATION.findIndex((item) => item.step === store.currentStep)
+ return index >= 0 ? index + 1 : 1
+})
+const progressPercent = computed(() => Math.round((currentStepNumber.value / totalSteps) * 100))
const currentComponent = computed(
() =>
@@ -145,12 +160,8 @@ const stepLabel = computed(
() => STEP_LABELS[store.currentStep] || STEP_LABELS[STUDENT_REGISTRATION.PERSONAL_INFORMATION]
)
-const goToStep = (tabKey) => {
- const targetStep = TAB_TO_STEP[tabKey]
- if (!targetStep) return
- if (store.canGoToStep(targetStep)) {
- store.updateCurrentStep(targetStep)
- }
+const goToStep = (step) => {
+ store.updateCurrentStep(step)
}
watch(
@@ -188,9 +199,8 @@ watch(
width: 100%;
@media (min-width: 1024px) {
- width: 8.333%;
- min-width: 5rem;
- max-width: 6.5rem;
+ width: 13.5rem;
+ min-width: 13.5rem;
}
}
@@ -206,23 +216,97 @@ watch(
}
&__heading {
+ margin-bottom: 1.25rem;
+ }
+
+ &__step-meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 0.375rem;
+ color: #6f6f6f;
+ font-family: var(--font-family-number-fa);
+ font-size: 0.875rem;
+ font-weight: 500;
+ }
+
+ &__progress {
+ width: 100%;
+ height: 0.35rem;
margin-bottom: 1rem;
+ overflow: hidden;
+ border-radius: 999px;
+ background: #f2f2f2;
+
+ span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: var(--color-primary);
+ transition: width 0.2s ease;
+ }
}
&__title {
font-family: var(--font-family-fa);
- font-weight: 500;
- font-size: 1.125rem;
- color: #4c4c4c;
- margin: 0 0 0.25rem;
+ font-weight: 600;
+ font-size: 1.375rem;
+ color: #3f3f3f;
+ margin: 0 0 0.375rem;
}
&__desc {
font-family: var(--font-family-fa);
font-weight: 400;
- font-size: 0.75rem;
- color: #adadad;
+ font-size: 0.9375rem;
+ line-height: 1.8;
+ color: #747474;
margin: 0;
}
+
+ &__loading {
+ padding: 3rem 1rem;
+ color: #747474;
+ font-family: var(--font-family-fa);
+ font-size: 1rem;
+ font-weight: 500;
+ text-align: center;
+ }
+
+ &__main :deep(.text-field__label),
+ &__main :deep(.textarea-field__label),
+ &__main :deep(.select-field__label),
+ &__main :deep(.datepicker-field__label) {
+ color: #626262;
+ font-size: 0.9375rem;
+ font-weight: 400;
+ }
+
+ &__main :deep(.text-field__input),
+ &__main :deep(.textarea-field__input),
+ &__main :deep(.select-field__trigger),
+ &__main :deep(.datepicker-field__trigger) {
+ min-height: 2.6rem;
+ font-size: 1rem;
+ font-weight: 400;
+ }
+
+ &__main :deep(.line-title__fa) {
+ font-size: 1rem;
+ font-weight: 600;
+ }
+
+ &__main :deep(.line-title__en) {
+ font-size: 0.8125rem;
+ font-weight: 400;
+ }
+
+ &__main :deep(.text-field__error),
+ &__main :deep(.textarea-field__error),
+ &__main :deep(.select-field__error),
+ &__main :deep(.datepicker-field__error) {
+ font-size: 0.8125rem;
+ font-weight: 400;
+ }
}
diff --git a/src/features/auth/schema/student-register.js b/src/features/auth/schema/student-register.js
index 133987d..7f60ad9 100644
--- a/src/features/auth/schema/student-register.js
+++ b/src/features/auth/schema/student-register.js
@@ -1,6 +1,5 @@
import { array, mixed, number, object, string } from 'yup'
-import { phoneNumberRule } from '@/constants/rules/phoneNumberRule'
import { nationalCodeRule } from '@/constants/rules/nationalCodeRule'
const requiredString = (min = 0) => {
@@ -16,7 +15,9 @@ export const personalInformationSchema = object().shape({
nationalCode: nationalCodeRule,
maritalStatus: string().required(),
gender: string().required(),
- phone: phoneNumberRule,
+ // The verified phone is read-only here and may be returned in international
+ // format (for example +98912...). Do not re-validate it as a local 09 number.
+ phone: string().required(),
provinceId: mixed().required(),
cityId: mixed().required(),
address: requiredString(10),
@@ -118,11 +119,11 @@ export const skillAssessmentPart5Schema = object().shape({
})
export const skillAssessmentPart6Schema = object().shape({
- faithProductionAudio: mixed().required(),
+ faithProductionAudio: mixed().nullable().notRequired(),
faithProductionId: mixed().notRequired(),
})
export const skillAssessmentPart7Schema = object().shape({
- leaderMessageVideo: mixed().required(),
+ leaderMessageVideo: mixed().nullable().notRequired(),
leaderMessageId: mixed().notRequired(),
})
diff --git a/src/features/auth/store/student-registration.js b/src/features/auth/store/student-registration.js
index ba9fb81..ab3a99c 100644
--- a/src/features/auth/store/student-registration.js
+++ b/src/features/auth/store/student-registration.js
@@ -145,6 +145,21 @@ const readPersisted = () => {
}
}
+const normalizeCompletedSteps = (steps) =>
+ Array.isArray(steps) ? STUDENT_REGISTRATION_ORDER.filter((step) => steps.includes(step)) : []
+
+const firstIncompleteIndex = (steps) => {
+ const index = STUDENT_REGISTRATION_ORDER.findIndex((step) => !steps.includes(step))
+ return index === -1 ? STUDENT_REGISTRATION_ORDER.length - 1 : index
+}
+
+const resolveCurrentStep = (candidate, completed) => {
+ const candidateIndex = STUDENT_REGISTRATION_ORDER.indexOf(candidate)
+ const reachableIndex = firstIncompleteIndex(completed)
+ if (candidateIndex >= 0 && candidateIndex <= reachableIndex) return candidate
+ return STUDENT_REGISTRATION_ORDER[reachableIndex]
+}
+
export const useStudentRegistrationStore = defineStore('studentRegistration', () => {
const persisted = readPersisted() || {}
const initial = { ...emptyState(), ...persisted }
@@ -160,8 +175,8 @@ export const useStudentRegistrationStore = defineStore('studentRegistration', ()
const skill5 = ref({ ...emptyState().skill5, ...persisted.skill5 })
const skill6 = ref({ ...emptyState().skill6, ...persisted.skill6 })
const skill7 = ref({ ...emptyState().skill7, ...persisted.skill7 })
- const currentStep = ref(initial.currentStep)
- const completedSteps = ref(initial.completedSteps || [])
+ const completedSteps = ref(normalizeCompletedSteps(initial.completedSteps))
+ const currentStep = ref(resolveCurrentStep(initial.currentStep, completedSteps.value))
const registrationMedia = ref(initial.registrationMedia || {})
const sectionRefs = {
@@ -207,18 +222,33 @@ export const useStudentRegistrationStore = defineStore('studentRegistration', ()
)
const markStepAsCompleted = (stepKey) => {
- if (!completedSteps.value.includes(stepKey)) {
+ if (STUDENT_REGISTRATION_ORDER.includes(stepKey) && !completedSteps.value.includes(stepKey)) {
completedSteps.value = [...completedSteps.value, stepKey]
}
}
const updateCurrentStep = (stepKey) => {
- if (stepKey) currentStep.value = stepKey
+ if (canGoToStep(stepKey)) currentStep.value = stepKey
+ }
+
+ const updateSection = (section, value) => {
+ const target = sectionRefs[section]
+ if (!target || !value || typeof value !== 'object') return
+ target.value = { ...target.value, ...value }
+ }
+
+ const setRegistrationMedia = (value) => {
+ if (!value || typeof value !== 'object') return
+ registrationMedia.value = { ...registrationMedia.value, ...value }
}
const goToNextStep = () => {
const idx = STUDENT_REGISTRATION_ORDER.indexOf(currentStep.value)
- if (idx >= 0 && idx < STUDENT_REGISTRATION_ORDER.length - 1) {
+ if (
+ idx >= 0 &&
+ idx < STUDENT_REGISTRATION_ORDER.length - 1 &&
+ completedSteps.value.includes(currentStep.value)
+ ) {
currentStep.value = STUDENT_REGISTRATION_ORDER[idx + 1]
}
}
@@ -229,9 +259,8 @@ export const useStudentRegistrationStore = defineStore('studentRegistration', ()
}
const canGoToStep = (stepKey) => {
- const currentIndex = STUDENT_REGISTRATION_ORDER.indexOf(currentStep.value)
const targetIndex = STUDENT_REGISTRATION_ORDER.indexOf(stepKey)
- return targetIndex <= currentIndex + 1
+ return targetIndex >= 0 && targetIndex <= firstIncompleteIndex(completedSteps.value)
}
const buildCompletePayload = () => ({
@@ -307,6 +336,8 @@ export const useStudentRegistrationStore = defineStore('studentRegistration', ()
registrationMedia,
markStepAsCompleted,
updateCurrentStep,
+ updateSection,
+ setRegistrationMedia,
goToNextStep,
goToPrevStep,
canGoToStep,
diff --git a/src/features/student/sessions/pages/StudentSessionDetailsPage.vue b/src/features/student/sessions/pages/StudentSessionDetailsPage.vue
index 9ff3de9..77fa8fd 100644
--- a/src/features/student/sessions/pages/StudentSessionDetailsPage.vue
+++ b/src/features/student/sessions/pages/StudentSessionDetailsPage.vue
@@ -31,10 +31,7 @@
/>
-
+
diff --git a/src/services/mock/fixtures/me.js b/src/services/mock/fixtures/me.js
index 532c9ae..6cbacd8 100644
--- a/src/services/mock/fixtures/me.js
+++ b/src/services/mock/fixtures/me.js
@@ -18,7 +18,7 @@ export const currentMe = {
lastName: 'محمدی',
fullName: 'سجاد محمدی',
phoneNumber: '09123456789',
- nationalCode: '0079827498',
+ nationalCode: '0079827497',
status: 'approved',
address: {
address: 'خیابان آزادی، پلاک ۱۲',
diff --git a/src/services/mock/routes/auth.js b/src/services/mock/routes/auth.js
index 0d03b28..343d953 100644
--- a/src/services/mock/routes/auth.js
+++ b/src/services/mock/routes/auth.js
@@ -4,6 +4,7 @@ import { endpoints } from '@/services/api/endpoints'
import { currentMe } from '@/services/mock/fixtures/me'
const fakeToken = 'mock-token-1234567890'
+let registerData = {}
register('POST', endpoints.login, () => ({
data: { user: currentMe, token: fakeToken },
@@ -86,6 +87,13 @@ register('POST', endpoints.completeProfile, ({ data }) => ({
data: { user: { ...currentMe, ...data } },
}))
+register('GET', endpoints.registerData, () => ({ data: registerData }))
+
+register('POST', endpoints.registerData, ({ data }) => {
+ registerData = { ...registerData, ...data?.data }
+ return { data: registerData }
+})
+
register('GET', endpoints.getRegistrationQuestionVideo, () => ({
data: { url: '', id: 0 },
}))
diff --git a/src/services/query/auth.js b/src/services/query/auth.js
index 9bf7009..c123830 100644
--- a/src/services/query/auth.js
+++ b/src/services/query/auth.js
@@ -60,10 +60,21 @@ export const useCompleteProfileMutation = () => useMutation({ mutationFn: apiCom
export const useUpdateProfileMutation = () => useMutation({ mutationFn: apiUpdateProfile })
+const getRegistrationVideoIfAvailable = async () => {
+ try {
+ return await apiGetRegistrationQuestionVideo()
+ } catch (error) {
+ // The migrated backend does not expose this optional content endpoint yet.
+ // Treat a missing route as "no tutorial uploaded" so registration remains usable.
+ if (error?.response?.status === 404) return {}
+ throw error
+ }
+}
+
export const useGetRegistrationVideoQuery = (options = {}) =>
useQuery({
queryKey: authKeys.registrationVideo(),
- queryFn: () => apiGetRegistrationQuestionVideo(),
+ queryFn: getRegistrationVideoIfAvailable,
select: (response) => response?.data ?? response,
...options,
})
diff --git a/src/utils/date-convertor.js b/src/utils/date-convertor.js
index b40353f..d8ee485 100644
--- a/src/utils/date-convertor.js
+++ b/src/utils/date-convertor.js
@@ -35,8 +35,7 @@ export function gregorianToJalaliString(isoString, separator = '/', persianDigit
jd = jd.replace(/\d/g, (d) => enToFa[d])
}
return `${jy}${separator}${jm}${separator}${jd}`
- } catch (error) {
- console.error('Date conversion error:', error)
+ } catch {
return ''
}
}
@@ -83,8 +82,7 @@ export function gregorianToJalaliStringLong(isoString, separator = '/', persianD
}
return ` ${jy}${separator}${jm}${separator}${jd}، ${hh}:${mm}`
- } catch (error) {
- console.error('Date conversion error:', error)
+ } catch {
return ''
}
}
@@ -98,8 +96,7 @@ export function JalaliToGregorianString(jalaliStr) {
2,
'0'
)}T00:00:00.000Z`
- } catch (error) {
- console.log('Jalali to Gregorian conversion error:', error)
+ } catch {
return ''
}
}
@@ -119,8 +116,7 @@ export function JalaliToGregorianStringWithTime(jalaliDateTimeStr) {
2,
'0'
)}T${timePart}:00.000Z`
- } catch (error) {
- console.log('Jalali to Gregorian with time conversion error:', error)
+ } catch {
return ''
}
}