795 lines
27 KiB
JavaScript
795 lines
27 KiB
JavaScript
import express from 'express';
|
|
import path from 'path';
|
|
import cors from 'cors';
|
|
import { fileURLToPath } from 'url';
|
|
import dotenv from 'dotenv';
|
|
import bcrypt from 'bcryptjs';
|
|
import pool, { initDatabase } from './server/db.js';
|
|
|
|
dotenv.config();
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3006;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json({ limit: '50mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
|
|
|
// Helper: Convert camelCase to snake_case
|
|
const camelToSnake = (str) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
const snakeToCamel = (str) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
|
|
// Helper: Convert object keys
|
|
const convertKeysToSnake = (obj) => {
|
|
const result = {};
|
|
for (const key in obj) {
|
|
result[camelToSnake(key)] = obj[key];
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const convertKeysToCamel = (obj) => {
|
|
const result = {};
|
|
for (const key in obj) {
|
|
result[snakeToCamel(key)] = obj[key];
|
|
}
|
|
return result;
|
|
};
|
|
|
|
// ==================== AUTH API ====================
|
|
|
|
// Login
|
|
app.post('/api/auth/login', async (req, res) => {
|
|
try {
|
|
const { username, password } = req.body;
|
|
|
|
if (!username || !password) {
|
|
return res.status(400).json({ success: false, message: 'Username dan password diperlukan' });
|
|
}
|
|
|
|
const [users] = await pool.query('SELECT * FROM users WHERE username = ?', [username]);
|
|
|
|
if (users.length === 0) {
|
|
return res.status(401).json({ success: false, message: 'Username atau password salah' });
|
|
}
|
|
|
|
const user = users[0];
|
|
const isValid = await bcrypt.compare(password, user.password);
|
|
|
|
if (!isValid) {
|
|
return res.status(401).json({ success: false, message: 'Username atau password salah' });
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
fullName: user.full_name,
|
|
role: user.role
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
res.status(500).json({ success: false, message: 'Terjadi kesalahan server' });
|
|
}
|
|
});
|
|
|
|
// ==================== STUDENTS API ====================
|
|
|
|
// Get all students
|
|
app.get('/api/students', async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM students ORDER BY nama ASC');
|
|
const students = rows.map(row => {
|
|
const student = convertKeysToCamel(row);
|
|
// Normalize field names for frontend compatibility
|
|
return {
|
|
...student,
|
|
id: String(student.id),
|
|
fotoDiterimaUrl: student.fotoDiterimaUrl || '',
|
|
fotoLulusUrl: student.fotoLulusUrl || '',
|
|
};
|
|
});
|
|
res.json(students);
|
|
} catch (error) {
|
|
console.error('Error fetching students:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil data siswa' });
|
|
}
|
|
});
|
|
|
|
// Get single student
|
|
app.get('/api/students/:id', async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM students WHERE id = ?', [req.params.id]);
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ success: false, message: 'Siswa tidak ditemukan' });
|
|
}
|
|
const student = convertKeysToCamel(rows[0]);
|
|
student.id = String(student.id);
|
|
res.json(student);
|
|
} catch (error) {
|
|
console.error('Error fetching student:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil data siswa' });
|
|
}
|
|
});
|
|
|
|
// Create/Update student
|
|
app.post('/api/students', async (req, res) => {
|
|
try {
|
|
const studentData = req.body;
|
|
const { id, ...data } = studentData;
|
|
|
|
// Convert keys to snake_case for database
|
|
const dbData = convertKeysToSnake(data);
|
|
|
|
// Remove undefined/null values and clean up data
|
|
const cleanData = {};
|
|
for (const [key, value] of Object.entries(dbData)) {
|
|
if (value !== undefined && value !== null) {
|
|
cleanData[key] = value;
|
|
}
|
|
}
|
|
|
|
if (id && id !== '' && !isNaN(parseInt(id))) {
|
|
// Update existing student
|
|
const fields = Object.keys(cleanData);
|
|
const values = Object.values(cleanData);
|
|
const setClause = fields.map(f => `${f} = ?`).join(', ');
|
|
|
|
await pool.query(`UPDATE students SET ${setClause} WHERE id = ?`, [...values, parseInt(id)]);
|
|
res.json({ success: true, message: 'Data siswa berhasil diupdate', id: parseInt(id) });
|
|
} else {
|
|
// Insert new student
|
|
const fields = Object.keys(cleanData);
|
|
const values = Object.values(cleanData);
|
|
const placeholders = fields.map(() => '?').join(', ');
|
|
|
|
const [result] = await pool.query(
|
|
`INSERT INTO students (${fields.join(', ')}) VALUES (${placeholders})`,
|
|
values
|
|
);
|
|
res.json({ success: true, message: 'Data siswa berhasil disimpan', id: result.insertId });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving student:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal menyimpan data siswa: ' + error.message });
|
|
}
|
|
});
|
|
|
|
// Delete student
|
|
app.delete('/api/students/:id', async (req, res) => {
|
|
try {
|
|
const [result] = await pool.query('DELETE FROM students WHERE id = ?', [req.params.id]);
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ success: false, message: 'Siswa tidak ditemukan' });
|
|
}
|
|
res.json({ success: true, message: 'Data siswa berhasil dihapus' });
|
|
} catch (error) {
|
|
console.error('Error deleting student:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal menghapus data siswa' });
|
|
}
|
|
});
|
|
|
|
// ==================== SETTINGS API ====================
|
|
|
|
// Get settings
|
|
app.get('/api/settings', async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
|
|
if (rows.length === 0) {
|
|
res.json({
|
|
schoolName: 'SMA NEGERI 1 ABIANSEMAL',
|
|
logoUrl: 'https://iili.io/KN7pUR2.png',
|
|
faviconUrl: '',
|
|
tahunAjaran: '2025/2026',
|
|
margins: { top: 20, right: 20, bottom: 20, left: 20 }
|
|
});
|
|
} else {
|
|
res.json(JSON.parse(rows[0].setting_value));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching settings:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil pengaturan' });
|
|
}
|
|
});
|
|
|
|
// Save settings
|
|
app.post('/api/settings', async (req, res) => {
|
|
try {
|
|
const settings = req.body;
|
|
const [existing] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
|
|
|
|
if (existing.length === 0) {
|
|
await pool.query('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)',
|
|
['app_settings', JSON.stringify(settings)]);
|
|
} else {
|
|
await pool.query('UPDATE settings SET setting_value = ? WHERE setting_key = ?',
|
|
[JSON.stringify(settings), 'app_settings']);
|
|
}
|
|
|
|
res.json({ success: true, message: 'Pengaturan berhasil disimpan' });
|
|
} catch (error) {
|
|
console.error('Error saving settings:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal menyimpan pengaturan' });
|
|
}
|
|
});
|
|
|
|
// ==================== STATISTICS API ====================
|
|
|
|
// Get student statistics
|
|
app.get('/api/stats', async (req, res) => {
|
|
try {
|
|
const [total] = await pool.query('SELECT COUNT(*) as count FROM students');
|
|
const [laki] = await pool.query("SELECT COUNT(*) as count FROM students WHERE jenis_kelamin = 'L'");
|
|
const [perempuan] = await pool.query("SELECT COUNT(*) as count FROM students WHERE jenis_kelamin = 'P'");
|
|
const [byTahun] = await pool.query(
|
|
'SELECT tahun_ajaran, COUNT(*) as count FROM students GROUP BY tahun_ajaran ORDER BY tahun_ajaran DESC'
|
|
);
|
|
|
|
res.json({
|
|
total: total[0].count,
|
|
laki: laki[0].count,
|
|
perempuan: perempuan[0].count,
|
|
byTahunAjaran: byTahun
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching stats:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil statistik' });
|
|
}
|
|
});
|
|
|
|
// ==================== LEGER API ====================
|
|
|
|
// Get all legers for a student
|
|
app.get('/api/students/:id/legers', async (req, res) => {
|
|
try {
|
|
const studentId = req.params.id;
|
|
const [rows] = await pool.query(
|
|
'SELECT id, student_id, nis, nama, semester, file_name, file_size, uploaded_at, updated_at FROM student_legers WHERE student_id = ? ORDER BY semester ASC',
|
|
[studentId]
|
|
);
|
|
res.json(rows);
|
|
} catch (error) {
|
|
console.error('Error fetching legers:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil data leger' });
|
|
}
|
|
});
|
|
|
|
// Get single leger with file data (for download/view)
|
|
app.get('/api/students/:id/legers/:semester', async (req, res) => {
|
|
try {
|
|
const { id, semester } = req.params;
|
|
const [rows] = await pool.query(
|
|
'SELECT * FROM student_legers WHERE student_id = ? AND semester = ?',
|
|
[id, semester]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ success: false, message: 'Leger tidak ditemukan' });
|
|
}
|
|
|
|
res.json(rows[0]);
|
|
} catch (error) {
|
|
console.error('Error fetching leger:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil data leger' });
|
|
}
|
|
});
|
|
|
|
// Upload/Update leger
|
|
app.post('/api/students/:id/legers', async (req, res) => {
|
|
try {
|
|
const studentId = req.params.id;
|
|
const { semester, fileName, fileData, fileSize, nis, nama } = req.body;
|
|
|
|
if (!semester || !fileName || !fileData) {
|
|
return res.status(400).json({ success: false, message: 'Data tidak lengkap' });
|
|
}
|
|
|
|
// Check if leger already exists for this student and semester
|
|
const [existing] = await pool.query(
|
|
'SELECT id FROM student_legers WHERE student_id = ? AND semester = ?',
|
|
[studentId, semester]
|
|
);
|
|
|
|
if (existing.length > 0) {
|
|
// Update existing
|
|
await pool.query(
|
|
'UPDATE student_legers SET file_name = ?, file_data = ?, file_size = ?, nis = ?, nama = ?, updated_at = NOW() WHERE student_id = ? AND semester = ?',
|
|
[fileName, fileData, fileSize, nis, nama, studentId, semester]
|
|
);
|
|
res.json({ success: true, message: `Leger semester ${semester} berhasil diperbarui` });
|
|
} else {
|
|
// Insert new
|
|
await pool.query(
|
|
'INSERT INTO student_legers (student_id, nis, nama, semester, file_name, file_data, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
[studentId, nis, nama, semester, fileName, fileData, fileSize]
|
|
);
|
|
res.json({ success: true, message: `Leger semester ${semester} berhasil diupload` });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error uploading leger:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengupload leger: ' + error.message });
|
|
}
|
|
});
|
|
|
|
// Delete leger
|
|
app.delete('/api/students/:id/legers/:semester', async (req, res) => {
|
|
try {
|
|
const { id, semester } = req.params;
|
|
const [result] = await pool.query(
|
|
'DELETE FROM student_legers WHERE student_id = ? AND semester = ?',
|
|
[id, semester]
|
|
);
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ success: false, message: 'Leger tidak ditemukan' });
|
|
}
|
|
|
|
res.json({ success: true, message: `Leger semester ${semester} berhasil dihapus` });
|
|
} catch (error) {
|
|
console.error('Error deleting leger:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal menghapus leger' });
|
|
}
|
|
});
|
|
|
|
// Export all legers for a student as JSON (for backup)
|
|
app.get('/api/students/:id/legers/export/all', async (req, res) => {
|
|
try {
|
|
const studentId = req.params.id;
|
|
const [rows] = await pool.query(
|
|
'SELECT * FROM student_legers WHERE student_id = ? ORDER BY semester ASC',
|
|
[studentId]
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
studentId,
|
|
legers: rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error exporting legers:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengekspor leger' });
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// ==================== DOCUMENTS API ====================
|
|
|
|
// Get all documents for a student (optionally filter by type)
|
|
app.get('/api/students/:id/documents', async (req, res) => {
|
|
try {
|
|
const studentId = req.params.id;
|
|
const docType = req.query.type;
|
|
|
|
let query = 'SELECT id, student_id, nis, nama, doc_type, doc_name, file_name, file_size, uploaded_at, updated_at FROM student_documents WHERE student_id = ?';
|
|
const params = [studentId];
|
|
|
|
if (docType) {
|
|
query += ' AND doc_type = ?';
|
|
params.push(docType);
|
|
}
|
|
|
|
query += ' ORDER BY uploaded_at DESC';
|
|
|
|
const [rows] = await pool.query(query, params);
|
|
res.json(rows);
|
|
} catch (error) {
|
|
console.error('Error fetching documents:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil data dokumen' });
|
|
}
|
|
});
|
|
|
|
// Get single document with file data
|
|
app.get('/api/students/:id/documents/:docId', async (req, res) => {
|
|
try {
|
|
const { id, docId } = req.params;
|
|
const [rows] = await pool.query(
|
|
'SELECT * FROM student_documents WHERE student_id = ? AND id = ?',
|
|
[id, docId]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ success: false, message: 'Dokumen tidak ditemukan' });
|
|
}
|
|
|
|
res.json(rows[0]);
|
|
} catch (error) {
|
|
console.error('Error fetching document:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil data dokumen' });
|
|
}
|
|
});
|
|
|
|
// Upload document
|
|
app.post('/api/students/:id/documents', async (req, res) => {
|
|
try {
|
|
const studentId = req.params.id;
|
|
const { docType, docName, fileName, fileData, fileSize, nis, nama } = req.body;
|
|
|
|
if (!docType || !fileName || !fileData) {
|
|
return res.status(400).json({ success: false, message: 'Data tidak lengkap' });
|
|
}
|
|
|
|
// For ijazah, check if one already exists (only allow 1 ijazah per student)
|
|
if (docType === 'ijazah') {
|
|
const [existing] = await pool.query(
|
|
'SELECT id FROM student_documents WHERE student_id = ? AND doc_type = ?',
|
|
[studentId, 'ijazah']
|
|
);
|
|
|
|
if (existing.length > 0) {
|
|
// Update existing ijazah
|
|
await pool.query(
|
|
'UPDATE student_documents SET doc_name = ?, file_name = ?, file_data = ?, file_size = ?, nis = ?, nama = ?, updated_at = NOW() WHERE student_id = ? AND doc_type = ?',
|
|
[docName || 'Ijazah', fileName, fileData, fileSize, nis, nama, studentId, 'ijazah']
|
|
);
|
|
return res.json({ success: true, message: 'Ijazah berhasil diperbarui' });
|
|
}
|
|
}
|
|
|
|
// Insert new document
|
|
await pool.query(
|
|
'INSERT INTO student_documents (student_id, nis, nama, doc_type, doc_name, file_name, file_data, file_size) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[studentId, nis, nama, docType, docName || (docType === 'ijazah' ? 'Ijazah' : 'Sertifikat'), fileName, fileData, fileSize]
|
|
);
|
|
|
|
const docLabel = docType === 'ijazah' ? 'Ijazah' : 'Sertifikat';
|
|
res.json({ success: true, message: `${docLabel} berhasil diupload` });
|
|
} catch (error) {
|
|
console.error('Error uploading document:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengupload dokumen: ' + error.message });
|
|
}
|
|
});
|
|
|
|
// Delete document
|
|
app.delete('/api/students/:id/documents/:docId', async (req, res) => {
|
|
try {
|
|
const { id, docId } = req.params;
|
|
const [result] = await pool.query(
|
|
'DELETE FROM student_documents WHERE student_id = ? AND id = ?',
|
|
[id, docId]
|
|
);
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ success: false, message: 'Dokumen tidak ditemukan' });
|
|
}
|
|
|
|
res.json({ success: true, message: 'Dokumen berhasil dihapus' });
|
|
} catch (error) {
|
|
console.error('Error deleting document:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal menghapus dokumen' });
|
|
}
|
|
});
|
|
|
|
// ==================== PUBLIC STUDENT ACCESS API ====================
|
|
|
|
// Search student by name and birth date (Public)
|
|
app.post('/api/public/find-student', async (req, res) => {
|
|
try {
|
|
const { nama, tanggalLahir } = req.body;
|
|
|
|
if (!nama || !tanggalLahir) {
|
|
return res.status(400).json({ success: false, message: 'Nama dan Tanggal Lahir harus diisi' });
|
|
}
|
|
|
|
console.log(`Searching for: Name="${nama}", DOB="${tanggalLahir}"`);
|
|
|
|
// Helper to format date from YYYY-MM-DD to DD/MM/YYYY
|
|
const formatDateDMY = (dateStr) => {
|
|
if (!dateStr) return '';
|
|
const parts = dateStr.split('-');
|
|
if (parts.length === 3) {
|
|
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
|
}
|
|
return dateStr;
|
|
};
|
|
|
|
// Helper to format date from YYYY-MM-DD to DD-MM-YYYY
|
|
const formatDateDMYHyphen = (dateStr) => {
|
|
if (!dateStr) return '';
|
|
const parts = dateStr.split('-');
|
|
if (parts.length === 3) {
|
|
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
|
}
|
|
return dateStr;
|
|
};
|
|
|
|
const tanggalLahirDMY = formatDateDMY(tanggalLahir);
|
|
const tanggalLahirDMYHyphen = formatDateDMYHyphen(tanggalLahir);
|
|
|
|
// Try multiple date formats
|
|
// Robust search: Remove all spaces for name comparison to handle typo/spacing issues
|
|
// AND try multiple date formats
|
|
let query = `
|
|
SELECT id, nis, nama, tahun_ajaran, tanggal_lahir
|
|
FROM students
|
|
WHERE
|
|
LOWER(REPLACE(nama, ' ', '')) = LOWER(REPLACE(?, ' ', ''))
|
|
AND (
|
|
tanggal_lahir = ?
|
|
OR tanggal_lahir = ?
|
|
OR tanggal_lahir = ?
|
|
)
|
|
`;
|
|
let params = [nama, tanggalLahir, tanggalLahirDMY, tanggalLahirDMYHyphen];
|
|
|
|
const [rows] = await pool.query(query, params);
|
|
|
|
if (rows.length === 0) {
|
|
console.log('Search returned no results');
|
|
|
|
// Check for partial matches for debugging
|
|
// Name match (ignoring spaces)
|
|
const [nameCheck] = await pool.query(
|
|
"SELECT count(*) as count FROM students WHERE LOWER(REPLACE(nama, ' ', '')) = LOWER(REPLACE(?, ' ', ''))",
|
|
[nama]
|
|
);
|
|
|
|
// Date match (any format)
|
|
const [dateCheck] = await pool.query(
|
|
"SELECT count(*) as count FROM students WHERE tanggal_lahir = ? OR tanggal_lahir = ? OR tanggal_lahir = ?",
|
|
[tanggalLahir, tanggalLahirDMY, tanggalLahirDMYHyphen]
|
|
);
|
|
|
|
const debugInfo = {
|
|
nameMatches: nameCheck[0].count,
|
|
dateMatches: dateCheck[0].count,
|
|
searchedName: nama,
|
|
searchedDate: tanggalLahir,
|
|
searchedDateDMY: tanggalLahirDMY,
|
|
searchedDateDMYHyphen: tanggalLahirDMYHyphen
|
|
};
|
|
|
|
console.log('Debug info:', debugInfo);
|
|
|
|
let message = 'Data tidak ditemukan.';
|
|
if (nameCheck[0].count > 0 && dateCheck[0].count === 0) {
|
|
message = 'Nama siswa ditemukan, tetapi tanggal lahir tidak cocok. Pastikan tanggal lahir sesuai data sekolah.';
|
|
} else if (nameCheck[0].count === 0 && dateCheck[0].count > 0) {
|
|
message = 'Tanggal lahir valid, tetapi nama tidak ditemukan. Periksa penulisan nama (sesuai Ijazah/Akta).';
|
|
} else {
|
|
message = 'Data siswa tidak ditemukan. Periksa Nama dan Tanggal Lahir.';
|
|
}
|
|
|
|
return res.status(404).json({
|
|
success: false,
|
|
message,
|
|
debug: debugInfo
|
|
});
|
|
}
|
|
|
|
// Jika ditemukan, kembalikan data minimal (NIS & ID) untuk akses selanjutnya
|
|
res.json({
|
|
success: true,
|
|
student: convertKeysToCamel(rows[0])
|
|
});
|
|
} catch (error) {
|
|
console.error('Public search error:', error);
|
|
res.status(500).json({ success: false, message: 'Terjadi kesalahan pada server' });
|
|
}
|
|
});
|
|
|
|
// Health Check
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
env: process.env.NODE_ENV,
|
|
db_name: process.env.DB_NAME
|
|
});
|
|
});
|
|
|
|
// Get student detail for public view (requires student ID)
|
|
app.get('/api/public/students/:id', async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM students WHERE id = ?', [req.params.id]);
|
|
if (rows.length === 0) return res.status(404).json({ message: 'Siswa tidak ditemukan' });
|
|
res.json(convertKeysToCamel(rows[0]));
|
|
} catch (error) {
|
|
res.status(500).json({ message: error.message });
|
|
}
|
|
});
|
|
|
|
// ==================== ACADEMIC YEARS API ====================
|
|
|
|
// Get all years
|
|
app.get('/api/years', async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM academic_years ORDER BY year_name DESC');
|
|
res.json(rows.map(convertKeysToCamel));
|
|
} catch (error) {
|
|
console.error('Error fetching years:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil data tahun ajaran' });
|
|
}
|
|
});
|
|
|
|
// Add year
|
|
app.post('/api/years', async (req, res) => {
|
|
try {
|
|
const { yearName } = req.body;
|
|
await pool.query('INSERT INTO academic_years (year_name) VALUES (?)', [yearName]);
|
|
res.json({ success: true, message: 'Tahun ajaran berhasil ditambah' });
|
|
} catch (error) {
|
|
console.error('Error adding year:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal menambah tahun ajaran' });
|
|
}
|
|
});
|
|
|
|
// Set active year
|
|
app.post('/api/years/:id/active', async (req, res) => {
|
|
try {
|
|
const yearId = req.params.id;
|
|
// Set all to false
|
|
await pool.query('UPDATE academic_years SET is_active = FALSE');
|
|
// Set target to true
|
|
await pool.query('UPDATE academic_years SET is_active = TRUE WHERE id = ?', [yearId]);
|
|
|
|
// Also sync to global settings for backward compatibility/quick access
|
|
const [yearRows] = await pool.query('SELECT year_name FROM academic_years WHERE id = ?', [yearId]);
|
|
if (yearRows.length > 0) {
|
|
const newYear = yearRows[0].year_name;
|
|
const [settingsRows] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
|
|
if (settingsRows.length > 0) {
|
|
const currentSettings = JSON.parse(settingsRows[0].setting_value);
|
|
currentSettings.tahunAjaran = newYear;
|
|
await pool.query('UPDATE settings SET setting_value = ? WHERE setting_key = ?',
|
|
[JSON.stringify(currentSettings), 'app_settings']);
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, message: 'Tahun ajaran aktif berhasil diubah' });
|
|
} catch (error) {
|
|
console.error('Error setting active year:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengubah tahun ajaran aktif' });
|
|
}
|
|
});
|
|
|
|
// Delete year
|
|
app.delete('/api/years/:id', async (req, res) => {
|
|
try {
|
|
const yearId = req.params.id;
|
|
const [year] = await pool.query('SELECT is_active FROM academic_years WHERE id = ?', [yearId]);
|
|
|
|
if (year.length > 0 && year[0].is_active) {
|
|
return res.status(400).json({ success: false, message: 'Tidak dapat menghapus tahun ajaran yang sedang aktif' });
|
|
}
|
|
|
|
await pool.query('DELETE FROM academic_years WHERE id = ?', [yearId]);
|
|
res.json({ success: true, message: 'Tahun ajaran berhasil dihapus' });
|
|
} catch (error) {
|
|
console.error('Error deleting year:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal menghapus tahun ajaran' });
|
|
}
|
|
});
|
|
|
|
// ==================== USER MANAGEMENT API ====================
|
|
|
|
// Get all users
|
|
app.get('/api/users', async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT id, username, full_name, role, created_at FROM users ORDER BY full_name ASC');
|
|
res.json(rows.map(convertKeysToCamel));
|
|
} catch (error) {
|
|
console.error('Error fetching users:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal mengambil data pengguna' });
|
|
}
|
|
});
|
|
|
|
// Add new user
|
|
app.post('/api/users', async (req, res) => {
|
|
try {
|
|
const { username, password, fullName, role } = req.body;
|
|
|
|
if (!username || !password || !fullName || !role) {
|
|
return res.status(400).json({ success: false, message: 'Semua data harus diisi' });
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
|
|
await pool.query(
|
|
'INSERT INTO users (username, password, full_name, role) VALUES (?, ?, ?, ?)',
|
|
[username, hashedPassword, fullName, role]
|
|
);
|
|
|
|
res.json({ success: true, message: 'Pengguna berhasil ditambahkan' });
|
|
} catch (error) {
|
|
console.error('Error adding user:', error);
|
|
if (error.code === 'ER_DUP_ENTRY') {
|
|
res.status(400).json({ success: false, message: 'Username sudah digunakan' });
|
|
} else {
|
|
res.status(500).json({ success: false, message: 'Gagal menambah pengguna' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update user
|
|
app.put('/api/users/:id', async (req, res) => {
|
|
try {
|
|
const userId = req.params.id;
|
|
const { username, password, fullName, role } = req.body;
|
|
|
|
if (!username || !fullName || !role) {
|
|
return res.status(400).json({ success: false, message: 'Data nama, username dan role harus diisi' });
|
|
}
|
|
|
|
// Check if username exists for OTHER users
|
|
const [existing] = await pool.query('SELECT id FROM users WHERE username = ? AND id != ?', [username, userId]);
|
|
if (existing.length > 0) {
|
|
return res.status(400).json({ success: false, message: 'Username sudah digunakan oleh pengguna lain' });
|
|
}
|
|
|
|
if (password && password.trim() !== '') {
|
|
// Update with password
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
await pool.query(
|
|
'UPDATE users SET username = ?, password = ?, full_name = ?, role = ? WHERE id = ?',
|
|
[username, hashedPassword, fullName, role, userId]
|
|
);
|
|
} else {
|
|
// Update without password
|
|
await pool.query(
|
|
'UPDATE users SET username = ?, full_name = ?, role = ? WHERE id = ?',
|
|
[username, fullName, role, userId]
|
|
);
|
|
}
|
|
|
|
res.json({ success: true, message: 'Data pengguna berhasil diperbarui' });
|
|
} catch (error) {
|
|
console.error('Error updating user:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal memperbarui pengguna' });
|
|
}
|
|
});
|
|
|
|
// Delete user
|
|
app.delete('/api/users/:id', async (req, res) => {
|
|
try {
|
|
const userId = req.params.id;
|
|
|
|
// Prevent deleting the last admin or the currently logged in user (would need a better check but for now simpler)
|
|
const [userRows] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
|
|
if (userRows.length > 0 && userRows[0].username === 'Kesiswaan') {
|
|
return res.status(400).json({ success: false, message: 'Account utama tidak dapat dihapus' });
|
|
}
|
|
|
|
await pool.query('DELETE FROM users WHERE id = ?', [userId]);
|
|
res.json({ success: true, message: 'Pengguna berhasil dihapus' });
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
res.status(500).json({ success: false, message: 'Gagal menghapus pengguna' });
|
|
}
|
|
});
|
|
|
|
// ==================== STATIC FILES ====================
|
|
|
|
// Serve static files from the dist directory
|
|
app.use(express.static(path.join(__dirname, 'dist')));
|
|
|
|
// Handle SPA routing
|
|
app.get('*', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
|
});
|
|
|
|
// ==================== START SERVER ====================
|
|
|
|
const startServer = async () => {
|
|
try {
|
|
// Initialize database
|
|
await initDatabase();
|
|
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`\n🚀 Server is running on http://localhost:${PORT}`);
|
|
console.log(`📦 Database: ${process.env.DB_NAME || 'db_bukuiduk'}`);
|
|
console.log(`🌐 Environment: Node.js ${process.version}`);
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Failed to start server:', error);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
startServer(); |