feat: add professor import support, user account creation, and IBAN/card fields
This commit is contained in:
@@ -5,6 +5,7 @@ const bcrypt = require('bcryptjs');
|
|||||||
const Course = require('../courses/courseModel');
|
const Course = require('../courses/courseModel');
|
||||||
const Class = require('../classes/classModel');
|
const Class = require('../classes/classModel');
|
||||||
const User = require('../users/userModel');
|
const User = require('../users/userModel');
|
||||||
|
const Professor = require('../professors/professorModel');
|
||||||
const Role = require('../roles/roleModel');
|
const Role = require('../roles/roleModel');
|
||||||
const Session = require('../sessions/sessionModel');
|
const Session = require('../sessions/sessionModel');
|
||||||
const Payment = require('../payments/paymentModel');
|
const Payment = require('../payments/paymentModel');
|
||||||
@@ -13,7 +14,7 @@ const paymentService = require('../payments/paymentService');
|
|||||||
const AppError = require('../../utils/AppError');
|
const AppError = require('../../utils/AppError');
|
||||||
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
||||||
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
|
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
|
||||||
const { parseImportDate, utcDayRange } = require('../../utils/jalaliDate');
|
const { parseImportDate, utcDayRange, toEnglishDigits } = require('../../utils/jalaliDate');
|
||||||
const {
|
const {
|
||||||
namesMatch,
|
namesMatch,
|
||||||
normalizePersonName,
|
normalizePersonName,
|
||||||
@@ -38,13 +39,25 @@ const allocatePlaceholderNationalId = async (phoneNumber) => {
|
|||||||
const base = `TMP${(phoneNumber || '').replace(/\D/g, '').slice(-10) || Date.now().toString().slice(-10)}`;
|
const base = `TMP${(phoneNumber || '').replace(/\D/g, '').slice(-10) || Date.now().toString().slice(-10)}`;
|
||||||
let candidate = base.padEnd(10, '0').slice(0, 10);
|
let candidate = base.padEnd(10, '0').slice(0, 10);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (await User.exists({ nationalIdCode: candidate })) {
|
while ((await User.exists({ nationalIdCode: candidate })) || (await Professor.exists({ nationalIdCode: candidate }))) {
|
||||||
i += 1;
|
i += 1;
|
||||||
candidate = `${base.slice(0, 7)}${String(i).padStart(3, '0')}`.slice(0, 10);
|
candidate = `${base.slice(0, 7)}${String(i).padStart(3, '0')}`.slice(0, 10);
|
||||||
}
|
}
|
||||||
return candidate;
|
return candidate;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allocatePlaceholderPhone = async (seed) => {
|
||||||
|
const baseDigits = (seed || Date.now().toString()).replace(/\D/g, '');
|
||||||
|
const suffix = baseDigits.slice(-7).padStart(7, '0');
|
||||||
|
let candidate = `0999${suffix}`.slice(0, 11);
|
||||||
|
let i = 0;
|
||||||
|
while ((await User.exists({ phoneNumber: candidate })) || (await Professor.exists({ phoneNumber: candidate }))) {
|
||||||
|
i += 1;
|
||||||
|
candidate = `0999${String(i).padStart(7, '0')}`.slice(0, 11);
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
};
|
||||||
|
|
||||||
const findExistingUser = async ({ nationalIdCode, phoneNumber, name }) => {
|
const findExistingUser = async ({ nationalIdCode, phoneNumber, name }) => {
|
||||||
if (nationalIdCode) {
|
if (nationalIdCode) {
|
||||||
const byId = await User.findOne({ nationalIdCode });
|
const byId = await User.findOne({ nationalIdCode });
|
||||||
@@ -379,18 +392,134 @@ const migrateEmbeddedPaymentTransactions = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const upsertProfessorAndUser = async (profInput, professorRole, userRole, stats, warnings) => {
|
||||||
|
const name = normalizePersonName(profInput.name);
|
||||||
|
const surname = normalizePersonName(profInput.surname);
|
||||||
|
const fullName = mergeFullName(name, surname);
|
||||||
|
|
||||||
|
let phoneNumber = normalizePhone(profInput.phoneNumber || profInput.phone);
|
||||||
|
let nationalIdCode = normalizeNationalId(profInput.nationalIdCode || profInput.nationalId);
|
||||||
|
const cardNumber = profInput.cardNumber ? String(profInput.cardNumber).replace(/\D/g, '').trim() : undefined;
|
||||||
|
const shabaNumber = (profInput.shabaNumber || profInput.iban)
|
||||||
|
? String(profInput.shabaNumber || profInput.iban).replace(/[^0-9A-Za-z]/g, '').trim()
|
||||||
|
: undefined;
|
||||||
|
const email = profInput.email ? String(profInput.email).trim().toLowerCase() : undefined;
|
||||||
|
|
||||||
|
if (!fullName && !phoneNumber && !nationalIdCode) {
|
||||||
|
warnings.push({ reason: 'missing_identity', professor: profInput });
|
||||||
|
stats.professorsSkipped += 1;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nationalIdCode) {
|
||||||
|
nationalIdCode = await allocatePlaceholderNationalId(phoneNumber || name);
|
||||||
|
warnings.push({
|
||||||
|
reason: 'placeholder_national_id',
|
||||||
|
professor: { name: fullName, phoneNumber, nationalIdCode }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!phoneNumber) {
|
||||||
|
phoneNumber = await allocatePlaceholderPhone(nationalIdCode || name);
|
||||||
|
warnings.push({
|
||||||
|
reason: 'placeholder_phone',
|
||||||
|
professor: { name: fullName, phoneNumber, nationalIdCode }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let existingProf = null;
|
||||||
|
if (nationalIdCode) {
|
||||||
|
existingProf = await Professor.findOne({ nationalIdCode });
|
||||||
|
}
|
||||||
|
if (!existingProf && phoneNumber) {
|
||||||
|
existingProf = await Professor.findOne({ phoneNumber });
|
||||||
|
}
|
||||||
|
if (!existingProf && fullName) {
|
||||||
|
const allProfs = await Professor.find({});
|
||||||
|
existingProf = allProfs.find((p) => namesMatch(`${p.name} ${p.surname}`, fullName) || namesMatch(p.name, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
let professorDoc;
|
||||||
|
if (existingProf) {
|
||||||
|
if (name) existingProf.name = name;
|
||||||
|
if (surname) existingProf.surname = surname;
|
||||||
|
if (cardNumber) existingProf.cardNumber = cardNumber;
|
||||||
|
if (shabaNumber) existingProf.shabaNumber = shabaNumber;
|
||||||
|
if (email && !existingProf.email) existingProf.email = email;
|
||||||
|
if (profInput.bio && !existingProf.bio) existingProf.bio = profInput.bio;
|
||||||
|
await existingProf.save();
|
||||||
|
professorDoc = existingProf;
|
||||||
|
stats.professorsUpdated += 1;
|
||||||
|
} else {
|
||||||
|
professorDoc = await Professor.create({
|
||||||
|
name: name || fullName,
|
||||||
|
surname: surname || '',
|
||||||
|
nationalIdCode,
|
||||||
|
phoneNumber,
|
||||||
|
cardNumber: cardNumber || undefined,
|
||||||
|
shabaNumber: shabaNumber || undefined,
|
||||||
|
email: email || undefined,
|
||||||
|
bio: profInput.bio || undefined,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
stats.professorsCreated += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetRole = professorRole || userRole;
|
||||||
|
let user = await findExistingUser({ nationalIdCode, phoneNumber, name: fullName });
|
||||||
|
if (user) {
|
||||||
|
if (fullName && !user.name) user.name = fullName;
|
||||||
|
if (cardNumber && !user.cardNumber) user.cardNumber = cardNumber;
|
||||||
|
if (shabaNumber && !user.shabaNumber) user.shabaNumber = shabaNumber;
|
||||||
|
if (email && !user.email) user.email = email;
|
||||||
|
if (targetRole && String(user.role) === String(userRole?._id)) {
|
||||||
|
user.role = targetRole._id;
|
||||||
|
}
|
||||||
|
await user.save();
|
||||||
|
stats.studentsUpdated += 1;
|
||||||
|
} else {
|
||||||
|
const username = await allocateUniqueUsername();
|
||||||
|
const plainPassword = generateSimplePassword();
|
||||||
|
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
||||||
|
|
||||||
|
user = await User.create({
|
||||||
|
name: fullName || `استاد ${phoneNumber}`,
|
||||||
|
nationalIdCode,
|
||||||
|
phoneNumber,
|
||||||
|
cardNumber: cardNumber || undefined,
|
||||||
|
shabaNumber: shabaNumber || undefined,
|
||||||
|
email: email || undefined,
|
||||||
|
username,
|
||||||
|
passwordHash,
|
||||||
|
role: targetRole._id,
|
||||||
|
preferredMessenger: ['SMS'],
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
stats.studentsCreated += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { professor: professorDoc, user };
|
||||||
|
};
|
||||||
|
|
||||||
const importData = async (rawPayload) => {
|
const importData = async (rawPayload) => {
|
||||||
const payload = normalizeImportPayload(rawPayload);
|
const payload = normalizeImportPayload(rawPayload);
|
||||||
if (!payload || !Array.isArray(payload.courses)) {
|
if (!payload || (!Array.isArray(payload.courses) && !Array.isArray(payload.professors))) {
|
||||||
throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload: courses array required');
|
throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload: courses or professors array required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRole = await Role.findOne({ name: 'User' });
|
const userRole = await Role.findOne({ name: 'User' });
|
||||||
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
||||||
|
let professorRole = await Role.findOne({ name: 'Professor' });
|
||||||
|
if (!professorRole) {
|
||||||
|
professorRole = userRole;
|
||||||
|
}
|
||||||
|
|
||||||
await migrateEmbeddedPaymentTransactions();
|
await migrateEmbeddedPaymentTransactions();
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
|
professorsCreated: 0,
|
||||||
|
professorsUpdated: 0,
|
||||||
|
professorsSkipped: 0,
|
||||||
coursesCreated: 0,
|
coursesCreated: 0,
|
||||||
coursesReused: 0,
|
coursesReused: 0,
|
||||||
classesCreated: 0,
|
classesCreated: 0,
|
||||||
@@ -411,7 +540,13 @@ const importData = async (rawPayload) => {
|
|||||||
};
|
};
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
|
||||||
for (const courseInput of payload.courses) {
|
const professors = Array.isArray(payload.professors) ? payload.professors : [];
|
||||||
|
for (const profInput of professors) {
|
||||||
|
await upsertProfessorAndUser(profInput, professorRole, userRole, stats, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
const courses = Array.isArray(payload.courses) ? payload.courses : [];
|
||||||
|
for (const courseInput of courses) {
|
||||||
const title = String(courseInput.title || '').trim();
|
const title = String(courseInput.title || '').trim();
|
||||||
if (!title) continue;
|
if (!title) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const { toEnglishDigits } = require('../../utils/jalaliDate');
|
const { toEnglishDigits } = require('../../utils/jalaliDate');
|
||||||
|
|
||||||
const TITLE_PREFIX = /^(آقای|اقای|خانم|آقا)\s+/;
|
const TITLE_PREFIX = /^(آقای|اقای|خانم|آقا|سرکار\s+خانم|سرکار)\s+/;
|
||||||
const HONORIFIC_MIDDLE = /\s+خ\s+/g;
|
const HONORIFIC_MIDDLE = /\s+خ\s+/g;
|
||||||
|
|
||||||
const normalizePersonName = (value) => {
|
const normalizePersonName = (value) => {
|
||||||
@@ -28,14 +28,20 @@ const namesMatch = (left, right) => {
|
|||||||
const normalizePhone = (raw) => {
|
const normalizePhone = (raw) => {
|
||||||
if (raw == null || raw === '') return '';
|
if (raw == null || raw === '') return '';
|
||||||
let digits = toEnglishDigits(raw).replace(/\D/g, '');
|
let digits = toEnglishDigits(raw).replace(/\D/g, '');
|
||||||
if (digits.startsWith('98') && digits.length === 12) digits = `0${digits.slice(2)}`;
|
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 === 10 && digits.startsWith('9')) digits = `0${digits}`;
|
||||||
|
if (digits.length === 11 && digits.startsWith('9')) digits = `0${digits.slice(0, 10)}`;
|
||||||
|
if (digits.length > 11 && digits.startsWith('09')) digits = digits.slice(0, 11);
|
||||||
return digits;
|
return digits;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeNationalId = (raw) => {
|
const normalizeNationalId = (raw) => {
|
||||||
if (raw == null || raw === '') return '';
|
if (raw == null || raw === '') return '';
|
||||||
return toEnglishDigits(raw).replace(/\D/g, '');
|
let digits = toEnglishDigits(raw).replace(/\D/g, '');
|
||||||
|
if (digits && digits.length < 10 && /^\d+$/.test(digits)) {
|
||||||
|
digits = digits.padStart(10, '0');
|
||||||
|
}
|
||||||
|
return digits;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapAttendanceStatus = (raw) => {
|
const mapAttendanceStatus = (raw) => {
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ const {
|
|||||||
describe('importHelpers', () => {
|
describe('importHelpers', () => {
|
||||||
it('normalizes honorifics so spreadsheet names match database users', () => {
|
it('normalizes honorifics so spreadsheet names match database users', () => {
|
||||||
assert.equal(normalizePersonName('مریم خ پناهیان'), 'مریم پناهیان');
|
assert.equal(normalizePersonName('مریم خ پناهیان'), 'مریم پناهیان');
|
||||||
|
assert.equal(normalizePersonName('سرکار فاطمه'), 'فاطمه');
|
||||||
|
assert.equal(normalizePersonName('سرکار خانم جاذبی'), 'جاذبی');
|
||||||
assert.ok(namesMatch('آقای رضایی', 'آقای رضایی'));
|
assert.ok(namesMatch('آقای رضایی', 'آقای رضایی'));
|
||||||
assert.equal(normalizePhone(9168942141), '09168942141');
|
assert.equal(normalizePhone(9168942141), '09168942141');
|
||||||
|
assert.equal(normalizePhone('93568933888'), '09356893388');
|
||||||
|
assert.equal(normalizePhone('09161117529'), '09161117529');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps spreadsheet attendance marks', () => {
|
it('maps spreadsheet attendance marks', () => {
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ const professorSchema = new mongoose.Schema({
|
|||||||
trim: true,
|
trim: true,
|
||||||
lowercase: true
|
lowercase: true
|
||||||
},
|
},
|
||||||
|
cardNumber: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
shabaNumber: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
bio: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
expertise: [{
|
expertise: [{
|
||||||
type: String,
|
type: String,
|
||||||
trim: true
|
trim: true
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ const createProfessor = async (data) => {
|
|||||||
surname: String(data.surname || '').trim(),
|
surname: String(data.surname || '').trim(),
|
||||||
nationalIdCode: String(data.nationalIdCode || data.nationalId || '').trim(),
|
nationalIdCode: String(data.nationalIdCode || data.nationalId || '').trim(),
|
||||||
phoneNumber: String(data.phoneNumber || data.phone || '').trim(),
|
phoneNumber: String(data.phoneNumber || data.phone || '').trim(),
|
||||||
email: data.email ? String(data.email).trim().toLowerCase() : undefined
|
email: data.email ? String(data.email).trim().toLowerCase() : undefined,
|
||||||
|
cardNumber: data.cardNumber ? String(data.cardNumber).trim() : undefined,
|
||||||
|
shabaNumber: data.shabaNumber || data.iban ? String(data.shabaNumber || data.iban).trim() : undefined,
|
||||||
|
bio: data.bio ? String(data.bio).trim() : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const existing = await Professor.findOne({
|
const existing = await Professor.findOne({
|
||||||
@@ -73,6 +76,17 @@ const updateProfessor = async (id, updateData) => {
|
|||||||
if (payload.email !== undefined) {
|
if (payload.email !== undefined) {
|
||||||
payload.email = payload.email ? String(payload.email).trim().toLowerCase() : undefined;
|
payload.email = payload.email ? String(payload.email).trim().toLowerCase() : undefined;
|
||||||
}
|
}
|
||||||
|
if (payload.cardNumber !== undefined) {
|
||||||
|
payload.cardNumber = payload.cardNumber ? String(payload.cardNumber).trim() : undefined;
|
||||||
|
}
|
||||||
|
if (payload.shabaNumber !== undefined || payload.iban !== undefined) {
|
||||||
|
const val = payload.shabaNumber || payload.iban;
|
||||||
|
payload.shabaNumber = val ? String(val).trim() : undefined;
|
||||||
|
delete payload.iban;
|
||||||
|
}
|
||||||
|
if (payload.bio !== undefined) {
|
||||||
|
payload.bio = payload.bio ? String(payload.bio).trim() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(professor, payload);
|
Object.assign(professor, payload);
|
||||||
await professor.save();
|
await professor.save();
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ const userSchema = new mongoose.Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
trim: true
|
trim: true
|
||||||
},
|
},
|
||||||
|
cardNumber: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
shabaNumber: {
|
||||||
|
type: String,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
username: {
|
username: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ const pickProfileFields = (body = {}) => {
|
|||||||
birthDate: body.birthDate || undefined,
|
birthDate: body.birthDate || undefined,
|
||||||
education: body.education,
|
education: body.education,
|
||||||
parentPhoneNumber: body.parentPhoneNumber,
|
parentPhoneNumber: body.parentPhoneNumber,
|
||||||
|
cardNumber: body.cardNumber,
|
||||||
|
shabaNumber: body.shabaNumber || body.iban,
|
||||||
preferredMessenger: normalizePreferredMessengers(body.preferredMessenger),
|
preferredMessenger: normalizePreferredMessengers(body.preferredMessenger),
|
||||||
adminNotes: normalizeAdminNotes(body.adminNotes)
|
adminNotes: normalizeAdminNotes(body.adminNotes)
|
||||||
};
|
};
|
||||||
@@ -257,6 +259,13 @@ const updateUser = async (id, body) => {
|
|||||||
if (preferredMessenger !== undefined) {
|
if (preferredMessenger !== undefined) {
|
||||||
update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || [];
|
update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || [];
|
||||||
}
|
}
|
||||||
|
if (body.shabaNumber !== undefined || body.iban !== undefined) {
|
||||||
|
update.shabaNumber = (body.shabaNumber || body.iban || '').trim() || undefined;
|
||||||
|
delete update.iban;
|
||||||
|
}
|
||||||
|
if (body.cardNumber !== undefined) {
|
||||||
|
update.cardNumber = (body.cardNumber || '').trim() || undefined;
|
||||||
|
}
|
||||||
if (body.adminNotes !== undefined) {
|
if (body.adminNotes !== undefined) {
|
||||||
update.adminNotes = normalizeAdminNotes(body.adminNotes) || [];
|
update.adminNotes = normalizeAdminNotes(body.adminNotes) || [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// /scripts/convert-professors.js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const ODS_FILE = path.resolve(__dirname, '../../raw-data/Untitled spreadsheet.ods');
|
||||||
|
const OUTPUT_FILE = path.resolve(__dirname, '../../raw-data/professors-import.json');
|
||||||
|
|
||||||
|
const pythonExtractScript = `
|
||||||
|
import zipfile, xml.etree.ElementTree as ET, json, sys
|
||||||
|
|
||||||
|
ods_path = sys.argv[1]
|
||||||
|
with zipfile.ZipFile(ods_path) as z:
|
||||||
|
content = z.read('content.xml')
|
||||||
|
tree = ET.fromstring(content)
|
||||||
|
for table in tree.iter('{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table'):
|
||||||
|
rows = []
|
||||||
|
for row in table.iter('{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-row'):
|
||||||
|
row_vals = []
|
||||||
|
for cell in row.iter('{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-cell'):
|
||||||
|
texts = [t.text for t in cell.iter('{urn:oasis:names:tc:opendocument:xmlns:text:1.0}p') if t.text]
|
||||||
|
repeat = int(cell.attrib.get('{urn:oasis:names:tc:opendocument:xmlns:table:1.0}number-columns-repeated', 1))
|
||||||
|
val = ' '.join(texts).strip() if texts else ''
|
||||||
|
if repeat > 1 and not val:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for _ in range(min(repeat, 20)):
|
||||||
|
row_vals.append(val)
|
||||||
|
if any(row_vals):
|
||||||
|
rows.append(row_vals)
|
||||||
|
print(json.dumps(rows, ensure_ascii=False))
|
||||||
|
break
|
||||||
|
`;
|
||||||
|
|
||||||
|
const extractOdsRows = (filePath) => {
|
||||||
|
const output = execSync(`python3 -c "${pythonExtractScript.replace(/"/g, '\\"')}" "${filePath}"`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
maxBuffer: 10 * 1024 * 1024
|
||||||
|
});
|
||||||
|
return JSON.parse(output);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toEnglishDigits = (str = '') =>
|
||||||
|
String(str)
|
||||||
|
.replace(/[۰-۹]/g, (d) => '0123456789'['۰۱۲۳۴۵۶۷۸۹'.indexOf(d)])
|
||||||
|
.replace(/[٠-٩]/g, (d) => '0123456789'['٠١٢٣٤٥٦٧٨٩'.indexOf(d)]);
|
||||||
|
|
||||||
|
const cleanPhone = (raw) => {
|
||||||
|
if (!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}`;
|
||||||
|
if (digits.length === 11 && digits.startsWith('9')) digits = `0${digits.slice(0, 10)}`;
|
||||||
|
if (digits.length > 11 && digits.startsWith('09')) digits = digits.slice(0, 11);
|
||||||
|
return digits;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanNationalId = (raw) => {
|
||||||
|
if (!raw) return '';
|
||||||
|
let digits = toEnglishDigits(raw).replace(/\D/g, '');
|
||||||
|
if (digits && digits.length < 10 && /^\d+$/.test(digits)) {
|
||||||
|
digits = digits.padStart(10, '0');
|
||||||
|
}
|
||||||
|
return digits;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanCard = (raw) => {
|
||||||
|
if (!raw) return '';
|
||||||
|
return toEnglishDigits(raw).replace(/\D/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanShaba = (raw) => {
|
||||||
|
if (!raw) return '';
|
||||||
|
let val = toEnglishDigits(raw).replace(/[^0-9A-Za-z]/g, '');
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
if (!fs.existsSync(ODS_FILE)) {
|
||||||
|
console.error(`ODS file not found: ${ODS_FILE}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = extractOdsRows(ODS_FILE);
|
||||||
|
if (!rows || rows.length < 2) {
|
||||||
|
console.error('No rows found in ODS file');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = rows[0];
|
||||||
|
console.log('Detected headers:', header);
|
||||||
|
|
||||||
|
const professors = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < rows.length; i += 1) {
|
||||||
|
const r = rows[i];
|
||||||
|
// 0: ردیف, 1: نام, 2: نام خانوادگی, 3: کد ملی, 4: شماره تماس, 5: درصد, 6: شماره کارت, 7: شمار شبا
|
||||||
|
const rawName = r[1] || '';
|
||||||
|
const rawSurname = r[2] || '';
|
||||||
|
const rawNid = r[3] || '';
|
||||||
|
const rawPhone = r[4] || '';
|
||||||
|
const rawCard = r[6] || '';
|
||||||
|
const rawShaba = r[7] || '';
|
||||||
|
|
||||||
|
// Strip honorifics like 'سرکار'
|
||||||
|
const name = rawName.replace(/^(آقای|اقای|خانم|آقا|سرکار\s+خانم|سرکار)\s+/, '').trim();
|
||||||
|
const surname = rawSurname.trim();
|
||||||
|
|
||||||
|
const nationalIdCode = cleanNationalId(rawNid);
|
||||||
|
const phoneNumber = cleanPhone(rawPhone);
|
||||||
|
const cardNumber = cleanCard(rawCard);
|
||||||
|
const shabaNumber = cleanShaba(rawShaba);
|
||||||
|
|
||||||
|
professors.push({
|
||||||
|
name,
|
||||||
|
surname,
|
||||||
|
nationalIdCode,
|
||||||
|
phoneNumber,
|
||||||
|
cardNumber,
|
||||||
|
shabaNumber
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
version: 1,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
source: 'Untitled spreadsheet.ods',
|
||||||
|
summary: {
|
||||||
|
professors: professors.length
|
||||||
|
},
|
||||||
|
professors
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(payload, null, 2), 'utf-8');
|
||||||
|
console.log(`Successfully wrote ${professors.length} professors to ${OUTPUT_FILE}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extractOdsRows,
|
||||||
|
main
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user