From e700609236d82be08056ef3e9e7f5cb726192e78 Mon Sep 17 00:00:00 2001 From: sajjadtalkhabi <97734061+sajjadtalkhabi@users.noreply.github.com> Date: Sat, 27 Jun 2026 20:07:58 +0330 Subject: [PATCH] fix --- src/components/form/VoiceRecorder.vue | 28 +++++- .../modals/AssignmentDetailsModal.vue | 4 - .../admin/exams/pages/ExamFormPage.vue | 4 +- .../admin/messages/pages/TicketsListPage.vue | 3 +- .../components/modals/SessionDetailsModal.vue | 28 ++++-- .../admin/sessions/pages/SessionFormPage.vue | 5 +- .../modals/UserVerificationDetailsModal.vue | 95 ++++++++++--------- .../auth/components/VerifyCodeForm.vue | 18 ++-- .../studentRegister/MediaRecorder.vue | 33 ++++++- .../studentRegister/PersonalInformation.vue | 58 ++++++----- src/features/auth/pages/SignupPage.vue | 2 +- src/features/auth/schema/index.js | 2 +- src/features/auth/schema/student-register.js | 5 +- .../auth/store/student-registration.js | 6 +- .../exams/components/StudentExamSummary.vue | 11 --- .../lessons/components/StudentLessonItem.vue | 10 +- .../student/profile/pages/EditProfilePage.vue | 62 ++++++------ src/features/student/profile/schema/index.js | 2 +- .../components/StudentSessionItem.vue | 13 +-- .../pages/StudentSessionDetailsPage.vue | 11 ++- src/services/api/endpoints.js | 2 +- src/services/query/admin-tickets.js | 11 +-- 22 files changed, 236 insertions(+), 177 deletions(-) diff --git a/src/components/form/VoiceRecorder.vue b/src/components/form/VoiceRecorder.vue index 852c807..7b0cf2d 100644 --- a/src/components/form/VoiceRecorder.vue +++ b/src/components/form/VoiceRecorder.vue @@ -121,6 +121,23 @@ let mediaStream = null let chunks = [] let timerHandle = null let timerStart = 0 +let recorderType = null + +// The backend only accepts these audio extensions. Browsers can't record mp3/wav, +// but most can record into an mp4 container (→ .m4a), so prefer a supported type +// whose extension the backend allows; fall back to webm only as a last resort. +const ACCEPTED_AUDIO_TYPES = [ + { mime: 'audio/mp4', ext: 'm4a' }, + { mime: 'audio/mpeg', ext: 'mp3' }, + { mime: 'audio/wav', ext: 'wav' }, +] + +const pickRecorderType = () => { + const supported = ACCEPTED_AUDIO_TYPES.find((c) => + window.MediaRecorder?.isTypeSupported?.(c.mime) + ) + return supported || { mime: 'audio/webm', ext: 'webm' } +} const formattedTime = computed(() => { const s = Math.floor(elapsedMs.value / 1000) @@ -163,16 +180,19 @@ const startRecording = async () => { try { mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }) chunks = [] - mediaRecorder = new window.MediaRecorder(mediaStream) + recorderType = pickRecorderType() + mediaRecorder = new window.MediaRecorder(mediaStream, { mimeType: recorderType.mime }) mediaRecorder.ondataavailable = (e) => { if (e.data?.size > 0) chunks.push(e.data) } mediaRecorder.onstop = () => { isRecording.value = false stopTracks() - const blob = new Blob(chunks, { type: 'audio/webm' }) - const file = new File([blob], `recording-${Date.now()}.webm`, { - type: 'audio/webm', + const mime = recorderType?.mime || mediaRecorder?.mimeType || 'audio/webm' + const ext = recorderType?.ext || 'webm' + const blob = new Blob(chunks, { type: mime }) + const file = new File([blob], `recording-${Date.now()}.${ext}`, { + type: mime, lastModified: Date.now(), }) setRecording(file) diff --git a/src/features/admin/assignments/components/modals/AssignmentDetailsModal.vue b/src/features/admin/assignments/components/modals/AssignmentDetailsModal.vue index 0c071e7..bfb4a3f 100644 --- a/src/features/admin/assignments/components/modals/AssignmentDetailsModal.vue +++ b/src/features/admin/assignments/components/modals/AssignmentDetailsModal.vue @@ -21,10 +21,6 @@
- diff --git a/src/features/admin/exams/pages/ExamFormPage.vue b/src/features/admin/exams/pages/ExamFormPage.vue index 7835968..b243e4a 100644 --- a/src/features/admin/exams/pages/ExamFormPage.vue +++ b/src/features/admin/exams/pages/ExamFormPage.vue @@ -198,7 +198,6 @@ const normalizeExistingQuestions = (raw = []) => { optionText: o.optionText || '', isCorrect: !!o.isCorrect, })), - // No `__local` flag — these came from the server, so the builder will lock them. } }) } @@ -280,7 +279,6 @@ const submitting = computed( const postQuestionsSequentially = async (id, list) => { for (const payload of list) { - // Sequential so question position ordering is preserved on the backend. // eslint-disable-next-line no-await-in-loop await addQuestionMutation.mutateAsync({ examId: id, payload }) } @@ -302,7 +300,7 @@ const onSubmit = async () => { if (targetExamId && newQuestions.length > 0) { await postQuestionsSequentially(targetExamId, newQuestions) } - await queryClient.invalidateQueries({ queryKey: adminExamsKeys.all }) + await queryClient.invalidateQueries({ queryKey: adminExamsKeys.all, refetchType: 'all' }) router.push({ name: 'admin-exams' }) } diff --git a/src/features/admin/messages/pages/TicketsListPage.vue b/src/features/admin/messages/pages/TicketsListPage.vue index a00906e..9f67dfc 100644 --- a/src/features/admin/messages/pages/TicketsListPage.vue +++ b/src/features/admin/messages/pages/TicketsListPage.vue @@ -59,7 +59,8 @@ const { data, isLoading } = useAdminTicketsListQuery(filters, pagination, { keepPreviousData: true, }) -const tickets = computed(() => data.value?.data ?? []) +const tickets = computed(() => data.value?.items ?? []) + const paginationMeta = computed(() => ({ page: pagination.value.page, perPage: pagination.value.perPage, diff --git a/src/features/admin/sessions/components/modals/SessionDetailsModal.vue b/src/features/admin/sessions/components/modals/SessionDetailsModal.vue index 9da2db8..15db997 100644 --- a/src/features/admin/sessions/components/modals/SessionDetailsModal.vue +++ b/src/features/admin/sessions/components/modals/SessionDetailsModal.vue @@ -39,11 +39,9 @@ :src="mediaUrl" :video-id="session.id" /> - +
+ +
session.value?.course?.title || '—') const isOnline = computed(() => session.value?.type === 'online') const collectionToContentType = (collectionName) => { - if (collectionName === 'videos') return 'video' + if (collectionName === 'video') return 'video' if (collectionName === 'voice') return 'voice' - if (collectionName === 'pdfs') return 'text' + if (collectionName === 'pdf') return 'text' return '' } @@ -173,6 +171,22 @@ const mediaFileName = computed(() => contentMedia.value?.fileName || 'فایل justify-content: center; } + &__voice { + width: 100%; + + /* stylelint-disable-next-line selector-class-pattern */ + :deep(.voice-recorder) { + min-height: auto; + padding: 0.75rem; + } + + /* stylelint-disable-next-line selector-class-pattern */ + :deep(.voice-recorder__main-btn) { + width: 4rem; + height: 4rem; + } + } + &__file { display: inline-flex; align-items: center; diff --git a/src/features/admin/sessions/pages/SessionFormPage.vue b/src/features/admin/sessions/pages/SessionFormPage.vue index e538e90..2e48417 100644 --- a/src/features/admin/sessions/pages/SessionFormPage.vue +++ b/src/features/admin/sessions/pages/SessionFormPage.vue @@ -357,7 +357,10 @@ const onSubmit = async () => { } else { await addMutation.mutateAsync(payload) } - await queryClient.invalidateQueries({ queryKey: adminSessionsKeys.all }) + // The list query is inactive while we're on the form page, and the global + // `refetchOnMount: false` means navigating back won't refetch a merely-stale + // query — so force a refetch of all matching queries, active or not. + await queryClient.invalidateQueries({ queryKey: adminSessionsKeys.all, refetchType: 'all' }) router.push({ name: 'admin-sessions' }) } diff --git a/src/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue b/src/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue index 7f31a98..a3e144e 100644 --- a/src/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue +++ b/src/features/admin/users/components/verifications/modals/UserVerificationDetailsModal.vue @@ -17,19 +17,19 @@ - - + + - - - + + +
- +
@@ -39,15 +39,15 @@
@@ -56,30 +56,12 @@ :desc="profile.fieldOfStudy || ''" />
-
- -
-
- -
-
- -
-
@@ -214,6 +198,7 @@ import useModal from '@/composables/useModal' import BasicModal from '@/components/BasicModal.vue' import { formatJalaaliDate } from '@/utils/date-utils' import LineTitleBlock from '@/components/LineTitleBlock.vue' +import VoiceRecorder from '@/components/form/VoiceRecorder.vue' import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue' import { useAdminUserQuery, useAdminUserRegisterDataQuery } from '@/services/query/admin-users' import { @@ -227,7 +212,6 @@ import { SEMINARY_LEVEL, SESSION_CANCELLATION_RESPONSE, UNIVERSITY_LEVEL, - VERIFICATION_MEDIA_TYPE, } from '@/enums' defineOptions({ name: 'UserVerificationDetailsModal' }) @@ -248,15 +232,23 @@ const { data: registerData } = useAdminUserRegisterDataQuery(userIdRef, { const user = computed(() => modalData.value.user || fetched.value || null) -// The register-data endpoint returns the answers the user gave during the -// registration steps — the same fields the sections below render. Merge it over -// the user's stored profile so reviewing admins see what was actually submitted. const profile = computed(() => ({ ...user.value?.profile, ...registerData.value })) -const fullName = computed( - () => - user.value?.name || `${user.value?.firstName || ''} ${user.value?.lastName || ''}`.trim() || '—' -) +const fullName = computed(() => user.value?.name || '—') +const personal = computed(() => { + const u = user.value || {} + const p = u.profile || {} + return { + birthDate: u.birthday || p.birthDate || '', + phone: u.phone || u.phoneNumber || '', + nationalCode: u.nationalCode || '', + maritalStatus: u.marriageStatus || p.maritalStatus || '', + gender: u.gender || p.gender || '', + address: typeof u.address === 'string' ? u.address : u.address?.address || '', + province: u.province?.name || u.address?.province?.name || '', + city: u.city?.name || u.address?.city?.name || '', + } +}) const platforms = computed(() => Array.isArray(profile.value.propagationPlatforms) ? profile.value.propagationPlatforms : [] @@ -276,13 +268,10 @@ const onlinePlatformDetails = computed( () => platforms.value.find((p) => p.platform === 'online')?.platformDetails || '' ) -const verificationMedia = computed(() => user.value?.verification?.media || []) -const faithProduction = computed(() => - verificationMedia.value.find((m) => m.type === VERIFICATION_MEDIA_TYPE.FAITH_PRODUCTION) -) -const leaderMessage = computed(() => - verificationMedia.value.find((m) => m.type === VERIFICATION_MEDIA_TYPE.LEADER_MESSAGE) -) +// Skill parts 6 & 7 store the uploaded media objects in register-data under +// `faithProductionAudio` / `leaderMessageVideo` (each carries a `url`). +const faithProduction = computed(() => profile.value.faithProductionAudio || null) +const leaderMessage = computed(() => profile.value.leaderMessageVideo || null)