From 3a4ac1396d2c5b9d03cd9c9af0885789096b081e Mon Sep 17 00:00:00 2001 From: Kavehhn174 Date: Tue, 25 Aug 2026 03:48:26 +0330 Subject: [PATCH] feat(dashboard): add professor user selection, loading overlays on all edit/detail views, and SMS bypass management --- src/components/common/LoadingOverlay.vue | 103 ++++++++ src/views/classes/ClassDetailView.vue | 12 +- src/views/classes/ClassFormView.vue | 7 +- .../ContactInquiryDetailView.vue | 11 +- src/views/courses/CourseDetailView.vue | 11 +- src/views/courses/CourseFormView.vue | 8 +- src/views/payments/PaymentDetailView.vue | 12 +- .../PendingStudentDetailView.vue | 11 +- src/views/professors/ProfessorFormView.vue | 117 ++++++++- src/views/roles/RoleFormView.vue | 9 +- src/views/sessions/SessionFormView.vue | 8 +- src/views/settings/SettingsView.vue | 243 ++++++++++++++++++ src/views/users/UserDetailView.vue | 12 +- src/views/users/UserFormView.vue | 8 +- 14 files changed, 548 insertions(+), 24 deletions(-) create mode 100644 src/components/common/LoadingOverlay.vue diff --git a/src/components/common/LoadingOverlay.vue b/src/components/common/LoadingOverlay.vue new file mode 100644 index 0000000..30c8c71 --- /dev/null +++ b/src/components/common/LoadingOverlay.vue @@ -0,0 +1,103 @@ + + + + + + diff --git a/src/views/classes/ClassDetailView.vue b/src/views/classes/ClassDetailView.vue index 4d384c1..acfc311 100644 --- a/src/views/classes/ClassDetailView.vue +++ b/src/views/classes/ClassDetailView.vue @@ -1,6 +1,7 @@ - + @@ -256,6 +258,7 @@ import { useToast } from '@/composables/useToast'; import { usePermission } from '@/composables/usePermission'; import { getPayableAmount } from '@/utils/paymentAmount'; import PageHeader from '@/components/common/PageHeader.vue'; +import LoadingOverlay from '@/components/common/LoadingOverlay.vue'; import StatusTag from '@/components/common/StatusTag.vue'; import TableSkeleton from '@/components/common/TableSkeleton.vue'; import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue'; @@ -402,12 +405,17 @@ const removeDialogMessage = computed(() => { : 'آیا این دانشجو از کلاس حذف شود؟'; }); +const isFetching = ref(true); + const fetchClass = async () => { + isFetching.value = true; try { const res = await classApi.getOne(classId); classData.value = res.data || res; } catch (err) { showError(err); + } finally { + isFetching.value = false; } }; diff --git a/src/views/classes/ClassFormView.vue b/src/views/classes/ClassFormView.vue index 3725f60..d66d2d2 100644 --- a/src/views/classes/ClassFormView.vue +++ b/src/views/classes/ClassFormView.vue @@ -24,7 +24,8 @@ -
+
+
@@ -347,6 +348,7 @@ import { usePersianDate } from '@/composables/usePersianDate'; import { useToast } from '@/composables/useToast'; import { usePermission } from '@/composables/usePermission'; import PageHeader from '@/components/common/PageHeader.vue'; +import LoadingOverlay from '@/components/common/LoadingOverlay.vue'; import AdminNotesField from '@/components/common/AdminNotesField.vue'; import ClassSessionsSection from '@/components/classes/ClassSessionsSection.vue'; import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue'; @@ -489,6 +491,7 @@ const applyCourseDefaults = (course) => { }; const fetchData = async () => { + isFetching.value = true; try { const [cResult, pResult, uResult] = await Promise.allSettled([ courseApi.getAll({ limit: 100 }), @@ -566,6 +569,8 @@ const fetchData = async () => { } } catch (err) { showError(err); + } finally { + isFetching.value = false; } }; diff --git a/src/views/contactInquiries/ContactInquiryDetailView.vue b/src/views/contactInquiries/ContactInquiryDetailView.vue index 4f19691..ac4ad2c 100644 --- a/src/views/contactInquiries/ContactInquiryDetailView.vue +++ b/src/views/contactInquiries/ContactInquiryDetailView.vue @@ -1,6 +1,7 @@ - @@ -102,6 +104,7 @@ import { contactInquiryApi } from '@/api/contactInquiryApi'; import { usePersianDate } from '@/composables/usePersianDate'; import { useToast } from '@/composables/useToast'; import PageHeader from '@/components/common/PageHeader.vue'; +import LoadingOverlay from '@/components/common/LoadingOverlay.vue'; import Button from 'primevue/button'; import Dropdown from 'primevue/select'; import Textarea from 'primevue/textarea'; @@ -111,6 +114,7 @@ const route = useRoute(); const { toPersianDigits, formatJalali } = usePersianDate(); const { showSuccess, showError } = useToast(); +const isFetching = ref(true); const inquiry = ref(null); const saving = ref(false); const form = reactive({ status: 'new', notes: '' }); @@ -136,6 +140,7 @@ const METHOD_LABELS = { const methodLabel = (value) => METHOD_LABELS[value] || value; const load = async () => { + isFetching.value = true; try { const res = await contactInquiryApi.getOne(route.params.id); const data = res.data || res; @@ -150,6 +155,8 @@ const load = async () => { } } catch (err) { showError(err); + } finally { + isFetching.value = false; } }; diff --git a/src/views/courses/CourseDetailView.vue b/src/views/courses/CourseDetailView.vue index 7ffb435..c871e2d 100644 --- a/src/views/courses/CourseDetailView.vue +++ b/src/views/courses/CourseDetailView.vue @@ -1,6 +1,7 @@ - @@ -67,6 +69,7 @@ import { courseApi } from '@/api/courseApi'; import { sessionApi } from '@/api/sessionApi'; import { usePersianDate } from '@/composables/usePersianDate'; import PageHeader from '@/components/common/PageHeader.vue'; +import LoadingOverlay from '@/components/common/LoadingOverlay.vue'; import PermissionGate from '@/components/common/PermissionGate.vue'; import StatusTag from '@/components/common/StatusTag.vue'; import Button from 'primevue/button'; @@ -78,10 +81,12 @@ const route = useRoute(); const courseId = route.params.id; const { toPersianDigits, formatJalali } = usePersianDate(); +const isFetching = ref(true); const course = ref(null); const courseSessions = ref([]); const fetchDetail = async () => { + isFetching.value = true; try { const res = await courseApi.getOne(courseId); course.value = res.data || res; @@ -99,6 +104,8 @@ const fetchDetail = async () => { courseSessions.value = Array.isArray(list) ? list : []; } catch (e) { console.warn('Fetch course detail error:', e); + } finally { + isFetching.value = false; } }; diff --git a/src/views/courses/CourseFormView.vue b/src/views/courses/CourseFormView.vue index e9fb103..05f9f21 100644 --- a/src/views/courses/CourseFormView.vue +++ b/src/views/courses/CourseFormView.vue @@ -9,7 +9,8 @@ -
+
+

مشخصات اصلی دوره

@@ -157,6 +158,7 @@ import { usePersianDate } from '@/composables/usePersianDate'; import { useToast } from '@/composables/useToast'; import PageHeader from '@/components/common/PageHeader.vue'; +import LoadingOverlay from '@/components/common/LoadingOverlay.vue'; import StatusTag from '@/components/common/StatusTag.vue'; import TableSkeleton from '@/components/common/TableSkeleton.vue'; import InputText from 'primevue/inputtext'; @@ -178,6 +180,7 @@ const { showSuccess, showError } = useToast(); const courseId = route.params.id; const isEditMode = computed(() => !!courseId); +const isFetching = ref(false); const isSubmitting = ref(false); const loadingClasses = ref(false); const courseClasses = ref([]); @@ -219,6 +222,7 @@ const fetchClassesForCourse = async () => { const fetchCourse = async () => { if (!courseId) return; + isFetching.value = true; try { const res = await courseApi.getOne(courseId); const data = res.data || res; @@ -233,6 +237,8 @@ const fetchCourse = async () => { highlightsText.value = Array.isArray(data.highlights) ? data.highlights.join('\n') : ''; } catch (err) { showError(err); + } finally { + isFetching.value = false; } }; diff --git a/src/views/payments/PaymentDetailView.vue b/src/views/payments/PaymentDetailView.vue index 6a19a0b..12983e0 100644 --- a/src/views/payments/PaymentDetailView.vue +++ b/src/views/payments/PaymentDetailView.vue @@ -1,6 +1,7 @@ - +
@@ -269,6 +271,7 @@ import { usePersianDate } from '@/composables/usePersianDate'; import { useToast } from '@/composables/useToast'; import { getPayableAmount } from '@/utils/paymentAmount'; import PageHeader from '@/components/common/PageHeader.vue'; +import LoadingOverlay from '@/components/common/LoadingOverlay.vue'; import PermissionGate from '@/components/common/PermissionGate.vue'; import StatusTag from '@/components/common/StatusTag.vue'; import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue'; @@ -352,14 +355,17 @@ const transactionStatus = (trx) => { return trx.status || (trx.date ? 'paid' : 'pending'); }; -const transactionRowClass = (trx) => (isTransactionCancelled(trx) ? 'transaction-row-cancelled' : ''); +const isFetching = ref(true); const fetchPayment = async () => { + isFetching.value = true; try { const res = await paymentApi.getOne(paymentId); payment.value = res.data || res; } catch (err) { showError(err); + } finally { + isFetching.value = false; } }; diff --git a/src/views/pendingStudents/PendingStudentDetailView.vue b/src/views/pendingStudents/PendingStudentDetailView.vue index ead41a4..cb447a6 100644 --- a/src/views/pendingStudents/PendingStudentDetailView.vue +++ b/src/views/pendingStudents/PendingStudentDetailView.vue @@ -1,6 +1,7 @@ - @@ -127,6 +129,7 @@ import { pendingStudentApi } from '@/api/pendingStudentApi'; import { usePersianDate } from '@/composables/usePersianDate'; import { useToast } from '@/composables/useToast'; import PageHeader from '@/components/common/PageHeader.vue'; +import LoadingOverlay from '@/components/common/LoadingOverlay.vue'; import StatusTag from '@/components/common/StatusTag.vue'; import Button from 'primevue/button'; import Textarea from 'primevue/textarea'; @@ -136,6 +139,7 @@ const router = useRouter(); const { formatJalali } = usePersianDate(); const { showSuccess, showError } = useToast(); +const isFetching = ref(true); const pending = ref(null); const adminNotes = ref(''); const approving = ref(false); @@ -153,6 +157,7 @@ const formatPrice = (amount) => { }; async function load() { + isFetching.value = true; try { const res = await pendingStudentApi.getOne(route.params.id); pending.value = res.data?.data || res.data; @@ -160,6 +165,8 @@ async function load() { } catch (error) { showError(error?.response?.data?.error?.message || 'بارگذاری ناموفق بود'); router.push('/pending-students'); + } finally { + isFetching.value = false; } } diff --git a/src/views/professors/ProfessorFormView.vue b/src/views/professors/ProfessorFormView.vue index fe019c4..968fcfe 100644 --- a/src/views/professors/ProfessorFormView.vue +++ b/src/views/professors/ProfessorFormView.vue @@ -3,16 +3,54 @@