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;