Fix
This commit is contained in:
+7
-4
@@ -5,7 +5,7 @@ module.exports = {
|
|||||||
node: true,
|
node: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
},
|
},
|
||||||
plugins: ['prettier', 'unicorn', 'import'],
|
plugins: ['prettier', 'unicorn', 'import', 'waterfall'],
|
||||||
ignorePatterns: ['node_modules/', 'dist/', 'build/', '*.min.js'],
|
ignorePatterns: ['node_modules/', 'dist/', 'build/', '*.min.js'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2021,
|
ecmaVersion: 2021,
|
||||||
@@ -71,7 +71,10 @@ module.exports = {
|
|||||||
'unicorn/prefer-logical-operator-over-ternary': 'off',
|
'unicorn/prefer-logical-operator-over-ternary': 'off',
|
||||||
'unicorn/prefer-number-properties': 'off',
|
'unicorn/prefer-number-properties': 'off',
|
||||||
'unicorn/prefer-ternary': '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-irregular-whitespace': 'off',
|
||||||
'no-empty': 'off',
|
'no-empty': 'off',
|
||||||
'import/extensions': 'off',
|
'import/extensions': 'off',
|
||||||
@@ -80,9 +83,9 @@ module.exports = {
|
|||||||
'vue/html-self-closing': 'off',
|
'vue/html-self-closing': 'off',
|
||||||
'vue/max-attributes-per-line': 'off',
|
'vue/max-attributes-per-line': 'off',
|
||||||
'vue/html-indent': 'off',
|
'vue/html-indent': 'off',
|
||||||
'vue/no-unused-components': 'off',
|
'vue/no-unused-components': 'error',
|
||||||
'vue/no-reserved-component-names': 'off',
|
'vue/no-reserved-component-names': 'off',
|
||||||
'vue/no-unused-vars': 'off',
|
'vue/no-unused-vars': 'error',
|
||||||
'vue/require-v-for-key': 'off',
|
'vue/require-v-for-key': 'off',
|
||||||
'vue/require-toggle-inside-transition': 'off',
|
'vue/require-toggle-inside-transition': 'off',
|
||||||
'vue/require-valid-default-prop': 'off',
|
'vue/require-valid-default-prop': 'off',
|
||||||
|
|||||||
Generated
+8
@@ -29,6 +29,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-unicorn": "^55.0.0",
|
"eslint-plugin-unicorn": "^55.0.0",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"eslint-plugin-waterfall": "^1.0.1",
|
||||||
"postcss-html": "^1.8.0",
|
"postcss-html": "^1.8.0",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"sass": "^1.83.0",
|
"sass": "^1.83.0",
|
||||||
@@ -3467,6 +3468,13 @@
|
|||||||
"eslint": ">=6.0.0"
|
"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": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-unicorn": "^55.0.0",
|
"eslint-plugin-unicorn": "^55.0.0",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"eslint-plugin-waterfall": "^1.0.1",
|
||||||
"postcss-html": "^1.8.0",
|
"postcss-html": "^1.8.0",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"sass": "^1.83.0",
|
"sass": "^1.83.0",
|
||||||
|
|||||||
+6
-7
@@ -5,16 +5,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, watch } from 'vue'
|
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
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 { 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()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { 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({
|
const props = defineProps({
|
||||||
name: { type: String, default: '' },
|
name: { type: String, default: '' },
|
||||||
|
|||||||
@@ -28,10 +28,7 @@
|
|||||||
role="tooltip"
|
role="tooltip"
|
||||||
>
|
>
|
||||||
{{ tooltip }}
|
{{ tooltip }}
|
||||||
<span
|
<span class="circle-button__tooltip-arrow" :style="arrowStyle" />
|
||||||
class="circle-button__tooltip-arrow"
|
|
||||||
:style="arrowStyle"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -98,8 +95,8 @@ const updatePosition = async () => {
|
|||||||
const side = result.placement.split('-')[0]
|
const side = result.placement.split('-')[0]
|
||||||
const oppositeSide = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[side]
|
const oppositeSide = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }[side]
|
||||||
arrowStyle.value = {
|
arrowStyle.value = {
|
||||||
left: arrowData.x != null ? `${arrowData.x}px` : '',
|
left: arrowData.x == null ? '' : `${arrowData.x}px`,
|
||||||
top: arrowData.y != null ? `${arrowData.y}px` : '',
|
top: arrowData.y == null ? '' : `${arrowData.y}px`,
|
||||||
[oppositeSide]: '-0.25rem',
|
[oppositeSide]: '-0.25rem',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { useModalStore } from '@/store/modal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import { useModalStore } from '@/store/modal'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ConfirmModal' })
|
defineOptions({ name: 'ConfirmModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
computePosition,
|
computePosition,
|
||||||
@@ -35,8 +36,6 @@ import {
|
|||||||
shift,
|
shift,
|
||||||
} from '@floating-ui/dom'
|
} from '@floating-ui/dom'
|
||||||
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Boolean, default: false },
|
modelValue: { type: Boolean, default: false },
|
||||||
reference: { type: [Object, null], default: null },
|
reference: { type: [Object, null], default: null },
|
||||||
|
|||||||
@@ -34,11 +34,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { useUIStore } from '@/store/ui'
|
||||||
|
import { computed, useSlots } from 'vue'
|
||||||
|
import { gallery } from '@/utils/gallery'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
pageTitle: { type: String, default: '' },
|
pageTitle: { type: String, default: '' },
|
||||||
|
|||||||
@@ -50,11 +50,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 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 {
|
import {
|
||||||
formatJalaaliDate,
|
formatJalaaliDate,
|
||||||
formatJalaaliDateTime,
|
formatJalaaliDateTime,
|
||||||
|
|||||||
@@ -62,8 +62,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Array, default: () => [] },
|
modelValue: { type: Array, default: () => [] },
|
||||||
|
|||||||
@@ -83,12 +83,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { markRaw, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { Cropper, CircleStencil } from 'vue-advanced-cropper'
|
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({
|
const props = defineProps({
|
||||||
modelValue: { type: [Object, String], default: null },
|
modelValue: { type: [Object, String], default: null },
|
||||||
|
|||||||
@@ -24,9 +24,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import TextField from '@/components/form/TextField.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: String, default: '' },
|
modelValue: { type: String, default: '' },
|
||||||
|
|||||||
@@ -74,9 +74,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
|
|
||||||
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom'
|
||||||
|
|
||||||
let uid = 0
|
let uid = 0
|
||||||
const nextUid = () => `select-field-${++uid}`
|
const nextUid = () => `select-field-${++uid}`
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
import { tokenService } from '@/services/api/token-service'
|
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
import { tokenService } from '@/services/api/token-service'
|
||||||
|
|
||||||
export default function useAuth() {
|
export default function useAuth() {
|
||||||
const store = useAuthStore()
|
const store = useAuthStore()
|
||||||
|
|||||||
@@ -76,11 +76,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
assignment: { type: Object, required: true },
|
assignment: { type: Object, required: true },
|
||||||
|
|||||||
@@ -59,15 +59,14 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
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 TextField from '@/components/form/TextField.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.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 { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -115,30 +115,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
import { computed, ref, watch } from 'vue'
|
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 { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import useModal from '@/composables/useModal'
|
import TextField from '@/components/form/TextField.vue'
|
||||||
import useYup from '@/composables/useYup'
|
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 {
|
import {
|
||||||
adminAssignmentsKeys,
|
adminAssignmentsKeys,
|
||||||
useAddAdminAssignmentMutation,
|
useAddAdminAssignmentMutation,
|
||||||
useAdminAssignmentQuery,
|
useAdminAssignmentQuery,
|
||||||
useUpdateAdminAssignmentMutation,
|
useUpdateAdminAssignmentMutation,
|
||||||
} from '@/services/query/admin-assignments'
|
} 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' })
|
defineOptions({ name: 'AddAssignmentModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -90,18 +90,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import { ASSIGNMENT_PRIORITY } from '@/enums'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.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 { useAdminAssignmentQuery } from '@/services/query/admin-assignments'
|
||||||
import { ASSIGNMENT_PRIORITY } from '@/enums'
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AssignmentDetailsModal' })
|
defineOptions({ name: 'AssignmentDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -125,28 +125,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import useModal from '@/composables/useModal'
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
import useYup from '@/composables/useYup'
|
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 {
|
import {
|
||||||
adminAssignmentsKeys,
|
adminAssignmentsKeys,
|
||||||
useAdminAssignmentSubmissionQuery,
|
useAdminAssignmentSubmissionQuery,
|
||||||
useReviewAdminAssignmentSubmissionMutation,
|
useReviewAdminAssignmentSubmissionMutation,
|
||||||
} from '@/services/query/admin-assignments'
|
} 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' })
|
defineOptions({ name: 'AssignmentSubmissionDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -66,18 +66,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import useModal from '@/composables/useModal'
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import { useAdminAssignmentSubmissionsQuery } from '@/services/query/admin-assignments'
|
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
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' })
|
defineOptions({ name: 'AssignmentSubmissionsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -50,28 +50,27 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.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 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 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 AssignmentSubmissionsModal from '@/features/admin/assignments/components/modals/AssignmentSubmissionsModal.vue'
|
||||||
import AssignmentSubmissionDetailsModal from '@/features/admin/assignments/components/modals/AssignmentSubmissionDetailsModal.vue'
|
import AssignmentSubmissionDetailsModal from '@/features/admin/assignments/components/modals/AssignmentSubmissionDetailsModal.vue'
|
||||||
import AssignmentDetailsModal from '@/features/admin/assignments/components/modals/AssignmentDetailsModal.vue'
|
|
||||||
import {
|
import {
|
||||||
adminAssignmentsKeys,
|
adminAssignmentsKeys,
|
||||||
useAdminAssignmentsListQuery,
|
useAdminAssignmentsListQuery,
|
||||||
useDeleteAdminAssignmentMutation,
|
useDeleteAdminAssignmentMutation,
|
||||||
} from '@/services/query/admin-assignments'
|
} from '@/services/query/admin-assignments'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|||||||
@@ -57,12 +57,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
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 { CONSULTATION_STATUS } from '@/enums'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
consultation: { type: Object, required: true },
|
consultation: { type: Object, required: true },
|
||||||
|
|||||||
@@ -47,13 +47,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
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 TextField from '@/components/form/TextField.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.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({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -48,21 +48,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
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 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 {
|
import {
|
||||||
adminConsultationsKeys,
|
adminConsultationsKeys,
|
||||||
useAdminConsultationQuery,
|
useAdminConsultationQuery,
|
||||||
useSendAdminConsultationMessageMutation,
|
useSendAdminConsultationMessageMutation,
|
||||||
} from '@/services/query/admin-consultations'
|
} from '@/services/query/admin-consultations'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ConsultationDetailsModal' })
|
defineOptions({ name: 'ConsultationDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -41,24 +41,23 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 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 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 ConsultationDetailsModal from '@/features/admin/consultations/components/modals/ConsultationDetailsModal.vue'
|
||||||
import {
|
import {
|
||||||
adminConsultationsKeys,
|
adminConsultationsKeys,
|
||||||
useAdminConsultationsListQuery,
|
useAdminConsultationsListQuery,
|
||||||
useChangeAdminConsultationStatusMutation,
|
useChangeAdminConsultationStatusMutation,
|
||||||
} from '@/services/query/admin-consultations'
|
} from '@/services/query/admin-consultations'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
|
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -82,11 +82,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
course: { type: Object, required: true },
|
course: { type: Object, required: true },
|
||||||
|
|||||||
@@ -56,12 +56,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import TextField from '@/components/form/TextField.vue'
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.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'
|
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -86,18 +86,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
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 useModal from '@/composables/useModal'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
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 { useAdminUsersListQuery } from '@/services/query/admin-users'
|
||||||
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import {
|
import {
|
||||||
adminCourseTemplatesKeys,
|
adminCourseTemplatesKeys,
|
||||||
useAddAdminTemplateStudentMutation,
|
useAddAdminTemplateStudentMutation,
|
||||||
|
|||||||
@@ -104,31 +104,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { toast } from 'vue3-toastify'
|
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 { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 TextField from '@/components/form/TextField.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import useModal from '@/composables/useModal'
|
import { useUploadTemporaryMutation } from '@/services/query/auth'
|
||||||
import useYup from '@/composables/useYup'
|
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
||||||
|
import { offeredCourseSchema } from '@/features/admin/courses/schema'
|
||||||
|
import { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
||||||
import {
|
import {
|
||||||
adminCoursesKeys,
|
adminCoursesKeys,
|
||||||
useAddAdminCourseMutation,
|
useAddAdminCourseMutation,
|
||||||
useAdminCourseQuery,
|
useAdminCourseQuery,
|
||||||
useUpdateAdminCourseMutation,
|
useUpdateAdminCourseMutation,
|
||||||
} from '@/services/query/admin-courses'
|
} 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' })
|
defineOptions({ name: 'AddOfferedCourseModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -83,18 +83,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
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 useModal from '@/composables/useModal'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
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 { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
||||||
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import {
|
import {
|
||||||
adminCourseTemplatesKeys,
|
adminCourseTemplatesKeys,
|
||||||
useAdminTemplateSessionsQuery,
|
useAdminTemplateSessionsQuery,
|
||||||
|
|||||||
@@ -150,21 +150,21 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 CircleButton from '@/components/CircleButton.vue'
|
||||||
|
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
||||||
|
import { usePagination } from '@/composables/usePagination'
|
||||||
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.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 PaginationBlock from '@/components/blocks/PaginationBlock.vue'
|
||||||
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
import CourseSessionItem from '@/features/admin/courses/components/CourseSessionItem.vue'
|
import CourseSessionItem from '@/features/admin/courses/components/CourseSessionItem.vue'
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import {
|
import {
|
||||||
adminCourseTemplatesKeys,
|
adminCourseTemplatesKeys,
|
||||||
useAdminCourseTemplateQuery,
|
useAdminCourseTemplateQuery,
|
||||||
@@ -172,7 +172,6 @@ import {
|
|||||||
useAdminTemplateStudentsQuery,
|
useAdminTemplateStudentsQuery,
|
||||||
useRemoveAdminTemplateStudentMutation,
|
useRemoveAdminTemplateStudentMutation,
|
||||||
} from '@/services/query/admin-course-templates'
|
} from '@/services/query/admin-course-templates'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
|
|
||||||
defineOptions({ name: 'CourseDetailsModal' })
|
defineOptions({ name: 'CourseDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -123,21 +123,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue3-toastify'
|
import useDebounce from '@/composables/useDebounce'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import TextField from '@/components/form/TextField.vue'
|
import TextField from '@/components/form/TextField.vue'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import useYup from '@/composables/useYup'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
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 {
|
import {
|
||||||
adminCourseTemplatesKeys,
|
adminCourseTemplatesKeys,
|
||||||
useAddAdminCourseTemplateMutation,
|
useAddAdminCourseTemplateMutation,
|
||||||
@@ -145,10 +149,6 @@ import {
|
|||||||
useAdminCourseTemplatesListQuery,
|
useAdminCourseTemplatesListQuery,
|
||||||
useUpdateAdminCourseTemplateMutation,
|
useUpdateAdminCourseTemplateMutation,
|
||||||
} from '@/services/query/admin-course-templates'
|
} 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 route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -83,36 +83,35 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.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 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 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 AddCourseStudentModal from '@/features/admin/courses/components/modals/AddCourseStudentModal.vue'
|
||||||
import AddSessionToCourseModal from '@/features/admin/courses/components/modals/AddSessionToCourseModal.vue'
|
import AddSessionToCourseModal from '@/features/admin/courses/components/modals/AddSessionToCourseModal.vue'
|
||||||
import {
|
|
||||||
adminCourseTemplatesKeys,
|
|
||||||
useAdminCourseTemplatesListQuery,
|
|
||||||
useChangeAdminCourseTemplateStatusMutation,
|
|
||||||
useDeleteAdminCourseTemplateMutation,
|
|
||||||
} from '@/services/query/admin-course-templates'
|
|
||||||
import {
|
import {
|
||||||
adminCoursesKeys,
|
adminCoursesKeys,
|
||||||
useAdminCoursesListQuery,
|
useAdminCoursesListQuery,
|
||||||
useChangeAdminCourseStatusMutation,
|
useChangeAdminCourseStatusMutation,
|
||||||
useDeleteAdminCourseMutation,
|
useDeleteAdminCourseMutation,
|
||||||
} from '@/services/query/admin-courses'
|
} from '@/services/query/admin-courses'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
import {
|
||||||
import useModal from '@/composables/useModal'
|
adminCourseTemplatesKeys,
|
||||||
|
useAdminCourseTemplatesListQuery,
|
||||||
|
useChangeAdminCourseTemplateStatusMutation,
|
||||||
|
useDeleteAdminCourseTemplateMutation,
|
||||||
|
} from '@/services/query/admin-course-templates'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -76,11 +76,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
exam: { type: Object, required: true },
|
exam: { type: Object, required: true },
|
||||||
|
|||||||
@@ -114,9 +114,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Array, default: () => [] },
|
modelValue: { type: Array, default: () => [] },
|
||||||
|
|||||||
@@ -49,14 +49,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
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 TextField from '@/components/form/TextField.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.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 { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -1,68 +1,54 @@
|
|||||||
<template>
|
<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 }">
|
<template #default="{ close }">
|
||||||
<div class="exam-details">
|
<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" />
|
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
|
||||||
<template v-else-if="exam">
|
<template v-else-if="exam">
|
||||||
<div class="exam-details__head">
|
<div class="exam-details__summary">
|
||||||
<p class="exam-details__title">{{ exam.title || '—' }}</p>
|
<div class="exam-details__summary-grid">
|
||||||
<p class="exam-details__sub">
|
<div v-for="summary in summaryItems" :key="summary.title" class="exam-details__cell">
|
||||||
<span>دوره: {{ exam.courseTemplate?.title || exam.courseTemplateTitle || '—' }}</span>
|
<LineInfoBlock
|
||||||
<span class="exam-details__sep">|</span>
|
:title="summary.title"
|
||||||
<span>جلسه: {{ exam.session?.title || exam.sessionTitle || '—' }}</span>
|
:desc="summary.numeric ? '' : summary.value"
|
||||||
</p>
|
:numeric-desc="summary.numeric ? summary.value : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="exam-details__description">
|
||||||
|
<LineInfoBlock title="توضیحات آزمون" :desc="exam.description || '—'" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="exam-details__grid">
|
<div class="exam-details__questions">
|
||||||
<LineInfoBlock
|
<div
|
||||||
title="مدت آزمون"
|
v-for="question in displayQuestions"
|
||||||
:numeric-desc="exam.durationMinutes != null ? `${exam.durationMinutes} دقیقه` : '—'"
|
:key="question.id"
|
||||||
/>
|
class="exam-details__question"
|
||||||
<LineInfoBlock title="حداقل نمره قبولی" :numeric-desc="exam.passingScore ?? '—'" />
|
>
|
||||||
<LineInfoBlock
|
<div class="exam-details__question-head">
|
||||||
title="تعداد سوالات"
|
<p class="exam-details__question-title">
|
||||||
:numeric-desc="exam.questionsCount ?? questionsLength"
|
<span class="exam-details__question-order">{{ question.order }} :</span>
|
||||||
/>
|
{{ question.title }}
|
||||||
<LineInfoBlock title="رندوم" :desc="exam.randomize ? 'بله' : 'خیر'" />
|
</p>
|
||||||
<LineInfoBlock
|
<span v-if="question.score" class="exam-details__question-score">
|
||||||
title="تاریخ اعتبار"
|
{{ question.score }} نمره
|
||||||
: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 }} نمره)
|
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
<ul class="exam-details__answers">
|
<div class="exam-details__answers">
|
||||||
<li
|
<div
|
||||||
v-for="(a, ai) in q.answers || []"
|
v-for="answer in question.answers"
|
||||||
:key="a.id"
|
:key="answer.id"
|
||||||
class="exam-details__answer"
|
class="exam-details__answer"
|
||||||
:class="{
|
|
||||||
'exam-details__answer--correct': String(q.correctAnswerId) === String(a.id),
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<span class="exam-details__answer-index">{{ optionLabel(ai) }}:</span>
|
<span
|
||||||
{{ a.title }}
|
class="exam-details__answer-dot"
|
||||||
</li>
|
:class="{ 'exam-details__answer-dot--correct': answer.isCorrect }"
|
||||||
</ul>
|
/>
|
||||||
|
<p class="exam-details__answer-text">{{ answer.title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -71,7 +57,7 @@
|
|||||||
<div class="exam-details__divider" />
|
<div class="exam-details__divider" />
|
||||||
|
|
||||||
<div class="exam-details__footer">
|
<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>
|
<template #appendIcon>
|
||||||
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
@@ -84,17 +70,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 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 { 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' })
|
defineOptions({ name: 'ExamDetailsModal' })
|
||||||
|
|
||||||
@@ -107,10 +91,65 @@ const { data: exam, isLoading } = useAdminExamQuery(examId, {
|
|||||||
enabled: () => !!examId.value,
|
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 displayQuestions = computed(() => {
|
||||||
const optionLabel = (index) => labels[index] || `گزینه ${index + 1}`
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -118,105 +157,122 @@ const optionLabel = (index) => labels[index] || `گزینه ${index + 1}`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
|
|
||||||
&__head {
|
&__summary {
|
||||||
margin: 1rem 0 0.5rem;
|
margin-block: 0.5rem 0.75rem;
|
||||||
|
padding: 1rem 0.875rem;
|
||||||
|
border-radius: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__summary-grid {
|
||||||
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 {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 0.25rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
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 {
|
&__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 {
|
&__question {
|
||||||
padding: 0.625rem 0;
|
background: #fff;
|
||||||
border-block-end: 1px solid #f0f0f0;
|
border-radius: 1.25rem;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&__question-head {
|
||||||
border: none;
|
display: flex;
|
||||||
}
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.625rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__question-title {
|
&__question-title {
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
font-size: 0.9rem;
|
font-weight: 500;
|
||||||
color: #4b4b4b;
|
font-size: 0.875rem;
|
||||||
margin: 0 0 0.375rem;
|
line-height: 1.6;
|
||||||
|
color: #a7a7a7;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__question-index {
|
&__question-order {
|
||||||
color: #b3b3b3;
|
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__question-score {
|
&__question-score {
|
||||||
margin-inline-start: 0.25rem;
|
flex-shrink: 0;
|
||||||
color: #007074;
|
font-family: var(--font-family-en);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
color: #9c9c9c;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__answers {
|
&__answers {
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
gap: 0.25rem;
|
gap: 0.625rem 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__answer {
|
&__answer {
|
||||||
font-family: var(--font-family-fa);
|
display: inline-flex;
|
||||||
font-size: 0.85rem;
|
align-items: baseline;
|
||||||
color: #4b4b4b;
|
gap: 0.375rem;
|
||||||
padding: 0.25rem 0.625rem;
|
}
|
||||||
border-radius: 0.5rem;
|
|
||||||
background: #fafafa;
|
&__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 {
|
&--correct {
|
||||||
background: rgba(0, 154, 18, 8%);
|
border-color: #007074;
|
||||||
color: #009a12;
|
background: #007074;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__answer-index {
|
&__answer-text {
|
||||||
color: #9c9c9c;
|
font-family: var(--font-family-fa);
|
||||||
margin-inline-end: 0.375rem;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #4b4b4b;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
@@ -229,8 +285,9 @@ const optionLabel = (index) => labels[index] || `گزینه ${index + 1}`
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__close-btn {
|
&__confirm-btn {
|
||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
|
padding: 0 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,77 +1,95 @@
|
|||||||
<template>
|
<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 }">
|
<template #default="{ close }">
|
||||||
<div class="participant-details">
|
<div class="participant-details">
|
||||||
<LineTitleBlock title="جزئیات شرکتکننده" title-en="Participant Details" />
|
<LineTitleBlock title="شرکت کنندگان" title-en="Participants" />
|
||||||
<p v-if="participant" class="participant-details__name">
|
|
||||||
{{ participantName }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
|
<p v-if="participantName" class="participant-details__name">{{ participantName }}</p>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div v-if="isOpen(attempt.id)" class="participant-details__body">
|
<div class="participant-details__card">
|
||||||
<div
|
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
|
||||||
v-for="(question, qIdx) in attempt.questions || []"
|
<div v-else-if="attempts.length > 0" class="participant-details__list">
|
||||||
:key="question.id"
|
<div v-for="attempt in attempts" :key="attempt.id" class="participant-details__attempt">
|
||||||
class="participant-details__question"
|
<button
|
||||||
|
type="button"
|
||||||
|
class="participant-details__attempt-head"
|
||||||
|
@click="toggle(attempt.id)"
|
||||||
>
|
>
|
||||||
<p class="participant-details__question-title">
|
<div class="participant-details__caret">
|
||||||
{{ qIdx + 1 }}- {{ question.title }}
|
<SvgIcon
|
||||||
</p>
|
:name="isOpen(attempt.id) ? 'caret-down' : 'caret-left'"
|
||||||
<div class="participant-details__answers">
|
:size="24"
|
||||||
<span
|
color="#d8d8d8"
|
||||||
v-for="(answer, aIdx) in question.answers || []"
|
/>
|
||||||
:key="answer.id"
|
</div>
|
||||||
class="participant-details__answer"
|
<div class="participant-details__attempt-info">
|
||||||
:class="answerClass(question, answer)"
|
<p class="participant-details__attempt-title">{{ attempt.title }}</p>
|
||||||
>
|
<div class="participant-details__attempt-meta">
|
||||||
<span class="participant-details__answer-index">{{ aIdx + 1 }}.</span>
|
<div class="participant-details__meta-item">
|
||||||
{{ answer.title }}
|
<span class="participant-details__meta-label">دوره :</span>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<NoItems v-else title="موردی یافت نشد" desc="تلاشی برای این شرکتکننده ثبت نشده است." />
|
||||||
<NoItems v-else title="موردی یافت نشد" desc="تلاشی برای این شرکتکننده ثبت نشده است." />
|
|
||||||
|
|
||||||
<div class="participant-details__divider" />
|
<div class="participant-details__divider" />
|
||||||
|
<div class="participant-details__footer">
|
||||||
<div class="participant-details__footer">
|
<BaseButton text="تایید" custom-class="participant-details__confirm-btn" @click="close">
|
||||||
<BaseButton text="بستن" custom-class="participant-details__close-btn" @click="close">
|
<template #appendIcon>
|
||||||
<template #appendIcon>
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
</template>
|
||||||
</template>
|
</BaseButton>
|
||||||
</BaseButton>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -79,20 +97,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import useModal from '@/composables/useModal'
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
import { useAdminExamParticipantQuery } from '@/services/query/admin-exams'
|
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
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' })
|
defineOptions({ name: 'ExamParticipantDetailsModal' })
|
||||||
|
|
||||||
|
const TONE_MAP = {
|
||||||
|
good: 'success',
|
||||||
|
success: 'success',
|
||||||
|
bad: 'danger',
|
||||||
|
danger: 'danger',
|
||||||
|
neutral: 'neutral',
|
||||||
|
}
|
||||||
|
|
||||||
const { getModal } = useModal()
|
const { getModal } = useModal()
|
||||||
|
|
||||||
const modalData = computed(() => getModal('ExamParticipantDetailsModal')?.data ?? {})
|
const modalData = computed(() => getModal('ExamParticipantDetailsModal')?.data ?? {})
|
||||||
@@ -104,16 +129,22 @@ const { data: detail, isLoading } = useAdminExamParticipantQuery(examId, partici
|
|||||||
enabled: () => !!examId.value && !!participantId.value,
|
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 attempts = computed(() =>
|
||||||
const isOpen = (id) => opened.value.has(id)
|
rawAttempts.value.map((attempt, index) => ({
|
||||||
const toggle = (id) => {
|
id: attempt.id ?? index + 1,
|
||||||
const next = new Set(opened.value)
|
title: attempt.title || `آزمون مرتبه ${index + 1}`,
|
||||||
if (next.has(id)) next.delete(id)
|
courseTitle: attempt.courseTitle || '—',
|
||||||
else next.add(id)
|
sessionTitle: attempt.sessionTitle || '—',
|
||||||
opened.value = next
|
faDate: attempt.faDate || formatJalaaliDate(attempt.date) || '—',
|
||||||
}
|
score: attempt.score ?? '—',
|
||||||
|
tone: TONE_MAP[attempt.scoreTone] || 'neutral',
|
||||||
|
questions: attempt.questions || [],
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const participantName = computed(() => {
|
const participantName = computed(() => {
|
||||||
const p = participant.value
|
const p = participant.value
|
||||||
@@ -121,14 +152,26 @@ const participantName = computed(() => {
|
|||||||
return `${p.firstName || ''} ${p.lastName || ''}`.trim() || p.fullName || ''
|
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) => {
|
watch(
|
||||||
const isCorrect = String(question.correctAnswerId) === String(answer.id)
|
attempts,
|
||||||
const isUserChoice = String(question.userAnswerId) === String(answer.id)
|
(list) => {
|
||||||
if (isUserChoice && isCorrect) return 'participant-details__answer--correct'
|
if (openId.value == null && list.length > 0) openId.value = list[0].id
|
||||||
if (isUserChoice && !isCorrect) return 'participant-details__answer--wrong'
|
},
|
||||||
if (isCorrect) return 'participant-details__answer--correct-outline'
|
{ 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 ''
|
return ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -145,15 +188,26 @@ const answerClass = (question, answer) => {
|
|||||||
margin: 0.5rem 0 1rem;
|
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 {
|
&__list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__attempt {
|
&__attempt {
|
||||||
border: 1px solid #e7e2dd;
|
border: 1px solid #e7e2dd;
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@@ -161,129 +215,184 @@ const answerClass = (question, answer) => {
|
|||||||
&__attempt-head {
|
&__attempt-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: start;
|
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
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 {
|
&__attempt-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__attempt-title {
|
&__attempt-title {
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 1rem;
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.7;
|
||||||
color: #494949;
|
color: #494949;
|
||||||
margin: 0 0 0.5rem;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__attempt-meta {
|
&__attempt-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.625rem 1.25rem;
|
align-items: center;
|
||||||
font-family: var(--font-family-fa);
|
column-gap: 1.25rem;
|
||||||
font-size: 0.75rem;
|
row-gap: 0.5rem;
|
||||||
color: #8e8e8e;
|
color: #8e8e8e;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__sep {
|
&__meta-item {
|
||||||
color: #d6d6d6;
|
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 {
|
&__score {
|
||||||
padding: 0.25rem 0.875rem;
|
padding: 0.375rem 0.75rem;
|
||||||
border-radius: 0.55rem;
|
border-radius: 0.55rem;
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
font-size: 0.75rem;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
&--good {
|
&--success {
|
||||||
background: rgba(0, 154, 18, 8%);
|
background: #e8f8ee;
|
||||||
color: #009a12;
|
color: #169c56;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background: #fdebeb;
|
||||||
|
color: #cc2831;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--neutral {
|
&--neutral {
|
||||||
background: rgba(180, 180, 180, 8%);
|
background: #f4f4f4;
|
||||||
color: #8e8e8e;
|
color: #8e8e8e;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
&--bad {
|
|
||||||
background: rgba(204, 40, 49, 8%);
|
&__score-value {
|
||||||
color: var(--color-error);
|
font-family: var(--font-family-en);
|
||||||
}
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
padding: 1rem 1.25rem 1.5rem;
|
padding: 1.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.25rem;
|
gap: 2rem;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
padding: 1.25rem 1.5rem 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__question-title {
|
&__question-title {
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.75;
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
margin: 0 0 0.75rem;
|
margin: 0 0 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__answers {
|
&__answers {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.625rem 1rem;
|
column-gap: 3rem;
|
||||||
|
row-gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__answer {
|
&__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;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
gap: 0.375rem;
|
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 {
|
&--correct {
|
||||||
background: rgba(0, 154, 18, 8%);
|
border-color: #007074;
|
||||||
border-color: rgba(0, 154, 18, 40%);
|
background: #007074;
|
||||||
color: #009a12;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--correct-outline {
|
|
||||||
border-color: rgba(0, 154, 18, 40%);
|
|
||||||
color: #009a12;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--wrong {
|
&--wrong {
|
||||||
background: rgba(204, 40, 49, 8%);
|
border-color: #cc2831;
|
||||||
border-color: rgba(204, 40, 49, 40%);
|
background: #cc2831;
|
||||||
color: var(--color-error);
|
}
|
||||||
|
|
||||||
|
&--selected-correct {
|
||||||
|
border-color: #9a9a9a;
|
||||||
|
background: #9a9a9a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__answer-index {
|
&__answer-text {
|
||||||
font-family: var(--font-family-en);
|
font-family: var(--font-family-fa);
|
||||||
color: #b3b3b3;
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #5b5b5b;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
border-block-end: 1px solid var(--color-thd-gray);
|
border-block-end: 1px solid #e7e2dd;
|
||||||
margin-block: 1.25rem;
|
margin-block: 1.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__close-btn {
|
&__confirm-btn {
|
||||||
min-width: 12rem;
|
min-width: 11.5rem;
|
||||||
|
padding: 0 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -63,18 +63,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import useModal from '@/composables/useModal'
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import { useAdminExamParticipantsQuery } from '@/services/query/admin-exams'
|
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
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' })
|
defineOptions({ name: 'ExamParticipantsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -104,28 +104,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import useDebounce from '@/composables/useDebounce'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
|
||||||
import BaseButton from '@/components/BaseButton.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 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 ExamQuestionBuilder from '@/features/admin/exams/components/ExamQuestionBuilder.vue'
|
||||||
import useYup from '@/composables/useYup'
|
|
||||||
import {
|
import {
|
||||||
adminExamsKeys,
|
adminExamsKeys,
|
||||||
useAddAdminExamMutation,
|
useAddAdminExamMutation,
|
||||||
useAdminExamQuery,
|
useAdminExamQuery,
|
||||||
useUpdateAdminExamMutation,
|
useUpdateAdminExamMutation,
|
||||||
} from '@/services/query/admin-exams'
|
} 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 route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -50,27 +50,26 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.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 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 ExamParticipantsModal from '@/features/admin/exams/components/modals/ExamParticipantsModal.vue'
|
||||||
import ExamParticipantDetailsModal from '@/features/admin/exams/components/modals/ExamParticipantDetailsModal.vue'
|
import ExamParticipantDetailsModal from '@/features/admin/exams/components/modals/ExamParticipantDetailsModal.vue'
|
||||||
import ExamDetailsModal from '@/features/admin/exams/components/modals/ExamDetailsModal.vue'
|
|
||||||
import {
|
import {
|
||||||
adminExamsKeys,
|
adminExamsKeys,
|
||||||
useAdminExamsListQuery,
|
useAdminExamsListQuery,
|
||||||
useDeleteAdminExamMutation,
|
useDeleteAdminExamMutation,
|
||||||
} from '@/services/query/admin-exams'
|
} from '@/services/query/admin-exams'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -46,10 +46,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { TICKET_STATUS } from '@/enums'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { TICKET_STATUS } from '@/enums'
|
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -46,14 +46,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { TICKET_STATUS } from '@/enums'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import TextField from '@/components/form/TextField.vue'
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.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({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicModal width="95%" max-width="64rem" min-width="auto" :show-close-button="true">
|
<BasicModal width="95%" max-width="64rem" min-width="auto" :show-close-button="true">
|
||||||
<template #default="{ close }">
|
<template #default>
|
||||||
<div class="ticket-details">
|
<div class="ticket-details">
|
||||||
<LineTitleBlock title="جزئیات تیکت" title-en="Ticket Details" />
|
<LineTitleBlock title="جزئیات تیکت" title-en="Ticket Details" />
|
||||||
|
|
||||||
@@ -103,8 +103,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
computePosition,
|
computePosition,
|
||||||
@@ -112,20 +120,11 @@ import {
|
|||||||
offset as offsetMiddleware,
|
offset as offsetMiddleware,
|
||||||
shift,
|
shift,
|
||||||
} from '@floating-ui/dom'
|
} 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 {
|
import {
|
||||||
adminTicketsKeys,
|
adminTicketsKeys,
|
||||||
useAdminTicketQuery,
|
useAdminTicketQuery,
|
||||||
useSendAdminTicketMessageMutation,
|
useSendAdminTicketMessageMutation,
|
||||||
} from '@/services/query/admin-tickets'
|
} from '@/services/query/admin-tickets'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
|
||||||
|
|
||||||
defineOptions({ name: 'TicketDetailsModal' })
|
defineOptions({ name: 'TicketDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -37,19 +37,18 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
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 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()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
|||||||
@@ -61,12 +61,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { SERVICE_STATUS, SERVICE_TYPE } from '@/enums'
|
import { SERVICE_STATUS, SERVICE_TYPE } from '@/enums'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
service: { type: Object, required: true },
|
service: { type: Object, required: true },
|
||||||
|
|||||||
@@ -46,14 +46,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { SERVICE_STATUS } from '@/enums'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import TextField from '@/components/form/TextField.vue'
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.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({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -66,13 +66,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { toast } from 'vue3-toastify'
|
import { toast } from 'vue3-toastify'
|
||||||
|
import { SERVICE_STATUS } from '@/enums'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
import { SERVICE_STATUS } from '@/enums'
|
|
||||||
|
|
||||||
defineOptions({ name: 'SendMessageModal' })
|
defineOptions({ name: 'SendMessageModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { SERVICE_TYPE } from '@/enums'
|
||||||
import useModal from '@/composables/useModal'
|
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' })
|
defineOptions({ name: 'ServiceDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -36,16 +36,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
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 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()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
|||||||
@@ -72,11 +72,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
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 { 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({
|
const props = defineProps({
|
||||||
session: { type: Object, required: true },
|
session: { type: Object, required: true },
|
||||||
|
|||||||
@@ -56,16 +56,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { SESSION_TYPE } from '@/enums'
|
||||||
import { computed, ref, watch } from 'vue'
|
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 TextField from '@/components/form/TextField.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.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 { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
|
||||||
import { SESSION_TYPE } from '@/enums'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicModal width="95%" max-width="60rem" min-width="auto" :show-close-button="true">
|
<BasicModal width="95%" max-width="60rem" min-width="auto" :show-close-button="true">
|
||||||
<template #default="{ data, close }">
|
<template #default="{ close }">
|
||||||
<div class="session-attendance">
|
<div class="session-attendance">
|
||||||
<LineTitleBlock title="حضور و غیاب" title-en="Attendance" />
|
<LineTitleBlock title="حضور و غیاب" title-en="Attendance" />
|
||||||
|
|
||||||
@@ -77,21 +77,20 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import useDebounce from '@/composables/useDebounce'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 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 { 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 { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
||||||
import { useAdminCoursesListQuery } from '@/services/query/admin-courses'
|
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' })
|
defineOptions({ name: 'SessionAttendanceModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -111,18 +111,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.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 { useAdminSessionQuery } from '@/services/query/admin-sessions'
|
||||||
import { SESSION_PLATFORM, SESSION_TYPE } from '@/enums'
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
|
|
||||||
defineOptions({ name: 'SessionDetailsModal' })
|
defineOptions({ name: 'SessionDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -218,34 +218,34 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue3-toastify'
|
import useDebounce from '@/composables/useDebounce'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import TextField from '@/components/form/TextField.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 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 ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import FileUploader from '@/components/form/FileUploader.vue'
|
import FileUploader from '@/components/form/FileUploader.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
import useYup from '@/composables/useYup'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
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 {
|
import {
|
||||||
adminSessionsKeys,
|
adminSessionsKeys,
|
||||||
useAddAdminSessionMutation,
|
useAddAdminSessionMutation,
|
||||||
useAdminSessionQuery,
|
useAdminSessionQuery,
|
||||||
useUpdateAdminSessionMutation,
|
useUpdateAdminSessionMutation,
|
||||||
} from '@/services/query/admin-sessions'
|
} 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 route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -49,26 +49,25 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.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 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 SessionDetailsModal from '@/features/admin/sessions/components/modals/SessionDetailsModal.vue'
|
||||||
|
import SessionAttendanceModal from '@/features/admin/sessions/components/modals/SessionAttendanceModal.vue'
|
||||||
import {
|
import {
|
||||||
adminSessionsKeys,
|
adminSessionsKeys,
|
||||||
useAdminSessionsListQuery,
|
useAdminSessionsListQuery,
|
||||||
useDeleteAdminSessionMutation,
|
useDeleteAdminSessionMutation,
|
||||||
} from '@/services/query/admin-sessions'
|
} from '@/services/query/admin-sessions'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -78,12 +78,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import BaseButton from '@/components/BaseButton.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 SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
import ToggleSwitch from '@/components/form/ToggleSwitch.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
term: { type: Object, required: true },
|
term: { type: Object, required: true },
|
||||||
|
|||||||
@@ -47,12 +47,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import TextField from '@/components/form/TextField.vue'
|
import TextField from '@/components/form/TextField.vue'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
import DatePickerField from '@/components/form/DatePickerField.vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -83,24 +83,23 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
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 useModal from '@/composables/useModal'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
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 {
|
import {
|
||||||
adminTermsKeys,
|
adminTermsKeys,
|
||||||
useAddAdminTermStudentsMutation,
|
useAddAdminTermStudentsMutation,
|
||||||
useAdminTermStudentsQuery,
|
useAdminTermStudentsQuery,
|
||||||
useRemoveAdminTermStudentMutation,
|
useRemoveAdminTermStudentMutation,
|
||||||
} from '@/services/query/admin-terms'
|
} from '@/services/query/admin-terms'
|
||||||
import { useAdminUsersListQuery } from '@/services/query/admin-users'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AddTermStudentModal' })
|
defineOptions({ name: 'AddTermStudentModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -86,17 +86,16 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
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 useModal from '@/composables/useModal'
|
||||||
import useDebounce from '@/composables/useDebounce'
|
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 { useAdminCourseTemplatesListQuery } from '@/services/query/admin-course-templates'
|
||||||
import {
|
import {
|
||||||
adminCoursesKeys,
|
adminCoursesKeys,
|
||||||
@@ -104,7 +103,6 @@ import {
|
|||||||
useAdminCoursesListQuery,
|
useAdminCoursesListQuery,
|
||||||
useDeleteAdminCourseMutation,
|
useDeleteAdminCourseMutation,
|
||||||
} from '@/services/query/admin-courses'
|
} from '@/services/query/admin-courses'
|
||||||
import { adminTermsKeys } from '@/services/query/admin-terms'
|
|
||||||
|
|
||||||
defineOptions({ name: 'AttachCourseToTermModal' })
|
defineOptions({ name: 'AttachCourseToTermModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -138,22 +138,28 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 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 { 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 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 {
|
import {
|
||||||
adminTermsKeys,
|
adminTermsKeys,
|
||||||
useAdminTermQuery,
|
useAdminTermQuery,
|
||||||
@@ -161,13 +167,6 @@ import {
|
|||||||
useRemoveAdminTermStudentMutation,
|
useRemoveAdminTermStudentMutation,
|
||||||
useToggleAdminTermStudentLeaveMutation,
|
useToggleAdminTermStudentLeaveMutation,
|
||||||
} from '@/services/query/admin-terms'
|
} from '@/services/query/admin-terms'
|
||||||
import {
|
|
||||||
adminCoursesKeys,
|
|
||||||
useAdminCoursesListQuery,
|
|
||||||
useChangeAdminCourseStatusMutation,
|
|
||||||
useDeleteAdminCourseMutation,
|
|
||||||
} from '@/services/query/admin-courses'
|
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
|
|
||||||
defineOptions({ name: 'TermDetailsModal' })
|
defineOptions({ name: 'TermDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -100,28 +100,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue3-toastify'
|
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 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 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 {
|
import {
|
||||||
adminTermsKeys,
|
adminTermsKeys,
|
||||||
useAddAdminTermMutation,
|
useAddAdminTermMutation,
|
||||||
useAdminTermQuery,
|
useAdminTermQuery,
|
||||||
useUpdateAdminTermMutation,
|
useUpdateAdminTermMutation,
|
||||||
} from '@/services/query/admin-terms'
|
} 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 route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -53,22 +53,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.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 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 TermDetailsModal from '@/features/admin/terms/components/modals/TermDetailsModal.vue'
|
||||||
import AddTermStudentModal from '@/features/admin/terms/components/modals/AddTermStudentModal.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 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 {
|
import {
|
||||||
adminTermsKeys,
|
adminTermsKeys,
|
||||||
useAdminTermsListQuery,
|
useAdminTermsListQuery,
|
||||||
@@ -76,8 +77,6 @@ import {
|
|||||||
useCloneAdminTermMutation,
|
useCloneAdminTermMutation,
|
||||||
useDeleteAdminTermMutation,
|
useDeleteAdminTermMutation,
|
||||||
} from '@/services/query/admin-terms'
|
} from '@/services/query/admin-terms'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -66,14 +66,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 { 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({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -68,11 +68,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import { CHANGEABLE_ROLES, ROLE_LABELS } from '@/enums'
|
||||||
|
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||||
import {
|
import {
|
||||||
adminUsersKeys,
|
adminUsersKeys,
|
||||||
useAdminRolesListQuery,
|
useAdminRolesListQuery,
|
||||||
|
|||||||
@@ -92,16 +92,16 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 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 { useAdminUserQuery } from '@/services/query/admin-users'
|
||||||
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import {
|
import {
|
||||||
EDUCATION_STATUS,
|
EDUCATION_STATUS,
|
||||||
GENDER,
|
GENDER,
|
||||||
@@ -110,7 +110,6 @@ import {
|
|||||||
SEMINARY_LEVEL,
|
SEMINARY_LEVEL,
|
||||||
UNIVERSITY_LEVEL,
|
UNIVERSITY_LEVEL,
|
||||||
} from '@/enums'
|
} from '@/enums'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
|
|
||||||
defineOptions({ name: 'UserDetailsModal' })
|
defineOptions({ name: 'UserDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -80,11 +80,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
user: { type: Object, required: true },
|
user: { type: Object, required: true },
|
||||||
|
|||||||
@@ -50,11 +50,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
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 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({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
+1
-1
@@ -30,8 +30,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'RejectReasonDetailsModal' })
|
defineOptions({ name: 'RejectReasonDetailsModal' })
|
||||||
|
|
||||||
|
|||||||
@@ -67,20 +67,20 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
|
import { PRIORITY_STATUS } from '@/enums'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 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 {
|
import {
|
||||||
adminPendingStudentsKeys,
|
adminPendingStudentsKeys,
|
||||||
useChangePendingStudentStatusMutation,
|
useChangePendingStudentStatusMutation,
|
||||||
} from '@/services/query/admin-pending-students'
|
} 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' })
|
defineOptions({ name: 'RejectReasonModal' })
|
||||||
|
|
||||||
|
|||||||
+7
-15
@@ -225,14 +225,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.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 LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import {
|
||||||
import useModal from '@/composables/useModal'
|
usePendingStudentQuery,
|
||||||
|
useDownloadPendingStudentPdfMutation,
|
||||||
|
} from '@/services/query/admin-pending-students'
|
||||||
import {
|
import {
|
||||||
AUDIENCE_CONFLICT_RESPONSE,
|
AUDIENCE_CONFLICT_RESPONSE,
|
||||||
COMPETING_PROPAGATOR_RESPONSE,
|
COMPETING_PROPAGATOR_RESPONSE,
|
||||||
@@ -247,11 +250,6 @@ import {
|
|||||||
USER_STATUS,
|
USER_STATUS,
|
||||||
VERIFICATION_MEDIA_TYPE,
|
VERIFICATION_MEDIA_TYPE,
|
||||||
} from '@/enums'
|
} from '@/enums'
|
||||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
import {
|
|
||||||
usePendingStudentQuery,
|
|
||||||
useDownloadPendingStudentPdfMutation,
|
|
||||||
} from '@/services/query/admin-pending-students'
|
|
||||||
|
|
||||||
defineOptions({ name: 'UserVerificationDetailsModal' })
|
defineOptions({ name: 'UserVerificationDetailsModal' })
|
||||||
|
|
||||||
@@ -297,12 +295,6 @@ const leaderMessage = computed(() =>
|
|||||||
verificationMedia.value.find((m) => m.type === VERIFICATION_MEDIA_TYPE.LEADER_MESSAGE)
|
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 pdfLoading = ref(false)
|
||||||
const pdfMutation = useDownloadPendingStudentPdfMutation()
|
const pdfMutation = useDownloadPendingStudentPdfMutation()
|
||||||
|
|
||||||
|
|||||||
@@ -219,21 +219,26 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { toast } from 'vue3-toastify'
|
import { toast } from 'vue3-toastify'
|
||||||
|
import useYup from '@/composables/useYup'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import TextField from '@/components/form/TextField.vue'
|
import TextField from '@/components/form/TextField.vue'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
import SelectField from '@/components/form/SelectField.vue'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
|
||||||
import PasswordField from '@/components/form/PasswordField.vue'
|
|
||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import { GENDER, MARITAL_STATUS, ROLE_LABELS } from '@/enums'
|
||||||
import useYup from '@/composables/useYup'
|
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 {
|
import {
|
||||||
adminUsersKeys,
|
adminUsersKeys,
|
||||||
useAddAdminUserMutation,
|
useAddAdminUserMutation,
|
||||||
@@ -241,11 +246,6 @@ import {
|
|||||||
useAdminUserQuery,
|
useAdminUserQuery,
|
||||||
useUpdateAdminUserMutation,
|
useUpdateAdminUserMutation,
|
||||||
} from '@/services/query/admin-users'
|
} 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 route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -41,26 +41,25 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 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 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 RejectReasonModal from '@/features/admin/users/components/verifications/modals/RejectReasonModal.vue'
|
||||||
import RejectReasonDetailsModal from '@/features/admin/users/components/verifications/modals/RejectReasonDetailsModal.vue'
|
import RejectReasonDetailsModal from '@/features/admin/users/components/verifications/modals/RejectReasonDetailsModal.vue'
|
||||||
|
import UserVerificationDetailsModal from '@/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue'
|
||||||
import {
|
import {
|
||||||
adminPendingStudentsKeys,
|
adminPendingStudentsKeys,
|
||||||
useChangePendingStudentStatusMutation,
|
useChangePendingStudentStatusMutation,
|
||||||
usePendingStudentsListQuery,
|
usePendingStudentsListQuery,
|
||||||
} from '@/services/query/admin-pending-students'
|
} from '@/services/query/admin-pending-students'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|||||||
@@ -48,27 +48,26 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
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 BaseButton from '@/components/BaseButton.vue'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.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 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 UserDetailsModal from '@/features/admin/users/components/modals/UserDetailsModal.vue'
|
||||||
import UserAssignmentsModal from '@/features/admin/users/components/modals/UserAssignmentsModal.vue'
|
import UserAssignmentsModal from '@/features/admin/users/components/modals/UserAssignmentsModal.vue'
|
||||||
import UserExamsModal from '@/features/admin/users/components/modals/UserExamsModal.vue'
|
|
||||||
import {
|
import {
|
||||||
adminUsersKeys,
|
adminUsersKeys,
|
||||||
useAdminUsersListQuery,
|
useAdminUsersListQuery,
|
||||||
useDeleteAdminUserMutation,
|
useDeleteAdminUserMutation,
|
||||||
} from '@/services/query/admin-users'
|
} from '@/services/query/admin-users'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
|
||||||
import useModal from '@/composables/useModal'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { object, string } from 'yup'
|
import { object, string } from 'yup'
|
||||||
|
import { passwordRule } from '@/constants/rules/passwordRule'
|
||||||
import { phoneNumberRule } from '@/constants/rules/phoneNumberRule'
|
import { phoneNumberRule } from '@/constants/rules/phoneNumberRule'
|
||||||
import { nationalCodeRule } from '@/constants/rules/nationalCodeRule'
|
import { nationalCodeRule } from '@/constants/rules/nationalCodeRule'
|
||||||
import { passwordRule } from '@/constants/rules/passwordRule'
|
|
||||||
import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule'
|
import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule'
|
||||||
|
|
||||||
export const rejectionReasonSchema = object().shape({
|
export const rejectionReasonSchema = object().shape({
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import PatternSection from '@/features/auth/components/PatternSection.vue'
|
|
||||||
import { gallery } from '@/utils/gallery'
|
import { gallery } from '@/utils/gallery'
|
||||||
|
import PatternSection from '@/features/auth/components/PatternSection.vue'
|
||||||
|
|
||||||
const bgPattern = gallery.loginPattern
|
const bgPattern = gallery.loginPattern
|
||||||
const bgPatternMobile = gallery.loginPatternMobile
|
const bgPatternMobile = gallery.loginPatternMobile
|
||||||
|
|||||||
@@ -65,17 +65,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
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 useYup from '@/composables/useYup'
|
||||||
import useAuth from '@/composables/useAuth'
|
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 { 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'
|
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
|
||||||
|
|
||||||
const emit = defineEmits(['next'])
|
const emit = defineEmits(['next'])
|
||||||
|
|||||||
@@ -48,16 +48,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
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 useYup from '@/composables/useYup'
|
||||||
import useAuth from '@/composables/useAuth'
|
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 { 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({
|
const props = defineProps({
|
||||||
mode: { type: String, default: 'loginWithCode' },
|
mode: { type: String, default: 'loginWithCode' },
|
||||||
|
|||||||
@@ -84,16 +84,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
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 useYup from '@/composables/useYup'
|
||||||
import useAuth from '@/composables/useAuth'
|
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 { 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'])
|
const emit = defineEmits(['next'])
|
||||||
|
|
||||||
|
|||||||
@@ -46,13 +46,16 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import AuthHeading from '@/features/auth/components/AuthHeading.vue'
|
import { useRouter } from 'vue-router'
|
||||||
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 useYup from '@/composables/useYup'
|
||||||
import useAuth from '@/composables/useAuth'
|
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 {
|
import {
|
||||||
useForgotPasswordMutation,
|
useForgotPasswordMutation,
|
||||||
useLoginWithCodeMutation,
|
useLoginWithCodeMutation,
|
||||||
@@ -60,9 +63,6 @@ import {
|
|||||||
useVerifyCodeMutation,
|
useVerifyCodeMutation,
|
||||||
useVerifyForgotPasswordCodeMutation,
|
useVerifyForgotPasswordCodeMutation,
|
||||||
} from '@/services/query/auth'
|
} 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({
|
const props = defineProps({
|
||||||
phoneNumber: { type: String, default: '' },
|
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 { useAuthStore } from '@/store/auth'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
export const useRedirectAfterLogin = () => {
|
export const useRedirectAfterLogin = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -18,12 +18,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { LOGIN_STEPS } from '@/enums'
|
||||||
import AuthShell from '@/features/auth/components/AuthShell.vue'
|
import AuthShell from '@/features/auth/components/AuthShell.vue'
|
||||||
import LoginForm from '@/features/auth/components/LoginForm.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 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 step = ref(LOGIN_STEPS.LOGIN)
|
||||||
const mode = ref('loginWithCode')
|
const mode = ref('loginWithCode')
|
||||||
|
|||||||
@@ -52,18 +52,18 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
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 useYup from '@/composables/useYup'
|
||||||
import useAuth from '@/composables/useAuth'
|
import useAuth from '@/composables/useAuth'
|
||||||
import { useResetPasswordMutation } from '@/services/query/auth'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import { useRedirectAfterLogin } from '@/features/auth/composables/useRedirectAfterLogin'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import TextField from '@/components/form/TextField.vue'
|
||||||
import { resetPasswordSchema } from '@/features/auth/schema'
|
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 { user, applySession } = useAuth()
|
||||||
const redirectAfterLogin = useRedirectAfterLogin()
|
const redirectAfterLogin = useRedirectAfterLogin()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user