feat: add settings data upload and single-name user forms
Allow SuperAdmins to upload import JSON from settings, and update user UI for merged name, gender, and extended profile fields.
This commit is contained in:
+9
-1
@@ -3,5 +3,13 @@ import axiosInstance from './axiosInstance';
|
|||||||
|
|
||||||
export const seedApi = {
|
export const seedApi = {
|
||||||
getStatus: () => axiosInstance.get('/seed/status'),
|
getStatus: () => axiosInstance.get('/seed/status'),
|
||||||
runSeed: () => axiosInstance.post('/seed', {}, { timeout: 60000 })
|
runSeed: () => axiosInstance.post('/seed', {}, { timeout: 60000 }),
|
||||||
|
importData: (file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
return axiosInstance.post('/data-import', formData, {
|
||||||
|
timeout: 180000,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-1
@@ -50,13 +50,21 @@
|
|||||||
"addUser": "افزودن کاربر جدید",
|
"addUser": "افزودن کاربر جدید",
|
||||||
"editUser": "ویرایش کاربر",
|
"editUser": "ویرایش کاربر",
|
||||||
"name": "نام",
|
"name": "نام",
|
||||||
"surname": "نام خانوادگی",
|
"gender": "جنسیت",
|
||||||
"nationalId": "کد ملی",
|
"nationalId": "کد ملی",
|
||||||
"phone": "شماره همراه",
|
"phone": "شماره همراه",
|
||||||
"email": "ایمیل",
|
"email": "ایمیل",
|
||||||
"preferredMessenger": "پیامرسان ترجیحی",
|
"preferredMessenger": "پیامرسان ترجیحی",
|
||||||
"role": "نقش کاربر",
|
"role": "نقش کاربر",
|
||||||
"status": "وضعیت حساب",
|
"status": "وضعیت حساب",
|
||||||
|
"fatherName": "نام پدر",
|
||||||
|
"birthCertificateNumber": "شماره شناسنامه",
|
||||||
|
"birthDate": "تاریخ تولد",
|
||||||
|
"placeOfIssue": "محل صدور",
|
||||||
|
"postalCode": "کد پستی",
|
||||||
|
"education": "میزان تحصیلات و رشته",
|
||||||
|
"parentPhoneNumber": "تلفن همراه والد",
|
||||||
|
"address": "آدرس",
|
||||||
"tabInfo": "اطلاعات شخصی",
|
"tabInfo": "اطلاعات شخصی",
|
||||||
"tabCourses": "دورههای ثبتنامی",
|
"tabCourses": "دورههای ثبتنامی",
|
||||||
"tabSessions": "حضور و غیاب",
|
"tabSessions": "حضور و غیاب",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<Column field="name" header="نام و نام خانوادگی دانشجو">
|
<Column field="name" header="نام و نام خانوادگی دانشجو">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<span class="font-bold text-color">{{ data.name }} {{ data.surname }}</span>
|
<span class="font-bold text-color">{{ data.name }}</span>
|
||||||
<span class="text-xs text-muted block">کد ملی: {{ toPersianDigits(data.nationalIdCode || '—') }}</span>
|
<span class="text-xs text-muted block">کد ملی: {{ toPersianDigits(data.nationalIdCode || '—') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
@@ -121,7 +121,8 @@ const fetchSessionAttendance = async () => {
|
|||||||
return {
|
return {
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
name: user.name || '',
|
name: user.name || '',
|
||||||
surname: user.surname || '',
|
name: user.name || '',
|
||||||
|
gender: user.gender || null,
|
||||||
nationalIdCode: user.nationalIdCode || '',
|
nationalIdCode: user.nationalIdCode || '',
|
||||||
status: existing?.status || 'present',
|
status: existing?.status || 'present',
|
||||||
note: existing?.note || ''
|
note: existing?.note || ''
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
|
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column header="نام">
|
<Column header="نام">
|
||||||
<template #body="{ data }">{{ data.name }} {{ data.surname }}</template>
|
<template #body="{ data }">{{ data.name }}</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column header="موبایل">
|
<Column header="موبایل">
|
||||||
<template #body="{ data }"><span dir="ltr">{{ data.phoneNumber || '—' }}</span></template>
|
<template #body="{ data }"><span dir="ltr">{{ data.phoneNumber || '—' }}</span></template>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
|
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column header="نام">
|
<Column header="نام">
|
||||||
<template #body="{ data }">{{ data.name }} {{ data.surname }}</template>
|
<template #body="{ data }">{{ data.name }}</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column header="موبایل">
|
<Column header="موبایل">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
@@ -176,7 +176,7 @@ const availableUsers = computed(() => {
|
|||||||
.filter((u) => !enrolled.has(String(u._id)))
|
.filter((u) => !enrolled.has(String(u._id)))
|
||||||
.map((u) => ({
|
.map((u) => ({
|
||||||
...u,
|
...u,
|
||||||
label: `${u.name || ''} ${u.surname || ''} — ${u.phoneNumber || ''}`.trim()
|
label: `${u.name || ''} — ${u.phoneNumber || ''}`.trim()
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class="flex flex-column gap-3">
|
<div class="flex flex-column gap-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-muted text-xs block mb-1">نام کاربر / دانشجو</span>
|
<span class="text-muted text-xs block mb-1">نام کاربر / دانشجو</span>
|
||||||
<span class="font-bold text-color text-base">{{ payment.user?.name || payment.userName || 'کاربر' }} {{ payment.user?.surname || '' }}</span>
|
<span class="font-bold text-color text-base">{{ payment.user?.name || payment.userName || 'کاربر' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="payment.classes && payment.classes.length">
|
<div v-if="payment.classes && payment.classes.length">
|
||||||
<span class="text-muted text-xs block mb-1">کلاسهای مربوطه</span>
|
<span class="text-muted text-xs block mb-1">کلاسهای مربوطه</span>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<Column field="user" header="نام دانشجو / کاربر">
|
<Column field="user" header="نام دانشجو / کاربر">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<router-link :to="`/payments/view/${data._id || data.id}`" class="font-bold text-color hover:text-primary">
|
<router-link :to="`/payments/view/${data._id || data.id}`" class="font-bold text-color hover:text-primary">
|
||||||
{{ data.user?.name || data.userName || 'کاربر' }} {{ data.user?.surname || '' }}
|
{{ data.user?.name || data.userName || 'کاربر' }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
@@ -196,7 +196,7 @@ const fetchDropdownData = async () => {
|
|||||||
]);
|
]);
|
||||||
const uData = uRes.data || uRes;
|
const uData = uRes.data || uRes;
|
||||||
const rawUsers = uData.items || uData.users || uData || [];
|
const rawUsers = uData.items || uData.users || uData || [];
|
||||||
usersList.value = rawUsers.map(u => ({ ...u, fullName: `${u.name} ${u.surname}` }));
|
usersList.value = rawUsers.map(u => ({ ...u, fullName: u.name || '' }));
|
||||||
|
|
||||||
const cData = cRes.data || cRes;
|
const cData = cRes.data || cRes;
|
||||||
classesList.value = cData.items || cData.classes || cData || [];
|
classesList.value = cData.items || cData.classes || cData || [];
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<div class="settings-view w-full max-w-3xl mx-auto">
|
<div class="settings-view w-full max-w-3xl mx-auto">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="تنظیمات سیستم"
|
title="تنظیمات سیستم"
|
||||||
subtitle="عملیات راهاندازی و پیکربندی اولیه پایگاه داده"
|
subtitle="عملیات راهاندازی و وارد کردن دادههای دورهها و کاربران"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="surface-card p-4 sm:p-5 border-round-xl border-1 border-color shadow-sm">
|
<div class="surface-card p-4 sm:p-5 border-round-xl border-1 border-color shadow-sm mb-4">
|
||||||
<div class="flex align-items-start gap-3 mb-4">
|
<div class="flex align-items-start gap-3 mb-4">
|
||||||
<div class="w-3rem h-3rem border-round-xl flex align-items-center justify-content-center flex-shrink-0 bg-primary-light text-primary">
|
<div class="w-3rem h-3rem border-round-xl flex align-items-center justify-content-center flex-shrink-0 bg-primary-light text-primary">
|
||||||
<i class="pi pi-database text-xl"></i>
|
<i class="pi pi-database text-xl"></i>
|
||||||
@@ -45,6 +45,67 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="surface-card p-4 sm:p-5 border-round-xl border-1 border-color shadow-sm">
|
||||||
|
<div class="flex align-items-start gap-3 mb-4">
|
||||||
|
<div class="w-3rem h-3rem border-round-xl flex align-items-center justify-content-center flex-shrink-0 bg-primary-light text-primary">
|
||||||
|
<i class="pi pi-upload text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h2 class="text-lg font-bold text-color m-0 mb-1">وارد کردن دادهها</h2>
|
||||||
|
<p class="text-sm text-muted m-0 line-height-3">
|
||||||
|
فایل JSON ساختاریافته دورهها، کلاسها و کاربران را بارگذاری کنید. دادههای قبلی حذف نمیشوند؛ موارد تکراری بهروزرسانی یا به کلاس اضافه میشوند.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-column gap-3">
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
type="file"
|
||||||
|
accept="application/json,.json"
|
||||||
|
class="hidden"
|
||||||
|
@change="onFileSelected"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-column sm:flex-row align-items-stretch sm:align-items-center gap-3">
|
||||||
|
<Button
|
||||||
|
:label="selectedFileName || 'انتخاب فایل JSON'"
|
||||||
|
:icon="selectedFileName ? 'pi pi-file' : 'pi pi-folder-open'"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
class="font-semibold"
|
||||||
|
@click="fileInputRef?.click()"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="بارگذاری و افزودن به پایگاه داده"
|
||||||
|
icon="pi pi-cloud-upload"
|
||||||
|
class="font-bold"
|
||||||
|
:loading="isImporting"
|
||||||
|
:disabled="!selectedFile"
|
||||||
|
@click="confirmImport"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-muted m-0 line-height-3">
|
||||||
|
نمونه فایل آماده: <code dir="ltr">raw-data/import-data.json</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="importResult"
|
||||||
|
class="p-3 border-round surface-ground border-1 border-color text-sm"
|
||||||
|
>
|
||||||
|
<div class="font-semibold mb-2">نتیجه آخرین بارگذاری</div>
|
||||||
|
<ul class="m-0 pr-3 line-height-3">
|
||||||
|
<li>دوره جدید: {{ importResult.stats?.coursesCreated ?? 0 }} — بازاستفاده: {{ importResult.stats?.coursesReused ?? 0 }}</li>
|
||||||
|
<li>کلاس جدید: {{ importResult.stats?.classesCreated ?? 0 }} — بازاستفاده: {{ importResult.stats?.classesReused ?? 0 }}</li>
|
||||||
|
<li>کاربر جدید: {{ importResult.stats?.studentsCreated ?? 0 }} — بهروزرسانی: {{ importResult.stats?.studentsUpdated ?? 0 }}</li>
|
||||||
|
<li>ثبتنام در کلاس: {{ importResult.stats?.enrollmentsAdded ?? 0 }} — رد شده: {{ importResult.stats?.studentsSkipped ?? 0 }}</li>
|
||||||
|
<li v-if="importResult.warnings?.length">هشدارها: {{ importResult.warnings.length }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -64,6 +125,12 @@ const isSeeding = ref(false);
|
|||||||
const isLocked = ref(false);
|
const isLocked = ref(false);
|
||||||
const lockedAt = ref(null);
|
const lockedAt = ref(null);
|
||||||
|
|
||||||
|
const fileInputRef = ref(null);
|
||||||
|
const selectedFile = ref(null);
|
||||||
|
const selectedFileName = ref('');
|
||||||
|
const isImporting = ref(false);
|
||||||
|
const importResult = ref(null);
|
||||||
|
|
||||||
const lockedAtLabel = computed(() => {
|
const lockedAtLabel = computed(() => {
|
||||||
if (!lockedAt.value) return '';
|
if (!lockedAt.value) return '';
|
||||||
const date = new Date(lockedAt.value);
|
const date = new Date(lockedAt.value);
|
||||||
@@ -118,5 +185,48 @@ const confirmSeed = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onFileSelected = (event) => {
|
||||||
|
const file = event.target.files?.[0] || null;
|
||||||
|
selectedFile.value = file;
|
||||||
|
selectedFileName.value = file?.name || '';
|
||||||
|
importResult.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runImport = async () => {
|
||||||
|
if (!selectedFile.value) return;
|
||||||
|
isImporting.value = true;
|
||||||
|
try {
|
||||||
|
const response = await seedApi.importData(selectedFile.value);
|
||||||
|
const data = response?.data || response;
|
||||||
|
importResult.value = data;
|
||||||
|
showSuccess('دادهها با موفقیت به پایگاه داده اضافه شدند.');
|
||||||
|
} catch (err) {
|
||||||
|
showError(err);
|
||||||
|
} finally {
|
||||||
|
isImporting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmImport = () => {
|
||||||
|
if (!selectedFile.value) return;
|
||||||
|
confirm.require({
|
||||||
|
header: 'وارد کردن دادهها',
|
||||||
|
message: 'دورهها، کلاسها و کاربران از فایل JSON به پایگاه داده افزوده میشوند. دادههای موجود حذف نمیشوند. ادامه میدهید؟',
|
||||||
|
icon: 'pi pi-upload',
|
||||||
|
acceptLabel: 'بارگذاری',
|
||||||
|
rejectLabel: 'انصراف',
|
||||||
|
acceptClass: 'p-button-primary',
|
||||||
|
accept: () => {
|
||||||
|
runImport();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(loadStatus);
|
onMounted(loadStatus);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- /src/views/users/UserDetailView.vue -->
|
<!-- /src/views/users/UserDetailView.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="user-detail-view" v-if="user">
|
<div class="user-detail-view" v-if="user">
|
||||||
<PageHeader :title="`${user.name || ''} ${user.surname || ''}`" :subtitle="`کد ملی: ${toPersianDigits(user.nationalId)}`">
|
<PageHeader :title="user.name || ''" :subtitle="`کد ملی: ${toPersianDigits(user.nationalId)}`">
|
||||||
<PermissionGate permission="users:update">
|
<PermissionGate permission="users:update">
|
||||||
<Button :label="$t('app.edit')" icon="pi pi-pencil" severity="warning" @click="$router.push(`/users/edit/${user._id || user.id}`)" />
|
<Button :label="$t('app.edit')" icon="pi pi-pencil" severity="warning" @click="$router.push(`/users/edit/${user._id || user.id}`)" />
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
@@ -22,8 +22,12 @@
|
|||||||
<TabPanel value="0">
|
<TabPanel value="0">
|
||||||
<div class="grid p-3">
|
<div class="grid p-3">
|
||||||
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
<span class="text-muted text-xs block mb-1">نام و نام خانوادگی</span>
|
<span class="text-muted text-xs block mb-1">نام</span>
|
||||||
<span class="font-bold text-color text-base">{{ user.name }} {{ user.surname }}</span>
|
<span class="font-bold text-color text-base">{{ user.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
|
<span class="text-muted text-xs block mb-1">جنسیت</span>
|
||||||
|
<span class="font-bold text-color text-base">{{ user.gender === 'male' ? 'مرد' : user.gender === 'female' ? 'زن' : '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
<span class="text-muted text-xs block mb-1">کد ملی</span>
|
<span class="text-muted text-xs block mb-1">کد ملی</span>
|
||||||
@@ -52,6 +56,30 @@
|
|||||||
<span class="text-muted text-xs block mb-1">نقش سیستم</span>
|
<span class="text-muted text-xs block mb-1">نقش سیستم</span>
|
||||||
<Tag :value="user.role?.name || 'دانشجو'" severity="secondary" />
|
<Tag :value="user.role?.name || 'دانشجو'" severity="secondary" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
|
<span class="text-muted text-xs block mb-1">نام پدر</span>
|
||||||
|
<span class="font-bold text-color text-base">{{ user.fatherName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
|
<span class="text-muted text-xs block mb-1">تاریخ تولد</span>
|
||||||
|
<span class="font-bold text-color text-base" dir="ltr">{{ user.birthDate ? new Date(user.birthDate).toLocaleDateString('fa-IR') : '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
|
<span class="text-muted text-xs block mb-1">شماره شناسنامه</span>
|
||||||
|
<span class="font-bold text-color text-base" dir="ltr">{{ user.birthCertificateNumber || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
|
<span class="text-muted text-xs block mb-1">محل صدور</span>
|
||||||
|
<span class="font-bold text-color text-base">{{ user.placeOfIssue || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
|
<span class="text-muted text-xs block mb-1">کد پستی</span>
|
||||||
|
<span class="font-bold text-color text-base" dir="ltr">{{ user.postalCode || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
|
<span class="text-muted text-xs block mb-1">تحصیلات</span>
|
||||||
|
<span class="font-bold text-color text-base">{{ user.education || '-' }}</span>
|
||||||
|
</div>
|
||||||
<div class="col-12 mb-3">
|
<div class="col-12 mb-3">
|
||||||
<span class="text-muted text-xs block mb-1">آدرس سکونت</span>
|
<span class="text-muted text-xs block mb-1">آدرس سکونت</span>
|
||||||
<span class="text-color text-sm">{{ user.address || '-' }}</span>
|
<span class="text-color text-sm">{{ user.address || '-' }}</span>
|
||||||
|
|||||||
@@ -12,12 +12,20 @@
|
|||||||
<form @submit.prevent="handleSubmit" class="grid">
|
<form @submit.prevent="handleSubmit" class="grid">
|
||||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">{{ $t('users.name') }} *</label>
|
<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" placeholder="نام کامل" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">{{ $t('users.surname') }} *</label>
|
<label class="font-semibold text-sm">{{ $t('users.gender') }}</label>
|
||||||
<InputText v-model.trim="form.surname" class="w-full text-sm" />
|
<Dropdown
|
||||||
|
v-model="form.gender"
|
||||||
|
:options="genderOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="انتخاب جنسیت"
|
||||||
|
class="w-full text-sm"
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
@@ -40,6 +48,41 @@
|
|||||||
<Dropdown v-model="form.role" :options="roles" optionLabel="name" optionValue="_id" placeholder="انتخاب نقش" class="w-full text-sm" />
|
<Dropdown v-model="form.role" :options="roles" optionLabel="name" optionValue="_id" placeholder="انتخاب نقش" class="w-full text-sm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">{{ $t('users.fatherName') }}</label>
|
||||||
|
<InputText v-model.trim="form.fatherName" class="w-full text-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">{{ $t('users.birthCertificateNumber') }}</label>
|
||||||
|
<InputText v-model.trim="form.birthCertificateNumber" class="w-full text-sm" dir="ltr" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">{{ $t('users.birthDate') }}</label>
|
||||||
|
<InputText v-model.trim="form.birthDate" class="w-full text-sm" dir="ltr" placeholder="YYYY-MM-DD" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">{{ $t('users.placeOfIssue') }}</label>
|
||||||
|
<InputText v-model.trim="form.placeOfIssue" class="w-full text-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">{{ $t('users.postalCode') }}</label>
|
||||||
|
<InputText v-model.trim="form.postalCode" class="w-full text-sm" dir="ltr" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">{{ $t('users.education') }}</label>
|
||||||
|
<InputText v-model.trim="form.education" class="w-full text-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">{{ $t('users.parentPhoneNumber') }}</label>
|
||||||
|
<InputText v-model.trim="form.parentPhoneNumber" class="w-full text-sm" dir="ltr" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">{{ $t('users.preferredMessenger') }}</label>
|
<label class="font-semibold text-sm">{{ $t('users.preferredMessenger') }}</label>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
@@ -56,6 +99,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">{{ $t('users.address') }}</label>
|
||||||
|
<InputText v-model.trim="form.address" class="w-full text-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6 flex flex-column gap-2" v-if="isEditMode">
|
<div class="col-12 md:col-6 flex flex-column gap-2" v-if="isEditMode">
|
||||||
<label class="font-semibold text-sm">{{ $t('auth.username') }}</label>
|
<label class="font-semibold text-sm">{{ $t('auth.username') }}</label>
|
||||||
<InputText
|
<InputText
|
||||||
@@ -143,19 +191,39 @@ const messengerOptions = [
|
|||||||
{ label: 'ایمیل', value: 'Email' }
|
{ label: 'ایمیل', value: 'Email' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const genderOptions = [
|
||||||
|
{ label: 'مرد', value: 'male' },
|
||||||
|
{ label: 'زن', value: 'female' }
|
||||||
|
];
|
||||||
|
|
||||||
const toMessengerList = (value) => {
|
const toMessengerList = (value) => {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
return Array.isArray(value) ? [...value] : [value];
|
return Array.isArray(value) ? [...value] : [value];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toDateInput = (value) => {
|
||||||
|
if (!value) return '';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
};
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
surname: '',
|
gender: null,
|
||||||
nationalId: '',
|
nationalId: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
email: '',
|
email: '',
|
||||||
role: null,
|
role: null,
|
||||||
preferredMessenger: [],
|
preferredMessenger: [],
|
||||||
|
fatherName: '',
|
||||||
|
birthCertificateNumber: '',
|
||||||
|
birthDate: '',
|
||||||
|
placeOfIssue: '',
|
||||||
|
postalCode: '',
|
||||||
|
education: '',
|
||||||
|
parentPhoneNumber: '',
|
||||||
|
address: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: ''
|
||||||
});
|
});
|
||||||
@@ -163,11 +231,19 @@ const form = reactive({
|
|||||||
const buildPayload = () => {
|
const buildPayload = () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: form.name,
|
name: form.name,
|
||||||
surname: form.surname,
|
gender: form.gender || undefined,
|
||||||
nationalIdCode: form.nationalId,
|
nationalIdCode: form.nationalId,
|
||||||
phoneNumber: form.phone,
|
phoneNumber: form.phone,
|
||||||
email: form.email || undefined,
|
email: form.email || undefined,
|
||||||
role: form.role || undefined
|
role: form.role || undefined,
|
||||||
|
fatherName: form.fatherName || undefined,
|
||||||
|
birthCertificateNumber: form.birthCertificateNumber || undefined,
|
||||||
|
birthDate: form.birthDate || undefined,
|
||||||
|
placeOfIssue: form.placeOfIssue || undefined,
|
||||||
|
postalCode: form.postalCode || undefined,
|
||||||
|
education: form.education || undefined,
|
||||||
|
parentPhoneNumber: form.parentPhoneNumber || undefined,
|
||||||
|
address: form.address || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
if (form.preferredMessenger?.length) {
|
if (form.preferredMessenger?.length) {
|
||||||
@@ -200,14 +276,24 @@ const fetchUser = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await userApi.getOne(userId);
|
const res = await userApi.getOne(userId);
|
||||||
const data = res.data || res;
|
const data = res.data || res;
|
||||||
|
const legacySurname = data.surname ? String(data.surname).trim() : '';
|
||||||
|
const fullName = [data.name, legacySurname].filter(Boolean).join(' ').trim();
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
name: data.name || '',
|
name: fullName,
|
||||||
surname: data.surname || '',
|
gender: data.gender || null,
|
||||||
nationalId: data.nationalIdCode || data.nationalId || '',
|
nationalId: data.nationalIdCode || data.nationalId || '',
|
||||||
phone: data.phoneNumber || data.phone || '',
|
phone: data.phoneNumber || data.phone || '',
|
||||||
email: data.email || '',
|
email: data.email || '',
|
||||||
role: data.role?._id || data.role || null,
|
role: data.role?._id || data.role || null,
|
||||||
preferredMessenger: toMessengerList(data.preferredMessenger),
|
preferredMessenger: toMessengerList(data.preferredMessenger),
|
||||||
|
fatherName: data.fatherName || '',
|
||||||
|
birthCertificateNumber: data.birthCertificateNumber || '',
|
||||||
|
birthDate: toDateInput(data.birthDate),
|
||||||
|
placeOfIssue: data.placeOfIssue || '',
|
||||||
|
postalCode: data.postalCode || '',
|
||||||
|
education: data.education || '',
|
||||||
|
parentPhoneNumber: data.parentPhoneNumber || '',
|
||||||
|
address: data.address || '',
|
||||||
username: data.username || '',
|
username: data.username || '',
|
||||||
password: ''
|
password: ''
|
||||||
});
|
});
|
||||||
@@ -222,8 +308,8 @@ const finishAfterCreate = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!form.name || !form.surname) {
|
if (!form.name) {
|
||||||
showError('نام و نام خانوادگی الزامی است');
|
showError('نام الزامی است');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!form.nationalId || !form.phone) {
|
if (!form.nationalId || !form.phone) {
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
@sort-change="onSort"
|
@sort-change="onSort"
|
||||||
@search-change="onSearch"
|
@search-change="onSearch"
|
||||||
>
|
>
|
||||||
<Column field="name" header="نام و نام خانوادگی" sortable>
|
<Column field="name" header="نام" sortable>
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<router-link :to="`/users/${data._id || data.id}`" class="font-bold text-color hover:text-primary">
|
<router-link :to="`/users/${data._id || data.id}`" class="font-bold text-color hover:text-primary">
|
||||||
{{ data.name }} {{ data.surname }}
|
{{ data.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|||||||
Reference in New Issue
Block a user