fix: test
This commit is contained in:
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
# Use the locally-installed lint-staged so this works under environments
|
||||||
|
# where npm/npx is broken (e.g. GitHub Desktop's bundled git env).
|
||||||
npx lint-staged
|
# exec ./node_modules/.bin/lint-staged
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"*.{js,vue}": "eslint --fix",
|
||||||
|
"*.scss": "prettier --write"
|
||||||
|
}
|
||||||
Generated
+16
@@ -13,6 +13,7 @@
|
|||||||
"@tanstack/vue-query-devtools": "^5.62.2",
|
"@tanstack/vue-query-devtools": "^5.62.2",
|
||||||
"@tinymce/tinymce-vue": "^4.0.7",
|
"@tinymce/tinymce-vue": "^4.0.7",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"husky": "^8.0.0",
|
||||||
"jalaali-js": "^1.2.8",
|
"jalaali-js": "^1.2.8",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
@@ -4264,6 +4265,21 @@
|
|||||||
"entities": "^4.4.0"
|
"entities": "^4.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/husky": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-4qbE/5dzNDNxFEkX9MNRPKl5+omTXQzdILCUWiqG/lWIAioiM5vln265/l6I2Zx8gpW8l1ukZwGQeCFbBZ6+6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"husky": "lib/bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/typicode"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lint:fix": "eslint . --ext .js,.vue --fix --max-warnings=0",
|
"lint:fix": "eslint . --ext .js,.vue --fix --max-warnings=0",
|
||||||
"lint:css:check": "stylelint \"src/**/*.{css,scss,vue}\" --max-warnings=0",
|
"lint:css:check": "stylelint \"src/**/*.{css,scss,vue}\" --max-warnings=0",
|
||||||
"lint:css:fix": "stylelint \"src/**/*.{css,scss,vue}\" --fix --max-warnings=0",
|
"lint:css:fix": "stylelint \"src/**/*.{css,scss,vue}\" --fix --max-warnings=0",
|
||||||
|
"prepare": "husky install",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check ."
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"@tanstack/vue-query-devtools": "^5.62.2",
|
"@tanstack/vue-query-devtools": "^5.62.2",
|
||||||
"@tinymce/tinymce-vue": "^4.0.7",
|
"@tinymce/tinymce-vue": "^4.0.7",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"husky": "^8.0.0",
|
||||||
"jalaali-js": "^1.2.8",
|
"jalaali-js": "^1.2.8",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
<div class="assignment-item__sub">
|
<div class="assignment-item__sub">
|
||||||
<span class="assignment-item__sub-label">دوره:</span>
|
<span class="assignment-item__sub-label">دوره:</span>
|
||||||
<span class="assignment-item__sub-value">
|
<span class="assignment-item__sub-value">
|
||||||
{{ assignment.course?.title || assignment.courseTitle || '—' }}
|
{{ assignment.session?.course?.title || '—' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="assignment-item__dot">|</span>
|
<span class="assignment-item__dot">|</span>
|
||||||
<span class="assignment-item__sub-label">جلسه:</span>
|
<span class="assignment-item__sub-label">جلسه:</span>
|
||||||
<span class="assignment-item__sub-value">
|
<span class="assignment-item__sub-value">
|
||||||
{{ assignment.session?.title || assignment.sessionTitle || '—' }}
|
{{ assignment.session?.title || '—' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,12 +92,10 @@ const duration = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const submissions = computed(() =>
|
const submissions = computed(() =>
|
||||||
props.assignment.submissionsCount == null ? '—' : `${props.assignment.submissionsCount} نفر`
|
props.assignment.submittersCount == null ? '—' : `${props.assignment.submittersCount} نفر`
|
||||||
)
|
)
|
||||||
|
|
||||||
const createdAt = computed(
|
const createdAt = computed(() => formatJalaaliDate(props.assignment.createdAt) || '—')
|
||||||
() => props.assignment.faCreatedAt || formatJalaaliDate(props.assignment.createdAt) || '—'
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -7,6 +7,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
v-model="form.termId"
|
||||||
|
name="termId"
|
||||||
|
label="ترم"
|
||||||
|
:options="termOptions"
|
||||||
|
option-label="title"
|
||||||
|
option-value="id"
|
||||||
|
:searchable="true"
|
||||||
|
:on-search="searchTerms"
|
||||||
|
@update:modelValue="onTermChange"
|
||||||
|
/>
|
||||||
|
<SelectField
|
||||||
|
v-if="form.termId"
|
||||||
v-model="form.courseId"
|
v-model="form.courseId"
|
||||||
name="courseId"
|
name="courseId"
|
||||||
label="دوره"
|
label="دوره"
|
||||||
@@ -15,8 +27,10 @@
|
|||||||
option-value="id"
|
option-value="id"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:on-search="searchTemplates"
|
:on-search="searchTemplates"
|
||||||
|
@update:modelValue="onCourseChange"
|
||||||
/>
|
/>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
v-if="form.courseId"
|
||||||
v-model="form.sessionId"
|
v-model="form.sessionId"
|
||||||
name="sessionId"
|
name="sessionId"
|
||||||
label="جلسه"
|
label="جلسه"
|
||||||
@@ -65,6 +79,7 @@ import TextField from '@/components/form/TextField.vue'
|
|||||||
import CircleButton from '@/components/CircleButton.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 { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
||||||
import { useAdminCoursesListQuery } from '@/services/query/admin-courses'
|
import { useAdminCoursesListQuery } from '@/services/query/admin-courses'
|
||||||
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
||||||
|
|
||||||
@@ -73,6 +88,7 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
title: '',
|
title: '',
|
||||||
|
termId: '',
|
||||||
courseId: '',
|
courseId: '',
|
||||||
sessionId: '',
|
sessionId: '',
|
||||||
fromDate: '',
|
fromDate: '',
|
||||||
@@ -85,6 +101,7 @@ const emit = defineEmits(['update:modelValue', 'apply', 'reset'])
|
|||||||
|
|
||||||
const emptyForm = () => ({
|
const emptyForm = () => ({
|
||||||
title: '',
|
title: '',
|
||||||
|
termId: '',
|
||||||
courseId: '',
|
courseId: '',
|
||||||
sessionId: '',
|
sessionId: '',
|
||||||
fromDate: '',
|
fromDate: '',
|
||||||
@@ -102,10 +119,21 @@ watch(
|
|||||||
|
|
||||||
const todayIso = new Date().toISOString()
|
const todayIso = new Date().toISOString()
|
||||||
|
|
||||||
|
const termSearch = ref('')
|
||||||
|
const termFilters = computed(() => ({ title: termSearch.value }))
|
||||||
|
const termPagination = ref({ page: 1, perPage: 100 })
|
||||||
|
const { data: termsResponse } = useAdminTermsListQuery(termFilters, termPagination)
|
||||||
|
const termOptions = computed(() => termsResponse.value?.data ?? [])
|
||||||
|
|
||||||
const templateSearch = ref('')
|
const templateSearch = ref('')
|
||||||
const templateFilters = computed(() => ({ title: templateSearch.value }))
|
const templateFilters = computed(() => ({
|
||||||
|
title: templateSearch.value,
|
||||||
|
termId: form.value.termId || undefined,
|
||||||
|
}))
|
||||||
const templatePagination = ref({ page: 1, perPage: 10 })
|
const templatePagination = ref({ page: 1, perPage: 10 })
|
||||||
const { data: templatesResponse } = useAdminCoursesListQuery(templateFilters, templatePagination)
|
const { data: templatesResponse } = useAdminCoursesListQuery(templateFilters, templatePagination, {
|
||||||
|
enabled: () => !!form.value.termId,
|
||||||
|
})
|
||||||
const templateOptions = computed(() => templatesResponse.value?.data ?? [])
|
const templateOptions = computed(() => templatesResponse.value?.data ?? [])
|
||||||
|
|
||||||
const sessionSearch = ref('')
|
const sessionSearch = ref('')
|
||||||
@@ -114,9 +142,14 @@ const sessionListFilters = computed(() => ({
|
|||||||
courseId: form.value.courseId || undefined,
|
courseId: form.value.courseId || undefined,
|
||||||
}))
|
}))
|
||||||
const sessionPagination = ref({ page: 1, perPage: 10 })
|
const sessionPagination = ref({ page: 1, perPage: 10 })
|
||||||
const { data: sessionsResponse } = useAdminSessionsListQuery(sessionListFilters, sessionPagination)
|
const { data: sessionsResponse } = useAdminSessionsListQuery(sessionListFilters, sessionPagination, {
|
||||||
|
enabled: () => !!form.value.courseId,
|
||||||
|
})
|
||||||
const sessionOptions = computed(() => sessionsResponse.value?.data ?? [])
|
const sessionOptions = computed(() => sessionsResponse.value?.data ?? [])
|
||||||
|
|
||||||
|
const searchTerms = useDebounce((q) => {
|
||||||
|
termSearch.value = q || ''
|
||||||
|
}, 400)
|
||||||
const searchTemplates = useDebounce((q) => {
|
const searchTemplates = useDebounce((q) => {
|
||||||
templateSearch.value = q || ''
|
templateSearch.value = q || ''
|
||||||
}, 400)
|
}, 400)
|
||||||
@@ -124,6 +157,15 @@ const searchSessions = useDebounce((q) => {
|
|||||||
sessionSearch.value = q || ''
|
sessionSearch.value = q || ''
|
||||||
}, 400)
|
}, 400)
|
||||||
|
|
||||||
|
const onTermChange = () => {
|
||||||
|
form.value.courseId = ''
|
||||||
|
form.value.sessionId = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCourseChange = () => {
|
||||||
|
form.value.sessionId = ''
|
||||||
|
}
|
||||||
|
|
||||||
const hasFilters = computed(() =>
|
const hasFilters = computed(() =>
|
||||||
Object.values(form.value).some((v) => v !== '' && v !== null && v !== undefined)
|
Object.values(form.value).some((v) => v !== '' && v !== null && v !== undefined)
|
||||||
)
|
)
|
||||||
@@ -157,7 +199,7 @@ const onReset = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(6, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,18 @@
|
|||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
|
<SelectField
|
||||||
|
v-model="form.termId"
|
||||||
|
name="termId"
|
||||||
|
label="ترم مرتبط"
|
||||||
|
:options="termOptions"
|
||||||
|
option-label="title"
|
||||||
|
option-value="id"
|
||||||
|
:searchable="true"
|
||||||
|
:on-search="searchTerms"
|
||||||
|
:error="errors.termId"
|
||||||
|
@update:model-value="onTermChange"
|
||||||
|
/>
|
||||||
<SelectField
|
<SelectField
|
||||||
v-model="form.courseId"
|
v-model="form.courseId"
|
||||||
name="courseId"
|
name="courseId"
|
||||||
@@ -30,6 +42,7 @@
|
|||||||
option-value="id"
|
option-value="id"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:on-search="searchTemplates"
|
:on-search="searchTemplates"
|
||||||
|
:disabled="!form.termId"
|
||||||
:error="errors.courseId"
|
:error="errors.courseId"
|
||||||
@update:model-value="onCourseChange"
|
@update:model-value="onCourseChange"
|
||||||
/>
|
/>
|
||||||
@@ -77,13 +90,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="add-assignment__files">
|
<div class="add-assignment__files">
|
||||||
<LineTitleBlock title="فایلهای تکلیف" title-en="Assignment Files" />
|
|
||||||
<FileUploader
|
<FileUploader
|
||||||
v-model="attachments"
|
v-model="attachments"
|
||||||
accept=".mp4,.mov,.avi,.mp3,.wav,.jpg,.jpeg,.png,.pdf,.txt,.doc,.docx"
|
accept=".mp4,.mov,.avi,.mp3,.wav,.jpg,.jpeg,.png,.pdf,.txt,.doc,.docx"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:max-files="10"
|
:max-files="10"
|
||||||
context="assignment"
|
@select="onAttachmentsSelect"
|
||||||
|
@error="onAttachmentsError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,6 +130,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
import useYup from '@/composables/useYup'
|
import useYup from '@/composables/useYup'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
@@ -130,9 +144,12 @@ 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 LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import FileUploader from '@/components/form/FileUploader.vue'
|
import FileUploader from '@/components/form/FileUploader.vue'
|
||||||
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
import DatePickerField from '@/components/form/DatePickerField.vue'
|
||||||
import { assignmentSchema } from '@/features/admin/assignments/schema'
|
import { assignmentSchema } from '@/features/admin/assignments/schema'
|
||||||
|
import { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
||||||
import { useAdminCoursesListQuery } from '@/services/query/admin-courses'
|
import { useAdminCoursesListQuery } from '@/services/query/admin-courses'
|
||||||
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
||||||
import {
|
import {
|
||||||
@@ -158,6 +175,7 @@ const priorityOptions = Object.entries(ASSIGNMENT_PRIORITY).map(([value, label])
|
|||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
title: '',
|
title: '',
|
||||||
|
termId: '',
|
||||||
courseId: '',
|
courseId: '',
|
||||||
sessionId: '',
|
sessionId: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
@@ -172,10 +190,28 @@ const schema = assignmentSchema
|
|||||||
|
|
||||||
const { validate, validateAt, errors, resetErrors } = useYup(schema)
|
const { validate, validateAt, errors, resetErrors } = useYup(schema)
|
||||||
|
|
||||||
|
const termSearch = ref('')
|
||||||
|
const termFilters = computed(() => ({ title: termSearch.value }))
|
||||||
|
const termPagination = ref({ page: 1, perPage: 100 })
|
||||||
|
const { data: termsResponse } = useAdminTermsListQuery(termFilters, termPagination)
|
||||||
|
const selectedTerm = ref(null)
|
||||||
|
const termOptions = computed(() => {
|
||||||
|
const base = termsResponse.value?.data ?? []
|
||||||
|
if (selectedTerm.value && !base.some((t) => t.id === selectedTerm.value.id)) {
|
||||||
|
return [...base, selectedTerm.value]
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
})
|
||||||
|
|
||||||
const templateSearch = ref('')
|
const templateSearch = ref('')
|
||||||
const templateFilters = computed(() => ({ title: templateSearch.value }))
|
const templateFilters = computed(() => ({
|
||||||
|
title: templateSearch.value,
|
||||||
|
termId: form.value.termId || undefined,
|
||||||
|
}))
|
||||||
const templatePagination = ref({ page: 1, perPage: 10 })
|
const templatePagination = ref({ page: 1, perPage: 10 })
|
||||||
const { data: templatesResponse } = useAdminCoursesListQuery(templateFilters, templatePagination)
|
const { data: templatesResponse } = useAdminCoursesListQuery(templateFilters, templatePagination, {
|
||||||
|
enabled: () => !!form.value.termId,
|
||||||
|
})
|
||||||
const selectedTemplate = ref(null)
|
const selectedTemplate = ref(null)
|
||||||
const templateOptions = computed(() => {
|
const templateOptions = computed(() => {
|
||||||
const base = templatesResponse.value?.data ?? []
|
const base = templatesResponse.value?.data ?? []
|
||||||
@@ -203,6 +239,9 @@ const sessionOptions = computed(() => {
|
|||||||
return base
|
return base
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const searchTerms = useDebounce((q) => {
|
||||||
|
termSearch.value = q || ''
|
||||||
|
}, 400)
|
||||||
const searchTemplates = useDebounce((q) => {
|
const searchTemplates = useDebounce((q) => {
|
||||||
templateSearch.value = q || ''
|
templateSearch.value = q || ''
|
||||||
}, 400)
|
}, 400)
|
||||||
@@ -210,37 +249,78 @@ const searchSessions = useDebounce((q) => {
|
|||||||
sessionSearch.value = q || ''
|
sessionSearch.value = q || ''
|
||||||
}, 400)
|
}, 400)
|
||||||
|
|
||||||
|
const onTermChange = (value) => {
|
||||||
|
form.value.termId = value
|
||||||
|
form.value.courseId = ''
|
||||||
|
form.value.sessionId = ''
|
||||||
|
selectedTemplate.value = null
|
||||||
|
selectedSession.value = null
|
||||||
|
}
|
||||||
|
|
||||||
const onCourseChange = (value) => {
|
const onCourseChange = (value) => {
|
||||||
form.value.courseId = value
|
form.value.courseId = value
|
||||||
form.value.sessionId = ''
|
form.value.sessionId = ''
|
||||||
selectedSession.value = null
|
selectedSession.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadMediaMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
|
const onAttachmentsSelect = async (files) => {
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const fd = objectToFormData({ file, purpose: 'homework_file', context: 'homework' })
|
||||||
|
const response = await uploadMediaMutation.mutateAsync(fd)
|
||||||
|
const payload = response?.data ?? response
|
||||||
|
const id = payload?.id ?? payload?.uploadId
|
||||||
|
if (id == null) continue
|
||||||
|
attachments.value = [
|
||||||
|
...attachments.value,
|
||||||
|
{ id, name: file.name, size: file.size, url: payload?.url ?? '' },
|
||||||
|
]
|
||||||
|
} catch {
|
||||||
|
toast.error(`بارگذاری فایل "${file.name}" با خطا مواجه شد.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAttachmentsError = (msg) => toast.error(msg)
|
||||||
|
|
||||||
const { data: existingAssignment } = useAdminAssignmentQuery(assignmentId, {
|
const { data: existingAssignment } = useAdminAssignmentQuery(assignmentId, {
|
||||||
enabled: () => !!assignmentId.value,
|
enabled: () => !!assignmentId.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(existingAssignment, (assignment) => {
|
watch(existingAssignment, (assignment) => {
|
||||||
if (!assignment) return
|
if (!assignment) return
|
||||||
const tpl = assignment.course
|
|
||||||
const sessionEntity = assignment.session
|
const sessionEntity = assignment.session
|
||||||
|
const tpl = sessionEntity?.course || assignment.course
|
||||||
|
const termEntity = tpl?.term || assignment.term
|
||||||
|
const termId = termEntity?.id || tpl?.termId || assignment.termId || ''
|
||||||
|
const priorityValue =
|
||||||
|
assignment.priority ||
|
||||||
|
(typeof assignment.isPriority === 'boolean'
|
||||||
|
? assignment.isPriority
|
||||||
|
? 'mandatory'
|
||||||
|
: 'optional'
|
||||||
|
: 'mandatory')
|
||||||
|
if (termEntity) selectedTerm.value = termEntity
|
||||||
if (tpl) selectedTemplate.value = tpl
|
if (tpl) selectedTemplate.value = tpl
|
||||||
if (sessionEntity) selectedSession.value = sessionEntity
|
if (sessionEntity) selectedSession.value = sessionEntity
|
||||||
form.value = {
|
form.value = {
|
||||||
title: assignment.title || '',
|
title: assignment.title || '',
|
||||||
courseId: tpl?.id || assignment.courseId || '',
|
termId,
|
||||||
|
courseId: tpl?.id || sessionEntity?.courseId || assignment.courseId || '',
|
||||||
sessionId: sessionEntity?.id || assignment.sessionId || '',
|
sessionId: sessionEntity?.id || assignment.sessionId || '',
|
||||||
startDate: assignment.startDate || '',
|
startDate: assignment.startDate || '',
|
||||||
endDate: assignment.endDate || '',
|
endDate: assignment.endDate || assignment.deadline || '',
|
||||||
priority: assignment.priority || 'mandatory',
|
priority: priorityValue,
|
||||||
description: assignment.description || '',
|
description: assignment.description || '',
|
||||||
}
|
}
|
||||||
attachments.value = Array.isArray(assignment.attachments)
|
attachments.value = Array.isArray(assignment.media)
|
||||||
? assignment.attachments.map((file) => ({
|
? assignment.media.map((m) => ({
|
||||||
id: file.id,
|
id: m.id,
|
||||||
name: file.title || `فایل ${file.id}`,
|
name: m.fileName || `فایل ${m.id}`,
|
||||||
url: file.fileUrl || '',
|
size: m.fileSize ?? 0,
|
||||||
type: file.type || 'other',
|
url: m.url || m.downloadUrl || '',
|
||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
})
|
})
|
||||||
@@ -254,13 +334,13 @@ const onSubmit = async (close) => {
|
|||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
const finalPayload = {
|
const finalPayload = {
|
||||||
...payload,
|
sessionId: payload.sessionId,
|
||||||
attachments: attachments.value.map((file, index) => ({
|
title: payload.title,
|
||||||
fileId: file.id,
|
description: payload.description ?? '',
|
||||||
type: file.type || 'other',
|
deadline: payload.endDate || null,
|
||||||
title: file.name,
|
isActive: true,
|
||||||
order: index + 1,
|
isPriority: payload.priority === 'mandatory',
|
||||||
})),
|
mediaIds: attachments.value.map((file) => file.id).filter((id) => id != null),
|
||||||
}
|
}
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateMutation.mutateAsync({ id: assignmentId.value, payload: finalPayload })
|
await updateMutation.mutateAsync({ id: assignmentId.value, payload: finalPayload })
|
||||||
|
|||||||
@@ -14,12 +14,9 @@
|
|||||||
<div class="assignment-details__head">
|
<div class="assignment-details__head">
|
||||||
<p class="assignment-details__title">{{ assignment.title || '—' }}</p>
|
<p class="assignment-details__title">{{ assignment.title || '—' }}</p>
|
||||||
<p class="assignment-details__sub">
|
<p class="assignment-details__sub">
|
||||||
<span>
|
<span>دوره: {{ courseTitle }}</span>
|
||||||
دوره:
|
|
||||||
{{ assignment.course?.title || assignment.courseTitle || '—' }}
|
|
||||||
</span>
|
|
||||||
<span class="assignment-details__sep">|</span>
|
<span class="assignment-details__sep">|</span>
|
||||||
<span>جلسه: {{ assignment.session?.title || assignment.sessionTitle || '—' }}</span>
|
<span>جلسه: {{ sessionTitle }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -28,21 +25,10 @@
|
|||||||
title="تاریخ شروع"
|
title="تاریخ شروع"
|
||||||
:numeric-desc="formatJalaaliDate(assignment.startDate) || '—'"
|
:numeric-desc="formatJalaaliDate(assignment.startDate) || '—'"
|
||||||
/>
|
/>
|
||||||
<LineInfoBlock
|
<LineInfoBlock title="تاریخ پایان" :numeric-desc="deadlineLabel" />
|
||||||
title="تاریخ پایان"
|
<LineInfoBlock title="مدت زمان" :numeric-desc="durationLabel" />
|
||||||
:numeric-desc="formatJalaaliDate(assignment.endDate) || '—'"
|
<LineInfoBlock title="اولویت" :desc="priorityLabel" />
|
||||||
/>
|
<LineInfoBlock title="تعداد فرستندگان" :numeric-desc="submittersLabel" />
|
||||||
<LineInfoBlock
|
|
||||||
title="مدت زمان"
|
|
||||||
:numeric-desc="
|
|
||||||
assignment.durationDays != null ? `${assignment.durationDays} روز` : '—'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<LineInfoBlock title="اولویت" :desc="ASSIGNMENT_PRIORITY[assignment.priority] || '—'" />
|
|
||||||
<LineInfoBlock
|
|
||||||
title="تعداد فرستندگان"
|
|
||||||
:numeric-desc="assignment.submissionsCount ?? 0"
|
|
||||||
/>
|
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="تاریخ ثبت"
|
title="تاریخ ثبت"
|
||||||
:numeric-desc="
|
:numeric-desc="
|
||||||
@@ -55,18 +41,18 @@
|
|||||||
<LineInfoBlock title="توضیحات" :desc="assignment.description" />
|
<LineInfoBlock title="توضیحات" :desc="assignment.description" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="assignment.attachments?.length" class="assignment-details__attachments">
|
<div v-if="attachmentItems.length" class="assignment-details__attachments">
|
||||||
<LineTitleBlock title="فایلهای تکلیف" title-en="Attachments" />
|
<LineTitleBlock title="فایلهای تکلیف" title-en="Attachments" />
|
||||||
<ul class="assignment-details__attachments-list">
|
<ul class="assignment-details__attachments-list">
|
||||||
<li v-for="file in assignment.attachments" :key="file.id">
|
<li v-for="file in attachmentItems" :key="file.id">
|
||||||
<a
|
<a
|
||||||
:href="file.fileUrl || '#'"
|
:href="file.url || '#'"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="assignment-details__file"
|
class="assignment-details__file"
|
||||||
>
|
>
|
||||||
<SvgIcon name="file" :size="16" color="var(--color-prim-gray)" />
|
<SvgIcon name="file" :size="16" color="var(--color-prim-gray)" />
|
||||||
<span>{{ file.title || `فایل ${file.id}` }}</span>
|
<span>{{ file.title }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -112,6 +98,65 @@ const assignmentId = computed(() => modalData.value.id ?? null)
|
|||||||
const { data: assignment, isLoading } = useAdminAssignmentQuery(assignmentId, {
|
const { data: assignment, isLoading } = useAdminAssignmentQuery(assignmentId, {
|
||||||
enabled: () => !!assignmentId.value,
|
enabled: () => !!assignmentId.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const courseTitle = computed(
|
||||||
|
() =>
|
||||||
|
assignment.value?.session?.course?.title ||
|
||||||
|
assignment.value?.course?.title ||
|
||||||
|
assignment.value?.courseTitle ||
|
||||||
|
'—'
|
||||||
|
)
|
||||||
|
|
||||||
|
const sessionTitle = computed(
|
||||||
|
() => assignment.value?.session?.title || assignment.value?.sessionTitle || '—'
|
||||||
|
)
|
||||||
|
|
||||||
|
const deadlineRaw = computed(() => assignment.value?.endDate || assignment.value?.deadline || '')
|
||||||
|
const deadlineLabel = computed(() => formatJalaaliDate(deadlineRaw.value) || '—')
|
||||||
|
|
||||||
|
const durationLabel = computed(() => {
|
||||||
|
if (assignment.value?.durationDays != null) return `${assignment.value.durationDays} روز`
|
||||||
|
const start = assignment.value?.startDate || assignment.value?.createdAt
|
||||||
|
const end = deadlineRaw.value
|
||||||
|
if (!start || !end) return '—'
|
||||||
|
const diff = Math.ceil((new Date(end).getTime() - new Date(start).getTime()) / 86_400_000)
|
||||||
|
return diff >= 0 ? `${diff} روز` : '—'
|
||||||
|
})
|
||||||
|
|
||||||
|
const priorityLabel = computed(() => {
|
||||||
|
const priority =
|
||||||
|
assignment.value?.priority ||
|
||||||
|
(typeof assignment.value?.isPriority === 'boolean'
|
||||||
|
? assignment.value.isPriority
|
||||||
|
? 'mandatory'
|
||||||
|
: 'optional'
|
||||||
|
: null)
|
||||||
|
return ASSIGNMENT_PRIORITY[priority] || '—'
|
||||||
|
})
|
||||||
|
|
||||||
|
const submittersLabel = computed(
|
||||||
|
() => assignment.value?.submittersCount ?? assignment.value?.submissionsCount ?? 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const attachmentItems = computed(() => {
|
||||||
|
const media = assignment.value?.media
|
||||||
|
if (Array.isArray(media) && media.length > 0) {
|
||||||
|
return media.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
title: m.fileName || `فایل ${m.id}`,
|
||||||
|
url: m.url || m.downloadUrl || '',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
const legacy = assignment.value?.attachments
|
||||||
|
if (Array.isArray(legacy)) {
|
||||||
|
return legacy.map((a) => ({
|
||||||
|
id: a.id,
|
||||||
|
title: a.title || `فایل ${a.id}`,
|
||||||
|
url: a.fileUrl || '',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicModal width="95%" max-width="60rem" min-width="auto" :show-close-button="true">
|
<BasicModal
|
||||||
<template #default="{ data, close }">
|
title="تکالیف فرستادگان"
|
||||||
|
title-en="Delegates’ Submissions"
|
||||||
|
width="95%"
|
||||||
|
max-width="60rem"
|
||||||
|
min-width="auto"
|
||||||
|
:show-close-button="true"
|
||||||
|
>
|
||||||
|
<template #default="{ close }">
|
||||||
<div class="assignment-submissions">
|
<div class="assignment-submissions">
|
||||||
<LineTitleBlock title="تکالیف فرستادگان" title-en="Delegates’ Submissions" />
|
|
||||||
<p v-if="data?.title" class="assignment-submissions__assignment">{{ data.title }}</p>
|
|
||||||
|
|
||||||
<SkeletonLoaderBlock v-if="isLoading" :rows="4" :cols-per-row="1" />
|
<SkeletonLoaderBlock v-if="isLoading" :rows="4" :cols-per-row="1" />
|
||||||
<div v-else-if="submissions.length > 0" class="assignment-submissions__list">
|
<div v-else-if="submissions.length > 0" class="assignment-submissions__list">
|
||||||
<div
|
<div
|
||||||
@@ -101,15 +105,16 @@ const paginationMeta = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const userName = (user) =>
|
const userName = (user) =>
|
||||||
`${user?.firstName || ''} ${user?.lastName || ''}`.trim() || user?.fullName || '—'
|
user?.name || `${user?.firstName || ''} ${user?.lastName || ''}`.trim() || user?.fullName || '—'
|
||||||
|
|
||||||
const locationFromAddress = (user) => {
|
const locationFromAddress = (user) => {
|
||||||
const province = user?.address?.province?.name || ''
|
const province = user?.province?.name || user?.address?.province?.name || ''
|
||||||
const city = user?.address?.city?.name || ''
|
const city = user?.city?.name || user?.address?.city?.name || ''
|
||||||
return [province, city].filter(Boolean).join('، ')
|
return [province, city].filter(Boolean).join('، ')
|
||||||
}
|
}
|
||||||
|
|
||||||
const submittedAt = (sub) => sub.faSubmittedAt || formatJalaaliDate(sub.submittedAt) || '—'
|
const submittedAt = (sub) =>
|
||||||
|
sub.faSubmittedAt || formatJalaaliDate(sub.submittedAt || sub.createdAt || sub.reviewedAt) || '—'
|
||||||
|
|
||||||
const onShowDetails = (submission) => {
|
const onShowDetails = (submission) => {
|
||||||
openModal('AssignmentSubmissionDetailsModal', {
|
openModal('AssignmentSubmissionDetailsModal', {
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const { openModal, isModal } = useModal()
|
|||||||
|
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
title: '',
|
title: '',
|
||||||
|
termId: '',
|
||||||
courseId: '',
|
courseId: '',
|
||||||
sessionId: '',
|
sessionId: '',
|
||||||
fromDate: '',
|
fromDate: '',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const assignmentSchema = object().shape({
|
|||||||
title: string().required().min(3),
|
title: string().required().min(3),
|
||||||
courseId: string().required(),
|
courseId: string().required(),
|
||||||
sessionId: string().required(),
|
sessionId: string().required(),
|
||||||
startDate: string().required(),
|
startDate: string().nullable().notRequired(),
|
||||||
endDate: string().required(),
|
endDate: string().required(),
|
||||||
priority: string().required(),
|
priority: string().required(),
|
||||||
description: string().nullable().notRequired(),
|
description: string().nullable().notRequired(),
|
||||||
|
|||||||
@@ -35,15 +35,15 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="exam-question-builder__col exam-question-builder__col--score">
|
<div class="exam-question-builder__col exam-question-builder__col--score">
|
||||||
<label class="exam-question-builder__label">ترتیب</label>
|
<label class="exam-question-builder__label">بارم نمره</label>
|
||||||
<input
|
<input
|
||||||
:value="question.position"
|
:value="question.score"
|
||||||
:disabled="disabled || isLocked(question)"
|
:disabled="disabled || isLocked(question)"
|
||||||
type="text"
|
type="text"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
class="exam-question-builder__input"
|
class="exam-question-builder__input"
|
||||||
@input="updateQuestion(question.id, { position: $event.target.value })"
|
@input="updateQuestion(question.id, { score: $event.target.value })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +147,7 @@ const createOption = () => ({ id: createId('option'), optionText: '', isCorrect:
|
|||||||
const createQuestion = () => ({
|
const createQuestion = () => ({
|
||||||
id: createId('question'),
|
id: createId('question'),
|
||||||
questionText: '',
|
questionText: '',
|
||||||
position: questions.value.length + 1,
|
score: '',
|
||||||
options: [createOption(), createOption()],
|
options: [createOption(), createOption()],
|
||||||
__local: true,
|
__local: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
v-model="form.termId"
|
||||||
|
name="termId"
|
||||||
|
label="ترم"
|
||||||
|
:options="termOptions"
|
||||||
|
option-label="title"
|
||||||
|
option-value="id"
|
||||||
|
:searchable="true"
|
||||||
|
:on-search="searchTerms"
|
||||||
|
@update:modelValue="onTermChange"
|
||||||
|
/>
|
||||||
|
<SelectField
|
||||||
|
v-if="form.termId"
|
||||||
v-model="form.courseId"
|
v-model="form.courseId"
|
||||||
name="courseId"
|
name="courseId"
|
||||||
label="دوره"
|
label="دوره"
|
||||||
@@ -15,6 +27,18 @@
|
|||||||
option-value="id"
|
option-value="id"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:on-search="searchTemplates"
|
:on-search="searchTemplates"
|
||||||
|
@update:modelValue="onCourseChange"
|
||||||
|
/>
|
||||||
|
<SelectField
|
||||||
|
v-if="form.courseId"
|
||||||
|
v-model="form.sessionId"
|
||||||
|
name="sessionId"
|
||||||
|
label="جلسه"
|
||||||
|
:options="sessionOptions"
|
||||||
|
option-label="title"
|
||||||
|
option-value="id"
|
||||||
|
:searchable="true"
|
||||||
|
:on-search="searchSessions"
|
||||||
/>
|
/>
|
||||||
<DatePickerField v-model="form.fromDate" name="fromDate" label="از تاریخ" :max="todayIso" />
|
<DatePickerField v-model="form.fromDate" name="fromDate" label="از تاریخ" :max="todayIso" />
|
||||||
<DatePickerField v-model="form.toDate" name="toDate" label="تا تاریخ" :max="todayIso" />
|
<DatePickerField v-model="form.toDate" name="toDate" label="تا تاریخ" :max="todayIso" />
|
||||||
@@ -55,18 +79,34 @@ import TextField from '@/components/form/TextField.vue'
|
|||||||
import CircleButton from '@/components/CircleButton.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 { useAdminTermsListQuery } from '@/services/query/admin-terms'
|
||||||
import { useAdminCoursesListQuery } from '@/services/query/admin-courses'
|
import { useAdminCoursesListQuery } from '@/services/query/admin-courses'
|
||||||
|
import { useAdminSessionsListQuery } from '@/services/query/admin-sessions'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ title: '', courseId: '', fromDate: '', toDate: '' }),
|
default: () => ({
|
||||||
|
title: '',
|
||||||
|
termId: '',
|
||||||
|
courseId: '',
|
||||||
|
sessionId: '',
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'apply', 'reset'])
|
const emit = defineEmits(['update:modelValue', 'apply', 'reset'])
|
||||||
|
|
||||||
const emptyForm = () => ({ title: '', courseId: '', fromDate: '', toDate: '' })
|
const emptyForm = () => ({
|
||||||
|
title: '',
|
||||||
|
termId: '',
|
||||||
|
courseId: '',
|
||||||
|
sessionId: '',
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
})
|
||||||
const form = ref({ ...emptyForm(), ...props.modelValue })
|
const form = ref({ ...emptyForm(), ...props.modelValue })
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -79,15 +119,52 @@ watch(
|
|||||||
|
|
||||||
const todayIso = new Date().toISOString()
|
const todayIso = new Date().toISOString()
|
||||||
|
|
||||||
|
const termSearch = ref('')
|
||||||
|
const termFilters = computed(() => ({ title: termSearch.value }))
|
||||||
|
const termPagination = ref({ page: 1, perPage: 100 })
|
||||||
|
const { data: termsResponse } = useAdminTermsListQuery(termFilters, termPagination)
|
||||||
|
const termOptions = computed(() => termsResponse.value?.data ?? [])
|
||||||
|
|
||||||
const templateSearch = ref('')
|
const templateSearch = ref('')
|
||||||
const templateFilters = computed(() => ({ title: templateSearch.value }))
|
const templateFilters = computed(() => ({
|
||||||
|
title: templateSearch.value,
|
||||||
|
termId: form.value.termId || undefined,
|
||||||
|
}))
|
||||||
const templatePagination = ref({ page: 1, perPage: 10 })
|
const templatePagination = ref({ page: 1, perPage: 10 })
|
||||||
const { data: templatesResponse } = useAdminCoursesListQuery(templateFilters, templatePagination)
|
const { data: templatesResponse } = useAdminCoursesListQuery(templateFilters, templatePagination, {
|
||||||
|
enabled: () => !!form.value.termId,
|
||||||
|
})
|
||||||
const templateOptions = computed(() => templatesResponse.value?.data ?? [])
|
const templateOptions = computed(() => templatesResponse.value?.data ?? [])
|
||||||
|
|
||||||
|
const sessionSearch = ref('')
|
||||||
|
const sessionListFilters = computed(() => ({
|
||||||
|
title: sessionSearch.value,
|
||||||
|
courseId: form.value.courseId || undefined,
|
||||||
|
}))
|
||||||
|
const sessionPagination = ref({ page: 1, perPage: 10 })
|
||||||
|
const { data: sessionsResponse } = useAdminSessionsListQuery(sessionListFilters, sessionPagination, {
|
||||||
|
enabled: () => !!form.value.courseId,
|
||||||
|
})
|
||||||
|
const sessionOptions = computed(() => sessionsResponse.value?.data ?? [])
|
||||||
|
|
||||||
|
const searchTerms = useDebounce((q) => {
|
||||||
|
termSearch.value = q || ''
|
||||||
|
}, 400)
|
||||||
const searchTemplates = useDebounce((q) => {
|
const searchTemplates = useDebounce((q) => {
|
||||||
templateSearch.value = q || ''
|
templateSearch.value = q || ''
|
||||||
}, 400)
|
}, 400)
|
||||||
|
const searchSessions = useDebounce((q) => {
|
||||||
|
sessionSearch.value = q || ''
|
||||||
|
}, 400)
|
||||||
|
|
||||||
|
const onTermChange = () => {
|
||||||
|
form.value.courseId = ''
|
||||||
|
form.value.sessionId = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCourseChange = () => {
|
||||||
|
form.value.sessionId = ''
|
||||||
|
}
|
||||||
|
|
||||||
const hasFilters = computed(() =>
|
const hasFilters = computed(() =>
|
||||||
Object.values(form.value).some((v) => v !== '' && v !== null && v !== undefined)
|
Object.values(form.value).some((v) => v !== '' && v !== null && v !== undefined)
|
||||||
@@ -118,7 +195,11 @@ const onReset = () => {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ const blankOption = () => ({ id: createId('option'), optionText: '', isCorrect:
|
|||||||
const blankQuestion = () => ({
|
const blankQuestion = () => ({
|
||||||
id: createId('question'),
|
id: createId('question'),
|
||||||
questionText: '',
|
questionText: '',
|
||||||
position: 1,
|
score: '',
|
||||||
options: [blankOption(), { ...blankOption(), id: createId('option') }],
|
options: [blankOption(), { ...blankOption(), id: createId('option') }],
|
||||||
__local: true,
|
__local: true,
|
||||||
})
|
})
|
||||||
@@ -172,7 +172,7 @@ const normalizeExistingQuestions = (raw = []) => {
|
|||||||
return {
|
return {
|
||||||
id: q.id ?? createId(`question-${qIdx}`),
|
id: q.id ?? createId(`question-${qIdx}`),
|
||||||
questionText: q.questionText || '',
|
questionText: q.questionText || '',
|
||||||
position: q.position ?? qIdx + 1,
|
score: q.score ?? '',
|
||||||
options: options.map((o, oIdx) => ({
|
options: options.map((o, oIdx) => ({
|
||||||
id: o.id ?? createId(`option-${qIdx}-${oIdx}`),
|
id: o.id ?? createId(`option-${qIdx}-${oIdx}`),
|
||||||
optionText: o.optionText || '',
|
optionText: o.optionText || '',
|
||||||
@@ -220,7 +220,8 @@ const validateLocalQuestions = () => {
|
|||||||
questionError.value = ''
|
questionError.value = ''
|
||||||
return localOnes.map((q, idx) => ({
|
return localOnes.map((q, idx) => ({
|
||||||
questionText: q.questionText.trim(),
|
questionText: q.questionText.trim(),
|
||||||
position: Number(q.position) || idx + 1,
|
position: idx + 1,
|
||||||
|
score: Number(q.score) || 0,
|
||||||
options: q.options
|
options: q.options
|
||||||
.filter((o) => String(o.optionText || '').trim())
|
.filter((o) => String(o.optionText || '').trim())
|
||||||
.map((o) => ({
|
.map((o) => ({
|
||||||
|
|||||||
@@ -75,7 +75,14 @@ const router = useRouter()
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
const filters = ref({ title: '', courseId: '', fromDate: '', toDate: '' })
|
const filters = ref({
|
||||||
|
title: '',
|
||||||
|
termId: '',
|
||||||
|
courseId: '',
|
||||||
|
sessionId: '',
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
})
|
||||||
const { pagination, setPage, reset: resetPagination } = usePagination({ page: 1, perPage: 10 })
|
const { pagination, setPage, reset: resetPagination } = usePagination({ page: 1, perPage: 10 })
|
||||||
|
|
||||||
const { data, isLoading } = useAdminExamsListQuery(filters, pagination, {
|
const { data, isLoading } = useAdminExamsListQuery(filters, pagination, {
|
||||||
|
|||||||
@@ -34,9 +34,9 @@
|
|||||||
<div class="add-term-student__text">
|
<div class="add-term-student__text">
|
||||||
<p class="add-term-student__name">{{ userLabel(user) }}</p>
|
<p class="add-term-student__name">{{ userLabel(user) }}</p>
|
||||||
<p class="add-term-student__meta">
|
<p class="add-term-student__meta">
|
||||||
<span>{{ user.address?.province?.name || '—' }}</span>
|
<span>{{ user?.province?.name || '—' }}</span>
|
||||||
<span class="add-term-student__sep">،</span>
|
<span class="add-term-student__sep">،</span>
|
||||||
<span>{{ user.address?.city?.name || '—' }}</span>
|
<span>{{ user?.city?.name || '—' }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,11 +46,11 @@
|
|||||||
<LineInfoBlock title="کد ملی" :numeric-desc="user.nationalCode || '—'" />
|
<LineInfoBlock title="کد ملی" :numeric-desc="user.nationalCode || '—'" />
|
||||||
<LineInfoBlock title="وضعیت تاهل" :desc="MARITAL_STATUS[user.maritalStatus] || '—'" />
|
<LineInfoBlock title="وضعیت تاهل" :desc="MARITAL_STATUS[user.maritalStatus] || '—'" />
|
||||||
<LineInfoBlock title="جنسیت" :desc="GENDER[user.gender] || '—'" />
|
<LineInfoBlock title="جنسیت" :desc="GENDER[user.gender] || '—'" />
|
||||||
<LineInfoBlock title="استان" :desc="user.address?.province?.name || '—'" />
|
<LineInfoBlock title="استان" :desc="user?.province?.name || '—'" />
|
||||||
<LineInfoBlock title="شهر" :desc="user.address?.city?.name || '—'" />
|
<LineInfoBlock title="شهر" :desc="user?.city?.name || '—'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LineInfoBlock title="آدرس" :desc="user.address?.address || '—'" />
|
<LineInfoBlock title="آدرس" :desc="user?.address || '—'" />
|
||||||
<LineInfoBlock title="بیوگرافی" :desc="user.bio || '—'" />
|
<LineInfoBlock title="بیوگرافی" :desc="user.bio || '—'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
<div class="verification-item__info">
|
<div class="verification-item__info">
|
||||||
<p class="verification-item__name">{{ fullName }}</p>
|
<p class="verification-item__name">{{ fullName }}</p>
|
||||||
<p class="verification-item__location">
|
<p class="verification-item__location">
|
||||||
<span>{{ user.address?.province?.name || '—' }}</span>
|
<span>{{ user?.province?.name || '—' }}</span>
|
||||||
<span class="verification-item__sep">،</span>
|
<span class="verification-item__sep">،</span>
|
||||||
<span>{{ user.address?.city?.name || '—' }}</span>
|
<span>{{ user?.city?.name || '—' }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-3
@@ -26,10 +26,10 @@
|
|||||||
:desc="profile.faMaritalStatus || MARITAL_STATUS[profile.maritalStatus] || ''"
|
:desc="profile.faMaritalStatus || MARITAL_STATUS[profile.maritalStatus] || ''"
|
||||||
/>
|
/>
|
||||||
<LineInfoBlock title="جنسیت" :desc="profile.faGender || GENDER[profile.gender] || ''" />
|
<LineInfoBlock title="جنسیت" :desc="profile.faGender || GENDER[profile.gender] || ''" />
|
||||||
<LineInfoBlock title="استان" :desc="user.address?.province?.name || '-'" />
|
<LineInfoBlock title="استان" :desc="user?.province?.name || '-'" />
|
||||||
<LineInfoBlock title="شهر" :desc="user.address?.city?.name || '-'" />
|
<LineInfoBlock title="شهر" :desc="user?.city?.name || '-'" />
|
||||||
<div class="user-verification-details__cell user-verification-details__cell--span-3">
|
<div class="user-verification-details__cell user-verification-details__cell--span-3">
|
||||||
<LineInfoBlock title="آدرس" :desc="user.address?.address || '-'" />
|
<LineInfoBlock title="آدرس" :desc="user?.address || '-'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -231,9 +231,9 @@ import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
|||||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||||
import { GENDER, MARITAL_STATUS, ROLE_LABELS } from '@/enums'
|
import { GENDER, MARITAL_STATUS, ROLE_LABELS } from '@/enums'
|
||||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import PasswordField from '@/components/form/PasswordField.vue'
|
import PasswordField from '@/components/form/PasswordField.vue'
|
||||||
import { useUploadMediaMutation } from '@/services/query/auth'
|
|
||||||
import DatePickerField from '@/components/form/DatePickerField.vue'
|
import DatePickerField from '@/components/form/DatePickerField.vue'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
@@ -337,9 +337,9 @@ watch(existingUser, (user) => {
|
|||||||
gender: user.profile?.gender || '',
|
gender: user.profile?.gender || '',
|
||||||
bio: user.profile?.bio || '',
|
bio: user.profile?.bio || '',
|
||||||
phoneNumber: user.phoneNumber || '',
|
phoneNumber: user.phoneNumber || '',
|
||||||
provinceId: user.address?.province?.id || '',
|
provinceId: user?.province?.id || '',
|
||||||
cityId: user.address?.city?.id || '',
|
cityId: user?.city?.id || '',
|
||||||
address: user.address?.address || '',
|
address: user?.address || '',
|
||||||
avatarId: user.profile?.avatarId || null,
|
avatarId: user.profile?.avatarId || null,
|
||||||
}
|
}
|
||||||
initialNationalCode.value = user.nationalCode || ''
|
initialNationalCode.value = user.nationalCode || ''
|
||||||
|
|||||||
@@ -295,7 +295,10 @@ const { data: profile } = useGetMeQuery()
|
|||||||
|
|
||||||
watch(profile, (user) => {
|
watch(profile, (user) => {
|
||||||
if (!user) return
|
if (!user) return
|
||||||
const nameParts = String(user.name || '').trim().split(/\s+/).filter(Boolean)
|
const nameParts = String(user.name || '')
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
const firstName = user.firstName || nameParts[0] || ''
|
const firstName = user.firstName || nameParts[0] || ''
|
||||||
const lastName = user.lastName || nameParts.slice(1).join(' ') || ''
|
const lastName = user.lastName || nameParts.slice(1).join(' ') || ''
|
||||||
const phone = user.phone || user.phoneNumber || ''
|
const phone = user.phone || user.phoneNumber || ''
|
||||||
@@ -309,9 +312,9 @@ watch(profile, (user) => {
|
|||||||
gender: user.profile?.gender || '',
|
gender: user.profile?.gender || '',
|
||||||
bio: user.profile?.bio || '',
|
bio: user.profile?.bio || '',
|
||||||
phoneNumber: phone,
|
phoneNumber: phone,
|
||||||
provinceId: user.address?.province?.id || '',
|
provinceId: user?.province?.id || '',
|
||||||
cityId: user.address?.city?.id || '',
|
cityId: user?.city?.id || '',
|
||||||
address: user.address?.address || '',
|
address: user?.address || '',
|
||||||
avatarId: user.profile?.avatarId || null,
|
avatarId: user.profile?.avatarId || null,
|
||||||
}
|
}
|
||||||
initialNationalCode.value = user.nationalCode || ''
|
initialNationalCode.value = user.nationalCode || ''
|
||||||
|
|||||||
@@ -84,10 +84,7 @@ export const endpoints = {
|
|||||||
deleteSession: '/sessions/:id',
|
deleteSession: '/sessions/:id',
|
||||||
|
|
||||||
// ─── Backend-aligned: Exams ────────────────────────────────────────────────
|
// ─── Backend-aligned: Exams ────────────────────────────────────────────────
|
||||||
// `getExamsList` is not yet in the backend Postman doc, but per the user
|
getExamsList: '/admin/exams',
|
||||||
// the backend will add it at /exams — wired up now so the FE doesn't need
|
|
||||||
// to change when the endpoint lands.
|
|
||||||
getExamsList: '/exams',
|
|
||||||
showExam: '/exams/:id',
|
showExam: '/exams/:id',
|
||||||
addNewExam: '/exams',
|
addNewExam: '/exams',
|
||||||
updateExam: '/exams/:id',
|
updateExam: '/exams/:id',
|
||||||
@@ -134,7 +131,7 @@ export const endpoints = {
|
|||||||
// backend Postman doc currently exposes only create/update/delete/submit/review;
|
// backend Postman doc currently exposes only create/update/delete/submit/review;
|
||||||
// list + show + submissions list + show-submission stay mock-only until backend
|
// list + show + submissions list + show-submission stay mock-only until backend
|
||||||
// adds them, but URL shape is locked in.
|
// adds them, but URL shape is locked in.
|
||||||
getAssignmentsList: '/homeworks',
|
getAssignmentsList: '/admin/homeworks',
|
||||||
showAssignment: '/homeworks/:id',
|
showAssignment: '/homeworks/:id',
|
||||||
getAssignmentSubmissions: '/homeworks/:homeworkId/submissions',
|
getAssignmentSubmissions: '/homeworks/:homeworkId/submissions',
|
||||||
showAssignmentSubmission: '/homework-submissions/:submissionId',
|
showAssignmentSubmission: '/homework-submissions/:submissionId',
|
||||||
|
|||||||
@@ -20,11 +20,18 @@ const computeDurationDays = (start, end) => {
|
|||||||
return diff >= 0 ? diff : null
|
return diff >= 0 ? diff : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const courseTermId = (courseId) => {
|
||||||
|
if (courseId == null) return null
|
||||||
|
const course = adminCourses.find((c) => c.id === Number(courseId))
|
||||||
|
return course?.termId ?? course?.term?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
register('GET', endpoints.getAssignmentsList, ({ query }) => {
|
register('GET', endpoints.getAssignmentsList, ({ query }) => {
|
||||||
let list = filterItems(adminAssignments, query, {
|
let list = filterItems(adminAssignments, query, {
|
||||||
title: 'includes',
|
title: 'includes',
|
||||||
courseId: (item, v) => String(item.course?.id) === String(v),
|
courseId: (item, v) => String(item.course?.id) === String(v),
|
||||||
sessionId: (item, v) => String(item.session?.id) === String(v),
|
sessionId: (item, v) => String(item.session?.id) === String(v),
|
||||||
|
termId: (item, v) => String(courseTermId(item.course?.id)) === String(v),
|
||||||
})
|
})
|
||||||
list = filterDateRange(list, query)
|
list = filterDateRange(list, query)
|
||||||
return paginate(list, query)
|
return paginate(list, query)
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ register('GET', endpoints.getExamsList, ({ query }) => {
|
|||||||
title: 'includes',
|
title: 'includes',
|
||||||
courseId: (item, v) => String(item.course?.id) === String(v),
|
courseId: (item, v) => String(item.course?.id) === String(v),
|
||||||
sessionId: (item, v) => String(item.sessionId) === String(v),
|
sessionId: (item, v) => String(item.sessionId) === String(v),
|
||||||
|
termId: (item, v) => {
|
||||||
|
const course = adminCourses.find((c) => c.id === Number(item.course?.id))
|
||||||
|
const termId = course?.termId ?? course?.term?.id ?? null
|
||||||
|
return String(termId) === String(v)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
list = filterDateRange(list, query)
|
list = filterDateRange(list, query)
|
||||||
const { data: items, meta } = paginate(list, query)
|
const { data: items, meta } = paginate(list, query)
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export const useAdminAssignmentsListQuery = (filtersRef, paginationRef, options
|
|||||||
...cleanFilters(filtersRef.value),
|
...cleanFilters(filtersRef.value),
|
||||||
...paginationRef.value,
|
...paginationRef.value,
|
||||||
}),
|
}),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data?.items ?? response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -65,6 +69,10 @@ export const useAdminAssignmentSubmissionsQuery = (
|
|||||||
...cleanFilters(filtersRef?.value || {}),
|
...cleanFilters(filtersRef?.value || {}),
|
||||||
...paginationRef?.value,
|
...paginationRef?.value,
|
||||||
}),
|
}),
|
||||||
|
select: (response) => ({
|
||||||
|
data: response?.data?.items ?? response?.data ?? [],
|
||||||
|
meta: response?.data?.meta ?? response?.meta,
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user