Initial commit apps directory with .gitignore
This commit is contained in:
153
Sistem-Pelanggaran-Siswa/routes/achievement-criteria.js
Normal file
153
Sistem-Pelanggaran-Siswa/routes/achievement-criteria.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import express from 'express';
|
||||
import pool from '../database.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET all achievement criteria
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, level, achievement_type, participant_type, group_size, score
|
||||
FROM achievement_criteria
|
||||
ORDER BY level, achievement_type, participant_type
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching achievement criteria:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch achievement criteria' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST create new achievement criteria
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { level, achievementType, participantType, groupSize, score } = req.body;
|
||||
|
||||
// Check if combination already exists
|
||||
const [existing] = await pool.execute(`
|
||||
SELECT id FROM achievement_criteria
|
||||
WHERE level = ? AND achievement_type = ? AND participant_type = ? AND group_size = ?
|
||||
`, [level, achievementType, participantType, groupSize || null]);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return res.status(400).json({
|
||||
status: 'error',
|
||||
error: 'Kombinasi kriteria ini sudah ada'
|
||||
});
|
||||
}
|
||||
|
||||
const [result] = await pool.execute(`
|
||||
INSERT INTO achievement_criteria (level, achievement_type, participant_type, group_size, score)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [level, achievementType, participantType, groupSize || null, score]);
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
id: result.insertId,
|
||||
message: 'Kriteria prestasi berhasil ditambahkan'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating achievement criteria:', error);
|
||||
res.status(500).json({ status: 'error', error: 'Failed to create achievement criteria' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update achievement criteria
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { level, achievementType, participantType, groupSize, score } = req.body;
|
||||
const { id } = req.params;
|
||||
|
||||
// Check if combination already exists for other entries
|
||||
const [existing] = await pool.execute(`
|
||||
SELECT id FROM achievement_criteria
|
||||
WHERE level = ? AND achievement_type = ? AND participant_type = ? AND group_size = ? AND id != ?
|
||||
`, [level, achievementType, participantType, groupSize || null, id]);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return res.status(400).json({
|
||||
status: 'error',
|
||||
error: 'Kombinasi kriteria ini sudah ada'
|
||||
});
|
||||
}
|
||||
|
||||
await pool.execute(`
|
||||
UPDATE achievement_criteria
|
||||
SET level = ?, achievement_type = ?, participant_type = ?, group_size = ?, score = ?
|
||||
WHERE id = ?
|
||||
`, [level, achievementType, participantType, groupSize || null, score, id]);
|
||||
|
||||
res.json({ status: 'success', message: 'Kriteria prestasi berhasil diperbarui' });
|
||||
} catch (error) {
|
||||
console.error('Error updating achievement criteria:', error);
|
||||
res.status(500).json({ status: 'error', error: 'Failed to update achievement criteria' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE achievement criteria
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.execute('DELETE FROM achievement_criteria WHERE id = ?', [req.params.id]);
|
||||
res.json({ status: 'success', message: 'Kriteria prestasi berhasil dihapus' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting achievement criteria:', error);
|
||||
res.status(500).json({ status: 'error', error: 'Failed to delete achievement criteria' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST import bulk achievement criteria
|
||||
router.post('/import', async (req, res) => {
|
||||
try {
|
||||
const { criteria } = req.body;
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const item of criteria) {
|
||||
try {
|
||||
const { level, achievementType, participantType, groupSize, score } = item;
|
||||
|
||||
// Insert or update if combination exists
|
||||
await pool.execute(`
|
||||
INSERT INTO achievement_criteria (level, achievement_type, participant_type, group_size, score)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE score = VALUES(score)
|
||||
`, [level, achievementType, participantType, groupSize || null, score]);
|
||||
success++;
|
||||
} catch (e) {
|
||||
failed++;
|
||||
errors.push(`${item.level} - ${item.achievementType}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ status: 'success', success, failed, errors });
|
||||
} catch (error) {
|
||||
console.error('Error importing achievement criteria:', error);
|
||||
res.status(500).json({ status: 'error', error: 'Failed to import achievement criteria' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET score by criteria (for use in achievement form)
|
||||
router.get('/score', async (req, res) => {
|
||||
try {
|
||||
const { level, achievementType, participantType, groupSize } = req.query;
|
||||
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT score FROM achievement_criteria
|
||||
WHERE level = ? AND achievement_type = ? AND participant_type = ? AND (group_size = ? OR group_size IS NULL)
|
||||
ORDER BY group_size DESC
|
||||
LIMIT 1
|
||||
`, [level, achievementType, participantType, groupSize || null]);
|
||||
|
||||
if (rows.length > 0) {
|
||||
res.json({ score: rows[0].score });
|
||||
} else {
|
||||
res.json({ score: null, message: 'Kriteria tidak ditemukan' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching score:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch score' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
65
Sistem-Pelanggaran-Siswa/routes/achievements.js
Normal file
65
Sistem-Pelanggaran-Siswa/routes/achievements.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import express from 'express';
|
||||
import pool from '../database.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET all achievements
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, student_id as studentId, title, level, date, score_reduction as scoreReduction
|
||||
FROM achievements
|
||||
ORDER BY date DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching achievements:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch achievements' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET achievements by student
|
||||
router.get('/student/:studentId', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, student_id as studentId, title, level, date, score_reduction as scoreReduction
|
||||
FROM achievements
|
||||
WHERE student_id = ?
|
||||
ORDER BY date DESC
|
||||
`, [req.params.studentId]);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching student achievements:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch achievements' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST create achievement
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { id, studentId, title, level, date, scoreReduction } = req.body;
|
||||
|
||||
await pool.execute(`
|
||||
INSERT INTO achievements (id, student_id, title, level, date, score_reduction)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, [id || Date.now().toString(), studentId, title, level, date || new Date().toISOString().split('T')[0], scoreReduction]);
|
||||
|
||||
res.status(201).json({ status: 'success', message: 'Achievement recorded' });
|
||||
} catch (error) {
|
||||
console.error('Error creating achievement:', error);
|
||||
res.status(500).json({ error: 'Failed to create achievement' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE achievement
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.execute('DELETE FROM achievements WHERE id = ?', [req.params.id]);
|
||||
res.json({ status: 'success', message: 'Achievement deleted' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting achievement:', error);
|
||||
res.status(500).json({ error: 'Failed to delete achievement' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
80
Sistem-Pelanggaran-Siswa/routes/settings.js
Normal file
80
Sistem-Pelanggaran-Siswa/routes/settings.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import express from 'express';
|
||||
import pool from '../database.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET all settings
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute('SELECT setting_key, setting_value FROM settings');
|
||||
|
||||
// Convert array to object
|
||||
const settings = {};
|
||||
rows.forEach(row => {
|
||||
settings[row.setting_key] = row.setting_value;
|
||||
});
|
||||
|
||||
res.json(settings);
|
||||
} catch (error) {
|
||||
console.error('Error fetching settings:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch settings' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET single setting
|
||||
router.get('/:key', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT setting_value FROM settings WHERE setting_key = ?',
|
||||
[req.params.key]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Setting not found' });
|
||||
}
|
||||
res.json({ key: req.params.key, value: rows[0].setting_value });
|
||||
} catch (error) {
|
||||
console.error('Error fetching setting:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch setting' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update settings (bulk)
|
||||
router.put('/', async (req, res) => {
|
||||
try {
|
||||
const settings = req.body;
|
||||
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
await pool.execute(`
|
||||
INSERT INTO settings (setting_key, setting_value)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE setting_value = ?
|
||||
`, [key, value, value]);
|
||||
}
|
||||
|
||||
res.json({ status: 'success', message: 'Settings updated' });
|
||||
} catch (error) {
|
||||
console.error('Error updating settings:', error);
|
||||
res.status(500).json({ error: 'Failed to update settings' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update single setting
|
||||
router.put('/:key', async (req, res) => {
|
||||
try {
|
||||
const { value } = req.body;
|
||||
|
||||
await pool.execute(`
|
||||
INSERT INTO settings (setting_key, setting_value)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE setting_value = ?
|
||||
`, [req.params.key, value, value]);
|
||||
|
||||
res.json({ status: 'success', message: 'Setting updated' });
|
||||
} catch (error) {
|
||||
console.error('Error updating setting:', error);
|
||||
res.status(500).json({ error: 'Failed to update setting' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
312
Sistem-Pelanggaran-Siswa/routes/students.js
Normal file
312
Sistem-Pelanggaran-Siswa/routes/students.js
Normal file
@@ -0,0 +1,312 @@
|
||||
import express from 'express';
|
||||
import pool from '../database.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET all students
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, name, class, parent_name as parentName, parent_phone as parentPhone, shift, face_descriptor as faceDescriptor
|
||||
FROM students
|
||||
ORDER BY class, name
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching students:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch students' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET single student
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, name, class, parent_name as parentName, parent_phone as parentPhone, shift, face_descriptor as faceDescriptor
|
||||
FROM students
|
||||
WHERE id = ?
|
||||
`, [req.params.id]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Student not found' });
|
||||
}
|
||||
res.json(rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching student:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch student' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST create student
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { id, name, class: studentClass, parentName, parentPhone, shift } = req.body;
|
||||
|
||||
await pool.execute(`
|
||||
INSERT INTO students (id, name, class, parent_name, parent_phone, shift)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, [id, name, studentClass, parentName, parentPhone, shift || 'Siang']);
|
||||
|
||||
res.status(201).json({ status: 'success', message: 'Student created' });
|
||||
} catch (error) {
|
||||
console.error('Error creating student:', error);
|
||||
res.status(500).json({ error: 'Failed to create student' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update student
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { name, class: studentClass, parentName, parentPhone, shift, faceDescriptor } = req.body;
|
||||
|
||||
await pool.execute(`
|
||||
UPDATE students
|
||||
SET name = ?, class = ?, parent_name = ?, parent_phone = ?, shift = ?, face_descriptor = ?
|
||||
WHERE id = ?
|
||||
`, [name, studentClass, parentName, parentPhone, shift, faceDescriptor, req.params.id]);
|
||||
|
||||
res.json({ status: 'success', message: 'Student updated' });
|
||||
} catch (error) {
|
||||
console.error('Error updating student:', error);
|
||||
res.status(500).json({ error: 'Failed to update student' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE student
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.execute('DELETE FROM students WHERE id = ?', [req.params.id]);
|
||||
res.json({ status: 'success', message: 'Student deleted' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting student:', error);
|
||||
res.status(500).json({ error: 'Failed to delete student' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET class statistics for promotion
|
||||
router.get('/stats/classes', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT class, COUNT(*) as count
|
||||
FROM students
|
||||
GROUP BY class
|
||||
ORDER BY class
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching class stats:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch class stats' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST promote students to next class
|
||||
router.post('/promote', async (req, res) => {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
// Get promotion mapping: X -> XI, XI -> XII, XII -> LULUS (delete)
|
||||
const promotionRules = [
|
||||
{ from: 'X', to: 'XI' },
|
||||
{ from: 'XI', to: 'XII' },
|
||||
];
|
||||
|
||||
let promoted = 0;
|
||||
let graduated = 0;
|
||||
|
||||
// First, handle graduates (XII -> Lulus/Delete)
|
||||
// Count XII students first
|
||||
const [xiiStudents] = await connection.execute(
|
||||
"SELECT COUNT(*) as count FROM students WHERE class LIKE 'XII%'"
|
||||
);
|
||||
graduated = xiiStudents[0].count;
|
||||
|
||||
// Archive XII students before deletion (optional - can create alumni table later)
|
||||
// For now, just delete them
|
||||
await connection.execute("DELETE FROM students WHERE class LIKE 'XII%'");
|
||||
|
||||
// Promote XI to XII
|
||||
const [xiResult] = await connection.execute(`
|
||||
UPDATE students
|
||||
SET class = REPLACE(class, 'XI-', 'XII-')
|
||||
WHERE class LIKE 'XI-%'
|
||||
`);
|
||||
|
||||
// Also handle XI without dash
|
||||
await connection.execute(`
|
||||
UPDATE students
|
||||
SET class = REPLACE(class, 'XI', 'XII')
|
||||
WHERE class = 'XI' OR (class LIKE 'XI%' AND class NOT LIKE 'XII%')
|
||||
`);
|
||||
|
||||
// Promote X to XI
|
||||
const [xResult] = await connection.execute(`
|
||||
UPDATE students
|
||||
SET class = REPLACE(class, 'X-', 'XI-')
|
||||
WHERE class LIKE 'X-%' AND class NOT LIKE 'XI%' AND class NOT LIKE 'XII%'
|
||||
`);
|
||||
|
||||
// Also handle X without dash
|
||||
await connection.execute(`
|
||||
UPDATE students
|
||||
SET class = 'XI'
|
||||
WHERE class = 'X'
|
||||
`);
|
||||
|
||||
// Count promoted students
|
||||
const [countResult] = await connection.execute('SELECT COUNT(*) as count FROM students');
|
||||
promoted = countResult[0].count;
|
||||
|
||||
await connection.commit();
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
message: 'Kenaikan kelas berhasil!',
|
||||
graduated,
|
||||
totalRemaining: promoted
|
||||
});
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
console.error('Error promoting students:', error);
|
||||
res.status(500).json({ error: 'Failed to promote students' });
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
// POST graduate specific class (manual)
|
||||
router.post('/graduate/:classPrefix', async (req, res) => {
|
||||
try {
|
||||
const { classPrefix } = req.params;
|
||||
const [result] = await pool.execute(
|
||||
'DELETE FROM students WHERE class LIKE ?',
|
||||
[`${classPrefix}%`]
|
||||
);
|
||||
res.json({
|
||||
status: 'success',
|
||||
message: `Siswa kelas ${classPrefix} telah diluluskan`,
|
||||
count: result.affectedRows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error graduating students:', error);
|
||||
res.status(500).json({ error: 'Failed to graduate students' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST promote individual students with mapping
|
||||
router.post('/promote-mapping', async (req, res) => {
|
||||
const { mappings } = req.body; // Array of { studentId, newClass }
|
||||
|
||||
if (!mappings || !Array.isArray(mappings) || mappings.length === 0) {
|
||||
return res.status(400).json({ error: 'Data mapping diperlukan' });
|
||||
}
|
||||
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
const errors = [];
|
||||
const graduatedIds = [];
|
||||
|
||||
for (const mapping of mappings) {
|
||||
const { studentId, newClass } = mapping;
|
||||
|
||||
if (!studentId) {
|
||||
failed++;
|
||||
errors.push(`ID siswa tidak valid`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (newClass === 'LULUS' || newClass === 'lulus' || newClass === '') {
|
||||
// Graduate this student (delete)
|
||||
await connection.execute(
|
||||
'DELETE FROM students WHERE id = ?',
|
||||
[studentId]
|
||||
);
|
||||
graduatedIds.push(studentId);
|
||||
success++;
|
||||
} else {
|
||||
// Update class
|
||||
const [result] = await connection.execute(
|
||||
'UPDATE students SET class = ? WHERE id = ?',
|
||||
[newClass, studentId]
|
||||
);
|
||||
|
||||
if (result.affectedRows > 0) {
|
||||
success++;
|
||||
} else {
|
||||
failed++;
|
||||
errors.push(`Siswa ${studentId} tidak ditemukan`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
failed++;
|
||||
errors.push(`Error untuk siswa ${studentId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await connection.commit();
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
message: `Proses selesai: ${success} berhasil, ${failed} gagal`,
|
||||
success,
|
||||
failed,
|
||||
graduated: graduatedIds.length,
|
||||
errors: errors.slice(0, 10) // Limit error messages
|
||||
});
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
console.error('Error in promote-mapping:', error);
|
||||
res.status(500).json({ error: 'Gagal memproses kenaikan kelas' });
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
});
|
||||
|
||||
// GET available classes for dropdown
|
||||
router.get('/available-classes', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT DISTINCT class
|
||||
FROM students
|
||||
ORDER BY class
|
||||
`);
|
||||
|
||||
// Generate next level classes
|
||||
const currentClasses = rows.map(r => r.class);
|
||||
const allClasses = new Set(currentClasses);
|
||||
|
||||
// Add common class patterns for next level
|
||||
currentClasses.forEach(c => {
|
||||
// Extract level and suffix (e.g., "X-A" -> "X", "A")
|
||||
const match = c.match(/^(X|XI|XII)-?(.*)$/i);
|
||||
if (match) {
|
||||
const level = match[1].toUpperCase();
|
||||
const suffix = match[2];
|
||||
|
||||
if (level === 'X') {
|
||||
allClasses.add(`XI-${suffix}`);
|
||||
allClasses.add(`XI${suffix}`);
|
||||
} else if (level === 'XI') {
|
||||
allClasses.add(`XII-${suffix}`);
|
||||
allClasses.add(`XII${suffix}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sort and return unique classes
|
||||
const sortedClasses = Array.from(allClasses).sort();
|
||||
res.json(sortedClasses);
|
||||
} catch (error) {
|
||||
console.error('Error fetching available classes:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch classes' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
160
Sistem-Pelanggaran-Siswa/routes/users.js
Normal file
160
Sistem-Pelanggaran-Siswa/routes/users.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import express from 'express';
|
||||
import pool from '../database.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET all users (without passwords)
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, username, name, role, permissions, is_active as isActive, created_at as createdAt
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
// Parse permissions JSON
|
||||
const users = rows.map(user => ({
|
||||
...user,
|
||||
permissions: user.permissions ? JSON.parse(user.permissions) : []
|
||||
}));
|
||||
|
||||
res.json(users);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch users' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET single user
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, username, name, role, permissions, is_active as isActive
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
`, [req.params.id]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const user = {
|
||||
...rows[0],
|
||||
permissions: rows[0].permissions ? JSON.parse(rows[0].permissions) : []
|
||||
};
|
||||
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch user' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST create user
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { username, password, name, role, permissions } = req.body;
|
||||
|
||||
// Check if username already exists
|
||||
const [existing] = await pool.execute('SELECT id FROM users WHERE username = ?', [username]);
|
||||
if (existing.length > 0) {
|
||||
return res.status(400).json({ error: 'Username sudah digunakan' });
|
||||
}
|
||||
|
||||
const permissionsJson = JSON.stringify(permissions || []);
|
||||
|
||||
await pool.execute(`
|
||||
INSERT INTO users (username, password, name, role, permissions)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [username, password, name, role || 'staff', permissionsJson]);
|
||||
|
||||
res.status(201).json({ status: 'success', message: 'User created' });
|
||||
} catch (error) {
|
||||
console.error('Error creating user:', error);
|
||||
res.status(500).json({ error: 'Failed to create user' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update user
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { username, password, name, role, permissions, isActive } = req.body;
|
||||
const permissionsJson = JSON.stringify(permissions || []);
|
||||
|
||||
// Check if username already exists (for another user)
|
||||
const [existing] = await pool.execute(
|
||||
'SELECT id FROM users WHERE username = ? AND id != ?',
|
||||
[username, req.params.id]
|
||||
);
|
||||
if (existing.length > 0) {
|
||||
return res.status(400).json({ error: 'Username sudah digunakan oleh user lain' });
|
||||
}
|
||||
|
||||
if (password && password.trim() !== '') {
|
||||
await pool.execute(`
|
||||
UPDATE users
|
||||
SET username = ?, password = ?, name = ?, role = ?, permissions = ?, is_active = ?
|
||||
WHERE id = ?
|
||||
`, [username, password, name, role, permissionsJson, isActive !== false, req.params.id]);
|
||||
} else {
|
||||
await pool.execute(`
|
||||
UPDATE users
|
||||
SET username = ?, name = ?, role = ?, permissions = ?, is_active = ?
|
||||
WHERE id = ?
|
||||
`, [username, name, role, permissionsJson, isActive !== false, req.params.id]);
|
||||
}
|
||||
|
||||
res.json({ status: 'success', message: 'User updated' });
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
res.status(500).json({ error: 'Failed to update user' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE user
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
// Prevent deleting the last admin
|
||||
const [admins] = await pool.execute("SELECT id FROM users WHERE role = 'admin' AND is_active = TRUE");
|
||||
const [userToDelete] = await pool.execute('SELECT role FROM users WHERE id = ?', [req.params.id]);
|
||||
|
||||
if (userToDelete.length > 0 && userToDelete[0].role === 'admin' && admins.length <= 1) {
|
||||
return res.status(400).json({ error: 'Tidak dapat menghapus admin terakhir' });
|
||||
}
|
||||
|
||||
await pool.execute('DELETE FROM users WHERE id = ?', [req.params.id]);
|
||||
res.json({ status: 'success', message: 'User deleted' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
res.status(500).json({ error: 'Failed to delete user' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST login - verify credentials and return user with permissions
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, username, name, role, permissions, is_active as isActive
|
||||
FROM users
|
||||
WHERE username = ? AND password = ? AND is_active = TRUE
|
||||
`, [username, password]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(401).json({ error: 'Username atau password salah' });
|
||||
}
|
||||
|
||||
const user = {
|
||||
...rows[0],
|
||||
permissions: rows[0].permissions ? JSON.parse(rows[0].permissions) : []
|
||||
};
|
||||
|
||||
res.json({ status: 'success', user });
|
||||
} catch (error) {
|
||||
console.error('Error during login:', error);
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
132
Sistem-Pelanggaran-Siswa/routes/violation-rules.js
Normal file
132
Sistem-Pelanggaran-Siswa/routes/violation-rules.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import express from 'express';
|
||||
import pool from '../database.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET all violation rules
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, code, description, category, score, default_sanction
|
||||
FROM violation_rules
|
||||
ORDER BY category, score DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching violation rules:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch violation rules' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST create new violation rule
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { code, description, category, score, defaultSanction } = req.body;
|
||||
|
||||
// Check if code already exists
|
||||
const [existing] = await pool.execute(
|
||||
'SELECT id FROM violation_rules WHERE code = ?', [code]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return res.status(400).json({
|
||||
status: 'error',
|
||||
error: `Kode "${code}" sudah digunakan`
|
||||
});
|
||||
}
|
||||
|
||||
const [result] = await pool.execute(`
|
||||
INSERT INTO violation_rules (code, description, category, score, default_sanction)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [code, description, category, score, defaultSanction || null]);
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
id: result.insertId,
|
||||
message: 'Jenis pelanggaran berhasil ditambahkan'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating violation rule:', error);
|
||||
res.status(500).json({ status: 'error', error: 'Failed to create violation rule' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update violation rule
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { code, description, category, score, defaultSanction } = req.body;
|
||||
const { id } = req.params;
|
||||
|
||||
// Check if code already exists for other rules
|
||||
const [existing] = await pool.execute(
|
||||
'SELECT id FROM violation_rules WHERE code = ? AND id != ?', [code, id]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return res.status(400).json({
|
||||
status: 'error',
|
||||
error: `Kode "${code}" sudah digunakan`
|
||||
});
|
||||
}
|
||||
|
||||
await pool.execute(`
|
||||
UPDATE violation_rules
|
||||
SET code = ?, description = ?, category = ?, score = ?, default_sanction = ?
|
||||
WHERE id = ?
|
||||
`, [code, description, category, score, defaultSanction || null, id]);
|
||||
|
||||
res.json({ status: 'success', message: 'Jenis pelanggaran berhasil diperbarui' });
|
||||
} catch (error) {
|
||||
console.error('Error updating violation rule:', error);
|
||||
res.status(500).json({ status: 'error', error: 'Failed to update violation rule' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE violation rule
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.execute('DELETE FROM violation_rules WHERE id = ?', [req.params.id]);
|
||||
res.json({ status: 'success', message: 'Jenis pelanggaran berhasil dihapus' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting violation rule:', error);
|
||||
res.status(500).json({ status: 'error', error: 'Failed to delete violation rule' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST import bulk violation rules
|
||||
router.post('/import', async (req, res) => {
|
||||
try {
|
||||
const { rules } = req.body;
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const rule of rules) {
|
||||
try {
|
||||
const { code, description, category, score, defaultSanction } = rule;
|
||||
|
||||
// Insert or update if code exists
|
||||
await pool.execute(`
|
||||
INSERT INTO violation_rules (code, description, category, score, default_sanction)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
description = VALUES(description),
|
||||
category = VALUES(category),
|
||||
score = VALUES(score),
|
||||
default_sanction = VALUES(default_sanction)
|
||||
`, [code, description, category, score, defaultSanction || null]);
|
||||
success++;
|
||||
} catch (e) {
|
||||
failed++;
|
||||
errors.push(`${rule.code}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ status: 'success', success, failed, errors });
|
||||
} catch (error) {
|
||||
console.error('Error importing violation rules:', error);
|
||||
res.status(500).json({ status: 'error', error: 'Failed to import violation rules' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
85
Sistem-Pelanggaran-Siswa/routes/violations.js
Normal file
85
Sistem-Pelanggaran-Siswa/routes/violations.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import express from 'express';
|
||||
import pool from '../database.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET all violations
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, student_id as studentId, rule_id as ruleId, date, description,
|
||||
score, sanction, timestamp, photo_url as photoUrl
|
||||
FROM violations
|
||||
ORDER BY timestamp DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching violations:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch violations' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET violations by student
|
||||
router.get('/student/:studentId', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.execute(`
|
||||
SELECT id, student_id as studentId, rule_id as ruleId, date, description,
|
||||
score, sanction, timestamp, photo_url as photoUrl
|
||||
FROM violations
|
||||
WHERE student_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
`, [req.params.studentId]);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching student violations:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch violations' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST create violation
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { id, studentId, ruleId, date, description, score, sanction, timestamp, photoUrl } = req.body;
|
||||
|
||||
await pool.execute(`
|
||||
INSERT INTO violations (id, student_id, rule_id, date, description, score, sanction, timestamp, photo_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [id || Date.now().toString(), studentId, ruleId, date, description, score, sanction || '-', timestamp || Date.now(), photoUrl || null]);
|
||||
|
||||
res.status(201).json({ status: 'success', message: 'Violation recorded' });
|
||||
} catch (error) {
|
||||
console.error('Error creating violation:', error);
|
||||
res.status(500).json({ error: 'Failed to create violation' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update violation
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { score, description, sanction } = req.body;
|
||||
// We only allow updating score, description, sanction for now as per sync requirement
|
||||
await pool.execute(`
|
||||
UPDATE violations
|
||||
SET score = ?, description = ?, sanction = ?
|
||||
WHERE id = ?
|
||||
`, [score, description, sanction, req.params.id]);
|
||||
|
||||
res.json({ status: 'success', message: 'Violation updated' });
|
||||
} catch (error) {
|
||||
console.error('Error updating violation:', error);
|
||||
res.status(500).json({ error: 'Failed to update violation' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE violation
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.execute('DELETE FROM violations WHERE id = ?', [req.params.id]);
|
||||
res.json({ status: 'success', message: 'Violation deleted' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting violation:', error);
|
||||
res.status(500).json({ error: 'Failed to delete violation' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user