Initial commit apps directory with .gitignore
This commit is contained in:
795
buku-induk-sma-negeri-1-abiansemal/server.js
Normal file
795
buku-induk-sma-negeri-1-abiansemal/server.js
Normal file
@@ -0,0 +1,795 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user