Compare commits

...

3 Commits

Author SHA1 Message Date
sajjadtalkhabi bc9ce01d3f Merge branch 'master' of ssh://109.122.252.86:2222/front-end/banu-front
Deploy banu-front / deploy (push) Successful in 3m33s
2026-07-30 17:41:10 +03:30
sajjadtalkhabi 38051db180 fix: bugs 2026-07-30 17:40:41 +03:30
sajjadtalkhabi 36b747d267 fix: bugs 2026-07-30 17:38:27 +03:30
40 changed files with 314 additions and 97 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Banu</title> <title>مدرسه بانو</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+4
View File
@@ -4,6 +4,10 @@
box-sizing: border-box; box-sizing: border-box;
} }
html {
font-size: 106.25%;
}
body { body {
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
+21 -21
View File
@@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: montserrat; font-family: montserrat;
src: url('../fonts/montserrat/Montserrat-ExtraLight.ttf') format('truetype'); src: url('../fonts/montserrat/Montserrat-Regular.ttf') format('truetype');
font-weight: 200; font-weight: 200;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -16,8 +16,8 @@
@font-face { @font-face {
font-family: aria; font-family: aria;
src: url('../fonts/aria/AriaWebFaNum-Light.woff2') format('woff2'); src: url('../fonts/aria/AriaWeb-Normal.woff2') format('woff2');
font-weight: 300; font-weight: 200;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@@ -25,7 +25,7 @@
@font-face { @font-face {
font-family: aria; font-family: aria;
src: url('../fonts/aria/AriaWebFaNum-Regular.woff2') format('woff2'); src: url('../fonts/aria/AriaWebFaNum-Regular.woff2') format('woff2');
font-weight: 400; font-weight: 300;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@@ -33,6 +33,14 @@
@font-face { @font-face {
font-family: aria; font-family: aria;
src: url('../fonts/aria/AriaWebFaNum-Medium.woff2') format('woff2'); src: url('../fonts/aria/AriaWebFaNum-Medium.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: aria;
src: url('../fonts/aria/AriaWebFaNum-SemiBold.woff2') format('woff2');
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -46,25 +54,17 @@
font-display: swap; font-display: swap;
} }
@font-face {
font-family: aria;
src: url('../fonts/aria/AriaWeb-Normal.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: iran-yekan;
src: url('../fonts/iran-yekan/iranyekanwebregularfanum.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face { @font-face {
font-family: iran-yekan; font-family: iran-yekan;
src: url('../fonts/iran-yekan/iranyekanwebmediumfanum.woff') format('woff'); src: url('../fonts/iran-yekan/iranyekanwebmediumfanum.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: iran-yekan;
src: url('../fonts/iran-yekan/iranyekanwebboldfanum.woff') format('woff');
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -80,7 +80,7 @@
@font-face { @font-face {
font-family: iran-yekan; font-family: iran-yekan;
src: url('../fonts/iran-yekan/iranyekanweblightfanum.woff') format('woff'); src: url('../fonts/iran-yekan/iranyekanwebregularfanum.woff') format('woff');
font-weight: 300; font-weight: 300;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
+4 -1
View File
@@ -15,6 +15,10 @@ export const fields = {
lastName: 'نام خانوادگی', lastName: 'نام خانوادگی',
nationalCode: 'کد ملی', nationalCode: 'کد ملی',
postalCode: 'کد پستی', postalCode: 'کد پستی',
virtualPhoneNumber: 'شماره تلفن مجازی',
seminaryCode: 'کد حوزوی',
seminaryServiceCenterCode: 'کد مرکز خدمات حوزه',
childrenCount: 'تعداد فرزند',
birthDate: 'تاریخ تولد', birthDate: 'تاریخ تولد',
birthday: 'تاریخ تولد', birthday: 'تاریخ تولد',
@@ -76,7 +80,6 @@ export const fields = {
seminaryLevel: 'مقطع حوزه', seminaryLevel: 'مقطع حوزه',
universityLevel: 'مقطع دانشگاهی', universityLevel: 'مقطع دانشگاهی',
universityName: 'نام حوزه علمیه / دانشگاه', universityName: 'نام حوزه علمیه / دانشگاه',
workExperienceSummary: 'خلاصهٔ سوابق شغلی',
hijabApproach: 'رویکرد حجاب', hijabApproach: 'رویکرد حجاب',
prerequisites: 'پیش‌نیازها', prerequisites: 'پیش‌نیازها',
+2 -6
View File
@@ -6,7 +6,7 @@ export const getEnumObject = (enumArray, key = 'value', value = 'label') =>
export const ROLES = Object.freeze([ export const ROLES = Object.freeze([
{ value: 'admin', label: 'مدیر', changeable: false }, { value: 'admin', label: 'مدیر', changeable: false },
{ value: 'student', label: 'دانشجو', changeable: true }, { value: 'student', label: 'طلبه', changeable: true },
{ value: 'teacher', label: 'استاد', changeable: true }, { value: 'teacher', label: 'استاد', changeable: true },
{ value: 'counselor', label: 'مشاور', changeable: true }, { value: 'counselor', label: 'مشاور', changeable: true },
{ value: 'missionary', label: 'مبلغ', changeable: true }, { value: 'missionary', label: 'مبلغ', changeable: true },
@@ -53,8 +53,6 @@ export const USER_STATUS = Object.freeze({
export const MARITAL_STATUS = Object.freeze({ export const MARITAL_STATUS = Object.freeze({
single: 'مجرد', single: 'مجرد',
married: 'متاهل', married: 'متاهل',
divorced: 'مطلقه',
widowed: 'بیوه',
}) })
export const GENDER = Object.freeze({ export const GENDER = Object.freeze({
@@ -68,7 +66,6 @@ export const CHANGEABLE_ROLES = Object.freeze(ROLES.filter((r) => r.changeable).
export const EDUCATION_STATUS = Object.freeze({ export const EDUCATION_STATUS = Object.freeze({
seminary_student: 'طلبه', seminary_student: 'طلبه',
seminary_and_university_student: 'طلبه و دانشجو',
graduate: 'فارغ التحصیلی', graduate: 'فارغ التحصیلی',
}) })
@@ -76,14 +73,13 @@ export const SEMINARY_LEVEL = Object.freeze({
level_2: 'سطح دو', level_2: 'سطح دو',
level_3: 'سطح سه', level_3: 'سطح سه',
level_4: 'سطح چهار', level_4: 'سطح چهار',
not_applicable: 'طلبه نیستم',
}) })
export const UNIVERSITY_LEVEL = Object.freeze({ export const UNIVERSITY_LEVEL = Object.freeze({
bachelor: ' مقطع کارشناسی (لیسانس)', bachelor: ' مقطع کارشناسی (لیسانس)',
master: ' مقطع کارشناسی ارشد (فوق لیسانس)', master: ' مقطع کارشناسی ارشد (فوق لیسانس)',
phd: ' مقطع دکتری', phd: ' مقطع دکتری',
not_applicable: 'دانشجو نیستم', not_applicable: 'طلبه نیستم',
}) })
export const PROPAGATION_PLATFORM = Object.freeze({ export const PROPAGATION_PLATFORM = Object.freeze({
@@ -49,7 +49,7 @@
</div> </div>
<div class="submission-details__row"> <div class="submission-details__row">
<LineInfoBlock title="پاسخ دانشجو" :desc="submission.userDescription || '—'" /> <LineInfoBlock title="پاسخ طلبه" :desc="submission.userDescription || '—'" />
</div> </div>
<div v-if="submission.attachments?.length" class="submission-details__attachments"> <div v-if="submission.attachments?.length" class="submission-details__attachments">
@@ -1,9 +1,9 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<div class="dashboard__main"> <div class="dashboard__main">
<!-- اطلاعات موسسه --> <!-- اطلاعات مدرسه -->
<section class="dashboard__section"> <section class="dashboard__section">
<BoxedIconTitleBlock title="اطلاعات موسسه" desc="آخرین اطلاعات موسسه شما."> <BoxedIconTitleBlock title="اطلاعات مدرسه" desc="آخرین اطلاعات مدرسه شما.">
<template #icon> <template #icon>
<SvgIcon name="heart" :size="24" color="var(--color-primary)" /> <SvgIcon name="heart" :size="24" color="var(--color-primary)" />
</template> </template>
+1 -1
View File
@@ -7,7 +7,7 @@ export default [
layout: 'admin', layout: 'admin',
role: 'admin', role: 'admin',
title: 'سلام بر شما', title: 'سلام بر شما',
subtitle: 'به سامانه موسسه بانو خوش آمدید.', subtitle: 'به سامانه مدرسه بانو خوش آمدید.',
}, },
}, },
{ {
@@ -1,7 +1,7 @@
<template> <template>
<BasicModal <BasicModal
title="افزودن دانشجو" title="افزودن طلبه"
title-en="Add Student" title-en="Add Seminarian"
width="95%" width="95%"
max-width="42rem" max-width="42rem"
min-width="auto" min-width="auto"
@@ -15,7 +15,7 @@
v-model="searchInput" v-model="searchInput"
type="text" type="text"
class="add-term-student__search-input" class="add-term-student__search-input"
placeholder="اسم دانشجو را وارد نمایید" placeholder="اسم طلبه را وارد نمایید"
@input="onSearchInput" @input="onSearchInput"
/> />
</div> </div>
@@ -56,7 +56,7 @@
</div> </div>
</div> </div>
<NoItems v-else title="بدون نتیجه" desc="دانشجویی یافت نشد." /> <NoItems v-else title="بدون نتیجه" desc="طلبه‌ای یافت نشد." />
<div class="add-term-student__divider" /> <div class="add-term-student__divider" />
@@ -28,7 +28,7 @@
title="تاریخ پایان" title="تاریخ پایان"
:numeric-desc="formatJalaaliDate(term.endsAt) || '-'" :numeric-desc="formatJalaaliDate(term.endsAt) || '-'"
/> />
<LineInfoBlock title="تعداد دانشجویان" :numeric-desc="term.studentsCount ?? 0" /> <LineInfoBlock title="تعداد طلبه‌ها" :numeric-desc="term.studentsCount ?? 0" />
<LineInfoBlock title="تعداد دوره‌ها" :numeric-desc="term.coursesCount ?? 0" /> <LineInfoBlock title="تعداد دوره‌ها" :numeric-desc="term.coursesCount ?? 0" />
<LineInfoBlock title="وضعیت" :desc="term.isActive ? 'فعال' : 'غیرفعال'" /> <LineInfoBlock title="وضعیت" :desc="term.isActive ? 'فعال' : 'غیرفعال'" />
<div v-if="term.description" class="term-details__cell--full"> <div v-if="term.description" class="term-details__cell--full">
@@ -41,9 +41,9 @@
<TabsBlock :tabs="tabs" v-model="activeTab"> <TabsBlock :tabs="tabs" v-model="activeTab">
<template #students> <template #students>
<div class="term-details__panel-header"> <div class="term-details__panel-header">
<LineTitleBlock title="لیست دانشجویان این ترم" title-en="Students List" /> <LineTitleBlock title="لیست طلبه‌های این ترم" title-en="Seminarians List" />
<BaseButton <BaseButton
text="افزودن دانشجو" text="افزودن طلبه"
custom-class="term-details__panel-btn" custom-class="term-details__panel-btn"
@click="onOpenAddStudent" @click="onOpenAddStudent"
> >
@@ -76,7 +76,7 @@
</div> </div>
</div> </div>
</div> </div>
<NoItems v-else title="متاسفیم" desc="دانشجویی برای نمایش وجود ندارد." /> <NoItems v-else title="متاسفیم" desc="طلبه‌ای برای نمایش وجود ندارد." />
<PaginationBlock :pagination="studentPaginationMeta" @update:page="setStudentPage" /> <PaginationBlock :pagination="studentPaginationMeta" @update:page="setStudentPage" />
</template> </template>
@@ -161,7 +161,7 @@ const { data: term } = useAdminTermQuery(termId, {
const tabs = [ const tabs = [
{ name: 'courses', label: 'دوره‌ها', icon: 'list-bullets' }, { name: 'courses', label: 'دوره‌ها', icon: 'list-bullets' },
{ name: 'students', label: 'دانشجویان', icon: 'users' }, { name: 'students', label: 'طلبه‌ها', icon: 'users' },
] ]
const activeTab = ref('courses') const activeTab = ref('courses')
+1 -1
View File
@@ -12,5 +12,5 @@ export const termSchema = object().shape({
}) })
export const addTermStudentSchema = object().shape({ export const addTermStudentSchema = object().shape({
studentIds: array().of(mixed()).min(1, 'حداقل یک دانشجو را انتخاب کنید').required(), studentIds: array().of(mixed()).min(1, 'حداقل یک طلبه را انتخاب کنید').required(),
}) })
@@ -59,12 +59,6 @@
<section class="user-verification-details__section"> <section class="user-verification-details__section">
<LineTitleBlock title="اطلاعات شغلی" title-en="Job information" /> <LineTitleBlock title="اطلاعات شغلی" title-en="Job information" />
<div class="user-verification-details__grid user-verification-details__grid--4"> <div class="user-verification-details__grid user-verification-details__grid--4">
<div class="user-verification-details__cell user-verification-details__cell--full">
<LineInfoBlock
title="خلاصه‌ای از سوابق شغلی"
:desc="profile.workExperienceSummary || ''"
/>
</div>
<div class="user-verification-details__cell user-verification-details__cell--full"> <div class="user-verification-details__cell user-verification-details__cell--full">
<LineInfoBlock <LineInfoBlock
title="خلاصه‌ای از فعالیت‌ها در موقعیت‌های شغلی" title="خلاصه‌ای از فعالیت‌ها در موقعیت‌های شغلی"
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="auth-heading"> <div class="auth-heading">
<img :src="logo" alt="banu" class="auth-heading__logo" /> <img :src="logo" alt="مدرسه بانو" class="auth-heading__logo" />
<h2 class="auth-heading__title">{{ title }}</h2> <h2 class="auth-heading__title">{{ title }}</h2>
<p v-if="step === 'otp'" class="auth-heading__subtitle"> <p v-if="step === 'otp'" class="auth-heading__subtitle">
<span class="auth-heading__phone-label">ارسال کد فعالسازی به شماره</span> <span class="auth-heading__phone-label">ارسال کد فعالسازی به شماره</span>
@@ -2,7 +2,7 @@
<div class="pattern-section"> <div class="pattern-section">
<div class="pattern-section__bg" :style="{ backgroundImage: `url(${bgImage})` }" /> <div class="pattern-section__bg" :style="{ backgroundImage: `url(${bgImage})` }" />
<div class="pattern-section__front"> <div class="pattern-section__front">
<img :src="frontImage" alt="banu" /> <img :src="frontImage" alt="مدرسه بانو" />
</div> </div>
</div> </div>
</template> </template>
@@ -144,7 +144,7 @@ const onResend = async () => {
const body = { phone: props.phoneNumber } const body = { phone: props.phoneNumber }
if (props.mode === 'forgotPassword') await forgotMutation.mutateAsync(body) if (props.mode === 'forgotPassword') await forgotMutation.mutateAsync(body)
else if (props.mode === 'loginWithCode') await requestLoginOtpMutation.mutateAsync(body) else if (props.mode === 'loginWithCode') await requestLoginOtpMutation.mutateAsync(body)
else await resendCodeMutation.mutateAsync({ phoneNumber: props.phoneNumber }) else await resendCodeMutation.mutateAsync(body)
} }
</script> </script>
@@ -28,7 +28,7 @@
<SelectField <SelectField
v-model="form.universityLevel" v-model="form.universityLevel"
name="universityLevel" name="universityLevel"
label="آخرین مدرک تحصیلی" label="آخرین مدرک تحصیلی دانشگاهی"
:options="universityLevelOptions" :options="universityLevelOptions"
option-label="label" option-label="label"
option-value="value" option-value="value"
@@ -65,20 +65,6 @@
<LineTitleBlock title="مشخصات شغلی" title-en="Job Details" /> <LineTitleBlock title="مشخصات شغلی" title-en="Job Details" />
<div class="educational-info__row"> <div class="educational-info__row">
<div class="educational-info__cell educational-info__cell--full">
<TextareaField
v-model="form.workExperienceSummary"
name="workExperienceSummary"
label="خلاصه ای از سوابق شغلی خود را ذکر بفرمایید."
:row="isMobile ? 5 : 1"
:error="errors.workExperienceSummary"
@blur="validateAt('workExperienceSummary', form.workExperienceSummary)"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
<div class="educational-info__cell educational-info__cell--full"> <div class="educational-info__cell educational-info__cell--full">
<TextareaField <TextareaField
v-model="form.activitySummary" v-model="form.activitySummary"
@@ -75,6 +75,51 @@
:error="errors.gender" :error="errors.gender"
/> />
</div> </div>
<div class="personal-info__cell">
<TextField
v-model="form.childrenCount"
name="childrenCount"
label="تعداد فرزند"
inputmode="numeric"
:convert-digits="true"
:error="errors.childrenCount"
@blur="validateAt('childrenCount', form.childrenCount)"
>
<template #appendIcon>
<SvgIcon name="users-three" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="personal-info__cell">
<TextField
v-model="form.seminaryCode"
name="seminaryCode"
label="کد حوزوی"
inputmode="numeric"
:convert-digits="true"
:error="errors.seminaryCode"
@blur="validateAt('seminaryCode', form.seminaryCode)"
>
<template #appendIcon>
<SvgIcon name="graduation-cap" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="personal-info__cell">
<TextField
v-model="form.seminaryServiceCenterCode"
name="seminaryServiceCenterCode"
label="کد مرکز خدمات حوزه"
inputmode="numeric"
:convert-digits="true"
:error="errors.seminaryServiceCenterCode"
@blur="validateAt('seminaryServiceCenterCode', form.seminaryServiceCenterCode)"
>
<template #appendIcon>
<SvgIcon name="check-square" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
</div> </div>
<LineTitleBlock title="اطلاعات تماس" title-en="Contact Details" /> <LineTitleBlock title="اطلاعات تماس" title-en="Contact Details" />
@@ -95,6 +140,36 @@
</template> </template>
</TextField> </TextField>
</div> </div>
<div class="personal-info__cell">
<TextField
v-model="form.virtualPhoneNumber"
name="virtualPhoneNumber"
label="شماره تلفن مجازی"
inputmode="numeric"
:convert-digits="true"
:error="errors.virtualPhoneNumber"
@blur="validateAt('virtualPhoneNumber', form.virtualPhoneNumber)"
>
<template #appendIcon>
<SvgIcon name="phone" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="personal-info__cell">
<TextField
v-model="form.postalCode"
name="postalCode"
label="کد پستی"
inputmode="numeric"
:convert-digits="true"
:error="errors.postalCode"
@blur="validateAt('postalCode', form.postalCode)"
>
<template #appendIcon>
<SvgIcon name="map-pin-simple-area" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="personal-info__cell"> <div class="personal-info__cell">
<SelectField <SelectField
v-model="form.provinceId" v-model="form.provinceId"
@@ -141,7 +216,11 @@
<StepActions <StepActions
:show-back="false" :show-back="false"
:loading="uploadMutation.isPending.value || updateProfileMutation.isPending.value" :loading="
uploadMutation.isPending.value ||
updateProfileMutation.isPending.value ||
saveRegisterMutation.isPending.value
"
/> />
</form> </form>
</template> </template>
@@ -166,7 +245,12 @@ import { personalInformationSchema } from '@/features/auth/schema/student-regist
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue' import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration' import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/query/common' import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/query/common'
import { authKeys, useUpdateProfileMutation, useUploadMediaMutation } from '@/services/query/auth' import {
authKeys,
useSaveRegisterDataMutation,
useUpdateProfileMutation,
useUploadMediaMutation,
} from '@/services/query/auth'
const store = useStudentRegistrationStore() const store = useStudentRegistrationStore()
const { user } = useAuth() const { user } = useAuth()
@@ -212,6 +296,11 @@ const form = ref({
cityId: user.value?.cityId || store.personal.cityId || null, cityId: user.value?.cityId || store.personal.cityId || null,
address: address:
(typeof user.value?.address === 'string' && user.value.address) || store.personal.address || '', (typeof user.value?.address === 'string' && user.value.address) || store.personal.address || '',
postalCode: store.personal.postalCode || '',
virtualPhoneNumber: store.personal.virtualPhoneNumber || '',
seminaryCode: store.personal.seminaryCode || '',
seminaryServiceCenterCode: store.personal.seminaryServiceCenterCode || '',
childrenCount: store.personal.childrenCount ?? '',
avatar: initialAvatar, avatar: initialAvatar,
avatarId: store.personal.avatarId || '', avatarId: store.personal.avatarId || '',
}) })
@@ -258,6 +347,7 @@ const onAvatarCropped = async (file) => {
const onAvatarError = (msg) => toast.error(msg) const onAvatarError = (msg) => toast.error(msg)
const updateProfileMutation = useUpdateProfileMutation() const updateProfileMutation = useUpdateProfileMutation()
const saveRegisterMutation = useSaveRegisterDataMutation()
const onSubmit = async () => { const onSubmit = async () => {
const { isValid, payload } = await validate(form.value) const { isValid, payload } = await validate(form.value)
@@ -280,6 +370,13 @@ const onSubmit = async () => {
: {}), : {}),
} }
await updateProfileMutation.mutateAsync(body) await updateProfileMutation.mutateAsync(body)
await saveRegisterMutation.mutateAsync({
postalCode: payload.postalCode,
virtualPhoneNumber: payload.virtualPhoneNumber,
seminaryCode: payload.seminaryCode,
seminaryServiceCenterCode: payload.seminaryServiceCenterCode,
childrenCount: payload.childrenCount,
})
await queryClient.invalidateQueries({ queryKey: authKeys.me() }) await queryClient.invalidateQueries({ queryKey: authKeys.me() })
store.personal = { ...store.personal, ...next } store.personal = { ...store.personal, ...next }
store.markStepAsCompleted(STUDENT_REGISTRATION.PERSONAL_INFORMATION) store.markStepAsCompleted(STUDENT_REGISTRATION.PERSONAL_INFORMATION)
@@ -1,6 +1,6 @@
<template> <template>
<form class="professional-info" @submit.prevent="onSubmit"> <form class="professional-info" @submit.prevent="onSubmit">
<LineTitleBlock title="مشخصات شغلی" title-en="Job Details" /> <LineTitleBlock title="مشخصات تبلیغی" title-en="Propagation Details" />
<div class="professional-info__row"> <div class="professional-info__row">
<div class="professional-info__cell professional-info__cell--5"> <div class="professional-info__cell professional-info__cell--5">
<TextField <TextField
@@ -49,7 +49,7 @@
<SelectField <SelectField
v-model="form.propagationPlatforms" v-model="form.propagationPlatforms"
name="propagationPlatforms" name="propagationPlatforms"
label="در چه بستر هایی فعالیت تبلیغی داشته اید؟" label="در چه بستر هایی فعالیت تبلیغی داشته اید؟ (امکان انتخاب چند گزینه وجود دارد)"
:options="platformOptions" :options="platformOptions"
option-label="label" option-label="label"
option-value="value" option-value="value"
@@ -1,11 +1,11 @@
<template> <template>
<aside class="sidebar-steps"> <aside class="sidebar-steps">
<div class="sidebar-steps__mobile-logo"> <div class="sidebar-steps__mobile-logo">
<img :src="logoPinkishRed" alt="banu" /> <img :src="logoPinkishRed" alt="مدرسه بانو" />
</div> </div>
<div class="sidebar-steps__panel"> <div class="sidebar-steps__panel">
<div class="sidebar-steps__logo"> <div class="sidebar-steps__logo">
<img :src="logoWhite" alt="banu" /> <img :src="logoWhite" alt="مدرسه بانو" />
</div> </div>
<ul class="sidebar-steps__list"> <ul class="sidebar-steps__list">
<li <li
@@ -93,8 +93,8 @@ const STEP_LABELS = {
desc: 'لطفاً اطلاعات تحصیلی و شغلی خود را به‌صورت کامل و دقیق وارد کنید', desc: 'لطفاً اطلاعات تحصیلی و شغلی خود را به‌صورت کامل و دقیق وارد کنید',
}, },
[STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION]: { [STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION]: {
title: 'اطلاعات فعالیت تبلیغاتی و حرفه‌ای', title: 'اطلاعات فعالیت‌های تبلیغی',
desc: 'لطفاً اطلاعات تبلیغاتی و حرفه‌ای خود را به‌صورت کامل و دقیق وارد کنید', desc: 'لطفاً اطلاعات فعالیت‌های تبلیغی خود را به‌صورت کامل و دقیق وارد کنید',
}, },
[STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1]: { [STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1]: {
title: 'بخش مهارت (بخش اول)', title: 'بخش مهارت (بخش اول)',
+9 -1
View File
@@ -20,6 +20,15 @@ export const personalInformationSchema = object().shape({
provinceId: mixed().required(), provinceId: mixed().required(),
cityId: mixed().required(), cityId: mixed().required(),
address: requiredString(10), address: requiredString(10),
postalCode: string()
.required()
.matches(/^\d{10}$/, 'کد پستی باید ۱۰ رقم باشد'),
virtualPhoneNumber: string()
.required()
.matches(/^\d{10,15}$/, 'شماره تلفن مجازی نامعتبر است'),
seminaryCode: requiredString(),
seminaryServiceCenterCode: requiredString(),
childrenCount: number().typeError('تعداد فرزند باید عدد باشد').required().min(0).max(20),
}) })
export const educationalInformationSchema = object().shape({ export const educationalInformationSchema = object().shape({
@@ -28,7 +37,6 @@ export const educationalInformationSchema = object().shape({
universityLevel: string().required(), universityLevel: string().required(),
universityName: requiredString(5), universityName: requiredString(5),
fieldOfStudy: requiredString(5), fieldOfStudy: requiredString(5),
workExperienceSummary: requiredString(10),
activitySummary: requiredString(10), activitySummary: requiredString(10),
}) })
@@ -18,6 +18,11 @@ const emptyState = () => ({
provinceId: null, provinceId: null,
cityId: null, cityId: null,
address: '', address: '',
postalCode: '',
virtualPhoneNumber: '',
seminaryCode: '',
seminaryServiceCenterCode: '',
childrenCount: '',
}, },
educational: { educational: {
educationStatus: '', educationStatus: '',
@@ -25,7 +30,6 @@ const emptyState = () => ({
universityLevel: '', universityLevel: '',
universityName: '', universityName: '',
fieldOfStudy: '', fieldOfStudy: '',
workExperienceSummary: '',
activitySummary: '', activitySummary: '',
}, },
professional: { professional: {
@@ -83,6 +87,11 @@ const SECTION_FIELDS = {
'provinceId', 'provinceId',
'cityId', 'cityId',
'address', 'address',
'postalCode',
'virtualPhoneNumber',
'seminaryCode',
'seminaryServiceCenterCode',
'childrenCount',
], ],
educational: [ educational: [
'educationStatus', 'educationStatus',
@@ -90,7 +99,6 @@ const SECTION_FIELDS = {
'universityLevel', 'universityLevel',
'universityName', 'universityName',
'fieldOfStudy', 'fieldOfStudy',
'workExperienceSummary',
'activitySummary', 'activitySummary',
], ],
professional: [ professional: [
+1 -1
View File
@@ -7,7 +7,7 @@ export default [
layout: 'counselor', layout: 'counselor',
role: 'counselor', role: 'counselor',
title: 'سلام بر شما', title: 'سلام بر شما',
subtitle: 'به پنل مشاوره بانو خوش آمدید.', subtitle: 'به پنل مشاوره مدرسه بانو خوش آمدید.',
}, },
}, },
{ {
@@ -10,7 +10,7 @@
<template #default="{ data, close }"> <template #default="{ data, close }">
<form class="sample-modal__form" @submit.prevent="onSubmit(close)"> <form class="sample-modal__form" @submit.prevent="onSubmit(close)">
<p class="sample-modal__intro"> <p class="sample-modal__intro">
این مودال الگوی استاندارد بنو است: عنوان با این مودال الگوی استاندارد مدرسه بانو است: عنوان با
<code>LineTitleBlock</code> <code>LineTitleBlock</code>
در راست، دکمهٔ بستن داخلی در چپ. در راست، دکمهٔ بستن داخلی در چپ.
</p> </p>
@@ -82,6 +82,7 @@ const onSubmit = async (close) => {
close() close()
form.value = { subject: '', note: '' } form.value = { subject: '', note: '' }
resetErrors() resetErrors()
// eslint-disable-next-line no-console
console.info('SampleModal submit', payload) console.info('SampleModal submit', payload)
} }
</script> </script>
@@ -142,7 +142,7 @@
/> />
<BaseButton <BaseButton
variant="secondary" variant="secondary"
text="نمایش مودال نمونه (الگوی بنو)" text="نمایش مودال نمونه (الگوی مدرسه بانو)"
customClass="global-components__btn-wide" customClass="global-components__btn-wide"
@click="openModal('SampleModal', { context: 'پیش‌نمایش' })" @click="openModal('SampleModal', { context: 'پیش‌نمایش' })"
/> />
+1 -1
View File
@@ -7,7 +7,7 @@ export default [
layout: 'missionary', layout: 'missionary',
role: 'missionary', role: 'missionary',
title: 'سلام بر شما', title: 'سلام بر شما',
subtitle: 'به پنل اعزام بانو خوش آمدید.', subtitle: 'به پنل اعزام مدرسه بانو خوش آمدید.',
}, },
}, },
{ {
@@ -25,7 +25,7 @@
</div> </div>
<div v-else class="se__empty"> <div v-else class="se__empty">
<SvgIcon name="scroll" :size="128" color="#c3c3c3" /> <SvgIcon name="scroll" :size="128" color="#c3c3c3" />
<p class="se__empty-title">دانشجوی گرامی</p> <p class="se__empty-title">طلبه گرامی</p>
<p class="se__empty-desc">شما تاکنون هیچ آزمونی را به شروع نکرده اید.</p> <p class="se__empty-desc">شما تاکنون هیچ آزمونی را به شروع نکرده اید.</p>
</div> </div>
</div> </div>
@@ -101,6 +101,66 @@
option-value="value" option-value="value"
/> />
</div> </div>
<div class="edit-profile__cell edit-profile__cell--third">
<TextField
v-model="form.childrenCount"
name="childrenCount"
label="تعداد فرزند"
inputmode="numeric"
:convert-digits="true"
:error="errors.childrenCount"
@blur="validateAt('childrenCount', form.childrenCount)"
>
<template #appendIcon>
<SvgIcon name="users-three" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="edit-profile__cell edit-profile__cell--third">
<TextField
v-model="form.virtualPhoneNumber"
name="virtualPhoneNumber"
label="شماره تلفن مجازی"
inputmode="numeric"
:convert-digits="true"
:error="errors.virtualPhoneNumber"
@blur="validateAt('virtualPhoneNumber', form.virtualPhoneNumber)"
>
<template #appendIcon>
<SvgIcon name="phone" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="edit-profile__cell edit-profile__cell--third">
<TextField
v-model="form.seminaryCode"
name="seminaryCode"
label="کد حوزوی"
inputmode="numeric"
:convert-digits="true"
:error="errors.seminaryCode"
@blur="validateAt('seminaryCode', form.seminaryCode)"
>
<template #appendIcon>
<SvgIcon name="graduation-cap" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="edit-profile__cell edit-profile__cell--third">
<TextField
v-model="form.seminaryServiceCenterCode"
name="seminaryServiceCenterCode"
label="کد مرکز خدمات حوزه"
inputmode="numeric"
:convert-digits="true"
:error="errors.seminaryServiceCenterCode"
@blur="validateAt('seminaryServiceCenterCode', form.seminaryServiceCenterCode)"
>
<template #appendIcon>
<SvgIcon name="check-square" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
</div> </div>
<LineTitleBlock title="مشخصات محل سکونت" title-en="Address Details" /> <LineTitleBlock title="مشخصات محل سکونت" title-en="Address Details" />
@@ -127,6 +187,21 @@
:disabled="!form.provinceId" :disabled="!form.provinceId"
/> />
</div> </div>
<div class="edit-profile__cell edit-profile__cell--third">
<TextField
v-model="form.postalCode"
name="postalCode"
label="کد پستی"
inputmode="numeric"
:convert-digits="true"
:error="errors.postalCode"
@blur="validateAt('postalCode', form.postalCode)"
>
<template #appendIcon>
<SvgIcon name="map-pin-simple-area" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="edit-profile__cell edit-profile__cell--full"> <div class="edit-profile__cell edit-profile__cell--full">
<TextareaField <TextareaField
v-model="form.address" v-model="form.address"
@@ -227,6 +302,8 @@ import { studentProfileSchema, studentResetPasswordSchema } from '@/features/stu
import { import {
authKeys, authKeys,
useGetMeQuery, useGetMeQuery,
useGetRegisterDataQuery,
useSaveRegisterDataMutation,
useUpdateProfileMutation, useUpdateProfileMutation,
useUploadMediaMutation, useUploadMediaMutation,
} from '@/services/query/auth' } from '@/services/query/auth'
@@ -262,6 +339,11 @@ const form = ref({
provinceId: '', provinceId: '',
cityId: '', cityId: '',
address: '', address: '',
postalCode: '',
virtualPhoneNumber: '',
seminaryCode: '',
seminaryServiceCenterCode: '',
childrenCount: '',
avatarMediaId: null, avatarMediaId: null,
}) })
@@ -326,6 +408,24 @@ watch(
{ immediate: true } { immediate: true }
) )
const { data: registerData } = useGetRegisterDataQuery()
watch(
registerData,
(data) => {
if (!data) return
form.value = {
...form.value,
postalCode: data.postalCode || '',
virtualPhoneNumber: data.virtualPhoneNumber || '',
seminaryCode: data.seminaryCode || '',
seminaryServiceCenterCode: data.seminaryServiceCenterCode || '',
childrenCount: data.childrenCount ?? '',
}
},
{ immediate: true }
)
const uploadMutation = useUploadMediaMutation() const uploadMutation = useUploadMediaMutation()
const onAvatarCropped = async (file) => { const onAvatarCropped = async (file) => {
@@ -343,6 +443,7 @@ const onAvatarCropped = async (file) => {
const onAvatarError = (msg) => toast.error(msg) const onAvatarError = (msg) => toast.error(msg)
const updateMutation = useUpdateProfileMutation() const updateMutation = useUpdateProfileMutation()
const saveRegisterDataMutation = useSaveRegisterDataMutation()
const resetPasswordMutation = useUpdateProfileMutation() const resetPasswordMutation = useUpdateProfileMutation()
const onSubmit = async () => { const onSubmit = async () => {
@@ -359,7 +460,18 @@ const onSubmit = async () => {
...(payload.avatarMediaId ? { avatarMediaId: payload.avatarMediaId } : {}), ...(payload.avatarMediaId ? { avatarMediaId: payload.avatarMediaId } : {}),
} }
await updateMutation.mutateAsync(body) await updateMutation.mutateAsync(body)
await saveRegisterDataMutation.mutateAsync({
postalCode: payload.postalCode || '',
virtualPhoneNumber: payload.virtualPhoneNumber || '',
seminaryCode: payload.seminaryCode || '',
seminaryServiceCenterCode: payload.seminaryServiceCenterCode || '',
childrenCount:
payload.childrenCount == null || payload.childrenCount === ''
? ''
: Number(payload.childrenCount),
})
await queryClient.invalidateQueries({ queryKey: authKeys.me() }) await queryClient.invalidateQueries({ queryKey: authKeys.me() })
await queryClient.invalidateQueries({ queryKey: authKeys.registerData() })
toast.success('پروفایل با موفقیت به‌روز شد') toast.success('پروفایل با موفقیت به‌روز شد')
router.push({ name: 'student-dashboard' }) router.push({ name: 'student-dashboard' })
} }
@@ -15,6 +15,20 @@ export const studentProfileSchema = object().shape({
provinceId: string().nullable().notRequired(), provinceId: string().nullable().notRequired(),
cityId: string().nullable().notRequired(), cityId: string().nullable().notRequired(),
address: string().nullable().notRequired(), address: string().nullable().notRequired(),
postalCode: string()
.nullable()
.notRequired()
.matches(/^\d{10}$/, { message: 'کد پستی باید ۱۰ رقم باشد', excludeEmptyString: true }),
virtualPhoneNumber: string()
.nullable()
.notRequired()
.matches(/^\d{10,15}$/, { message: 'شماره تلفن مجازی نامعتبر است', excludeEmptyString: true }),
seminaryCode: string().nullable().notRequired(),
seminaryServiceCenterCode: string().nullable().notRequired(),
childrenCount: string()
.nullable()
.notRequired()
.matches(/^\d+$/, { message: 'تعداد فرزند باید عدد باشد', excludeEmptyString: true }),
}) })
export const studentResetPasswordSchema = object().shape({ export const studentResetPasswordSchema = object().shape({
+1 -1
View File
@@ -7,7 +7,7 @@ export default [
layout: 'student', layout: 'student',
role: 'student', role: 'student',
title: 'سلام بر شما', title: 'سلام بر شما',
subtitle: 'به سامانه موسسه بانو خوش آمدید.', subtitle: 'به سامانه مدرسه بانو خوش آمدید.',
}, },
}, },
{ {
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<aside class="admin-sidebar"> <aside class="admin-sidebar">
<div class="admin-sidebar__logo"> <div class="admin-sidebar__logo">
<img :src="logo" alt="banu" /> <img :src="logo" alt="مدرسه بانو" />
</div> </div>
<nav class="admin-sidebar__nav"> <nav class="admin-sidebar__nav">
<ul> <ul>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<aside class="counselor-sidebar"> <aside class="counselor-sidebar">
<div class="counselor-sidebar__logo"> <div class="counselor-sidebar__logo">
<img :src="logo" alt="banu" /> <img :src="logo" alt="مدرسه بانو" />
</div> </div>
<nav class="counselor-sidebar__nav"> <nav class="counselor-sidebar__nav">
<ul> <ul>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<aside class="student-sidebar" :style="{ backgroundImage: `url(${bgPattern})` }"> <aside class="student-sidebar" :style="{ backgroundImage: `url(${bgPattern})` }">
<div class="student-sidebar__logo"> <div class="student-sidebar__logo">
<img :src="logo" alt="banu" /> <img :src="logo" alt="مدرسه بانو" />
</div> </div>
<nav class="student-sidebar__nav"> <nav class="student-sidebar__nav">
<ul> <ul>
+1 -1
View File
@@ -2,7 +2,7 @@ import { ROLE_HOME_ROUTES } from '@/enums'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
import { tokenService } from '@/services/api/token-service' import { tokenService } from '@/services/api/token-service'
const APP_TITLE = 'بنو' const APP_TITLE = 'مدرسه بانو'
const isPublic = (to) => to.meta?.public === true const isPublic = (to) => to.meta?.public === true
+1 -1
View File
@@ -14,7 +14,7 @@ export const endpoints = {
verifyForgotPasswordCode: '/auth/password/verify-otp', verifyForgotPasswordCode: '/auth/password/verify-otp',
resetPassword: '/auth/password/reset', resetPassword: '/auth/password/reset',
resendVerificationCodeForRegister: '/resend-verification-code', resendVerificationCodeForRegister: '/auth/login/otp/request',
verifyCode: '/auth/login/otp/verify', verifyCode: '/auth/login/otp/verify',
completeProfile: '/complete-profile', completeProfile: '/complete-profile',
getRegistrationQuestionVideo: '/verification-registration-media', getRegistrationQuestionVideo: '/verification-registration-media',
@@ -32,7 +32,6 @@ const makeStudent = (i) => {
universityLevel: 'not_applicable', universityLevel: 'not_applicable',
universityName: 'حوزه علمیه قم', universityName: 'حوزه علمیه قم',
fieldOfStudy: 'فقه و اصول', fieldOfStudy: 'فقه و اصول',
workExperienceSummary: 'پنج سال تدریس در مدرسه دینی.',
activitySummary: 'برگزاری جلسات قرآن و حدیث به صورت هفتگی.', activitySummary: 'برگزاری جلسات قرآن و حدیث به صورت هفتگی.',
propagationExperienceYears: 6, propagationExperienceYears: 6,
propagationMethodDescription: 'محوریت بیان داستان‌های قرآنی و قرابت‌های اخلاقی.', propagationMethodDescription: 'محوریت بیان داستان‌های قرآنی و قرابت‌های اخلاقی.',
+1 -1
View File
@@ -207,7 +207,7 @@ export const adminTickets = [
type: 'service', type: 'service',
category: 'insurance', category: 'insurance',
status: 'answered', status: 'answered',
subject: 'درخواست بیمه دانشجویی', subject: 'درخواست بیمه طلبگی',
createdAt: '2026-05-10T08:20:00.000Z', createdAt: '2026-05-10T08:20:00.000Z',
media: [], media: [],
student: studentJane, student: studentJane,
@@ -38,7 +38,7 @@ export const missionaryRequests = [
requesterPhone: '09354445566', requesterPhone: '09354445566',
requesterEmail: 'fatemeh@example.com', requesterEmail: 'fatemeh@example.com',
title: 'جلسه پرسش و پاسخ اعتقادی دانشگاه', title: 'جلسه پرسش و پاسخ اعتقادی دانشگاه',
description: 'برگزاری نشست پرسش و پاسخ برای دانشجویان خوابگاه.', description: 'برگزاری نشست پرسش و پاسخ برای طلبه‌های خوابگاه.',
location: 'اصفهان، ایران', location: 'اصفهان، ایران',
requestedDate: '2026-06-02', requestedDate: '2026-06-02',
status: 'accepted', status: 'accepted',
-1
View File
@@ -49,7 +49,6 @@ register('GET', endpoints.getUserRegisterData, ({ params }) => {
{ key: 'universityLevel', value: 'not_applicable' }, { key: 'universityLevel', value: 'not_applicable' },
{ key: 'universityName', value: 'حوزه علمیه قم' }, { key: 'universityName', value: 'حوزه علمیه قم' },
{ key: 'fieldOfStudy', value: 'فقه و اصول' }, { key: 'fieldOfStudy', value: 'فقه و اصول' },
{ key: 'workExperienceSummary', value: 'پنج سال تدریس در مدرسه دینی.' },
{ key: 'activitySummary', value: 'برگزاری جلسات قرآن و حدیث به صورت هفتگی.' }, { key: 'activitySummary', value: 'برگزاری جلسات قرآن و حدیث به صورت هفتگی.' },
{ key: 'specializedTopics', value: 'تربیت دینی نوجوانان' }, { key: 'specializedTopics', value: 'تربیت دینی نوجوانان' },
{ key: 'propagationExperienceYears', value: 6 }, { key: 'propagationExperienceYears', value: 6 },
-4
View File
@@ -27,10 +27,6 @@ register('POST', endpoints.register, ({ data }) => ({
data: { user: { ...currentMe, ...data }, token: fakeToken }, data: { user: { ...currentMe, ...data }, token: fakeToken },
})) }))
register('POST', endpoints.resendVerificationCodeForRegister, () => ({
data: { message: 'کد جدید ارسال شد' },
}))
register('POST', endpoints.forgotPassword, () => ({ register('POST', endpoints.forgotPassword, () => ({
success: true, success: true,
message: 'If the phone is registered, a code has been sent.', message: 'If the phone is registered, a code has been sent.',