Initial commit apps directory with .gitignore
129
Sistem-Pelanggaran-Siswa/.gitignore
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# OS metadata
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project specific backups
|
||||
*.tar.gz
|
||||
302
Sistem-Pelanggaran-Siswa/database.js
Normal file
@@ -0,0 +1,302 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Database connection pool
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '3306'),
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'sipasi_db',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
// Auto-create tables on startup
|
||||
export async function initDatabase() {
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
try {
|
||||
console.log('🔌 Connected to MySQL database');
|
||||
|
||||
// Create students table
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id VARCHAR(20) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
class VARCHAR(50) NOT NULL,
|
||||
parent_name VARCHAR(255),
|
||||
parent_phone VARCHAR(20),
|
||||
shift VARCHAR(20) DEFAULT 'Siang',
|
||||
face_descriptor TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "students" ready');
|
||||
|
||||
// Add columns if they don't exist (for existing databases)
|
||||
try {
|
||||
await connection.execute('ALTER TABLE students ADD COLUMN IF NOT EXISTS shift VARCHAR(20) DEFAULT "Siang"');
|
||||
await connection.execute('ALTER TABLE students ADD COLUMN IF NOT EXISTS face_descriptor TEXT');
|
||||
} catch (err) {
|
||||
// Ignore if columns already exist or IF NOT EXISTS is not supported
|
||||
}
|
||||
|
||||
// Create violations table
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS violations (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
student_id VARCHAR(20) NOT NULL,
|
||||
rule_id VARCHAR(10) NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
description TEXT,
|
||||
score INT NOT NULL DEFAULT 0,
|
||||
sanction VARCHAR(255),
|
||||
academic_year VARCHAR(20),
|
||||
timestamp BIGINT,
|
||||
photo_url TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "violations" ready');
|
||||
|
||||
try {
|
||||
await connection.execute('ALTER TABLE violations ADD COLUMN IF NOT EXISTS academic_year VARCHAR(20)');
|
||||
} catch (err) { }
|
||||
|
||||
// Create achievements table
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS achievements (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
student_id VARCHAR(20) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
level ENUM('Nasional', 'Provinsi', 'Kabupaten/Kota', 'Sekolah') NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
score_reduction INT NOT NULL DEFAULT 0,
|
||||
academic_year VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "achievements" ready');
|
||||
|
||||
try {
|
||||
await connection.execute('ALTER TABLE achievements ADD COLUMN IF NOT EXISTS academic_year VARCHAR(20)');
|
||||
} catch (err) { }
|
||||
|
||||
// Create settings table
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
setting_key VARCHAR(100) PRIMARY KEY,
|
||||
setting_value LONGTEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "settings" ready');
|
||||
|
||||
// Migration: Update setting_value to LONGTEXT for existing tables
|
||||
try {
|
||||
await connection.execute('ALTER TABLE settings MODIFY COLUMN setting_value LONGTEXT');
|
||||
} catch (err) { }
|
||||
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin', 'guru', 'staff') DEFAULT 'staff',
|
||||
permissions TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "users" ready');
|
||||
|
||||
// Create violation_rules table for dynamic violation categories
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS violation_rules (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(20) NOT NULL UNIQUE,
|
||||
description VARCHAR(500) NOT NULL,
|
||||
category ENUM('Perilaku', 'Kerajinan', 'Kerapian') NOT NULL,
|
||||
score INT NOT NULL DEFAULT 0,
|
||||
default_sanction VARCHAR(255),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "violation_rules" ready');
|
||||
|
||||
// Create achievement_criteria table for achievement scoring reference
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS achievement_criteria (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
level ENUM('Kabupaten/Kota', 'Provinsi', 'Nasional', 'Internasional') NOT NULL,
|
||||
achievement_type ENUM('Juara I', 'Juara II', 'Juara III', 'Harapan I', 'Harapan II', 'Harapan III') NOT NULL,
|
||||
participant_type ENUM('Perorangan', 'Kelompok') NOT NULL,
|
||||
group_size ENUM('1-3 orang', '4-6 orang', '7-12 orang', 'Lebih dari 12 orang') DEFAULT NULL,
|
||||
score INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_criteria (level, achievement_type, participant_type, group_size)
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "achievement_criteria" ready');
|
||||
|
||||
// Insert default admin user if not exists
|
||||
const [userRows] = await connection.execute('SELECT COUNT(*) as count FROM users');
|
||||
if (userRows[0].count === 0) {
|
||||
const defaultPermissions = JSON.stringify(['dashboard', 'violation', 'achievement', 'students', 'report', 'settings']);
|
||||
await connection.execute(`
|
||||
INSERT INTO users (username, password, name, role, permissions)
|
||||
VALUES ('admin', 'Smanab100%', 'Administrator', 'admin', ?)
|
||||
`, [defaultPermissions]);
|
||||
console.log('✅ Default admin user created');
|
||||
}
|
||||
|
||||
// Insert default violation rules if table is empty
|
||||
const [violationRulesRows] = await connection.execute('SELECT COUNT(*) as count FROM violation_rules');
|
||||
if (violationRulesRows[0].count === 0) {
|
||||
const defaultRules = [
|
||||
['A1', 'Melakukan atau terlibat tindakan pidana', 'Perilaku', 100, null],
|
||||
['A2', 'Membawa dan menggunakan narkoba', 'Perilaku', 100, null],
|
||||
['A3', 'Menganiaya/intimidasi pada guru, kepala sekolah, dll', 'Perilaku', 50, null],
|
||||
['A4', 'Membawa dan mengkonsumsi miras', 'Perilaku', 25, null],
|
||||
['A5', 'Membawa dan menggunakan senjata tajam tanpa ijin', 'Perilaku', 25, null],
|
||||
['A6', 'Membawa dan mengedarkan tayangan porno (buku, VCD, dll)', 'Perilaku', 25, null],
|
||||
['A7', 'Berkelahi atau terlibat tawuran', 'Perilaku', 25, null],
|
||||
['A8', 'Berbuat asusila', 'Perilaku', 25, null],
|
||||
['A9', 'Merusak sarana & prasarana milik sekolah dan orang lain', 'Perilaku', 25, 'Mengganti sarana'],
|
||||
['A10', 'Membawa rokok atau merokok di sekolah', 'Perilaku', 15, null],
|
||||
['A11', 'Mengancam warga sekolah dan orang lain', 'Perilaku', 15, null],
|
||||
['A12', 'Memalsukan tanda tangan kepala sekolah, guru dan stempel', 'Perilaku', 15, null],
|
||||
['A13', 'Berada di luar lingkungan sekolah tanpa ijin saat PBM', 'Perilaku', 15, null],
|
||||
['A14', 'Menerobos atau melompat pagar sekolah', 'Perilaku', 15, null],
|
||||
|
||||
['B1', 'Absen tanpa keterangan (Alpha)', 'Kerajinan', 5, null],
|
||||
['B2', 'Tidak menyelesaikan administrasi yang ditentukan', 'Kerajinan', 1, null],
|
||||
['B3', 'Terlambat hadir ke sekolah', 'Kerajinan', 1, null],
|
||||
['B4', 'Terlambat mengikuti pembelajaran', 'Kerajinan', 1, null],
|
||||
['B5', 'Terlambat menyerahkan tugas', 'Kerajinan', 1, null],
|
||||
['B6', 'Absen mengikuti ulangan tanpa ijin', 'Kerajinan', 2, null],
|
||||
['B7', 'Absen mengikuti upacara sekolah tanpa ijin', 'Kerajinan', 2, null],
|
||||
['B8', 'Kesalahan yang terulang sampai 3x', 'Kerajinan', 6, null],
|
||||
|
||||
['C1', 'Memakai pakaian seragam tidak sesuai dengan ketentuan', 'Kerapian', 5, null],
|
||||
['C2', 'Memelihara rambut tidak sesuai dengan ketentuan', 'Kerapian', 5, null],
|
||||
['C3', 'Mengecat rambut selain warna hitam', 'Kerapian', 5, null],
|
||||
['C4', 'Siswa putra/putri memakai perhiasan berlebihan', 'Kerapian', 5, null]
|
||||
];
|
||||
|
||||
for (const rule of defaultRules) {
|
||||
await connection.execute(`
|
||||
INSERT INTO violation_rules (code, description, category, score, default_sanction)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, rule);
|
||||
}
|
||||
console.log('✅ Default violation rules inserted');
|
||||
}
|
||||
|
||||
// Insert default achievement criteria if table is empty
|
||||
const [achievementCriteriaRows] = await connection.execute('SELECT COUNT(*) as count FROM achievement_criteria');
|
||||
if (achievementCriteriaRows[0].count === 0) {
|
||||
const levels = ['Kabupaten/Kota', 'Provinsi', 'Nasional', 'Internasional'];
|
||||
const achievements = ['Juara I', 'Juara II', 'Juara III', 'Harapan I', 'Harapan II', 'Harapan III'];
|
||||
const groupSizes = ['1-3 orang', '4-6 orang', '7-12 orang', 'Lebih dari 12 orang'];
|
||||
|
||||
const baseScores = {
|
||||
'Internasional': { 'Juara I': 50, 'Juara II': 45, 'Juara III': 40, 'Harapan I': 35, 'Harapan II': 30, 'Harapan III': 25 },
|
||||
'Nasional': { 'Juara I': 40, 'Juara II': 35, 'Juara III': 30, 'Harapan I': 25, 'Harapan II': 20, 'Harapan III': 15 },
|
||||
'Provinsi': { 'Juara I': 30, 'Juara II': 25, 'Juara III': 20, 'Harapan I': 15, 'Harapan II': 10, 'Harapan III': 5 },
|
||||
'Kabupaten/Kota': { 'Juara I': 20, 'Juara II': 15, 'Juara III': 10, 'Harapan I': 8, 'Harapan II': 5, 'Harapan III': 3 }
|
||||
};
|
||||
|
||||
// Insert Perorangan criteria
|
||||
for (const level of levels) {
|
||||
for (const achievement of achievements) {
|
||||
const score = baseScores[level][achievement];
|
||||
await connection.execute(`
|
||||
INSERT INTO achievement_criteria (level, achievement_type, participant_type, group_size, score)
|
||||
VALUES (?, ?, 'Perorangan', NULL, ?)
|
||||
`, [level, achievement, score]);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert Kelompok criteria with different group sizes
|
||||
for (const level of levels) {
|
||||
for (const achievement of achievements) {
|
||||
for (const groupSize of groupSizes) {
|
||||
// Score decreases slightly for larger groups
|
||||
let baseScore = baseScores[level][achievement];
|
||||
if (groupSize === '1-3 orang') baseScore = Math.floor(baseScore * 0.9);
|
||||
else if (groupSize === '4-6 orang') baseScore = Math.floor(baseScore * 0.8);
|
||||
else if (groupSize === '7-12 orang') baseScore = Math.floor(baseScore * 0.7);
|
||||
else baseScore = Math.floor(baseScore * 0.6);
|
||||
|
||||
await connection.execute(`
|
||||
INSERT INTO achievement_criteria (level, achievement_type, participant_type, group_size, score)
|
||||
VALUES (?, ?, 'Kelompok', ?, ?)
|
||||
`, [level, achievement, groupSize, baseScore]);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('✅ Default achievement criteria inserted');
|
||||
}
|
||||
|
||||
// Insert default settings if not exists
|
||||
const defaultSettings = [
|
||||
['schoolName', 'SMA Negeri 1 Abiansemal'],
|
||||
['academicYear', '2025/2026'],
|
||||
['logoBase64', ''],
|
||||
['kopBase64', ''],
|
||||
['principalName', ''],
|
||||
['principalNip', ''],
|
||||
['counselorName', ''],
|
||||
['counselorNip', '']
|
||||
];
|
||||
|
||||
for (const [key, value] of defaultSettings) {
|
||||
await connection.execute(`
|
||||
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES (?, ?)
|
||||
`, [key, value]);
|
||||
}
|
||||
console.log('✅ Default settings initialized');
|
||||
|
||||
// Insert sample students if table is empty
|
||||
const [studentRows] = await connection.execute('SELECT COUNT(*) as count FROM students');
|
||||
if (studentRows[0].count === 0) {
|
||||
const sampleStudents = [
|
||||
['2023001', 'I Putu Gede Mahendra', 'X-1', 'I Made Wijaya', '081234567890', 'Pagi'],
|
||||
['2023002', 'Ni Kadek Ayu Lestari', 'X-1', 'I Ketut Arta', '081987654321', 'Pagi'],
|
||||
['2023003', 'Komang Budi Utama', 'XI-IPA-2', 'Wayan Sudira', '08122334455', 'Siang'],
|
||||
['2023004', 'Dewa Ayu Dewi', 'XII-IPS-1', 'Dewa Rai', '08177665544', 'Siang']
|
||||
];
|
||||
|
||||
for (const student of sampleStudents) {
|
||||
await connection.execute(`
|
||||
INSERT INTO students (id, name, class, parent_name, parent_phone, shift) VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, student);
|
||||
}
|
||||
console.log('✅ Sample students inserted');
|
||||
}
|
||||
|
||||
console.log('🎉 Database initialization complete!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization error:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
export default pool;
|
||||
3651
Sistem-Pelanggaran-Siswa/package-lock.json
generated
Normal file
33
Sistem-Pelanggaran-Siswa/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "sipasi---sma-n-1-abiansemal",
|
||||
"private": true,
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "node server.js",
|
||||
"server": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^5.2.1",
|
||||
"jspdf": "^3.0.4",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"lucide-react": "^0.561.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.6.5",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"recharts": "^3.6.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
SURAT PERINGATAN TINGKAT 1 (SP 1)
|
||||
(Skor Pelanggaran Total: 50)
|
||||
|
||||
Nomor Surat:
|
||||
|
||||
Perihal: Surat Peringatan Tingkat 1 (SP 1)
|
||||
Yth. Bapak/Ibu Wali Murid dari:
|
||||
Keterangan
|
||||
:
|
||||
Data Siswa
|
||||
Nama Siswa
|
||||
:
|
||||
[Nama Lengkap Siswa]
|
||||
Nomor Induk Siswa (NIS)
|
||||
:
|
||||
[student id]
|
||||
Kelas
|
||||
:
|
||||
[Kelas Siswa]
|
||||
|
||||
Dengan hormat,
|
||||
Berdasarkan hasil evaluasi dan rekapitulasi data pelanggaran tata tertib sekolah, dengan ini kami sampaikan bahwa siswa/i tersebut di atas telah mencapai total Skor Pelanggaran 50.
|
||||
Pelanggaran tersebut meliputi:
|
||||
1. [Contoh Pelanggaran 1, Tanggal] (Skor: [Angka])
|
||||
2. [Contoh Pelanggaran 2, Tanggal] (Skor: [Angka])
|
||||
3. [dan seterusnya, hingga mencapai total 50 skor]
|
||||
Konsekuensi:
|
||||
1. Surat Peringatan Tingkat 1 (SP 1) ini merupakan teguran lisan dan tertulis pertama.
|
||||
2. Siswa diwajibkan mengikuti program pembinaan dari Guru Bimbingan Konseling (BK) selama [Jumlah] minggu/bulan.
|
||||
3. Orang tua/wali diminta untuk melakukan pertemuan konsultasi dengan Wali Kelas dan Guru BK untuk membahas solusi dan komitmen perubahan perilaku.
|
||||
4. Siswa wajib menunjukkan perubahan sikap positif dan tidak melakukan pelanggaran dalam jangka waktu [Jumlah] bulan ke depan.
|
||||
Apabila siswa kembali melakukan pelanggaran hingga mencapai total Skor Pelanggaran 75, maka sekolah akan menerbitkan Surat Peringatan Tingkat 2 (SP 2).
|
||||
Demikian surat ini disampaikan agar menjadi perhatian dan ditindaklanjuti sebagaimana mestinya. Atas perhatian dan kerja sama Bapak/Ibu, kami ucapkan terima kasih.
|
||||
|
||||
Badung, [Tanggal Hari Ini]
|
||||
Pihak Sekolah
|
||||
Wali Murid / Orang Tua
|
||||
Guru Bimbingan Konseling
|
||||
@@ -0,0 +1,35 @@
|
||||
SURAT PERINGATAN TINGKAT 2 (SP 2)
|
||||
(Skor Pelanggaran Total: 75)
|
||||
|
||||
Nomor Surat:
|
||||
Perihal: Surat Peringatan Tingkat 2 (SP 2
|
||||
|
||||
Yth. Bapak/Ibu Wali Murid dari:
|
||||
Keterangan
|
||||
:
|
||||
Data Siswa
|
||||
Nama Siswa
|
||||
:
|
||||
[Nama Lengkap Siswa]
|
||||
Nomor Induk Siswa (NIS)
|
||||
:
|
||||
[student id]
|
||||
Kelas
|
||||
:
|
||||
[Kelas Siswa]
|
||||
|
||||
Dengan hormat,
|
||||
Merujuk pada Surat Peringatan Tingkat 1 (SP 1) tertanggal [Tanggal SP 1 diterbitkan], dan setelah dilakukannya pembinaan, dengan ini kami sampaikan bahwa siswa/i tersebut di atas kembali melakukan pelanggaran tata tertib sekolah hingga mencapai total Skor Pelanggaran 75.
|
||||
Pelanggaran tersebut menunjukkan kurangnya komitmen siswa dalam mematuhi tata tertib sekolah setelah diterbitkannya SP 1.
|
||||
Konsekuensi:
|
||||
1. Surat Peringatan Tingkat 2 (SP 2) ini merupakan peringatan keras kedua.
|
||||
2. Siswa dikenakan sanksi berupa Skorsing Belajar selama [Jumlah Hari] hari (misalnya, 3-5 hari). Selama skorsing, siswa wajib menyelesaikan tugas akademik dari sekolah di rumah di bawah pengawasan orang tua/wali.
|
||||
3. Orang tua/wali diwajibkan membuat Surat Pernyataan Komitmen yang ditandatangani di atas materai, yang menyatakan kesediaan untuk aktif mengawasi dan membimbing siswa di rumah dan berjanji bahwa siswa tidak akan mengulangi pelanggaran serupa.
|
||||
4. Jika siswa kembali melakukan pelanggaran hingga mencapai total Skor Pelanggaran 100, maka sekolah akan menerbitkan Surat Peringatan Tingkat 3 (SP 3) yang berujung pada pengembalian siswa kepada orang tua/wali.
|
||||
Kami memohon Bapak/Ibu untuk bekerja sama dengan sekolah demi perbaikan perilaku siswa.
|
||||
Demikian surat ini disampaikan untuk ditindaklanjuti.
|
||||
|
||||
Badung, [Tanggal Hari Ini]
|
||||
Pihak Sekolah
|
||||
Wali Murid / Orang Tua
|
||||
Guru Bimbingan Konseling
|
||||
@@ -0,0 +1,35 @@
|
||||
SURAT PERINGATAN TINGKAT 3 (SP 3)
|
||||
(Skor Pelanggaran Total: 100)
|
||||
|
||||
Nomor Surat:
|
||||
Perihal: Surat Peringatan Tingkat 3 (SP 3) dan Keputusan Pengembalian Siswa
|
||||
|
||||
Yth. Bapak/Ibu Wali Murid dari:
|
||||
Keterangan
|
||||
:
|
||||
Data Siswa
|
||||
Nama Siswa
|
||||
:
|
||||
[Nama Lengkap Siswa]
|
||||
Nomor Induk Siswa (NIS)
|
||||
:
|
||||
[student id]
|
||||
Kelas
|
||||
:
|
||||
[Kelas Siswa]
|
||||
|
||||
Dengan hormat,
|
||||
Mengacu pada:
|
||||
1. Surat Peringatan Tingkat 1 (SP 1) tertanggal [Tanggal SP 1].
|
||||
2. Surat Peringatan Tingkat 2 (SP 2) tertanggal [Tanggal SP 2].
|
||||
3. Surat Pernyataan Komitmen Orang Tua/Wali tertanggal [Tanggal Surat Pernyataan].
|
||||
Dengan sangat menyesal kami sampaikan bahwa siswa/i tersebut di atas telah kembali melakukan pelanggaran serius dan/atau akumulasi pelanggaran ringan lainnya hingga mencapai total Skor Pelanggaran 100.
|
||||
Meskipun telah dilakukan berbagai upaya pembinaan, pendampingan, dan penerapan sanksi skorsing, siswa/i menunjukkan tidak adanya perubahan signifikan dalam kepatuhan terhadap tata tertib sekolah.
|
||||
Keputusan Akhir Sekolah:
|
||||
Berdasarkan pertimbangan Dewan Guru, Komite Sekolah, dan sesuai dengan ketentuan Tata Tertib Sekolah Pasal [Nomor Pasal terkait SP 3], maka sekolah memutuskan untuk:
|
||||
1. Mengembalikan siswa/i [Nama Siswa] kepada orang tua/wali terhitung sejak tanggal [Tanggal Efektif Pengembalian].
|
||||
2. Dengan keputusan ini, siswa/i tersebut tidak lagi terdaftar sebagai peserta didik di [Nama Sekolah].
|
||||
3. Orang tua/wali dipersilakan mengurus kepindahan siswa ke sekolah lain. Sekolah akan membantu dalam proses administrasi perpindahan.
|
||||
Keputusan ini diambil sebagai langkah terakhir dan demi menjaga iklim belajar yang kondusif di lingkungan sekolah. Kami berharap Bapak/Ibu dapat memberikan perhatian dan pembinaan yang lebih intensif di luar lingkungan sekolah.
|
||||
Demikian Surat Peringatan Tingkat 3 ini disampaikan untuk diperhatikan dan dilaksanakan.
|
||||
|
||||
21
Sistem-Pelanggaran-Siswa/public/images/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Folder Gambar Publik
|
||||
|
||||
Folder ini berisi aset gambar untuk aplikasi SIPASI.
|
||||
|
||||
## File yang diperlukan:
|
||||
|
||||
### 1. `logo.png`
|
||||
- **Kegunaan**: Logo aplikasi dan Favicon
|
||||
- **Ukuran yang disarankan**: 512x512 px (minimal 192x192 px)
|
||||
- **Format**: PNG dengan background transparan
|
||||
|
||||
### 2. `kop-sekolah.png`
|
||||
- **Kegunaan**: Header cetak laporan
|
||||
- **Ukuran yang disarankan**: Lebar 800px (tinggi menyesuaikan)
|
||||
- **Format**: PNG atau JPG
|
||||
|
||||
## Cara Mengakses
|
||||
|
||||
Gambar dapat diakses melalui URL:
|
||||
- Logo: `http://localhost:3007/images/logo.png`
|
||||
- KOP: `http://localhost:3007/images/kop-sekolah.png`
|
||||
BIN
Sistem-Pelanggaran-Siswa/public/images/kop-sekolah.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
Sistem-Pelanggaran-Siswa/public/images/logo.png
Normal file
|
After Width: | Height: | Size: 516 KiB |
@@ -0,0 +1,67 @@
|
||||
import XLSX from 'xlsx';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Template Kriteria Pelanggaran
|
||||
const violationRulesData = [
|
||||
{ Kode: 'A1', Deskripsi: 'Membawa/menggunakan senjata tajam/api', Kategori: 'Perilaku', Skor: 100, Sanksi: '' },
|
||||
{ Kode: 'A2', Deskripsi: 'Membawa/menggunakan narkoba/miras', Kategori: 'Perilaku', Skor: 100, Sanksi: '' },
|
||||
{ Kode: 'A3', Deskripsi: 'Berkelahi/tawuran', Kategori: 'Perilaku', Skor: 100, Sanksi: '' },
|
||||
{ Kode: 'A4', Deskripsi: 'Merusak sarana sekolah', Kategori: 'Perilaku', Skor: 50, Sanksi: 'Mengganti sarana' },
|
||||
{ Kode: 'A5', Deskripsi: 'Membuat gaduh di kelas', Kategori: 'Perilaku', Skor: 25, Sanksi: '' },
|
||||
{ Kode: 'B1', Deskripsi: 'Absen tanpa keterangan (Alpha)', Kategori: 'Kerajinan', Skor: 5, Sanksi: '' },
|
||||
{ Kode: 'B2', Deskripsi: 'Terlambat hadir ke sekolah', Kategori: 'Kerajinan', Skor: 5, Sanksi: '' },
|
||||
{ Kode: 'B3', Deskripsi: 'Absen ulangan tanpa izin', Kategori: 'Kerajinan', Skor: 10, Sanksi: '' },
|
||||
{ Kode: 'C1', Deskripsi: 'Seragam tidak sesuai ketentuan', Kategori: 'Kerapian', Skor: 20, Sanksi: '' },
|
||||
{ Kode: 'C2', Deskripsi: 'Rambut gondrong/tidak rapi', Kategori: 'Kerapian', Skor: 10, Sanksi: '' },
|
||||
];
|
||||
|
||||
const wsViolation = XLSX.utils.json_to_sheet(violationRulesData);
|
||||
wsViolation['!cols'] = [
|
||||
{ wch: 8 }, // Kode
|
||||
{ wch: 50 }, // Deskripsi
|
||||
{ wch: 15 }, // Kategori
|
||||
{ wch: 8 }, // Skor
|
||||
{ wch: 25 }, // Sanksi
|
||||
];
|
||||
|
||||
const wbViolation = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wbViolation, wsViolation, 'Kriteria Pelanggaran');
|
||||
XLSX.writeFile(wbViolation, path.join(__dirname, 'Template-Import-Kriteria-Pelanggaran.xlsx'));
|
||||
console.log('✅ Template Kriteria Pelanggaran created!');
|
||||
|
||||
// Template Kriteria Prestasi
|
||||
const achievementCriteriaData = [
|
||||
{ Tingkat: 'Internasional', Prestasi: 'Juara I', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 50 },
|
||||
{ Tingkat: 'Internasional', Prestasi: 'Juara II', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 45 },
|
||||
{ Tingkat: 'Internasional', Prestasi: 'Juara III', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 40 },
|
||||
{ Tingkat: 'Nasional', Prestasi: 'Juara I', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 40 },
|
||||
{ Tingkat: 'Nasional', Prestasi: 'Juara II', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 35 },
|
||||
{ Tingkat: 'Nasional', Prestasi: 'Juara III', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 30 },
|
||||
{ Tingkat: 'Provinsi', Prestasi: 'Juara I', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 30 },
|
||||
{ Tingkat: 'Provinsi', Prestasi: 'Juara II', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 25 },
|
||||
{ Tingkat: 'Kabupaten/Kota', Prestasi: 'Juara I', Jenis: 'Perorangan', 'Ukuran Kelompok': '', Skor: 20 },
|
||||
{ Tingkat: 'Kabupaten/Kota', Prestasi: 'Juara I', Jenis: 'Kelompok', 'Ukuran Kelompok': '1-3 orang', Skor: 18 },
|
||||
{ Tingkat: 'Kabupaten/Kota', Prestasi: 'Juara I', Jenis: 'Kelompok', 'Ukuran Kelompok': '4-6 orang', Skor: 16 },
|
||||
{ Tingkat: 'Kabupaten/Kota', Prestasi: 'Juara I', Jenis: 'Kelompok', 'Ukuran Kelompok': '7-12 orang', Skor: 14 },
|
||||
{ Tingkat: 'Kabupaten/Kota', Prestasi: 'Juara I', Jenis: 'Kelompok', 'Ukuran Kelompok': 'Lebih dari 12 orang', Skor: 12 },
|
||||
];
|
||||
|
||||
const wsAchievement = XLSX.utils.json_to_sheet(achievementCriteriaData);
|
||||
wsAchievement['!cols'] = [
|
||||
{ wch: 18 }, // Tingkat
|
||||
{ wch: 15 }, // Prestasi
|
||||
{ wch: 12 }, // Jenis
|
||||
{ wch: 22 }, // Ukuran Kelompok
|
||||
{ wch: 8 }, // Skor
|
||||
];
|
||||
|
||||
const wbAchievement = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wbAchievement, wsAchievement, 'Kriteria Prestasi');
|
||||
XLSX.writeFile(wbAchievement, path.join(__dirname, 'Template-Import-Kriteria-Prestasi.xlsx'));
|
||||
console.log('✅ Template Kriteria Prestasi created!');
|
||||
|
||||
console.log('\n📁 Templates saved to public/templates/ folder');
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
241
Sistem-Pelanggaran-Siswa/server.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
import multer from 'multer';
|
||||
import fs from 'fs';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// Import database and routes
|
||||
import pool, { initDatabase } from './database.js';
|
||||
import studentsRouter from './routes/students.js';
|
||||
import violationsRouter from './routes/violations.js';
|
||||
import achievementsRouter from './routes/achievements.js';
|
||||
import settingsRouter from './routes/settings.js';
|
||||
import usersRouter from './routes/users.js';
|
||||
import violationRulesRouter from './routes/violation-rules.js';
|
||||
import achievementCriteriaRouter from './routes/achievement-criteria.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3007;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Static files - serve from dist folder (built frontend)
|
||||
app.use(express.static(path.join(__dirname, './dist')));
|
||||
|
||||
// Also serve public folder for images
|
||||
app.use('/images', express.static(path.join(__dirname, './public/images')));
|
||||
|
||||
// File upload configuration
|
||||
const uploadDir = path.join(__dirname, './uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
// Create monthly folder
|
||||
const date = new Date();
|
||||
const monthNames = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
const monthFolder = `${monthNames[date.getMonth()]} ${date.getFullYear()}`;
|
||||
const targetDir = path.join(uploadDir, monthFolder);
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
cb(null, targetDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `violation-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Serve uploaded files
|
||||
app.use('/uploads', express.static(uploadDir));
|
||||
|
||||
// API Routes
|
||||
app.use('/api/students', studentsRouter);
|
||||
app.use('/api/violations', violationsRouter);
|
||||
app.use('/api/achievements', achievementsRouter);
|
||||
app.use('/api/settings', settingsRouter);
|
||||
app.use('/api/users', usersRouter);
|
||||
app.use('/api/violation-rules', violationRulesRouter);
|
||||
app.use('/api/achievement-criteria', achievementCriteriaRouter);
|
||||
|
||||
// File upload endpoint
|
||||
app.post('/api/upload', upload.single('photo'), (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
// Build the relative path for the uploaded file
|
||||
const relativePath = req.file.path.replace(uploadDir, '').replace(/\\/g, '/');
|
||||
const photoUrl = `/uploads${relativePath}`;
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
photoUrl,
|
||||
filename: req.file.filename
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ error: 'Failed to upload file' });
|
||||
}
|
||||
});
|
||||
|
||||
// Base64 image upload (for compatibility with existing frontend)
|
||||
app.post('/api/upload-base64', async (req, res) => {
|
||||
try {
|
||||
const { photoBase64, violationId } = req.body;
|
||||
|
||||
if (!photoBase64) {
|
||||
return res.status(400).json({ error: 'No image data provided' });
|
||||
}
|
||||
|
||||
// Extract base64 data
|
||||
const matches = photoBase64.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
||||
if (!matches || matches.length !== 3) {
|
||||
return res.status(400).json({ error: 'Invalid base64 image format' });
|
||||
}
|
||||
|
||||
const type = matches[1];
|
||||
const data = matches[2];
|
||||
const buffer = Buffer.from(data, 'base64');
|
||||
|
||||
// Determine file extension
|
||||
let ext = '.jpg';
|
||||
if (type.includes('png')) ext = '.png';
|
||||
else if (type.includes('gif')) ext = '.gif';
|
||||
else if (type.includes('webp')) ext = '.webp';
|
||||
|
||||
// Create monthly folder
|
||||
const date = new Date();
|
||||
const monthNames = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
const monthFolder = `${monthNames[date.getMonth()]} ${date.getFullYear()}`;
|
||||
const targetDir = path.join(uploadDir, monthFolder);
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate filename
|
||||
const filename = `violation-${violationId || Date.now()}${ext}`;
|
||||
const filePath = path.join(targetDir, filename);
|
||||
|
||||
// Write file
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
|
||||
// Build URL
|
||||
const photoUrl = `/uploads/${monthFolder}/${filename}`;
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
photoUrl,
|
||||
filename
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Base64 upload error:', error);
|
||||
res.status(500).json({ error: 'Failed to save image' });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint with DB verification
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
// Test database connection
|
||||
const [rows] = await pool.execute('SELECT 1 as connection_test');
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
server: 'SIPASI API Server',
|
||||
version: '2.0.0',
|
||||
database: {
|
||||
status: 'connected',
|
||||
type: 'MySQL',
|
||||
ping: 'ok'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
server: 'SIPASI API Server',
|
||||
database: {
|
||||
status: 'disconnected',
|
||||
error: error.message
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// SPA fallback - serve index.html for all non-API routes
|
||||
app.use((req, res, next) => {
|
||||
// Skip API routes
|
||||
if (req.path.startsWith('/api')) {
|
||||
return next();
|
||||
}
|
||||
// Serve index.html for client-side routing
|
||||
res.sendFile(path.join(__dirname, './dist/index.html'));
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Server error:', err);
|
||||
res.status(500).json({ error: err.message || 'Internal server error' });
|
||||
});
|
||||
|
||||
// Initialize database and start server
|
||||
async function startServer() {
|
||||
try {
|
||||
await initDatabase();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ 🏫 SIPASI - Sistem Informasi Pelanggaran Siswa ║
|
||||
║ SMA Negeri 1 Abiansemal ║
|
||||
║ ║
|
||||
║ ✅ Server running on port ${PORT} ║
|
||||
║ 📡 API: http://localhost:${PORT}/api ║
|
||||
║ 🌐 Web: http://localhost:${PORT} ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
||||
155
Sistem-Pelanggaran-Siswa/server/database.js
Normal file
@@ -0,0 +1,155 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Database connection pool
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '3306'),
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'sipasi_db',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
// Auto-create tables on startup
|
||||
export async function initDatabase() {
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
try {
|
||||
console.log('🔌 Connected to MySQL database');
|
||||
|
||||
// Create students table
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id VARCHAR(20) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
class VARCHAR(50) NOT NULL,
|
||||
parent_name VARCHAR(255),
|
||||
parent_phone VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "students" ready');
|
||||
|
||||
// Create violations table
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS violations (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
student_id VARCHAR(20) NOT NULL,
|
||||
rule_id VARCHAR(10) NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
description TEXT,
|
||||
score INT NOT NULL DEFAULT 0,
|
||||
sanction VARCHAR(255),
|
||||
timestamp BIGINT,
|
||||
photo_url TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "violations" ready');
|
||||
|
||||
// Create achievements table
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS achievements (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
student_id VARCHAR(20) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
level ENUM('Nasional', 'Provinsi', 'Kabupaten/Kota', 'Sekolah') NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
score_reduction INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "achievements" ready');
|
||||
|
||||
// Create settings table
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
setting_key VARCHAR(100) PRIMARY KEY,
|
||||
setting_value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "settings" ready');
|
||||
|
||||
// Create users table for role-based access
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin', 'guru', 'staff') DEFAULT 'staff',
|
||||
permissions TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ Table "users" ready');
|
||||
|
||||
// Insert default admin user if not exists
|
||||
const [userRows] = await connection.execute('SELECT COUNT(*) as count FROM users');
|
||||
if (userRows[0].count === 0) {
|
||||
const defaultPermissions = JSON.stringify(['dashboard', 'violation', 'achievement', 'students', 'report', 'settings']);
|
||||
await connection.execute(`
|
||||
INSERT INTO users (username, password, name, role, permissions)
|
||||
VALUES ('admin', 'Smanab100%', 'Administrator', 'admin', ?)
|
||||
`, [defaultPermissions]);
|
||||
console.log('✅ Default admin user created');
|
||||
}
|
||||
|
||||
// Insert default settings if not exists
|
||||
const defaultSettings = [
|
||||
['schoolName', 'SMA Negeri 1 Abiansemal'],
|
||||
['username', 'admin'],
|
||||
['password', 'Smanab100%'],
|
||||
['principalName', ''],
|
||||
['principalNip', ''],
|
||||
['counselorName', ''],
|
||||
['counselorNip', '']
|
||||
];
|
||||
|
||||
for (const [key, value] of defaultSettings) {
|
||||
await connection.execute(`
|
||||
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES (?, ?)
|
||||
`, [key, value]);
|
||||
}
|
||||
console.log('✅ Default settings initialized');
|
||||
|
||||
// Insert sample students if table is empty
|
||||
const [studentRows] = await connection.execute('SELECT COUNT(*) as count FROM students');
|
||||
if (studentRows[0].count === 0) {
|
||||
const sampleStudents = [
|
||||
['2023001', 'I Putu Gede Mahendra', 'X-1', 'I Made Wijaya', '081234567890'],
|
||||
['2023002', 'Ni Kadek Ayu Lestari', 'X-1', 'I Ketut Arta', '081987654321'],
|
||||
['2023003', 'Komang Budi Utama', 'XI-IPA-2', 'Wayan Sudira', '08122334455'],
|
||||
['2023004', 'Dewa Ayu Dewi', 'XII-IPS-1', 'Dewa Rai', '08177665544']
|
||||
];
|
||||
|
||||
for (const student of sampleStudents) {
|
||||
await connection.execute(`
|
||||
INSERT INTO students (id, name, class, parent_name, parent_phone) VALUES (?, ?, ?, ?, ?)
|
||||
`, student);
|
||||
}
|
||||
console.log('✅ Sample students inserted');
|
||||
}
|
||||
|
||||
console.log('🎉 Database initialization complete!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization error:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
export default pool;
|
||||
237
Sistem-Pelanggaran-Siswa/server/index.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
import multer from 'multer';
|
||||
import fs from 'fs';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// Import database and routes
|
||||
import pool, { initDatabase } from './database.js';
|
||||
import studentsRouter from './routes/students.js';
|
||||
import violationsRouter from './routes/violations.js';
|
||||
import achievementsRouter from './routes/achievements.js';
|
||||
import settingsRouter from './routes/settings.js';
|
||||
import usersRouter from './routes/users.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3007;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Static files - serve from dist folder (built frontend)
|
||||
app.use(express.static(path.join(__dirname, '../dist')));
|
||||
|
||||
// Also serve public folder for images
|
||||
app.use('/images', express.static(path.join(__dirname, '../public/images')));
|
||||
|
||||
// File upload configuration
|
||||
const uploadDir = path.join(__dirname, '../uploads');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
// Create monthly folder
|
||||
const date = new Date();
|
||||
const monthNames = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
const monthFolder = `${monthNames[date.getMonth()]} ${date.getFullYear()}`;
|
||||
const targetDir = path.join(uploadDir, monthFolder);
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
cb(null, targetDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `violation-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Serve uploaded files
|
||||
app.use('/uploads', express.static(uploadDir));
|
||||
|
||||
// API Routes
|
||||
app.use('/api/students', studentsRouter);
|
||||
app.use('/api/violations', violationsRouter);
|
||||
app.use('/api/achievements', achievementsRouter);
|
||||
app.use('/api/settings', settingsRouter);
|
||||
app.use('/api/users', usersRouter);
|
||||
|
||||
// File upload endpoint
|
||||
app.post('/api/upload', upload.single('photo'), (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
// Build the relative path for the uploaded file
|
||||
const relativePath = req.file.path.replace(uploadDir, '').replace(/\\/g, '/');
|
||||
const photoUrl = `/uploads${relativePath}`;
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
photoUrl,
|
||||
filename: req.file.filename
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ error: 'Failed to upload file' });
|
||||
}
|
||||
});
|
||||
|
||||
// Base64 image upload (for compatibility with existing frontend)
|
||||
app.post('/api/upload-base64', async (req, res) => {
|
||||
try {
|
||||
const { photoBase64, violationId } = req.body;
|
||||
|
||||
if (!photoBase64) {
|
||||
return res.status(400).json({ error: 'No image data provided' });
|
||||
}
|
||||
|
||||
// Extract base64 data
|
||||
const matches = photoBase64.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
||||
if (!matches || matches.length !== 3) {
|
||||
return res.status(400).json({ error: 'Invalid base64 image format' });
|
||||
}
|
||||
|
||||
const type = matches[1];
|
||||
const data = matches[2];
|
||||
const buffer = Buffer.from(data, 'base64');
|
||||
|
||||
// Determine file extension
|
||||
let ext = '.jpg';
|
||||
if (type.includes('png')) ext = '.png';
|
||||
else if (type.includes('gif')) ext = '.gif';
|
||||
else if (type.includes('webp')) ext = '.webp';
|
||||
|
||||
// Create monthly folder
|
||||
const date = new Date();
|
||||
const monthNames = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
const monthFolder = `${monthNames[date.getMonth()]} ${date.getFullYear()}`;
|
||||
const targetDir = path.join(uploadDir, monthFolder);
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate filename
|
||||
const filename = `violation-${violationId || Date.now()}${ext}`;
|
||||
const filePath = path.join(targetDir, filename);
|
||||
|
||||
// Write file
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
|
||||
// Build URL
|
||||
const photoUrl = `/uploads/${monthFolder}/${filename}`;
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
photoUrl,
|
||||
filename
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Base64 upload error:', error);
|
||||
res.status(500).json({ error: 'Failed to save image' });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint with DB verification
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
// Test database connection
|
||||
const [rows] = await pool.execute('SELECT 1 as connection_test');
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
server: 'SIPASI API Server',
|
||||
version: '2.0.0',
|
||||
database: {
|
||||
status: 'connected',
|
||||
type: 'MySQL',
|
||||
ping: 'ok'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
server: 'SIPASI API Server',
|
||||
database: {
|
||||
status: 'disconnected',
|
||||
error: error.message
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// SPA fallback - serve index.html for all non-API routes
|
||||
app.use((req, res, next) => {
|
||||
// Skip API routes
|
||||
if (req.path.startsWith('/api')) {
|
||||
return next();
|
||||
}
|
||||
// Serve index.html for client-side routing
|
||||
res.sendFile(path.join(__dirname, '../dist/index.html'));
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Server error:', err);
|
||||
res.status(500).json({ error: err.message || 'Internal server error' });
|
||||
});
|
||||
|
||||
// Initialize database and start server
|
||||
async function startServer() {
|
||||
try {
|
||||
await initDatabase();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ 🏫 SIPASI - Sistem Informasi Pelanggaran Siswa ║
|
||||
║ SMA Negeri 1 Abiansemal ║
|
||||
║ ║
|
||||
║ ✅ Server running on port ${PORT} ║
|
||||
║ 📡 API: http://localhost:${PORT}/api ║
|
||||
║ 🌐 Web: http://localhost:${PORT} ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
||||
65
Sistem-Pelanggaran-Siswa/server/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/server/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;
|
||||
86
Sistem-Pelanggaran-Siswa/server/routes/students.js
Normal file
@@ -0,0 +1,86 @@
|
||||
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
|
||||
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
|
||||
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 } = req.body;
|
||||
|
||||
await pool.execute(`
|
||||
INSERT INTO students (id, name, class, parent_name, parent_phone)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [id, name, studentClass, parentName, parentPhone]);
|
||||
|
||||
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 } = req.body;
|
||||
|
||||
await pool.execute(`
|
||||
UPDATE students
|
||||
SET name = ?, class = ?, parent_name = ?, parent_phone = ?
|
||||
WHERE id = ?
|
||||
`, [name, studentClass, parentName, parentPhone, 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' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
160
Sistem-Pelanggaran-Siswa/server/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;
|
||||
67
Sistem-Pelanggaran-Siswa/server/routes/violations.js
Normal file
@@ -0,0 +1,67 @@
|
||||
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' });
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 266 KiB |