update
This commit is contained in:
19
models/AcademicYear.js
Normal file
19
models/AcademicYear.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const AcademicYear = sequelize.define('academic_years', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: 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 },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'academic_years',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = AcademicYear;
|
||||
22
models/AttendanceDaily.js
Normal file
22
models/AttendanceDaily.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const AttendanceDaily = sequelize.define('attendance_daily', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
user_id: { type: DataTypes.UUID, allowNull: false },
|
||||
school_id: { type: DataTypes.UUID, allowNull: false },
|
||||
class_id: { type: DataTypes.UUID },
|
||||
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 },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'attendance_daily',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = AttendanceDaily;
|
||||
90
models/AttendanceLog.js
Normal file
90
models/AttendanceLog.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* AttendanceLogs Model - Dữ liệu thô điểm danh từ QR
|
||||
* Bảng này cần partition theo thời gian do data lớn
|
||||
*/
|
||||
const AttendanceLog = sequelize.define('attendance_logs', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users_auth',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'ID người dùng (học sinh/giáo viên)',
|
||||
},
|
||||
school_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'schools',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'ID trường',
|
||||
},
|
||||
check_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: 'Thời gian quét QR',
|
||||
},
|
||||
check_type: {
|
||||
type: DataTypes.ENUM('IN', 'OUT'),
|
||||
allowNull: false,
|
||||
comment: 'Loại: Vào hoặc Ra',
|
||||
},
|
||||
device_id: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'ID thiết bị quét',
|
||||
},
|
||||
location: {
|
||||
type: DataTypes.STRING(200),
|
||||
comment: 'Vị trí quét (cổng, lớp học)',
|
||||
},
|
||||
qr_version: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: 'Version QR code',
|
||||
},
|
||||
ip_address: {
|
||||
type: DataTypes.STRING(45),
|
||||
comment: 'IP address thiết bị (IPv4/IPv6)',
|
||||
},
|
||||
user_agent: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'User agent',
|
||||
},
|
||||
is_valid: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'QR code hợp lệ',
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Ghi chú',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'attendance_logs',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['user_id', 'check_time'] },
|
||||
{ fields: ['school_id', 'check_time'] },
|
||||
{ fields: ['check_time'] },
|
||||
{ fields: ['device_id'] },
|
||||
{ fields: ['is_valid'] },
|
||||
],
|
||||
comment: 'Bảng log điểm danh thô - Cần partition theo tháng',
|
||||
});
|
||||
|
||||
module.exports = AttendanceLog;
|
||||
35
models/AuditLog.js
Normal file
35
models/AuditLog.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const AuditLog = sequelize.define('audit_logs', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
school_id: { type: DataTypes.UUID },
|
||||
user_id: { type: DataTypes.UUID },
|
||||
action: { type: DataTypes.STRING(100), allowNull: false },
|
||||
entity_type: { type: DataTypes.STRING(100) },
|
||||
entity_id: { type: DataTypes.UUID },
|
||||
old_values: { type: DataTypes.JSON },
|
||||
new_values: { type: DataTypes.JSON },
|
||||
ip_address: { type: DataTypes.STRING(45) },
|
||||
user_agent: { type: DataTypes.TEXT },
|
||||
status: {
|
||||
type: DataTypes.ENUM('success', 'failed'),
|
||||
defaultValue: 'success'
|
||||
},
|
||||
error_message: { type: DataTypes.TEXT },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'audit_logs',
|
||||
timestamps: false,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ fields: ['school_id'] },
|
||||
{ fields: ['user_id'] },
|
||||
{ fields: ['action'] },
|
||||
{ fields: ['entity_type', 'entity_id'] },
|
||||
{ fields: ['created_at'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = AuditLog;
|
||||
69
models/Chapter.js
Normal file
69
models/Chapter.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* Chapter Model - Chương trong giáo trình
|
||||
* Một Subject có nhiều Chapter
|
||||
*/
|
||||
const Chapter = sequelize.define('chapters', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
subject_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
comment: 'ID của giáo trình'
|
||||
},
|
||||
chapter_number: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: 'Số thứ tự chương (1, 2, 3...)'
|
||||
},
|
||||
chapter_title: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: 'Tiêu đề chương'
|
||||
},
|
||||
chapter_description: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Mô tả về chương học'
|
||||
},
|
||||
duration_minutes: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: 'Thời lượng học (phút)'
|
||||
},
|
||||
is_published: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Đã xuất bản chưa'
|
||||
},
|
||||
display_order: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Thứ tự hiển thị'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
}, {
|
||||
tableName: 'chapters',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['subject_id'] },
|
||||
{ fields: ['subject_id', 'chapter_number'], unique: true },
|
||||
{ fields: ['display_order'] },
|
||||
{ fields: ['is_published'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = Chapter;
|
||||
22
models/Class.js
Normal file
22
models/Class.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Class = sequelize.define('classes', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
class_code: { type: DataTypes.STRING(20), unique: true, allowNull: false },
|
||||
class_name: { type: DataTypes.STRING(100), allowNull: false },
|
||||
school_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'schools', key: 'id' } },
|
||||
academic_year_id: { type: DataTypes.UUID, allowNull: false },
|
||||
grade_level: { type: DataTypes.INTEGER },
|
||||
homeroom_teacher_id: { type: DataTypes.UUID },
|
||||
max_students: { type: DataTypes.INTEGER, defaultValue: 30 },
|
||||
current_students: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'classes',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = Class;
|
||||
22
models/ClassSchedule.js
Normal file
22
models/ClassSchedule.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const ClassSchedule = sequelize.define('class_schedules', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
class_id: { type: DataTypes.UUID, allowNull: false },
|
||||
subject_id: { type: DataTypes.UUID, allowNull: false },
|
||||
teacher_id: { type: DataTypes.UUID },
|
||||
room_id: { type: DataTypes.UUID },
|
||||
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 },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'class_schedules',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = ClassSchedule;
|
||||
101
models/Game.js
Normal file
101
models/Game.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* Game Model - Trò chơi giáo dục
|
||||
* Các game engine/template để render nội dung học tập
|
||||
* Một game có thể render nhiều lesson khác nhau có cùng type
|
||||
*/
|
||||
const Game = sequelize.define('games', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: 'Tên trò chơi'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Mô tả về trò chơi'
|
||||
},
|
||||
url: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
comment: 'URL của game (HTML5 game, Unity WebGL, etc.)'
|
||||
},
|
||||
thumbnail: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'URL ảnh thumbnail'
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: 'Loại game: counting_quiz, math_game, word_puzzle, etc. - Must match lesson content_json.type'
|
||||
},
|
||||
config: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Cấu hình game: controls, settings, features, etc.'
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Game có sẵn sàng để sử dụng không'
|
||||
},
|
||||
is_premium: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Yêu cầu premium để chơi'
|
||||
},
|
||||
min_grade: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: 'Cấp lớp tối thiểu (1-12)'
|
||||
},
|
||||
max_grade: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: 'Cấp lớp tối đa (1-12)'
|
||||
},
|
||||
difficulty_level: {
|
||||
type: DataTypes.ENUM('easy', 'medium', 'hard'),
|
||||
comment: 'Độ khó'
|
||||
},
|
||||
play_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Số lần đã chơi'
|
||||
},
|
||||
rating: {
|
||||
type: DataTypes.DECIMAL(3, 2),
|
||||
comment: 'Đánh giá (0.00 - 5.00)'
|
||||
},
|
||||
display_order: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Thứ tự hiển thị'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
}, {
|
||||
tableName: 'games',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['type'] },
|
||||
{ fields: ['is_active'] },
|
||||
{ fields: ['is_premium'] },
|
||||
{ fields: ['display_order'] },
|
||||
{ fields: ['difficulty_level'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = Game;
|
||||
100
models/Grade.js
Normal file
100
models/Grade.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* Grades Model - Điểm số chi tiết của học sinh
|
||||
* Bảng lớn nhất trong hệ thống
|
||||
*/
|
||||
const Grade = sequelize.define('grades', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
grade_item_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'grade_items',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'ID cột điểm',
|
||||
},
|
||||
student_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
comment: 'ID học sinh',
|
||||
},
|
||||
class_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'classes',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'ID lớp học',
|
||||
},
|
||||
score: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
comment: 'Điểm số',
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
grade_level: {
|
||||
type: DataTypes.STRING(5),
|
||||
comment: 'Xếp loại (A, B, C, D, F)',
|
||||
},
|
||||
comment: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Nhận xét của giáo viên',
|
||||
},
|
||||
graded_by: {
|
||||
type: DataTypes.UUID,
|
||||
comment: 'ID giáo viên chấm điểm',
|
||||
},
|
||||
graded_at: {
|
||||
type: DataTypes.DATE,
|
||||
comment: 'Thời gian chấm điểm',
|
||||
},
|
||||
is_published: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Đã công bố cho học sinh/phụ huynh',
|
||||
},
|
||||
published_at: {
|
||||
type: DataTypes.DATE,
|
||||
comment: 'Thời gian công bố',
|
||||
},
|
||||
is_locked: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Khóa điểm, không cho sửa',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'grades',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['grade_item_id', 'student_id'], unique: true },
|
||||
{ fields: ['student_id', 'class_id'] },
|
||||
{ fields: ['class_id'] },
|
||||
{ fields: ['graded_by'] },
|
||||
{ fields: ['is_published'] },
|
||||
{ fields: ['created_at'] },
|
||||
],
|
||||
comment: 'Bảng điểm chi tiết - Dữ liệu lớn nhất hệ thống',
|
||||
});
|
||||
|
||||
module.exports = Grade;
|
||||
19
models/GradeCategory.js
Normal file
19
models/GradeCategory.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const GradeCategory = sequelize.define('grade_categories', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
category_code: { type: DataTypes.STRING(20), allowNull: false },
|
||||
category_name: { type: DataTypes.STRING(100), allowNull: false },
|
||||
subject_id: { type: DataTypes.UUID },
|
||||
weight: { type: DataTypes.DECIMAL(5, 2), comment: 'Trọng số %' },
|
||||
description: { type: DataTypes.TEXT },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'grade_categories',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = GradeCategory;
|
||||
21
models/GradeHistory.js
Normal file
21
models/GradeHistory.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const GradeHistory = sequelize.define('grade_histories', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
grade_id: { type: DataTypes.UUID, allowNull: false },
|
||||
student_id: { type: DataTypes.UUID, allowNull: false },
|
||||
old_score: { type: DataTypes.DECIMAL(5, 2) },
|
||||
new_score: { type: DataTypes.DECIMAL(5, 2) },
|
||||
changed_by: { type: DataTypes.UUID },
|
||||
change_reason: { type: DataTypes.TEXT },
|
||||
changed_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'grade_histories',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = GradeHistory;
|
||||
29
models/GradeItem.js
Normal file
29
models/GradeItem.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const GradeItem = sequelize.define('grade_items', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
item_name: { type: DataTypes.STRING(200), allowNull: false },
|
||||
category_id: { type: DataTypes.UUID, allowNull: false },
|
||||
class_id: { type: DataTypes.UUID, allowNull: false },
|
||||
max_score: { type: DataTypes.DECIMAL(5, 2), defaultValue: 100 },
|
||||
exam_date: { type: DataTypes.DATE },
|
||||
description: { type: DataTypes.TEXT },
|
||||
is_premium: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung premium' },
|
||||
is_training: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung đào tạo nhân sự' },
|
||||
is_public: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung công khai' },
|
||||
min_subscription_tier: { type: DataTypes.STRING(50), comment: 'Gói tối thiểu' },
|
||||
min_interaction_time: { type: DataTypes.INTEGER, defaultValue: 0, comment: 'Thời gian tương tác tối thiểu (giây)' },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'grade_items',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['is_premium'] },
|
||||
{ fields: ['is_training'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = GradeItem;
|
||||
20
models/GradeSummary.js
Normal file
20
models/GradeSummary.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const GradeSummary = sequelize.define('grade_summaries', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
student_id: { type: DataTypes.UUID, allowNull: false },
|
||||
subject_id: { type: DataTypes.UUID, allowNull: false },
|
||||
academic_year_id: { type: DataTypes.UUID, allowNull: false },
|
||||
semester: { type: DataTypes.INTEGER },
|
||||
average_score: { type: DataTypes.DECIMAL(5, 2) },
|
||||
final_grade: { type: DataTypes.STRING(5) },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'grade_summaries',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = GradeSummary;
|
||||
22
models/LeaveRequest.js
Normal file
22
models/LeaveRequest.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const LeaveRequest = sequelize.define('leave_requests', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
student_id: { type: DataTypes.UUID, allowNull: false },
|
||||
requested_by: { type: DataTypes.UUID, 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.UUID },
|
||||
approved_at: { type: DataTypes.DATE },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'leave_requests',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = LeaveRequest;
|
||||
101
models/Lesson.js
Normal file
101
models/Lesson.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* Lesson Model - Bài học trong chương
|
||||
* Một Chapter có nhiều Lesson
|
||||
*/
|
||||
const Lesson = sequelize.define('lessons', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
chapter_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
comment: 'ID của chương'
|
||||
},
|
||||
lesson_number: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: 'Số thứ tự bài học (1, 2, 3...)'
|
||||
},
|
||||
lesson_title: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: 'Tiêu đề bài học'
|
||||
},
|
||||
lesson_type: {
|
||||
type: DataTypes.ENUM('json_content', 'url_content'),
|
||||
defaultValue: 'json_content',
|
||||
comment: 'Loại bài học: json_content (nội dung JSON) hoặc url_content (URL)'
|
||||
},
|
||||
lesson_description: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Mô tả bài học'
|
||||
},
|
||||
|
||||
// Dạng 1: JSON Content - Nội dung học tập dạng JSON
|
||||
content_json: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Nội dung học tập dạng JSON: text, quiz, interactive, assignment, etc.'
|
||||
},
|
||||
|
||||
// Dạng 2: URL Content - Chứa link external
|
||||
content_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
comment: 'URL nội dung: video, audio, document, external link'
|
||||
},
|
||||
content_type: {
|
||||
type: DataTypes.STRING(50),
|
||||
comment: 'Loại content cho URL: video, audio, pdf, external_link, youtube, etc.'
|
||||
},
|
||||
|
||||
duration_minutes: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: 'Thời lượng (phút)'
|
||||
},
|
||||
is_published: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Đã xuất bản'
|
||||
},
|
||||
is_free: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Cho phép học thử miễn phí'
|
||||
},
|
||||
display_order: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Thứ tự hiển thị'
|
||||
},
|
||||
thumbnail_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
comment: 'URL ảnh thumbnail'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
}, {
|
||||
tableName: 'lessons',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['chapter_id'] },
|
||||
{ fields: ['chapter_id', 'lesson_number'], unique: true },
|
||||
{ fields: ['display_order'] },
|
||||
{ fields: ['is_published'] },
|
||||
{ fields: ['lesson_type'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = Lesson;
|
||||
36
models/Message.js
Normal file
36
models/Message.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Message = sequelize.define('messages', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
school_id: { type: DataTypes.UUID },
|
||||
sender_id: { type: DataTypes.UUID, allowNull: false },
|
||||
receiver_id: { type: DataTypes.UUID },
|
||||
receiver_role: { type: DataTypes.STRING(50) },
|
||||
subject: { type: DataTypes.STRING(255) },
|
||||
content: { type: DataTypes.TEXT, allowNull: false },
|
||||
message_type: {
|
||||
type: DataTypes.ENUM('direct', 'broadcast', 'class_message', 'parent_teacher'),
|
||||
defaultValue: 'direct'
|
||||
},
|
||||
parent_message_id: { type: DataTypes.UUID },
|
||||
attachments: { type: DataTypes.JSON },
|
||||
is_read: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||
read_at: { type: DataTypes.DATE },
|
||||
is_archived: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'messages',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['school_id'] },
|
||||
{ fields: ['sender_id'] },
|
||||
{ fields: ['receiver_id'] },
|
||||
{ fields: ['is_read'] },
|
||||
{ fields: ['parent_message_id'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = Message;
|
||||
40
models/Notification.js
Normal file
40
models/Notification.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Notification = sequelize.define('notifications', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
school_id: { type: DataTypes.UUID },
|
||||
title: { type: DataTypes.STRING(255), allowNull: false },
|
||||
content: { type: DataTypes.TEXT },
|
||||
notification_type: {
|
||||
type: DataTypes.ENUM('announcement', 'event', 'emergency', 'reminder', 'grade_update', 'attendance_alert'),
|
||||
defaultValue: 'announcement'
|
||||
},
|
||||
priority: {
|
||||
type: DataTypes.ENUM('low', 'normal', 'high', 'urgent'),
|
||||
defaultValue: 'normal'
|
||||
},
|
||||
target_role: { type: DataTypes.STRING(50) },
|
||||
target_users: { type: DataTypes.JSON },
|
||||
scheduled_at: { type: DataTypes.DATE },
|
||||
sent_at: { type: DataTypes.DATE },
|
||||
created_by: { type: DataTypes.UUID },
|
||||
status: {
|
||||
type: DataTypes.ENUM('draft', 'scheduled', 'sent', 'cancelled'),
|
||||
defaultValue: 'draft'
|
||||
},
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'notifications',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['school_id'] },
|
||||
{ fields: ['notification_type'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['scheduled_at'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = Notification;
|
||||
33
models/NotificationLog.js
Normal file
33
models/NotificationLog.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const NotificationLog = sequelize.define('notification_logs', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
notification_id: { type: DataTypes.UUID, allowNull: false },
|
||||
user_id: { type: DataTypes.UUID, allowNull: false },
|
||||
sent_at: { type: DataTypes.DATE },
|
||||
read_at: { type: DataTypes.DATE },
|
||||
delivery_status: {
|
||||
type: DataTypes.ENUM('pending', 'sent', 'delivered', 'failed', 'read'),
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
delivery_channel: {
|
||||
type: DataTypes.ENUM('in_app', 'email', 'sms', 'push'),
|
||||
defaultValue: 'in_app'
|
||||
},
|
||||
error_message: { type: DataTypes.TEXT },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'notification_logs',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['notification_id'] },
|
||||
{ fields: ['user_id'] },
|
||||
{ fields: ['delivery_status'] },
|
||||
{ fields: ['read_at'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = NotificationLog;
|
||||
95
models/ParentAssignedTask.js
Normal file
95
models/ParentAssignedTask.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* ParentAssignedTask Model - Phụ huynh gán bài tập cho con
|
||||
*/
|
||||
const ParentAssignedTask = sequelize.define('parent_assigned_tasks', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
parent_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users_auth',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Phụ huynh gán',
|
||||
},
|
||||
student_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'student_details',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Học sinh được gán',
|
||||
},
|
||||
grade_item_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'grade_items',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Bài tập được gán',
|
||||
},
|
||||
assigned_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: 'Thời gian gán',
|
||||
},
|
||||
due_date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
comment: 'Hạn hoàn thành',
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'overdue'),
|
||||
defaultValue: 'pending',
|
||||
comment: 'Trạng thái',
|
||||
},
|
||||
message: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Lời nhắn của phụ huynh',
|
||||
},
|
||||
priority: {
|
||||
type: DataTypes.ENUM('low', 'normal', 'high'),
|
||||
defaultValue: 'normal',
|
||||
comment: 'Độ ưu tiên',
|
||||
},
|
||||
completion_date: {
|
||||
type: DataTypes.DATE,
|
||||
comment: 'Ngày hoàn thành',
|
||||
},
|
||||
student_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Ghi chú của học sinh khi hoàn thành',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'parent_assigned_tasks',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['parent_id'] },
|
||||
{ fields: ['student_id'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['due_date'] },
|
||||
],
|
||||
comment: 'Bảng quản lý bài tập phụ huynh gán cho con',
|
||||
});
|
||||
|
||||
module.exports = ParentAssignedTask;
|
||||
24
models/ParentStudentMap.js
Normal file
24
models/ParentStudentMap.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const ParentStudentMap = sequelize.define('parent_student_map', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
parent_id: { type: DataTypes.UUID, allowNull: false },
|
||||
student_id: { type: DataTypes.UUID, allowNull: false },
|
||||
relationship: { type: DataTypes.ENUM('father', 'mother', 'guardian', 'other'), allowNull: false },
|
||||
is_primary_contact: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||||
can_assign_tasks: { type: DataTypes.BOOLEAN, defaultValue: true, comment: 'Quyền gán bài tập' },
|
||||
can_view_detailed_grades: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Xem điểm chi tiết' },
|
||||
can_communicate_teacher: { type: DataTypes.BOOLEAN, defaultValue: true, comment: 'Liên hệ giáo viên' },
|
||||
permission_level: { type: DataTypes.ENUM('limited', 'standard', 'full'), defaultValue: 'standard', comment: 'Cấp quyền' },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'parent_student_map',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['can_assign_tasks', 'can_view_detailed_grades'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = ParentStudentMap;
|
||||
24
models/Permission.js
Normal file
24
models/Permission.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Permission = sequelize.define('permissions', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: 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 },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'permissions',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['permission_code'], unique: true },
|
||||
{ fields: ['resource', 'action'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = Permission;
|
||||
64
models/Role.js
Normal file
64
models/Role.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* Roles Model - Danh sách 9 vai trò trong hệ thống
|
||||
*/
|
||||
const Role = sequelize.define('roles', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
role_code: {
|
||||
type: DataTypes.STRING(50),
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
comment: 'Mã vai trò (system_admin, center_manager, etc.)',
|
||||
},
|
||||
role_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: 'Tên vai trò',
|
||||
},
|
||||
role_description: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Mô tả vai trò',
|
||||
},
|
||||
level: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Cấp độ vai trò (0=cao nhất)',
|
||||
},
|
||||
is_system_role: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Vai trò hệ thống không thể xóa',
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'roles',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['role_code'], unique: true },
|
||||
{ fields: ['level'] },
|
||||
{ fields: ['is_active'] },
|
||||
],
|
||||
comment: 'Bảng quản lý vai trò (9 vai trò)',
|
||||
});
|
||||
|
||||
module.exports = Role;
|
||||
18
models/RolePermission.js
Normal file
18
models/RolePermission.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const RolePermission = sequelize.define('role_permissions', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
role_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'roles', key: 'id' } },
|
||||
permission_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'permissions', key: 'id' } },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'role_permissions',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['role_id', 'permission_id'], unique: true },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = RolePermission;
|
||||
20
models/Room.js
Normal file
20
models/Room.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Room = sequelize.define('rooms', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
room_code: { type: DataTypes.STRING(20), allowNull: false },
|
||||
room_name: { type: DataTypes.STRING(100), allowNull: false },
|
||||
school_id: { type: DataTypes.UUID, allowNull: false },
|
||||
room_type: { type: DataTypes.ENUM('classroom', 'lab', 'library', 'gym', 'other') },
|
||||
capacity: { type: DataTypes.INTEGER },
|
||||
floor: { type: DataTypes.INTEGER },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'rooms',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = Room;
|
||||
83
models/School.js
Normal file
83
models/School.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* Schools Model - Quản lý thông tin 200 trường
|
||||
*/
|
||||
const School = sequelize.define('schools', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
school_code: {
|
||||
type: DataTypes.STRING(20),
|
||||
unique: false,
|
||||
allowNull: false,
|
||||
comment: 'Mã trường duy nhất',
|
||||
},
|
||||
school_name: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: 'Tên trường',
|
||||
},
|
||||
school_type: {
|
||||
type: DataTypes.ENUM('preschool', 'primary', 'secondary', 'high_school'),
|
||||
allowNull: false,
|
||||
comment: 'Loại trường',
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Địa chỉ trường',
|
||||
},
|
||||
city: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'Thành phố',
|
||||
},
|
||||
district: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'Quận/Huyện',
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
comment: 'Số điện thoại',
|
||||
},
|
||||
logo: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: 'URL logo của trường',
|
||||
},
|
||||
config: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: {},
|
||||
comment: 'Cấu hình riêng của trường (JSON)',
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Trạng thái hoạt động',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'schools',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['school_code'], unique: true },
|
||||
{ fields: ['school_type'] },
|
||||
{ fields: ['is_active'] },
|
||||
{ fields: ['city', 'district'] },
|
||||
],
|
||||
comment: 'Bảng quản lý thông tin 200 trường học',
|
||||
});
|
||||
|
||||
module.exports = School;
|
||||
92
models/StaffAchievement.js
Normal file
92
models/StaffAchievement.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* StaffAchievement Model - Lưu trữ chứng chỉ và thành tích đào tạo
|
||||
*/
|
||||
const StaffAchievement = sequelize.define('staff_achievements', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
staff_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users_auth',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Nhân viên',
|
||||
},
|
||||
course_id: {
|
||||
type: DataTypes.UUID,
|
||||
references: {
|
||||
model: 'subjects',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Khóa học trong hệ thống',
|
||||
},
|
||||
course_name: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
comment: 'Tên khóa học',
|
||||
},
|
||||
completion_date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: 'Ngày hoàn thành',
|
||||
},
|
||||
certificate_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
comment: 'URL file chứng chỉ',
|
||||
},
|
||||
certificate_code: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'Mã chứng chỉ',
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('mandatory', 'self_study', 'external'),
|
||||
allowNull: false,
|
||||
comment: 'Loại: bắt buộc, tự học, bên ngoài',
|
||||
},
|
||||
score: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
comment: 'Điểm số (0-100)',
|
||||
},
|
||||
total_hours: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: 'Tổng số giờ học',
|
||||
},
|
||||
verified_by: {
|
||||
type: DataTypes.UUID,
|
||||
references: {
|
||||
model: 'users_auth',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Admin xác nhận',
|
||||
},
|
||||
verified_at: {
|
||||
type: DataTypes.DATE,
|
||||
comment: 'Thời gian xác nhận',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'staff_achievements',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['staff_id'] },
|
||||
{ fields: ['type'] },
|
||||
{ fields: ['completion_date'] },
|
||||
{ fields: ['certificate_code'] },
|
||||
],
|
||||
comment: 'Bảng lưu trữ chứng chỉ và thành tích đào tạo',
|
||||
});
|
||||
|
||||
module.exports = StaffAchievement;
|
||||
21
models/StaffContract.js
Normal file
21
models/StaffContract.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const StaffContract = sequelize.define('staff_contracts', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
teacher_id: { type: DataTypes.UUID, allowNull: false },
|
||||
school_id: { type: DataTypes.UUID, 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' },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'staff_contracts',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
});
|
||||
|
||||
module.exports = StaffContract;
|
||||
90
models/StaffTrainingAssignment.js
Normal file
90
models/StaffTrainingAssignment.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* StaffTrainingAssignment Model - Quản lý phân công đào tạo nhân sự
|
||||
*/
|
||||
const StaffTrainingAssignment = sequelize.define('staff_training_assignments', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
staff_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users_auth',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Nhân viên được giao',
|
||||
},
|
||||
subject_id: {
|
||||
type: DataTypes.UUID,
|
||||
references: {
|
||||
model: 'subjects',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Khóa học/môn học bắt buộc',
|
||||
},
|
||||
course_name: {
|
||||
type: DataTypes.STRING(200),
|
||||
comment: 'Tên khóa học (nếu không có subject_id)',
|
||||
},
|
||||
assigned_by: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users_auth',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Admin giao nhiệm vụ',
|
||||
},
|
||||
assigned_date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: 'Ngày giao',
|
||||
},
|
||||
deadline: {
|
||||
type: DataTypes.DATEONLY,
|
||||
comment: 'Hạn hoàn thành',
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'overdue'),
|
||||
defaultValue: 'pending',
|
||||
comment: 'Trạng thái',
|
||||
},
|
||||
priority: {
|
||||
type: DataTypes.ENUM('low', 'normal', 'high', 'urgent'),
|
||||
defaultValue: 'normal',
|
||||
comment: 'Độ ưu tiên',
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Ghi chú',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'staff_training_assignments',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['staff_id'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['deadline'] },
|
||||
{ fields: ['assigned_by'] },
|
||||
],
|
||||
comment: 'Bảng quản lý phân công đào tạo nhân sự',
|
||||
});
|
||||
|
||||
module.exports = StaffTrainingAssignment;
|
||||
24
models/StudentDetail.js
Normal file
24
models/StudentDetail.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const StudentDetail = sequelize.define('student_details', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
user_id: { type: DataTypes.UUID, unique: true, allowNull: false, references: { model: 'user_profiles', key: 'user_id' } },
|
||||
student_code: { type: DataTypes.STRING(50), unique: true, allowNull: false },
|
||||
enrollment_date: { type: DataTypes.DATE },
|
||||
current_class_id: { type: DataTypes.UUID, references: { model: 'classes', key: 'id' } },
|
||||
status: { type: DataTypes.ENUM('active', 'on_leave', 'graduated', 'dropped'), defaultValue: 'active' },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'student_details',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['user_id'], unique: true },
|
||||
{ fields: ['student_code'], unique: true },
|
||||
{ fields: ['current_class_id'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = StudentDetail;
|
||||
29
models/Subject.js
Normal file
29
models/Subject.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Subject = sequelize.define('subjects', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: 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 },
|
||||
is_premium: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung premium' },
|
||||
is_training: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung đào tạo nhân sự' },
|
||||
is_public: { type: DataTypes.BOOLEAN, defaultValue: false, comment: 'Nội dung tự học công khai' },
|
||||
required_role: { type: DataTypes.STRING(50), comment: 'Role yêu cầu' },
|
||||
min_subscription_tier: { type: DataTypes.STRING(50), comment: 'Gói tối thiểu: basic, premium, vip' },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'subjects',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['is_premium'] },
|
||||
{ fields: ['is_training'] },
|
||||
{ fields: ['is_public'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = Subject;
|
||||
69
models/SubscriptionPlan.js
Normal file
69
models/SubscriptionPlan.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* SubscriptionPlan Model - Định nghĩa các gói thẻ tháng
|
||||
*/
|
||||
const SubscriptionPlan = sequelize.define('subscription_plans', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: 'Tên gói: VIP Học sinh, Premium Giáo viên, etc.',
|
||||
},
|
||||
price: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
comment: 'Giá tiền (VND)',
|
||||
},
|
||||
duration_days: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 30,
|
||||
comment: 'Thời hạn gói (ngày)',
|
||||
},
|
||||
target_role: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: 'Role được phép mua: student, parent, teacher',
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: 'Mô tả gói',
|
||||
},
|
||||
features: {
|
||||
type: DataTypes.JSON,
|
||||
comment: 'Danh sách tính năng: ["detailed_reports", "priority_support"]',
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Trạng thái kích hoạt',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'subscription_plans',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['target_role'] },
|
||||
{ fields: ['is_active'] },
|
||||
{ fields: ['price'] },
|
||||
],
|
||||
comment: 'Bảng định nghĩa các gói thẻ tháng',
|
||||
});
|
||||
|
||||
module.exports = SubscriptionPlan;
|
||||
31
models/TeacherDetail.js
Normal file
31
models/TeacherDetail.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const TeacherDetail = sequelize.define('teacher_details', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
user_id: { type: DataTypes.UUID, unique: true, allowNull: false, references: { model: 'user_profiles', key: 'user_id' } },
|
||||
teacher_code: { type: DataTypes.STRING(50), 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' },
|
||||
skill_tags: { type: DataTypes.JSON, comment: 'Array of skills: [{"name": "Python", "level": "expert"}]' },
|
||||
certifications: { type: DataTypes.JSON, comment: 'Array of certificates' },
|
||||
training_hours: { type: DataTypes.INTEGER, defaultValue: 0, comment: 'Tổng giờ đào tạo' },
|
||||
last_training_date: { type: DataTypes.DATEONLY, comment: 'Ngày đào tạo gần nhất' },
|
||||
training_score_avg: { type: DataTypes.DECIMAL(5, 2), comment: 'Điểm TB các khóa học' },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'teacher_details',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['user_id'], unique: true },
|
||||
{ fields: ['teacher_code'], unique: true },
|
||||
{ fields: ['teacher_type'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = TeacherDetail;
|
||||
27
models/UserAssignment.js
Normal file
27
models/UserAssignment.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const UserAssignment = sequelize.define('user_assignments', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
user_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'users_auth', key: 'id' } },
|
||||
role_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'roles', key: 'id' } },
|
||||
school_id: { type: DataTypes.UUID, references: { model: 'schools', key: 'id' } },
|
||||
class_id: { type: DataTypes.UUID, 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 },
|
||||
created_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
updated_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'user_assignments',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['user_id', 'role_id', 'school_id'] },
|
||||
{ fields: ['school_id'] },
|
||||
{ fields: ['is_active'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = UserAssignment;
|
||||
79
models/UserProfile.js
Normal file
79
models/UserProfile.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const UserProfile = sequelize.define('user_profiles', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.UUID,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users_auth',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
full_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
},
|
||||
first_name: {
|
||||
type: DataTypes.STRING(50),
|
||||
},
|
||||
last_name: {
|
||||
type: DataTypes.STRING(50),
|
||||
},
|
||||
date_of_birth: {
|
||||
type: DataTypes.DATE,
|
||||
},
|
||||
gender: {
|
||||
type: DataTypes.ENUM('male', 'female', 'other'),
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
},
|
||||
avatar_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
school_id: {
|
||||
type: DataTypes.UUID,
|
||||
},
|
||||
city: {
|
||||
type: DataTypes.STRING(100),
|
||||
},
|
||||
district: {
|
||||
type: DataTypes.STRING(100),
|
||||
},
|
||||
etc: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: {},
|
||||
comment: 'Thông tin bổ sung (JSONB)',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'user_profiles',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['user_id'], unique: true },
|
||||
{ fields: ['full_name'] },
|
||||
{ fields: ['phone'] },
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = UserProfile;
|
||||
87
models/UserSubscription.js
Normal file
87
models/UserSubscription.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* UserSubscription Model - Quản lý subscription của từng user
|
||||
*/
|
||||
const UserSubscription = sequelize.define('user_subscriptions', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users_auth',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'User mua gói',
|
||||
},
|
||||
plan_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'subscription_plans',
|
||||
key: 'id',
|
||||
},
|
||||
comment: 'Gói đã mua',
|
||||
},
|
||||
start_date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false,
|
||||
comment: 'Ngày bắt đầu',
|
||||
},
|
||||
end_date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false,
|
||||
comment: 'Ngày kết thúc',
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'expired', 'cancelled'),
|
||||
defaultValue: 'active',
|
||||
allowNull: false,
|
||||
comment: 'Trạng thái subscription',
|
||||
},
|
||||
transaction_id: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: 'Mã giao dịch để đối soát',
|
||||
},
|
||||
payment_method: {
|
||||
type: DataTypes.STRING(50),
|
||||
comment: 'Phương thức thanh toán: momo, vnpay, banking',
|
||||
},
|
||||
payment_amount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
comment: 'Số tiền đã thanh toán',
|
||||
},
|
||||
auto_renew: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Tự động gia hạn',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'user_subscriptions',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['user_id'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['end_date'] },
|
||||
{ fields: ['transaction_id'] },
|
||||
],
|
||||
comment: 'Bảng quản lý subscription của user',
|
||||
});
|
||||
|
||||
module.exports = UserSubscription;
|
||||
112
models/UsersAuth.js
Normal file
112
models/UsersAuth.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* UsersAuth Model - Thông tin đăng nhập người dùng
|
||||
*/
|
||||
const UsersAuth = sequelize.define('users_auth', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
comment: 'Tên đăng nhập duy nhất',
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
isEmail: true,
|
||||
},
|
||||
comment: 'Email đăng nhập',
|
||||
},
|
||||
password_hash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
comment: 'Mật khẩu đã hash',
|
||||
},
|
||||
salt: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
comment: 'Salt cho mật khẩu',
|
||||
},
|
||||
qr_version: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1,
|
||||
comment: 'Phiên bản mã QR cho điểm danh',
|
||||
},
|
||||
qr_secret: {
|
||||
type: DataTypes.STRING(255),
|
||||
comment: 'Secret key cho QR code',
|
||||
},
|
||||
current_session_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
comment: 'ID phiên đăng nhập hiện tại',
|
||||
},
|
||||
last_login: {
|
||||
type: DataTypes.DATE,
|
||||
comment: 'Lần đăng nhập cuối',
|
||||
},
|
||||
last_login_ip: {
|
||||
type: DataTypes.STRING(50),
|
||||
comment: 'IP đăng nhập cuối',
|
||||
},
|
||||
login_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Số lần đăng nhập',
|
||||
},
|
||||
login_attempts: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Số lần thử đăng nhập',
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: 'Trạng thái tài khoản',
|
||||
},
|
||||
is_locked: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Tài khoản bị khóa',
|
||||
},
|
||||
failed_login_attempts: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: 'Số lần đăng nhập thất bại',
|
||||
},
|
||||
locked_until: {
|
||||
type: DataTypes.DATE,
|
||||
comment: 'Khóa tài khoản đến',
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
}, {
|
||||
tableName: 'users_auth',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['username'], unique: true },
|
||||
{ fields: ['email'], unique: true },
|
||||
{ fields: ['is_active'] },
|
||||
{ fields: ['qr_version'] },
|
||||
{ fields: ['current_session_id'] },
|
||||
],
|
||||
comment: 'Bảng quản lý thông tin đăng nhập người dùng',
|
||||
});
|
||||
|
||||
module.exports = UsersAuth;
|
||||
271
models/index.js
Normal file
271
models/index.js
Normal file
@@ -0,0 +1,271 @@
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* Import all models
|
||||
*/
|
||||
// Group 1: System & Authorization
|
||||
const School = require('./School');
|
||||
const UsersAuth = require('./UsersAuth');
|
||||
const Role = require('./Role');
|
||||
const Permission = require('./Permission');
|
||||
const RolePermission = require('./RolePermission');
|
||||
const UserAssignment = require('./UserAssignment');
|
||||
|
||||
// Group 2: User Profiles
|
||||
const UserProfile = require('./UserProfile');
|
||||
const StudentDetail = require('./StudentDetail');
|
||||
const TeacherDetail = require('./TeacherDetail');
|
||||
const ParentStudentMap = require('./ParentStudentMap');
|
||||
const StaffContract = require('./StaffContract');
|
||||
|
||||
// Group 3: Academic Structure
|
||||
const AcademicYear = require('./AcademicYear');
|
||||
const Subject = require('./Subject');
|
||||
const Class = require('./Class');
|
||||
const ClassSchedule = require('./ClassSchedule');
|
||||
const Room = require('./Room');
|
||||
|
||||
// Group 3.1: Learning Content (NEW)
|
||||
const Chapter = require('./Chapter');
|
||||
const Lesson = require('./Lesson');
|
||||
const Game = require('./Game');
|
||||
|
||||
// Group 4: Attendance
|
||||
const AttendanceLog = require('./AttendanceLog');
|
||||
const AttendanceDaily = require('./AttendanceDaily');
|
||||
const LeaveRequest = require('./LeaveRequest');
|
||||
|
||||
// Group 5: Gradebook
|
||||
const GradeCategory = require('./GradeCategory');
|
||||
const GradeItem = require('./GradeItem');
|
||||
const Grade = require('./Grade');
|
||||
const GradeSummary = require('./GradeSummary');
|
||||
const GradeHistory = require('./GradeHistory');
|
||||
|
||||
// Group 6: Notifications
|
||||
const Notification = require('./Notification');
|
||||
const NotificationLog = require('./NotificationLog');
|
||||
const Message = require('./Message');
|
||||
const AuditLog = require('./AuditLog');
|
||||
|
||||
// Group 7: Subscription & Training
|
||||
const SubscriptionPlan = require('./SubscriptionPlan');
|
||||
const UserSubscription = require('./UserSubscription');
|
||||
const StaffTrainingAssignment = require('./StaffTrainingAssignment');
|
||||
const StaffAchievement = require('./StaffAchievement');
|
||||
const ParentAssignedTask = require('./ParentAssignedTask');
|
||||
|
||||
/**
|
||||
* Define relationships between models
|
||||
*/
|
||||
const setupRelationships = () => {
|
||||
// UsersAuth relationships
|
||||
UsersAuth.hasOne(UserProfile, { foreignKey: 'user_id', as: 'profile' });
|
||||
UsersAuth.hasMany(UserAssignment, { foreignKey: 'user_id', as: 'assignments' });
|
||||
|
||||
// UserProfile relationships
|
||||
UserProfile.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'auth' });
|
||||
UserProfile.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
|
||||
UserProfile.hasOne(StudentDetail, { foreignKey: 'user_id', as: 'studentDetail' });
|
||||
UserProfile.hasOne(TeacherDetail, { foreignKey: 'user_id', as: 'teacherDetail' });
|
||||
|
||||
// School relationships
|
||||
School.hasMany(Class, { foreignKey: 'school_id', as: 'classes' });
|
||||
School.hasMany(Room, { foreignKey: 'school_id', as: 'rooms' });
|
||||
School.hasMany(UserAssignment, { foreignKey: 'school_id', as: 'assignments' });
|
||||
School.hasMany(AttendanceLog, { foreignKey: 'school_id', as: 'attendanceLogs' });
|
||||
|
||||
// Role & Permission relationships
|
||||
Role.belongsToMany(Permission, {
|
||||
through: RolePermission,
|
||||
foreignKey: 'role_id',
|
||||
otherKey: 'permission_id',
|
||||
as: 'permissions'
|
||||
});
|
||||
Permission.belongsToMany(Role, {
|
||||
through: RolePermission,
|
||||
foreignKey: 'permission_id',
|
||||
otherKey: 'role_id',
|
||||
as: 'roles'
|
||||
});
|
||||
|
||||
// UserAssignment relationships
|
||||
UserAssignment.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
|
||||
UserAssignment.belongsTo(Role, { foreignKey: 'role_id', as: 'role' });
|
||||
UserAssignment.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
|
||||
UserAssignment.belongsTo(Class, { foreignKey: 'class_id', as: 'class' });
|
||||
|
||||
// StudentDetail relationships
|
||||
StudentDetail.belongsTo(UserProfile, {
|
||||
foreignKey: 'user_id',
|
||||
targetKey: 'user_id',
|
||||
as: 'profile'
|
||||
});
|
||||
StudentDetail.belongsTo(Class, { foreignKey: 'current_class_id', as: 'currentClass' });
|
||||
StudentDetail.belongsToMany(UserProfile, {
|
||||
through: ParentStudentMap,
|
||||
foreignKey: 'student_id',
|
||||
otherKey: 'parent_id',
|
||||
as: 'parents'
|
||||
});
|
||||
|
||||
// TeacherDetail relationships
|
||||
TeacherDetail.belongsTo(UserProfile, { foreignKey: 'user_id', as: 'profile' });
|
||||
TeacherDetail.hasMany(StaffContract, { foreignKey: 'teacher_id', as: 'contracts' });
|
||||
|
||||
// Class relationships
|
||||
Class.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
|
||||
Class.belongsTo(AcademicYear, { foreignKey: 'academic_year_id', as: 'academicYear' });
|
||||
Class.belongsTo(TeacherDetail, { foreignKey: 'homeroom_teacher_id', as: 'homeroomTeacher' });
|
||||
Class.hasMany(StudentDetail, { foreignKey: 'current_class_id', as: 'students' });
|
||||
Class.hasMany(ClassSchedule, { foreignKey: 'class_id', as: 'schedules' });
|
||||
Class.hasMany(Grade, { foreignKey: 'class_id', as: 'grades' });
|
||||
|
||||
// ClassSchedule relationships
|
||||
ClassSchedule.belongsTo(Class, { foreignKey: 'class_id', as: 'class' });
|
||||
ClassSchedule.belongsTo(Subject, { foreignKey: 'subject_id', as: 'subject' });
|
||||
ClassSchedule.belongsTo(Room, { foreignKey: 'room_id', as: 'room' });
|
||||
ClassSchedule.belongsTo(TeacherDetail, { foreignKey: 'teacher_id', as: 'teacher' });
|
||||
|
||||
// Learning Content relationships (NEW)
|
||||
// Subject -> Chapter (1:N)
|
||||
Subject.hasMany(Chapter, { foreignKey: 'subject_id', as: 'chapters' });
|
||||
Chapter.belongsTo(Subject, { foreignKey: 'subject_id', as: 'subject' });
|
||||
|
||||
// Chapter -> Lesson (1:N)
|
||||
Chapter.hasMany(Lesson, { foreignKey: 'chapter_id', as: 'lessons' });
|
||||
Lesson.belongsTo(Chapter, { foreignKey: 'chapter_id', as: 'chapter' });
|
||||
|
||||
// Attendance relationships
|
||||
AttendanceLog.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
|
||||
AttendanceLog.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
|
||||
|
||||
AttendanceDaily.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
|
||||
AttendanceDaily.belongsTo(School, { foreignKey: 'school_id', as: 'school' });
|
||||
AttendanceDaily.belongsTo(Class, { foreignKey: 'class_id', as: 'class' });
|
||||
|
||||
LeaveRequest.belongsTo(StudentDetail, { foreignKey: 'student_id', as: 'student' });
|
||||
LeaveRequest.belongsTo(UserProfile, { foreignKey: 'requested_by', as: 'requester' });
|
||||
LeaveRequest.belongsTo(TeacherDetail, { foreignKey: 'approved_by', as: 'approver' });
|
||||
|
||||
// Grade relationships
|
||||
GradeCategory.belongsTo(Subject, { foreignKey: 'subject_id', as: 'subject' });
|
||||
GradeCategory.hasMany(GradeItem, { foreignKey: 'category_id', as: 'items' });
|
||||
|
||||
GradeItem.belongsTo(GradeCategory, { foreignKey: 'category_id', as: 'category' });
|
||||
GradeItem.belongsTo(Class, { foreignKey: 'class_id', as: 'class' });
|
||||
GradeItem.hasMany(Grade, { foreignKey: 'grade_item_id', as: 'grades' });
|
||||
|
||||
Grade.belongsTo(GradeItem, { foreignKey: 'grade_item_id', as: 'gradeItem' });
|
||||
Grade.belongsTo(StudentDetail, { foreignKey: 'student_id', as: 'student' });
|
||||
Grade.belongsTo(Class, { foreignKey: 'class_id', as: 'class' });
|
||||
Grade.belongsTo(TeacherDetail, { foreignKey: 'graded_by', as: 'gradedBy' });
|
||||
Grade.hasMany(GradeHistory, { foreignKey: 'grade_id', as: 'history' });
|
||||
|
||||
GradeSummary.belongsTo(StudentDetail, { foreignKey: 'student_id', as: 'student' });
|
||||
GradeSummary.belongsTo(Subject, { foreignKey: 'subject_id', as: 'subject' });
|
||||
GradeSummary.belongsTo(AcademicYear, { foreignKey: 'academic_year_id', as: 'academicYear' });
|
||||
|
||||
// Notification relationships
|
||||
Notification.hasMany(NotificationLog, { foreignKey: 'notification_id', as: 'logs' });
|
||||
NotificationLog.belongsTo(Notification, { foreignKey: 'notification_id', as: 'notification' });
|
||||
NotificationLog.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
|
||||
|
||||
Message.belongsTo(UserProfile, { foreignKey: 'sender_id', as: 'sender' });
|
||||
Message.belongsTo(UserProfile, { foreignKey: 'recipient_id', as: 'recipient' });
|
||||
|
||||
// Subscription relationships
|
||||
SubscriptionPlan.hasMany(UserSubscription, { foreignKey: 'plan_id', as: 'subscriptions' });
|
||||
UserSubscription.belongsTo(SubscriptionPlan, { foreignKey: 'plan_id', as: 'plan' });
|
||||
UserSubscription.belongsTo(UsersAuth, { foreignKey: 'user_id', as: 'user' });
|
||||
|
||||
// Training relationships
|
||||
StaffTrainingAssignment.belongsTo(UsersAuth, { foreignKey: 'staff_id', as: 'staff' });
|
||||
StaffTrainingAssignment.belongsTo(UsersAuth, { foreignKey: 'assigned_by', as: 'assigner' });
|
||||
StaffTrainingAssignment.belongsTo(Subject, { foreignKey: 'subject_id', as: 'subject' });
|
||||
|
||||
StaffAchievement.belongsTo(UsersAuth, { foreignKey: 'staff_id', as: 'staff' });
|
||||
StaffAchievement.belongsTo(Subject, { foreignKey: 'course_id', as: 'course' });
|
||||
StaffAchievement.belongsTo(UsersAuth, { foreignKey: 'verified_by', as: 'verifier' });
|
||||
|
||||
// Parent assigned task relationships
|
||||
ParentAssignedTask.belongsTo(UsersAuth, { foreignKey: 'parent_id', as: 'parent' });
|
||||
ParentAssignedTask.belongsTo(StudentDetail, { foreignKey: 'student_id', as: 'student' });
|
||||
ParentAssignedTask.belongsTo(GradeItem, { foreignKey: 'grade_item_id', as: 'gradeItem' });
|
||||
|
||||
console.log('✅ Model relationships configured');
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync database (only in development)
|
||||
*/
|
||||
const syncDatabase = async (options = {}) => {
|
||||
try {
|
||||
await sequelize.sync(options);
|
||||
console.log('✅ Database synchronized');
|
||||
} catch (error) {
|
||||
console.error('❌ Database sync failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Export all models and utilities
|
||||
*/
|
||||
module.exports = {
|
||||
sequelize,
|
||||
setupRelationships,
|
||||
syncDatabase,
|
||||
|
||||
// Group 1: System & Authorization
|
||||
School,
|
||||
UsersAuth,
|
||||
Role,
|
||||
Permission,
|
||||
RolePermission,
|
||||
UserAssignment,
|
||||
|
||||
// Group 2: User Profiles
|
||||
UserProfile,
|
||||
StudentDetail,
|
||||
TeacherDetail,
|
||||
ParentStudentMap,
|
||||
StaffContract,
|
||||
|
||||
// Group 3: Academic Structure
|
||||
AcademicYear,
|
||||
Subject,
|
||||
Class,
|
||||
ClassSchedule,
|
||||
Room,
|
||||
|
||||
// Group 3.1: Learning Content (NEW)
|
||||
Chapter,
|
||||
Lesson,
|
||||
Game,
|
||||
|
||||
// Group 4: Attendance
|
||||
AttendanceLog,
|
||||
AttendanceDaily,
|
||||
LeaveRequest,
|
||||
|
||||
// Group 5: Gradebook
|
||||
GradeCategory,
|
||||
GradeItem,
|
||||
Grade,
|
||||
GradeSummary,
|
||||
GradeHistory,
|
||||
|
||||
// Group 6: Notifications
|
||||
Notification,
|
||||
NotificationLog,
|
||||
Message,
|
||||
AuditLog,
|
||||
|
||||
// Group 7: Subscription & Training
|
||||
SubscriptionPlan,
|
||||
UserSubscription,
|
||||
StaffTrainingAssignment,
|
||||
StaffAchievement,
|
||||
ParentAssignedTask,
|
||||
};
|
||||
Reference in New Issue
Block a user