diff --git a/src/api/seedApi.js b/src/api/seedApi.js index db04cd0..7f76cc8 100644 --- a/src/api/seedApi.js +++ b/src/api/seedApi.js @@ -3,5 +3,13 @@ import axiosInstance from './axiosInstance'; export const seedApi = { 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' } + }); + } }; diff --git a/src/locales/fa.json b/src/locales/fa.json index 3802d62..2cabb04 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -50,13 +50,21 @@ "addUser": "افزودن کاربر جدید", "editUser": "ویرایش کاربر", "name": "نام", - "surname": "نام خانوادگی", + "gender": "جنسیت", "nationalId": "کد ملی", "phone": "شماره همراه", "email": "ایمیل", "preferredMessenger": "پیامرسان ترجیحی", "role": "نقش کاربر", "status": "وضعیت حساب", + "fatherName": "نام پدر", + "birthCertificateNumber": "شماره شناسنامه", + "birthDate": "تاریخ تولد", + "placeOfIssue": "محل صدور", + "postalCode": "کد پستی", + "education": "میزان تحصیلات و رشته", + "parentPhoneNumber": "تلفن همراه والد", + "address": "آدرس", "tabInfo": "اطلاعات شخصی", "tabCourses": "دورههای ثبتنامی", "tabSessions": "حضور و غیاب", diff --git a/src/views/attendances/AttendanceEntryView.vue b/src/views/attendances/AttendanceEntryView.vue index eff0085..855bd27 100644 --- a/src/views/attendances/AttendanceEntryView.vue +++ b/src/views/attendances/AttendanceEntryView.vue @@ -31,7 +31,7 @@ - {{ data.name }} {{ data.surname }} + {{ data.name }} کد ملی: {{ toPersianDigits(data.nationalIdCode || '—') }} @@ -121,7 +121,8 @@ const fetchSessionAttendance = async () => { return { userId: user._id, name: user.name || '', - surname: user.surname || '', + name: user.name || '', + gender: user.gender || null, nationalIdCode: user.nationalIdCode || '', status: existing?.status || 'present', note: existing?.note || '' diff --git a/src/views/classes/ClassDetailView.vue b/src/views/classes/ClassDetailView.vue index c23bff6..6993aaf 100644 --- a/src/views/classes/ClassDetailView.vue +++ b/src/views/classes/ClassDetailView.vue @@ -47,7 +47,7 @@ {{ toPersianDigits(index + 1) }} - {{ data.name }} {{ data.surname }} + {{ data.name }} {{ data.phoneNumber || '—' }} diff --git a/src/views/classes/ClassFormView.vue b/src/views/classes/ClassFormView.vue index a5f4230..286a381 100644 --- a/src/views/classes/ClassFormView.vue +++ b/src/views/classes/ClassFormView.vue @@ -97,7 +97,7 @@ {{ toPersianDigits(index + 1) }} - {{ data.name }} {{ data.surname }} + {{ data.name }} @@ -176,7 +176,7 @@ const availableUsers = computed(() => { .filter((u) => !enrolled.has(String(u._id))) .map((u) => ({ ...u, - label: `${u.name || ''} ${u.surname || ''} — ${u.phoneNumber || ''}`.trim() + label: `${u.name || ''} — ${u.phoneNumber || ''}`.trim() })); }); diff --git a/src/views/payments/PaymentDetailView.vue b/src/views/payments/PaymentDetailView.vue index 31eb13a..9a43ac1 100644 --- a/src/views/payments/PaymentDetailView.vue +++ b/src/views/payments/PaymentDetailView.vue @@ -13,7 +13,7 @@ نام کاربر / دانشجو - {{ payment.user?.name || payment.userName || 'کاربر' }} {{ payment.user?.surname || '' }} + {{ payment.user?.name || payment.userName || 'کاربر' }} کلاسهای مربوطه diff --git a/src/views/payments/PaymentListView.vue b/src/views/payments/PaymentListView.vue index 5a98865..73647ea 100644 --- a/src/views/payments/PaymentListView.vue +++ b/src/views/payments/PaymentListView.vue @@ -22,7 +22,7 @@ - {{ data.user?.name || data.userName || 'کاربر' }} {{ data.user?.surname || '' }} + {{ data.user?.name || data.userName || 'کاربر' }} @@ -196,7 +196,7 @@ const fetchDropdownData = async () => { ]); const uData = uRes.data || uRes; 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; classesList.value = cData.items || cData.classes || cData || []; diff --git a/src/views/settings/SettingsView.vue b/src/views/settings/SettingsView.vue index 9e695bc..860f014 100644 --- a/src/views/settings/SettingsView.vue +++ b/src/views/settings/SettingsView.vue @@ -3,10 +3,10 @@ - + @@ -45,6 +45,67 @@ + + + + + + + + وارد کردن دادهها + + فایل JSON ساختاریافته دورهها، کلاسها و کاربران را بارگذاری کنید. دادههای قبلی حذف نمیشوند؛ موارد تکراری بهروزرسانی یا به کلاس اضافه میشوند. + + + + + + + + + + + + + + نمونه فایل آماده: raw-data/import-data.json + + + + نتیجه آخرین بارگذاری + + دوره جدید: {{ importResult.stats?.coursesCreated ?? 0 }} — بازاستفاده: {{ importResult.stats?.coursesReused ?? 0 }} + کلاس جدید: {{ importResult.stats?.classesCreated ?? 0 }} — بازاستفاده: {{ importResult.stats?.classesReused ?? 0 }} + کاربر جدید: {{ importResult.stats?.studentsCreated ?? 0 }} — بهروزرسانی: {{ importResult.stats?.studentsUpdated ?? 0 }} + ثبتنام در کلاس: {{ importResult.stats?.enrollmentsAdded ?? 0 }} — رد شده: {{ importResult.stats?.studentsSkipped ?? 0 }} + هشدارها: {{ importResult.warnings.length }} + + + + @@ -64,6 +125,12 @@ const isSeeding = ref(false); const isLocked = ref(false); 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(() => { if (!lockedAt.value) return ''; 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); + + diff --git a/src/views/users/UserDetailView.vue b/src/views/users/UserDetailView.vue index cfe29f4..ae9ab8d 100644 --- a/src/views/users/UserDetailView.vue +++ b/src/views/users/UserDetailView.vue @@ -1,7 +1,7 @@ - + @@ -22,8 +22,12 @@ - نام و نام خانوادگی - {{ user.name }} {{ user.surname }} + نام + {{ user.name }} + + + جنسیت + {{ user.gender === 'male' ? 'مرد' : user.gender === 'female' ? 'زن' : '-' }} کد ملی @@ -52,6 +56,30 @@ نقش سیستم + + نام پدر + {{ user.fatherName || '-' }} + + + تاریخ تولد + {{ user.birthDate ? new Date(user.birthDate).toLocaleDateString('fa-IR') : '-' }} + + + شماره شناسنامه + {{ user.birthCertificateNumber || '-' }} + + + محل صدور + {{ user.placeOfIssue || '-' }} + + + کد پستی + {{ user.postalCode || '-' }} + + + تحصیلات + {{ user.education || '-' }} + آدرس سکونت {{ user.address || '-' }} diff --git a/src/views/users/UserFormView.vue b/src/views/users/UserFormView.vue index f79a8af..77c3dea 100644 --- a/src/views/users/UserFormView.vue +++ b/src/views/users/UserFormView.vue @@ -12,12 +12,20 @@ {{ $t('users.name') }} * - + - {{ $t('users.surname') }} * - + {{ $t('users.gender') }} + @@ -40,6 +48,41 @@ + + {{ $t('users.fatherName') }} + + + + + {{ $t('users.birthCertificateNumber') }} + + + + + {{ $t('users.birthDate') }} + + + + + {{ $t('users.placeOfIssue') }} + + + + + {{ $t('users.postalCode') }} + + + + + {{ $t('users.education') }} + + + + + {{ $t('users.parentPhoneNumber') }} + + + {{ $t('users.preferredMessenger') }} + + {{ $t('users.address') }} + + + {{ $t('auth.username') }} { if (!value) return []; 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({ name: '', - surname: '', + gender: null, nationalId: '', phone: '', email: '', role: null, preferredMessenger: [], + fatherName: '', + birthCertificateNumber: '', + birthDate: '', + placeOfIssue: '', + postalCode: '', + education: '', + parentPhoneNumber: '', + address: '', username: '', password: '' }); @@ -163,11 +231,19 @@ const form = reactive({ const buildPayload = () => { const payload = { name: form.name, - surname: form.surname, + gender: form.gender || undefined, nationalIdCode: form.nationalId, phoneNumber: form.phone, 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) { @@ -200,14 +276,24 @@ const fetchUser = async () => { try { const res = await userApi.getOne(userId); 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, { - name: data.name || '', - surname: data.surname || '', + name: fullName, + gender: data.gender || null, nationalId: data.nationalIdCode || data.nationalId || '', phone: data.phoneNumber || data.phone || '', email: data.email || '', role: data.role?._id || data.role || null, 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 || '', password: '' }); @@ -222,8 +308,8 @@ const finishAfterCreate = () => { }; const handleSubmit = async () => { - if (!form.name || !form.surname) { - showError('نام و نام خانوادگی الزامی است'); + if (!form.name) { + showError('نام الزامی است'); return; } if (!form.nationalId || !form.phone) { diff --git a/src/views/users/UserListView.vue b/src/views/users/UserListView.vue index 2d9d28d..752c174 100644 --- a/src/views/users/UserListView.vue +++ b/src/views/users/UserListView.vue @@ -19,10 +19,10 @@ @sort-change="onSort" @search-change="onSearch" > - + - {{ data.name }} {{ data.surname }} + {{ data.name }}
+ فایل JSON ساختاریافته دورهها، کلاسها و کاربران را بارگذاری کنید. دادههای قبلی حذف نمیشوند؛ موارد تکراری بهروزرسانی یا به کلاس اضافه میشوند. +
+ نمونه فایل آماده: raw-data/import-data.json +
raw-data/import-data.json