@@ -5,12 +5,6 @@
|
||||
|
||||
<div class="ses__stats">
|
||||
<div class="ses__stat">
|
||||
<SvgIcon name="check" :size="12" color="#5d5d5d" />
|
||||
<span class="ses__stat-label">تعداد آزمونها :</span>
|
||||
<span class="ses__stat-value">{{ attemptsCountText }}</span>
|
||||
</div>
|
||||
<div class="ses__stat">
|
||||
<SvgIcon name="scroll" :size="12" color="#5d5d5d" />
|
||||
<span class="ses__stat-label">تعداد سوال :</span>
|
||||
<span class="ses__stat-value">{{ questionsCountText }}</span>
|
||||
</div>
|
||||
@@ -65,11 +59,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['start'])
|
||||
|
||||
const attemptsCountText = computed(() =>
|
||||
props.exam.attemptsCount == null
|
||||
? `${(props.exam.attempts || []).length} آزمون`
|
||||
: `${props.exam.attemptsCount} آزمون`
|
||||
)
|
||||
const questionsCountText = computed(() =>
|
||||
props.exam.questionsCount == null ? '—' : `${props.exam.questionsCount}`
|
||||
)
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
<p class="student-lesson-item__title">{{ lesson.title || '—' }}</p>
|
||||
<Badge
|
||||
variant="primary"
|
||||
label="زمان یادگیری :"
|
||||
:value="hoursText"
|
||||
label="ظرفیت :"
|
||||
:value="capacityText"
|
||||
class="student-lesson-item__hours"
|
||||
>
|
||||
<template #prepend>
|
||||
<SvgIcon name="calendar" :size="11" color="#007074" />
|
||||
<SvgIcon name="users-three" :size="11" color="#007074" />
|
||||
</template>
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -75,8 +75,8 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['show-details'])
|
||||
|
||||
const hoursText = computed(() =>
|
||||
props.lesson.learningHours == null ? '—' : `${props.lesson.learningHours} ساعت`
|
||||
const capacityText = computed(() =>
|
||||
props.lesson.capacity == null ? '—' : `${props.lesson.capacity} نفر`
|
||||
)
|
||||
const sessionsText = computed(() =>
|
||||
props.lesson.sessionsCount == null ? '—' : `${props.lesson.sessionsCount} جلسه`
|
||||
|
||||
@@ -41,11 +41,11 @@
|
||||
|
||||
<div class="edit-profile__cell edit-profile__cell--third">
|
||||
<DatePickerField
|
||||
v-model="form.birthDate"
|
||||
name="birthDate"
|
||||
v-model="form.birthday"
|
||||
name="birthday"
|
||||
label="تاریخ تولد"
|
||||
:max="todayIso"
|
||||
:error="errors.birthDate"
|
||||
:error="errors.birthday"
|
||||
/>
|
||||
</div>
|
||||
<div class="edit-profile__cell edit-profile__cell--third">
|
||||
@@ -253,7 +253,7 @@ const initialPhoneNumber = ref('')
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
birthDate: '',
|
||||
birthday: '',
|
||||
nationalCode: '',
|
||||
marriageStatus: '',
|
||||
gender: '',
|
||||
@@ -298,34 +298,38 @@ const onProvinceChange = () => {
|
||||
|
||||
const { data: profile } = useGetMeQuery()
|
||||
|
||||
watch(profile, (user) => {
|
||||
if (!user) return
|
||||
const fullName =
|
||||
user.name || [user.firstName, user.lastName].filter(Boolean).join(' ').trim() || ''
|
||||
const phone = user.phone || user.phoneNumber || ''
|
||||
form.value = {
|
||||
...form.value,
|
||||
name: fullName,
|
||||
birthDate: user.profile?.birthDate || '',
|
||||
nationalCode: user.nationalCode || '',
|
||||
marriageStatus: user.profile?.marriageStatus || '',
|
||||
gender: user.profile?.gender || '',
|
||||
phone,
|
||||
provinceId: user?.province?.id || '',
|
||||
cityId: user?.city?.id || '',
|
||||
address: user?.address || '',
|
||||
avatarMediaId: user.profile?.avatarMediaId || null,
|
||||
}
|
||||
initialNationalCode.value = user.nationalCode || ''
|
||||
initialPhoneNumber.value = phone
|
||||
if (user.avatarUrl) avatar.value = { url: user.avatarUrl }
|
||||
})
|
||||
watch(
|
||||
profile,
|
||||
(user) => {
|
||||
if (!user) return
|
||||
const fullName =
|
||||
user.name || [user.firstName, user.lastName].filter(Boolean).join(' ').trim() || ''
|
||||
const phone = user.phone || user.phoneNumber || ''
|
||||
form.value = {
|
||||
...form.value,
|
||||
name: fullName,
|
||||
birthday: user?.birthday || '',
|
||||
nationalCode: user.nationalCode || '',
|
||||
marriageStatus: user?.marriageStatus || '',
|
||||
gender: user?.gender || '',
|
||||
phone,
|
||||
provinceId: user?.province?.id || '',
|
||||
cityId: user?.city?.id || '',
|
||||
address: user?.address || '',
|
||||
avatarMediaId: user?.avatarMediaId || null,
|
||||
}
|
||||
initialNationalCode.value = user.nationalCode || ''
|
||||
initialPhoneNumber.value = phone
|
||||
if (user.avatarUrl) avatar.value = { url: user.avatarUrl }
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const uploadMutation = useUploadMediaMutation()
|
||||
|
||||
const onAvatarCropped = async (file) => {
|
||||
try {
|
||||
const formData = objectToFormData({ file, subType: 'avatar', context: 'user' })
|
||||
const formData = objectToFormData({ file, purpose: 'avatar', context: 'user' })
|
||||
const response = await uploadMutation.mutateAsync(formData)
|
||||
const payload = response?.data ?? response
|
||||
avatar.value = { url: payload?.url, uploadId: payload?.uploadId, ...payload }
|
||||
@@ -344,7 +348,7 @@ const onSubmit = async () => {
|
||||
const { isValid, payload } = await validate(form.value)
|
||||
if (!isValid) return
|
||||
// Backend PATCH /me accepts: name, email, phone, avatar_media_id.
|
||||
// Other UI-only fields (province/city/address/birthDate/…) are not in
|
||||
// Other UI-only fields (province/city/address/birthday/…) are not in
|
||||
// the backend Postman doc and are dropped here. Password is handled
|
||||
// separately via POST /reset-password.
|
||||
const body = {
|
||||
@@ -471,7 +475,7 @@ const onCancel = () => router.push({ name: 'student-dashboard' })
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.625rem;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const studentProfileSchema = object().shape({
|
||||
name: string().required().min(2),
|
||||
phone: phoneNumberRule,
|
||||
nationalCode: nationalCodeRule,
|
||||
birthDate: string().nullable().notRequired(),
|
||||
birthday: string().nullable().notRequired(),
|
||||
marriageStatus: string().nullable().notRequired(),
|
||||
gender: string().nullable().notRequired(),
|
||||
provinceId: string().nullable().notRequired(),
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<p class="student-session-item__title">{{ session.title || '—' }}</p>
|
||||
<div class="student-session-item__hours">
|
||||
<SvgIcon name="calendar" :size="9" color="#007074" />
|
||||
<span class="student-session-item__hours-label">زمان جلسه :</span>
|
||||
<span class="student-session-item__hours-value">{{ durationText }}</span>
|
||||
<span class="student-session-item__hours-label">مهلت :</span>
|
||||
<span class="student-session-item__hours-value">{{ deadlineText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<Badge v-if="isComplete" variant="success">تکمیل شده</Badge>
|
||||
<Badge v-if="session.isSeen" variant="success">دیده شده</Badge>
|
||||
<Badge v-if="session.grade != null" variant="primary" label="نمره" :value="session.grade" />
|
||||
<Badge v-if="session.needsAssignment" icon="paper-plane">دارای تکلیف</Badge>
|
||||
<Badge v-if="session.hasQuiz" icon="file">دارای آزمون</Badge>
|
||||
<Badge v-if="session.homeworksCount > 0" icon="paper-plane">دارای تکلیف</Badge>
|
||||
<Badge v-if="session.examsCount > 0" icon="file">دارای آزمون</Badge>
|
||||
<Badge v-if="isOnline" icon="users-three">جلسه آنلاین</Badge>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@ import { computed } from 'vue'
|
||||
import Badge from '@/components/Badge.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||
|
||||
const props = defineProps({
|
||||
session: { type: Object, required: true },
|
||||
@@ -44,8 +45,8 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['start'])
|
||||
|
||||
const durationText = computed(() =>
|
||||
props.session.durationHours == null ? '—' : `${props.session.durationHours} ساعت`
|
||||
const deadlineText = computed(
|
||||
() => formatJalaaliDate(props.session.deadline || props.session.endDate) || '—'
|
||||
)
|
||||
|
||||
const isOnline = computed(() => props.session.type === 'online')
|
||||
|
||||
@@ -119,8 +119,6 @@ const { data: session, isLoading } = useStudentSessionQuery(sessionId, {
|
||||
enabled: () => !!sessionId.value,
|
||||
})
|
||||
|
||||
// The homework for this session comes from the dedicated endpoint (the session
|
||||
// payload itself doesn't embed homeworks).
|
||||
const homeworkFilters = computed(() => ({ sessionId: sessionId.value }))
|
||||
const homeworkPagination = ref({ page: 1, perPage: 20 })
|
||||
const { data: homeworksData } = useStudentHomeworksListQuery(homeworkFilters, homeworkPagination, {
|
||||
@@ -152,7 +150,14 @@ const homeworkId = computed(
|
||||
)
|
||||
|
||||
const uploadHomeworkFile = async (file) => {
|
||||
const fd = objectToFormData({ file, purpose: 'homework_file', context: 'homework' })
|
||||
// The backend validates allowed extensions per `purpose`: `homework_file`
|
||||
// accepts documents/images only, so a recorded voice must go up as `voice`.
|
||||
const isAudio = (file?.type || '').startsWith('audio/')
|
||||
const fd = objectToFormData({
|
||||
file,
|
||||
purpose: isAudio ? 'voice' : 'homework_file',
|
||||
context: 'homework',
|
||||
})
|
||||
const response = await uploadMediaMutation.mutateAsync(fd)
|
||||
const payload = response?.data ?? response
|
||||
return payload?.id ?? null
|
||||
|
||||
Reference in New Issue
Block a user