330 lines
9.4 KiB
JavaScript
330 lines
9.4 KiB
JavaScript
const { UsersAuth, UserProfile, TeacherDetail, School } = require('../models');
|
|
const bcrypt = require('bcrypt');
|
|
const { sequelize } = require('../config/database');
|
|
|
|
/**
|
|
* Teacher Profile Service
|
|
* Xử lý việc tạo user_profile, users_auth cho teacher
|
|
*/
|
|
class TeacherProfileService {
|
|
/**
|
|
* Tạo full user cho teacher mới (users_auth + user_profile + teacher_detail)
|
|
* @param {Object} teacherData - Dữ liệu teacher
|
|
* @returns {Promise<Object>} - Teacher đã tạo kèm profile
|
|
*/
|
|
async createTeacherWithProfile(teacherData) {
|
|
const transaction = await sequelize.transaction();
|
|
|
|
try {
|
|
const {
|
|
// Teacher specific
|
|
teacher_code,
|
|
teacher_type,
|
|
qualification,
|
|
specialization,
|
|
hire_date,
|
|
status = 'active',
|
|
skill_tags,
|
|
certifications,
|
|
|
|
// User auth (optional - tự generate nếu không có)
|
|
username,
|
|
email,
|
|
password,
|
|
|
|
// Profile (optional - có thể để trống)
|
|
full_name,
|
|
first_name,
|
|
last_name,
|
|
phone,
|
|
date_of_birth,
|
|
gender,
|
|
address,
|
|
school_id,
|
|
city,
|
|
district,
|
|
avatar_url,
|
|
} = teacherData;
|
|
|
|
// Validate required fields
|
|
if (!teacher_code || !teacher_type) {
|
|
throw new Error('teacher_code and teacher_type are required');
|
|
}
|
|
|
|
// 1. Tạo users_auth
|
|
const generatedUsername = username || this._generateUsername(teacher_code, teacher_type);
|
|
const generatedEmail = email || this._generateEmail(teacher_code, teacher_type);
|
|
const generatedPassword = password || this._generateDefaultPassword(teacher_code);
|
|
|
|
const salt = await bcrypt.genSalt(10);
|
|
const password_hash = await bcrypt.hash(generatedPassword, salt);
|
|
|
|
const userAuth = await UsersAuth.create({
|
|
username: generatedUsername,
|
|
email: generatedEmail,
|
|
password_hash: password_hash,
|
|
salt: salt,
|
|
is_active: true,
|
|
qr_version: 1,
|
|
qr_secret: this._generateQRSecret(),
|
|
}, { transaction });
|
|
|
|
// 2. Tạo user_profile với thông tin cơ bản
|
|
const profileData = {
|
|
user_id: userAuth.id,
|
|
full_name: full_name || this._generateFullNameFromCode(teacher_code),
|
|
first_name: first_name || '',
|
|
last_name: last_name || '',
|
|
phone: phone || '',
|
|
date_of_birth: date_of_birth || null,
|
|
gender: gender || null,
|
|
address: address || '',
|
|
school_id: school_id || null,
|
|
city: city || '',
|
|
district: district || '',
|
|
avatar_url: avatar_url || null,
|
|
etc: {
|
|
teacher_code: teacher_code,
|
|
created_from: 'teacher_import',
|
|
needs_profile_update: true, // Flag để user update sau
|
|
}
|
|
};
|
|
|
|
const userProfile = await UserProfile.create(profileData, { transaction });
|
|
|
|
// 3. Tạo teacher_detail
|
|
const teacherDetail = await TeacherDetail.create({
|
|
user_id: userAuth.id,
|
|
teacher_code: teacher_code,
|
|
teacher_type: teacher_type,
|
|
qualification: qualification || '',
|
|
specialization: specialization || '',
|
|
hire_date: hire_date || new Date(),
|
|
status: status,
|
|
skill_tags: skill_tags || [],
|
|
certifications: certifications || [],
|
|
training_hours: 0,
|
|
last_training_date: null,
|
|
training_score_avg: null,
|
|
}, { transaction });
|
|
|
|
await transaction.commit();
|
|
|
|
// Return full data
|
|
return {
|
|
user_id: userAuth.id,
|
|
username: generatedUsername,
|
|
email: generatedEmail,
|
|
temporary_password: password ? null : generatedPassword, // Only return if auto-generated
|
|
profile: userProfile,
|
|
teacher_detail: teacherDetail,
|
|
};
|
|
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync existing teachers: Tạo user_profile cho các teacher đã có sẵn
|
|
* @param {string} teacherId - Optional: sync specific teacher or all
|
|
* @returns {Promise<Object>} - Results
|
|
*/
|
|
async syncExistingTeachers(teacherId = null) {
|
|
const results = {
|
|
success: [],
|
|
failed: [],
|
|
skipped: [],
|
|
};
|
|
|
|
try {
|
|
// Get teachers without user_profile
|
|
const whereClause = teacherId ? { id: teacherId } : {};
|
|
|
|
const teachers = await TeacherDetail.findAll({
|
|
where: whereClause,
|
|
include: [{
|
|
model: UserProfile,
|
|
as: 'profile',
|
|
required: false, // LEFT JOIN to find teachers without profile
|
|
}],
|
|
});
|
|
|
|
for (const teacher of teachers) {
|
|
try {
|
|
// Skip if already has profile
|
|
if (teacher.profile) {
|
|
results.skipped.push({
|
|
teacher_code: teacher.teacher_code,
|
|
reason: 'Already has profile',
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Check if user_id exists in users_auth
|
|
const userAuth = await UsersAuth.findByPk(teacher.user_id);
|
|
|
|
if (!userAuth) {
|
|
results.failed.push({
|
|
teacher_code: teacher.teacher_code,
|
|
reason: 'user_id not found in users_auth',
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Create user_profile
|
|
const userProfile = await UserProfile.create({
|
|
user_id: teacher.user_id,
|
|
full_name: this._generateFullNameFromCode(teacher.teacher_code),
|
|
first_name: '',
|
|
last_name: '',
|
|
phone: '',
|
|
date_of_birth: null,
|
|
gender: null,
|
|
address: '',
|
|
school_id: null,
|
|
city: '',
|
|
district: '',
|
|
avatar_url: null,
|
|
etc: {
|
|
teacher_code: teacher.teacher_code,
|
|
teacher_type: teacher.teacher_type,
|
|
synced_from: 'sync_script',
|
|
needs_profile_update: true,
|
|
synced_at: new Date().toISOString(),
|
|
}
|
|
});
|
|
|
|
results.success.push({
|
|
teacher_code: teacher.teacher_code,
|
|
user_id: teacher.user_id,
|
|
profile_id: userProfile.id,
|
|
});
|
|
|
|
} catch (error) {
|
|
results.failed.push({
|
|
teacher_code: teacher.teacher_code,
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
|
|
} catch (error) {
|
|
throw new Error(`Sync failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update teacher profile (cho phép user tự update)
|
|
* @param {string} userId - User ID
|
|
* @param {Object} profileData - Data to update
|
|
* @returns {Promise<Object>} - Updated profile
|
|
*/
|
|
async updateTeacherProfile(userId, profileData) {
|
|
const transaction = await sequelize.transaction();
|
|
|
|
try {
|
|
const userProfile = await UserProfile.findOne({
|
|
where: { user_id: userId },
|
|
transaction,
|
|
});
|
|
|
|
if (!userProfile) {
|
|
throw new Error('User profile not found');
|
|
}
|
|
|
|
// Update allowed fields
|
|
const allowedFields = [
|
|
'full_name', 'first_name', 'last_name',
|
|
'phone', 'date_of_birth', 'gender',
|
|
'address', 'city', 'district',
|
|
'avatar_url',
|
|
];
|
|
|
|
const updateData = {};
|
|
allowedFields.forEach(field => {
|
|
if (profileData[field] !== undefined) {
|
|
updateData[field] = profileData[field];
|
|
}
|
|
});
|
|
|
|
// Update etc to remove needs_profile_update flag
|
|
if (Object.keys(updateData).length > 0) {
|
|
updateData.etc = {
|
|
...userProfile.etc,
|
|
needs_profile_update: false,
|
|
last_updated: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
await userProfile.update(updateData, { transaction });
|
|
await transaction.commit();
|
|
|
|
return userProfile;
|
|
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============ Helper Methods ============
|
|
|
|
/**
|
|
* Generate username from teacher_code
|
|
*/
|
|
_generateUsername(teacher_code, teacher_type) {
|
|
const prefix = teacher_type === 'foreign' ? 'ft' : 'gv';
|
|
return `${prefix}_${teacher_code.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
|
|
}
|
|
|
|
/**
|
|
* Generate email from teacher_code
|
|
*/
|
|
_generateEmail(teacher_code, teacher_type) {
|
|
const username = this._generateUsername(teacher_code, teacher_type);
|
|
return `${username}@sena.edu.vn`;
|
|
}
|
|
|
|
/**
|
|
* Generate default password (nên đổi sau lần đầu login)
|
|
*/
|
|
_generateDefaultPassword(teacher_code) {
|
|
return `Sena${teacher_code}@2026`;
|
|
}
|
|
|
|
/**
|
|
* Generate QR secret for attendance
|
|
*/
|
|
_generateQRSecret() {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
let secret = '';
|
|
for (let i = 0; i < 32; i++) {
|
|
secret += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return secret;
|
|
}
|
|
|
|
/**
|
|
* Generate full_name from teacher_code
|
|
* VD: GV001 -> "Giáo viên GV001"
|
|
* FT001 -> "Teacher FT001"
|
|
*/
|
|
_generateFullNameFromCode(teacher_code) {
|
|
// Detect if foreign teacher
|
|
const isForeign = teacher_code.toUpperCase().startsWith('FT') ||
|
|
teacher_code.toUpperCase().includes('FOREIGN');
|
|
|
|
if (isForeign) {
|
|
return `Teacher ${teacher_code}`;
|
|
}
|
|
|
|
return `Giáo viên ${teacher_code}`;
|
|
}
|
|
}
|
|
|
|
module.exports = new TeacherProfileService();
|