Compare commits
3
Commits
a2a7e756ad
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4ac1396d | ||
|
|
0201089aee | ||
|
|
e9f1f534d5 |
+2
-2
@@ -2,9 +2,9 @@ FROM node:24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY package*.json .npmrc* ./
|
||||
|
||||
RUN npm install
|
||||
RUN npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<!-- /src/components/common/LoadingOverlay.vue -->
|
||||
<template>
|
||||
<Transition name="fade-overlay">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="loading-overlay-container flex flex-column align-items-center justify-content-center"
|
||||
:class="{ 'fixed-overlay': fullscreen }"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="loading-card surface-card p-4 border-round-xl border-1 border-color shadow-3 flex flex-column align-items-center gap-3">
|
||||
<ProgressSpinner
|
||||
style="width: 44px; height: 44px"
|
||||
strokeWidth="4"
|
||||
fill="transparent"
|
||||
animationDuration=".8s"
|
||||
aria-label="در حال بارگذاری"
|
||||
/>
|
||||
<div class="flex flex-column align-items-center gap-1 text-center">
|
||||
<span class="font-bold text-sm text-color">{{ message }}</span>
|
||||
<span v-if="subMessage" class="text-xs text-muted">{{ subMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: 'در حال دریافت اطلاعات...'
|
||||
},
|
||||
subMessage: {
|
||||
type: String,
|
||||
default: 'لطفاً چند لحظه صبر کنید'
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-overlay-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
z-index: 50;
|
||||
border-radius: inherit;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
:root[class*='dark'] .loading-overlay-container,
|
||||
.dark-theme .loading-overlay-container {
|
||||
background: rgba(18, 24, 38, 0.82);
|
||||
}
|
||||
|
||||
.fixed-overlay {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-card {
|
||||
min-width: 220px;
|
||||
background: var(--surface-card);
|
||||
animation: pulse-card 1.8s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse-card {
|
||||
0% {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-overlay-enter-active,
|
||||
.fade-overlay-leave-active {
|
||||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||
}
|
||||
|
||||
.fade-overlay-enter-from,
|
||||
.fade-overlay-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- /src/views/classes/ClassDetailView.vue -->
|
||||
<template>
|
||||
<div class="class-detail-view" v-if="classData">
|
||||
<div class="class-detail-view relative">
|
||||
<LoadingOverlay :loading="isFetching || !classData" message="در حال دریافت مشخصات کلاس..." />
|
||||
<div v-if="classData">
|
||||
<PageHeader :title="classData.name" :subtitle="`دوره: ${classData.course?.title || '-'}`">
|
||||
<PermissionGate permission="financial_reports:read">
|
||||
<Button
|
||||
@@ -241,6 +242,7 @@
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm mb-4">
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm mb-4 relative overflow-hidden">
|
||||
<LoadingOverlay :loading="isFetching" message="در حال دریافت اطلاعات کلاس..." />
|
||||
<form @submit.prevent="handleSubmit" class="grid">
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">نام کلاس *</label>
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- /src/views/contactInquiries/ContactInquiryDetailView.vue -->
|
||||
<template>
|
||||
<div class="contact-inquiry-detail" v-if="inquiry">
|
||||
<div class="contact-inquiry-detail relative">
|
||||
<LoadingOverlay :loading="isFetching || !inquiry" message="در حال بارگذاری جزئیات درخواست تماس..." />
|
||||
<div v-if="inquiry">
|
||||
<PageHeader
|
||||
:title="`${inquiry.name} ${inquiry.surname}`"
|
||||
subtitle="جزئیات و پیگیری درخواست تماس"
|
||||
@@ -92,6 +93,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- /src/views/courses/CourseDetailView.vue -->
|
||||
<template>
|
||||
<div class="course-detail-view" v-if="course">
|
||||
<div class="course-detail-view relative">
|
||||
<LoadingOverlay :loading="isFetching || !course" message="در حال دریافت مشخصات دوره..." />
|
||||
<div v-if="course">
|
||||
<PageHeader :title="course.title" :subtitle="course.type === 'Private' ? 'دوره خصوصی' : 'دوره عمومی'">
|
||||
<PermissionGate permission="courses:update">
|
||||
<Button :label="$t('app.edit')" icon="pi pi-pencil" severity="warning" @click="$router.push(`/courses/edit/${course._id || course.id}`)" />
|
||||
@@ -57,6 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
</PageHeader>
|
||||
|
||||
<!-- Course Details -->
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm mb-4">
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm mb-4 relative overflow-hidden">
|
||||
<LoadingOverlay :loading="isFetching" message="در حال دریافت اطلاعات دوره..." />
|
||||
<h2 class="text-lg font-bold text-color mb-4 pb-2 border-bottom-1 border-color">مشخصات اصلی دوره</h2>
|
||||
<form @submit.prevent="handleSubmitCourse" class="grid">
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- /src/views/payments/PaymentDetailView.vue -->
|
||||
<template>
|
||||
<div class="payment-detail-view" v-if="payment">
|
||||
<div class="payment-detail-view relative">
|
||||
<LoadingOverlay :loading="isFetching || !payment" message="در حال دریافت اطلاعات صورتحساب..." />
|
||||
<div v-if="payment">
|
||||
<PageHeader :title="$t('payments.paymentDetail')" :subtitle="`کد صورتحساب: ${payment._id || payment.id}`">
|
||||
<Button label="بازگشت" icon="pi pi-arrow-left" text severity="secondary" @click="$router.push('/payments')" />
|
||||
</PageHeader>
|
||||
@@ -258,6 +259,7 @@
|
||||
<Button label="لغو تراکنش" icon="pi pi-ban" severity="danger" :loading="isSubmittingCancel" @click="handleCancelTransaction" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- /src/views/pendingStudents/PendingStudentDetailView.vue -->
|
||||
<template>
|
||||
<div class="pending-student-detail" v-if="pending">
|
||||
<div class="pending-student-detail relative">
|
||||
<LoadingOverlay :loading="isFetching || !pending" message="در حال دریافت اطلاعات ثبتنام در انتظار..." />
|
||||
<div v-if="pending">
|
||||
<PageHeader
|
||||
:title="pending.user?.name || 'ثبتنام در انتظار'"
|
||||
subtitle="بررسی پرداخت، تأیید ثبتنام و افزودن به کلاس"
|
||||
@@ -117,6 +118,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,54 @@
|
||||
<div class="professor-form-view w-full max-w-4xl mx-auto">
|
||||
<PageHeader
|
||||
:title="isEditMode ? $t('professors.editProfessor') : $t('professors.addProfessor')"
|
||||
:subtitle="isEditMode ? 'ویرایش رزومه و تخصصهای استاد' : 'ثبت نام و اطلاعات استاد جدید'"
|
||||
:subtitle="isEditMode ? 'ویرایش رزومه و مشخصات استاد' : 'ثبت مشخصات استاد جدید یا ارتقای کاربر به استاد'"
|
||||
>
|
||||
<Button label="انصراف" text severity="secondary" @click="$router.push('/professors')" />
|
||||
</PageHeader>
|
||||
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm">
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm relative overflow-hidden">
|
||||
<!-- Loading Overlay -->
|
||||
<LoadingOverlay :loading="isFetching" message="در حال دریافت اطلاعات استاد..." />
|
||||
|
||||
<!-- Mode selection when creating -->
|
||||
<div v-if="!isEditMode" class="mb-4 pb-3 border-bottom-1 border-color">
|
||||
<label class="font-bold text-sm text-color block mb-2">روش افزودن استاد</label>
|
||||
<SelectButton
|
||||
v-model="creationMode"
|
||||
:options="creationModeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:allowEmpty="false"
|
||||
class="w-full sm:w-auto text-sm"
|
||||
/>
|
||||
|
||||
<div v-if="creationMode === 'existing'" class="surface-ground p-3 border-round-lg border-1 border-color mt-3">
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">انتخاب کاربر از لیست کاربران سامانه *</label>
|
||||
<Select
|
||||
v-model="selectedUserId"
|
||||
:options="userOptions"
|
||||
optionLabel="displayName"
|
||||
optionValue="_id"
|
||||
placeholder="جستجو و انتخاب کاربر (نام، کدملی یا شماره همراه)..."
|
||||
filter
|
||||
filterPlaceholder="جستجو..."
|
||||
class="w-full text-sm"
|
||||
:loading="isLoadingUsers"
|
||||
@change="onUserSelected"
|
||||
/>
|
||||
<small class="text-xs text-primary flex align-items-center gap-1 mt-1">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
با انتخاب کاربر، اطلاعات وی به صورت خودکار بارگذاری شده و نقش او به «استاد» ارتقا مییابد.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="grid">
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">{{ $t('users.name') }} *</label>
|
||||
<InputText v-model.trim="form.name" class="w-full text-sm" />
|
||||
<InputText v-model.trim="form.name" class="w-full text-sm" :disabled="creationMode === 'existing' && !form.name" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
@@ -47,12 +85,17 @@
|
||||
|
||||
<div class="col-12 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">{{ $t('professors.bio') }}</label>
|
||||
<Textarea v-model="form.bio" rows="3" class="w-full text-sm" />
|
||||
<Textarea v-model="form.bio" rows="3" class="w-full text-sm" placeholder="سوابق، رزومه و یادداشتهای مربوط به استاد..." />
|
||||
</div>
|
||||
|
||||
<div class="col-12 flex justify-content-end gap-2 mt-4 pt-3 border-top-1 border-color">
|
||||
<Button label="انصراف" text severity="secondary" @click="$router.push('/professors')" />
|
||||
<Button type="submit" :label="$t('app.save')" icon="pi pi-check" :loading="isSubmitting" />
|
||||
<Button
|
||||
type="submit"
|
||||
:label="isEditMode ? $t('app.save') : (creationMode === 'existing' ? 'ارتقا به استاد و ذخیره' : 'ایجاد استاد')"
|
||||
icon="pi pi-check"
|
||||
:loading="isSubmitting"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -63,11 +106,15 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { professorApi } from '@/api/professorApi';
|
||||
import { userApi } from '@/api/userApi';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import Button from 'primevue/button';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import Select from 'primevue/select';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -75,8 +122,19 @@ const { showSuccess, showError } = useToast();
|
||||
|
||||
const professorId = route.params.id;
|
||||
const isEditMode = computed(() => !!professorId);
|
||||
const isFetching = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const creationMode = ref('existing');
|
||||
const creationModeOptions = [
|
||||
{ label: 'انتخاب از کاربران موجود سامانه', value: 'existing' },
|
||||
{ label: 'ثبت کاربر و استاد جدید', value: 'new' }
|
||||
];
|
||||
|
||||
const selectedUserId = ref(null);
|
||||
const userOptions = ref([]);
|
||||
const isLoadingUsers = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
surname: '',
|
||||
@@ -88,8 +146,42 @@ const form = reactive({
|
||||
bio: ''
|
||||
});
|
||||
|
||||
const loadUsers = async () => {
|
||||
if (isEditMode.value) return;
|
||||
isLoadingUsers.value = true;
|
||||
try {
|
||||
const res = await userApi.getAll({ limit: 300 });
|
||||
const items = res.data?.items || res.data || [];
|
||||
userOptions.value = items.map((u) => ({
|
||||
_id: u._id,
|
||||
displayName: `${u.name || 'بدون نام'} | ${u.phoneNumber || u.phone || '-'} | کد: ${u.uniqueCode || u.nationalIdCode || '-'}`,
|
||||
raw: u
|
||||
}));
|
||||
} catch (err) {
|
||||
console.warn('Failed to load users list:', err);
|
||||
} finally {
|
||||
isLoadingUsers.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onUserSelected = () => {
|
||||
const found = userOptions.value.find((u) => u._id === selectedUserId.value);
|
||||
if (found && found.raw) {
|
||||
const u = found.raw;
|
||||
const parts = String(u.name || '').trim().split(/\s+/);
|
||||
form.name = parts[0] || u.name || '';
|
||||
form.surname = parts.length > 1 ? parts.slice(1).join(' ') : (parts[0] || '');
|
||||
form.nationalIdCode = u.nationalIdCode || u.nationalId || '';
|
||||
form.phoneNumber = u.phoneNumber || u.phone || '';
|
||||
form.email = u.email || '';
|
||||
form.cardNumber = u.cardNumber || '';
|
||||
form.shabaNumber = u.shabaNumber || u.iban || '';
|
||||
}
|
||||
};
|
||||
|
||||
const fetchProfessor = async () => {
|
||||
if (!professorId) return;
|
||||
isFetching.value = true;
|
||||
try {
|
||||
const res = await professorApi.getOne(professorId);
|
||||
const data = res.data || res;
|
||||
@@ -105,6 +197,8 @@ const fetchProfessor = async () => {
|
||||
});
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,9 +223,16 @@ const handleSubmit = async () => {
|
||||
shabaNumber: form.shabaNumber || undefined,
|
||||
bio: form.bio || undefined
|
||||
};
|
||||
|
||||
if (isEditMode.value) {
|
||||
await professorApi.update(professorId, payload);
|
||||
showSuccess('اطلاعات استاد با موفقیت ویرایش شد');
|
||||
} else if (creationMode.value === 'existing' && selectedUserId.value) {
|
||||
await professorApi.createFromUser({
|
||||
userId: selectedUserId.value,
|
||||
...payload
|
||||
});
|
||||
showSuccess('کاربر با موفقیت به نقش استاد ارتقا یافت و پروفایل استادی ایجاد شد');
|
||||
} else {
|
||||
await professorApi.create(payload);
|
||||
showSuccess('استاد جدید با موفقیت ایجاد شد');
|
||||
@@ -145,6 +246,10 @@ const handleSubmit = async () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchProfessor();
|
||||
if (isEditMode.value) {
|
||||
fetchProfessor();
|
||||
} else {
|
||||
loadUsers();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<Button label="انصراف" text severity="secondary" @click="$router.push('/roles')" />
|
||||
</PageHeader>
|
||||
|
||||
<div class="grid">
|
||||
<div class="grid relative">
|
||||
<!-- Loading Overlay -->
|
||||
<LoadingOverlay :loading="isFetching" message="در حال دریافت اطلاعات نقش..." />
|
||||
<!-- Basic Info -->
|
||||
<div class="col-12">
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm mb-4">
|
||||
@@ -149,6 +151,7 @@ import { roleApi } from '@/api/roleApi';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { PERMISSION_GROUPS, ALL_PERMISSIONS } from '@/constants/permissions';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import Badge from 'primevue/badge';
|
||||
@@ -160,6 +163,7 @@ const { showSuccess, showError } = useToast();
|
||||
|
||||
const roleId = route.params.id;
|
||||
const isEditMode = computed(() => !!roleId);
|
||||
const isFetching = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const isSystemRole = ref(false);
|
||||
|
||||
@@ -227,6 +231,7 @@ const togglePermission = (key) => {
|
||||
// ── API ───────────────────────────────────────────────────────────────────────
|
||||
const fetchRole = async () => {
|
||||
if (!roleId) return;
|
||||
isFetching.value = true;
|
||||
try {
|
||||
const res = await roleApi.getOne(roleId);
|
||||
const data = res.data || res;
|
||||
@@ -238,6 +243,8 @@ const fetchRole = async () => {
|
||||
});
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
<Button label="انصراف" text severity="secondary" @click="$router.push('/sessions')" />
|
||||
</PageHeader>
|
||||
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm">
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm relative overflow-hidden">
|
||||
<LoadingOverlay :loading="isFetching" message="در حال دریافت اطلاعات جلسه..." />
|
||||
<form @submit.prevent="handleSubmit" class="grid">
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">انتخاب دوره *</label>
|
||||
@@ -102,6 +103,7 @@ import { professorApi } from '@/api/professorApi';
|
||||
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 AdminNotesField from '@/components/common/AdminNotesField.vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Textarea from 'primevue/textarea';
|
||||
@@ -117,6 +119,7 @@ const { toLatinDigits, toJalaliPickerValue, toGregorianIso, getTodayJalali } = u
|
||||
const sessionId = route.params.id;
|
||||
const isEditMode = computed(() => !!sessionId);
|
||||
|
||||
const isFetching = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const courses = ref([]);
|
||||
const classes = ref([]);
|
||||
@@ -163,6 +166,7 @@ const onCourseChange = async () => {
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
isFetching.value = true;
|
||||
try {
|
||||
const [cResult, pResult] = await Promise.allSettled([
|
||||
courseApi.getAll({ limit: 100 }),
|
||||
@@ -205,6 +209,8 @@ const fetchData = async () => {
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -81,6 +81,88 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SMS Bypass Numbers Section ──────────────────────────────── -->
|
||||
<div class="mt-5 pt-4 border-top-1 border-color">
|
||||
<div class="flex flex-column sm:flex-row sm:align-items-center justify-content-between gap-3 mb-3">
|
||||
<div class="flex align-items-start gap-3">
|
||||
<div class="w-3rem h-3rem border-round-xl flex align-items-center justify-content-center flex-shrink-0 bg-orange-100 text-orange-600">
|
||||
<i class="pi pi-shield text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<h2 class="text-lg font-bold text-color m-0">شمارههای مجاز (Bypass سوییچ خاموش پیامک)</h2>
|
||||
<Tag :value="`${toPersianDigits(smsBypassNumbers.length)} شماره`" severity="info" class="text-xs" />
|
||||
</div>
|
||||
<p class="text-xs text-muted m-0 mt-1 line-height-3">
|
||||
پیامکها به این شمارهها حتی در صورت خاموش بودن ارسال پیامک در داشبورد یا غیرفعال بودن سوییچهای اطلاعرسانی ارسال میشوند (مناسب تست، مدیریت و مانیتورینگ).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label="افزودن شماره مجاز"
|
||||
icon="pi pi-plus"
|
||||
size="small"
|
||||
severity="success"
|
||||
@click="openAddBypassDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!smsBypassNumbers.length" class="p-4 border-round-lg surface-ground border-1 border-color text-center text-muted text-sm">
|
||||
<i class="pi pi-info-circle ml-1"></i>
|
||||
هیچ شماره مجازی تعریف نشده است. برای افزودن شماره دکمه «افزودن شماره مجاز» را بزنید.
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-column gap-2">
|
||||
<div
|
||||
v-for="(item, index) in smsBypassNumbers"
|
||||
:key="item._id || index"
|
||||
class="p-3 border-round-lg surface-ground border-1 border-color flex flex-column sm:flex-row sm:align-items-center justify-content-between gap-3"
|
||||
>
|
||||
<div class="flex align-items-center gap-3">
|
||||
<div class="w-2.5rem h-2.5rem border-round-lg flex align-items-center justify-content-center flex-shrink-0" :class="item.isActive !== false ? 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-400'">
|
||||
<i class="pi pi-phone"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex align-items-center gap-2 flex-wrap">
|
||||
<span class="font-bold text-sm text-color" dir="ltr">{{ toPersianDigits(item.phoneNumber) }}</span>
|
||||
<Tag v-if="item.label" :value="item.label" severity="secondary" class="text-xs" />
|
||||
<Tag :value="item.isActive !== false ? 'فعال' : 'غیرفعال'" :severity="item.isActive !== false ? 'success' : 'secondary'" class="text-xs" />
|
||||
</div>
|
||||
<span v-if="item.createdAt" class="text-xs text-muted block mt-1">
|
||||
تاریخ ثبت: {{ formatJalali(item.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center gap-2">
|
||||
<InputSwitch
|
||||
:modelValue="item.isActive !== false"
|
||||
@update:modelValue="toggleBypassStatus(item, index)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
severity="secondary"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
tooltip="ویرایش"
|
||||
@click="openEditBypassDialog(item, index)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
tooltip="حذف"
|
||||
@click="deleteBypassItem(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
@@ -274,6 +356,54 @@
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
<!-- ── SMS Bypass Number Add / Edit Dialog ────────────────────────── -->
|
||||
<Dialog
|
||||
v-model:visible="showBypassDialog"
|
||||
:header="isEditingBypass ? 'ویرایش شماره مجاز' : 'افزودن شماره مجاز (Bypass)'"
|
||||
modal
|
||||
:style="{ width: '440px' }"
|
||||
>
|
||||
<div class="flex flex-column gap-3 py-2">
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">شماره همراه *</label>
|
||||
<InputText
|
||||
v-model.trim="bypassForm.phoneNumber"
|
||||
placeholder="مثلا: 09123456789"
|
||||
dir="ltr"
|
||||
class="text-sm w-full"
|
||||
/>
|
||||
<small class="text-xs text-muted">شماره همراه با فرمت ۰۹... وارد شود.</small>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">عنوان یا برچسب شماره</label>
|
||||
<InputText
|
||||
v-model.trim="bypassForm.label"
|
||||
placeholder="مثلا: مدیر سامانه، تیم پشتیبانی، تست سرور"
|
||||
class="text-sm w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center justify-content-between p-3 border-round surface-ground border-1 border-color mt-2">
|
||||
<div>
|
||||
<span class="font-semibold text-sm text-color block">فعال بودن وضعیت Bypass</span>
|
||||
<span class="text-xs text-muted">در صورت فعال بودن، پیامکها بدون توجه به سوییچها ارسال میشوند.</span>
|
||||
</div>
|
||||
<InputSwitch v-model="bypassForm.isActive" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="انصراف" text severity="secondary" @click="showBypassDialog = false" />
|
||||
<Button
|
||||
:label="isEditingBypass ? 'ذخیره تغییرات' : 'افزودن شماره'"
|
||||
icon="pi pi-check"
|
||||
:loading="isSavingBypass"
|
||||
@click="saveBypassItem"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -290,13 +420,16 @@ import TabList from 'primevue/tablist';
|
||||
import Tab from 'primevue/tab';
|
||||
import TabPanels from 'primevue/tabpanels';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import { seedApi } from '@/api/seedApi';
|
||||
import { settingsApi } from '@/api/settingsApi';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
|
||||
const confirm = useConfirm();
|
||||
const { showSuccess, showError } = useToast();
|
||||
const { toPersianDigits, formatJalali } = usePersianDate();
|
||||
|
||||
const isSettingsLoading = ref(true);
|
||||
const isSavingSettings = ref(false);
|
||||
@@ -306,6 +439,16 @@ const smsTemplates = ref([]);
|
||||
const templateForm = ref({});
|
||||
const notificationSettings = ref([]);
|
||||
const notificationForm = ref({});
|
||||
const smsBypassNumbers = ref([]);
|
||||
const showBypassDialog = ref(false);
|
||||
const isEditingBypass = ref(false);
|
||||
const editingBypassIndex = ref(-1);
|
||||
const isSavingBypass = ref(false);
|
||||
const bypassForm = ref({
|
||||
phoneNumber: '',
|
||||
label: '',
|
||||
isActive: true
|
||||
});
|
||||
const messagingForm = ref({
|
||||
smsEnabled: true,
|
||||
emailEnabled: true,
|
||||
@@ -672,10 +815,110 @@ const applyNotificationSettings = (payload) => {
|
||||
notificationForm.value = form;
|
||||
};
|
||||
|
||||
const applyBypassNumbers = (payload) => {
|
||||
const data = payload?.data?.data || payload?.data || payload || {};
|
||||
smsBypassNumbers.value = Array.isArray(data.smsBypassNumbers) ? [...data.smsBypassNumbers] : [];
|
||||
};
|
||||
|
||||
const applySettings = (payload) => {
|
||||
applySmsTemplates(payload);
|
||||
applyMessaging(payload);
|
||||
applyNotificationSettings(payload);
|
||||
applyBypassNumbers(payload);
|
||||
};
|
||||
|
||||
const openAddBypassDialog = () => {
|
||||
bypassForm.value = {
|
||||
phoneNumber: '',
|
||||
label: '',
|
||||
isActive: true
|
||||
};
|
||||
isEditingBypass.value = false;
|
||||
editingBypassIndex.value = -1;
|
||||
showBypassDialog.value = true;
|
||||
};
|
||||
|
||||
const openEditBypassDialog = (item, index) => {
|
||||
bypassForm.value = {
|
||||
_id: item._id,
|
||||
phoneNumber: item.phoneNumber || '',
|
||||
label: item.label || '',
|
||||
isActive: item.isActive !== false
|
||||
};
|
||||
isEditingBypass.value = true;
|
||||
editingBypassIndex.value = index;
|
||||
showBypassDialog.value = true;
|
||||
};
|
||||
|
||||
const saveBypassItem = async () => {
|
||||
const phone = String(bypassForm.value.phoneNumber || '').trim();
|
||||
if (!phone || phone.length < 10) {
|
||||
showError('شماره همراه معتبر الزامی است (حداقل ۱۰ رقم)');
|
||||
return;
|
||||
}
|
||||
|
||||
isSavingBypass.value = true;
|
||||
try {
|
||||
const list = [...smsBypassNumbers.value];
|
||||
if (isEditingBypass.value && editingBypassIndex.value >= 0) {
|
||||
list[editingBypassIndex.value] = {
|
||||
...list[editingBypassIndex.value],
|
||||
phoneNumber: phone,
|
||||
label: String(bypassForm.value.label || '').trim(),
|
||||
isActive: bypassForm.value.isActive !== false
|
||||
};
|
||||
} else {
|
||||
list.push({
|
||||
phoneNumber: phone,
|
||||
label: String(bypassForm.value.label || '').trim(),
|
||||
isActive: bypassForm.value.isActive !== false,
|
||||
createdAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const res = await settingsApi.save({ smsBypassNumbers: list });
|
||||
applySettings(res);
|
||||
showSuccess(isEditingBypass.value ? 'شماره مجاز با موفقیت ویرایش شد' : 'شماره مجاز جدید اضافه شد');
|
||||
showBypassDialog.value = false;
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isSavingBypass.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleBypassStatus = async (item, index) => {
|
||||
try {
|
||||
const list = [...smsBypassNumbers.value];
|
||||
list[index] = { ...list[index], isActive: !list[index].isActive };
|
||||
const res = await settingsApi.save({ smsBypassNumbers: list });
|
||||
applySettings(res);
|
||||
showSuccess(`وضعیت شماره ${item.phoneNumber} بهروزرسانی شد`);
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBypassItem = (index) => {
|
||||
const item = smsBypassNumbers.value[index];
|
||||
confirm.require({
|
||||
message: `آیا از حذف شماره «${item.phoneNumber}» از لیست مجاز اطمینان دارید؟`,
|
||||
header: 'تایید حذف شماره مجاز',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptClass: 'p-button-danger',
|
||||
acceptLabel: 'بله، حذف شود',
|
||||
rejectLabel: 'انصراف',
|
||||
accept: async () => {
|
||||
try {
|
||||
const list = smsBypassNumbers.value.filter((_, i) => i !== index);
|
||||
const res = await settingsApi.save({ smsBypassNumbers: list });
|
||||
applySettings(res);
|
||||
showSuccess('شماره با موفقیت از لیست مجاز حذف شد');
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- /src/views/users/UserDetailView.vue -->
|
||||
<template>
|
||||
<div class="user-detail-view" v-if="profileData">
|
||||
<div class="user-detail-view relative">
|
||||
<LoadingOverlay :loading="isFetching || !profileData" message="در حال بارگذاری پرونده جامع کاربر..." />
|
||||
<div v-if="profileData">
|
||||
<!-- ── Profile Header & Quick KPI Bar ──────────────────────────────────── -->
|
||||
<div class="surface-card p-4 border-round-xl border-1 border-color shadow-sm mb-4">
|
||||
<div class="flex flex-column md:flex-row md:align-items-center justify-content-between gap-4 pb-3 border-bottom-1 border-color">
|
||||
@@ -634,6 +635,7 @@
|
||||
:preselected-user-id="String(userId)"
|
||||
@saved="fetchFullProfile"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -646,6 +648,7 @@ import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||||
import StatusTag from '@/components/common/StatusTag.vue';
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue';
|
||||
import UserFilesSection from '@/components/uploader/UserFilesSection.vue';
|
||||
import AddProfessorFromUserDialog from '@/components/professors/AddProfessorFromUserDialog.vue';
|
||||
import StudentAttendanceChart from '@/components/users/charts/StudentAttendanceChart.vue';
|
||||
@@ -740,12 +743,17 @@ const getWaitlistSeverity = (status) => {
|
||||
return 'secondary';
|
||||
};
|
||||
|
||||
const isFetching = ref(true);
|
||||
|
||||
const fetchFullProfile = async () => {
|
||||
isFetching.value = true;
|
||||
try {
|
||||
const res = await userApi.getFullProfile(userId);
|
||||
profileData.value = res.data || res;
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
<Button label="انصراف" text severity="secondary" @click="$router.push('/users')" />
|
||||
</PageHeader>
|
||||
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm">
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm relative overflow-hidden">
|
||||
<LoadingOverlay :loading="isFetching" message="در حال دریافت اطلاعات کاربر..." />
|
||||
<form @submit.prevent="handleSubmit" class="grid">
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">{{ $t('users.name') }} *</label>
|
||||
@@ -207,6 +208,7 @@ import { roleApi } from '@/api/roleApi';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue';
|
||||
import AdminNotesField from '@/components/common/AdminNotesField.vue';
|
||||
import UserFilesSection from '@/components/uploader/UserFilesSection.vue';
|
||||
import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue';
|
||||
@@ -223,6 +225,7 @@ const { showSuccess, showError, showWarn } = useToast();
|
||||
|
||||
const userId = route.params.id;
|
||||
const isEditMode = computed(() => !!userId);
|
||||
const isFetching = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const isResettingPassword = ref(false);
|
||||
const filesSection = ref(null);
|
||||
@@ -341,6 +344,7 @@ const fetchRoles = async () => {
|
||||
|
||||
const fetchUser = async () => {
|
||||
if (!userId) return;
|
||||
isFetching.value = true;
|
||||
try {
|
||||
const res = await userApi.getOne(userId);
|
||||
const data = res.data || res;
|
||||
@@ -370,6 +374,8 @@ const fetchUser = async () => {
|
||||
});
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user