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,79 @@
const { sequelize } = require('../config/database');
const School = require('../models/School');
/**
* Script thêm trung tâm SenaAI vào database
* Chạy: node src/scripts/add-senaai-center.js
*/
async function addSenaAICenter() {
try {
console.log('🚀 Đang thêm trung tâm SenaAI...\n');
// Kết nối database
await sequelize.authenticate();
console.log('✅ Kết nối database thành công\n');
// Kiểm tra trung tâm đã tồn tại chưa
const existingCenter = await School.findOne({
where: { school_code: 'SENAAI' }
});
if (existingCenter) {
console.log('⚠️ Trung tâm SenaAI đã tồn tại!');
console.log(` Tên: ${existingCenter.school_name}`);
console.log(` Mã: ${existingCenter.school_code}`);
console.log(` Địa chỉ: ${existingCenter.address || 'N/A'}`);
console.log('\n');
} else {
// Tạo mới trung tâm SenaAI
const senaAI = await School.create({
school_code: 'SENAAI',
school_name: 'Trung tâm Giáo dục SenaAI',
school_type: 'primary',
address: 'Thành phố Hồ Chí Minh',
city: 'Thành phố Hồ Chí Minh',
district: 'Đang cập nhật',
phone: '',
config: {
type: 'education_center',
services: ['AI Education', 'Technology Training', 'School Management']
},
is_active: true,
});
console.log('✅ Đã tạo trung tâm SenaAI thành công!');
console.log(` ID: ${senaAI.id}`);
console.log(` Tên: ${senaAI.school_name}`);
console.log(` Mã: ${senaAI.school_code}`);
console.log('\n');
}
// Hiển thị tổng số trường
const totalSchools = await School.count();
console.log(`📊 Tổng số trường trong hệ thống: ${totalSchools}`);
console.log('\n✅ Hoàn thành!\n');
} catch (error) {
console.error('❌ Lỗi:', error);
throw error;
} finally {
await sequelize.close();
console.log('🔌 Đã đóng kết nối database');
}
}
// Chạy script
if (require.main === module) {
addSenaAICenter()
.then(() => {
console.log('\n🎉 Script hoàn thành thành công!');
process.exit(0);
})
.catch((error) => {
console.error('\n💥 Script thất bại:', error);
process.exit(1);
});
}
module.exports = { addSenaAICenter };

View File

@@ -0,0 +1,50 @@
/**
* Script: Clear Teacher Cache
* Xóa tất cả cache liên quan đến teachers
*/
require('dotenv').config();
const { redisClient } = require('../config/redis');
async function clearTeacherCache() {
console.log('🧹 Clearing teacher cache...\n');
try {
// Redis client is already connected via config
console.log('✅ Using existing Redis connection\n');
// Delete all teacher-related cache patterns
const patterns = [
'teachers:list:*',
'teacher:*',
];
let totalDeleted = 0;
for (const pattern of patterns) {
console.log(`Searching for: ${pattern}`);
const keys = await redisClient.keys(pattern);
if (keys.length > 0) {
console.log(` Found ${keys.length} keys`);
await redisClient.del(...keys);
totalDeleted += keys.length;
console.log(` ✅ Deleted ${keys.length} keys\n`);
} else {
console.log(` No keys found\n`);
}
}
console.log('='.repeat(60));
console.log(`✅ Cache cleared! Total deleted: ${totalDeleted} keys`);
console.log('='.repeat(60));
process.exit(0);
} catch (error) {
console.error('❌ Error:', error.message);
process.exit(1);
}
}
clearTeacherCache();

111
scripts/create-test-user.js Normal file
View File

@@ -0,0 +1,111 @@
const { UsersAuth, UserProfile } = require('../models');
const { initializeDatabase } = require('../config/database');
const { setupRelationships } = require('../models');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
/**
* Script tạo user test để thử nghiệm login
*/
async function createTestUser() {
try {
console.log('🚀 Bắt đầu tạo user test...\n');
// Khởi tạo database
await initializeDatabase();
setupRelationships();
// Test credentials
const testUsers = [
{
username: 'admin',
email: 'admin@senaai.tech',
password: 'admin123',
full_name: 'Administrator',
phone: '0900000001',
},
{
username: 'teacher1',
email: 'teacher1@senaai.tech',
password: 'teacher123',
full_name: 'Giáo viên Test',
phone: '0900000002',
},
{
username: 'student1',
email: 'student1@senaai.tech',
password: 'student123',
full_name: 'Học sinh Test',
phone: '0900000003',
},
];
for (const userData of testUsers) {
console.log(`📝 Đang tạo user: ${userData.username}...`);
// Kiểm tra user đã tồn tại chưa
const existingUser = await UsersAuth.findOne({
where: {
[require('sequelize').Op.or]: [
{ username: userData.username },
{ email: userData.email },
],
},
});
if (existingUser) {
console.log(` ⚠️ User ${userData.username} đã tồn tại, bỏ qua.\n`);
continue;
}
// Hash password
const salt = crypto.randomBytes(16).toString('hex');
const passwordHash = await bcrypt.hash(userData.password + salt, 10);
// Tạo user
const newUser = await UsersAuth.create({
username: userData.username,
email: userData.email,
password_hash: passwordHash,
salt,
qr_secret: crypto.randomBytes(32).toString('hex'),
is_active: true,
});
// Tạo profile
await UserProfile.create({
user_id: newUser.id,
full_name: userData.full_name,
phone: userData.phone,
});
console.log(` ✅ Đã tạo user: ${userData.username}`);
console.log(` Email: ${userData.email}`);
console.log(` Password: ${userData.password}\n`);
}
console.log('✅ Hoàn tất tạo test users!\n');
console.log('═══════════════════════════════════════════════════');
console.log('📋 THÔNG TIN ĐĂNG NHẬP TEST:');
console.log('═══════════════════════════════════════════════════');
testUsers.forEach(user => {
console.log(`\n👤 ${user.full_name}`);
console.log(` Username: ${user.username}`);
console.log(` Email: ${user.email}`);
console.log(` Password: ${user.password}`);
});
console.log('\n═══════════════════════════════════════════════════');
console.log('\n🌐 Mở trình duyệt và truy cập:');
console.log(' http://localhost:4000/login.html');
console.log('\n💡 Sử dụng thông tin trên để đăng nhập!\n');
process.exit(0);
} catch (error) {
console.error('❌ Lỗi:', error.message);
console.error(error);
process.exit(1);
}
}
// Run script
createTestUser();

View File

@@ -0,0 +1,22 @@
const { sequelize } = require('../config/database');
async function dropTables() {
try {
console.log('🔄 Dropping old grade tables...');
await sequelize.query('DROP TABLE IF EXISTS grade_history');
await sequelize.query('DROP TABLE IF EXISTS grade_summaries');
await sequelize.query('DROP TABLE IF EXISTS grades');
await sequelize.query('DROP TABLE IF EXISTS grade_items');
await sequelize.query('DROP TABLE IF EXISTS grade_categories');
await sequelize.query('DROP TABLE IF EXISTS leave_requests');
console.log('✅ Tables dropped successfully');
process.exit(0);
} catch (error) {
console.error('❌ Error dropping tables:', error.message);
process.exit(1);
}
}
dropTables();

View File

@@ -0,0 +1,267 @@
/**
* Script: Dump và Sync All Teachers
* Mục đích: Kiểm tra và tạo user_profile cho TẤT CẢ teachers trong hệ thống
*
* Usage:
* node src/scripts/dump-and-sync-teachers.js
* node src/scripts/dump-and-sync-teachers.js --auto-fix
*/
require('dotenv').config();
const { sequelize, TeacherDetail, UserProfile, UsersAuth } = require('../models');
const teacherProfileService = require('../services/teacherProfileService');
async function dumpAllTeachers() {
console.log('📊 Dumping All Teachers Information...\n');
console.log('='.repeat(80));
try {
// Get all teachers (without join to avoid association issues)
const teachers = await TeacherDetail.findAll({
order: [['created_at', 'ASC']],
});
console.log(`\n📈 Total Teachers: ${teachers.length}\n`);
const withProfile = [];
const withoutProfile = [];
const invalidUserId = [];
for (const teacher of teachers) {
const teacherInfo = {
id: teacher.id,
user_id: teacher.user_id,
teacher_code: teacher.teacher_code,
teacher_type: teacher.teacher_type,
status: teacher.status,
created_at: teacher.created_at,
};
// Check if user_id exists in users_auth
const userAuth = await UsersAuth.findByPk(teacher.user_id, {
attributes: ['id', 'username', 'email', 'is_active'],
});
if (!userAuth) {
invalidUserId.push({
...teacherInfo,
issue: 'user_id not found in users_auth',
});
continue;
}
teacherInfo.username = userAuth.username;
teacherInfo.email = userAuth.email;
teacherInfo.is_active = userAuth.is_active;
// Check if profile exists
const userProfile = await UserProfile.findOne({
where: { user_id: teacher.user_id },
});
if (userProfile) {
teacherInfo.profile = {
id: userProfile.id,
full_name: userProfile.full_name,
phone: userProfile.phone,
needs_update: userProfile.etc?.needs_profile_update,
};
withProfile.push(teacherInfo);
} else {
withoutProfile.push(teacherInfo);
}
}
// Display results
console.log('✅ Teachers WITH Profile:', withProfile.length);
if (withProfile.length > 0) {
console.log('\nSample (first 5):');
withProfile.slice(0, 5).forEach((t, i) => {
console.log(` ${i + 1}. ${t.teacher_code} (${t.teacher_type}) - ${t.username}`);
console.log(` Profile: ${t.profile.full_name} | Phone: ${t.profile.phone || 'N/A'}`);
});
if (withProfile.length > 5) {
console.log(` ... and ${withProfile.length - 5} more\n`);
}
}
console.log('\n❌ Teachers WITHOUT Profile:', withoutProfile.length);
if (withoutProfile.length > 0) {
console.log('\nList:');
withoutProfile.forEach((t, i) => {
console.log(` ${i + 1}. ${t.teacher_code} (${t.teacher_type}) - ${t.username} - user_id: ${t.user_id}`);
});
}
console.log('\n⚠ Teachers with Invalid user_id:', invalidUserId.length);
if (invalidUserId.length > 0) {
console.log('\nList:');
invalidUserId.forEach((t, i) => {
console.log(` ${i + 1}. ${t.teacher_code} - ${t.issue} - user_id: ${t.user_id}`);
});
}
console.log('\n' + '='.repeat(80));
return {
total: teachers.length,
withProfile: withProfile.length,
withoutProfile: withoutProfile.length,
invalidUserId: invalidUserId.length,
teachers: {
withProfile,
withoutProfile,
invalidUserId,
}
};
} catch (error) {
console.error('❌ Dump failed:', error.message);
throw error;
}
}
async function autoFixProfiles(teachersData) {
console.log('\n🔧 Starting Auto-Fix for Teachers without Profile...\n');
const { withoutProfile } = teachersData.teachers;
if (withoutProfile.length === 0) {
console.log('✅ No teachers need fixing. All have profiles!\n');
return { success: 0, failed: 0 };
}
console.log(`📋 Processing ${withoutProfile.length} teachers...\n`);
const results = {
success: [],
failed: [],
};
for (const teacher of withoutProfile) {
try {
console.log(`Processing: ${teacher.teacher_code} (${teacher.username})...`);
// Create profile using service
const userProfile = await UserProfile.create({
user_id: teacher.user_id,
full_name: teacherProfileService._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,
username: teacher.username,
synced_from: 'dump-and-sync-script',
needs_profile_update: true,
synced_at: new Date().toISOString(),
}
});
results.success.push({
teacher_code: teacher.teacher_code,
username: teacher.username,
user_id: teacher.user_id,
profile_id: userProfile.id,
full_name: userProfile.full_name,
});
console.log(` ✅ Created profile: ${userProfile.full_name}\n`);
} catch (error) {
results.failed.push({
teacher_code: teacher.teacher_code,
username: teacher.username,
error: error.message,
});
console.log(` ❌ Failed: ${error.message}\n`);
}
}
// Summary
console.log('\n' + '='.repeat(80));
console.log('📊 AUTO-FIX SUMMARY');
console.log('='.repeat(80));
console.log(`✅ Success: ${results.success.length}`);
console.log(`❌ Failed: ${results.failed.length}`);
if (results.success.length > 0) {
console.log('\n✅ Successfully Created Profiles:');
results.success.forEach((item, i) => {
console.log(` ${i + 1}. ${item.teacher_code}${item.full_name}`);
console.log(` user_id: ${item.user_id} | profile_id: ${item.profile_id}`);
});
}
if (results.failed.length > 0) {
console.log('\n❌ Failed:');
results.failed.forEach((item, i) => {
console.log(` ${i + 1}. ${item.teacher_code} - ${item.error}`);
});
}
console.log('\n' + '='.repeat(80));
return {
success: results.success.length,
failed: results.failed.length,
details: results,
};
}
async function main() {
console.log('\n🚀 Teacher Profile Dump & Sync Tool\n');
try {
// Parse arguments
const args = process.argv.slice(2);
const autoFix = args.includes('--auto-fix');
// Connect to database
await sequelize.authenticate();
console.log('✅ Database connected\n');
// Step 1: Dump all teachers
const dumpResults = await dumpAllTeachers();
// Step 2: Ask for auto-fix if not specified
if (!autoFix && dumpResults.withoutProfile > 0) {
console.log('\n⚠ Found teachers without profile!');
console.log('💡 Run with --auto-fix to create profiles automatically:');
console.log(' node src/scripts/dump-and-sync-teachers.js --auto-fix\n');
process.exit(0);
}
// Step 3: Auto-fix if requested
if (autoFix && dumpResults.withoutProfile > 0) {
const fixResults = await autoFixProfiles(dumpResults);
console.log('\n🎉 COMPLETED!');
console.log(` Total: ${dumpResults.total} teachers`);
console.log(` Fixed: ${fixResults.success} profiles`);
console.log(` Already had: ${dumpResults.withProfile} profiles`);
console.log(` Failed: ${fixResults.failed} profiles\n`);
} else if (autoFix) {
console.log('\n✅ All teachers already have profiles! Nothing to fix.\n');
}
process.exit(0);
} catch (error) {
console.error('\n❌ Error:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// Run
main();

View File

@@ -0,0 +1,193 @@
/**
* Script: Export Teacher Profiles to JSON
* Xuất chi tiết thông tin tất cả teachers ra file JSON
*
* Usage:
* node src/scripts/export-teachers-info.js
* node src/scripts/export-teachers-info.js --output=teachers.json
*/
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const { sequelize, TeacherDetail, UserProfile, UsersAuth } = require('../models');
async function exportTeachersInfo() {
console.log('📤 Exporting All Teachers Information...\n');
try {
await sequelize.authenticate();
console.log('✅ Database connected\n');
// Get all teachers
const teachers = await TeacherDetail.findAll({
order: [['created_at', 'ASC']],
});
console.log(`📊 Found ${teachers.length} teachers\n`);
console.log('Processing...\n');
const exportData = [];
for (const teacher of teachers) {
// Get auth info
const userAuth = await UsersAuth.findByPk(teacher.user_id, {
attributes: ['id', 'username', 'email', 'is_active', 'last_login', 'login_count', 'qr_version'],
});
// Get profile info
const userProfile = await UserProfile.findOne({
where: { user_id: teacher.user_id },
});
const teacherData = {
// Teacher Details
teacher_id: teacher.id,
teacher_code: teacher.teacher_code,
teacher_type: teacher.teacher_type,
status: teacher.status,
qualification: teacher.qualification,
specialization: teacher.specialization,
hire_date: teacher.hire_date,
training_hours: teacher.training_hours,
last_training_date: teacher.last_training_date,
training_score_avg: teacher.training_score_avg,
skill_tags: teacher.skill_tags,
certifications: teacher.certifications,
created_at: teacher.created_at,
updated_at: teacher.updated_at,
// Auth Info
user_id: teacher.user_id,
username: userAuth?.username,
email: userAuth?.email,
is_active: userAuth?.is_active,
last_login: userAuth?.last_login,
login_count: userAuth?.login_count,
qr_version: userAuth?.qr_version,
// Profile Info
has_profile: !!userProfile,
profile: userProfile ? {
profile_id: userProfile.id,
full_name: userProfile.full_name,
first_name: userProfile.first_name,
last_name: userProfile.last_name,
phone: userProfile.phone,
date_of_birth: userProfile.date_of_birth,
gender: userProfile.gender,
address: userProfile.address,
school_id: userProfile.school_id,
city: userProfile.city,
district: userProfile.district,
avatar_url: userProfile.avatar_url,
etc: userProfile.etc,
created_at: userProfile.created_at,
updated_at: userProfile.updated_at,
} : null,
};
exportData.push(teacherData);
}
// Summary statistics
const stats = {
total_teachers: exportData.length,
with_profile: exportData.filter(t => t.has_profile).length,
without_profile: exportData.filter(t => !t.has_profile).length,
active: exportData.filter(t => t.is_active).length,
inactive: exportData.filter(t => !t.is_active).length,
by_type: {
foreign: exportData.filter(t => t.teacher_type === 'foreign').length,
homeroom: exportData.filter(t => t.teacher_type === 'homeroom').length,
subject: exportData.filter(t => t.teacher_type === 'subject').length,
assistant: exportData.filter(t => t.teacher_type === 'assistant').length,
},
by_status: {
active: exportData.filter(t => t.status === 'active').length,
on_leave: exportData.filter(t => t.status === 'on_leave').length,
resigned: exportData.filter(t => t.status === 'resigned').length,
},
};
// Parse output filename
const args = process.argv.slice(2);
const outputArg = args.find(arg => arg.startsWith('--output='));
const outputFileName = outputArg ? outputArg.split('=')[1] : 'teachers-export.json';
const outputPath = path.join(process.cwd(), '..', 'data', outputFileName);
// Prepare export object
const exportObject = {
exported_at: new Date().toISOString(),
statistics: stats,
teachers: exportData,
};
// Write to file
fs.writeFileSync(outputPath, JSON.stringify(exportObject, null, 2), 'utf8');
console.log('✅ Export completed!\n');
console.log('📊 Statistics:');
console.log('='.repeat(60));
console.log(`Total Teachers: ${stats.total_teachers}`);
console.log(` ✅ With Profile: ${stats.with_profile}`);
console.log(` ❌ Without Profile: ${stats.without_profile}`);
console.log(` 🟢 Active: ${stats.active}`);
console.log(` 🔴 Inactive: ${stats.inactive}`);
console.log('\nBy Teacher Type:');
console.log(` Foreign: ${stats.by_type.foreign}`);
console.log(` Homeroom: ${stats.by_type.homeroom}`);
console.log(` Subject: ${stats.by_type.subject}`);
console.log(` Assistant: ${stats.by_type.assistant}`);
console.log('\nBy Status:');
console.log(` Active: ${stats.by_status.active}`);
console.log(` On Leave: ${stats.by_status.on_leave}`);
console.log(` Resigned: ${stats.by_status.resigned}`);
console.log('='.repeat(60));
console.log(`\n📁 File saved: ${outputPath}`);
console.log(` Size: ${(fs.statSync(outputPath).size / 1024).toFixed(2)} KB\n`);
// Show sample data
console.log('📋 Sample Teacher Data (first 3):\n');
exportData.slice(0, 3).forEach((t, i) => {
console.log(`${i + 1}. ${t.teacher_code} (${t.teacher_type})`);
console.log(` Username: ${t.username}`);
console.log(` Email: ${t.email}`);
console.log(` Full Name: ${t.profile?.full_name || 'N/A'}`);
console.log(` Phone: ${t.profile?.phone || 'N/A'}`);
console.log(` Status: ${t.status} | Active: ${t.is_active}`);
console.log('');
});
return {
outputPath,
stats,
};
} catch (error) {
console.error('❌ Export failed:', error.message);
throw error;
}
}
async function main() {
console.log('\n🚀 Teacher Profile Export Tool\n');
try {
const result = await exportTeachersInfo();
console.log('🎉 Done! You can now view the exported file:\n');
console.log(` cat ${result.outputPath}`);
console.log(` or open it in a text editor/VS Code\n`);
process.exit(0);
} catch (error) {
console.error('\n❌ Error:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// Run
main();

305
scripts/generate-models.js Normal file
View File

@@ -0,0 +1,305 @@
/**
* Tạo nhanh các models còn lại
* Chạy script này để generate các file model
*/
const fs = require('fs');
const path = require('path');
const models = [
{
name: 'Permission',
tableName: 'permissions',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
permission_code: { type: DataTypes.STRING(50), unique: true, allowNull: false },
permission_name: { type: DataTypes.STRING(100), allowNull: false },
permission_description: { type: DataTypes.TEXT },
resource: { type: DataTypes.STRING(50), comment: 'Tài nguyên (students, grades, etc.)' },
action: { type: DataTypes.STRING(50), comment: 'Hành động (view, create, edit, delete)' },
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
`,
},
{
name: 'RolePermission',
tableName: 'role_permissions',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
role_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'roles', key: 'id' } },
permission_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'permissions', key: 'id' } },
`,
},
{
name: 'UserAssignment',
tableName: 'user_assignments',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users_auth', key: 'id' } },
role_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'roles', key: 'id' } },
school_id: { type: DataTypes.INTEGER, references: { model: 'schools', key: 'id' } },
class_id: { type: DataTypes.INTEGER, references: { model: 'classes', key: 'id' } },
is_primary: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Vai trò chính' },
valid_from: { type: DataTypes.DATE },
valid_until: { type: DataTypes.DATE },
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
`,
},
{
name: 'StudentDetail',
tableName: 'student_details',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
user_id: { type: DataTypes.INTEGER, unique: true, allowNull: false, references: { model: 'user_profiles', key: 'user_id' } },
student_code: { type: DataTypes.STRING(20), unique: true, allowNull: false },
enrollment_date: { type: DataTypes.DATE },
current_class_id: { type: DataTypes.INTEGER, references: { model: 'classes', key: 'id' } },
status: { type: DataTypes.ENUM('active', 'on_leave', 'graduated', 'dropped'), defaultValue: 'active' },
`,
},
{
name: 'TeacherDetail',
tableName: 'teacher_details',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
user_id: { type: DataTypes.INTEGER, unique: true, allowNull: false, references: { model: 'user_profiles', key: 'user_id' } },
teacher_code: { type: DataTypes.STRING(20), unique: true, allowNull: false },
teacher_type: { type: DataTypes.ENUM('foreign', 'assistant', 'homeroom', 'specialist'), allowNull: false },
qualification: { type: DataTypes.STRING(200) },
specialization: { type: DataTypes.STRING(200) },
hire_date: { type: DataTypes.DATE },
status: { type: DataTypes.ENUM('active', 'on_leave', 'resigned'), defaultValue: 'active' },
`,
},
{
name: 'ParentStudentMap',
tableName: 'parent_student_map',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
parent_id: { type: DataTypes.INTEGER, allowNull: false },
student_id: { type: DataTypes.INTEGER, allowNull: false },
relationship: { type: DataTypes.ENUM('father', 'mother', 'guardian', 'other'), allowNull: false },
is_primary_contact: { type: DataTypes.BOOLEAN, defaultValue: false },
`,
},
{
name: 'StaffContract',
tableName: 'staff_contracts',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
teacher_id: { type: DataTypes.INTEGER, allowNull: false },
school_id: { type: DataTypes.INTEGER, allowNull: false },
contract_type: { type: DataTypes.ENUM('full_time', 'part_time', 'contract'), allowNull: false },
start_date: { type: DataTypes.DATE, allowNull: false },
end_date: { type: DataTypes.DATE },
salary: { type: DataTypes.DECIMAL(12, 2) },
status: { type: DataTypes.ENUM('active', 'expired', 'terminated'), defaultValue: 'active' },
`,
},
{
name: 'AcademicYear',
tableName: 'academic_years',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
year_name: { type: DataTypes.STRING(50), allowNull: false },
start_date: { type: DataTypes.DATE, allowNull: false },
end_date: { type: DataTypes.DATE, allowNull: false },
semester_count: { type: DataTypes.INTEGER, defaultValue: 2 },
is_current: { type: DataTypes.BOOLEAN, defaultValue: false },
`,
},
{
name: 'Subject',
tableName: 'subjects',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
subject_code: { type: DataTypes.STRING(20), unique: true, allowNull: false },
subject_name: { type: DataTypes.STRING(100), allowNull: false },
subject_name_en: { type: DataTypes.STRING(100) },
description: { type: DataTypes.TEXT },
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
`,
},
{
name: 'Class',
tableName: 'classes',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
class_code: { type: DataTypes.STRING(20), unique: true, allowNull: false },
class_name: { type: DataTypes.STRING(100), allowNull: false },
school_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'schools', key: 'id' } },
academic_year_id: { type: DataTypes.INTEGER, allowNull: false },
grade_level: { type: DataTypes.INTEGER },
homeroom_teacher_id: { type: DataTypes.INTEGER },
max_students: { type: DataTypes.INTEGER, defaultValue: 30 },
current_students: { type: DataTypes.INTEGER, defaultValue: 0 },
`,
},
{
name: 'ClassSchedule',
tableName: 'class_schedules',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
class_id: { type: DataTypes.INTEGER, allowNull: false },
subject_id: { type: DataTypes.INTEGER, allowNull: false },
teacher_id: { type: DataTypes.INTEGER },
room_id: { type: DataTypes.INTEGER },
day_of_week: { type: DataTypes.INTEGER, comment: '1=Monday, 7=Sunday' },
period: { type: DataTypes.INTEGER, comment: 'Tiết học' },
start_time: { type: DataTypes.TIME },
end_time: { type: DataTypes.TIME },
`,
},
{
name: 'Room',
tableName: 'rooms',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
room_code: { type: DataTypes.STRING(20), allowNull: false },
room_name: { type: DataTypes.STRING(100), allowNull: false },
school_id: { type: DataTypes.INTEGER, allowNull: false },
room_type: { type: DataTypes.ENUM('classroom', 'lab', 'library', 'gym', 'other') },
capacity: { type: DataTypes.INTEGER },
floor: { type: DataTypes.INTEGER },
`,
},
{
name: 'AttendanceDaily',
tableName: 'attendance_daily',
fields: `
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: DataTypes.INTEGER, allowNull: false },
school_id: { type: DataTypes.INTEGER, allowNull: false },
class_id: { type: DataTypes.INTEGER },
attendance_date: { type: DataTypes.DATE, allowNull: false },
status: { type: DataTypes.ENUM('present', 'absent', 'late', 'excused'), allowNull: false },
check_in_time: { type: DataTypes.TIME },
check_out_time: { type: DataTypes.TIME },
notes: { type: DataTypes.TEXT },
`,
},
{
name: 'LeaveRequest',
tableName: 'leave_requests',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
student_id: { type: DataTypes.INTEGER, allowNull: false },
requested_by: { type: DataTypes.INTEGER, allowNull: false, comment: 'Parent user_id' },
leave_from: { type: DataTypes.DATE, allowNull: false },
leave_to: { type: DataTypes.DATE, allowNull: false },
reason: { type: DataTypes.TEXT, allowNull: false },
status: { type: DataTypes.ENUM('pending', 'approved', 'rejected'), defaultValue: 'pending' },
approved_by: { type: DataTypes.INTEGER },
approved_at: { type: DataTypes.DATE },
`,
},
{
name: 'GradeCategory',
tableName: 'grade_categories',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
category_code: { type: DataTypes.STRING(20), allowNull: false },
category_name: { type: DataTypes.STRING(100), allowNull: false },
subject_id: { type: DataTypes.INTEGER },
weight: { type: DataTypes.DECIMAL(5, 2), comment: 'Trọng số %' },
description: { type: DataTypes.TEXT },
`,
},
{
name: 'GradeItem',
tableName: 'grade_items',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
item_name: { type: DataTypes.STRING(200), allowNull: false },
category_id: { type: DataTypes.INTEGER, allowNull: false },
class_id: { type: DataTypes.INTEGER, allowNull: false },
max_score: { type: DataTypes.DECIMAL(5, 2), defaultValue: 100 },
exam_date: { type: DataTypes.DATE },
description: { type: DataTypes.TEXT },
`,
},
{
name: 'GradeSummary',
tableName: 'grade_summaries',
fields: `
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
student_id: { type: DataTypes.INTEGER, allowNull: false },
subject_id: { type: DataTypes.INTEGER, allowNull: false },
academic_year_id: { type: DataTypes.INTEGER, allowNull: false },
semester: { type: DataTypes.INTEGER },
average_score: { type: DataTypes.DECIMAL(5, 2) },
final_grade: { type: DataTypes.STRING(5) },
`,
},
{
name: 'GradeHistory',
tableName: 'grade_history',
fields: `
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
grade_id: { type: DataTypes.BIGINT, allowNull: false },
changed_by: { type: DataTypes.INTEGER, allowNull: false },
old_score: { type: DataTypes.DECIMAL(5, 2) },
new_score: { type: DataTypes.DECIMAL(5, 2) },
change_reason: { type: DataTypes.TEXT },
changed_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
`,
},
{
name: 'Notification',
tableName: 'notifications',
fields: `
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
title: { type: DataTypes.STRING(200), allowNull: false },
content: { type: DataTypes.TEXT, allowNull: false },
notification_type: { type: DataTypes.ENUM('system', 'school', 'class', 'personal'), allowNull: false },
priority: { type: DataTypes.ENUM('low', 'medium', 'high', 'urgent'), defaultValue: 'medium' },
target_audience: { type: DataTypes.JSONB, comment: 'Schools, classes, roles to send' },
scheduled_at: { type: DataTypes.DATE },
expires_at: { type: DataTypes.DATE },
`,
},
{
name: 'NotificationLog',
tableName: 'notification_logs',
fields: `
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
notification_id: { type: DataTypes.INTEGER, allowNull: false },
user_id: { type: DataTypes.INTEGER, allowNull: false },
sent_at: { type: DataTypes.DATE },
read_at: { type: DataTypes.DATE },
status: { type: DataTypes.ENUM('pending', 'sent', 'failed', 'read'), defaultValue: 'pending' },
`,
},
{
name: 'Message',
tableName: 'messages',
fields: `
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
sender_id: { type: DataTypes.INTEGER, allowNull: false },
recipient_id: { type: DataTypes.INTEGER, allowNull: false },
subject: { type: DataTypes.STRING(200) },
content: { type: DataTypes.TEXT, allowNull: false },
is_read: { type: DataTypes.BOOLEAN, defaultValue: false },
read_at: { type: DataTypes.DATE },
parent_message_id: { type: DataTypes.BIGINT, comment: 'For thread replies' },
`,
},
{
name: 'AuditLog',
tableName: 'audit_logs_system',
fields: `
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: DataTypes.INTEGER, allowNull: false },
action: { type: DataTypes.STRING(100), allowNull: false },
resource_type: { type: DataTypes.STRING(50) },
resource_id: { type: DataTypes.INTEGER },
old_values: { type: DataTypes.JSONB },
new_values: { type: DataTypes.JSONB },
ip_address: { type: DataTypes.INET },
user_agent: { type: DataTypes.TEXT },
performed_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
`,
},
];
console.log(`Tạo ${models.length} file models...`);
console.log('Các models cần tạo:', models.map(m => m.name).join(', '));

View File

@@ -0,0 +1,315 @@
const { sequelize } = require('../config/database');
const UsersAuth = require('../models/UsersAuth');
const UserProfile = require('../models/UserProfile');
const StudentDetail = require('../models/StudentDetail');
const School = require('../models/School');
const Class = require('../models/Class');
const bcrypt = require('bcrypt');
const fs = require('fs');
const path = require('path');
/**
* Script import học sinh trường Tiểu học Ấp Đình từ apdinh.tsv
* Chạy: node src/scripts/import-apdinh-students.js
*/
// Hàm hash password
async function hashPassword(password) {
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(password, salt);
return { hash, salt };
}
// Hàm chuyển tiếng Việt có dấu thành không dấu
function removeVietnameseTones(str) {
str = str.toLowerCase();
str = str.replace(/à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ/g, 'a');
str = str.replace(/è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ/g, 'e');
str = str.replace(/ì|í|ị|ỉ|ĩ/g, 'i');
str = str.replace(/ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ/g, 'o');
str = str.replace(/ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ/g, 'u');
str = str.replace(/ỳ|ý|ỵ|ỷ|ỹ/g, 'y');
str = str.replace(/đ/g, 'd');
return str;
}
// Hàm tạo username từ họ tên và ngày sinh
function generateUsername(fullName, dateOfBirth) {
// Chuyển tên thành không dấu và viết liền
const nameParts = fullName.trim().split(/\s+/);
const nameSlug = nameParts.map(part => removeVietnameseTones(part)).join('');
// Parse ngày sinh: 23/06/2019 -> 2306 (chỉ lấy ngày tháng)
const dateParts = dateOfBirth.split('/');
const dateSlug = dateParts[0] + dateParts[1]; // Chỉ lấy ngày và tháng
// Format: pri_apdinh_[tênkhôngdấu]_[ngàytháng]
return `pri_apdinh_${nameSlug}_${dateSlug}`.toLowerCase();
}
// Hàm parse ngày sinh từ format dd/mm/yyyy
function parseDate(dateStr) {
const [day, month, year] = dateStr.split('/');
return new Date(year, month - 1, day);
}
// Hàm tạo mật khẩu mặc định từ ngày sinh
function generateDefaultPassword(dateOfBirth) {
// Mật khẩu: ddmmyyyy
return dateOfBirth.replace(/\//g, '');
}
async function importApdinhStudents() {
try {
console.log('🚀 Bắt đầu import học sinh Trường Tiểu học Ấp Đình...\n');
// 1. Đọc file apdinh.tsv
const filePath = path.join(__dirname, '../../data/apdinh.tsv');
const fileContent = fs.readFileSync(filePath, 'utf8');
const lines = fileContent.split('\n').filter(line => line.trim());
// Bỏ header
const dataLines = lines.slice(1);
console.log(`📚 Tìm thấy ${dataLines.length} học sinh trong file\n`);
// Kết nối database
await sequelize.authenticate();
console.log('✅ Kết nối database thành công\n');
// 2. Tìm trường Tiểu học Ấp Đình
console.log('🔍 Đang tìm trường Tiểu học Ấp Đình...');
const school = await School.findOne({
where: sequelize.where(
sequelize.fn('LOWER', sequelize.col('school_name')),
'LIKE',
'%ấp đình%'
)
});
if (!school) {
throw new Error('❌ Không tìm thấy trường Tiểu học Ấp Đình trong database!');
}
console.log(`✅ Tìm thấy trường: ${school.school_name} (${school.school_code})\n`);
// 3. Lấy tất cả các lớp của trường Ấp Đình
console.log('🔍 Đang tải danh sách lớp...');
const classes = await Class.findAll({
where: { school_id: school.id }
});
if (classes.length === 0) {
throw new Error('❌ Không tìm thấy lớp nào của trường Ấp Đình! Vui lòng import classes trước.');
}
console.log(`✅ Tìm thấy ${classes.length} lớp\n`);
// Tạo map class_name -> class_id để tra cứu nhanh
const classMap = {};
classes.forEach(cls => {
// Lưu cả class_code và class_name
classMap[cls.class_code] = cls;
classMap[cls.class_name] = cls;
});
let importedCount = 0;
let skippedCount = 0;
const errors = [];
const classStats = {};
// 4. Parse và import từng học sinh
for (const line of dataLines) {
try {
// Parse TSV: STT\tLớp\tHọ Tên học sinh\tNgày sinh
const parts = line.split('\t');
if (parts.length < 4) {
console.log(`⚠️ Bỏ qua dòng không hợp lệ: ${line}`);
continue;
}
const [stt, className, fullName, dateOfBirth] = parts.map(p => p.trim());
// Bỏ qua nếu thiếu thông tin
if (!fullName || !dateOfBirth || !className) {
console.log(`⚠️ Bỏ qua dòng thiếu thông tin: ${line}`);
continue;
}
// Tìm class_id từ className (format: 1_1, 1_2, etc.)
const classKey = className.replace('_', ''); // 1_1 -> 11
let classObj = null;
// Thử tìm class theo nhiều cách
for (const cls of classes) {
if (cls.class_code.includes(className) ||
cls.class_name.includes(className) ||
cls.class_code.includes(classKey)) {
classObj = cls;
break;
}
}
if (!classObj) {
console.log(`⚠️ Không tìm thấy lớp ${className}, bỏ qua: ${fullName}`);
errors.push({
line,
error: `Không tìm thấy lớp ${className}`
});
continue;
}
// Tạo username và password
const username = generateUsername(fullName, dateOfBirth);
const password = generateDefaultPassword(dateOfBirth);
const email = `${username}@apdinh.edu.vn`;
const studentCode = username.toUpperCase();
console.log(`📖 [${className}] Đang xử lý: ${fullName} (${username})`);
// Kiểm tra user đã tồn tại
const existingAuth = await UsersAuth.findOne({
where: { username }
});
if (existingAuth) {
console.log(` ⚠️ User đã tồn tại, bỏ qua\n`);
skippedCount++;
continue;
}
// Hash password
const { hash, salt } = await hashPassword(password);
// Parse date of birth
const dob = parseDate(dateOfBirth);
// Transaction để đảm bảo tính toàn vẹn dữ liệu
await sequelize.transaction(async (t) => {
// 5a. Tạo UsersAuth
const userAuth = await UsersAuth.create({
username,
email,
password_hash: hash,
salt,
role: 'student',
is_active: true,
is_verified: true,
}, { transaction: t });
// 5b. Tạo UserProfile
const nameParts = fullName.trim().split(/\s+/);
const lastName = nameParts[0];
const firstName = nameParts.slice(1).join(' ');
const userProfile = await UserProfile.create({
user_id: userAuth.id,
full_name: fullName,
first_name: firstName || fullName,
last_name: lastName,
date_of_birth: dob,
phone: '',
school_id: school.id,
address: 'Huyện Hóc Môn',
city: 'Thành phố Hồ Chí Minh',
district: 'Huyện Hóc Môn',
}, { transaction: t });
// 5c. Tạo StudentDetail với class_id
await StudentDetail.create({
user_id: userAuth.id,
student_code: studentCode,
enrollment_date: new Date(),
current_class_id: classObj.id,
status: 'active',
}, { transaction: t });
// Cập nhật current_students trong class
await classObj.increment('current_students', { transaction: t });
});
console.log(` ✅ Tạo thành công (Lớp: ${classObj.class_name}, User: ${username}, Pass: ${password})\n`);
importedCount++;
// Thống kê theo lớp
if (!classStats[classObj.class_name]) {
classStats[classObj.class_name] = 0;
}
classStats[classObj.class_name]++;
} catch (error) {
console.error(` ❌ Lỗi: ${error.message}\n`);
errors.push({
line,
error: error.message
});
}
}
// Tổng kết
console.log('\n' + '='.repeat(70));
console.log('📊 KẾT QUẢ IMPORT TRƯỜNG TIỂU HỌC ẤP ĐÌNH');
console.log('='.repeat(70));
console.log(`✅ Học sinh mới: ${importedCount}`);
console.log(`⚠️ Đã tồn tại: ${skippedCount}`);
console.log(`❌ Lỗi: ${errors.length}`);
console.log('='.repeat(70));
// Thống kê theo lớp
console.log('\n📚 THỐNG KÊ THEO LỚP:');
const sortedClasses = Object.keys(classStats).sort();
for (const className of sortedClasses) {
console.log(` ${className}: ${classStats[className]} học sinh`);
}
if (errors.length > 0) {
console.log('\n⚠ CHI TIẾT LỖI:');
errors.slice(0, 10).forEach((err, index) => {
console.log(`\n${index + 1}. ${err.line}`);
console.log(` Lỗi: ${err.error}`);
});
if (errors.length > 10) {
console.log(`\n ... và ${errors.length - 10} lỗi khác`);
}
}
// Thống kê tổng hợp - đếm qua UserProfile
const totalStudents = await UserProfile.count({
where: { school_id: school.id }
});
console.log('\n📊 THỐNG KÊ TỔNG HỢP:');
console.log(` Tổng số học sinh trường Ấp Đình: ${totalStudents}`);
console.log(` Tổng số lớp: ${classes.length}`);
console.log('\n✅ Hoàn thành import dữ liệu!\n');
// Xuất file credentials để giáo viên có thể tra cứu
const credentialsPath = path.join(__dirname, '../../data/apdinh-credentials.txt');
let credentialsContent = '# Thông tin đăng nhập học sinh Trường Tiểu học Ấp Đình\n';
credentialsContent += '# Format: Lớp | Họ tên | Username | Password\n\n';
console.log(`📄 Đang tạo file thông tin đăng nhập: apdinh-credentials.txt\n`);
} catch (error) {
console.error('❌ Lỗi nghiêm trọng:', error);
throw error;
} finally {
await sequelize.close();
console.log('🔌 Đã đóng kết nối database');
}
}
// Chạy script
if (require.main === module) {
importApdinhStudents()
.then(() => {
console.log('\n🎉 Script hoàn thành thành công!');
process.exit(0);
})
.catch((error) => {
console.error('\n💥 Script thất bại:', error);
process.exit(1);
});
}
module.exports = { importApdinhStudents };

View File

@@ -0,0 +1,201 @@
const { sequelize } = require('../config/database');
const UsersAuth = require('../models/UsersAuth');
const UserProfile = require('../models/UserProfile');
const TeacherDetail = require('../models/TeacherDetail');
const School = require('../models/School');
const bcrypt = require('bcrypt');
const fs = require('fs');
const path = require('path');
/**
* Script import giáo viên nước ngoài từ fteacher.txt
* Chạy: node src/scripts/import-foreign-teachers.js
*/
// Hàm hash password
async function hashPassword(password) {
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(password, salt);
return { hash, salt };
}
// Hàm parse tên từ username
function parseNameFromUsername(username) {
// tdv_altaf -> Altaf
const name = username.replace('tdv_', '').replace(/_/g, ' ');
return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
}
// Hàm tạo email từ username
function generateEmail(username) {
// tdv_altaf -> altaf@senaai.edu.vn
const name = username.replace('tdv_', '');
return `${name}@senaai.edu.vn`;
}
async function importForeignTeachers() {
try {
console.log('🚀 Bắt đầu import giáo viên nước ngoài...\n');
// Đọc file fteacher.txt
const filePath = path.join(__dirname, '../../data/fteacher.txt');
const fileContent = fs.readFileSync(filePath, 'utf8');
const lines = fileContent.split('\n').filter(line => line.trim());
console.log(`📚 Tìm thấy ${lines.length} giáo viên trong file\n`);
// Kết nối database
await sequelize.authenticate();
console.log('✅ Kết nối database thành công\n');
// Lấy thông tin trung tâm SenaAI
const senaAI = await School.findOne({
where: { school_code: 'SENAAI' }
});
if (!senaAI) {
throw new Error('❌ Không tìm thấy trung tâm SenaAI trong database. Vui lòng chạy add-senaai-center.js trước!');
}
let importedCount = 0;
let skippedCount = 0;
const errors = [];
// Parse từng dòng
for (const line of lines) {
try {
// Parse: Created user: tdv_altaf with password: 13082025
const match = line.match(/Created user: (\S+) with password: (\S+)/);
if (!match) {
console.log(`⚠️ Bỏ qua dòng không hợp lệ: ${line}`);
continue;
}
const [, username, password] = match;
const fullName = parseNameFromUsername(username);
const email = generateEmail(username);
const teacherCode = username.toUpperCase();
console.log(`📖 Đang xử lý: ${fullName} (${username})`);
// Kiểm tra user đã tồn tại
const existingAuth = await UsersAuth.findOne({
where: { username }
});
if (existingAuth) {
console.log(` ⚠️ User đã tồn tại, bỏ qua\n`);
skippedCount++;
continue;
}
// Hash password
const { hash, salt } = await hashPassword(password);
// Transaction để đảm bảo tính toàn vẹn dữ liệu
await sequelize.transaction(async (t) => {
// 1. Tạo UsersAuth
const userAuth = await UsersAuth.create({
username,
email,
password_hash: hash,
salt,
role: 'teacher',
is_active: true,
is_verified: true,
}, { transaction: t });
// 2. Tạo UserProfile
const userProfile = await UserProfile.create({
user_id: userAuth.id,
full_name: fullName,
first_name: fullName.split(' ')[0],
last_name: fullName.split(' ').slice(1).join(' ') || fullName,
phone: '',
school_id: senaAI.id,
address: 'SenaAI Education Center',
city: 'Thành phố Hồ Chí Minh',
district: 'Đang cập nhật',
}, { transaction: t });
// 3. Tạo TeacherDetail
await TeacherDetail.create({
user_id: userAuth.id,
teacher_code: teacherCode,
teacher_type: 'foreign',
qualification: 'Native Speaker',
specialization: 'English Language Teaching',
hire_date: new Date(),
status: 'active',
skill_tags: [
{ name: 'English Teaching', level: 'expert' },
{ name: 'Communication', level: 'expert' }
],
certifications: [],
training_hours: 0,
}, { transaction: t });
});
console.log(` ✅ Tạo thành công (Email: ${email})\n`);
importedCount++;
} catch (error) {
console.error(` ❌ Lỗi: ${error.message}\n`);
errors.push({
line,
error: error.message
});
}
}
// Tổng kết
console.log('\n' + '='.repeat(60));
console.log('📊 KẾT QUẢ IMPORT');
console.log('='.repeat(60));
console.log(`✅ Giáo viên mới: ${importedCount}`);
console.log(`⚠️ Đã tồn tại: ${skippedCount}`);
console.log(`❌ Lỗi: ${errors.length}`);
console.log('='.repeat(60));
if (errors.length > 0) {
console.log('\n⚠ CHI TIẾT LỖI:');
errors.forEach((err, index) => {
console.log(`\n${index + 1}. ${err.line}`);
console.log(` Lỗi: ${err.error}`);
});
}
// Thống kê giáo viên nước ngoài
const foreignTeachersCount = await TeacherDetail.count({
where: { teacher_type: 'foreign' }
});
console.log('\n📊 THỐNG KÊ:');
console.log(` Tổng số giáo viên nước ngoài: ${foreignTeachersCount}`);
console.log('\n✅ Hoàn thành import dữ liệu!\n');
} catch (error) {
console.error('❌ Lỗi nghiêm trọng:', error);
throw error;
} finally {
await sequelize.close();
console.log('🔌 Đã đóng kết nối database');
}
}
// Chạy script
if (require.main === module) {
importForeignTeachers()
.then(() => {
console.log('\n🎉 Script hoàn thành thành công!');
process.exit(0);
})
.catch((error) => {
console.error('\n💥 Script thất bại:', error);
process.exit(1);
});
}
module.exports = { importForeignTeachers };

183
scripts/import-schools.js Normal file
View File

@@ -0,0 +1,183 @@
const { sequelize } = require('../config/database');
const School = require('../models/School');
const Class = require('../models/Class');
const fs = require('fs');
const path = require('path');
/**
* Script import dữ liệu trường và lớp học từ schools.json
* Chạy: node src/scripts/import-schools.js
*/
async function importSchoolsData() {
try {
console.log('🚀 Bắt đầu import dữ liệu trường học...\n');
// Đọc file schools.json
const schoolsDataPath = path.join(__dirname, '../../data/schools.json');
const schoolsData = JSON.parse(fs.readFileSync(schoolsDataPath, 'utf8'));
console.log(`📚 Tìm thấy ${schoolsData.length} trường học trong file\n`);
// Kết nối database
await sequelize.authenticate();
console.log('✅ Kết nối database thành công\n');
// Đồng bộ models (tạo bảng nếu chưa có)
await sequelize.sync({ alter: false });
console.log('✅ Đồng bộ database models thành công\n');
let importedSchools = 0;
let importedClasses = 0;
let skippedSchools = 0;
const errors = [];
// Import từng trường
for (const schoolData of schoolsData) {
try {
console.log(`📖 Đang xử lý: ${schoolData.school_name} (${schoolData.school_code})`);
// Kiểm tra trường đã tồn tại chưa
let school = await School.findOne({
where: { school_code: schoolData.school_code }
});
if (school) {
console.log(` ⚠️ Trường đã tồn tại, cập nhật thông tin...`);
// Cập nhật thông tin trường
await school.update({
school_name: schoolData.school_name,
school_type: schoolData.school_type,
address: schoolData.address,
city: schoolData.city,
district: schoolData.district,
phone: schoolData.phone,
});
skippedSchools++;
} else {
// Tạo mới trường
school = await School.create({
school_code: schoolData.school_code,
school_name: schoolData.school_name,
school_type: schoolData.school_type,
address: schoolData.address,
city: schoolData.city,
district: schoolData.district,
phone: schoolData.phone,
is_active: true,
});
importedSchools++;
console.log(` ✅ Tạo mới trường thành công`);
}
// Import các lớp học
if (schoolData.classes && Array.isArray(schoolData.classes)) {
console.log(` 📝 Đang import ${schoolData.classes.length} lớp học...`);
for (const classData of schoolData.classes) {
try {
// Xử lý định dạng classes khác nhau trong JSON
let classNames = [];
let gradeLevel = null;
if (classData.name) {
// Format 1: { "grade": 1, "name": "1A" }
classNames = [classData.name];
gradeLevel = classData.grade;
} else if (classData.names) {
// Format 2: { "grade": 3, "names": ["3,1", "3,2", ...] }
classNames = classData.names;
gradeLevel = classData.grade;
}
// Tạo từng lớp
for (const className of classNames) {
// Tạo class_code duy nhất
const classCode = `${schoolData.school_code}_${className.replace(/[\/,\s.]/g, '_')}`;
// Kiểm tra lớp đã tồn tại chưa
const existingClass = await Class.findOne({
where: { class_code: classCode }
});
if (!existingClass) {
await Class.create({
class_code: classCode,
class_name: className,
school_id: school.id,
academic_year_id: '00000000-0000-0000-0000-000000000000', // Temporary UUID
grade_level: gradeLevel,
max_students: 35,
current_students: 0,
});
importedClasses++;
}
}
} catch (classError) {
console.error(` ❌ Lỗi import lớp: ${classError.message}`);
errors.push({
school: schoolData.school_name,
class: classData,
error: classError.message
});
}
}
}
console.log('');
} catch (schoolError) {
console.error(` ❌ Lỗi import trường: ${schoolError.message}\n`);
errors.push({
school: schoolData.school_name,
error: schoolError.message
});
}
}
// Tổng kết
console.log('\n' + '='.repeat(60));
console.log('📊 KẾT QUẢ IMPORT');
console.log('='.repeat(60));
console.log(`✅ Trường học mới: ${importedSchools}`);
console.log(`⚠️ Trường đã tồn tại: ${skippedSchools}`);
console.log(`✅ Lớp học mới: ${importedClasses}`);
console.log(`❌ Lỗi: ${errors.length}`);
console.log('='.repeat(60));
if (errors.length > 0) {
console.log('\n⚠ CHI TIẾT LỖI:');
errors.forEach((err, index) => {
console.log(`\n${index + 1}. ${err.school || 'Unknown'}`);
console.log(` Lỗi: ${err.error}`);
if (err.class) {
console.log(` Lớp: ${JSON.stringify(err.class)}`);
}
});
}
console.log('\n✅ Hoàn thành import dữ liệu!\n');
} catch (error) {
console.error('❌ Lỗi nghiêm trọng:', error);
throw error;
} finally {
await sequelize.close();
console.log('🔌 Đã đóng kết nối database');
}
}
// Chạy script
if (require.main === module) {
importSchoolsData()
.then(() => {
console.log('\n🎉 Script hoàn thành thành công!');
process.exit(0);
})
.catch((error) => {
console.error('\n💥 Script thất bại:', error);
process.exit(1);
});
}
module.exports = { importSchoolsData };

67
scripts/sync-database.js Normal file
View File

@@ -0,0 +1,67 @@
const { sequelize, setupRelationships, syncDatabase } = require('../models');
/**
* Script to sync all Sequelize models to database
* This will create all tables with proper foreign keys and indexes
*/
async function syncAllTables() {
try {
console.log('🚀 Starting database synchronization...\n');
// Setup model relationships first
setupRelationships();
console.log('✅ Model relationships configured\n');
// Test database connection
await sequelize.authenticate();
console.log('✅ Database connection established\n');
// Show current tables
const [tablesBefore] = await sequelize.query(`
SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME;
`);
console.log('📊 Tables before sync:', tablesBefore.length);
console.log(tablesBefore.map(t => t.TABLE_NAME).join(', '), '\n');
// Sync database (force: false = don't drop existing tables)
// Use { alter: true } to modify existing tables without dropping data
// Use { force: true } to drop and recreate all tables (CAREFUL: data loss!)
console.log('⚙️ Syncing database with alter mode...');
await syncDatabase({ alter: true });
// Show tables after sync
const [tablesAfter] = await sequelize.query(`
SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME;
`);
console.log('\n📊 Tables after sync:', tablesAfter.length);
console.log(tablesAfter.map(t => t.TABLE_NAME).join(', '), '\n');
// Show newly created tables
const newTables = tablesAfter.filter(
t => !tablesBefore.some(b => b.TABLE_NAME === t.TABLE_NAME)
);
if (newTables.length > 0) {
console.log('✨ Newly created tables:', newTables.length);
console.log(newTables.map(t => t.TABLE_NAME).join(', '), '\n');
}
console.log('╔════════════════════════════════════════════════════════╗');
console.log('║ ✅ DATABASE SYNC COMPLETED SUCCESSFULLY ║');
console.log('╚════════════════════════════════════════════════════════╝');
process.exit(0);
} catch (error) {
console.error('\n❌ Database sync failed:');
console.error(error);
process.exit(1);
}
}
// Run sync
syncAllTables();

View File

@@ -0,0 +1,91 @@
/**
* Script: Sync Teacher Profiles
* Mục đích: Tạo user_profile cho các teacher đã có sẵn trong hệ thống
*
* Usage:
* node src/scripts/sync-teacher-profiles.js
* node src/scripts/sync-teacher-profiles.js --teacher-id=uuid
* node src/scripts/sync-teacher-profiles.js --dry-run
*/
require('dotenv').config();
const teacherProfileService = require('../services/teacherProfileService');
const { sequelize } = require('../config/database');
async function main() {
console.log('🚀 Starting Teacher Profile Sync...\n');
try {
// Parse command line arguments
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const teacherIdArg = args.find(arg => arg.startsWith('--teacher-id='));
const teacherId = teacherIdArg ? teacherIdArg.split('=')[1] : null;
if (dryRun) {
console.log('⚠️ DRY RUN MODE - No changes will be made\n');
}
if (teacherId) {
console.log(`🎯 Syncing specific teacher: ${teacherId}\n`);
} else {
console.log('🔄 Syncing all teachers without profiles\n');
}
// Test database connection
await sequelize.authenticate();
console.log('✅ Database connection established\n');
if (dryRun) {
console.log('Dry run completed. Use without --dry-run to apply changes.');
process.exit(0);
}
// Execute sync
console.log('Processing...\n');
const results = await teacherProfileService.syncExistingTeachers(teacherId);
// Display results
console.log('\n' + '='.repeat(60));
console.log('📊 SYNC RESULTS');
console.log('='.repeat(60));
console.log(`\n✅ Success: ${results.success.length}`);
if (results.success.length > 0) {
results.success.forEach((item, index) => {
console.log(` ${index + 1}. ${item.teacher_code} → Profile created (user_id: ${item.user_id})`);
});
}
console.log(`\n⏭️ Skipped: ${results.skipped.length}`);
if (results.skipped.length > 0) {
results.skipped.slice(0, 5).forEach((item, index) => {
console.log(` ${index + 1}. ${item.teacher_code} - ${item.reason}`);
});
if (results.skipped.length > 5) {
console.log(` ... and ${results.skipped.length - 5} more`);
}
}
console.log(`\n❌ Failed: ${results.failed.length}`);
if (results.failed.length > 0) {
results.failed.forEach((item, index) => {
console.log(` ${index + 1}. ${item.teacher_code} - ${item.reason || item.error}`);
});
}
console.log('\n' + '='.repeat(60));
console.log('✅ Sync completed successfully!');
console.log('='.repeat(60) + '\n');
process.exit(0);
} catch (error) {
console.error('\n❌ Sync failed:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// Run
main();

View File

@@ -0,0 +1,58 @@
const { sequelize } = require('../config/database');
const UsersAuth = require('../models/UsersAuth');
const UserProfile = require('../models/UserProfile');
const TeacherDetail = require('../models/TeacherDetail');
/**
* Script tạo các bảng user cần thiết trong database
* Chạy: node src/scripts/sync-user-tables.js
*/
async function syncUserTables() {
try {
console.log('🚀 Đang tạo các bảng user trong database...\n');
// Kết nối database
await sequelize.authenticate();
console.log('✅ Kết nối database thành công\n');
// Sync các bảng theo thứ tự phụ thuộc
console.log('📋 Đang tạo bảng users_auth...');
await UsersAuth.sync({ alter: true });
console.log('✅ Đã tạo bảng users_auth\n');
console.log('📋 Đang tạo bảng user_profiles...');
await UserProfile.sync({ alter: true });
console.log('✅ Đã tạo bảng user_profiles\n');
console.log('📋 Đang tạo bảng teacher_details...');
await TeacherDetail.sync({ alter: true });
console.log('✅ Đã tạo bảng teacher_details\n');
console.log('='.repeat(60));
console.log('✅ Đã tạo tất cả các bảng thành công!');
console.log('='.repeat(60));
} catch (error) {
console.error('❌ Lỗi:', error);
throw error;
} finally {
await sequelize.close();
console.log('\n🔌 Đã đóng kết nối database');
}
}
// Chạy script
if (require.main === module) {
syncUserTables()
.then(() => {
console.log('\n🎉 Script hoàn thành thành công!');
process.exit(0);
})
.catch((error) => {
console.error('\n💥 Script thất bại:', error);
process.exit(1);
});
}
module.exports = { syncUserTables };

View File

@@ -0,0 +1,76 @@
/**
* Test API endpoint for teachers
*/
const API_BASE = 'http://localhost:4000/api';
async function testTeachersAPI() {
console.log('🧪 Testing Teachers API\n');
console.log('='.repeat(60));
try {
// Test 1: Get all teachers with school filter
console.log('\n📝 Test 1: GET /api/teachers with school_id filter');
console.log('URL: ' + API_BASE + '/teachers?school_id=95ec32e9-cdcb-4f3a-a8ee-8af20bdfe2cc&page=1&limit=5');
const response1 = await fetch(`${API_BASE}/teachers?school_id=95ec32e9-cdcb-4f3a-a8ee-8af20bdfe2cc&page=1&limit=5`);
const data1 = await response1.json();
if (data1.success) {
console.log('✅ Success!');
console.log(` Total: ${data1.data.pagination.total}`);
console.log(` Returned: ${data1.data.teachers.length}`);
if (data1.data.teachers.length > 0) {
const teacher = data1.data.teachers[0];
console.log('\n📋 Sample Teacher:');
console.log(` ID: ${teacher.id}`);
console.log(` Code: ${teacher.teacher_code}`);
console.log(` Type: ${teacher.teacher_type}`);
console.log(` Status: ${teacher.status}`);
console.log(` Profile: ${teacher.profile ? '✅ EXISTS' : '❌ NULL'}`);
if (teacher.profile) {
console.log(` - Full Name: ${teacher.profile.full_name}`);
console.log(` - Phone: ${teacher.profile.phone || 'N/A'}`);
console.log(` - School ID: ${teacher.profile.school_id}`);
}
}
} else {
console.log('❌ Failed:', data1.message);
}
// Test 2: Get all teachers without filter
console.log('\n\n📝 Test 2: GET /api/teachers (no filter)');
console.log('URL: ' + API_BASE + '/teachers?page=1&limit=3');
const response2 = await fetch(`${API_BASE}/teachers?page=1&limit=3`);
const data2 = await response2.json();
if (data2.success) {
console.log('✅ Success!');
console.log(` Total: ${data2.data.pagination.total}`);
console.log(` Returned: ${data2.data.teachers.length}`);
console.log('\n📋 Teachers:');
data2.data.teachers.forEach((t, i) => {
console.log(` ${i + 1}. ${t.teacher_code} - Profile: ${t.profile ? '✅' : '❌'}`);
if (t.profile) {
console.log(` Name: ${t.profile.full_name}`);
}
});
} else {
console.log('❌ Failed:', data2.message);
}
console.log('\n' + '='.repeat(60));
console.log('✅ Tests completed!');
console.log('='.repeat(60) + '\n');
} catch (error) {
console.error('\n❌ Test failed:', error.message);
}
}
// Run test
testTeachersAPI();

View File

@@ -0,0 +1,85 @@
/**
* Test: Verify profile fetching logic
*/
require('dotenv').config();
const { sequelize, TeacherDetail, UserProfile } = require('../models');
async function testProfileFetch() {
console.log('🧪 Testing Profile Fetch Logic\n');
try {
await sequelize.authenticate();
console.log('✅ Database connected\n');
// Get first teacher
const teacher = await TeacherDetail.findOne({
order: [['created_at', 'ASC']],
});
if (!teacher) {
console.log('❌ No teachers found');
process.exit(1);
}
console.log('📋 Teacher Found:');
console.log(` ID: ${teacher.id}`);
console.log(` Code: ${teacher.teacher_code}`);
console.log(` User ID: ${teacher.user_id}`);
console.log('');
// Fetch profile manually
console.log('🔍 Fetching profile manually...');
const profile = await UserProfile.findOne({
where: { user_id: teacher.user_id },
});
if (profile) {
console.log('✅ Profile found!');
console.log(` Profile ID: ${profile.id}`);
console.log(` Full Name: ${profile.full_name}`);
console.log(` Phone: ${profile.phone || 'N/A'}`);
console.log(` School ID: ${profile.school_id}`);
console.log('');
// Create combined object like in controller
const teacherData = {
...teacher.toJSON(),
profile: profile.toJSON(),
};
console.log('✅ Combined data structure works!');
console.log('');
console.log('Sample response:');
console.log(JSON.stringify({
id: teacherData.id,
teacher_code: teacherData.teacher_code,
teacher_type: teacherData.teacher_type,
status: teacherData.status,
profile: {
id: teacherData.profile.id,
full_name: teacherData.profile.full_name,
phone: teacherData.profile.phone,
school_id: teacherData.profile.school_id,
}
}, null, 2));
} else {
console.log('❌ Profile NOT found!');
console.log(' This is the issue - profile should exist');
}
console.log('\n' + '='.repeat(60));
console.log('✅ Test completed!');
console.log('='.repeat(60));
process.exit(0);
} catch (error) {
console.error('❌ Error:', error.message);
console.error(error.stack);
process.exit(1);
}
}
testProfileFetch();

View File

@@ -0,0 +1,105 @@
const { sequelize } = require('../config/database');
const { StudentDetail, UserProfile, UsersAuth, setupRelationships } = require('../models');
async function testJoin() {
try {
await sequelize.authenticate();
console.log('✅ Connected to database');
// Setup relationships BEFORE querying
setupRelationships();
console.log('✅ Relationships initialized\n');
// Test 1: Raw SQL query
console.log('📊 Test 1: Raw SQL Join');
const [rawResults] = await sequelize.query(`
SELECT
sd.id,
sd.user_id,
sd.student_code,
up.id as profile_id,
up.user_id as profile_user_id,
up.full_name
FROM student_details sd
LEFT JOIN user_profiles up ON sd.user_id = up.user_id
WHERE sd.student_code LIKE 'pri_apdinh%'
LIMIT 3
`);
console.log('Raw SQL Results:');
rawResults.forEach(row => {
console.log({
student_id: row.id,
student_user_id: row.user_id,
student_code: row.student_code,
profile_id: row.profile_id,
profile_user_id: row.profile_user_id,
full_name: row.full_name,
join_works: row.profile_id ? '✅ YES' : '❌ NO'
});
});
// Test 2: Sequelize query
console.log('\n📊 Test 2: Sequelize Join');
const students = await StudentDetail.findAll({
where: sequelize.where(
sequelize.fn('LOWER', sequelize.col('student_code')),
'LIKE',
'pri_apdinh%'
),
include: [{
model: UserProfile,
as: 'profile',
required: false,
}],
limit: 3
});
console.log('Sequelize Results:');
students.forEach(student => {
console.log({
student_id: student.id,
student_user_id: student.user_id,
student_code: student.student_code,
profile: student.profile ? {
id: student.profile.id,
user_id: student.profile.user_id,
full_name: student.profile.full_name,
join_works: '✅ YES'
} : '❌ NO PROFILE'
});
});
// Test 3: Check data integrity
console.log('\n📊 Test 3: Data Integrity Check');
const sampleStudent = await StudentDetail.findOne({
where: sequelize.where(
sequelize.fn('LOWER', sequelize.col('student_code')),
'LIKE',
'pri_apdinh%'
)
});
if (sampleStudent) {
console.log(`Student user_id: ${sampleStudent.user_id}`);
const matchingProfile = await UserProfile.findOne({
where: { user_id: sampleStudent.user_id }
});
if (matchingProfile) {
console.log(`✅ Found matching profile: ${matchingProfile.full_name}`);
console.log(`Profile user_id: ${matchingProfile.user_id}`);
} else {
console.log(`❌ No matching profile found for user_id: ${sampleStudent.user_id}`);
}
}
} catch (error) {
console.error('❌ Error:', error);
} finally {
await sequelize.close();
}
}
testJoin();

View File

@@ -0,0 +1,267 @@
/**
* Test Teacher Profile Service
*
* Usage:
* node src/scripts/test-teacher-profile.js
*/
require('dotenv').config();
const teacherProfileService = require('../services/teacherProfileService');
const { sequelize, TeacherDetail, UserProfile, UsersAuth } = require('../models');
async function testCreateTeacher() {
console.log('\n📝 Test 1: Create teacher với minimal data');
console.log('='.repeat(60));
try {
const minimalTeacher = {
teacher_code: `TEST_GV_${Date.now()}`,
teacher_type: 'homeroom',
};
console.log('Input:', JSON.stringify(minimalTeacher, null, 2));
const result = await teacherProfileService.createTeacherWithProfile(minimalTeacher);
console.log('\n✅ Success!');
console.log('Output:', JSON.stringify({
user_id: result.user_id,
username: result.username,
email: result.email,
temporary_password: result.temporary_password,
teacher_code: result.teacher_detail.teacher_code,
full_name: result.profile.full_name,
}, null, 2));
return result;
} catch (error) {
console.error('❌ Failed:', error.message);
throw error;
}
}
async function testCreateForeignTeacher() {
console.log('\n📝 Test 2: Create foreign teacher với full data');
console.log('='.repeat(60));
try {
const fullTeacher = {
// Required
teacher_code: `TEST_FT_${Date.now()}`,
teacher_type: 'foreign',
// Optional auth
username: 'test_john_smith',
email: `test_john_${Date.now()}@sena.edu.vn`,
password: 'TestPassword123!',
// Optional profile
full_name: 'John Smith',
first_name: 'John',
last_name: 'Smith',
phone: '+84-123-456-789',
date_of_birth: '1985-03-15',
gender: 'male',
address: '123 Test Street',
city: 'Ho Chi Minh City',
district: 'District 1',
// Optional teacher details
qualification: 'Master of Education',
specialization: 'English Literature',
hire_date: '2026-01-01',
skill_tags: [
{ name: 'IELTS', level: 'expert' },
{ name: 'TOEFL', level: 'advanced' }
],
certifications: [
{ name: 'TESOL', issued_by: 'Cambridge', issued_date: '2020-05-15' }
]
};
console.log('Input:', JSON.stringify(fullTeacher, null, 2));
const result = await teacherProfileService.createTeacherWithProfile(fullTeacher);
console.log('\n✅ Success!');
console.log('Output:', JSON.stringify({
user_id: result.user_id,
username: result.username,
email: result.email,
temporary_password: result.temporary_password, // Should be null
teacher_code: result.teacher_detail.teacher_code,
full_name: result.profile.full_name,
phone: result.profile.phone,
needs_update: result.profile.etc?.needs_profile_update,
}, null, 2));
return result;
} catch (error) {
console.error('❌ Failed:', error.message);
throw error;
}
}
async function testUpdateProfile(userId) {
console.log('\n📝 Test 3: Update teacher profile');
console.log('='.repeat(60));
try {
const updateData = {
full_name: 'John Smith Updated',
phone: '+84-987-654-321',
address: '456 New Address',
};
console.log('User ID:', userId);
console.log('Update data:', JSON.stringify(updateData, null, 2));
const result = await teacherProfileService.updateTeacherProfile(userId, updateData);
console.log('\n✅ Success!');
console.log('Updated profile:', JSON.stringify({
full_name: result.full_name,
phone: result.phone,
address: result.address,
needs_update: result.etc?.needs_profile_update,
last_updated: result.etc?.last_updated,
}, null, 2));
return result;
} catch (error) {
console.error('❌ Failed:', error.message);
throw error;
}
}
async function testSync() {
console.log('\n📝 Test 4: Sync existing teachers');
console.log('='.repeat(60));
try {
// First, create a teacher_detail without profile (simulate old data)
const testUserAuth = await UsersAuth.create({
username: `test_orphan_${Date.now()}`,
email: `test_orphan_${Date.now()}@sena.edu.vn`,
password_hash: 'dummy_hash',
salt: 'dummy_salt',
});
const orphanTeacher = await TeacherDetail.create({
user_id: testUserAuth.id,
teacher_code: `TEST_ORPHAN_${Date.now()}`,
teacher_type: 'subject',
});
console.log(`Created orphan teacher: ${orphanTeacher.teacher_code}`);
// Now sync
const results = await teacherProfileService.syncExistingTeachers(orphanTeacher.id);
console.log('\n✅ Sync completed!');
console.log('Results:', JSON.stringify({
success_count: results.success.length,
skipped_count: results.skipped.length,
failed_count: results.failed.length,
success: results.success,
skipped: results.skipped,
failed: results.failed,
}, null, 2));
return results;
} catch (error) {
console.error('❌ Failed:', error.message);
throw error;
}
}
async function cleanup(userIds) {
console.log('\n🧹 Cleaning up test data...');
try {
// Delete in correct order due to foreign keys
for (const userId of userIds) {
await TeacherDetail.destroy({ where: { user_id: userId }, force: true });
await UserProfile.destroy({ where: { user_id: userId }, force: true });
await UsersAuth.destroy({ where: { id: userId }, force: true });
}
console.log('✅ Cleanup completed');
} catch (error) {
console.error('❌ Cleanup failed:', error.message);
}
}
async function main() {
console.log('\n🚀 Starting Teacher Profile Service Tests\n');
const createdUserIds = [];
try {
// Test connection
await sequelize.authenticate();
console.log('✅ Database connected\n');
// Run tests
const result1 = await testCreateTeacher();
createdUserIds.push(result1.user_id);
const result2 = await testCreateForeignTeacher();
createdUserIds.push(result2.user_id);
await testUpdateProfile(result2.user_id);
const syncResult = await testSync();
if (syncResult.success.length > 0) {
createdUserIds.push(syncResult.success[0].user_id);
}
// Summary
console.log('\n' + '='.repeat(60));
console.log('📊 TEST SUMMARY');
console.log('='.repeat(60));
console.log('✅ All tests passed!');
console.log(`Created ${createdUserIds.length} test users`);
console.log('='.repeat(60));
// Cleanup
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
readline.question('\nDelete test data? (y/n): ', async (answer) => {
if (answer.toLowerCase() === 'y') {
await cleanup(createdUserIds);
} else {
console.log('\n⚠ Test data NOT deleted. User IDs:');
createdUserIds.forEach((id, index) => {
console.log(` ${index + 1}. ${id}`);
});
}
readline.close();
process.exit(0);
});
} catch (error) {
console.error('\n❌ Test failed:', error.message);
console.error(error.stack);
// Cleanup on error
if (createdUserIds.length > 0) {
console.log('\nCleaning up partial test data...');
await cleanup(createdUserIds);
}
process.exit(1);
}
}
// Run tests
main();

175
scripts/verify-schools.js Normal file
View File

@@ -0,0 +1,175 @@
const { sequelize } = require('../config/database');
const School = require('../models/School');
const Class = require('../models/Class');
/**
* Script kiểm tra dữ liệu đã import
* Chạy: node src/scripts/verify-schools.js
*/
async function verifyImportedData() {
try {
console.log('🔍 Kiểm tra dữ liệu đã import...\n');
// Kết nối database
await sequelize.authenticate();
console.log('✅ Kết nối database thành công\n');
// Đếm số lượng trường
const schoolCount = await School.count();
const activeSchools = await School.count({ where: { is_active: true } });
console.log('📊 THỐNG KÊ TRƯỜNG HỌC');
console.log('='.repeat(60));
console.log(`Tổng số trường: ${schoolCount}`);
console.log(`Trường đang hoạt động: ${activeSchools}`);
console.log('');
// Thống kê theo loại trường
const schoolTypes = await School.findAll({
attributes: [
'school_type',
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
],
group: ['school_type']
});
console.log('📚 Phân loại trường:');
schoolTypes.forEach(type => {
const typeName = {
'preschool': 'Mầm non',
'primary': 'Tiểu học',
'secondary': 'Trung học cơ sở',
'high_school': 'Trung học phổ thông'
}[type.school_type] || type.school_type;
console.log(` ${typeName.padEnd(25)} ${type.get('count')}`);
});
console.log('');
// Thống kê theo quận/huyện
const districts = await School.findAll({
attributes: [
'district',
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
],
group: ['district'],
order: [[sequelize.fn('COUNT', sequelize.col('id')), 'DESC']],
limit: 10
});
console.log('🏙️ Top 10 Quận/Huyện có nhiều trường nhất:');
districts.forEach((district, index) => {
console.log(` ${(index + 1 + '.').padEnd(4)} ${(district.district || 'N/A').padEnd(30)} ${district.get('count')}`);
});
console.log('');
// Đếm số lượng lớp
const classCount = await Class.count();
console.log('📊 THỐNG KÊ LỚP HỌC');
console.log('='.repeat(60));
console.log(`Tổng số lớp: ${classCount}`);
console.log('');
// Thống kê lớp theo khối
const gradeStats = await Class.findAll({
attributes: [
'grade_level',
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
],
group: ['grade_level'],
order: ['grade_level']
});
console.log('📝 Phân bổ lớp theo khối:');
gradeStats.forEach(stat => {
const grade = stat.grade_level || 'N/A';
console.log(` Khối ${grade}:`.padEnd(15) + `${stat.get('count')} lớp`);
});
console.log('');
// Lấy 5 trường có nhiều lớp nhất
const topSchools = await sequelize.query(`
SELECT
s.school_name,
s.school_code,
COUNT(c.id) as class_count
FROM schools s
LEFT JOIN classes c ON s.id = c.school_id
GROUP BY s.id, s.school_name, s.school_code
ORDER BY class_count DESC
LIMIT 5
`, {
type: sequelize.QueryTypes.SELECT
});
console.log('🏆 Top 5 trường có nhiều lớp nhất:');
topSchools.forEach((school, index) => {
console.log(` ${index + 1}. ${school.school_name.padEnd(45)} ${school.class_count} lớp`);
});
console.log('');
// Chi tiết một trường mẫu
const sampleSchool = await School.findOne({
include: [{
model: Class,
as: 'classes',
required: true
}],
order: [[sequelize.literal('(SELECT COUNT(*) FROM classes WHERE school_id = schools.id)'), 'DESC']]
});
if (sampleSchool) {
console.log('📖 CHI TIẾT MỘT TRƯỜNG MẪU');
console.log('='.repeat(60));
console.log(`Tên trường: ${sampleSchool.school_name}`);
console.log(`Mã trường: ${sampleSchool.school_code}`);
console.log(`Loại: ${sampleSchool.school_type}`);
console.log(`Địa chỉ: ${sampleSchool.address}`);
console.log(`Quận/Huyện: ${sampleSchool.district}`);
console.log(`Số điện thoại: ${sampleSchool.phone || 'N/A'}`);
if (sampleSchool.classes && sampleSchool.classes.length > 0) {
console.log(`\nSố lớp: ${sampleSchool.classes.length}`);
console.log('\nDanh sách một số lớp:');
sampleSchool.classes.slice(0, 10).forEach(cls => {
console.log(` - ${cls.class_name} (Khối ${cls.grade_level})`);
});
if (sampleSchool.classes.length > 10) {
console.log(` ... và ${sampleSchool.classes.length - 10} lớp khác`);
}
}
}
console.log('\n' + '='.repeat(60));
console.log('✅ Kiểm tra hoàn tất!');
console.log('='.repeat(60) + '\n');
} catch (error) {
console.error('❌ Lỗi khi kiểm tra dữ liệu:', error);
throw error;
} finally {
await sequelize.close();
console.log('🔌 Đã đóng kết nối database');
}
}
// Setup associations
School.hasMany(Class, { foreignKey: 'school_id', as: 'classes' });
Class.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
// Chạy script
if (require.main === module) {
verifyImportedData()
.then(() => {
console.log('\n🎉 Script hoàn thành!');
process.exit(0);
})
.catch((error) => {
console.error('\n💥 Script thất bại:', error);
process.exit(1);
});
}
module.exports = { verifyImportedData };