diff --git a/app.js b/app.js index cd94c19..e2efb28 100644 --- a/app.js +++ b/app.js @@ -79,6 +79,7 @@ app.get('/api/health', (req, res) => { const seedDatabase = require('./seed'); const seedRoutes = require('./components/seed/seedRoutes'); +const dataImportRoutes = require('./components/dataImport/dataImportRoutes'); app.use('/api/auth', authRoutes); app.use('/api/users', userRoutes); @@ -95,6 +96,7 @@ app.use('/api/dashboard', dashboardRoutes); app.use('/api/activity-logs', activityLogRoutes); app.use('/api/contact-inquiries', contactInquiryRoutes); app.use('/api/seed', seedRoutes); +app.use('/api/data-import', dataImportRoutes); // ── Error Handlers ──────────────────────────────────────────────────────────── app.use(notFoundHandler); diff --git a/components/activityLogs/activityLogService.js b/components/activityLogs/activityLogService.js index 01d5542..b9f301b 100644 --- a/components/activityLogs/activityLogService.js +++ b/components/activityLogs/activityLogService.js @@ -121,7 +121,7 @@ const getAllActivityLogs = async (queryParams = {}) => { const [logs, totalCount] = await Promise.all([ ActivityLog.find(filter) - .populate('actor', 'name surname username') + .populate('actor', 'name username') .sort(sort) .skip(skip) .limit(limit) diff --git a/components/certificates/certificateService.js b/components/certificates/certificateService.js index e9962ad..07a96d3 100644 --- a/components/certificates/certificateService.js +++ b/components/certificates/certificateService.js @@ -39,7 +39,7 @@ const createCertificate = async (data) => { const getCertificateById = async (id) => { const certificate = await Certificate.findById(id) - .populate('user', 'name surname username nationalIdCode') + .populate('user', 'name username nationalIdCode') .populate('course', 'title type'); if (!certificate) { throw new AppError('CERTIFICATE_NOT_FOUND'); @@ -56,7 +56,7 @@ const getAllCertificates = async (queryParams) => { const [certificates, totalCount] = await Promise.all([ Certificate.find(filter) - .populate('user', 'name surname username') + .populate('user', 'name username') .populate('course', 'title') .sort(sort) .skip(skip) diff --git a/components/classes/classService.js b/components/classes/classService.js index 9607728..3995732 100644 --- a/components/classes/classService.js +++ b/components/classes/classService.js @@ -32,7 +32,7 @@ const getOne = async (id) => { const cls = await Class.findById(id) .populate({ path: 'course', select: 'title type price' }) .populate({ path: 'professor', select: 'name surname phoneNumber' }) - .populate({ path: 'students', select: 'name surname phoneNumber' }) + .populate({ path: 'students', select: 'name phoneNumber gender' }) .lean(); if (!cls) throw new AppError('CLASS_NOT_FOUND'); return cls; diff --git a/components/dataImport/dataImportController.js b/components/dataImport/dataImportController.js new file mode 100644 index 0000000..8f00c34 --- /dev/null +++ b/components/dataImport/dataImportController.js @@ -0,0 +1,28 @@ +// /components/dataImport/dataImportController.js +'use strict'; + +const catchAsync = require('../../utils/catchAsync'); +const dataImportService = require('./dataImportService'); +const AppError = require('../../utils/AppError'); +const { successResponse } = require('../../utils/apiResponse'); + +exports.importJson = catchAsync(async (req, res) => { + let payload = req.body; + + if (req.file?.buffer) { + try { + payload = JSON.parse(req.file.buffer.toString('utf8')); + } catch { + throw new AppError('VALIDATION_FAILED', null, 'Uploaded file is not valid JSON'); + } + } + + // Allow { data: { courses: [...] } } wrappers + if (payload?.data?.courses) payload = payload.data; + if (!payload?.courses && payload?.version && payload?.courses == null) { + throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload'); + } + + const result = await dataImportService.importData(payload); + return successResponse(res, 200, 'Data imported successfully', result); +}); diff --git a/components/dataImport/dataImportRoutes.js b/components/dataImport/dataImportRoutes.js new file mode 100644 index 0000000..b85d930 --- /dev/null +++ b/components/dataImport/dataImportRoutes.js @@ -0,0 +1,21 @@ +// /components/dataImport/dataImportRoutes.js +'use strict'; + +const express = require('express'); +const multer = require('multer'); +const dataImportController = require('./dataImportController'); +const authMiddleware = require('../../middlewares/authMiddleware'); +const requireSuperAdmin = require('../../middlewares/requireSuperAdmin'); + +const router = express.Router(); +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 20 * 1024 * 1024 } +}); + +router.use(authMiddleware); +router.use(requireSuperAdmin); + +router.post('/', upload.single('file'), dataImportController.importJson); + +module.exports = router; diff --git a/components/dataImport/dataImportService.js b/components/dataImport/dataImportService.js new file mode 100644 index 0000000..8fe70ea --- /dev/null +++ b/components/dataImport/dataImportService.js @@ -0,0 +1,258 @@ +// /components/dataImport/dataImportService.js +'use strict'; + +const bcrypt = require('bcryptjs'); +const Course = require('../courses/courseModel'); +const Class = require('../classes/classModel'); +const User = require('../users/userModel'); +const Role = require('../roles/roleModel'); +const AppError = require('../../utils/AppError'); +const { generateUsername, generateSimplePassword } = require('../../utils/credentials'); +const { mergeFullName, normalizeGender } = require('../../utils/userProfile'); + +const toEnglishDigits = (value) => + String(value ?? '') + .replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d)) + .replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d)); + +const normalizePhone = (raw) => { + if (raw == null || raw === '') return ''; + let digits = toEnglishDigits(raw).replace(/\D/g, ''); + if (digits.startsWith('98') && digits.length === 12) digits = `0${digits.slice(2)}`; + if (digits.length === 10 && digits.startsWith('9')) digits = `0${digits}`; + return digits; +}; + +const normalizeNationalId = (raw) => { + if (raw == null || raw === '') return ''; + return toEnglishDigits(raw).replace(/\D/g, ''); +}; + +const allocateUniqueUsername = async () => { + for (let attempt = 0; attempt < 20; attempt += 1) { + const username = generateUsername(); + if (!(await User.exists({ username }))) return username; + } + throw new AppError('INTERNAL_SERVER_ERROR', null, 'Could not generate a unique username'); +}; + +const allocatePlaceholderNationalId = async (phoneNumber) => { + const base = `TMP${(phoneNumber || '').replace(/\D/g, '').slice(-10) || Date.now().toString().slice(-10)}`; + let candidate = base.padEnd(10, '0').slice(0, 10); + let i = 0; + while (await User.exists({ nationalIdCode: candidate })) { + i += 1; + candidate = `${base.slice(0, 7)}${String(i).padStart(3, '0')}`.slice(0, 10); + } + return candidate; +}; + +const findExistingUser = async ({ nationalIdCode, phoneNumber }) => { + if (nationalIdCode) { + const byId = await User.findOne({ nationalIdCode }); + if (byId) return byId; + } + if (phoneNumber) { + const byPhone = await User.findOne({ phoneNumber }); + if (byPhone) return byPhone; + } + return null; +}; + +const applyStudentProfile = (user, student) => { + const name = mergeFullName(student.name, student.surname); + if (name) user.name = name; + + const gender = normalizeGender(student.gender); + if (gender) user.gender = gender; + + const optionalFields = [ + 'address', + 'birthCertificateNumber', + 'postalCode', + 'placeOfIssue', + 'fatherName', + 'education', + 'parentPhoneNumber' + ]; + for (const field of optionalFields) { + if (student[field] && !user[field]) { + user[field] = String(student[field]).trim(); + } + } + + if (student.birthDate && !user.birthDate) { + const date = new Date(student.birthDate); + if (!Number.isNaN(date.getTime())) user.birthDate = date; + } +}; + +const upsertStudent = async (student, userRole, stats, warnings) => { + const phoneNumber = normalizePhone(student.phoneNumber || student.phone || student.parentPhoneNumber); + let nationalIdCode = normalizeNationalId(student.nationalIdCode || student.nationalId); + + if (!phoneNumber && !nationalIdCode) { + warnings.push({ + reason: 'missing_identity', + student: { name: student.name } + }); + stats.studentsSkipped += 1; + return null; + } + + let user = await findExistingUser({ nationalIdCode, phoneNumber }); + if (user) { + applyStudentProfile(user, student); + if (phoneNumber && !user.phoneNumber) user.phoneNumber = phoneNumber; + await user.save(); + stats.studentsUpdated += 1; + return user; + } + + if (!phoneNumber) { + warnings.push({ + reason: 'missing_phone', + student: { name: student.name, nationalIdCode } + }); + stats.studentsSkipped += 1; + return null; + } + + if (!nationalIdCode) { + nationalIdCode = await allocatePlaceholderNationalId(phoneNumber); + warnings.push({ + reason: 'placeholder_national_id', + student: { name: student.name, phoneNumber, nationalIdCode } + }); + } + + // Collision: national id exists with different phone, or reverse + const conflictById = await User.findOne({ nationalIdCode }); + if (conflictById) { + applyStudentProfile(conflictById, student); + await conflictById.save(); + stats.studentsUpdated += 1; + return conflictById; + } + + const username = await allocateUniqueUsername(); + const plainPassword = generateSimplePassword(); + const passwordHash = await bcrypt.hash(plainPassword, 10); + + user = await User.create({ + name: mergeFullName(student.name, student.surname) || `کاربر ${phoneNumber}`, + gender: normalizeGender(student.gender), + nationalIdCode, + phoneNumber, + address: student.address, + birthCertificateNumber: student.birthCertificateNumber, + postalCode: student.postalCode, + placeOfIssue: student.placeOfIssue, + fatherName: student.fatherName, + birthDate: student.birthDate ? new Date(student.birthDate) : undefined, + education: student.education, + parentPhoneNumber: student.parentPhoneNumber + ? normalizePhone(student.parentPhoneNumber) + : undefined, + username, + passwordHash, + role: userRole._id, + preferredMessenger: ['SMS'] + }); + + stats.studentsCreated += 1; + return user; +}; + +const importData = async (payload) => { + if (!payload || !Array.isArray(payload.courses)) { + throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload: courses array required'); + } + + const userRole = await Role.findOne({ name: 'User' }); + if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND'); + + const stats = { + coursesCreated: 0, + coursesReused: 0, + classesCreated: 0, + classesReused: 0, + studentsCreated: 0, + studentsUpdated: 0, + studentsSkipped: 0, + enrollmentsAdded: 0 + }; + const warnings = []; + + for (const courseInput of payload.courses) { + const title = String(courseInput.title || '').trim(); + if (!title) continue; + + const type = courseInput.type === 'Private' ? 'Private' : 'General'; + let course = await Course.findOne({ title }); + if (!course) { + course = await Course.create({ + title, + type, + price: Number(courseInput.price) || 0, + showOnFrontend: false, + description: courseInput.description || '' + }); + stats.coursesCreated += 1; + } else { + stats.coursesReused += 1; + if (type === 'Private' && course.type !== 'Private') { + course.type = 'Private'; + await course.save(); + } + } + + const classes = Array.isArray(courseInput.classes) ? courseInput.classes : []; + for (const classInput of classes) { + const className = String(classInput.name || '').trim(); + if (!className) continue; + + let cls = await Class.findOne({ name: className, course: course._id }); + if (!cls) { + cls = await Class.create({ + name: className, + course: course._id, + startDate: classInput.startDate ? new Date(classInput.startDate) : undefined, + tuitionFee: Number(classInput.tuitionFee) || Number(courseInput.price) || 0, + isActive: true + }); + stats.classesCreated += 1; + } else { + stats.classesReused += 1; + if (!cls.startDate && classInput.startDate) { + cls.startDate = new Date(classInput.startDate); + await cls.save(); + } + } + + const students = Array.isArray(classInput.students) ? classInput.students : []; + for (const student of students) { + const user = await upsertStudent(student, userRole, stats, warnings); + if (!user) continue; + + if (!user.courses.map(String).includes(String(course._id))) { + user.courses.push(course._id); + await user.save(); + } + + if (!cls.students.map(String).includes(String(user._id))) { + cls.students.push(user._id); + stats.enrollmentsAdded += 1; + } + } + + await cls.save(); + } + } + + return { stats, warnings }; +}; + +module.exports = { + importData +}; diff --git a/components/notifications/notificationService.js b/components/notifications/notificationService.js index a82da31..f3198bf 100644 --- a/components/notifications/notificationService.js +++ b/components/notifications/notificationService.js @@ -37,7 +37,7 @@ const createNotification = async (data) => { }; const getNotificationById = async (id) => { - const notification = await Notification.findById(id).populate('user', 'name surname username email phoneNumber'); + const notification = await Notification.findById(id).populate('user', 'name username email phoneNumber'); if (!notification) { throw new AppError('NOTIFICATION_NOT_FOUND'); } @@ -49,7 +49,7 @@ const getAllNotifications = async (queryParams) => { const filter = buildFilterQuery(queryParams, ['subject', 'body']); const [notifications, totalCount] = await Promise.all([ - Notification.find(filter).populate('user', 'name surname username').sort(sort).skip(skip).limit(limit), + Notification.find(filter).populate('user', 'name username').sort(sort).skip(skip).limit(limit), Notification.countDocuments(filter) ]); diff --git a/components/payments/paymentService.js b/components/payments/paymentService.js index 2933d60..73d14c5 100644 --- a/components/payments/paymentService.js +++ b/components/payments/paymentService.js @@ -18,7 +18,7 @@ const getAllPayments = async (query) => { const [items, total] = await Promise.all([ Payment.find(filter) - .populate({ path: 'user', select: 'name surname' }) + .populate({ path: 'user', select: 'name' }) .populate({ path: 'classes', select: 'name' }) .populate({ path: 'course', select: 'title' }) .skip(skip).limit(limit).sort({ createdAt: -1 }).lean(), @@ -30,7 +30,7 @@ const getAllPayments = async (query) => { const getPaymentById = async (id) => { const payment = await Payment.findById(id) - .populate({ path: 'user', select: 'name surname phoneNumber' }) + .populate({ path: 'user', select: 'name phoneNumber' }) .populate({ path: 'classes', select: 'name tuitionFee' }) .populate({ path: 'course', select: 'title price' }) .lean(); diff --git a/components/sessions/sessionService.js b/components/sessions/sessionService.js index 0f9c92d..e997016 100644 --- a/components/sessions/sessionService.js +++ b/components/sessions/sessionService.js @@ -85,10 +85,10 @@ const getSessionById = async (id) => { .populate({ path: 'class', select: 'name students capacity', - populate: { path: 'students', select: 'name surname nationalIdCode phoneNumber' } + populate: { path: 'students', select: 'name nationalIdCode phoneNumber gender' } }) .populate('professor', 'name surname email phoneNumber') - .populate('attendanceList.user', 'name surname username nationalIdCode'); + .populate('attendanceList.user', 'name username nationalIdCode'); if (!session) { throw new AppError('SESSION_NOT_FOUND'); } diff --git a/components/users/userModel.js b/components/users/userModel.js index 3cf06ba..77689aa 100644 --- a/components/users/userModel.js +++ b/components/users/userModel.js @@ -21,9 +21,9 @@ const userSchema = new mongoose.Schema({ required: true, trim: true }, - surname: { + gender: { type: String, - required: true, + enum: ['male', 'female'], trim: true }, phoneNumber: { @@ -51,6 +51,33 @@ const userSchema = new mongoose.Schema({ type: String, trim: true }, + birthCertificateNumber: { + type: String, + trim: true + }, + postalCode: { + type: String, + trim: true + }, + placeOfIssue: { + type: String, + trim: true + }, + fatherName: { + type: String, + trim: true + }, + birthDate: { + type: Date + }, + education: { + type: String, + trim: true + }, + parentPhoneNumber: { + type: String, + trim: true + }, username: { type: String, required: true, diff --git a/components/users/userService.js b/components/users/userService.js index 10f3e0e..8f9050c 100644 --- a/components/users/userService.js +++ b/components/users/userService.js @@ -9,6 +9,7 @@ const { calculateMeta } = require('../../utils/pagination'); const { generateUsername, generateSimplePassword } = require('../../utils/credentials'); const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages'); const logger = require('../../utils/logger'); +const { mergeFullName, normalizeGender } = require('../../utils/userProfile'); const POPULATE_ROLE = { path: 'role', select: 'name permissions' }; const SAFE_FIELDS = '-passwordHash -refreshTokens'; @@ -37,23 +38,46 @@ const allocateUniqueUsername = async (preferred) => { throw new AppError('INTERNAL_SERVER_ERROR', null, 'Could not generate a unique username'); }; +const pickProfileFields = (body = {}) => { + const fullName = mergeFullName(body.name, body.surname); + const fields = { + name: fullName || undefined, + gender: normalizeGender(body.gender), + nationalIdCode: body.nationalIdCode || body.nationalId || undefined, + phoneNumber: body.phoneNumber || body.phone || undefined, + email: body.email, + address: body.address, + birthCertificateNumber: body.birthCertificateNumber, + postalCode: body.postalCode, + placeOfIssue: body.placeOfIssue, + fatherName: body.fatherName, + birthDate: body.birthDate || undefined, + education: body.education, + parentPhoneNumber: body.parentPhoneNumber, + preferredMessenger: normalizePreferredMessengers(body.preferredMessenger) + }; + + Object.keys(fields).forEach((key) => { + if (fields[key] === '' || fields[key] === null || fields[key] === undefined) { + delete fields[key]; + } + }); + + return fields; +}; + const signUp = async (body) => { - const { name, surname, nationalId, nationalIdCode, phoneNumber, phone, username, password, email, address, preferredMessenger } = body; - + const profile = pickProfileFields(body); + const { username, password } = body; + const userRole = await Role.findOne({ name: 'User' }); if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND'); const passwordHash = await bcrypt.hash(password, 10); const user = await User.create({ - name, - surname, - nationalIdCode: nationalIdCode || nationalId, - phoneNumber: phoneNumber || phone, + ...profile, username, passwordHash, - email, - address, - preferredMessenger: normalizePreferredMessengers(preferredMessenger), role: userRole._id }); @@ -75,8 +99,9 @@ const getAllUsers = async (query) => { if (query.search) { filter.$or = [ { name: new RegExp(query.search, 'i') }, - { surname: new RegExp(query.search, 'i') }, - { username: new RegExp(query.search, 'i') } + { username: new RegExp(query.search, 'i') }, + { nationalIdCode: new RegExp(query.search, 'i') }, + { phoneNumber: new RegExp(query.search, 'i') } ]; } if (query.isActive !== undefined) filter.isActive = query.isActive === 'true'; @@ -92,20 +117,12 @@ const getAllUsers = async (query) => { const searchUsers = async (query) => getAllUsers(query); const createUserAdmin = async (body) => { + const profile = pickProfileFields(body); const { - name, - surname, - nationalIdCode, - nationalId, - phoneNumber, - phone, username: requestedUsername, password: requestedPassword, - email, roleId, - role, - address, - preferredMessenger, + role } = body; let roleObj = null; @@ -120,25 +137,18 @@ const createUserAdmin = async (body) => { const plainPassword = (requestedPassword && String(requestedPassword).trim()) || generateSimplePassword(); const username = await allocateUniqueUsername(requestedUsername); const passwordHash = await bcrypt.hash(plainPassword, 10); - const resolvedPhone = phoneNumber || phone; const user = await User.create({ - name, - surname, - nationalIdCode: nationalIdCode || nationalId, - phoneNumber: resolvedPhone, + ...profile, username, passwordHash, - email, - address, - preferredMessenger: normalizePreferredMessengers(preferredMessenger), - role: roleObj._id, + role: roleObj._id }); try { - await sendAccountCreatedSms(resolvedPhone, username, plainPassword); + await sendAccountCreatedSms(profile.phoneNumber, username, plainPassword); } catch (err) { - logger.error(`[createUserAdmin] Account SMS failed for ${resolvedPhone}: ${err.message}`); + logger.error(`[createUserAdmin] Account SMS failed for ${profile.phoneNumber}: ${err.message}`); } const created = await User.findById(user._id).select(SAFE_FIELDS).populate(POPULATE_ROLE).lean(); @@ -146,8 +156,8 @@ const createUserAdmin = async (body) => { ...created, generatedCredentials: { username, - password: plainPassword, - }, + password: plainPassword + } }; }; @@ -164,6 +174,9 @@ const updateUser = async (id, body) => { passwordHash, refreshTokens, preferredMessenger, + surname, + name, + gender, _id, id: bodyId, createdAt, @@ -173,8 +186,12 @@ const updateUser = async (id, body) => { } = body; const update = { ...rest }; + const mergedName = mergeFullName(name, surname); + if (mergedName) update.name = mergedName; + + const normalizedGender = normalizeGender(gender); + if (normalizedGender) update.gender = normalizedGender; - // Map frontend field aliases to schema fields if (nationalIdCode || nationalId) { update.nationalIdCode = nationalIdCode || nationalId; } @@ -188,12 +205,13 @@ const updateUser = async (id, body) => { update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || []; } - // Never overwrite username with an empty value on update if (typeof username === 'string' && username.trim()) { update.username = username.trim(); } - // Strip empty strings so required validators are not tripped + // Legacy field — drop if clients still send it + delete update.surname; + Object.keys(update).forEach((key) => { if (key === 'preferredMessenger') return; if (update[key] === '' || update[key] === null || update[key] === undefined) { @@ -218,7 +236,7 @@ const deleteUser = async (id) => { if (!user) throw new AppError('USER_NOT_FOUND'); }; -const enrollUserInCourse = async (userId, courseId, actorId) => { +const enrollUserInCourse = async (userId, courseId) => { const user = await User.findById(userId); if (!user) throw new AppError('USER_NOT_FOUND'); @@ -229,4 +247,13 @@ const enrollUserInCourse = async (userId, courseId, actorId) => { return User.findById(userId).select(SAFE_FIELDS).populate('courses').lean(); }; -module.exports = { signUp, getUserById, getAllUsers, searchUsers, createUserAdmin, updateUser, deleteUser, enrollUserInCourse }; +module.exports = { + signUp, + getUserById, + getAllUsers, + searchUsers, + createUserAdmin, + updateUser, + deleteUser, + enrollUserInCourse +}; diff --git a/data/import-data.json b/data/import-data.json new file mode 100644 index 0000000..07d35bc --- /dev/null +++ b/data/import-data.json @@ -0,0 +1,1383 @@ +{ + "version": 1, + "generatedAt": "2026-08-14T21:57:17.784Z", + "source": "برنامه آموزشی 1405", + "summary": { + "courses": 11, + "classes": 14, + "students": 123 + }, + "courses": [ + { + "title": "نظام مهندسی", + "type": "General", + "price": 0, + "classes": [ + { + "name": "نظام مهندسی اسفند 1404", + "folderName": "1- نظام مهندسی اسفند 1404", + "startDate": "2026-02-20", + "jalaliYear": 1404, + "jalaliMonth": 12, + "sourceFile": "نظام مهندسی اسفند 1404.xlsx", + "students": [ + { + "name": "رضا بوستانی", + "phoneNumber": "09166063364", + "nationalIdCode": "1881840409", + "placeOfIssue": "شوشتر", + "fatherName": "عبدالرضا", + "birthDate": "1984-09-16", + "birthDateJalali": "1363/06/25" + }, + { + "name": "محمد صفی پور شاه منصوری", + "phoneNumber": "09167182501", + "placeOfIssue": "مسجد سلیمان", + "fatherName": "حسین", + "birthDate": "2000-01-19", + "birthDateJalali": "1378/10/29" + }, + { + "name": "کیمیا بهادری", + "phoneNumber": "09372230316", + "nationalIdCode": "1743072465", + "placeOfIssue": "اهواز", + "fatherName": "علی", + "birthDate": "1999-11-16", + "birthDateJalali": "1378/08/25" + }, + { + "name": "رادکیا", + "phoneNumber": "09360326906" + }, + { + "name": "بهنام شریفات", + "phoneNumber": "09168536533", + "nationalIdCode": "1940620880", + "postalCode": "6136685878", + "placeOfIssue": "ماهشهر", + "fatherName": "جاسم", + "birthDate": "1999-01-02", + "birthDateJalali": "1377/10/12", + "education": "کارشناسی عمران", + "address": "اهوز گلستان خیابان شیراز شرقی ساختمان باران" + }, + { + "name": "محمد حسین سمیعی", + "phoneNumber": "09058983255", + "nationalIdCode": "1743012225", + "placeOfIssue": "اهواز", + "fatherName": "علی", + "birthDate": "1999-08-17", + "birthDateJalali": "1378/05/26" + }, + { + "name": "نگین مقدس", + "phoneNumber": "09306114785", + "nationalIdCode": "1743057903", + "placeOfIssue": "اهواز", + "fatherName": "داریوش", + "birthDate": "1999-10-14", + "birthDateJalali": "1378/07/22" + }, + { + "name": "سعید آرین راد", + "phoneNumber": "09166016551", + "nationalIdCode": "1971851450", + "placeOfIssue": "مسجد سلیمان", + "fatherName": "عباس", + "birthDate": "1964-11-29", + "birthDateJalali": "1343/09/08" + }, + { + "name": "احسان طالبی شوشتریان", + "phoneNumber": "09333142609", + "nationalIdCode": "1740999959", + "fatherName": "احمد", + "birthDate": "1992-03-26", + "birthDateJalali": "1371/01/06" + }, + { + "name": "رسول علائی شیخ رباط", + "phoneNumber": "09160022281", + "nationalIdCode": "5550073889", + "placeOfIssue": "کوهرنگ", + "fatherName": "هاجی علی", + "birthDate": "1993-04-30", + "birthDateJalali": "1372/02/10", + "education": "کارشناسی عمران", + "address": "اهواز، فاز 2 پاداد خیابان بهاران 8" + }, + { + "name": "رضوان شهری", + "phoneNumber": "09039667892" + }, + { + "name": "محمد جواد خوش آیند", + "phoneNumber": "09055699072", + "nationalIdCode": "174347869", + "placeOfIssue": "اهواز", + "fatherName": "محمود رضا", + "birthDate": "2001-06-16", + "birthDateJalali": "1380/03/26" + }, + { + "name": "ستایش باقری وانانی", + "phoneNumber": "09168364327" + }, + { + "name": "روژین نعیمی", + "phoneNumber": "09337575653", + "nationalIdCode": "1743135130", + "placeOfIssue": "اهواز", + "fatherName": "بیژن", + "birthDate": "2000-02-14", + "birthDateJalali": "1378/11/25" + }, + { + "name": "علی محمد عسگری", + "phoneNumber": "09166320903" + }, + { + "name": "شیما قلمباز", + "phoneNumber": "09337803317" + }, + { + "name": "الهام خیاط", + "phoneNumber": "09006214443", + "nationalIdCode": "1882234421", + "placeOfIssue": "شوشتر", + "fatherName": "محمد", + "birthDate": "1986-12-31", + "birthDateJalali": "1365/10/10" + }, + { + "name": "سامان خرم نیا", + "phoneNumber": "09120164609", + "nationalIdCode": "1742371248", + "fatherName": "مسعود", + "birthDate": "1997-01-21", + "birthDateJalali": "1375/11/02" + } + ] + }, + { + "name": "نظام مهندسی خرداد 1405", + "folderName": "7- نظام مهندسی خرداد 1405", + "startDate": "2026-05-22", + "jalaliYear": 1405, + "jalaliMonth": 3, + "sourceFile": "نظام مهندسی خرداد 1405.xlsx", + "students": [ + { + "name": "نسترن بگدلی", + "phoneNumber": "09168333647", + "nationalIdCode": "1742506811", + "placeOfIssue": "اهواز", + "fatherName": "کهزاد", + "birthDate": "1997-09-02", + "birthDateJalali": "1376/06/11" + }, + { + "name": "میلاد مهدیه", + "phoneNumber": "09034440804", + "nationalIdCode": "1740706447", + "placeOfIssue": "اهواز", + "fatherName": "عبدالهادی", + "birthDate": "1989-09-19", + "birthDateJalali": "1368/6/28" + }, + { + "name": "شیما قلمباز", + "phoneNumber": "09337803317", + "nationalIdCode": "1820311007", + "placeOfIssue": "اهواز", + "fatherName": "مجید", + "birthDate": "1998-04-03", + "birthDateJalali": "1377/1/14" + }, + { + "name": "غزال لوید", + "phoneNumber": "09355705721", + "nationalIdCode": "1741974895", + "postalCode": "6155945586", + "placeOfIssue": "اهواز", + "fatherName": "محمد رضا", + "birthDate": "1995-07-21", + "birthDateJalali": "1374/4/30" + }, + { + "name": "سید منصور فواضلی", + "phoneNumber": "09059852944", + "postalCode": "6145783439", + "placeOfIssue": "اهواز", + "fatherName": "سید فاضل", + "birthDate": "1988-09-23", + "birthDateJalali": "1367/7/1" + }, + { + "name": "وفا کعبی راد", + "phoneNumber": "09166177901", + "nationalIdCode": "1741050928", + "placeOfIssue": "اهواز", + "fatherName": "عبدالهادی", + "birthDate": "1992-02-25", + "birthDateJalali": "1370/12/6" + }, + { + "name": "محمد رضا کوتی", + "phoneNumber": "09163757925", + "nationalIdCode": "6800047422", + "placeOfIssue": "هویزه", + "fatherName": "خالد", + "birthDate": "1999-04-25", + "birthDateJalali": "1378/02/05" + }, + { + "name": "سامان خرم نیا", + "phoneNumber": "09120164609", + "nationalIdCode": "1742371248", + "fatherName": "مسعود", + "birthDate": "1997-01-21", + "birthDateJalali": "1375/11/02" + }, + { + "name": "زهرا شریفات", + "phoneNumber": "09393437557", + "nationalIdCode": "1900350017", + "placeOfIssue": "رامهرمز", + "fatherName": "مسلم", + "birthDate": "1996-05-18", + "birthDateJalali": "1375/02/29" + }, + { + "name": "کوثر مهاوی", + "phoneNumber": "09014759854", + "nationalIdCode": "1742239821", + "placeOfIssue": "اهواز", + "fatherName": "قیس", + "birthDate": "1996-08-04", + "birthDateJalali": "1375/05/14" + }, + { + "name": "ندا بحری مفرد", + "phoneNumber": "09391600979", + "nationalIdCode": "1757571582", + "placeOfIssue": "اهواز", + "fatherName": "بهرام", + "birthDate": "1985-09-17", + "birthDateJalali": "1364/06/26" + }, + { + "name": "سمیعی" + } + ] + } + ] + }, + { + "title": "اتوکد", + "type": "General", + "price": 0, + "classes": [ + { + "name": "اتوکد تیر 1405", + "folderName": "10- اتوکد تیر 1405", + "startDate": "2026-06-22", + "jalaliYear": 1405, + "jalaliMonth": 4, + "sourceFile": "10- اتوکد تیر 1405.xlsx", + "students": [ + { + "name": "فائزه کیان پور", + "phoneNumber": "09399641468", + "nationalIdCode": "1743769288", + "postalCode": "6154831904", + "placeOfIssue": "اهواز", + "fatherName": "سهراب", + "birthDate": "2002-09-22", + "birthDateJalali": "1381/06/31", + "education": "لیسانس معماری داخلی", + "address": "کیانشهر منازل الحدید خ الحدید2 مجتمع عدالت 14 طبقه 3 واحد 3" + }, + { + "name": "محمد قاسم مهدی پورده مرداس", + "phoneNumber": "09302039558", + "nationalIdCode": "1745897119", + "postalCode": "6153697319", + "placeOfIssue": "اهواز", + "fatherName": "علی", + "birthDate": "2009-10-22", + "birthDateJalali": "1388/07/30", + "education": "ریاضی فیزیک", + "address": "کمپلوی شمالی خیابان اعتصامی بین امیرکبیر و قصر شیرین" + }, + { + "name": "مریم پیرزاده سیبکی", + "phoneNumber": "09017182871", + "nationalIdCode": "1745781854", + "postalCode": "6176845511", + "placeOfIssue": "اهواز", + "fatherName": "شاپور", + "birthDate": "2009-07-06", + "birthDateJalali": "1388/04/15", + "education": "یازدهم معماری داخلی", + "address": "باهنر بلوار سبلان شمالی ساختمان خلیج فارس" + }, + { + "name": "سامان چکی", + "phoneNumber": "09102232554", + "nationalIdCode": "1743610211", + "postalCode": "6154833659", + "placeOfIssue": "اهواز", + "fatherName": "منصور", + "birthDate": "2002-02-02", + "birthDateJalali": "1380/11/13", + "education": "دیپلم مهندسی مکانیک", + "address": "کیانشهر خیابان شجاعت کوچه 5 الحدید پلاک 132" + }, + { + "name": "هستی حداد مزرعه", + "phoneNumber": "09017277390", + "nationalIdCode": "1810994489", + "placeOfIssue": "آبادان", + "fatherName": "عبدالرضا", + "birthDate": "2008-10-01", + "birthDateJalali": "1387/7/10", + "education": "معماری داخلی", + "address": "رسالت 8" + }, + { + "name": "الهه هاشمی", + "phoneNumber": "09024604383", + "nationalIdCode": "1745401458", + "postalCode": "6156655461", + "placeOfIssue": "اهواز", + "fatherName": "قادر", + "birthDate": "2008-05-13", + "birthDateJalali": "1387/2/24", + "education": "دیپلم نقشه کشی", + "address": "سید خلف اروند اصلی بین نهضت و عصمت" + }, + { + "name": "ایناس شریفات", + "phoneNumber": "09051745585", + "nationalIdCode": "1745760040", + "postalCode": "6134986843", + "placeOfIssue": "اهواز", + "fatherName": "فاخر", + "birthDate": "2009-06-18", + "birthDateJalali": "1388/3/28", + "education": "یازدهم معماری داخلی", + "address": "سعدی فیروزه غربی مجتمع زرین 2" + }, + { + "name": "حدیث عبیات", + "phoneNumber": "09382262778", + "nationalIdCode": "1980686580", + "postalCode": "6139750035", + "placeOfIssue": "دشت آزادگان", + "fatherName": "خالد", + "birthDate": "2008-12-27", + "birthDateJalali": "1387/10/07", + "education": "یازدهم معماری داخلی", + "address": "پردیس خیابان هسته 5 مجتمع لادن 3" + }, + { + "name": "آذین علی زاده راهداری", + "phoneNumber": "09391062056", + "nationalIdCode": "1745676295", + "postalCode": "6163854347", + "placeOfIssue": "اهواز", + "fatherName": "عسکر", + "birthDate": "2009-02-21", + "birthDateJalali": "1387/12/03", + "education": "معماری داخلی", + "address": "زیتون کارمندی خ صالحپور نبش زمرد مجتمع میلاد" + }, + { + "name": "اسرا غافلی عبدی", + "phoneNumber": "09394885833", + "nationalIdCode": "1745674179", + "postalCode": "6138817172", + "placeOfIssue": "اهواز", + "fatherName": "ناصر", + "birthDate": "2009-02-25", + "birthDateJalali": "1387/12/7", + "education": "معماری داخلی", + "address": "اتوبان گلستان شهرک پیام خیابان رضوان یک پلاک 32" + }, + { + "name": "زینب سیلاوی", + "phoneNumber": "09304710307", + "nationalIdCode": "1745772979", + "postalCode": "6139795219", + "placeOfIssue": "اهواز", + "fatherName": "مجتبی", + "birthDate": "2009-06-27", + "birthDateJalali": "1388/4/6" + } + ] + }, + { + "name": "اتوکد فروردین 1405", + "folderName": "2- اتوکد فروردین 1405", + "startDate": "2026-03-21", + "jalaliYear": 1405, + "jalaliMonth": 1, + "sourceFile": "اتوکد فروردین 1405.xlsx", + "students": [ + { + "name": "ایاد پورمزعل", + "phoneNumber": "09991082207", + "nationalIdCode": "1744116458", + "placeOfIssue": "اهواز", + "fatherName": "احمد", + "birthDate": "2004-01-11", + "birthDateJalali": "1382/10/21", + "education": "دانشجو کارشناسی معماری داخلی", + "address": "مهرشهر اهواز" + }, + { + "name": "زهرا بهرمان", + "phoneNumber": "09052067209", + "nationalIdCode": "1744040494", + "placeOfIssue": "اهواز", + "fatherName": "وحید", + "birthDate": "2003-10-02", + "birthDateJalali": "1382/07/10", + "education": "دانشجو معماری", + "address": "216 واحدی ملی حفاری بلوک 24" + }, + { + "name": "احسان برزکار", + "phoneNumber": "09165297436", + "nationalIdCode": "4819943375", + "placeOfIssue": "باغملک", + "fatherName": "بهمن", + "birthDate": "1988-08-23", + "birthDateJalali": "1367/06/01", + "education": "کارشناسی ارشد روانشناسی", + "address": "رامهرمز خیابان دهقان کوچه شهید غلام" + }, + { + "name": "شایان بندانی", + "phoneNumber": "09395647415", + "nationalIdCode": "1830592262", + "postalCode": "6177983801", + "placeOfIssue": "ایذه", + "fatherName": "سیاوش", + "birthDate": "1999-11-18", + "birthDateJalali": "1378/8/27", + "education": "کارشناسی عمران", + "address": "اهواز نبوت خیابان مطهر 1" + }, + { + "name": "شقایق فرفنی کاکش", + "phoneNumber": "09001656020", + "nationalIdCode": "1742692540", + "placeOfIssue": "اهواز", + "fatherName": "مهران", + "birthDate": "1998-07-05", + "birthDateJalali": "1377/04/14", + "education": "لیسانس مدیریت بازرگانی", + "address": "کوی ملت، 20 متری مقیمی بین خیابان 5 و 7 اقبال" + }, + { + "name": "ناهید طرفی حمدی", + "phoneNumber": "09375368689", + "nationalIdCode": "1980126054", + "postalCode": "6153843851", + "placeOfIssue": "دشت آزادگان", + "fatherName": "عبدالزهرا", + "birthDate": "1990-08-23", + "birthDateJalali": "1369/06/01", + "education": "دیپلم علوم انسانی", + "address": "غزنوی شالی خیابان بینا مجتمع میعاد 2" + }, + { + "name": "هاجر طرفی", + "phoneNumber": "09391247990", + "nationalIdCode": "1742317073", + "postalCode": "6153843851", + "placeOfIssue": "اهواز", + "fatherName": "عبدالزهرا", + "birthDate": "1995-06-05", + "birthDateJalali": "1374/3/15", + "education": "فوق دیپلم معماری", + "address": "غزنوی شالی خیابان بینا مجتمع میعاد 2" + } + ] + } + ] + }, + { + "title": "تکسا", + "type": "General", + "price": 0, + "classes": [ + { + "name": "تکسا تیر 1405", + "folderName": "11- تکسا تیر 1405", + "startDate": "2026-06-22", + "jalaliYear": 1405, + "jalaliMonth": 4, + "sourceFile": "11- تکسا تیر 1405.xlsx", + "students": [ + { + "name": "امین رفیعی", + "phoneNumber": "09168942141", + "nationalIdCode": "1911204610", + "birthCertificateNumber": "1747", + "postalCode": "4381614168", + "placeOfIssue": "رامهرمز", + "fatherName": "محمد رضا", + "birthDate": "1984-01-31", + "birthDateJalali": "1362/11/11", + "education": "کارشناسی", + "address": "رامهرمز، خ امت کوچه شهید قنواتی پور" + }, + { + "name": "نعمت الله علی پور", + "phoneNumber": "09167109828", + "nationalIdCode": "1841534730", + "placeOfIssue": "ایذه", + "fatherName": "امان الله", + "birthDate": "1973-05-15", + "birthDateJalali": "1352/02/25", + "education": "لیسانس", + "address": "کوی باهنر" + }, + { + "name": "مریم پناهیان", + "phoneNumber": "09385952552", + "nationalIdCode": "1882444914", + "postalCode": "6164694781", + "fatherName": "داراب", + "birthDate": "1980-05-07", + "birthDateJalali": "1359/02/17", + "education": "کارشناسی IT", + "address": "کوروش فاز 3 خ 10" + }, + { + "name": "سعید جنادله", + "phoneNumber": "09167048150", + "nationalIdCode": "1742375022", + "postalCode": "6153863813", + "placeOfIssue": "اهواز", + "fatherName": "فیصل", + "birthDate": "1997-02-19", + "birthDateJalali": "1375/12/01", + "education": "کارشناسی عمران", + "address": "کمپلو شمالی خ ایثار بین برهان و انقلاب پلاک 9" + }, + { + "name": "مجتبی لوری", + "phoneNumber": "09166211602", + "nationalIdCode": "1756822816", + "postalCode": "6153718789", + "placeOfIssue": "اهواز", + "fatherName": "غلامرضا", + "birthDate": "1986-05-16", + "birthDateJalali": "1365/02/26", + "education": "کارشناسی عمران", + "address": "کمپلو شمالی خ انقلاب" + }, + { + "name": "سپیده دیناروند", + "phoneNumber": "09333011328", + "nationalIdCode": "5260242831", + "postalCode": "6153783644", + "placeOfIssue": "شوش", + "fatherName": "علی عباس", + "birthDate": "1995-04-25", + "birthDateJalali": "1374/2/5", + "education": "لیسانس زبان", + "address": "اهواز، لشکر خیابان زکی زاده" + }, + { + "name": "احمد هلیچی", + "phoneNumber": "09965212005", + "nationalIdCode": "1910166839", + "birthCertificateNumber": "16698", + "postalCode": "6183978137", + "fatherName": "عباس", + "birthDate": "1981-09-21", + "birthDateJalali": "1360/6/30", + "education": "ارشد علوم سیاسی", + "address": "پاداد 14 غربی" + }, + { + "name": "مقداد عامری فر", + "phoneNumber": "09163037954", + "nationalIdCode": "1753693950", + "postalCode": "6164811836", + "placeOfIssue": "اهواز", + "fatherName": "عبدالرضا", + "birthDate": "1981-02-15", + "birthDateJalali": "1359/11/26", + "education": "فوق لیسانس عمران", + "address": "کوی ملت خ عامری خ 10 مجتمع رایان" + }, + { + "name": "آقای رضایی", + "phoneNumber": "09910507844" + }, + { + "name": "اقای جلیلی", + "phoneNumber": "09165399986" + } + ] + } + ] + }, + { + "title": "هوش مصنوعی", + "type": "General", + "price": 0, + "classes": [ + { + "name": "هوش مصنوعی تیر 1405", + "folderName": "12- هوش مصنوعی تیر 1405", + "startDate": "2026-06-22", + "jalaliYear": 1405, + "jalaliMonth": 4, + "sourceFile": "12- هوش مصنوعی تیر 1405.xlsx", + "students": [ + { + "name": "فرهود فروزش", + "phoneNumber": "09167806952", + "nationalIdCode": "1746631430", + "placeOfIssue": "اهواز", + "fatherName": "فرهنگ", + "birthDate": "2011-10-20", + "birthDateJalali": "1390/7/28", + "education": "پایه نهم", + "address": "کیانپارس خ 11 غربی فاز یک پلاک 90 مجتمع مینیاتور", + "parentPhoneNumber": "09166151874" + }, + { + "name": "رستان محمدیان", + "phoneNumber": "09378451704", + "nationalIdCode": "1747124834", + "placeOfIssue": "اهواز", + "fatherName": "رستم", + "birthDate": "2013-01-23", + "birthDateJalali": "1391/11/4", + "education": "پایه هشتم", + "address": "دانشگاه شهید چمران کوی استادان", + "parentPhoneNumber": "09168105566" + }, + { + "name": "امیر علی خنیاب نژاد", + "phoneNumber": "09162223810", + "nationalIdCode": "1747073350", + "placeOfIssue": "اهواز", + "fatherName": "صادق", + "birthDate": "2012-12-09", + "birthDateJalali": "1391/9/19", + "education": "پایه هفتم", + "address": "گلستان شهریور بین بهمن و اسفند شکوه 5 طبقه 3 واحد7", + "parentPhoneNumber": "09168105566" + }, + { + "name": "توحید قربانی", + "phoneNumber": "09169001358", + "placeOfIssue": "اهواز", + "fatherName": "احمد", + "birthDate": "2012-10-30", + "birthDateJalali": "1391/8/9", + "education": "پایه هفتم", + "address": "زیتون کارمندی خ هایت بین زیتون و زیبا", + "parentPhoneNumber": "09392942784" + }, + { + "name": "محمد طاها ابراهیمی", + "phoneNumber": "09168201132", + "nationalIdCode": "1746908432", + "placeOfIssue": "اهواز", + "fatherName": "محمد رضا", + "birthDate": "2012-07-22", + "birthDateJalali": "1391/5/1", + "education": "پایه هشتم", + "address": "کوی نفت ارشاد 8 پلاک 66" + }, + { + "name": "محمد کرم زاده", + "phoneNumber": "09169012093", + "placeOfIssue": "اهواز", + "fatherName": "آرش", + "birthDate": "2011-07-04", + "birthDateJalali": "1390/4/13", + "education": "پایه دهم", + "address": "شهرک نفت منازل آپارتمانی بلوک 14" + }, + { + "name": "محمد حسن جولان زاده", + "phoneNumber": "09167759131", + "fatherName": "علی", + "birthDate": "2012-10-26", + "birthDateJalali": "1391/8/5", + "education": "پایه هشتم", + "address": "کیانشهر بلوار امام رضا نبش رز هفت", + "parentPhoneNumber": "09030137655" + }, + { + "name": "امیر محمد بیرانوند", + "phoneNumber": "09217327371", + "nationalIdCode": "1747008354", + "placeOfIssue": "اهواز", + "fatherName": "ولی", + "birthDate": "2012-10-04", + "birthDateJalali": "1391/7/13", + "education": "پایه هشتم", + "address": "ملیراه خیابان پرستو شمالی مجتمع آریا 2 واحد 5", + "parentPhoneNumber": "09166608990" + }, + { + "name": "پویا تاجمیری", + "phoneNumber": "09169012484", + "nationalIdCode": "1746799145", + "fatherName": "پژمان", + "birthDate": "2012-03-21", + "birthDateJalali": "1391/1/2", + "education": "پایه هشتم", + "address": "کیان آباد خ 25 غربی پلاک 87", + "parentPhoneNumber": "09161117325" + }, + { + "name": "آراد عسکری", + "phoneNumber": "09163044210", + "placeOfIssue": "اهواز", + "fatherName": "عزیز", + "birthDateJalali": "1394", + "parentPhoneNumber": "09046115653" + }, + { + "name": "متین پام", + "phoneNumber": "916644949", + "nationalIdCode": "1747018880", + "placeOfIssue": "اهواز", + "fatherName": "محمد باقر", + "birthDate": "2002-11-03", + "birthDateJalali": "1381/8/12", + "education": "پایه هشتم", + "parentPhoneNumber": "09169412064" + }, + { + "name": "علیرضا داغله", + "phoneNumber": "09369718778", + "nationalIdCode": "1747178322", + "placeOfIssue": "اهواز", + "fatherName": "نوروز", + "birthDate": "2013-03-25", + "birthDateJalali": "1392/1/5", + "education": "پایه هفتم", + "address": "لشکرآباد منازل مسکونی غدیر" + } + ] + } + ] + }, + { + "title": "ICDL", + "type": "General", + "price": 0, + "classes": [ + { + "name": "ICDL تیر 1405", + "folderName": "13- ICDL تیر 1405", + "startDate": "2026-06-22", + "jalaliYear": 1405, + "jalaliMonth": 4, + "sourceFile": "13- ICDL تیر 1405.xlsx", + "students": [ + { + "name": "سمانه حسینوند", + "phoneNumber": "09034749982", + "nationalIdCode": "1960561723", + "postalCode": "61544854859", + "fatherName": "نور محمد", + "birthDate": "1998-10-02", + "birthDateJalali": "1377/07/10", + "education": "لیسانس پرستاری", + "address": "کیانشهر بلوار امام رضا خ 12" + }, + { + "name": "سارینا عبداللهی", + "phoneNumber": "09379441036", + "nationalIdCode": "7060055576", + "postalCode": "6154683976", + "placeOfIssue": "دورود", + "fatherName": "داود", + "birthDate": "2009-10-27", + "birthDateJalali": "1388/08/05", + "education": "حسابداری", + "address": "مهرشهر فاز 5 جنب منازل شرکت نفت مجتمع جام" + }, + { + "name": "هدیه ابوالقاسمی", + "phoneNumber": "09166009316", + "nationalIdCode": "1743398441", + "postalCode": "6155655379", + "placeOfIssue": "اهواز", + "fatherName": "امیر حسین", + "birthDate": "2001-03-23", + "birthDateJalali": "1380/01/03", + "education": "کارشناسی روانشناسی", + "address": "کیان آباد خ 3 غربی پلاک 144 ویلایی" + }, + { + "name": "سید نیما ناصرپور", + "phoneNumber": "09166007944", + "nationalIdCode": "1757060545", + "fatherName": "سید نعمت اله", + "birthDate": "1988-06-18", + "birthDateJalali": "1367/03/28" + }, + { + "name": "مائده دغلاوی", + "phoneNumber": "09363488104", + "nationalIdCode": "1746040742", + "postalCode": "6145687635", + "placeOfIssue": "اهواز", + "fatherName": "علی", + "birthDate": "2010-03-27", + "birthDateJalali": "1389/01/07", + "education": "دهم حسابداری", + "address": "کوی علوی خ ابو مسلم پلاک 49" + }, + { + "name": "بهار کریمی", + "phoneNumber": "09389006468", + "nationalIdCode": "1744476098", + "postalCode": "6173983661", + "placeOfIssue": "اهواز", + "fatherName": "محمد علی", + "birthDate": "2005-05-03", + "birthDateJalali": "1384/2/13", + "education": "دیپلم حقوق", + "address": "نیوساید خ شهریور نبش خیابان بهار پلاک 73" + }, + { + "name": "شاه حسینی", + "phoneNumber": "09330107453" + }, + { + "name": "اعظم حسین زاده", + "phoneNumber": "09166538969" + }, + { + "name": "خ نرگس بندری", + "phoneNumber": "09166538969" + } + ] + } + ] + }, + { + "title": "نقشه خوانی", + "type": "General", + "price": 0, + "classes": [ + { + "name": "نقشه خوانی مرداد 1405", + "folderName": "14- نقشه خوانی مرداد 1405", + "startDate": "2026-07-23", + "jalaliYear": 1405, + "jalaliMonth": 5, + "sourceFile": "14- نقشه خوانی مرداد 1405.xlsx", + "students": [ + { + "name": "احمد هلیچی", + "phoneNumber": "09965212005", + "nationalIdCode": "1910166839", + "birthCertificateNumber": "16698", + "postalCode": "6183978137", + "fatherName": "عباس", + "birthDate": "1981-09-21", + "birthDateJalali": "1360/6/30", + "education": "ارشد علوم سیاسی", + "address": "پاداد 14 غربی" + }, + { + "name": "علیرضا چراغ پور", + "phoneNumber": "09131102215", + "nationalIdCode": "1840245271", + "birthCertificateNumber": "384", + "postalCode": "6136755180", + "placeOfIssue": "ایذه", + "fatherName": "علی", + "birthDate": "1975-03-20", + "birthDateJalali": "1353/12/29" + }, + { + "name": "کامران کمائی", + "phoneNumber": "09163502957", + "nationalIdCode": "5279822248", + "postalCode": "6193875656", + "placeOfIssue": "بهبهان", + "fatherName": "غلامرضا", + "birthDate": "1971-12-11", + "birthDateJalali": "1350/9/20", + "address": "اهواز خیابان طالقانی خیابان علم الهدی نبش معمارزاده پلاک ۴۹" + }, + { + "name": "محمد قاسم مهدی پورده مرداس", + "phoneNumber": "09302039583", + "nationalIdCode": "1745897119", + "postalCode": "6153697319", + "placeOfIssue": "اهواز", + "fatherName": "علی", + "birthDate": "2009-10-22", + "birthDateJalali": "1388/07/30", + "education": "ریاضی فیزیک", + "address": "کمپلوی شمالی خیابان اعتصامی بین امیرکبیر و قصر شیرین" + }, + { + "name": "آتنا غزلاوی", + "phoneNumber": "09017572847" + }, + { + "name": "کعبی نقشه خوانی", + "phoneNumber": "09035012291" + }, + { + "name": "امیدی خ", + "phoneNumber": "09360859118" + } + ] + } + ] + }, + { + "title": "طراحی معماری", + "type": "General", + "price": 0, + "classes": [ + { + "name": "طراحی معماری فروردین 1405", + "folderName": "3- طراحی معماری فروردین 1405", + "startDate": "2026-03-21", + "jalaliYear": 1405, + "jalaliMonth": 1, + "sourceFile": "طراحی معماری فروردین 1405.xlsx", + "students": [ + { + "name": "مینا شریفی ریگی", + "phoneNumber": "09364304740", + "nationalIdCode": "1741249066", + "postalCode": "6143619941", + "placeOfIssue": "اهواز", + "fatherName": "غلامرضا", + "birthDate": "1992-10-09", + "birthDateJalali": "1371/07/17", + "education": "کارشناسی معماری", + "address": "کمپلو، شیخ بها جنوبی، خیابان بهارستان پلاک 69 واحد 2" + }, + { + "name": "امین سلمانوندی", + "phoneNumber": "09165773547", + "nationalIdCode": "4810238180", + "postalCode": "6395134699", + "fatherName": "گودرز", + "birthDate": "1996-05-13", + "birthDateJalali": "1375/2.24", + "education": "کارشناسی معماری", + "address": "خیابان شهرداری، خیابان شهید چمران" + }, + { + "name": "مهرآنا فتحی", + "phoneNumber": "09167326142", + "nationalIdCode": "1900372835", + "postalCode": "6183969655", + "placeOfIssue": "رامهرمز", + "fatherName": "مسعود", + "birthDate": "1997-11-12", + "birthDateJalali": "1376/08/21", + "education": "کارشناسی معماری", + "address": "بلوار جوادالامه، خیابان 16 غربی پاداد شهر پلاک 133" + }, + { + "name": "حسین غیبی حاجیور", + "phoneNumber": "09383771198", + "nationalIdCode": "5550033471", + "postalCode": "6137916663", + "placeOfIssue": "فارسان", + "fatherName": "خان محمد", + "birthDate": "1991-10-30", + "birthDateJalali": "1370/08/08", + "education": "کارشناسی معماری", + "address": "اهواز، گلستان انتهای خیابان دی" + }, + { + "name": "بهاره مرشدزاده", + "phoneNumber": "09165921547", + "nationalIdCode": "1741996678", + "placeOfIssue": "اهواز", + "fatherName": "علیرضا", + "birthDate": "1995-08-20", + "birthDateJalali": "1374/05/29", + "education": "کارشناسی معماری", + "address": "مهدیس، منازل فرهنگیان خیابان تربیت شرقی 3" + } + ] + }, + { + "name": "طراحی معماری خرداد 1405", + "folderName": "8- طراحی معماری خرداد 1405", + "startDate": "2026-05-22", + "jalaliYear": 1405, + "jalaliMonth": 3, + "sourceFile": "طراحی معماری خرداد 1405.xlsx", + "students": [ + { + "name": "شیدا صادقی", + "phoneNumber": "09163094128", + "nationalIdCode": "1754397042", + "placeOfIssue": "اهواز", + "fatherName": "فرامرز", + "birthDate": "1981-09-16", + "birthDateJalali": "1360/06/25" + }, + { + "name": "سیده راحله مهداوی", + "phoneNumber": "09374187991", + "nationalIdCode": "1742241778", + "postalCode": "6136656798", + "placeOfIssue": "اهواز", + "fatherName": "سید محمد", + "birthDate": "1996-08-28", + "birthDateJalali": "1375/6/7", + "education": "لیسانس معماری", + "address": "اهواز گلستان کوی سعدی خیابان ناهید غربی پلاک 8" + }, + { + "name": "مژده پور شریف", + "phoneNumber": "09165065002", + "nationalIdCode": "1850359520", + "postalCode": "6361615489", + "placeOfIssue": "بهبهان", + "fatherName": "خرج الله", + "birthDate": "1997-08-14", + "birthDateJalali": "1376/5/23", + "education": "لیسانس معماری", + "address": "بهبهان ذوالفقاری کوی فرهنگیان نبش میدان پلاک 22" + }, + { + "name": "مرجان عسکری ایلاق", + "phoneNumber": "09165828158", + "nationalIdCode": "1754382762", + "birthCertificateNumber": "787", + "placeOfIssue": "اهواز", + "fatherName": "موسی", + "birthDate": "1981-07-16", + "birthDateJalali": "1360/04/25", + "education": "ارشد معماری", + "address": "کوی ملت فاز 2 خ 4 پلاک 9" + }, + { + "name": "نازنین کریمی ناصری", + "phoneNumber": "09396126152", + "nationalIdCode": "1743189982", + "placeOfIssue": "اهواز", + "fatherName": "محمد رضا", + "birthDate": "2000-05-11", + "birthDateJalali": "1379/2/22", + "education": "لیسانس معماری", + "address": "بلوار آیت . بهبهانی کمپانی ( مصطفایی) خ کیومرث پلاک 7" + }, + { + "name": "عیدی", + "phoneNumber": "09370781249", + "nationalIdCode": "14500" + } + ] + } + ] + }, + { + "title": "کارگاه تکسا", + "type": "General", + "price": 0, + "classes": [ + { + "name": "کارگاه تکسا اردیبهشت 1405", + "folderName": "4- کارگاه تکسا اردیبهشت 1405", + "startDate": "2026-04-21", + "jalaliYear": 1405, + "jalaliMonth": 2, + "sourceFile": "کارگاه تکسا 1405.xlsx", + "students": [ + { + "name": "قنواتی", + "phoneNumber": "09165297436" + }, + { + "name": "کیانی کرمتی", + "phoneNumber": "09225123219" + }, + { + "name": "عالمشاه", + "phoneNumber": "09031606973" + }, + { + "name": "آتنا جمالپور", + "phoneNumber": "09160555485" + }, + { + "name": "مرادی", + "phoneNumber": "09166935510" + }, + { + "name": "احمدی", + "phoneNumber": "09216214657" + }, + { + "name": "حبیبی", + "phoneNumber": "09163105144" + }, + { + "name": "علی نژاد", + "phoneNumber": "09368236218" + }, + { + "name": "رفیعی", + "phoneNumber": "09168942141" + }, + { + "name": "سلیمانی", + "phoneNumber": "09166324233" + }, + { + "name": "جک نژادیان", + "phoneNumber": "09176640270" + }, + { + "name": "عبودی", + "phoneNumber": "09370550939" + } + ] + } + ] + }, + { + "title": "نقشه برداری", + "type": "General", + "price": 0, + "classes": [ + { + "name": "نقشه برداری اردیبهشت 1405", + "folderName": "5-نقشه برداری اردیبهشت 1405", + "startDate": "2026-04-21", + "jalaliYear": 1405, + "jalaliMonth": 2, + "sourceFile": "نقشه برداری اردیبهشت 1405.xlsx", + "students": [ + { + "name": "احسان برزکار", + "phoneNumber": "09165297436", + "nationalIdCode": "4819943375", + "postalCode": "6381613420", + "placeOfIssue": "باغملک", + "fatherName": "بهمن", + "birthDate": "1988-08-23", + "birthDateJalali": "1367/06/01", + "education": "کارشناسی ارشد روانشناسی", + "address": "رامهرمز خیابان دهقان کوچه شهید غلام" + }, + { + "name": "عباس تقیان دشت بزرگ", + "phoneNumber": "09106388430", + "nationalIdCode": "1870450671", + "postalCode": "6187817467", + "placeOfIssue": "هندیجان", + "fatherName": "محمد رضا", + "birthDate": "1997-02-15", + "birthDateJalali": "1375/11/27", + "education": "لیسانس عمران", + "address": "اهواز، فاز 2 پادادشهر، ایستگاه 6 خیابان 38 پلاک 38" + }, + { + "name": "اسد ساعدی", + "phoneNumber": "09052680643", + "nationalIdCode": "1744217653", + "placeOfIssue": "اهواز", + "fatherName": "محمد رضا", + "birthDate": "2004-05-14", + "birthDateJalali": "1383/02/25" + }, + { + "name": "یعقوب سعیدی", + "phoneNumber": "09169970600", + "nationalIdCode": "1740042591", + "postalCode": "6153843851", + "placeOfIssue": "اهواز", + "fatherName": "عبدالساده", + "birthDate": "1989-04-21", + "birthDateJalali": "1368/02/01", + "education": "دیپلم", + "address": "اهواز، کمپلو، غزنوی شمالی خ بینا مجتمع میعاد 2" + }, + { + "name": "امین رفیعی", + "phoneNumber": "09168942141", + "nationalIdCode": "1911204610", + "postalCode": "4381614168", + "placeOfIssue": "رامهرمز", + "fatherName": "محمد رضا", + "birthDate": "1984-01-31", + "birthDateJalali": "1362/11/11", + "education": "کارشناسی", + "address": "رامهرمز، خ امت کوچه شهید قنواتی پور" + }, + { + "name": "سید منصور فواضلی", + "phoneNumber": "09059852944", + "nationalIdCode": "5269947463", + "postalCode": "6145783439", + "placeOfIssue": "اهواز", + "fatherName": "سید فاضل", + "birthDate": "1988-09-23", + "birthDateJalali": "1367/07/01", + "education": "کارشناسی عمران", + "address": "اهواز، کوی علوی خ بهداشت پلاک 6" + } + ] + } + ] + }, + { + "title": "کنترل پروژه", + "type": "General", + "price": 0, + "classes": [ + { + "name": "کنترل پروژه خرداد 1405", + "folderName": "6- کنترل پروژه خرداد 1405", + "startDate": "2026-05-22", + "jalaliYear": 1405, + "jalaliMonth": 3, + "sourceFile": "کنترل پروژه خرداد 1405.xlsx", + "students": [ + { + "name": "آریا فرداد", + "phoneNumber": "09308110916", + "nationalIdCode": "1972340549", + "postalCode": "6164734181", + "placeOfIssue": "اهواز", + "fatherName": "حاجت مراد", + "birthDate": "1987-05-15", + "birthDateJalali": "1366/02/25", + "education": "فوق لیسانس صنایع", + "address": "کوی ملت خ 7 عامری پلاک 47 زنگ 6" + }, + { + "name": "علیرضا چامیان", + "phoneNumber": "09365525480", + "nationalIdCode": "1940684463", + "postalCode": "6155755486", + "placeOfIssue": "امیدیه", + "fatherName": "محمد جان", + "birthDate": "1995-03-27", + "birthDateJalali": "1374/01/07", + "education": "دیپلم ریاضی", + "address": "1374/1/7" + }, + { + "name": "حامد دیناروندی", + "phoneNumber": "09035761396", + "nationalIdCode": "5260386851", + "postalCode": "6471644313", + "placeOfIssue": "دزفول", + "fatherName": "حمید", + "birthDate": "1998-09-27", + "birthDateJalali": "1377/07/05", + "education": "کارشناسی عمران", + "address": "شوش، ابراهیم آباد خ امام حسین پلاک 65" + }, + { + "name": "سپیده دیناروند", + "phoneNumber": "09333011328", + "nationalIdCode": "5260242831", + "postalCode": "6153783644", + "placeOfIssue": "شوش", + "fatherName": "علی عباس", + "birthDate": "1995-04-25", + "birthDateJalali": "1374/2/5", + "education": "لیسانس زبان", + "address": "اهواز، لشکر خیابان زکی زاده" + }, + { + "name": "سمیه اسدی", + "phoneNumber": "09361065649", + "nationalIdCode": "1850097364", + "postalCode": "6351647440", + "placeOfIssue": "بهبهان", + "fatherName": "برزو", + "birthDate": "1990-04-09", + "birthDateJalali": "1369/1/20", + "education": "لیسانس", + "address": "بندر ماهشهر ناحیه صنعتی خیابان حجاب" + }, + { + "name": "مسعود خزاعی", + "phoneNumber": "09167231801", + "nationalIdCode": "2002263515", + "birthCertificateNumber": "143", + "postalCode": "6183884835", + "placeOfIssue": "دزفول", + "fatherName": "عبدالرحمن", + "birthDate": "1983-04-12", + "birthDateJalali": "1362/01/23", + "education": "فوق لیسانس عمران", + "address": "پادادشهر خ شجاعی بین خ 7 و 8 پاداد مجتمع آریانا پلاک 5 طبقه 2 واحد 2" + }, + { + "name": "هرمز مندنی زاده", + "phoneNumber": "09168275452", + "nationalIdCode": "1930590334", + "birthCertificateNumber": "139", + "postalCode": "6163974585", + "placeOfIssue": "اندیمشک", + "fatherName": "عزیزالله", + "birthDate": "1962-06-30", + "birthDateJalali": "1341/4/9", + "education": "کارشناس ارشد عمران", + "address": "اهواز زیتون کارمندی خ فروغ" + } + ] + } + ] + }, + { + "title": "اکسل", + "type": "Private", + "price": 0, + "classes": [ + { + "name": "اکسل خصوصی خرداد 1405", + "folderName": "9- اکسل خصوصی خرداد 1405", + "startDate": "2026-05-22", + "jalaliYear": 1405, + "jalaliMonth": 3, + "sourceFile": "9- اکسل خصوصی خرداد 1405.xlsx", + "students": [ + { + "name": "نازنین اسد زاده", + "phoneNumber": "09338146212", + "nationalIdCode": "1810715598", + "placeOfIssue": "آبادان", + "fatherName": "نادر", + "birthDate": "2003-08-31", + "birthDateJalali": "1382/6/9", + "education": "سیکل", + "address": "بلوار کارگر خیابان پویش" + } + ] + } + ] + } + ] +} diff --git a/middlewares/activityLogger.js b/middlewares/activityLogger.js index b4f12ae..9f12bdb 100644 --- a/middlewares/activityLogger.js +++ b/middlewares/activityLogger.js @@ -76,7 +76,7 @@ const activityLogger = (req, res, next) => { actor: actor?._id || null, actorUsername: actor?.username || req.body?.username || null, actorName: actor - ? `${actor.name || ''} ${actor.surname || ''}`.trim() || actor.username + ? `${actor.name || ''}`.trim() || actor.username : req.body?.username || null, action, resource: resolveResource(path), diff --git a/package-lock.json b/package-lock.json index 17d8f02..e7e6f2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@aws-sdk/client-s3": "^3.1106.0", "@aws-sdk/s3-request-presigner": "^3.1106.0", "axios": "^1.7.9", - "bcrypt": "^5.1.1", "bcryptjs": "^3.0.3", "cors": "^2.8.5", "dotenv": "^16.4.7", @@ -25,7 +24,8 @@ "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", "nodemailer": "^6.10.0", - "winston": "^3.17.0" + "winston": "^3.17.0", + "xlsx": "^0.18.5" }, "devDependencies": { "nodemon": "^3.1.9" @@ -391,26 +391,6 @@ "@hapi/hoek": "^9.0.0" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, "node_modules/@mongodb-js/saslprep": { "version": "1.4.13", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.13.tgz", @@ -553,12 +533,6 @@ "@types/webidl-conversions": "*" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -572,6 +546,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -584,15 +567,6 @@ "node": ">= 6.0.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -613,40 +587,6 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -687,20 +627,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -864,6 +790,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -889,13 +828,13 @@ "fsevents": "~2.3.2" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": ">=0.8" } }, "node_modules/color": { @@ -944,15 +883,6 @@ "node": ">=18" } }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -965,12 +895,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, "node_modules/concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", @@ -986,12 +910,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1051,6 +969,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1077,12 +1007,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1102,15 +1026,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -1152,12 +1067,6 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -1412,6 +1321,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -1421,36 +1339,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1475,27 +1363,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1533,27 +1400,6 @@ "node": ">= 0.4" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1567,34 +1413,6 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1644,12 +1462,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -1726,17 +1538,6 @@ "dev": true, "license": "ISC" }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1775,15 +1576,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1955,30 +1747,6 @@ "node": ">= 12.0.0" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2079,40 +1847,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -2258,12 +1992,6 @@ "node": ">= 0.6" } }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, "node_modules/node-cron": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", @@ -2276,48 +2004,6 @@ "node": ">=6.0.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/nodemailer": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", @@ -2356,21 +2042,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2381,19 +2052,6 @@ "node": ">=0.10.0" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2427,15 +2085,6 @@ "node": ">= 0.8" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", @@ -2454,15 +2103,6 @@ "node": ">= 0.8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", @@ -2600,22 +2240,6 @@ "node": ">=8.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2717,12 +2341,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2807,12 +2425,6 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -2835,6 +2447,18 @@ "memory-pager": "^1.0.2" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -2876,32 +2500,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2915,36 +2513,6 @@ "node": ">=4" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -3101,15 +2669,6 @@ "node": ">=18" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", @@ -3174,11 +2733,44 @@ "node": ">= 6" } }, - "node_modules/wrappy": { + "node_modules/wmf": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } }, "node_modules/xtend": { "version": "4.0.2", @@ -3188,12 +2780,6 @@ "engines": { "node": ">=0.4" } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" } } } diff --git a/package.json b/package.json index d8dbba4..f56dcb7 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", "nodemailer": "^6.10.0", - "winston": "^3.17.0" + "winston": "^3.17.0", + "xlsx": "^0.18.5" }, "devDependencies": { "nodemon": "^3.1.9" diff --git a/scripts/convert-raw-data.js b/scripts/convert-raw-data.js new file mode 100644 index 0000000..aeac72a --- /dev/null +++ b/scripts/convert-raw-data.js @@ -0,0 +1,389 @@ +#!/usr/bin/env node +'use strict'; + +/** + * Converts class-folder xlsx files under raw-data into a structured import JSON. + * Student photo folders are ignored. + * + * Usage: node scripts/convert-raw-data.js [raw-data-root] [output.json] + */ + +const fs = require('fs'); +const path = require('path'); +const XLSX = require('xlsx'); + +const MONTHS = { + فروردین: 1, + اردیبهشت: 2, + خرداد: 3, + تیر: 4, + مرداد: 5, + شهریور: 6, + مهر: 7, + آبان: 8, + آذر: 9, + دی: 10, + بهمن: 11, + اسفند: 12 +}; + +const INFO_SHEET_HINTS = ['اطلاعات کلی', 'مشخصات کلی', 'اطلاعات']; +const FALLBACK_SHEET_HINTS = ['شهریه']; + +const DEFAULT_RAW = path.join( + __dirname, + '..', + '..', + 'raw-data', + 'برنامه آموزشی 1405-20260814T213206Z-1-001', + 'برنامه آموزشی 1405' +); +const DEFAULT_OUT = path.join(__dirname, '..', 'data', 'import-data.json'); + +const toEnglishDigits = (value) => + String(value ?? '') + .replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d)) + .replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d)); + +const cleanText = (value) => { + if (value == null) return ''; + return String(value).replace(/\s+/g, ' ').trim(); +}; + +const normalizeHeader = (value) => + cleanText(value) + .replace(/\n/g, ' ') + .replace(/\s+/g, ' '); + +const jalaliToGregorian = (jy, jm, jd) => { + const gy = jy <= 979 ? 621 : 1600; + jy -= jy <= 979 ? 0 : 979; + let days = + 365 * jy + + Math.floor(jy / 33) * 8 + + Math.floor(((jy % 33) + 3) / 4) + + 78 + + jd + + (jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186); + let gyOut = gy + 400 * Math.floor(days / 146097); + days %= 146097; + if (days > 36524) { + gyOut += 100 * Math.floor(--days / 36524); + days %= 36524; + if (days >= 365) days += 1; + } + gyOut += 4 * Math.floor(days / 1461); + days %= 1461; + if (days > 365) { + gyOut += Math.floor((days - 1) / 365); + days = (days - 1) % 365; + } + let gd = days + 1; + const sal_a = [ + 0, + 31, + (gyOut % 4 === 0 && gyOut % 100 !== 0) || gyOut % 400 === 0 ? 29 : 28, + 31, + 30, + 31, + 30, + 31, + 31, + 30, + 31, + 30, + 31 + ]; + let gm = 0; + for (gm = 1; gm <= 12 && gd > sal_a[gm]; gm += 1) gd -= sal_a[gm]; + const mm = String(gm).padStart(2, '0'); + const dd = String(gd).padStart(2, '0'); + return `${gyOut}-${mm}-${dd}`; +}; + +const parseJalaliDate = (raw) => { + if (!raw) return null; + const text = toEnglishDigits(raw).replace(/[./\-]/g, '/').trim(); + const match = text.match(/^(\d{3,4})\/(\d{1,2})\/(\d{1,2})$/); + if (!match) return null; + const jy = Number(match[1]); + const jm = Number(match[2]); + const jd = Number(match[3]); + if (!jy || !jm || !jd || jm > 12 || jd > 31) return null; + try { + return jalaliToGregorian(jy, jm, jd); + } catch { + return null; + } +}; + +const normalizePhone = (raw) => { + if (raw == null || raw === '') return ''; + let digits = toEnglishDigits(raw).replace(/[^\d+]/g, ''); + if (digits.includes('-') || String(raw).includes('-')) { + // Prefer first Iranian mobile-looking segment + const parts = toEnglishDigits(raw) + .split(/[-–—,\/|\s]+/) + .map((p) => p.replace(/\D/g, '')) + .filter(Boolean); + const candidate = + parts.find((p) => /^(0?9\d{9})$/.test(p)) || + parts.find((p) => p.length >= 10) || + parts[0] || + ''; + digits = candidate; + } + digits = digits.replace(/\D/g, ''); + if (digits.startsWith('98') && digits.length === 12) digits = `0${digits.slice(2)}`; + if (digits.length === 10 && digits.startsWith('9')) digits = `0${digits}`; + if (digits.length === 11 && digits.startsWith('09')) return digits; + return digits || ''; +}; + +const normalizeNationalId = (raw) => { + if (raw == null || raw === '') return ''; + const digits = toEnglishDigits(raw).replace(/\D/g, ''); + return digits; +}; + +const parseFolderMeta = (folderName) => { + const cleaned = cleanText(folderName).replace(/^\d+\s*[-–—.]?\s*/, ''); + const monthNames = Object.keys(MONTHS).join('|'); + const monthMatch = cleaned.match(new RegExp(`(${monthNames})\\s+(\\d{4})$`)); + let className = cleaned; + let courseTitle = cleaned; + let jalaliYear = null; + let jalaliMonth = null; + let startDate = null; + + if (monthMatch) { + const monthName = monthMatch[1]; + jalaliYear = Number(monthMatch[2]); + jalaliMonth = MONTHS[monthName]; + className = cleaned; + courseTitle = cleanText(cleaned.slice(0, monthMatch.index)); + startDate = jalaliToGregorian(jalaliYear, jalaliMonth, 1); + } + + const isPrivate = /خصوصی/.test(courseTitle) || /خصوصی/.test(className); + if (isPrivate) { + courseTitle = cleanText(courseTitle.replace(/خصوصی/g, '')); + } + + return { + folderName, + courseTitle: courseTitle || className, + className, + type: isPrivate ? 'Private' : 'General', + jalaliYear, + jalaliMonth, + startDate + }; +}; + +const findHeaderMap = (headerRow) => { + const map = {}; + (headerRow || []).forEach((cell, idx) => { + const h = normalizeHeader(cell); + if (!h) return; + if (h === 'نام' || h === 'نام ') map.firstName = idx; + else if (h.includes('نام خانوادگی')) map.lastName = idx; + else if (h.includes('نام و نام خانوادگی')) map.fullName = idx; + else if (h.includes('تلفن همراه هنرجو') || h.includes('شماره تماس هنرجو')) map.phone = idx; + else if (h.includes('تلفن همراه والد')) map.parentPhone = idx; + else if (h.includes('تلفن همراه') || h.includes('شماره تماس') || h.includes('شماره همراه')) { + if (map.phone == null) map.phone = idx; + } else if (h.includes('کد ملی')) map.nationalId = idx; + else if (h.includes('شماره شناسنامه')) map.birthCertificateNumber = idx; + else if (h.includes('کد پستی')) map.postalCode = idx; + else if (h.includes('محل صدور')) map.placeOfIssue = idx; + else if (h.includes('نام پدر')) map.fatherName = idx; + else if (h.includes('تاریخ تولد')) map.birthDate = idx; + else if (h.includes('تحصیلات')) map.education = idx; + else if (h === 'آدرس' || h.includes('آدرس')) map.address = idx; + }); + return map; +}; + +const pickSheet = (wb, hints) => { + for (const hint of hints) { + const found = wb.SheetNames.find((n) => normalizeHeader(n) === hint || normalizeHeader(n).includes(hint)); + if (found) return found; + } + return null; +}; + +const rowToStudent = (row, map) => { + const get = (key) => (map[key] == null ? '' : cleanText(row[map[key]])); + + let name = ''; + if (map.fullName != null) { + name = get('fullName'); + } else { + name = cleanText(`${get('firstName')} ${get('lastName')}`); + } + + const phone = normalizePhone(get('phone') || get('parentPhone')); + const parentPhone = map.parentPhone != null ? normalizePhone(get('parentPhone')) : ''; + const nationalIdCode = normalizeNationalId(get('nationalId')); + const birthDateRaw = get('birthDate'); + const birthDate = parseJalaliDate(birthDateRaw); + + if (!name && !phone && !nationalIdCode) return null; + + const student = { + name: name || (nationalIdCode ? `کاربر ${nationalIdCode}` : `کاربر ${phone}`), + phoneNumber: phone || undefined, + nationalIdCode: nationalIdCode || undefined, + birthCertificateNumber: get('birthCertificateNumber') || undefined, + postalCode: get('postalCode') || undefined, + placeOfIssue: get('placeOfIssue') || undefined, + fatherName: get('fatherName') || undefined, + birthDate: birthDate || undefined, + birthDateJalali: birthDateRaw || undefined, + education: get('education') || undefined, + address: get('address') || undefined + }; + + if (parentPhone && parentPhone !== phone) { + student.parentPhoneNumber = parentPhone; + } + + // Drop undefined keys for cleaner JSON + Object.keys(student).forEach((k) => { + if (student[k] === undefined || student[k] === '') delete student[k]; + }); + + return student; +}; + +const extractStudentsFromSheet = (sheet) => { + const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, raw: false }); + let headerIdx = -1; + let map = {}; + + for (let i = 0; i < Math.min(rows.length, 15); i += 1) { + const candidate = findHeaderMap(rows[i]); + const hasIdentity = + candidate.firstName != null || + candidate.fullName != null || + candidate.lastName != null; + const hasContact = candidate.phone != null || candidate.nationalId != null; + if (hasIdentity && (hasContact || candidate.lastName != null)) { + headerIdx = i; + map = candidate; + break; + } + } + + if (headerIdx < 0) return []; + + const students = []; + for (let i = headerIdx + 1; i < rows.length; i += 1) { + const row = rows[i]; + if (!row || row.every((c) => c == null || String(c).trim() === '')) continue; + const student = rowToStudent(row, map); + if (student) students.push(student); + } + return students; +}; + +const findClassXlsx = (dirPath) => { + const files = fs.readdirSync(dirPath).filter((f) => f.endsWith('.xlsx') && !f.startsWith('~$')); + if (files.length === 0) return null; + // Prefer file whose name looks like the class (not "لیست شرکت کنندگان...") + const preferred = files.find((f) => !f.includes('لیست شرکت کنندگان')) || files[0]; + return path.join(dirPath, preferred); +}; + +const convertClassFolder = (dirPath, folderName) => { + const meta = parseFolderMeta(folderName); + const xlsxPath = findClassXlsx(dirPath); + let students = []; + let sourceFile = null; + + if (xlsxPath) { + sourceFile = path.basename(xlsxPath); + const wb = XLSX.readFile(xlsxPath, { cellDates: false, raw: false }); + const infoSheetName = pickSheet(wb, INFO_SHEET_HINTS); + if (infoSheetName) { + students = extractStudentsFromSheet(wb.Sheets[infoSheetName]); + } + if (students.length === 0) { + const fallback = pickSheet(wb, FALLBACK_SHEET_HINTS); + if (fallback) students = extractStudentsFromSheet(wb.Sheets[fallback]); + } + } + + return { + ...meta, + sourceFile, + students + }; +}; + +const buildImportDocument = (rawRoot) => { + const entries = fs + .readdirSync(rawRoot, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort((a, b) => a.localeCompare(b, 'fa')); + + const classes = entries.map((name) => convertClassFolder(path.join(rawRoot, name), name)); + + const coursesMap = new Map(); + for (const cls of classes) { + const key = cls.courseTitle; + if (!coursesMap.has(key)) { + coursesMap.set(key, { + title: cls.courseTitle, + type: cls.type, + price: 0, + classes: [] + }); + } + const course = coursesMap.get(key); + if (cls.type === 'Private') course.type = 'Private'; + course.classes.push({ + name: cls.className, + folderName: cls.folderName, + startDate: cls.startDate, + jalaliYear: cls.jalaliYear, + jalaliMonth: cls.jalaliMonth, + sourceFile: cls.sourceFile, + students: cls.students + }); + } + + const courses = [...coursesMap.values()]; + const userCount = classes.reduce((n, c) => n + c.students.length, 0); + + return { + version: 1, + generatedAt: new Date().toISOString(), + source: path.basename(rawRoot), + summary: { + courses: courses.length, + classes: classes.length, + students: userCount + }, + courses + }; +}; + +const main = () => { + const rawRoot = path.resolve(process.argv[2] || DEFAULT_RAW); + const outPath = path.resolve(process.argv[3] || DEFAULT_OUT); + + if (!fs.existsSync(rawRoot)) { + console.error(`Raw data folder not found: ${rawRoot}`); + process.exit(1); + } + + const doc = buildImportDocument(rawRoot); + fs.writeFileSync(outPath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8'); + console.log(`Wrote ${outPath}`); + console.log(JSON.stringify(doc.summary, null, 2)); +}; + +main(); diff --git a/seed.js b/seed.js index f86471f..f60e979 100644 --- a/seed.js +++ b/seed.js @@ -146,8 +146,7 @@ const seedDatabase = async ({ disconnectOnComplete = false } = {}) => { if (!adminUser) { const passwordHash = await bcrypt.hash(config.SUPERADMIN_PASSWORD, 10); await User.create({ - name: 'مدیر', - surname: 'ارشد', + name: 'مدیر ارشد', nationalIdCode: config.SUPERADMIN_NATIONAL_ID, phoneNumber: config.SUPERADMIN_PHONE, email: config.SUPERADMIN_EMAIL || undefined, diff --git a/utils/userProfile.js b/utils/userProfile.js new file mode 100644 index 0000000..375259d --- /dev/null +++ b/utils/userProfile.js @@ -0,0 +1,24 @@ +// /utils/userProfile.js +'use strict'; + +const ALLOWED_GENDERS = ['male', 'female']; + +const mergeFullName = (name, surname) => { + const parts = [name, surname].map((v) => (v == null ? '' : String(v).trim())).filter(Boolean); + return parts.join(' ').trim(); +}; + +const normalizeGender = (value) => { + if (value == null || value === '') return undefined; + const g = String(value).trim().toLowerCase(); + if (ALLOWED_GENDERS.includes(g)) return g; + if (g === 'مرد' || g === 'آقا') return 'male'; + if (g === 'زن' || g === 'خانم') return 'female'; + return undefined; +}; + +module.exports = { + ALLOWED_GENDERS, + mergeFullName, + normalizeGender +};