This commit is contained in:
Ken
2026-01-19 09:33:35 +07:00
parent 374dc12b2d
commit 70838a4bc1
103 changed files with 16929 additions and 2 deletions

View File

@@ -0,0 +1,329 @@
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();