303 lines
13 KiB
JavaScript
303 lines
13 KiB
JavaScript
import mysql from 'mysql2/promise';
|
|
import dotenv from 'dotenv';
|
|
|
|
dotenv.config();
|
|
|
|
// Database connection pool
|
|
const pool = mysql.createPool({
|
|
host: process.env.DB_HOST || 'localhost',
|
|
port: parseInt(process.env.DB_PORT || '3306'),
|
|
user: process.env.DB_USER || 'root',
|
|
password: process.env.DB_PASSWORD || '',
|
|
database: process.env.DB_NAME || 'sipasi_db',
|
|
waitForConnections: true,
|
|
connectionLimit: 10,
|
|
queueLimit: 0
|
|
});
|
|
|
|
// Auto-create tables on startup
|
|
export async function initDatabase() {
|
|
const connection = await pool.getConnection();
|
|
|
|
try {
|
|
console.log('🔌 Connected to MySQL database');
|
|
|
|
// Create students table
|
|
await connection.execute(`
|
|
CREATE TABLE IF NOT EXISTS students (
|
|
id VARCHAR(20) PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
class VARCHAR(50) NOT NULL,
|
|
parent_name VARCHAR(255),
|
|
parent_phone VARCHAR(20),
|
|
shift VARCHAR(20) DEFAULT 'Siang',
|
|
face_descriptor TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
console.log('✅ Table "students" ready');
|
|
|
|
// Add columns if they don't exist (for existing databases)
|
|
try {
|
|
await connection.execute('ALTER TABLE students ADD COLUMN IF NOT EXISTS shift VARCHAR(20) DEFAULT "Siang"');
|
|
await connection.execute('ALTER TABLE students ADD COLUMN IF NOT EXISTS face_descriptor TEXT');
|
|
} catch (err) {
|
|
// Ignore if columns already exist or IF NOT EXISTS is not supported
|
|
}
|
|
|
|
// Create violations table
|
|
await connection.execute(`
|
|
CREATE TABLE IF NOT EXISTS violations (
|
|
id VARCHAR(50) PRIMARY KEY,
|
|
student_id VARCHAR(20) NOT NULL,
|
|
rule_id VARCHAR(10) NOT NULL,
|
|
date DATE NOT NULL,
|
|
description TEXT,
|
|
score INT NOT NULL DEFAULT 0,
|
|
sanction VARCHAR(255),
|
|
academic_year VARCHAR(20),
|
|
timestamp BIGINT,
|
|
photo_url TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
console.log('✅ Table "violations" ready');
|
|
|
|
try {
|
|
await connection.execute('ALTER TABLE violations ADD COLUMN IF NOT EXISTS academic_year VARCHAR(20)');
|
|
} catch (err) { }
|
|
|
|
// Create achievements table
|
|
await connection.execute(`
|
|
CREATE TABLE IF NOT EXISTS achievements (
|
|
id VARCHAR(50) PRIMARY KEY,
|
|
student_id VARCHAR(20) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
level ENUM('Nasional', 'Provinsi', 'Kabupaten/Kota', 'Sekolah') NOT NULL,
|
|
date DATE NOT NULL,
|
|
score_reduction INT NOT NULL DEFAULT 0,
|
|
academic_year VARCHAR(20),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
console.log('✅ Table "achievements" ready');
|
|
|
|
try {
|
|
await connection.execute('ALTER TABLE achievements ADD COLUMN IF NOT EXISTS academic_year VARCHAR(20)');
|
|
} catch (err) { }
|
|
|
|
// Create settings table
|
|
await connection.execute(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
setting_key VARCHAR(100) PRIMARY KEY,
|
|
setting_value LONGTEXT,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
console.log('✅ Table "settings" ready');
|
|
|
|
// Migration: Update setting_value to LONGTEXT for existing tables
|
|
try {
|
|
await connection.execute('ALTER TABLE settings MODIFY COLUMN setting_value LONGTEXT');
|
|
} catch (err) { }
|
|
|
|
await connection.execute(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
username VARCHAR(50) NOT NULL UNIQUE,
|
|
password VARCHAR(255) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
role ENUM('admin', 'guru', 'staff') DEFAULT 'staff',
|
|
permissions TEXT,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
console.log('✅ Table "users" ready');
|
|
|
|
// Create violation_rules table for dynamic violation categories
|
|
await connection.execute(`
|
|
CREATE TABLE IF NOT EXISTS violation_rules (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
code VARCHAR(20) NOT NULL UNIQUE,
|
|
description VARCHAR(500) NOT NULL,
|
|
category ENUM('Perilaku', 'Kerajinan', 'Kerapian') NOT NULL,
|
|
score INT NOT NULL DEFAULT 0,
|
|
default_sanction VARCHAR(255),
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
console.log('✅ Table "violation_rules" ready');
|
|
|
|
// Create achievement_criteria table for achievement scoring reference
|
|
await connection.execute(`
|
|
CREATE TABLE IF NOT EXISTS achievement_criteria (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
level ENUM('Kabupaten/Kota', 'Provinsi', 'Nasional', 'Internasional') NOT NULL,
|
|
achievement_type ENUM('Juara I', 'Juara II', 'Juara III', 'Harapan I', 'Harapan II', 'Harapan III') NOT NULL,
|
|
participant_type ENUM('Perorangan', 'Kelompok') NOT NULL,
|
|
group_size ENUM('1-3 orang', '4-6 orang', '7-12 orang', 'Lebih dari 12 orang') DEFAULT NULL,
|
|
score INT NOT NULL DEFAULT 0,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
UNIQUE KEY unique_criteria (level, achievement_type, participant_type, group_size)
|
|
)
|
|
`);
|
|
console.log('✅ Table "achievement_criteria" ready');
|
|
|
|
// Insert default admin user if not exists
|
|
const [userRows] = await connection.execute('SELECT COUNT(*) as count FROM users');
|
|
if (userRows[0].count === 0) {
|
|
const defaultPermissions = JSON.stringify(['dashboard', 'violation', 'achievement', 'students', 'report', 'settings']);
|
|
await connection.execute(`
|
|
INSERT INTO users (username, password, name, role, permissions)
|
|
VALUES ('admin', 'Smanab100%', 'Administrator', 'admin', ?)
|
|
`, [defaultPermissions]);
|
|
console.log('✅ Default admin user created');
|
|
}
|
|
|
|
// Insert default violation rules if table is empty
|
|
const [violationRulesRows] = await connection.execute('SELECT COUNT(*) as count FROM violation_rules');
|
|
if (violationRulesRows[0].count === 0) {
|
|
const defaultRules = [
|
|
['A1', 'Melakukan atau terlibat tindakan pidana', 'Perilaku', 100, null],
|
|
['A2', 'Membawa dan menggunakan narkoba', 'Perilaku', 100, null],
|
|
['A3', 'Menganiaya/intimidasi pada guru, kepala sekolah, dll', 'Perilaku', 50, null],
|
|
['A4', 'Membawa dan mengkonsumsi miras', 'Perilaku', 25, null],
|
|
['A5', 'Membawa dan menggunakan senjata tajam tanpa ijin', 'Perilaku', 25, null],
|
|
['A6', 'Membawa dan mengedarkan tayangan porno (buku, VCD, dll)', 'Perilaku', 25, null],
|
|
['A7', 'Berkelahi atau terlibat tawuran', 'Perilaku', 25, null],
|
|
['A8', 'Berbuat asusila', 'Perilaku', 25, null],
|
|
['A9', 'Merusak sarana & prasarana milik sekolah dan orang lain', 'Perilaku', 25, 'Mengganti sarana'],
|
|
['A10', 'Membawa rokok atau merokok di sekolah', 'Perilaku', 15, null],
|
|
['A11', 'Mengancam warga sekolah dan orang lain', 'Perilaku', 15, null],
|
|
['A12', 'Memalsukan tanda tangan kepala sekolah, guru dan stempel', 'Perilaku', 15, null],
|
|
['A13', 'Berada di luar lingkungan sekolah tanpa ijin saat PBM', 'Perilaku', 15, null],
|
|
['A14', 'Menerobos atau melompat pagar sekolah', 'Perilaku', 15, null],
|
|
|
|
['B1', 'Absen tanpa keterangan (Alpha)', 'Kerajinan', 5, null],
|
|
['B2', 'Tidak menyelesaikan administrasi yang ditentukan', 'Kerajinan', 1, null],
|
|
['B3', 'Terlambat hadir ke sekolah', 'Kerajinan', 1, null],
|
|
['B4', 'Terlambat mengikuti pembelajaran', 'Kerajinan', 1, null],
|
|
['B5', 'Terlambat menyerahkan tugas', 'Kerajinan', 1, null],
|
|
['B6', 'Absen mengikuti ulangan tanpa ijin', 'Kerajinan', 2, null],
|
|
['B7', 'Absen mengikuti upacara sekolah tanpa ijin', 'Kerajinan', 2, null],
|
|
['B8', 'Kesalahan yang terulang sampai 3x', 'Kerajinan', 6, null],
|
|
|
|
['C1', 'Memakai pakaian seragam tidak sesuai dengan ketentuan', 'Kerapian', 5, null],
|
|
['C2', 'Memelihara rambut tidak sesuai dengan ketentuan', 'Kerapian', 5, null],
|
|
['C3', 'Mengecat rambut selain warna hitam', 'Kerapian', 5, null],
|
|
['C4', 'Siswa putra/putri memakai perhiasan berlebihan', 'Kerapian', 5, null]
|
|
];
|
|
|
|
for (const rule of defaultRules) {
|
|
await connection.execute(`
|
|
INSERT INTO violation_rules (code, description, category, score, default_sanction)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`, rule);
|
|
}
|
|
console.log('✅ Default violation rules inserted');
|
|
}
|
|
|
|
// Insert default achievement criteria if table is empty
|
|
const [achievementCriteriaRows] = await connection.execute('SELECT COUNT(*) as count FROM achievement_criteria');
|
|
if (achievementCriteriaRows[0].count === 0) {
|
|
const levels = ['Kabupaten/Kota', 'Provinsi', 'Nasional', 'Internasional'];
|
|
const achievements = ['Juara I', 'Juara II', 'Juara III', 'Harapan I', 'Harapan II', 'Harapan III'];
|
|
const groupSizes = ['1-3 orang', '4-6 orang', '7-12 orang', 'Lebih dari 12 orang'];
|
|
|
|
const baseScores = {
|
|
'Internasional': { 'Juara I': 50, 'Juara II': 45, 'Juara III': 40, 'Harapan I': 35, 'Harapan II': 30, 'Harapan III': 25 },
|
|
'Nasional': { 'Juara I': 40, 'Juara II': 35, 'Juara III': 30, 'Harapan I': 25, 'Harapan II': 20, 'Harapan III': 15 },
|
|
'Provinsi': { 'Juara I': 30, 'Juara II': 25, 'Juara III': 20, 'Harapan I': 15, 'Harapan II': 10, 'Harapan III': 5 },
|
|
'Kabupaten/Kota': { 'Juara I': 20, 'Juara II': 15, 'Juara III': 10, 'Harapan I': 8, 'Harapan II': 5, 'Harapan III': 3 }
|
|
};
|
|
|
|
// Insert Perorangan criteria
|
|
for (const level of levels) {
|
|
for (const achievement of achievements) {
|
|
const score = baseScores[level][achievement];
|
|
await connection.execute(`
|
|
INSERT INTO achievement_criteria (level, achievement_type, participant_type, group_size, score)
|
|
VALUES (?, ?, 'Perorangan', NULL, ?)
|
|
`, [level, achievement, score]);
|
|
}
|
|
}
|
|
|
|
// Insert Kelompok criteria with different group sizes
|
|
for (const level of levels) {
|
|
for (const achievement of achievements) {
|
|
for (const groupSize of groupSizes) {
|
|
// Score decreases slightly for larger groups
|
|
let baseScore = baseScores[level][achievement];
|
|
if (groupSize === '1-3 orang') baseScore = Math.floor(baseScore * 0.9);
|
|
else if (groupSize === '4-6 orang') baseScore = Math.floor(baseScore * 0.8);
|
|
else if (groupSize === '7-12 orang') baseScore = Math.floor(baseScore * 0.7);
|
|
else baseScore = Math.floor(baseScore * 0.6);
|
|
|
|
await connection.execute(`
|
|
INSERT INTO achievement_criteria (level, achievement_type, participant_type, group_size, score)
|
|
VALUES (?, ?, 'Kelompok', ?, ?)
|
|
`, [level, achievement, groupSize, baseScore]);
|
|
}
|
|
}
|
|
}
|
|
console.log('✅ Default achievement criteria inserted');
|
|
}
|
|
|
|
// Insert default settings if not exists
|
|
const defaultSettings = [
|
|
['schoolName', 'SMA Negeri 1 Abiansemal'],
|
|
['academicYear', '2025/2026'],
|
|
['logoBase64', ''],
|
|
['kopBase64', ''],
|
|
['principalName', ''],
|
|
['principalNip', ''],
|
|
['counselorName', ''],
|
|
['counselorNip', '']
|
|
];
|
|
|
|
for (const [key, value] of defaultSettings) {
|
|
await connection.execute(`
|
|
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES (?, ?)
|
|
`, [key, value]);
|
|
}
|
|
console.log('✅ Default settings initialized');
|
|
|
|
// Insert sample students if table is empty
|
|
const [studentRows] = await connection.execute('SELECT COUNT(*) as count FROM students');
|
|
if (studentRows[0].count === 0) {
|
|
const sampleStudents = [
|
|
['2023001', 'I Putu Gede Mahendra', 'X-1', 'I Made Wijaya', '081234567890', 'Pagi'],
|
|
['2023002', 'Ni Kadek Ayu Lestari', 'X-1', 'I Ketut Arta', '081987654321', 'Pagi'],
|
|
['2023003', 'Komang Budi Utama', 'XI-IPA-2', 'Wayan Sudira', '08122334455', 'Siang'],
|
|
['2023004', 'Dewa Ayu Dewi', 'XII-IPS-1', 'Dewa Rai', '08177665544', 'Siang']
|
|
];
|
|
|
|
for (const student of sampleStudents) {
|
|
await connection.execute(`
|
|
INSERT INTO students (id, name, class, parent_name, parent_phone, shift) VALUES (?, ?, ?, ?, ?, ?)
|
|
`, student);
|
|
}
|
|
console.log('✅ Sample students inserted');
|
|
}
|
|
|
|
console.log('🎉 Database initialization complete!');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Database initialization error:', error);
|
|
throw error;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
export default pool;
|