This commit is contained in:
sajjadtalkhabi
2026-05-13 03:06:59 +03:30
parent d2a577f254
commit b8ced45375
142 changed files with 3706 additions and 905 deletions
+7 -4
View File
@@ -5,7 +5,7 @@ module.exports = {
node: true,
es2021: true,
},
plugins: ['prettier', 'unicorn', 'import'],
plugins: ['prettier', 'unicorn', 'import', 'waterfall'],
ignorePatterns: ['node_modules/', 'dist/', 'build/', '*.min.js'],
parserOptions: {
ecmaVersion: 2021,
@@ -71,7 +71,10 @@ module.exports = {
'unicorn/prefer-logical-operator-over-ternary': 'off',
'unicorn/prefer-number-properties': 'off',
'unicorn/prefer-ternary': 'off',
'no-unused-vars': 'off',
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'waterfall/waterfall-imports': 'warn',
'waterfall/waterfall-requires': 'warn',
'waterfall/waterfall-objects': 'off',
'no-irregular-whitespace': 'off',
'no-empty': 'off',
'import/extensions': 'off',
@@ -80,9 +83,9 @@ module.exports = {
'vue/html-self-closing': 'off',
'vue/max-attributes-per-line': 'off',
'vue/html-indent': 'off',
'vue/no-unused-components': 'off',
'vue/no-unused-components': 'error',
'vue/no-reserved-component-names': 'off',
'vue/no-unused-vars': 'off',
'vue/no-unused-vars': 'error',
'vue/require-v-for-key': 'off',
'vue/require-toggle-inside-transition': 'off',
'vue/require-valid-default-prop': 'off',
+8
View File
@@ -29,6 +29,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-vue": "^9.15.1",
"eslint-plugin-waterfall": "^1.0.1",
"postcss-html": "^1.8.0",
"prettier": "2.8.8",
"sass": "^1.83.0",
@@ -3467,6 +3468,13 @@
"eslint": ">=6.0.0"
}
},
"node_modules/eslint-plugin-waterfall": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-waterfall/-/eslint-plugin-waterfall-1.0.1.tgz",
"integrity": "sha512-g6rmlPsbKXRnDDcOLjNifqU5Ctml/90KDsvJill1SMIyBVZ17MgcggC3lIM9UcnbFmOttVHBVV3a71Yd/33TKw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+1
View File
@@ -36,6 +36,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-vue": "^9.15.1",
"eslint-plugin-waterfall": "^1.0.1",
"postcss-html": "^1.8.0",
"prettier": "2.8.8",
"sass": "^1.83.0",
+6 -7
View File
@@ -5,16 +5,15 @@
</template>
<script setup>
import { computed, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
import ConfirmModal from '@/components/ConfirmModal.vue'
import PublicLayout from '@/layouts/PublicLayout.vue'
import AdminLayout from '@/layouts/AdminLayout.vue'
import StudentLayout from '@/layouts/StudentLayout.vue'
import { useModalStore } from '@/store/modal'
import AdminLayout from '@/layouts/AdminLayout.vue'
import PublicLayout from '@/layouts/PublicLayout.vue'
import StudentLayout from '@/layouts/StudentLayout.vue'
import ConfirmModal from '@/components/ConfirmModal.vue'
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
const route = useRoute()
+3 -4
View File
@@ -28,11 +28,10 @@
</template>
<script setup>
import { computed, getCurrentInstance, onBeforeUnmount, useSlots, watch } from 'vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useModalStore } from '@/store/modal'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { computed, getCurrentInstance, onBeforeUnmount, useSlots, watch } from 'vue'
const props = defineProps({
name: { type: String, default: '' },
+3 -6
View File
@@ -28,10 +28,7 @@
role="tooltip"
>
{{ tooltip }}
<span
class="circle-button__tooltip-arrow"
:style="arrowStyle"
/>
<span class="circle-button__tooltip-arrow" :style="arrowStyle" />
</div>
</Transition>
</Teleport>
@@ -98,8 +95,8 @@ const updatePosition = async () => {
const side = result.placement.split('-')[0]
const oppositeSide = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[side]
arrowStyle.value = {
left: arrowData.x != null ? `${arrowData.x}px` : '',
top: arrowData.y != null ? `${arrowData.y}px` : '',
left: arrowData.x == null ? '' : `${arrowData.x}px`,
top: arrowData.y == null ? '' : `${arrowData.y}px`,
[oppositeSide]: '-0.25rem',
}
}
+1 -1
View File
@@ -31,9 +31,9 @@
</template>
<script setup>
import { useModalStore } from '@/store/modal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import { useModalStore } from '@/store/modal'
defineOptions({ name: 'ConfirmModal' })
+1 -2
View File
@@ -27,6 +27,7 @@
<script setup>
import { onBeforeUnmount, ref, watch } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import {
autoUpdate,
computePosition,
@@ -35,8 +36,6 @@ import {
shift,
} from '@floating-ui/dom'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
reference: { type: [Object, null], default: null },
+3 -4
View File
@@ -34,11 +34,10 @@
</template>
<script setup>
import { computed, useSlots } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { gallery } from '@/utils/gallery'
import { useUIStore } from '@/store/ui'
import { computed, useSlots } from 'vue'
import { gallery } from '@/utils/gallery'
import SvgIcon from '@/components/icons/SvgIcon.vue'
defineProps({
pageTitle: { type: String, default: '' },
+3 -4
View File
@@ -50,11 +50,10 @@
</template>
<script setup>
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import DatePicker from 'vue3-persian-datetime-picker'
import { autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import DatePicker from 'vue3-persian-datetime-picker'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom'
import {
formatJalaaliDate,
formatJalaaliDateTime,
+1 -1
View File
@@ -62,8 +62,8 @@
<script setup>
import { computed, ref } from 'vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import CircleButton from '@/components/CircleButton.vue'
const props = defineProps({
modelValue: { type: Array, default: () => [] },
+3 -4
View File
@@ -83,12 +83,11 @@
</template>
<script setup>
import 'vue-advanced-cropper/dist/style.css'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import CircleButton from '@/components/CircleButton.vue'
import { markRaw, onBeforeUnmount, ref, watch } from 'vue'
import { Cropper, CircleStencil } from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
modelValue: { type: [Object, String], default: null },
+1 -2
View File
@@ -24,9 +24,8 @@
<script setup>
import { ref, watch } from 'vue'
import TextField from '@/components/form/TextField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
const props = defineProps({
modelValue: { type: String, default: '' },
+1 -2
View File
@@ -74,9 +74,8 @@
<script setup>
import { computed, onBeforeUnmount, ref } from 'vue'
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
let uid = 0
const nextUid = () => `select-field-${++uid}`
+1 -2
View File
@@ -1,7 +1,6 @@
import { storeToRefs } from 'pinia'
import { tokenService } from '@/services/api/token-service'
import { useAuthStore } from '@/store/auth'
import { tokenService } from '@/services/api/token-service'
export default function useAuth() {
const store = useAuthStore()
@@ -76,11 +76,10 @@
<script setup>
import { computed } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import CircleButton from '@/components/CircleButton.vue'
const props = defineProps({
assignment: { type: Object, required: true },
@@ -59,15 +59,14 @@
<script setup>
import { computed, ref, watch } from 'vue'
import useDebounce from '@/composables/useDebounce'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
import useDebounce from '@/composables/useDebounce'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
const props = defineProps({
modelValue: {
@@ -115,30 +115,30 @@
</template>
<script setup>
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import useModal from '@/composables/useModal'
import { ASSIGNMENT_PRIORITY } from '@/enums'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import TextField from '@/components/form/TextField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import FileUploader from '@/components/form/FileUploader.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import useYup from '@/composables/useYup'
import TextField from '@/components/form/TextField.vue'
import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import FileUploader from '@/components/form/FileUploader.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import { assignmentSchema } from '@/features/admin/assignments/schema'
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import {
adminAssignmentsKeys,
useAddAdminAssignmentMutation,
useAdminAssignmentQuery,
useUpdateAdminAssignmentMutation,
} from '@/services/query/admin-assignments'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
import useDebounce from '@/composables/useDebounce'
import { ASSIGNMENT_PRIORITY } from '@/enums'
import { assignmentSchema } from '@/features/admin/assignments/schema'
defineOptions({ name: 'AddAssignmentModal' })
@@ -90,18 +90,17 @@
<script setup>
import { computed } from 'vue'
import useModal from '@/composables/useModal'
import { ASSIGNMENT_PRIORITY } from '@/enums'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import { useAdminAssignmentQuery } from '@/services/query/admin-assignments'
import { ASSIGNMENT_PRIORITY } from '@/enums'
import { formatJalaaliDate } from '@/utils/date-utils'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
defineOptions({ name: 'AssignmentDetailsModal' })
@@ -125,28 +125,28 @@
</template>
<script setup>
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import TextField from '@/components/form/TextField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import SelectField from '@/components/form/SelectField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import useYup from '@/composables/useYup'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import TextField from '@/components/form/TextField.vue'
import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import { EDUCATION_STATUS, SEMINARY_LEVEL, UNIVERSITY_LEVEL } from '@/enums'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import { assignmentSubmissionReviewSchema } from '@/features/admin/assignments/schema'
import {
adminAssignmentsKeys,
useAdminAssignmentSubmissionQuery,
useReviewAdminAssignmentSubmissionMutation,
} from '@/services/query/admin-assignments'
import { EDUCATION_STATUS, SEMINARY_LEVEL, UNIVERSITY_LEVEL } from '@/enums'
import { assignmentSubmissionReviewSchema } from '@/features/admin/assignments/schema'
import { formatJalaaliDate } from '@/utils/date-utils'
defineOptions({ name: 'AssignmentSubmissionDetailsModal' })
@@ -66,18 +66,17 @@
<script setup>
import { computed, ref } from 'vue'
import useModal from '@/composables/useModal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import { usePagination } from '@/composables/usePagination'
import { useAdminAssignmentSubmissionsQuery } from '@/services/query/admin-assignments'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import { usePagination } from '@/composables/usePagination'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import { useAdminAssignmentSubmissionsQuery } from '@/services/query/admin-assignments'
defineOptions({ name: 'AssignmentSubmissionsModal' })
@@ -50,28 +50,27 @@
<script setup>
import { computed, ref } from 'vue'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import AssignmentsFilters from '@/features/admin/assignments/components/AssignmentsFilters.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import AssignmentItem from '@/features/admin/assignments/components/AssignmentItem.vue'
import AssignmentsFilters from '@/features/admin/assignments/components/AssignmentsFilters.vue'
import AddAssignmentModal from '@/features/admin/assignments/components/modals/AddAssignmentModal.vue'
import AssignmentDetailsModal from '@/features/admin/assignments/components/modals/AssignmentDetailsModal.vue'
import AssignmentSubmissionsModal from '@/features/admin/assignments/components/modals/AssignmentSubmissionsModal.vue'
import AssignmentSubmissionDetailsModal from '@/features/admin/assignments/components/modals/AssignmentSubmissionDetailsModal.vue'
import AssignmentDetailsModal from '@/features/admin/assignments/components/modals/AssignmentDetailsModal.vue'
import {
adminAssignmentsKeys,
useAdminAssignmentsListQuery,
useDeleteAdminAssignmentMutation,
} from '@/services/query/admin-assignments'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
const queryClient = useQueryClient()
const { openModal, isModal } = useModal()
@@ -57,12 +57,11 @@
<script setup>
import { computed, ref } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import DropdownMenu from '@/components/DropdownMenu.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { CONSULTATION_STATUS } from '@/enums'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import DropdownMenu from '@/components/DropdownMenu.vue'
const props = defineProps({
consultation: { type: Object, required: true },
@@ -47,13 +47,12 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { CONSULTATION_STATUS } from '@/enums'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { CONSULTATION_STATUS } from '@/enums'
const props = defineProps({
modelValue: {
@@ -48,21 +48,20 @@
</template>
<script setup>
import useModal from '@/composables/useModal'
import { computed, nextTick, ref, watch } from 'vue'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
adminConsultationsKeys,
useAdminConsultationQuery,
useSendAdminConsultationMessageMutation,
} from '@/services/query/admin-consultations'
import { formatJalaaliDate } from '@/utils/date-utils'
defineOptions({ name: 'ConsultationDetailsModal' })
@@ -41,24 +41,23 @@
<script setup>
import { computed, ref } from 'vue'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import ConsultationsFilters from '@/features/admin/consultations/components/ConsultationsFilters.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import ConsultationItem from '@/features/admin/consultations/components/ConsultationItem.vue'
import ConsultationsFilters from '@/features/admin/consultations/components/ConsultationsFilters.vue'
import ConsultationDetailsModal from '@/features/admin/consultations/components/modals/ConsultationDetailsModal.vue'
import {
adminConsultationsKeys,
useAdminConsultationsListQuery,
useChangeAdminConsultationStatusMutation,
} from '@/services/query/admin-consultations'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
const { openModal, isModal } = useModal()
const queryClient = useQueryClient()
@@ -82,11 +82,10 @@
<script setup>
import { computed } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import CircleButton from '@/components/CircleButton.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
course: { type: Object, required: true },
@@ -56,12 +56,11 @@
<script setup>
import { computed, ref, watch } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
const props = defineProps({
@@ -86,18 +86,17 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import CircleButton from '@/components/CircleButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { useAdminUsersListQuery } from '@/services/query/admin-users'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
adminCourseTemplatesKeys,
useAddAdminTemplateStudentMutation,
@@ -104,31 +104,31 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { toast } from 'vue3-toastify'
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import useModal from '@/composables/useModal'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import ImageCropper from '@/components/form/ImageCropper.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import useYup from '@/composables/useYup'
import { objectToFormData } from '@/utils/object-to-formdata'
import { useUploadTemporaryMutation } 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,
useUpdateAdminCourseMutation,
} from '@/services/query/admin-courses'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import useDebounce from '@/composables/useDebounce'
import { offeredCourseSchema } from '@/features/admin/courses/schema'
import { objectToFormData } from '@/utils/object-to-formdata'
defineOptions({ name: 'AddOfferedCourseModal' })
@@ -83,18 +83,17 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import CircleButton from '@/components/CircleButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
adminCourseTemplatesKeys,
useAdminTemplateSessionsQuery,
@@ -150,21 +150,21 @@
<script setup>
import { computed, ref } from 'vue'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import CircleButton from '@/components/CircleButton.vue'
import TabsBlock from '@/components/blocks/TabsBlock.vue'
import { usePagination } from '@/composables/usePagination'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import TabsBlock from '@/components/blocks/TabsBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import CourseSessionItem from '@/features/admin/courses/components/CourseSessionItem.vue'
import useModal from '@/composables/useModal'
import { usePagination } from '@/composables/usePagination'
import {
adminCourseTemplatesKeys,
useAdminCourseTemplateQuery,
@@ -172,7 +172,6 @@ import {
useAdminTemplateStudentsQuery,
useRemoveAdminTemplateStudentMutation,
} from '@/services/query/admin-course-templates'
import { formatJalaaliDate } from '@/utils/date-utils'
defineOptions({ name: 'CourseDetailsModal' })
@@ -123,21 +123,25 @@
</template>
<script setup>
import { toast } from 'vue3-toastify'
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue3-toastify'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import ImageCropper from '@/components/form/ImageCropper.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import useDebounce from '@/composables/useDebounce'
import { objectToFormData } from '@/utils/object-to-formdata'
import TextareaField from '@/components/form/TextareaField.vue'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import { useAdminUsersListQuery } from '@/services/query/admin-users'
import { courseTemplateSchema } from '@/features/admin/courses/schema'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import {
adminCourseTemplatesKeys,
useAddAdminCourseTemplateMutation,
@@ -145,10 +149,6 @@ import {
useAdminCourseTemplatesListQuery,
useUpdateAdminCourseTemplateMutation,
} from '@/services/query/admin-course-templates'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import { useAdminUsersListQuery } from '@/services/query/admin-users'
import { courseTemplateSchema } from '@/features/admin/courses/schema'
import { objectToFormData } from '@/utils/object-to-formdata'
const route = useRoute()
const router = useRouter()
@@ -83,36 +83,35 @@
<script setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import TabsBlock from '@/components/blocks/TabsBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import CoursesFilters from '@/features/admin/courses/components/CoursesFilters.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import TabsBlock from '@/components/blocks/TabsBlock.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import CourseItem from '@/features/admin/courses/components/CourseItem.vue'
import AddOfferedCourseModal from '@/features/admin/courses/components/modals/AddOfferedCourseModal.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import CoursesFilters from '@/features/admin/courses/components/CoursesFilters.vue'
import CourseDetailsModal from '@/features/admin/courses/components/modals/CourseDetailsModal.vue'
import AddOfferedCourseModal from '@/features/admin/courses/components/modals/AddOfferedCourseModal.vue'
import AddCourseStudentModal from '@/features/admin/courses/components/modals/AddCourseStudentModal.vue'
import AddSessionToCourseModal from '@/features/admin/courses/components/modals/AddSessionToCourseModal.vue'
import {
adminCourseTemplatesKeys,
useAdminCourseTemplatesListQuery,
useChangeAdminCourseTemplateStatusMutation,
useDeleteAdminCourseTemplateMutation,
} from '@/services/query/admin-course-templates'
import {
adminCoursesKeys,
useAdminCoursesListQuery,
useChangeAdminCourseStatusMutation,
useDeleteAdminCourseMutation,
} from '@/services/query/admin-courses'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
import {
adminCourseTemplatesKeys,
useAdminCourseTemplatesListQuery,
useChangeAdminCourseTemplateStatusMutation,
useDeleteAdminCourseTemplateMutation,
} from '@/services/query/admin-course-templates'
const router = useRouter()
const queryClient = useQueryClient()
@@ -76,11 +76,10 @@
<script setup>
import { computed } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import CircleButton from '@/components/CircleButton.vue'
const props = defineProps({
exam: { type: Object, required: true },
@@ -114,9 +114,8 @@
<script setup>
import { computed } from 'vue'
import SelectField from '@/components/form/SelectField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import SelectField from '@/components/form/SelectField.vue'
const props = defineProps({
modelValue: { type: Array, default: () => [] },
@@ -49,14 +49,13 @@
<script setup>
import { computed, ref, watch } from 'vue'
import useDebounce from '@/composables/useDebounce'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import useDebounce from '@/composables/useDebounce'
const props = defineProps({
modelValue: {
@@ -1,68 +1,54 @@
<template>
<BasicModal width="95%" max-width="60rem" min-width="auto" :show-close-button="true">
<BasicModal width="95%" max-width="64rem" min-width="auto" :show-close-button="true">
<template #default="{ close }">
<div class="exam-details">
<LineTitleBlock title="جزئیات آزمون" title-en="Exam Details" />
<LineTitleBlock title="جزئیات آزمون" title-en="Details" />
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
<template v-else-if="exam">
<div class="exam-details__head">
<p class="exam-details__title">{{ exam.title || '—' }}</p>
<p class="exam-details__sub">
<span>دوره: {{ exam.courseTemplate?.title || exam.courseTemplateTitle || '—' }}</span>
<span class="exam-details__sep">|</span>
<span>جلسه: {{ exam.session?.title || exam.sessionTitle || '—' }}</span>
</p>
<div class="exam-details__summary">
<div class="exam-details__summary-grid">
<div v-for="summary in summaryItems" :key="summary.title" class="exam-details__cell">
<LineInfoBlock
:title="summary.title"
:desc="summary.numeric ? '' : summary.value"
:numeric-desc="summary.numeric ? summary.value : ''"
/>
</div>
</div>
<div class="exam-details__description">
<LineInfoBlock title="توضیحات آزمون" :desc="exam.description || '—'" />
</div>
</div>
<div class="exam-details__grid">
<LineInfoBlock
title="مدت آزمون"
:numeric-desc="exam.durationMinutes != null ? `${exam.durationMinutes} دقیقه` : '—'"
/>
<LineInfoBlock title="حداقل نمره قبولی" :numeric-desc="exam.passingScore ?? ''" />
<LineInfoBlock
title="تعداد سوالات"
:numeric-desc="exam.questionsCount ?? questionsLength"
/>
<LineInfoBlock title="رندوم" :desc="exam.randomize ? 'بله' : 'خیر'" />
<LineInfoBlock
title="تاریخ اعتبار"
:numeric-desc="formatJalaaliDate(exam.endDate) || '—'"
/>
<LineInfoBlock
title="تاریخ ثبت"
:numeric-desc="exam.faCreatedAt || formatJalaaliDate(exam.createdAt) || '—'"
/>
</div>
<div v-if="exam.description" class="exam-details__desc">
<LineInfoBlock title="توضیحات" :desc="exam.description" />
</div>
<div v-if="exam.questions?.length" class="exam-details__questions">
<LineTitleBlock title="سوالات" title-en="Questions" />
<div v-for="(q, index) in exam.questions" :key="q.id" class="exam-details__question">
<p class="exam-details__question-title">
<span class="exam-details__question-index">{{ index + 1 }}.</span>
{{ q.title }}
<span v-if="q.score" class="exam-details__question-score">
({{ q.score }} نمره)
<div class="exam-details__questions">
<div
v-for="question in displayQuestions"
:key="question.id"
class="exam-details__question"
>
<div class="exam-details__question-head">
<p class="exam-details__question-title">
<span class="exam-details__question-order">{{ question.order }} :</span>
{{ question.title }}
</p>
<span v-if="question.score" class="exam-details__question-score">
{{ question.score }} نمره
</span>
</p>
<ul class="exam-details__answers">
<li
v-for="(a, ai) in q.answers || []"
:key="a.id"
</div>
<div class="exam-details__answers">
<div
v-for="answer in question.answers"
:key="answer.id"
class="exam-details__answer"
:class="{
'exam-details__answer--correct': String(q.correctAnswerId) === String(a.id),
}"
>
<span class="exam-details__answer-index">{{ optionLabel(ai) }}:</span>
{{ a.title }}
</li>
</ul>
<span
class="exam-details__answer-dot"
:class="{ 'exam-details__answer-dot--correct': answer.isCorrect }"
/>
<p class="exam-details__answer-text">{{ answer.title }}</p>
</div>
</div>
</div>
</div>
</template>
@@ -71,7 +57,7 @@
<div class="exam-details__divider" />
<div class="exam-details__footer">
<BaseButton text="بستن" custom-class="exam-details__close-btn" @click="close">
<BaseButton text="تایید" custom-class="exam-details__confirm-btn" @click="close">
<template #appendIcon>
<SvgIcon name="arrow-left" :size="18" color="#fff" />
</template>
@@ -84,17 +70,15 @@
<script setup>
import { computed } from 'vue'
import useModal from '@/composables/useModal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import NoItems from '@/components/blocks/NoItems.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { useAdminExamQuery } from '@/services/query/admin-exams'
import { formatJalaaliDate } from '@/utils/date-utils'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
defineOptions({ name: 'ExamDetailsModal' })
@@ -107,10 +91,65 @@ const { data: exam, isLoading } = useAdminExamQuery(examId, {
enabled: () => !!examId.value,
})
const questionsLength = computed(() => exam.value?.questions?.length ?? 0)
const summaryItems = computed(() => {
const e = exam.value || {}
return [
{ title: 'عنوان آزمون', value: e.title || '—', numeric: false },
{
title: 'جلسه مرتبط',
value: e.session?.title || e.sessionTitle || '—',
numeric: false,
},
{
title: 'دوره مرتبط',
value: e.courseTemplate?.title || e.courseTemplateTitle || '—',
numeric: false,
},
{ title: 'وضعیت', value: e.statusLabel || e.faStatus || e.status || '—', numeric: false },
{
title: 'مدت زمان',
value: e.durationMinutes == null ? '—' : `${e.durationMinutes} دقیقه`,
numeric: true,
},
{
title: 'تعداد سوالات',
value:
e.questionsCount == null
? e.questions?.length
? `${e.questions.length} سوال`
: '—'
: `${e.questionsCount} سوال`,
numeric: true,
},
]
})
const labels = ['گزینه اول', 'گزینه دوم', 'گزینه سوم', 'گزینه چهارم']
const optionLabel = (index) => labels[index] || `گزینه ${index + 1}`
const displayQuestions = computed(() => {
const raw = exam.value?.questions || []
return raw.map((question, index) => {
const answersSource =
(Array.isArray(question.answers) && question.answers) ||
(Array.isArray(question.options) && question.options) ||
[]
const answers = answersSource.map((answer, ai) => {
const id = answer?.id ?? `${question?.id || index + 1}-${ai + 1}`
const title =
(typeof answer === 'string' && answer) ||
answer?.title ||
answer?.text ||
answer?.label ||
'—'
return { id, title, isCorrect: String(id) === String(question?.correctAnswerId) }
})
return {
id: question?.id ?? `question-${index + 1}`,
order: index + 1,
title: question?.title || question?.question || '—',
score: question?.score ?? '',
answers,
}
})
})
</script>
<style lang="scss" scoped>
@@ -118,105 +157,122 @@ const optionLabel = (index) => labels[index] || `گزینه ${index + 1}`
width: 100%;
text-align: start;
&__head {
margin: 1rem 0 0.5rem;
&__summary {
margin-block: 0.5rem 0.75rem;
padding: 1rem 0.875rem;
border-radius: 1.25rem;
}
&__title {
font-family: var(--font-family-fa);
font-size: 1.1rem;
font-weight: 500;
color: #4b4b4b;
margin: 0 0 0.25rem;
}
&__sub {
font-family: var(--font-family-fa);
font-size: 0.75rem;
color: #535353;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
&__sep {
color: #c4c4c4;
}
&__grid {
&__summary-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.25rem;
gap: 0.5rem;
@media (min-width: 640px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
@media (min-width: 1280px) {
grid-template-columns: repeat(6, 1fr);
}
}
&__cell :deep(.line-info__title),
&__cell :deep(.line-info__value) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__description {
margin-top: 0.75rem;
padding-top: 0.75rem;
:deep(.line-info__title),
:deep(.line-info__desc) {
white-space: normal;
word-break: break-word;
}
}
&__desc,
&__questions {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-block: 1rem;
border-block-start: 1px solid #eaeaea;
padding-block-start: 1.25rem;
}
&__question {
padding: 0.625rem 0;
border-block-end: 1px solid #f0f0f0;
background: #fff;
border-radius: 1.25rem;
padding: 0.875rem 1rem;
}
&:last-child {
border: none;
}
&__question-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.625rem;
margin-bottom: 0.75rem;
}
&__question-title {
font-family: var(--font-family-fa);
font-size: 0.9rem;
color: #4b4b4b;
margin: 0 0 0.375rem;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.6;
color: #a7a7a7;
margin: 0;
flex: 1;
min-width: 0;
}
&__question-index {
color: #b3b3b3;
&__question-order {
margin-inline-end: 0.25rem;
}
&__question-score {
margin-inline-start: 0.25rem;
color: #007074;
flex-shrink: 0;
font-family: var(--font-family-en);
font-size: 0.75rem;
color: #9c9c9c;
}
&__answers {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
flex-wrap: wrap;
gap: 0.625rem 3rem;
}
&__answer {
font-family: var(--font-family-fa);
font-size: 0.85rem;
color: #4b4b4b;
padding: 0.25rem 0.625rem;
border-radius: 0.5rem;
background: #fafafa;
display: inline-flex;
align-items: baseline;
gap: 0.375rem;
}
&__answer-dot {
display: inline-block;
width: 0.75rem;
height: 0.75rem;
border-radius: 0.25rem;
border: 1px solid #b1b1b1;
background: transparent;
flex-shrink: 0;
&--correct {
background: rgba(0, 154, 18, 8%);
color: #009a12;
font-weight: 500;
border-color: #007074;
background: #007074;
}
}
&__answer-index {
color: #9c9c9c;
margin-inline-end: 0.375rem;
&__answer-text {
font-family: var(--font-family-fa);
font-size: 0.875rem;
line-height: 1.6;
color: #4b4b4b;
margin: 0;
}
&__divider {
@@ -229,8 +285,9 @@ const optionLabel = (index) => labels[index] || `گزینه ${index + 1}`
justify-content: flex-end;
}
&__close-btn {
&__confirm-btn {
min-width: 10rem;
padding: 0 2rem;
}
}
</style>
@@ -1,77 +1,95 @@
<template>
<BasicModal width="95%" max-width="80rem" min-width="auto" :show-close-button="true">
<BasicModal width="95%" max-width="76rem" min-width="auto" :show-close-button="true">
<template #default="{ close }">
<div class="participant-details">
<LineTitleBlock title="جزئیات شرکتکننده" title-en="Participant Details" />
<p v-if="participant" class="participant-details__name">
{{ participantName }}
</p>
<LineTitleBlock title="شرکت کنندگان" title-en="Participants" />
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
<div v-else-if="attempts.length > 0" class="participant-details__list">
<div v-for="attempt in attempts" :key="attempt.id" class="participant-details__attempt">
<button
type="button"
class="participant-details__attempt-head"
@click="toggle(attempt.id)"
>
<SvgIcon
:name="isOpen(attempt.id) ? 'caret-down' : 'caret-left'"
:size="18"
color="#d8d8d8"
/>
<div class="participant-details__attempt-info">
<p class="participant-details__attempt-title">{{ attempt.title || 'تلاش' }}</p>
<div class="participant-details__attempt-meta">
<span>دوره: {{ attempt.courseTitle || '—' }}</span>
<span class="participant-details__sep">|</span>
<span>جلسه: {{ attempt.sessionTitle || '—' }}</span>
<span class="participant-details__sep">|</span>
<span>تاریخ: {{ attemptDate(attempt) }}</span>
<span
class="participant-details__score"
:class="`participant-details__score--${attempt.scoreTone || 'neutral'}`"
>
نمره: {{ attempt.score ?? '—' }}
</span>
</div>
</div>
</button>
<p v-if="participantName" class="participant-details__name">{{ participantName }}</p>
<div v-if="isOpen(attempt.id)" class="participant-details__body">
<div
v-for="(question, qIdx) in attempt.questions || []"
:key="question.id"
class="participant-details__question"
<div class="participant-details__card">
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
<div v-else-if="attempts.length > 0" class="participant-details__list">
<div v-for="attempt in attempts" :key="attempt.id" class="participant-details__attempt">
<button
type="button"
class="participant-details__attempt-head"
@click="toggle(attempt.id)"
>
<p class="participant-details__question-title">
{{ qIdx + 1 }}- {{ question.title }}
</p>
<div class="participant-details__answers">
<span
v-for="(answer, aIdx) in question.answers || []"
:key="answer.id"
class="participant-details__answer"
:class="answerClass(question, answer)"
>
<span class="participant-details__answer-index">{{ aIdx + 1 }}.</span>
{{ answer.title }}
</span>
<div class="participant-details__caret">
<SvgIcon
:name="isOpen(attempt.id) ? 'caret-down' : 'caret-left'"
:size="24"
color="#d8d8d8"
/>
</div>
<div class="participant-details__attempt-info">
<p class="participant-details__attempt-title">{{ attempt.title }}</p>
<div class="participant-details__attempt-meta">
<div class="participant-details__meta-item">
<span class="participant-details__meta-label">دوره :</span>
<span class="participant-details__meta-value">{{ attempt.courseTitle }}</span>
</div>
<div class="participant-details__meta-item">
<span class="participant-details__meta-label">جلسه :</span>
<span class="participant-details__meta-value">
{{ attempt.sessionTitle }}
</span>
</div>
<div class="participant-details__meta-item">
<span class="participant-details__meta-label">تاریخ :</span>
<span
class="participant-details__meta-value participant-details__meta-value--en"
>
{{ attempt.faDate }}
</span>
</div>
<div
class="participant-details__score"
:class="`participant-details__score--${attempt.tone}`"
>
<span class="participant-details__meta-label">نمره :</span>
<span class="participant-details__score-value">{{ attempt.score }}</span>
</div>
</div>
</div>
</button>
<div v-if="isOpen(attempt.id)" class="participant-details__body">
<div
v-for="(question, qIdx) in attempt.questions"
:key="question.id"
class="participant-details__question"
>
<p class="participant-details__question-title">
{{ qIdx + 1 }}- {{ question.title }}
</p>
<div class="participant-details__answers">
<div
v-for="answer in question.answers"
:key="answer.id"
class="participant-details__answer"
>
<span
class="participant-details__answer-dot"
:class="answerStateClass(answer.id, question)"
/>
<p class="participant-details__answer-text">{{ answer.title }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<NoItems v-else title="موردی یافت نشد" desc="تلاشی برای این شرکت‌کننده ثبت نشده است." />
<NoItems v-else title="موردی یافت نشد" desc="تلاشی برای این شرکت‌کننده ثبت نشده است." />
<div class="participant-details__divider" />
<div class="participant-details__footer">
<BaseButton text="بستن" custom-class="participant-details__close-btn" @click="close">
<template #appendIcon>
<SvgIcon name="arrow-left" :size="18" color="#fff" />
</template>
</BaseButton>
<div class="participant-details__divider" />
<div class="participant-details__footer">
<BaseButton text="تایید" custom-class="participant-details__confirm-btn" @click="close">
<template #appendIcon>
<SvgIcon name="arrow-left" :size="18" color="#fff" />
</template>
</BaseButton>
</div>
</div>
</div>
</template>
@@ -79,20 +97,27 @@
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import useModal from '@/composables/useModal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import { useAdminExamParticipantQuery } from '@/services/query/admin-exams'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { useAdminExamParticipantQuery } from '@/services/query/admin-exams'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
defineOptions({ name: 'ExamParticipantDetailsModal' })
const TONE_MAP = {
good: 'success',
success: 'success',
bad: 'danger',
danger: 'danger',
neutral: 'neutral',
}
const { getModal } = useModal()
const modalData = computed(() => getModal('ExamParticipantDetailsModal')?.data ?? {})
@@ -104,16 +129,22 @@ const { data: detail, isLoading } = useAdminExamParticipantQuery(examId, partici
enabled: () => !!examId.value && !!participantId.value,
})
const attempts = computed(() => detail.value?.attempts || detail.value?.data?.attempts || [])
const rawAttempts = computed(
() => detail.value?.attempts || detail.value?.data?.attempts || participant.value?.attempts || []
)
const opened = ref(new Set())
const isOpen = (id) => opened.value.has(id)
const toggle = (id) => {
const next = new Set(opened.value)
if (next.has(id)) next.delete(id)
else next.add(id)
opened.value = next
}
const attempts = computed(() =>
rawAttempts.value.map((attempt, index) => ({
id: attempt.id ?? index + 1,
title: attempt.title || `آزمون مرتبه ${index + 1}`,
courseTitle: attempt.courseTitle || '—',
sessionTitle: attempt.sessionTitle || '—',
faDate: attempt.faDate || formatJalaaliDate(attempt.date) || '—',
score: attempt.score ?? '—',
tone: TONE_MAP[attempt.scoreTone] || 'neutral',
questions: attempt.questions || [],
}))
)
const participantName = computed(() => {
const p = participant.value
@@ -121,14 +152,26 @@ const participantName = computed(() => {
return `${p.firstName || ''} ${p.lastName || ''}`.trim() || p.fullName || ''
})
const attemptDate = (attempt) => attempt.faDate || formatJalaaliDate(attempt.date) || '—'
const openId = ref(null)
const isOpen = (id) => openId.value === id
const toggle = (id) => {
openId.value = openId.value === id ? null : id
}
const answerClass = (question, answer) => {
const isCorrect = String(question.correctAnswerId) === String(answer.id)
const isUserChoice = String(question.userAnswerId) === String(answer.id)
if (isUserChoice && isCorrect) return 'participant-details__answer--correct'
if (isUserChoice && !isCorrect) return 'participant-details__answer--wrong'
if (isCorrect) return 'participant-details__answer--correct-outline'
watch(
attempts,
(list) => {
if (openId.value == null && list.length > 0) openId.value = list[0].id
},
{ immediate: true }
)
const answerStateClass = (answerId, question) => {
const isSelected = String(question.userAnswerId ?? question.selectedAnswerId) === String(answerId)
const isCorrect = String(question.correctAnswerId) === String(answerId)
if (isSelected && isCorrect) return 'participant-details__answer-dot--selected-correct'
if (isCorrect) return 'participant-details__answer-dot--correct'
if (isSelected) return 'participant-details__answer-dot--wrong'
return ''
}
</script>
@@ -145,15 +188,26 @@ const answerClass = (question, answer) => {
margin: 0.5rem 0 1rem;
}
&__card {
border: 1px solid #e7e2dd;
border-radius: 1.75rem;
background: #fff;
padding: 1rem;
@media (min-width: 640px) {
padding: 1.5rem;
}
}
&__list {
display: flex;
flex-direction: column;
gap: 1rem;
gap: 1.25rem;
}
&__attempt {
border: 1px solid #e7e2dd;
border-radius: 1.5rem;
border-radius: 1.75rem;
overflow: hidden;
background: #fff;
}
@@ -161,129 +215,184 @@ const answerClass = (question, answer) => {
&__attempt-head {
display: flex;
align-items: flex-start;
gap: 0.75rem;
gap: 1rem;
width: 100%;
text-align: start;
padding: 1rem 1.25rem;
border: none;
background: transparent;
cursor: pointer;
text-align: start;
@media (min-width: 640px) {
padding: 1.25rem 1.5rem;
}
}
&__caret {
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 0.25rem;
}
&__attempt-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
&__attempt-title {
font-family: var(--font-family-fa);
font-weight: 500;
font-size: 1rem;
font-size: 1.125rem;
line-height: 1.7;
color: #494949;
margin: 0 0 0.5rem;
margin: 0;
}
&__attempt-meta {
display: flex;
flex-wrap: wrap;
gap: 0.625rem 1.25rem;
font-family: var(--font-family-fa);
font-size: 0.75rem;
align-items: center;
column-gap: 1.25rem;
row-gap: 0.5rem;
color: #8e8e8e;
}
&__sep {
color: #d6d6d6;
&__meta-item {
white-space: nowrap;
font-size: 0.875rem;
line-height: 1.5;
}
&__meta-label {
font-family: var(--font-family-fa);
font-weight: 300;
margin-inline-end: 0.25rem;
}
&__meta-value {
font-family: var(--font-family-fa);
color: #7c7c7c;
}
&__meta-value--en {
font-family: var(--font-family-en);
}
&__score {
padding: 0.25rem 0.875rem;
padding: 0.375rem 0.75rem;
border-radius: 0.55rem;
font-family: var(--font-family-fa);
font-size: 0.75rem;
font-size: 0.875rem;
line-height: 1.3;
&--good {
background: rgba(0, 154, 18, 8%);
color: #009a12;
&--success {
background: #e8f8ee;
color: #169c56;
}
&--danger {
background: #fdebeb;
color: #cc2831;
}
&--neutral {
background: rgba(180, 180, 180, 8%);
background: #f4f4f4;
color: #8e8e8e;
}
&--bad {
background: rgba(204, 40, 49, 8%);
color: var(--color-error);
}
}
&__score-value {
font-family: var(--font-family-en);
font-weight: 500;
}
&__body {
padding: 1rem 1.25rem 1.5rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
gap: 2rem;
@media (min-width: 640px) {
padding: 1.25rem 1.5rem 1.5rem;
}
}
&__question-title {
font-family: var(--font-family-fa);
font-weight: 300;
font-size: 0.875rem;
line-height: 1.75;
color: #bdbdbd;
margin: 0 0 0.75rem;
margin: 0 0 1.25rem;
}
&__answers {
display: flex;
flex-wrap: wrap;
gap: 0.625rem 1rem;
column-gap: 3rem;
row-gap: 1.25rem;
}
&__answer {
border: 1px solid #e7e2dd;
border-radius: 0.875rem;
padding: 0.375rem 0.875rem;
font-family: var(--font-family-fa);
font-size: 0.85rem;
color: #4b4b4b;
display: inline-flex;
align-items: center;
align-items: baseline;
gap: 0.375rem;
}
&__answer-dot {
display: inline-block;
width: 0.75rem;
height: 0.75rem;
border-radius: 0.25rem;
border: 1px solid #d4d4d4;
background: transparent;
flex-shrink: 0;
&--correct {
background: rgba(0, 154, 18, 8%);
border-color: rgba(0, 154, 18, 40%);
color: #009a12;
}
&--correct-outline {
border-color: rgba(0, 154, 18, 40%);
color: #009a12;
border-color: #007074;
background: #007074;
}
&--wrong {
background: rgba(204, 40, 49, 8%);
border-color: rgba(204, 40, 49, 40%);
color: var(--color-error);
border-color: #cc2831;
background: #cc2831;
}
&--selected-correct {
border-color: #9a9a9a;
background: #9a9a9a;
}
}
&__answer-index {
font-family: var(--font-family-en);
color: #b3b3b3;
&__answer-text {
font-family: var(--font-family-fa);
font-size: 1rem;
line-height: 1.7;
color: #5b5b5b;
margin: 0;
}
&__divider {
border-block-end: 1px solid var(--color-thd-gray);
margin-block: 1.25rem;
border-block-end: 1px solid #e7e2dd;
margin-block: 1.5rem 1rem;
}
&__footer {
display: flex;
justify-content: flex-end;
margin-top: 0.75rem;
}
&__close-btn {
min-width: 12rem;
&__confirm-btn {
min-width: 11.5rem;
padding: 0 2rem;
}
}
</style>
@@ -63,18 +63,17 @@
<script setup>
import { computed, ref } from 'vue'
import useModal from '@/composables/useModal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import { usePagination } from '@/composables/usePagination'
import { useAdminExamParticipantsQuery } from '@/services/query/admin-exams'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import { usePagination } from '@/composables/usePagination'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import { useAdminExamParticipantsQuery } from '@/services/query/admin-exams'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
defineOptions({ name: 'ExamParticipantsModal' })
+10 -10
View File
@@ -104,28 +104,28 @@
</template>
<script setup>
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import TextField from '@/components/form/TextField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import { examSchema } from '@/features/admin/exams/schema'
import SelectField from '@/components/form/SelectField.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import ExamQuestionBuilder from '@/features/admin/exams/components/ExamQuestionBuilder.vue'
import useYup from '@/composables/useYup'
import {
adminExamsKeys,
useAddAdminExamMutation,
useAdminExamQuery,
useUpdateAdminExamMutation,
} from '@/services/query/admin-exams'
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
import useDebounce from '@/composables/useDebounce'
import { examSchema } from '@/features/admin/exams/schema'
const route = useRoute()
const router = useRouter()
@@ -50,27 +50,26 @@
<script setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import ExamsFilters from '@/features/admin/exams/components/ExamsFilters.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import ExamItem from '@/features/admin/exams/components/ExamItem.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import ExamsFilters from '@/features/admin/exams/components/ExamsFilters.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import ExamDetailsModal from '@/features/admin/exams/components/modals/ExamDetailsModal.vue'
import ExamParticipantsModal from '@/features/admin/exams/components/modals/ExamParticipantsModal.vue'
import ExamParticipantDetailsModal from '@/features/admin/exams/components/modals/ExamParticipantDetailsModal.vue'
import ExamDetailsModal from '@/features/admin/exams/components/modals/ExamDetailsModal.vue'
import {
adminExamsKeys,
useAdminExamsListQuery,
useDeleteAdminExamMutation,
} from '@/services/query/admin-exams'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
const router = useRouter()
const queryClient = useQueryClient()
@@ -46,10 +46,9 @@
<script setup>
import { computed } from 'vue'
import { TICKET_STATUS } from '@/enums'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { TICKET_STATUS } from '@/enums'
import { formatJalaaliDate } from '@/utils/date-utils'
const props = defineProps({
@@ -46,14 +46,13 @@
</template>
<script setup>
import { TICKET_STATUS } from '@/enums'
import { computed, ref, watch } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { TICKET_STATUS } from '@/enums'
const props = defineProps({
modelValue: {
@@ -1,6 +1,6 @@
<template>
<BasicModal width="95%" max-width="64rem" min-width="auto" :show-close-button="true">
<template #default="{ close }">
<template #default>
<div class="ticket-details">
<LineTitleBlock title="جزئیات تیکت" title-en="Ticket Details" />
@@ -103,8 +103,16 @@
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { objectToFormData } from '@/utils/object-to-formdata'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
autoUpdate,
computePosition,
@@ -112,20 +120,11 @@ import {
offset as offsetMiddleware,
shift,
} from '@floating-ui/dom'
import BasicModal from '@/components/BasicModal.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import {
adminTicketsKeys,
useAdminTicketQuery,
useSendAdminTicketMessageMutation,
} from '@/services/query/admin-tickets'
import { formatJalaaliDate } from '@/utils/date-utils'
import { objectToFormData } from '@/utils/object-to-formdata'
defineOptions({ name: 'TicketDetailsModal' })
@@ -37,19 +37,18 @@
<script setup>
import { computed, ref } from 'vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TicketsFilters from '@/features/admin/messages/components/TicketsFilters.vue'
import TicketItem from '@/features/admin/messages/components/TicketItem.vue'
import TicketDetailsModal from '@/features/admin/messages/components/modals/TicketDetailsModal.vue'
import { useAdminTicketsListQuery } from '@/services/query/admin-tickets'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import { useAdminTicketsListQuery } from '@/services/query/admin-tickets'
import TicketItem from '@/features/admin/messages/components/TicketItem.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import TicketsFilters from '@/features/admin/messages/components/TicketsFilters.vue'
import TicketDetailsModal from '@/features/admin/messages/components/modals/TicketDetailsModal.vue'
const { openModal, isModal } = useModal()
@@ -61,12 +61,11 @@
<script setup>
import { computed, ref } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import DropdownMenu from '@/components/DropdownMenu.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { SERVICE_STATUS, SERVICE_TYPE } from '@/enums'
import { formatJalaaliDate } from '@/utils/date-utils'
import DropdownMenu from '@/components/DropdownMenu.vue'
const props = defineProps({
service: { type: Object, required: true },
@@ -46,14 +46,13 @@
</template>
<script setup>
import { SERVICE_STATUS } from '@/enums'
import { computed, ref, watch } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { SERVICE_STATUS } from '@/enums'
const props = defineProps({
modelValue: {
@@ -66,13 +66,12 @@
<script setup>
import { computed, ref } from 'vue'
import { toast } from 'vue3-toastify'
import { SERVICE_STATUS } from '@/enums'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import SelectField from '@/components/form/SelectField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { SERVICE_STATUS } from '@/enums'
defineOptions({ name: 'SendMessageModal' })
@@ -37,12 +37,12 @@
</template>
<script setup>
import BasicModal from '@/components/BasicModal.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { SERVICE_TYPE } from '@/enums'
import useModal from '@/composables/useModal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
defineOptions({ name: 'ServiceDetailsModal' })
@@ -36,16 +36,15 @@
<script setup>
import { computed, ref } from 'vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import ServicesFilters from '@/features/admin/services/components/ServicesFilters.vue'
import ServiceItem from '@/features/admin/services/components/ServiceItem.vue'
import ServiceDetailsModal from '@/features/admin/services/components/modals/ServiceDetailsModal.vue'
import SendMessageModal from '@/features/admin/services/components/modals/SendMessageModal.vue'
import useModal from '@/composables/useModal'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import ServiceItem from '@/features/admin/services/components/ServiceItem.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import ServicesFilters from '@/features/admin/services/components/ServicesFilters.vue'
import SendMessageModal from '@/features/admin/services/components/modals/SendMessageModal.vue'
import ServiceDetailsModal from '@/features/admin/services/components/modals/ServiceDetailsModal.vue'
const { openModal, isModal } = useModal()
@@ -72,11 +72,10 @@
<script setup>
import { computed } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { SESSION_TYPE } from '@/enums'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import CircleButton from '@/components/CircleButton.vue'
const props = defineProps({
session: { type: Object, required: true },
@@ -56,16 +56,15 @@
</template>
<script setup>
import { SESSION_TYPE } from '@/enums'
import { computed, ref, watch } from 'vue'
import useDebounce from '@/composables/useDebounce'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import useDebounce from '@/composables/useDebounce'
import { SESSION_TYPE } from '@/enums'
const props = defineProps({
modelValue: {
@@ -1,6 +1,6 @@
<template>
<BasicModal width="95%" max-width="60rem" min-width="auto" :show-close-button="true">
<template #default="{ data, close }">
<template #default="{ close }">
<div class="session-attendance">
<LineTitleBlock title="حضور و غیاب" title-en="Attendance" />
@@ -77,21 +77,20 @@
<script setup>
import { computed, ref, watch } from 'vue'
import useModal from '@/composables/useModal'
import useDebounce from '@/composables/useDebounce'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import SelectField from '@/components/form/SelectField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import NoItems from '@/components/blocks/NoItems.vue'
import SelectField from '@/components/form/SelectField.vue'
import { usePagination } from '@/composables/usePagination'
import { useAdminSessionAttendanceQuery } from '@/services/query/admin-sessions'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
import { useAdminCoursesListQuery } from '@/services/query/admin-courses'
import useDebounce from '@/composables/useDebounce'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import { useAdminSessionAttendanceQuery } from '@/services/query/admin-sessions'
defineOptions({ name: 'SessionAttendanceModal' })
@@ -111,18 +111,17 @@
<script setup>
import { computed } from 'vue'
import useModal from '@/composables/useModal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import { SESSION_PLATFORM, SESSION_TYPE } from '@/enums'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import { useAdminSessionQuery } from '@/services/query/admin-sessions'
import { SESSION_PLATFORM, SESSION_TYPE } from '@/enums'
import { formatJalaaliDate } from '@/utils/date-utils'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
defineOptions({ name: 'SessionDetailsModal' })
@@ -218,34 +218,34 @@
</template>
<script setup>
import { toast } from 'vue3-toastify'
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue3-toastify'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import { SESSION_PLATFORM, SESSION_TYPE } from '@/enums'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import ImageCropper from '@/components/form/ImageCropper.vue'
import FileUploader from '@/components/form/FileUploader.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import useDebounce from '@/composables/useDebounce'
import { objectToFormData } from '@/utils/object-to-formdata'
import TextareaField from '@/components/form/TextareaField.vue'
import { sessionSchema } from '@/features/admin/sessions/schema'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import DatePickerField from '@/components/form/DatePickerField.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import {
adminSessionsKeys,
useAddAdminSessionMutation,
useAdminSessionQuery,
useUpdateAdminSessionMutation,
} from '@/services/query/admin-sessions'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import { SESSION_PLATFORM, SESSION_TYPE } from '@/enums'
import { sessionSchema } from '@/features/admin/sessions/schema'
import { objectToFormData } from '@/utils/object-to-formdata'
const route = useRoute()
const router = useRouter()
@@ -49,26 +49,25 @@
<script setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import SessionsFilters from '@/features/admin/sessions/components/SessionsFilters.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SessionItem from '@/features/admin/sessions/components/SessionItem.vue'
import SessionAttendanceModal from '@/features/admin/sessions/components/modals/SessionAttendanceModal.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import SessionsFilters from '@/features/admin/sessions/components/SessionsFilters.vue'
import SessionDetailsModal from '@/features/admin/sessions/components/modals/SessionDetailsModal.vue'
import SessionAttendanceModal from '@/features/admin/sessions/components/modals/SessionAttendanceModal.vue'
import {
adminSessionsKeys,
useAdminSessionsListQuery,
useDeleteAdminSessionMutation,
} from '@/services/query/admin-sessions'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
const router = useRouter()
const queryClient = useQueryClient()
@@ -78,12 +78,11 @@
<script setup>
import { computed } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import CircleButton from '@/components/CircleButton.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
const props = defineProps({
term: { type: Object, required: true },
@@ -47,12 +47,11 @@
<script setup>
import { computed, ref, watch } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
const props = defineProps({
modelValue: {
@@ -83,24 +83,23 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import CircleButton from '@/components/CircleButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { useAdminUsersListQuery } from '@/services/query/admin-users'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
adminTermsKeys,
useAddAdminTermStudentsMutation,
useAdminTermStudentsQuery,
useRemoveAdminTermStudentMutation,
} from '@/services/query/admin-terms'
import { useAdminUsersListQuery } from '@/services/query/admin-users'
defineOptions({ name: 'AddTermStudentModal' })
@@ -86,17 +86,16 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import useDebounce from '@/composables/useDebounce'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import CircleButton from '@/components/CircleButton.vue'
import { adminTermsKeys } from '@/services/query/admin-terms'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
import {
adminCoursesKeys,
@@ -104,7 +103,6 @@ import {
useAdminCoursesListQuery,
useDeleteAdminCourseMutation,
} from '@/services/query/admin-courses'
import { adminTermsKeys } from '@/services/query/admin-terms'
defineOptions({ name: 'AttachCourseToTermModal' })
@@ -138,22 +138,28 @@
<script setup>
import { computed, ref } from 'vue'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import TabsBlock from '@/components/blocks/TabsBlock.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import CircleButton from '@/components/CircleButton.vue'
import TabsBlock from '@/components/blocks/TabsBlock.vue'
import { usePagination } from '@/composables/usePagination'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import CourseItem from '@/features/admin/courses/components/CourseItem.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
adminCoursesKeys,
useAdminCoursesListQuery,
useChangeAdminCourseStatusMutation,
useDeleteAdminCourseMutation,
} from '@/services/query/admin-courses'
import {
adminTermsKeys,
useAdminTermQuery,
@@ -161,13 +167,6 @@ import {
useRemoveAdminTermStudentMutation,
useToggleAdminTermStudentLeaveMutation,
} from '@/services/query/admin-terms'
import {
adminCoursesKeys,
useAdminCoursesListQuery,
useChangeAdminCourseStatusMutation,
useDeleteAdminCourseMutation,
} from '@/services/query/admin-courses'
import { formatJalaaliDate } from '@/utils/date-utils'
defineOptions({ name: 'TermDetailsModal' })
+11 -11
View File
@@ -100,28 +100,28 @@
</template>
<script setup>
import { toast } from 'vue3-toastify'
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue3-toastify'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import TextField from '@/components/form/TextField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import ImageCropper from '@/components/form/ImageCropper.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import TextField from '@/components/form/TextField.vue'
import { termSchema } from '@/features/admin/terms/schema'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import ImageCropper from '@/components/form/ImageCropper.vue'
import { objectToFormData } from '@/utils/object-to-formdata'
import TextareaField from '@/components/form/TextareaField.vue'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import DatePickerField from '@/components/form/DatePickerField.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import {
adminTermsKeys,
useAddAdminTermMutation,
useAdminTermQuery,
useUpdateAdminTermMutation,
} from '@/services/query/admin-terms'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import { termSchema } from '@/features/admin/terms/schema'
import { objectToFormData } from '@/utils/object-to-formdata'
const route = useRoute()
const router = useRouter()
@@ -53,22 +53,23 @@
<script setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TermsFilters from '@/features/admin/terms/components/TermsFilters.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import TermItem from '@/features/admin/terms/components/TermItem.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import TermsFilters from '@/features/admin/terms/components/TermsFilters.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import TermDetailsModal from '@/features/admin/terms/components/modals/TermDetailsModal.vue'
import AddTermStudentModal from '@/features/admin/terms/components/modals/AddTermStudentModal.vue'
import AttachCourseToTermModal from '@/features/admin/terms/components/modals/AttachCourseToTermModal.vue'
import AddOfferedCourseModal from '@/features/admin/courses/components/modals/AddOfferedCourseModal.vue'
import CourseDetailsModal from '@/features/admin/courses/components/modals/CourseDetailsModal.vue'
import AddOfferedCourseModal from '@/features/admin/courses/components/modals/AddOfferedCourseModal.vue'
import AttachCourseToTermModal from '@/features/admin/terms/components/modals/AttachCourseToTermModal.vue'
import {
adminTermsKeys,
useAdminTermsListQuery,
@@ -76,8 +77,6 @@ import {
useCloneAdminTermMutation,
useDeleteAdminTermMutation,
} from '@/services/query/admin-terms'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
const router = useRouter()
const queryClient = useQueryClient()
@@ -66,14 +66,13 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import TextField from '@/components/form/TextField.vue'
import SelectField from '@/components/form/SelectField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useAdminRolesListQuery } from '@/services/query/admin-users'
import { ROLE_LABELS } from '@/enums'
import { computed, ref, watch } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SelectField from '@/components/form/SelectField.vue'
import { useAdminRolesListQuery } from '@/services/query/admin-users'
const props = defineProps({
modelValue: {
@@ -68,11 +68,10 @@
<script setup>
import { computed, ref } from 'vue'
import { useQueryClient } from '@tanstack/vue-query'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import DropdownMenu from '@/components/DropdownMenu.vue'
import { CHANGEABLE_ROLES, ROLE_LABELS } from '@/enums'
import BaseButton from '@/components/BaseButton.vue'
import { CHANGEABLE_ROLES, ROLE_LABELS } from '@/enums'
import DropdownMenu from '@/components/DropdownMenu.vue'
import {
adminUsersKeys,
useAdminRolesListQuery,
@@ -92,16 +92,16 @@
<script setup>
import { computed } from 'vue'
import useModal from '@/composables/useModal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { useAdminUserQuery } from '@/services/query/admin-users'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
EDUCATION_STATUS,
GENDER,
@@ -110,7 +110,6 @@ import {
SEMINARY_LEVEL,
UNIVERSITY_LEVEL,
} from '@/enums'
import { formatJalaaliDate } from '@/utils/date-utils'
defineOptions({ name: 'UserDetailsModal' })
@@ -80,11 +80,10 @@
<script setup>
import { computed } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import CircleButton from '@/components/CircleButton.vue'
const props = defineProps({
user: { type: Object, required: true },
@@ -50,11 +50,10 @@
<script setup>
import { computed, ref, watch } from 'vue'
import TextField from '@/components/form/TextField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import CircleButton from '@/components/CircleButton.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
const props = defineProps({
modelValue: {
@@ -30,8 +30,8 @@
<script setup>
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
defineOptions({ name: 'RejectReasonDetailsModal' })
@@ -67,20 +67,20 @@
<script setup>
import { ref } from 'vue'
import useYup from '@/composables/useYup'
import { PRIORITY_STATUS } from '@/enums'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import TextField from '@/components/form/TextField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import TextField from '@/components/form/TextField.vue'
import { rejectionReasonSchema } from '../../../schema'
import SelectField from '@/components/form/SelectField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import {
adminPendingStudentsKeys,
useChangePendingStudentStatusMutation,
} from '@/services/query/admin-pending-students'
import SelectField from '@/components/form/SelectField.vue'
import { PRIORITY_STATUS } from '@/enums'
import { rejectionReasonSchema } from '../../../schema'
defineOptions({ name: 'RejectReasonModal' })
@@ -225,14 +225,17 @@
<script setup>
import { computed, ref } from 'vue'
import useModal from '@/composables/useModal'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import CircleButton from '@/components/CircleButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useModal from '@/composables/useModal'
import {
usePendingStudentQuery,
useDownloadPendingStudentPdfMutation,
} from '@/services/query/admin-pending-students'
import {
AUDIENCE_CONFLICT_RESPONSE,
COMPETING_PROPAGATOR_RESPONSE,
@@ -247,11 +250,6 @@ import {
USER_STATUS,
VERIFICATION_MEDIA_TYPE,
} from '@/enums'
import { formatJalaaliDate } from '@/utils/date-utils'
import {
usePendingStudentQuery,
useDownloadPendingStudentPdfMutation,
} from '@/services/query/admin-pending-students'
defineOptions({ name: 'UserVerificationDetailsModal' })
@@ -297,12 +295,6 @@ const leaderMessage = computed(() =>
verificationMedia.value.find((m) => m.type === VERIFICATION_MEDIA_TYPE.LEADER_MESSAGE)
)
const statusLabel = (status) => {
if (status === 'pending') return 'در انتظار تایید'
if (status === 'rejected') return 'رد شده'
return USER_STATUS[status] || status || ''
}
const pdfLoading = ref(false)
const pdfMutation = useDownloadPendingStudentPdfMutation()
+14 -14
View File
@@ -219,21 +219,26 @@
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue3-toastify'
import useYup from '@/composables/useYup'
import { useRoute, useRouter } from 'vue-router'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import SelectField from '@/components/form/SelectField.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import DatePickerField from '@/components/form/DatePickerField.vue'
import PasswordField from '@/components/form/PasswordField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import ImageCropper from '@/components/form/ImageCropper.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import { GENDER, MARITAL_STATUS, ROLE_LABELS } from '@/enums'
import { objectToFormData } from '@/utils/object-to-formdata'
import TextareaField from '@/components/form/TextareaField.vue'
import PasswordField from '@/components/form/PasswordField.vue'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import DatePickerField from '@/components/form/DatePickerField.vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import { userAddSchema, userEditSchema } from '@/features/admin/users/schema'
import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/query/common'
import {
adminUsersKeys,
useAddAdminUserMutation,
@@ -241,11 +246,6 @@ import {
useAdminUserQuery,
useUpdateAdminUserMutation,
} from '@/services/query/admin-users'
import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/query/common'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import { GENDER, MARITAL_STATUS, ROLE_LABELS } from '@/enums'
import { userAddSchema, userEditSchema } from '@/features/admin/users/schema'
import { objectToFormData } from '@/utils/object-to-formdata'
const route = useRoute()
const router = useRouter()
@@ -41,26 +41,25 @@
<script setup>
import { computed, ref } from 'vue'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import VerificationsFilters from '@/features/admin/users/components/verifications/VerificationsFilters.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import VerificationItem from '@/features/admin/users/components/verifications/VerificationItem.vue'
import UserVerificationDetailsModal from '@/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue'
import VerificationsFilters from '@/features/admin/users/components/verifications/VerificationsFilters.vue'
import RejectReasonModal from '@/features/admin/users/components/verifications/modals/RejectReasonModal.vue'
import RejectReasonDetailsModal from '@/features/admin/users/components/verifications/modals/RejectReasonDetailsModal.vue'
import UserVerificationDetailsModal from '@/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue'
import {
adminPendingStudentsKeys,
useChangePendingStudentStatusMutation,
usePendingStudentsListQuery,
} from '@/services/query/admin-pending-students'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
const queryClient = useQueryClient()
const { openModal, isModal } = useModal()
@@ -48,27 +48,26 @@
<script setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import UsersFilters from '@/features/admin/users/components/UsersFilters.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import UsersTable from '@/features/admin/users/components/UsersTable.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import UsersFilters from '@/features/admin/users/components/UsersFilters.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import UserExamsModal from '@/features/admin/users/components/modals/UserExamsModal.vue'
import UserDetailsModal from '@/features/admin/users/components/modals/UserDetailsModal.vue'
import UserAssignmentsModal from '@/features/admin/users/components/modals/UserAssignmentsModal.vue'
import UserExamsModal from '@/features/admin/users/components/modals/UserExamsModal.vue'
import {
adminUsersKeys,
useAdminUsersListQuery,
useDeleteAdminUserMutation,
} from '@/services/query/admin-users'
import { usePagination } from '@/composables/usePagination'
import useModal from '@/composables/useModal'
const router = useRouter()
const queryClient = useQueryClient()
+1 -2
View File
@@ -1,8 +1,7 @@
import { object, string } from 'yup'
import { passwordRule } from '@/constants/rules/passwordRule'
import { phoneNumberRule } from '@/constants/rules/phoneNumberRule'
import { nationalCodeRule } from '@/constants/rules/nationalCodeRule'
import { passwordRule } from '@/constants/rules/passwordRule'
import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule'
export const rejectionReasonSchema = object().shape({
+1 -1
View File
@@ -13,8 +13,8 @@
</template>
<script setup>
import PatternSection from '@/features/auth/components/PatternSection.vue'
import { gallery } from '@/utils/gallery'
import PatternSection from '@/features/auth/components/PatternSection.vue'
const bgPattern = gallery.loginPattern
const bgPatternMobile = gallery.loginPatternMobile
+7 -8
View File
@@ -65,17 +65,16 @@
<script setup>
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import BaseButton from '@/components/BaseButton.vue'
import TextField from '@/components/form/TextField.vue'
import PasswordField from '@/components/form/PasswordField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import useAuth from '@/composables/useAuth'
import { useLoginMutation } from '@/services/query/auth'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { loginSchema } from '@/features/auth/schema'
import TextField from '@/components/form/TextField.vue'
import { useLoginMutation } from '@/services/query/auth'
import PasswordField from '@/components/form/PasswordField.vue'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
const emit = defineEmits(['next'])
@@ -48,16 +48,15 @@
<script setup>
import { computed, ref } from 'vue'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import BaseButton from '@/components/BaseButton.vue'
import TextField from '@/components/form/TextField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import useAuth from '@/composables/useAuth'
import { useForgotPasswordMutation, useLoginWithCodeMutation } from '@/services/query/auth'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import { loginWithCodeSchema } from '@/features/auth/schema'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import { useForgotPasswordMutation, useLoginWithCodeMutation } from '@/services/query/auth'
const props = defineProps({
mode: { type: String, default: 'loginWithCode' },
+7 -7
View File
@@ -84,16 +84,16 @@
<script setup>
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import BaseButton from '@/components/BaseButton.vue'
import TextField from '@/components/form/TextField.vue'
import PasswordField from '@/components/form/PasswordField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import useAuth from '@/composables/useAuth'
import { useRegisterMutation } from '@/services/query/auth'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { signupSchema } from '@/features/auth/schema'
import TextField from '@/components/form/TextField.vue'
import { useRegisterMutation } from '@/services/query/auth'
import PasswordField from '@/components/form/PasswordField.vue'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
const emit = defineEmits(['next'])
@@ -46,13 +46,16 @@
<script setup>
import { computed, ref } from 'vue'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import BaseButton from '@/components/BaseButton.vue'
import TextField from '@/components/form/TextField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useRouter } from 'vue-router'
import useYup from '@/composables/useYup'
import useAuth from '@/composables/useAuth'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import { verifyCodeSchema } from '@/features/auth/schema'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
import {
useForgotPasswordMutation,
useLoginWithCodeMutation,
@@ -60,9 +63,6 @@ import {
useVerifyCodeMutation,
useVerifyForgotPasswordCodeMutation,
} from '@/services/query/auth'
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
import { useRouter } from 'vue-router'
import { verifyCodeSchema } from '@/features/auth/schema'
const props = defineProps({
phoneNumber: { type: String, default: '' },
@@ -0,0 +1,175 @@
<template>
<form class="educational-info" @submit.prevent="onSubmit">
<LineTitleBlock title="جزئیات تحصیلی" title-en="Educational Details" />
<div class="educational-info__row">
<div class="educational-info__cell">
<SelectField
v-model="form.educationStatus"
name="educationStatus"
label="وضعیت تحصیلی"
:options="educationStatusOptions"
option-label="label"
option-value="value"
:error="errors.educationStatus"
/>
</div>
<div class="educational-info__cell">
<SelectField
v-model="form.seminaryLevel"
name="seminaryLevel"
label="آخرین مدرک تحصیلی حوزوی"
:options="seminaryLevelOptions"
option-label="label"
option-value="value"
:error="errors.seminaryLevel"
/>
</div>
<div class="educational-info__cell">
<SelectField
v-model="form.universityLevel"
name="universityLevel"
label="آخرین مدرک تحصیلی"
:options="universityLevelOptions"
option-label="label"
option-value="value"
:error="errors.universityLevel"
/>
</div>
<div class="educational-info__cell">
<TextField
v-model="form.universityName"
name="universityName"
label="نام حوزه علمیه / دانشگاه"
:error="errors.universityName"
@blur="validateAt('universityName', form.universityName)"
>
<template #appendIcon>
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="educational-info__cell educational-info__cell--full">
<TextField
v-model="form.fieldOfStudy"
name="fieldOfStudy"
label="گرایش تخصصی حوزوی / رشته تحصیلی دانشگاهی"
:error="errors.fieldOfStudy"
@blur="validateAt('fieldOfStudy', form.fieldOfStudy)"
>
<template #appendIcon>
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
</div>
<LineTitleBlock title="مشخصات شغلی" title-en="Job Details" />
<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">
<TextareaField
v-model="form.activitySummary"
name="activitySummary"
label="خلاصه ای از فعالیت های خود را در موقعیت‌های شغلی ذکر شده بنویسید."
:row="isMobile ? 5 : 1"
:error="errors.activitySummary"
@blur="validateAt('activitySummary', form.activitySummary)"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
</div>
<div class="educational-info__divider" />
<StepActions @back="store.goToPrevStep()" />
</form>
</template>
<script setup>
import useYup from '@/composables/useYup'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import TextField from '@/components/form/TextField.vue'
import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
import { educationalInformationSchema } from '@/features/auth/schema/student-register'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
import { EDUCATION_STATUS, SEMINARY_LEVEL, STUDENT_REGISTRATION, UNIVERSITY_LEVEL } from '@/enums'
const store = useStudentRegistrationStore()
const isMobile = ref(window.innerWidth < 768)
const onResize = () => {
isMobile.value = window.innerWidth < 768
}
onMounted(() => window.addEventListener('resize', onResize))
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
const educationStatusOptions = Object.entries(EDUCATION_STATUS).map(([value, label]) => ({
value,
label,
}))
const seminaryLevelOptions = Object.entries(SEMINARY_LEVEL).map(([value, label]) => ({
value,
label,
}))
const universityLevelOptions = Object.entries(UNIVERSITY_LEVEL).map(([value, label]) => ({
value,
label,
}))
const form = ref({ ...store.educational })
const { validate, validateAt, errors } = useYup(educationalInformationSchema)
const onSubmit = async () => {
const { isValid, payload } = await validate(form.value)
if (!isValid) return
store.educational = { ...store.educational, ...payload }
store.markStepAsCompleted(STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION)
store.goToNextStep()
}
store.updateCurrentStep(STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION)
</script>
<style lang="scss" scoped>
.educational-info {
&__row {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem 0.625rem;
margin-bottom: 1rem;
@media (min-width: 1024px) {
grid-template-columns: repeat(4, 1fr);
}
}
&__cell--full {
grid-column: 1 / -1;
}
&__divider {
border-block-end: 1px solid var(--color-thd-gray);
margin: 1rem 0 1.5rem;
}
}
</style>
@@ -0,0 +1,364 @@
<template>
<div class="media-recorder" :class="`media-recorder--${kind}`">
<div class="media-recorder__frame">
<div v-if="previewUrl" class="media-recorder__preview">
<video
v-if="kind === 'video'"
ref="previewEl"
:src="previewUrl"
controls
playsinline
class="media-recorder__media"
/>
<audio
v-else
ref="previewEl"
:src="previewUrl"
controls
class="media-recorder__media media-recorder__media--audio"
/>
</div>
<div v-else-if="isRecording" class="media-recorder__live">
<video
v-if="kind === 'video'"
ref="liveEl"
autoplay
muted
playsinline
class="media-recorder__media"
/>
<div v-else class="media-recorder__pulse">
<SvgIcon name="paper-plane-right" :size="32" color="var(--color-primary)" />
</div>
<div class="media-recorder__timer">{{ formattedTime }}</div>
</div>
<div v-else class="media-recorder__empty">
<SvgIcon name="upload" :size="48" color="var(--color-thd-gray)" />
<p class="media-recorder__hint">{{ hintText }}</p>
</div>
</div>
<div class="media-recorder__actions">
<button
v-if="!isRecording && !previewUrl"
type="button"
class="media-recorder__btn media-recorder__btn--start"
:disabled="loading"
@click="onStart"
>
<span v-if="loading">در حال بارگذاری...</span>
<span v-else>شروع ضبط</span>
</button>
<button
v-if="isRecording"
type="button"
class="media-recorder__btn media-recorder__btn--stop"
@click="onStop"
>
پایان ضبط
</button>
<button
v-if="previewUrl && !loading"
type="button"
class="media-recorder__btn media-recorder__btn--reset"
@click="onReset"
>
ضبط دوباره
</button>
</div>
<p v-if="error" class="media-recorder__error">{{ error }}</p>
</div>
<PermissionModal v-if="isModal('RegisterPermissionModal')" />
</template>
<script setup>
import { toast } from 'vue3-toastify'
import useModal from '@/composables/useModal'
import { computed, onBeforeUnmount, ref } from 'vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { objectToFormData } from '@/utils/object-to-formdata'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import PermissionModal from '@/features/auth/components/studentRegister/PermissionModal.vue'
const props = defineProps({
modelValue: { type: Object, default: null },
kind: {
type: String,
default: 'audio',
validator: (v) => ['audio', 'video'].includes(v),
},
maxSeconds: { type: Number, default: 120 },
context: { type: String, default: 'verification' },
subType: { type: String, default: '' },
error: { type: String, default: '' },
})
const emit = defineEmits(['update:modelValue', 'uploaded'])
const { openModal, isModal } = useModal()
const previewUrl = ref(props.modelValue?.url || null)
const isRecording = ref(false)
const elapsedMs = ref(0)
const loading = ref(false)
const liveEl = ref(null)
const previewEl = ref(null)
let mediaRecorder = null
let mediaStream = null
let chunks = []
let timerHandle = null
let timerStart = 0
const hintText = computed(() =>
props.kind === 'video' ? 'ویدیوی خود را ضبط کنید' : 'صدای خود را ضبط کنید'
)
const formattedTime = computed(() => {
const s = Math.floor(elapsedMs.value / 1000)
const mm = String(Math.floor(s / 60)).padStart(2, '0')
const ss = String(s % 60).padStart(2, '0')
return `${mm}:${ss}`
})
const uploadMutation = useUploadTemporaryMutation()
const stopTracks = () => {
if (mediaStream) {
mediaStream.getTracks().forEach((t) => t.stop())
mediaStream = null
}
}
const clearTimer = () => {
if (timerHandle) {
clearInterval(timerHandle)
timerHandle = null
}
}
const onStart = async () => {
try {
const constraints = props.kind === 'video' ? { audio: true, video: true } : { audio: true }
mediaStream = await navigator.mediaDevices.getUserMedia(constraints)
if (props.kind === 'video' && liveEl.value) {
liveEl.value.srcObject = mediaStream
}
chunks = []
mediaRecorder = new window.MediaRecorder(mediaStream)
mediaRecorder.ondataavailable = (e) => {
if (e.data?.size > 0) chunks.push(e.data)
}
mediaRecorder.onstop = () => handleStop()
mediaRecorder.start()
isRecording.value = true
elapsedMs.value = 0
timerStart = Date.now()
timerHandle = setInterval(() => {
elapsedMs.value = Date.now() - timerStart
if (elapsedMs.value >= props.maxSeconds * 1000) onStop()
}, 250)
} catch {
openModal('RegisterPermissionModal', { kind: props.kind })
}
}
const onStop = () => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop()
}
clearTimer()
}
const handleStop = async () => {
isRecording.value = false
stopTracks()
const mime = props.kind === 'video' ? 'video/webm' : 'audio/webm'
const blob = new Blob(chunks, { type: mime })
const ext = props.kind === 'video' ? 'webm' : 'webm'
const file = new File([blob], `recording.${ext}`, { type: mime, lastModified: Date.now() })
if (previewUrl.value && previewUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl.value)
}
previewUrl.value = URL.createObjectURL(blob)
loading.value = true
try {
const fd = objectToFormData({
file,
subType: props.subType || props.kind,
context: props.context,
})
const response = await uploadMutation.mutateAsync(fd)
const payload = response?.data ?? response
const value = {
url: previewUrl.value,
uploadId: payload?.uploadId || payload?.id,
...payload,
}
emit('update:modelValue', value)
emit('uploaded', value)
} catch {
toast.error('بارگذاری فایل با خطا مواجه شد')
} finally {
loading.value = false
}
}
const onReset = () => {
if (previewUrl.value && previewUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl.value)
}
previewUrl.value = null
emit('update:modelValue', null)
}
onBeforeUnmount(() => {
stopTracks()
clearTimer()
if (previewUrl.value && previewUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl.value)
}
})
</script>
<style lang="scss" scoped>
.media-recorder {
display: flex;
flex-direction: column;
gap: 0.625rem;
width: 100%;
&__frame {
position: relative;
aspect-ratio: 3 / 4;
border-radius: 1rem;
background: #f3f4f6;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
&--audio &__frame {
aspect-ratio: 1;
}
&__empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: var(--color-thd-gray);
}
&__hint {
font-family: var(--font-family-fa);
font-size: 0.75rem;
margin: 0;
}
&__live,
&__preview {
width: 100%;
height: 100%;
position: relative;
}
&__media {
width: 100%;
height: 100%;
object-fit: cover;
background: #000;
}
&__media--audio {
width: 100%;
height: 2.5rem;
background: transparent;
position: absolute;
inset-block-end: 0.75rem;
inset-inline: 0.5rem;
width: calc(100% - 1rem);
}
&__pulse {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
animation: pulse 1.5s ease-in-out infinite;
}
&__timer {
position: absolute;
inset-block-start: 0.5rem;
inset-inline-start: 0.5rem;
background: rgba(0, 0, 0, 60%);
color: #fff;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-family: var(--font-family-en);
font-size: 0.75rem;
}
&__actions {
display: flex;
justify-content: center;
gap: 0.5rem;
}
&__btn {
border: none;
border-radius: 9999px;
padding: 0.5rem 1rem;
font-family: var(--font-family-fa);
font-size: 0.875rem;
cursor: pointer;
transition: opacity 0.15s ease;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--start {
background: var(--color-primary);
color: #fff;
}
&--stop {
background: var(--color-error);
color: #fff;
}
&--reset {
background: transparent;
color: var(--color-sec-gray);
border: 1px solid var(--color-thd-gray);
}
}
&__error {
font-family: var(--font-family-fa);
font-size: 0.75rem;
color: var(--color-error);
margin: 0;
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
</style>
@@ -0,0 +1,63 @@
<template>
<BasicModal
width="90%"
max-width="28rem"
min-width="auto"
:closable="false"
:show-close-button="false"
>
<template #default="{ data, close }">
<div class="permission-modal">
<SvgIcon name="warning" :size="48" color="var(--color-prim-gray)" />
<h3 class="permission-modal__title">دسترسی مورد نیاز</h3>
<p class="permission-modal__desc">{{ message(data) }}</p>
<BaseButton text="متوجه شدم" custom-class="permission-modal__btn" @click="close" />
</div>
</template>
</BasicModal>
</template>
<script setup>
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
defineOptions({ name: 'RegisterPermissionModal' })
const message = (data) => {
if (data?.kind === 'audio') {
return 'برای ضبط صدا، لطفاً دسترسی به میکروفون را فعال کنید.'
}
return 'برای ضبط ویدیو، لطفاً دسترسی به دوربین و میکروفون را فعال کنید.'
}
</script>
<style lang="scss" scoped>
.permission-modal {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 1rem 0;
text-align: center;
&__title {
font-family: var(--font-family-fa);
font-weight: 700;
font-size: 1.125rem;
margin: 0;
}
&__desc {
font-family: var(--font-family-fa);
font-size: 0.875rem;
color: var(--color-prim-gray);
margin: 0;
}
&__btn {
width: 100%;
max-width: 12rem;
}
}
</style>
@@ -0,0 +1,337 @@
<template>
<form class="personal-info" @submit.prevent="onSubmit">
<div class="personal-info__grid">
<div class="personal-info__avatar-col">
<ImageCropper
v-model="avatar"
name="avatar"
bg-color="#eeeeee"
:error="errors.avatar"
@crop="onAvatarCropped"
@error="onAvatarError"
/>
</div>
<div class="personal-info__main-col">
<LineTitleBlock title="مشخصات فردی" title-en="Personal Details" />
<div class="personal-info__row">
<div class="personal-info__cell">
<TextField
v-model="form.firstName"
name="firstName"
label="نام"
:error="errors.firstName"
@blur="validateAt('firstName', form.firstName)"
>
<template #appendIcon>
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="personal-info__cell">
<TextField
v-model="form.lastName"
name="lastName"
label="نام خانوادگی"
:error="errors.lastName"
@blur="validateAt('lastName', form.lastName)"
>
<template #appendIcon>
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="personal-info__cell">
<DatePickerField
v-model="form.birthDate"
name="birthDate"
label="تاریخ تولد"
:max="maxBirthDate"
:error="errors.birthDate"
/>
</div>
<div class="personal-info__cell">
<TextField
v-model="form.nationalCode"
name="nationalCode"
label="کد ملی"
inputmode="numeric"
:convert-digits="true"
:error="errors.nationalCode"
@blur="validateAt('nationalCode', form.nationalCode)"
>
<template #appendIcon>
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="personal-info__cell">
<SelectField
v-model="form.maritalStatus"
name="maritalStatus"
label="وضعیت تاهل"
:options="maritalStatusOptions"
option-label="label"
option-value="value"
:error="errors.maritalStatus"
/>
</div>
<div class="personal-info__cell">
<SelectField
v-model="form.gender"
name="gender"
label="جنسیت"
:options="genderOptions"
option-label="label"
option-value="value"
:error="errors.gender"
/>
</div>
</div>
<LineTitleBlock title="اطلاعات تماس" title-en="Contact Details" />
<div class="personal-info__row">
<div class="personal-info__cell personal-info__cell--full-sm">
<TextField
v-model="form.phoneNumber"
name="phoneNumber"
label="شماره تلفن همراه"
inputmode="numeric"
:convert-digits="true"
:disabled="!!initialPhoneNumber"
:error="errors.phoneNumber"
@blur="validateAt('phoneNumber', form.phoneNumber)"
>
<template #appendIcon>
<SvgIcon name="phone" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="personal-info__cell">
<SelectField
v-model="form.provinceId"
name="provinceId"
label="استان"
:options="provinces"
option-label="name"
option-value="id"
:error="errors.provinceId"
@change="onProvinceChange"
/>
</div>
<div class="personal-info__cell">
<SelectField
v-model="form.cityId"
name="cityId"
label="شهر"
:options="cities"
option-label="name"
option-value="id"
:disabled="!form.provinceId"
:error="errors.cityId"
/>
</div>
<div class="personal-info__cell personal-info__cell--full">
<TextareaField
v-model="form.address"
name="address"
label="آدرس محل سکونت"
:row="isMobile ? 5 : 1"
:error="errors.address"
@blur="validateAt('address', form.address)"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
</div>
</div>
</div>
<div class="personal-info__divider" />
<StepActions :show-back="false" :loading="uploadMutation.isPending.value" />
</form>
</template>
<script setup>
import { toast } from 'vue3-toastify'
import useYup from '@/composables/useYup'
import useAuth from '@/composables/useAuth'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { convertToJalali } from '@/utils/date-utils'
import TextField from '@/components/form/TextField.vue'
import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import ImageCropper from '@/components/form/ImageCropper.vue'
import { objectToFormData } from '@/utils/object-to-formdata'
import TextareaField from '@/components/form/TextareaField.vue'
import { useUploadTemporaryMutation } from '@/services/query/auth'
import DatePickerField from '@/components/form/DatePickerField.vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { GENDER, MARITAL_STATUS, STUDENT_REGISTRATION } from '@/enums'
import { personalInformationSchema } from '@/features/auth/schema/student-register'
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/query/common'
const store = useStudentRegistrationStore()
const { user } = useAuth()
const isMobile = ref(window.innerWidth < 768)
const onResize = () => {
isMobile.value = window.innerWidth < 768
}
onMounted(() => window.addEventListener('resize', onResize))
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
const maritalStatusOptions = Object.entries(MARITAL_STATUS).map(([value, label]) => ({
value,
label,
}))
const genderOptions = Object.entries(GENDER).map(([value, label]) => ({ value, label }))
const maxBirthDate = computed(() => {
const d = new Date()
d.setFullYear(d.getFullYear() - 15)
try {
return convertToJalali(d.toISOString())
} catch {
return d.toISOString()
}
})
const initialPhoneNumber = ref(user.value?.phoneNumber || store.personal.phoneNumber || '')
const form = ref({
firstName: store.personal.firstName || '',
lastName: store.personal.lastName || '',
birthDate: store.personal.birthDate || '',
nationalCode: store.personal.nationalCode || '',
maritalStatus: store.personal.maritalStatus || '',
gender: store.personal.gender || '',
phoneNumber: initialPhoneNumber.value,
provinceId: store.personal.provinceId || null,
cityId: store.personal.cityId || null,
address: store.personal.address || '',
avatar: store.personal.avatar || null,
avatarId: store.personal.avatarId || '',
})
const avatar = ref(store.personal.avatar || null)
const { validate, validateAt, errors } = useYup(personalInformationSchema)
const { data: provinces = ref([]) } = useGetProvincesQuery()
const provinceIdRef = ref(form.value.provinceId)
const { data: cities = ref([]) } = useGetCitiesOfProvinceQuery(provinceIdRef, {
enabled: () => !!provinceIdRef.value,
})
const onProvinceChange = () => {
provinceIdRef.value = form.value.provinceId
form.value.cityId = null
}
watch(
() => form.value.provinceId,
(val) => {
provinceIdRef.value = val
}
)
const uploadMutation = useUploadTemporaryMutation()
const onAvatarCropped = async (file) => {
try {
const fd = objectToFormData({ file, subType: 'avatar', context: 'verification' })
const response = await uploadMutation.mutateAsync(fd)
const payload = response?.data ?? response
const value = { url: payload?.url, uploadId: payload?.uploadId || payload?.id, ...payload }
avatar.value = value
form.value.avatar = value
form.value.avatarId = value.uploadId
} catch {
toast.error('بارگذاری تصویر با خطا مواجه شد')
}
}
const onAvatarError = (msg) => toast.error(msg)
const onSubmit = async () => {
const { isValid, payload } = await validate(form.value)
if (!isValid) return
store.personal = { ...store.personal, ...payload, avatar: form.value.avatar }
store.markStepAsCompleted(STUDENT_REGISTRATION.PERSONAL_INFORMATION)
store.goToNextStep()
}
store.updateCurrentStep(STUDENT_REGISTRATION.PERSONAL_INFORMATION)
</script>
<style lang="scss" scoped>
.personal-info {
display: flex;
flex-direction: column;
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
@media (min-width: 1024px) {
grid-template-columns: 3fr 9fr;
}
}
&__avatar-col {
order: 2;
@media (min-width: 1024px) {
order: 1;
padding: 0.5rem;
}
}
&__main-col {
order: 1;
@media (min-width: 1024px) {
order: 2;
padding-inline-start: 1.25rem;
}
}
&__row {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem 0.625rem;
margin-bottom: 1rem;
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1280px) {
grid-template-columns: repeat(3, 1fr);
}
}
&__cell--full {
grid-column: 1 / -1;
}
&__cell--full-sm {
@media (max-width: 767px) {
grid-column: 1 / -1;
}
}
&__divider {
border-block-end: 1px solid var(--color-thd-gray);
margin: 1rem 0 1.5rem;
}
}
</style>
@@ -0,0 +1,236 @@
<template>
<form class="professional-info" @submit.prevent="onSubmit">
<LineTitleBlock title="مشخصات شغلی" title-en="Job Details" />
<div class="professional-info__row">
<div class="professional-info__cell professional-info__cell--5">
<TextField
v-model="form.propagationExperienceYears"
name="propagationExperienceYears"
label="سابقه فعالیت تبلیغی شما چند سال می باشد؟"
inputmode="numeric"
:convert-digits="true"
:error="errors.propagationExperienceYears"
@blur="validateAt('propagationExperienceYears', form.propagationExperienceYears)"
>
<template #appendIcon>
<SvgIcon name="check-square" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="professional-info__cell professional-info__cell--7">
<TextareaField
v-model="form.propagationMethodDescription"
name="propagationMethodDescription"
label="لطفا توضیحاتی ناظر بر مدل فعالیت تبلیغی خود بفرمایید."
:row="isMobile ? 5 : 1"
:error="errors.propagationMethodDescription"
@blur="validateAt('propagationMethodDescription', form.propagationMethodDescription)"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
<div class="professional-info__cell professional-info__cell--full">
<TextareaField
v-model="form.specializedTopics"
name="specializedTopics"
label="موضوعات تخصصی تبلیغی خود را بفرمایید."
:row="isMobile ? 5 : 1"
:error="errors.specializedTopics"
@blur="validateAt('specializedTopics', form.specializedTopics)"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
<div class="professional-info__cell professional-info__cell--5">
<SelectField
v-model="form.propagationPlatforms"
name="propagationPlatforms"
label="در چه بستر هایی فعالیت تبلیغی داشته اید؟"
:options="platformOptions"
option-label="label"
option-value="value"
:multiple="true"
:error="errors.propagationPlatforms"
@change="onPlatformsChange"
/>
</div>
<div
:class="[
'professional-info__cell',
'professional-info__cell--7',
{ 'professional-info__cell--blurred': !hasOther },
]"
>
<TextareaField
v-model="form.otherPlatformDetails"
name="otherPlatformDetails"
label="نوع فعالیت خود را توضیح دهید."
:row="isMobile ? 5 : 1"
:disabled="!hasOther"
:error="errors.otherPlatformDetails"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
<div
:class="[
'professional-info__cell',
'professional-info__cell--full',
{ 'professional-info__cell--blurred': !hasOnline },
]"
>
<TextareaField
v-model="form.onlinePlatformDetails"
name="onlinePlatformDetails"
label="نام شبکه و مدل فعالیت تبلیغی خود را ذکر کنید."
:row="isMobile ? 5 : 1"
:disabled="!hasOnline"
:error="errors.onlinePlatformDetails"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
</div>
<div class="professional-info__divider" />
<StepActions @back="store.goToPrevStep()" />
</form>
</template>
<script setup>
import useYup from '@/composables/useYup'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import TextareaField from '@/components/form/TextareaField.vue'
import { PROPAGATION_PLATFORM, STUDENT_REGISTRATION } from '@/enums'
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
import { professionalInformationSchema } from '@/features/auth/schema/student-register'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
const store = useStudentRegistrationStore()
const isMobile = ref(window.innerWidth < 768)
const onResize = () => {
isMobile.value = window.innerWidth < 768
}
onMounted(() => window.addEventListener('resize', onResize))
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
const platformOptions = Object.entries(PROPAGATION_PLATFORM).map(([value, label]) => ({
value,
label,
}))
const seedPlatforms = (store.professional.propagationPlatforms || []).map((p) => p.platform || p)
const seedOther =
store.professional.propagationPlatforms?.find((p) => p.platform === 'other')?.platformDetails ||
''
const seedOnline =
store.professional.propagationPlatforms?.find((p) => p.platform === 'online')?.platformDetails ||
''
const form = ref({
propagationExperienceYears: store.professional.propagationExperienceYears || '',
propagationMethodDescription: store.professional.propagationMethodDescription || '',
specializedTopics: store.professional.specializedTopics || '',
propagationPlatforms: seedPlatforms,
otherPlatformDetails: seedOther,
onlinePlatformDetails: seedOnline,
})
const hasOther = computed(() => form.value.propagationPlatforms.includes('other'))
const hasOnline = computed(() => form.value.propagationPlatforms.includes('online'))
const onPlatformsChange = (selected) => {
let next = Array.isArray(selected) ? [...selected] : []
const hasNone = next.includes('none')
const hasOthers = next.some((v) => v !== 'none')
if (hasNone && hasOthers) {
const lastSelected = next.at(-1)
next = lastSelected === 'none' ? ['none'] : next.filter((v) => v !== 'none')
}
form.value.propagationPlatforms = next
if (!next.includes('online')) form.value.onlinePlatformDetails = ''
if (!next.includes('other')) form.value.otherPlatformDetails = ''
}
const { validate, validateAt, errors } = useYup(professionalInformationSchema)
const onSubmit = async () => {
const { isValid, payload } = await validate(form.value)
if (!isValid) return
const propagationPlatforms = payload.propagationPlatforms.map((platform) => {
if (platform === 'online') {
return { platform: 'online', platformDetails: payload.onlinePlatformDetails || '' }
}
if (platform === 'other') {
return { platform: 'other', platformDetails: payload.otherPlatformDetails || '' }
}
return { platform, platformDetails: '' }
})
store.professional = {
propagationExperienceYears: Number(payload.propagationExperienceYears),
propagationMethodDescription: payload.propagationMethodDescription,
specializedTopics: payload.specializedTopics,
propagationPlatforms,
}
store.markStepAsCompleted(STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION)
store.goToNextStep()
}
store.updateCurrentStep(STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION)
</script>
<style lang="scss" scoped>
.professional-info {
&__row {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem 0.625rem;
margin-bottom: 1rem;
@media (min-width: 1024px) {
grid-template-columns: repeat(12, 1fr);
}
}
&__cell--5 {
@media (min-width: 1024px) {
grid-column: span 5;
}
}
&__cell--7 {
@media (min-width: 1024px) {
grid-column: span 7;
}
}
&__cell--full {
grid-column: 1 / -1;
}
&__cell--blurred {
filter: blur(2px);
transition: filter 0.2s ease;
}
&__divider {
border-block-end: 1px solid var(--color-thd-gray);
margin: 1rem 0 1.5rem;
}
}
</style>
@@ -0,0 +1,126 @@
<template>
<aside class="sidebar-steps">
<div class="sidebar-steps__mobile-logo">
<img :src="logoPinkishRed" alt="banu" />
</div>
<div class="sidebar-steps__panel">
<div class="sidebar-steps__logo">
<img :src="logoWhite" alt="banu" />
</div>
<ul class="sidebar-steps__list">
<li
v-for="tab in tabs"
:key="tab.key"
class="sidebar-steps__item"
:class="{ 'sidebar-steps__item--active': activeTab === tab.key }"
@click="emit('update:active-tab', tab.key)"
>
<SvgIcon :name="tab.icon" :size="22" color="#fff" />
</li>
</ul>
</div>
</aside>
</template>
<script setup>
import { gallery } from '@/utils/gallery'
import SvgIcon from '@/components/icons/SvgIcon.vue'
defineProps({
tabs: { type: Array, required: true },
activeTab: { type: String, default: '' },
})
const emit = defineEmits(['update:active-tab'])
const logoWhite = gallery.logoWhite
const logoPinkishRed = gallery.logoPinkishRed
</script>
<style lang="scss" scoped>
.sidebar-steps {
height: 100%;
&__mobile-logo {
display: flex;
justify-content: center;
margin-bottom: 0.5rem;
img {
height: 2.5rem;
object-fit: contain;
}
@media (min-width: 1024px) {
display: none;
}
}
&__panel {
background: var(--color-primary);
border-radius: 1rem;
padding: 0.75rem;
height: 100%;
display: flex;
align-items: center;
gap: 1rem;
@media (min-width: 1024px) {
flex-direction: column;
align-items: center;
gap: 1.5rem;
padding: 1.25rem 0.75rem;
border-radius: 1.5rem;
}
}
&__logo {
display: none;
@media (min-width: 1024px) {
display: flex;
justify-content: center;
padding-top: 0.5rem;
}
img {
height: 3.5rem;
object-fit: contain;
}
}
&__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 1rem;
flex: 1;
@media (min-width: 1024px) {
flex-direction: column;
justify-content: flex-start;
gap: 1.25rem;
padding-top: 1rem;
}
}
&__item {
cursor: pointer;
opacity: 0.5;
transition: opacity 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
&--active,
&:hover {
opacity: 1;
}
}
}
</style>
@@ -0,0 +1,25 @@
<template>
<SkillSelectStep
:step="STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_1"
store-key="skill1"
select-field="responseToLowSatisfaction"
desc-field="responseToLowSatisfactionDescription"
:intro="intro"
:options="options"
:schema="skillAssessmentPart1Schema"
/>
</template>
<script setup>
import { LOW_SATISFACTION_RESPONSE, STUDENT_REGISTRATION } from '@/enums'
import { skillAssessmentPart1Schema } from '@/features/auth/schema/student-register'
import SkillSelectStep from '@/features/auth/components/studentRegister/SkillSelectStep.vue'
const intro =
'اگر عملکرد شما در جلسات خانگی مورد ارزیابی توسط مخاطبین‌تان قرار بگیرد - با فرض اینکه شما به صحت و درستی روش، مسیر و مبانی خود ایمان و اعتقاد دارید - و در نهایت میزان رضایت‌شان پایین باشد، در این شرایط:'
const options = Object.entries(LOW_SATISFACTION_RESPONSE).map(([value, label]) => ({
value,
label,
}))
</script>
@@ -0,0 +1,25 @@
<template>
<SkillSelectStep
:step="STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_2"
store-key="skill2"
select-field="responseToSessionCancellation"
desc-field="responseToSessionCancellationDescription"
:intro="intro"
:options="options"
:schema="skillAssessmentPart2Schema"
/>
</template>
<script setup>
import { SESSION_CANCELLATION_RESPONSE, STUDENT_REGISTRATION } from '@/enums'
import { skillAssessmentPart2Schema } from '@/features/auth/schema/student-register'
import SkillSelectStep from '@/features/auth/components/studentRegister/SkillSelectStep.vue'
const intro =
'در شرایطی که صاحب بیت اعلام کند به دلیل تأمین نشدن هزینه‌های جلسه نمی‌تواند برای ادامه آن همکاری کند، در این صورت شما:'
const options = Object.entries(SESSION_CANCELLATION_RESPONSE).map(([value, label]) => ({
value,
label,
}))
</script>
@@ -0,0 +1,25 @@
<template>
<SkillSelectStep
:step="STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_3"
store-key="skill3"
select-field="responseToAudienceConflict"
desc-field="responseToAudienceConflictDescription"
:intro="intro"
:options="options"
:schema="skillAssessmentPart3Schema"
/>
</template>
<script setup>
import { AUDIENCE_CONFLICT_RESPONSE, STUDENT_REGISTRATION } from '@/enums'
import { skillAssessmentPart3Schema } from '@/features/auth/schema/student-register'
import SkillSelectStep from '@/features/auth/components/studentRegister/SkillSelectStep.vue'
const intro =
'اگر در جلسه شما تناقضی بین درخواست برخی از مخاطبین با عده‌ای دیگر بوجود آید به نحوی که مثلاً عده‌ای متقاضی «بیان معارف انقلابی و نقل از امامین انقلاب» و عده‌ای مخالف این کار و طالب «بیان آیات و روایات صرف» باشند / عده‌ای به دنبال شنیدن «صحبت‌های سخنران» و عده‌ای دوستدار شنیدن «صحبت های مداح» باشند، در این صورت شما:'
const options = Object.entries(AUDIENCE_CONFLICT_RESPONSE).map(([value, label]) => ({
value,
label,
}))
</script>
@@ -0,0 +1,25 @@
<template>
<SkillSelectStep
:step="STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_4"
store-key="skill4"
select-field="responseToCompetingPropagator"
desc-field="responseToCompetingPropagatorDescription"
:intro="intro"
:options="options"
:schema="skillAssessmentPart4Schema"
/>
</template>
<script setup>
import { COMPETING_PROPAGATOR_RESPONSE, STUDENT_REGISTRATION } from '@/enums'
import { skillAssessmentPart4Schema } from '@/features/auth/schema/student-register'
import SkillSelectStep from '@/features/auth/components/studentRegister/SkillSelectStep.vue'
const intro =
'در کنار جلسه شما، جلسه‌ای با حضور یک مبلّغ قوی برگزار می‌شود که به دلیل «استفاده از ابزارها و روش‌های نوآورانه»، توانسته در جذب مخاطب قوی باشد، در حالی که می‌دانید «در حوزه مبانی ضعف و نقص دارد» در این صورت شما:'
const options = Object.entries(COMPETING_PROPAGATOR_RESPONSE).map(([value, label]) => ({
value,
label,
}))
</script>
@@ -0,0 +1,99 @@
<template>
<form class="skill-five" @submit.prevent="onSubmit">
<LineTitleBlock title="مهارت‌ها و تخصص‌ها" title-en="Skills & Expertise" />
<div class="skill-five__row">
<TextareaField
v-model="form.responseToUnqualifiedAdvisors"
name="responseToUnqualifiedAdvisors"
label="در بستر و محیط جلسه شما افرادی هستند که بواسطه ارائه اذکار و اوراد به مردم برای حل مسائل و مشکلات زندگی‌شان، محل رجوع شده و توانسته‌اند فضایی ایجاد نمایند که مردم به جای مراجعه به متخصصین در هر موضوع یا تلاش برای حل مشکلات، رجوع به این افراد را ترجیح دهند. این در حالی است که می‌دانید برخی دستورات و تجویز‌های این افراد نادرست بوده و با مقاصد مادی یا غیر موجه به این امور می‌پردازند. متناسب با این موضوع، برنامه و عملکرد شما چیست؟ اگر خواسته باشید توصیه‌های مشابه از اساتید و علمای مطرح یا اذکار توصیه شده را به مردم بگویید چگونه مرز این دو را مشخص می‌کنید؟"
:row="isMobile ? 5 : 1"
:error="errors.responseToUnqualifiedAdvisors"
@blur="validateAt('responseToUnqualifiedAdvisors', form.responseToUnqualifiedAdvisors)"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
<TextareaField
v-model="form.relevantCertificates"
name="relevantCertificates"
label="چه مدارک تحصیلی یا مهارتی و گواهینامه‌هایی را اگر داشته باشید می‌توانید با مخاطب تعامل داشته و بر او اثر بگذارید؟"
:row="isMobile ? 5 : 1"
:error="errors.relevantCertificates"
@blur="validateAt('relevantCertificates', form.relevantCertificates)"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
<TextareaField
v-model="form.hijabApproach"
name="hijabApproach"
label="در مواجهه با «مسئله حجاب در جامعه» یا در مواجهه با «فرد بی‌حجاب در جلسه شما» چه می‌کنید؟ (ایده، منطق و روش خود را بیان فرمایید)"
:row="isMobile ? 5 : 1"
:error="errors.hijabApproach"
@blur="validateAt('hijabApproach', form.hijabApproach)"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
<div class="skill-five__divider" />
<StepActions @back="store.goToPrevStep()" />
</form>
</template>
<script setup>
import useYup from '@/composables/useYup'
import { STUDENT_REGISTRATION } from '@/enums'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import TextareaField from '@/components/form/TextareaField.vue'
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
import { skillAssessmentPart5Schema } from '@/features/auth/schema/student-register'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
const store = useStudentRegistrationStore()
const isMobile = ref(window.innerWidth < 768)
const onResize = () => {
isMobile.value = window.innerWidth < 768
}
onMounted(() => window.addEventListener('resize', onResize))
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
const form = ref({ ...store.skill5 })
const { validate, validateAt, errors } = useYup(skillAssessmentPart5Schema)
const onSubmit = async () => {
const { isValid, payload } = await validate(form.value)
if (!isValid) return
store.skill5 = { ...store.skill5, ...payload }
store.markStepAsCompleted(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_5)
store.goToNextStep()
}
store.updateCurrentStep(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_5)
</script>
<style lang="scss" scoped>
.skill-five {
&__row {
display: flex;
flex-direction: column;
gap: 0.625rem;
margin-bottom: 1rem;
}
&__divider {
border-block-end: 1px solid var(--color-thd-gray);
margin: 1rem 0 1.5rem;
}
}
</style>
@@ -0,0 +1,151 @@
<template>
<form class="skill-six" @submit.prevent="onSubmit">
<div class="skill-six__grid">
<div class="skill-six__media-col">
<MediaRecorder
v-model="audio"
kind="audio"
sub-type="audio"
context="verification"
:max-seconds="180"
:error="errors.faithProductionAudio"
@uploaded="onUploaded"
/>
</div>
<div class="skill-six__content-col">
<LineTitleBlock title="مهارت‌ها و تخصص‌ها" title-en="Skills & Expertise" />
<p class="skill-six__intro">
لطفا ویدئو را مشاهده نمایید و در قالب صوت نظرات و تحلیل خود را بیان کرده و بفرمایید چگونه
میتوان از سرمایه ایمان الهی، عمل صالح تولید نمود؟
</p>
<div v-if="videoUrl" class="skill-six__video-wrap">
<video :src="videoUrl" controls playsinline class="skill-six__video" />
</div>
<div v-else class="skill-six__video-placeholder">
<p>ویدیویی در دسترس نیست</p>
</div>
</div>
</div>
<div class="skill-six__divider" />
<StepActions @back="store.goToPrevStep()" />
</form>
</template>
<script setup>
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import { STUDENT_REGISTRATION } from '@/enums'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { useGetRegistrationVideoQuery } from '@/services/query/auth'
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
import { skillAssessmentPart6Schema } from '@/features/auth/schema/student-register'
import MediaRecorder from '@/features/auth/components/studentRegister/MediaRecorder.vue'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
const store = useStudentRegistrationStore()
const audio = ref(store.skill6.faithProductionAudio || null)
const form = ref({
faithProductionAudio: store.skill6.faithProductionAudio || null,
faithProductionId: store.skill6.faithProductionId || '',
})
const { validate, errors } = useYup(skillAssessmentPart6Schema)
const onUploaded = (value) => {
form.value.faithProductionAudio = value
form.value.faithProductionId = value?.uploadId || ''
}
const { data: media } = useGetRegistrationVideoQuery()
const videoUrl = computed(
() =>
media.value?.faithProductionVideoUrl || store.registrationMedia.faithProductionVideoUrl || ''
)
watch(media, (val) => {
if (val) store.registrationMedia = { ...store.registrationMedia, ...val }
})
const onSubmit = async () => {
const { isValid, payload } = await validate(form.value)
if (!isValid) return
store.skill6 = { ...store.skill6, ...payload }
store.markStepAsCompleted(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6)
store.goToNextStep()
}
store.updateCurrentStep(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6)
</script>
<style lang="scss" scoped>
.skill-six {
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
@media (min-width: 1024px) {
grid-template-columns: 3fr 9fr;
}
}
&__media-col {
order: 2;
@media (min-width: 1024px) {
order: 1;
padding: 0.5rem;
}
}
&__content-col {
order: 1;
@media (min-width: 1024px) {
order: 2;
padding-inline-start: 1.25rem;
}
}
&__intro {
font-family: var(--font-family-fa);
font-weight: 300;
font-size: 0.875rem;
line-height: 1.7;
margin: 0 0 0.75rem;
text-align: justify;
}
&__video-wrap {
width: 100%;
border-radius: 1rem;
overflow: hidden;
background: #000;
}
&__video {
width: 100%;
height: auto;
display: block;
}
&__video-placeholder {
border-radius: 1rem;
background: #f3f4f6;
color: var(--color-thd-gray);
padding: 2rem;
text-align: center;
font-family: var(--font-family-fa);
font-size: 0.875rem;
}
&__divider {
border-block-end: 1px solid var(--color-thd-gray);
margin: 1rem 0 1.5rem;
}
}
</style>
@@ -0,0 +1,167 @@
<template>
<form class="skill-seven" @submit.prevent="onSubmit">
<div class="skill-seven__grid">
<div class="skill-seven__media-col">
<MediaRecorder
v-model="video"
kind="video"
sub-type="video"
context="verification"
:max-seconds="180"
:error="errors.leaderMessageVideo"
@uploaded="onUploaded"
/>
</div>
<div class="skill-seven__content-col">
<LineTitleBlock title="مهارت‌ها و تخصص‌ها" title-en="Skills & Expertise" />
<p class="skill-seven__intro">
لطفا درک و برداشت خود را از پیام رهبری به کنگره ۷ هزار زن شهید را در قالب یک ویدیو برای ما
ضبط کنید.
</p>
<div v-if="videoUrl" class="skill-seven__video-wrap">
<video :src="videoUrl" controls playsinline class="skill-seven__video" />
</div>
<div v-else class="skill-seven__video-placeholder">
<p>ویدیویی در دسترس نیست</p>
</div>
</div>
</div>
<div class="skill-seven__divider" />
<StepActions
:loading="completeMutation.isPending.value"
next-text="تایید اطلاعات"
@back="store.goToPrevStep()"
/>
</form>
</template>
<script setup>
import { toast } from 'vue3-toastify'
import useYup from '@/composables/useYup'
import { computed, ref, watch } from 'vue'
import useAuth from '@/composables/useAuth'
import { STUDENT_REGISTRATION } from '@/enums'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
import { skillAssessmentPart7Schema } from '@/features/auth/schema/student-register'
import MediaRecorder from '@/features/auth/components/studentRegister/MediaRecorder.vue'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
import { useCompleteProfileMutation, useGetRegistrationVideoQuery } from '@/services/query/auth'
const store = useStudentRegistrationStore()
const { applySession } = useAuth()
const video = ref(store.skill7.leaderMessageVideo || null)
const form = ref({
leaderMessageVideo: store.skill7.leaderMessageVideo || null,
leaderMessageId: store.skill7.leaderMessageId || '',
})
const { validate, errors } = useYup(skillAssessmentPart7Schema)
const onUploaded = (value) => {
form.value.leaderMessageVideo = value
form.value.leaderMessageId = value?.uploadId || ''
}
const { data: media } = useGetRegistrationVideoQuery()
const videoUrl = computed(
() => media.value?.leaderMessageVideoUrl || store.registrationMedia.leaderMessageVideoUrl || ''
)
watch(media, (val) => {
if (val) store.registrationMedia = { ...store.registrationMedia, ...val }
})
const completeMutation = useCompleteProfileMutation()
const onSubmit = async () => {
const { isValid, payload } = await validate(form.value)
if (!isValid) return
store.skill7 = { ...store.skill7, ...payload }
store.markStepAsCompleted(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7)
try {
const response = await completeMutation.mutateAsync(store.buildCompletePayload())
const data = response?.data ?? response
applySession(data)
store.isProfileCompleted = true
} catch (error) {
toast.error(error?.response?.data?.message || 'عملیات با خطا مواجه شد')
}
}
store.updateCurrentStep(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7)
</script>
<style lang="scss" scoped>
.skill-seven {
&__grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
@media (min-width: 1024px) {
grid-template-columns: 3fr 9fr;
}
}
&__media-col {
order: 2;
@media (min-width: 1024px) {
order: 1;
padding: 0.5rem;
}
}
&__content-col {
order: 1;
@media (min-width: 1024px) {
order: 2;
padding-inline-start: 1.25rem;
}
}
&__intro {
font-family: var(--font-family-fa);
font-weight: 300;
font-size: 0.875rem;
line-height: 1.7;
margin: 0 0 0.75rem;
text-align: justify;
}
&__video-wrap {
width: 100%;
border-radius: 1rem;
overflow: hidden;
background: #000;
}
&__video {
width: 100%;
height: auto;
display: block;
}
&__video-placeholder {
border-radius: 1rem;
background: #f3f4f6;
color: var(--color-thd-gray);
padding: 2rem;
text-align: center;
font-family: var(--font-family-fa);
font-size: 0.875rem;
}
&__divider {
border-block-end: 1px solid var(--color-thd-gray);
margin: 1rem 0 1.5rem;
}
}
</style>
@@ -0,0 +1,143 @@
<template>
<form class="skill-step" @submit.prevent="onSubmit">
<LineTitleBlock title="مهارت‌ها و تخصص‌ها" title-en="Skills & Expertise" />
<div class="skill-step__row">
<div class="skill-step__intro">
<p>{{ intro }}</p>
</div>
<div class="skill-step__select">
<SelectField
v-model="form[selectField]"
:name="selectField"
:options="options"
option-label="label"
option-value="value"
:error="errors[selectField]"
@change="onSelect"
/>
</div>
<div :class="['skill-step__description', { 'skill-step__description--blurred': !hasOther }]">
<TextareaField
v-model="form[descField]"
:name="descField"
label="کاری که انجام می‌دهید را بنویسید."
:row="isMobile ? 5 : 1"
:disabled="!hasOther"
:error="errors[descField]"
>
<template #appendIcon>
<SvgIcon name="file" :size="20" color="var(--color-thd-gray)" />
</template>
</TextareaField>
</div>
</div>
<div class="skill-step__divider" />
<StepActions @back="store.goToPrevStep()" />
</form>
</template>
<script setup>
import useYup from '@/composables/useYup'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import TextareaField from '@/components/form/TextareaField.vue'
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
const props = defineProps({
step: { type: Number, required: true },
storeKey: { type: String, required: true },
selectField: { type: String, required: true },
descField: { type: String, required: true },
intro: { type: String, required: true },
options: { type: Array, required: true },
schema: { type: Object, required: true },
})
const store = useStudentRegistrationStore()
const isMobile = ref(window.innerWidth < 768)
const onResize = () => {
isMobile.value = window.innerWidth < 768
}
onMounted(() => window.addEventListener('resize', onResize))
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
const seed = store[props.storeKey] || {}
const form = ref({
[props.selectField]: seed[props.selectField] || '',
[props.descField]: seed[props.descField] || '',
})
const hasOther = computed(() => form.value[props.selectField] === 'other')
const onSelect = () => {
if (!hasOther.value) form.value[props.descField] = ''
}
const { validate, errors } = useYup(props.schema)
const onSubmit = async () => {
const { isValid, payload } = await validate(form.value)
if (!isValid) return
store[props.storeKey] = { ...store[props.storeKey], ...payload }
store.markStepAsCompleted(props.step)
store.goToNextStep()
}
store.updateCurrentStep(props.step)
</script>
<style lang="scss" scoped>
.skill-step {
&__row {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem 0.625rem;
margin-bottom: 1rem;
@media (min-width: 1024px) {
grid-template-columns: repeat(12, 1fr);
}
}
&__intro {
@media (min-width: 1024px) {
grid-column: span 6;
}
p {
font-family: var(--font-family-fa);
font-weight: 300;
font-size: 0.875rem;
line-height: 1.7;
color: #a7a7a7;
text-align: justify;
margin: 0;
}
}
&__select {
@media (min-width: 1024px) {
grid-column: span 6;
}
}
&__description {
grid-column: 1 / -1;
transition: filter 0.2s ease;
&--blurred {
filter: blur(2px);
}
}
&__divider {
border-block-end: 1px solid var(--color-thd-gray);
margin: 1rem 0 1.5rem;
}
}
</style>
@@ -0,0 +1,76 @@
<template>
<div class="step-actions">
<div v-if="showBack" class="step-actions__back">
<BaseButton
variant="transparent"
type="button"
:text="backText"
custom-class="step-actions__back-btn"
@click="emit('back')"
>
<template #prependIcon>
<SvgIcon name="caret-right" :size="12" color="var(--color-sec-gray)" />
</template>
</BaseButton>
</div>
<div class="step-actions__next">
<BaseButton
type="submit"
:text="nextText"
:loading="loading"
custom-class="step-actions__next-btn"
>
<template #appendIcon>
<SvgIcon name="arrow-left" :size="18" color="#fff" />
</template>
</BaseButton>
</div>
</div>
</template>
<script setup>
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
defineProps({
showBack: { type: Boolean, default: true },
loading: { type: Boolean, default: false },
nextText: { type: String, default: 'مرحله بعدی' },
backText: { type: String, default: 'مرحله قبلی' },
})
const emit = defineEmits(['back'])
</script>
<style lang="scss" scoped>
.step-actions {
display: flex;
gap: 0.5rem;
&__back {
flex: 1 1 60%;
display: flex;
justify-content: flex-start;
}
&__next {
flex: 1 1 40%;
max-width: 16rem;
margin-inline-start: auto;
@media (min-width: 768px) {
flex: 0 0 16rem;
}
}
&__back-btn {
color: #969696;
padding: 0 1rem;
width: fit-content;
}
&__next-btn {
width: 100%;
}
}
</style>
@@ -0,0 +1,128 @@
<template>
<BasicModal
width="85%"
max-width="48rem"
min-width="auto"
:closable="false"
:show-close-button="false"
>
<template #default>
<div class="register-success">
<img :src="flower" alt="" class="register-success__image" />
<div class="register-success__content">
<h3 class="register-success__title">از پاسخدهی شما ممنونیم.</h3>
<p class="register-success__desc">
اطلاعات و مدارک شما برای ما با موفقیت ارسال شده است. پس از بررسی و تأیید هویت و مدارک
توسط مجموعه، حساب کاربری شما فعال میشود
</p>
<div class="register-success__actions">
<BaseButton
text="خروج"
:loading="logoutMutation.isPending.value"
custom-class="register-success__btn"
@click="onLogout"
>
<template #appendIcon>
<SvgIcon name="arrow-left" :size="18" color="#fff" />
</template>
</BaseButton>
</div>
</div>
</div>
</template>
</BasicModal>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { gallery } from '@/utils/gallery'
import useAuth from '@/composables/useAuth'
import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import { useLogoutMutation } from '@/services/query/auth'
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
defineOptions({ name: 'StudentRegisterSuccessModal' })
const router = useRouter()
const { clearSession } = useAuth()
const store = useStudentRegistrationStore()
const logoutMutation = useLogoutMutation()
const flower = gallery.registrationCompleteFlower
const onLogout = async () => {
try {
await logoutMutation.mutateAsync()
} catch {
/* ignore */
} finally {
clearSession()
store.reset()
router.push({ name: 'login' })
}
}
</script>
<style lang="scss" scoped>
.register-success {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 0.5rem 0;
@media (min-width: 1024px) {
flex-direction: row;
align-items: center;
padding: 1rem 2rem;
gap: 1.5rem;
}
&__image {
height: 4.8rem;
width: auto;
object-fit: contain;
}
&__content {
text-align: center;
@media (min-width: 1024px) {
text-align: right;
flex: 1;
}
}
&__title {
font-family: var(--font-family-fa);
font-weight: 700;
font-size: 1.125rem;
margin: 0 0 0.5rem;
}
&__desc {
font-family: var(--font-family-fa);
font-size: 0.875rem;
line-height: 1.7;
color: #7c7c7c;
margin: 0 0 1rem;
text-align: justify;
}
&__actions {
display: flex;
justify-content: center;
@media (min-width: 1024px) {
justify-content: flex-start;
}
}
&__btn {
min-width: 10rem;
padding: 0 1.5rem;
}
}
</style>
@@ -1,6 +1,5 @@
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/store/auth'
import { useRoute, useRouter } from 'vue-router'
export const useRedirectAfterLogin = () => {
const router = useRouter()
+2 -3
View File
@@ -18,12 +18,11 @@
<script setup>
import { ref } from 'vue'
import { LOGIN_STEPS } from '@/enums'
import AuthShell from '@/features/auth/components/AuthShell.vue'
import LoginForm from '@/features/auth/components/LoginForm.vue'
import LoginWithVerifyCodeForm from '@/features/auth/components/LoginWithVerifyCodeForm.vue'
import VerifyCodeForm from '@/features/auth/components/VerifyCodeForm.vue'
import { LOGIN_STEPS } from '@/enums'
import LoginWithVerifyCodeForm from '@/features/auth/components/LoginWithVerifyCodeForm.vue'
const step = ref(LOGIN_STEPS.LOGIN)
const mode = ref('loginWithCode')
@@ -52,18 +52,18 @@
<script setup>
import { ref } from 'vue'
import AuthShell from '@/features/auth/components/AuthShell.vue'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import BaseButton from '@/components/BaseButton.vue'
import TextField from '@/components/form/TextField.vue'
import PasswordField from '@/components/form/PasswordField.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import useYup from '@/composables/useYup'
import useAuth from '@/composables/useAuth'
import { useResetPasswordMutation } from '@/services/query/auth'
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue'
import { resetPasswordSchema } from '@/features/auth/schema'
import PasswordField from '@/components/form/PasswordField.vue'
import AuthShell from '@/features/auth/components/AuthShell.vue'
import { useResetPasswordMutation } from '@/services/query/auth'
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
import AuthCopyright from '@/features/auth/components/AuthCopyright.vue'
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
const { user, applySession } = useAuth()
const redirectAfterLogin = useRedirectAfterLogin()

Some files were not shown because too many files have changed in this diff Show More