Initial commit apps directory with .gitignore

This commit is contained in:
2026-02-22 15:15:41 +08:00
commit 0aa8cdd72c
228 changed files with 69672 additions and 0 deletions

129
Sistem-Pelanggaran-Siswa/.gitignore vendored Normal file
View 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

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View 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`

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

View File

@@ -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');

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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();

View 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;

View 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();

View 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;

View 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;

View 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;

View 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;

View 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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB