update
This commit is contained in:
329
services/teacherProfileService.js
Normal file
329
services/teacherProfileService.js
Normal 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();
|
||||
Reference in New Issue
Block a user