Initial commit apps directory with .gitignore
This commit is contained in:
BIN
buku-induk-sma-negeri-1-abiansemal/._package-lock.json
generated
Normal file
BIN
buku-induk-sma-negeri-1-abiansemal/._package-lock.json
generated
Normal file
Binary file not shown.
BIN
buku-induk-sma-negeri-1-abiansemal/._server.js
Normal file
BIN
buku-induk-sma-negeri-1-abiansemal/._server.js
Normal file
Binary file not shown.
129
buku-induk-sma-negeri-1-abiansemal/.gitignore
vendored
Normal file
129
buku-induk-sma-negeri-1-abiansemal/.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
|
||||
3422
buku-induk-sma-negeri-1-abiansemal/package-lock.json
generated
Normal file
3422
buku-induk-sma-negeri-1-abiansemal/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
buku-induk-sma-negeri-1-abiansemal/package.json
Normal file
35
buku-induk-sma-negeri-1-abiansemal/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "buku-induk-siswa",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:server": "node server.js",
|
||||
"dev:full": "concurrently \"npm run dev:server\" \"npm run dev\"",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.19.2",
|
||||
"html2pdf.js": "^0.10.1",
|
||||
"lucide-react": "^0.560.0",
|
||||
"mysql2": "^3.12.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11"
|
||||
}
|
||||
}
|
||||
795
buku-induk-sma-negeri-1-abiansemal/server.js
Normal file
795
buku-induk-sma-negeri-1-abiansemal/server.js
Normal file
@@ -0,0 +1,795 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import cors from 'cors';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import pool, { initDatabase } from './server/db.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3006;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
|
||||
// Helper: Convert camelCase to snake_case
|
||||
const camelToSnake = (str) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
||||
const snakeToCamel = (str) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
|
||||
// Helper: Convert object keys
|
||||
const convertKeysToSnake = (obj) => {
|
||||
const result = {};
|
||||
for (const key in obj) {
|
||||
result[camelToSnake(key)] = obj[key];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const convertKeysToCamel = (obj) => {
|
||||
const result = {};
|
||||
for (const key in obj) {
|
||||
result[snakeToCamel(key)] = obj[key];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// ==================== AUTH API ====================
|
||||
|
||||
// Login
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ success: false, message: 'Username dan password diperlukan' });
|
||||
}
|
||||
|
||||
const [users] = await pool.query('SELECT * FROM users WHERE username = ?', [username]);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(401).json({ success: false, message: 'Username atau password salah' });
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isValid) {
|
||||
return res.status(401).json({ success: false, message: 'Username atau password salah' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
fullName: user.full_name,
|
||||
role: user.role
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ success: false, message: 'Terjadi kesalahan server' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== STUDENTS API ====================
|
||||
|
||||
// Get all students
|
||||
app.get('/api/students', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM students ORDER BY nama ASC');
|
||||
const students = rows.map(row => {
|
||||
const student = convertKeysToCamel(row);
|
||||
// Normalize field names for frontend compatibility
|
||||
return {
|
||||
...student,
|
||||
id: String(student.id),
|
||||
fotoDiterimaUrl: student.fotoDiterimaUrl || '',
|
||||
fotoLulusUrl: student.fotoLulusUrl || '',
|
||||
};
|
||||
});
|
||||
res.json(students);
|
||||
} catch (error) {
|
||||
console.error('Error fetching students:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil data siswa' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single student
|
||||
app.get('/api/students/:id', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM students WHERE id = ?', [req.params.id]);
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ success: false, message: 'Siswa tidak ditemukan' });
|
||||
}
|
||||
const student = convertKeysToCamel(rows[0]);
|
||||
student.id = String(student.id);
|
||||
res.json(student);
|
||||
} catch (error) {
|
||||
console.error('Error fetching student:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil data siswa' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create/Update student
|
||||
app.post('/api/students', async (req, res) => {
|
||||
try {
|
||||
const studentData = req.body;
|
||||
const { id, ...data } = studentData;
|
||||
|
||||
// Convert keys to snake_case for database
|
||||
const dbData = convertKeysToSnake(data);
|
||||
|
||||
// Remove undefined/null values and clean up data
|
||||
const cleanData = {};
|
||||
for (const [key, value] of Object.entries(dbData)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
cleanData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (id && id !== '' && !isNaN(parseInt(id))) {
|
||||
// Update existing student
|
||||
const fields = Object.keys(cleanData);
|
||||
const values = Object.values(cleanData);
|
||||
const setClause = fields.map(f => `${f} = ?`).join(', ');
|
||||
|
||||
await pool.query(`UPDATE students SET ${setClause} WHERE id = ?`, [...values, parseInt(id)]);
|
||||
res.json({ success: true, message: 'Data siswa berhasil diupdate', id: parseInt(id) });
|
||||
} else {
|
||||
// Insert new student
|
||||
const fields = Object.keys(cleanData);
|
||||
const values = Object.values(cleanData);
|
||||
const placeholders = fields.map(() => '?').join(', ');
|
||||
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO students (${fields.join(', ')}) VALUES (${placeholders})`,
|
||||
values
|
||||
);
|
||||
res.json({ success: true, message: 'Data siswa berhasil disimpan', id: result.insertId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving student:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal menyimpan data siswa: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete student
|
||||
app.delete('/api/students/:id', async (req, res) => {
|
||||
try {
|
||||
const [result] = await pool.query('DELETE FROM students WHERE id = ?', [req.params.id]);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: 'Siswa tidak ditemukan' });
|
||||
}
|
||||
res.json({ success: true, message: 'Data siswa berhasil dihapus' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting student:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal menghapus data siswa' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== SETTINGS API ====================
|
||||
|
||||
// Get settings
|
||||
app.get('/api/settings', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
|
||||
if (rows.length === 0) {
|
||||
res.json({
|
||||
schoolName: 'SMA NEGERI 1 ABIANSEMAL',
|
||||
logoUrl: 'https://iili.io/KN7pUR2.png',
|
||||
faviconUrl: '',
|
||||
tahunAjaran: '2025/2026',
|
||||
margins: { top: 20, right: 20, bottom: 20, left: 20 }
|
||||
});
|
||||
} else {
|
||||
res.json(JSON.parse(rows[0].setting_value));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching settings:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil pengaturan' });
|
||||
}
|
||||
});
|
||||
|
||||
// Save settings
|
||||
app.post('/api/settings', async (req, res) => {
|
||||
try {
|
||||
const settings = req.body;
|
||||
const [existing] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
|
||||
|
||||
if (existing.length === 0) {
|
||||
await pool.query('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)',
|
||||
['app_settings', JSON.stringify(settings)]);
|
||||
} else {
|
||||
await pool.query('UPDATE settings SET setting_value = ? WHERE setting_key = ?',
|
||||
[JSON.stringify(settings), 'app_settings']);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Pengaturan berhasil disimpan' });
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal menyimpan pengaturan' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== STATISTICS API ====================
|
||||
|
||||
// Get student statistics
|
||||
app.get('/api/stats', async (req, res) => {
|
||||
try {
|
||||
const [total] = await pool.query('SELECT COUNT(*) as count FROM students');
|
||||
const [laki] = await pool.query("SELECT COUNT(*) as count FROM students WHERE jenis_kelamin = 'L'");
|
||||
const [perempuan] = await pool.query("SELECT COUNT(*) as count FROM students WHERE jenis_kelamin = 'P'");
|
||||
const [byTahun] = await pool.query(
|
||||
'SELECT tahun_ajaran, COUNT(*) as count FROM students GROUP BY tahun_ajaran ORDER BY tahun_ajaran DESC'
|
||||
);
|
||||
|
||||
res.json({
|
||||
total: total[0].count,
|
||||
laki: laki[0].count,
|
||||
perempuan: perempuan[0].count,
|
||||
byTahunAjaran: byTahun
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil statistik' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== LEGER API ====================
|
||||
|
||||
// Get all legers for a student
|
||||
app.get('/api/students/:id/legers', async (req, res) => {
|
||||
try {
|
||||
const studentId = req.params.id;
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, student_id, nis, nama, semester, file_name, file_size, uploaded_at, updated_at FROM student_legers WHERE student_id = ? ORDER BY semester ASC',
|
||||
[studentId]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching legers:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil data leger' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single leger with file data (for download/view)
|
||||
app.get('/api/students/:id/legers/:semester', async (req, res) => {
|
||||
try {
|
||||
const { id, semester } = req.params;
|
||||
const [rows] = await pool.query(
|
||||
'SELECT * FROM student_legers WHERE student_id = ? AND semester = ?',
|
||||
[id, semester]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ success: false, message: 'Leger tidak ditemukan' });
|
||||
}
|
||||
|
||||
res.json(rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching leger:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil data leger' });
|
||||
}
|
||||
});
|
||||
|
||||
// Upload/Update leger
|
||||
app.post('/api/students/:id/legers', async (req, res) => {
|
||||
try {
|
||||
const studentId = req.params.id;
|
||||
const { semester, fileName, fileData, fileSize, nis, nama } = req.body;
|
||||
|
||||
if (!semester || !fileName || !fileData) {
|
||||
return res.status(400).json({ success: false, message: 'Data tidak lengkap' });
|
||||
}
|
||||
|
||||
// Check if leger already exists for this student and semester
|
||||
const [existing] = await pool.query(
|
||||
'SELECT id FROM student_legers WHERE student_id = ? AND semester = ?',
|
||||
[studentId, semester]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Update existing
|
||||
await pool.query(
|
||||
'UPDATE student_legers SET file_name = ?, file_data = ?, file_size = ?, nis = ?, nama = ?, updated_at = NOW() WHERE student_id = ? AND semester = ?',
|
||||
[fileName, fileData, fileSize, nis, nama, studentId, semester]
|
||||
);
|
||||
res.json({ success: true, message: `Leger semester ${semester} berhasil diperbarui` });
|
||||
} else {
|
||||
// Insert new
|
||||
await pool.query(
|
||||
'INSERT INTO student_legers (student_id, nis, nama, semester, file_name, file_data, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[studentId, nis, nama, semester, fileName, fileData, fileSize]
|
||||
);
|
||||
res.json({ success: true, message: `Leger semester ${semester} berhasil diupload` });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading leger:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengupload leger: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete leger
|
||||
app.delete('/api/students/:id/legers/:semester', async (req, res) => {
|
||||
try {
|
||||
const { id, semester } = req.params;
|
||||
const [result] = await pool.query(
|
||||
'DELETE FROM student_legers WHERE student_id = ? AND semester = ?',
|
||||
[id, semester]
|
||||
);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: 'Leger tidak ditemukan' });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: `Leger semester ${semester} berhasil dihapus` });
|
||||
} catch (error) {
|
||||
console.error('Error deleting leger:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal menghapus leger' });
|
||||
}
|
||||
});
|
||||
|
||||
// Export all legers for a student as JSON (for backup)
|
||||
app.get('/api/students/:id/legers/export/all', async (req, res) => {
|
||||
try {
|
||||
const studentId = req.params.id;
|
||||
const [rows] = await pool.query(
|
||||
'SELECT * FROM student_legers WHERE student_id = ? ORDER BY semester ASC',
|
||||
[studentId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
studentId,
|
||||
legers: rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error exporting legers:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengekspor leger' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ==================== DOCUMENTS API ====================
|
||||
|
||||
// Get all documents for a student (optionally filter by type)
|
||||
app.get('/api/students/:id/documents', async (req, res) => {
|
||||
try {
|
||||
const studentId = req.params.id;
|
||||
const docType = req.query.type;
|
||||
|
||||
let query = 'SELECT id, student_id, nis, nama, doc_type, doc_name, file_name, file_size, uploaded_at, updated_at FROM student_documents WHERE student_id = ?';
|
||||
const params = [studentId];
|
||||
|
||||
if (docType) {
|
||||
query += ' AND doc_type = ?';
|
||||
params.push(docType);
|
||||
}
|
||||
|
||||
query += ' ORDER BY uploaded_at DESC';
|
||||
|
||||
const [rows] = await pool.query(query, params);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching documents:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil data dokumen' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single document with file data
|
||||
app.get('/api/students/:id/documents/:docId', async (req, res) => {
|
||||
try {
|
||||
const { id, docId } = req.params;
|
||||
const [rows] = await pool.query(
|
||||
'SELECT * FROM student_documents WHERE student_id = ? AND id = ?',
|
||||
[id, docId]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ success: false, message: 'Dokumen tidak ditemukan' });
|
||||
}
|
||||
|
||||
res.json(rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching document:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil data dokumen' });
|
||||
}
|
||||
});
|
||||
|
||||
// Upload document
|
||||
app.post('/api/students/:id/documents', async (req, res) => {
|
||||
try {
|
||||
const studentId = req.params.id;
|
||||
const { docType, docName, fileName, fileData, fileSize, nis, nama } = req.body;
|
||||
|
||||
if (!docType || !fileName || !fileData) {
|
||||
return res.status(400).json({ success: false, message: 'Data tidak lengkap' });
|
||||
}
|
||||
|
||||
// For ijazah, check if one already exists (only allow 1 ijazah per student)
|
||||
if (docType === 'ijazah') {
|
||||
const [existing] = await pool.query(
|
||||
'SELECT id FROM student_documents WHERE student_id = ? AND doc_type = ?',
|
||||
[studentId, 'ijazah']
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Update existing ijazah
|
||||
await pool.query(
|
||||
'UPDATE student_documents SET doc_name = ?, file_name = ?, file_data = ?, file_size = ?, nis = ?, nama = ?, updated_at = NOW() WHERE student_id = ? AND doc_type = ?',
|
||||
[docName || 'Ijazah', fileName, fileData, fileSize, nis, nama, studentId, 'ijazah']
|
||||
);
|
||||
return res.json({ success: true, message: 'Ijazah berhasil diperbarui' });
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new document
|
||||
await pool.query(
|
||||
'INSERT INTO student_documents (student_id, nis, nama, doc_type, doc_name, file_name, file_data, file_size) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[studentId, nis, nama, docType, docName || (docType === 'ijazah' ? 'Ijazah' : 'Sertifikat'), fileName, fileData, fileSize]
|
||||
);
|
||||
|
||||
const docLabel = docType === 'ijazah' ? 'Ijazah' : 'Sertifikat';
|
||||
res.json({ success: true, message: `${docLabel} berhasil diupload` });
|
||||
} catch (error) {
|
||||
console.error('Error uploading document:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengupload dokumen: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete document
|
||||
app.delete('/api/students/:id/documents/:docId', async (req, res) => {
|
||||
try {
|
||||
const { id, docId } = req.params;
|
||||
const [result] = await pool.query(
|
||||
'DELETE FROM student_documents WHERE student_id = ? AND id = ?',
|
||||
[id, docId]
|
||||
);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: 'Dokumen tidak ditemukan' });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Dokumen berhasil dihapus' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting document:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal menghapus dokumen' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== PUBLIC STUDENT ACCESS API ====================
|
||||
|
||||
// Search student by name and birth date (Public)
|
||||
app.post('/api/public/find-student', async (req, res) => {
|
||||
try {
|
||||
const { nama, tanggalLahir } = req.body;
|
||||
|
||||
if (!nama || !tanggalLahir) {
|
||||
return res.status(400).json({ success: false, message: 'Nama dan Tanggal Lahir harus diisi' });
|
||||
}
|
||||
|
||||
console.log(`Searching for: Name="${nama}", DOB="${tanggalLahir}"`);
|
||||
|
||||
// Helper to format date from YYYY-MM-DD to DD/MM/YYYY
|
||||
const formatDateDMY = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
// Helper to format date from YYYY-MM-DD to DD-MM-YYYY
|
||||
const formatDateDMYHyphen = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
}
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
const tanggalLahirDMY = formatDateDMY(tanggalLahir);
|
||||
const tanggalLahirDMYHyphen = formatDateDMYHyphen(tanggalLahir);
|
||||
|
||||
// Try multiple date formats
|
||||
// Robust search: Remove all spaces for name comparison to handle typo/spacing issues
|
||||
// AND try multiple date formats
|
||||
let query = `
|
||||
SELECT id, nis, nama, tahun_ajaran, tanggal_lahir
|
||||
FROM students
|
||||
WHERE
|
||||
LOWER(REPLACE(nama, ' ', '')) = LOWER(REPLACE(?, ' ', ''))
|
||||
AND (
|
||||
tanggal_lahir = ?
|
||||
OR tanggal_lahir = ?
|
||||
OR tanggal_lahir = ?
|
||||
)
|
||||
`;
|
||||
let params = [nama, tanggalLahir, tanggalLahirDMY, tanggalLahirDMYHyphen];
|
||||
|
||||
const [rows] = await pool.query(query, params);
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log('Search returned no results');
|
||||
|
||||
// Check for partial matches for debugging
|
||||
// Name match (ignoring spaces)
|
||||
const [nameCheck] = await pool.query(
|
||||
"SELECT count(*) as count FROM students WHERE LOWER(REPLACE(nama, ' ', '')) = LOWER(REPLACE(?, ' ', ''))",
|
||||
[nama]
|
||||
);
|
||||
|
||||
// Date match (any format)
|
||||
const [dateCheck] = await pool.query(
|
||||
"SELECT count(*) as count FROM students WHERE tanggal_lahir = ? OR tanggal_lahir = ? OR tanggal_lahir = ?",
|
||||
[tanggalLahir, tanggalLahirDMY, tanggalLahirDMYHyphen]
|
||||
);
|
||||
|
||||
const debugInfo = {
|
||||
nameMatches: nameCheck[0].count,
|
||||
dateMatches: dateCheck[0].count,
|
||||
searchedName: nama,
|
||||
searchedDate: tanggalLahir,
|
||||
searchedDateDMY: tanggalLahirDMY,
|
||||
searchedDateDMYHyphen: tanggalLahirDMYHyphen
|
||||
};
|
||||
|
||||
console.log('Debug info:', debugInfo);
|
||||
|
||||
let message = 'Data tidak ditemukan.';
|
||||
if (nameCheck[0].count > 0 && dateCheck[0].count === 0) {
|
||||
message = 'Nama siswa ditemukan, tetapi tanggal lahir tidak cocok. Pastikan tanggal lahir sesuai data sekolah.';
|
||||
} else if (nameCheck[0].count === 0 && dateCheck[0].count > 0) {
|
||||
message = 'Tanggal lahir valid, tetapi nama tidak ditemukan. Periksa penulisan nama (sesuai Ijazah/Akta).';
|
||||
} else {
|
||||
message = 'Data siswa tidak ditemukan. Periksa Nama dan Tanggal Lahir.';
|
||||
}
|
||||
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message,
|
||||
debug: debugInfo
|
||||
});
|
||||
}
|
||||
|
||||
// Jika ditemukan, kembalikan data minimal (NIS & ID) untuk akses selanjutnya
|
||||
res.json({
|
||||
success: true,
|
||||
student: convertKeysToCamel(rows[0])
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Public search error:', error);
|
||||
res.status(500).json({ success: false, message: 'Terjadi kesalahan pada server' });
|
||||
}
|
||||
});
|
||||
|
||||
// Health Check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
env: process.env.NODE_ENV,
|
||||
db_name: process.env.DB_NAME
|
||||
});
|
||||
});
|
||||
|
||||
// Get student detail for public view (requires student ID)
|
||||
app.get('/api/public/students/:id', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM students WHERE id = ?', [req.params.id]);
|
||||
if (rows.length === 0) return res.status(404).json({ message: 'Siswa tidak ditemukan' });
|
||||
res.json(convertKeysToCamel(rows[0]));
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== ACADEMIC YEARS API ====================
|
||||
|
||||
// Get all years
|
||||
app.get('/api/years', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM academic_years ORDER BY year_name DESC');
|
||||
res.json(rows.map(convertKeysToCamel));
|
||||
} catch (error) {
|
||||
console.error('Error fetching years:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil data tahun ajaran' });
|
||||
}
|
||||
});
|
||||
|
||||
// Add year
|
||||
app.post('/api/years', async (req, res) => {
|
||||
try {
|
||||
const { yearName } = req.body;
|
||||
await pool.query('INSERT INTO academic_years (year_name) VALUES (?)', [yearName]);
|
||||
res.json({ success: true, message: 'Tahun ajaran berhasil ditambah' });
|
||||
} catch (error) {
|
||||
console.error('Error adding year:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal menambah tahun ajaran' });
|
||||
}
|
||||
});
|
||||
|
||||
// Set active year
|
||||
app.post('/api/years/:id/active', async (req, res) => {
|
||||
try {
|
||||
const yearId = req.params.id;
|
||||
// Set all to false
|
||||
await pool.query('UPDATE academic_years SET is_active = FALSE');
|
||||
// Set target to true
|
||||
await pool.query('UPDATE academic_years SET is_active = TRUE WHERE id = ?', [yearId]);
|
||||
|
||||
// Also sync to global settings for backward compatibility/quick access
|
||||
const [yearRows] = await pool.query('SELECT year_name FROM academic_years WHERE id = ?', [yearId]);
|
||||
if (yearRows.length > 0) {
|
||||
const newYear = yearRows[0].year_name;
|
||||
const [settingsRows] = await pool.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
|
||||
if (settingsRows.length > 0) {
|
||||
const currentSettings = JSON.parse(settingsRows[0].setting_value);
|
||||
currentSettings.tahunAjaran = newYear;
|
||||
await pool.query('UPDATE settings SET setting_value = ? WHERE setting_key = ?',
|
||||
[JSON.stringify(currentSettings), 'app_settings']);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Tahun ajaran aktif berhasil diubah' });
|
||||
} catch (error) {
|
||||
console.error('Error setting active year:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengubah tahun ajaran aktif' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete year
|
||||
app.delete('/api/years/:id', async (req, res) => {
|
||||
try {
|
||||
const yearId = req.params.id;
|
||||
const [year] = await pool.query('SELECT is_active FROM academic_years WHERE id = ?', [yearId]);
|
||||
|
||||
if (year.length > 0 && year[0].is_active) {
|
||||
return res.status(400).json({ success: false, message: 'Tidak dapat menghapus tahun ajaran yang sedang aktif' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM academic_years WHERE id = ?', [yearId]);
|
||||
res.json({ success: true, message: 'Tahun ajaran berhasil dihapus' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting year:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal menghapus tahun ajaran' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== USER MANAGEMENT API ====================
|
||||
|
||||
// Get all users
|
||||
app.get('/api/users', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT id, username, full_name, role, created_at FROM users ORDER BY full_name ASC');
|
||||
res.json(rows.map(convertKeysToCamel));
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal mengambil data pengguna' });
|
||||
}
|
||||
});
|
||||
|
||||
// Add new user
|
||||
app.post('/api/users', async (req, res) => {
|
||||
try {
|
||||
const { username, password, fullName, role } = req.body;
|
||||
|
||||
if (!username || !password || !fullName || !role) {
|
||||
return res.status(400).json({ success: false, message: 'Semua data harus diisi' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO users (username, password, full_name, role) VALUES (?, ?, ?, ?)',
|
||||
[username, hashedPassword, fullName, role]
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Pengguna berhasil ditambahkan' });
|
||||
} catch (error) {
|
||||
console.error('Error adding user:', error);
|
||||
if (error.code === 'ER_DUP_ENTRY') {
|
||||
res.status(400).json({ success: false, message: 'Username sudah digunakan' });
|
||||
} else {
|
||||
res.status(500).json({ success: false, message: 'Gagal menambah pengguna' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update user
|
||||
app.put('/api/users/:id', async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { username, password, fullName, role } = req.body;
|
||||
|
||||
if (!username || !fullName || !role) {
|
||||
return res.status(400).json({ success: false, message: 'Data nama, username dan role harus diisi' });
|
||||
}
|
||||
|
||||
// Check if username exists for OTHER users
|
||||
const [existing] = await pool.query('SELECT id FROM users WHERE username = ? AND id != ?', [username, userId]);
|
||||
if (existing.length > 0) {
|
||||
return res.status(400).json({ success: false, message: 'Username sudah digunakan oleh pengguna lain' });
|
||||
}
|
||||
|
||||
if (password && password.trim() !== '') {
|
||||
// Update with password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
await pool.query(
|
||||
'UPDATE users SET username = ?, password = ?, full_name = ?, role = ? WHERE id = ?',
|
||||
[username, hashedPassword, fullName, role, userId]
|
||||
);
|
||||
} else {
|
||||
// Update without password
|
||||
await pool.query(
|
||||
'UPDATE users SET username = ?, full_name = ?, role = ? WHERE id = ?',
|
||||
[username, fullName, role, userId]
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Data pengguna berhasil diperbarui' });
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal memperbarui pengguna' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete user
|
||||
app.delete('/api/users/:id', async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
|
||||
// Prevent deleting the last admin or the currently logged in user (would need a better check but for now simpler)
|
||||
const [userRows] = await pool.query('SELECT username FROM users WHERE id = ?', [userId]);
|
||||
if (userRows.length > 0 && userRows[0].username === 'Kesiswaan') {
|
||||
return res.status(400).json({ success: false, message: 'Account utama tidak dapat dihapus' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM users WHERE id = ?', [userId]);
|
||||
res.json({ success: true, message: 'Pengguna berhasil dihapus' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
res.status(500).json({ success: false, message: 'Gagal menghapus pengguna' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== STATIC FILES ====================
|
||||
|
||||
// Serve static files from the dist directory
|
||||
app.use(express.static(path.join(__dirname, 'dist')));
|
||||
|
||||
// Handle SPA routing
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||
});
|
||||
|
||||
// ==================== START SERVER ====================
|
||||
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// Initialize database
|
||||
await initDatabase();
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`\n🚀 Server is running on http://localhost:${PORT}`);
|
||||
console.log(`📦 Database: ${process.env.DB_NAME || 'db_bukuiduk'}`);
|
||||
console.log(`🌐 Environment: Node.js ${process.version}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
startServer();
|
||||
BIN
buku-induk-sma-negeri-1-abiansemal/server/._db.js
Normal file
BIN
buku-induk-sma-negeri-1-abiansemal/server/._db.js
Normal file
Binary file not shown.
291
buku-induk-sma-negeri-1-abiansemal/server/db.js
Normal file
291
buku-induk-sma-negeri-1-abiansemal/server/db.js
Normal file
@@ -0,0 +1,291 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Create connection pool
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASS || '',
|
||||
database: process.env.DB_NAME || 'db_bukuiduk',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
charset: 'utf8mb4'
|
||||
});
|
||||
|
||||
// Initialize database and tables
|
||||
export const initDatabase = async () => {
|
||||
let conn;
|
||||
try {
|
||||
// First, create connection without database to create it if needed
|
||||
const initPool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASS || '',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 2,
|
||||
});
|
||||
|
||||
conn = await initPool.getConnection();
|
||||
|
||||
// Create database if not exists
|
||||
await conn.query(`CREATE DATABASE IF NOT EXISTS \`${process.env.DB_NAME || 'db_bukuiduk'}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
||||
console.log(`✓ Database '${process.env.DB_NAME || 'db_bukuiduk'}' ready`);
|
||||
|
||||
await conn.release();
|
||||
await initPool.end();
|
||||
|
||||
// Now create tables using main pool
|
||||
const mainConn = await pool.getConnection();
|
||||
|
||||
// Users table
|
||||
await mainConn.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(255),
|
||||
role ENUM('admin', 'operator') DEFAULT 'operator',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✓ Table users ready');
|
||||
|
||||
// Students table with all fields from the application
|
||||
await mainConn.query(`
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nis VARCHAR(50) NOT NULL,
|
||||
nama VARCHAR(255) NOT NULL,
|
||||
tahun_ajaran VARCHAR(20),
|
||||
foto_diterima_url LONGTEXT,
|
||||
foto_lulus_url LONGTEXT,
|
||||
|
||||
-- A. Keterangan Diri
|
||||
nama_panggilan VARCHAR(100),
|
||||
jenis_kelamin ENUM('L', 'P') DEFAULT 'L',
|
||||
tempat_lahir VARCHAR(100),
|
||||
tanggal_lahir VARCHAR(50),
|
||||
agama VARCHAR(50),
|
||||
kewarganegaraan VARCHAR(100) DEFAULT 'Indonesia',
|
||||
anak_keberapa VARCHAR(20),
|
||||
jumlah_saudara_kandung VARCHAR(20),
|
||||
jumlah_saudara_tiri VARCHAR(20),
|
||||
status_yatim VARCHAR(50),
|
||||
bahasa_sehari_hari VARCHAR(100),
|
||||
|
||||
-- B. Tempat Tinggal
|
||||
alamat TEXT,
|
||||
no_telp VARCHAR(50),
|
||||
jenis_tempat_tinggal VARCHAR(100),
|
||||
jarak_ke_sekolah VARCHAR(50),
|
||||
|
||||
-- C. Kesehatan
|
||||
golongan_darah VARCHAR(10),
|
||||
penyakit VARCHAR(255),
|
||||
kelainan_jasmani VARCHAR(255),
|
||||
tinggi_berat VARCHAR(100),
|
||||
|
||||
-- D. Pendidikan
|
||||
pendidikan_asal VARCHAR(255),
|
||||
no_ijasah_asal VARCHAR(100),
|
||||
lama_belajar_asal VARCHAR(50),
|
||||
pindahan_dari VARCHAR(255),
|
||||
alasan_pindah TEXT,
|
||||
diterima_di_kelas VARCHAR(50),
|
||||
program_jurusan VARCHAR(100),
|
||||
tanggal_diterima VARCHAR(50),
|
||||
hobby VARCHAR(255),
|
||||
cita_cita VARCHAR(255),
|
||||
|
||||
-- E. Ayah
|
||||
ayah_nama VARCHAR(255),
|
||||
ayah_nik VARCHAR(50),
|
||||
ayah_tahun_lahir VARCHAR(20),
|
||||
ayah_agama VARCHAR(50),
|
||||
ayah_warga_negara VARCHAR(100),
|
||||
ayah_pendidikan VARCHAR(100),
|
||||
ayah_pekerjaan VARCHAR(100),
|
||||
ayah_penghasilan VARCHAR(100),
|
||||
ayah_alamat TEXT,
|
||||
ayah_no_telp VARCHAR(50),
|
||||
|
||||
-- F. Ibu
|
||||
ibu_nama VARCHAR(255),
|
||||
ibu_nik VARCHAR(50),
|
||||
ibu_tahun_lahir VARCHAR(20),
|
||||
ibu_agama VARCHAR(50),
|
||||
ibu_warga_negara VARCHAR(100),
|
||||
ibu_pendidikan VARCHAR(100),
|
||||
ibu_pekerjaan VARCHAR(100),
|
||||
ibu_penghasilan VARCHAR(100),
|
||||
ibu_alamat TEXT,
|
||||
ibu_no_telp VARCHAR(50),
|
||||
|
||||
-- G. Wali
|
||||
wali_nama VARCHAR(255),
|
||||
wali_nik VARCHAR(50),
|
||||
wali_tahun_lahir VARCHAR(20),
|
||||
wali_agama VARCHAR(50),
|
||||
wali_pendidikan VARCHAR(100),
|
||||
wali_pekerjaan VARCHAR(100),
|
||||
wali_penghasilan VARCHAR(100),
|
||||
|
||||
-- H. Beasiswa
|
||||
bea_siswa1_tahun VARCHAR(20),
|
||||
bea_siswa1_kelas VARCHAR(50),
|
||||
bea_siswa1_dari VARCHAR(255),
|
||||
bea_siswa2_tahun VARCHAR(20),
|
||||
bea_siswa2_kelas VARCHAR(50),
|
||||
bea_siswa2_dari VARCHAR(255),
|
||||
bea_siswa3_tahun VARCHAR(20),
|
||||
bea_siswa3_kelas VARCHAR(50),
|
||||
bea_siswa3_dari VARCHAR(255),
|
||||
|
||||
-- Meninggalkan Sekolah
|
||||
tanggal_keluar VARCHAR(50),
|
||||
alasan_keluar TEXT,
|
||||
tamat_belajar_tahun VARCHAR(20),
|
||||
no_ijasah_tamat VARCHAR(100),
|
||||
|
||||
-- I. Setelah Pendidikan
|
||||
pekerjaan_perusahaan VARCHAR(255),
|
||||
pekerjaan_bidang_usaha VARCHAR(255),
|
||||
pekerjaan_penghasilan VARCHAR(100),
|
||||
pekerjaan_sesuai_kompetensi VARCHAR(20),
|
||||
lanjut_perguruan_tinggi VARCHAR(255),
|
||||
lanjut_program_studi VARCHAR(255),
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_nis (nis),
|
||||
INDEX idx_nama (nama),
|
||||
INDEX idx_tahun_ajaran (tahun_ajaran)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✓ Table students ready');
|
||||
|
||||
// Settings table
|
||||
await mainConn.query(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
setting_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
setting_value LONGTEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✓ Table settings ready');
|
||||
|
||||
// Student Legers table for PDF semester files
|
||||
await mainConn.query(`
|
||||
CREATE TABLE IF NOT EXISTS student_legers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
student_id INT NOT NULL,
|
||||
nis VARCHAR(50) NOT NULL,
|
||||
nama VARCHAR(255) NOT NULL,
|
||||
semester INT NOT NULL CHECK (semester >= 1 AND semester <= 6),
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_data LONGTEXT NOT NULL,
|
||||
file_size INT,
|
||||
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_student_id (student_id),
|
||||
INDEX idx_nis (nis),
|
||||
INDEX idx_semester (semester),
|
||||
UNIQUE KEY unique_student_semester (student_id, semester),
|
||||
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✓ Table student_legers ready');
|
||||
|
||||
// Student Documents table for Ijazah and Sertifikat
|
||||
await mainConn.query(`
|
||||
CREATE TABLE IF NOT EXISTS student_documents (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
student_id INT NOT NULL,
|
||||
nis VARCHAR(50) NOT NULL,
|
||||
nama VARCHAR(255) NOT NULL,
|
||||
doc_type ENUM('ijazah', 'sertifikat') NOT NULL,
|
||||
doc_name VARCHAR(255),
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_data LONGTEXT NOT NULL,
|
||||
file_size INT,
|
||||
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_student_id (student_id),
|
||||
INDEX idx_doc_type (doc_type),
|
||||
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✓ Table student_documents ready');
|
||||
|
||||
// Insert default user if not exists
|
||||
const [existingUsers] = await mainConn.query('SELECT * FROM users WHERE username = ?', ['Kesiswaan']);
|
||||
if (existingUsers.length === 0) {
|
||||
const bcryptModule = await import('bcryptjs');
|
||||
const bcrypt = bcryptModule.default;
|
||||
const hashedPassword = await bcrypt.hash('Smanab#1', 10);
|
||||
await mainConn.query(
|
||||
'INSERT INTO users (username, password, full_name, role) VALUES (?, ?, ?, ?)',
|
||||
['Kesiswaan', hashedPassword, 'Administrator Kesiswaan', 'admin']
|
||||
);
|
||||
console.log('✓ Default user created: Kesiswaan');
|
||||
}
|
||||
|
||||
// Insert default settings if not exists
|
||||
const [existingSettings] = await mainConn.query('SELECT * FROM settings WHERE setting_key = ?', ['app_settings']);
|
||||
if (existingSettings.length === 0) {
|
||||
const defaultSettings = {
|
||||
schoolName: 'SMA NEGERI 1 ABIANSEMAL',
|
||||
logoUrl: 'https://iili.io/KN7pUR2.png',
|
||||
faviconUrl: '',
|
||||
tahunAjaran: '2025/2026',
|
||||
margins: { top: 20, right: 20, bottom: 20, left: 20 }
|
||||
};
|
||||
await mainConn.query(
|
||||
'INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)',
|
||||
['app_settings', JSON.stringify(defaultSettings)]
|
||||
);
|
||||
console.log('✓ Default settings created');
|
||||
}
|
||||
|
||||
// Academic Years table
|
||||
await mainConn.query(`
|
||||
CREATE TABLE IF NOT EXISTS academic_years (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
year_name VARCHAR(20) NOT NULL UNIQUE,
|
||||
is_active BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log('✓ Table academic_years ready');
|
||||
|
||||
// Insert default academic year if none exist
|
||||
const [existingYears] = await mainConn.query('SELECT * FROM academic_years');
|
||||
if (existingYears.length === 0) {
|
||||
await mainConn.query(
|
||||
'INSERT INTO academic_years (year_name, is_active) VALUES (?, ?)',
|
||||
['2025/2026', true]
|
||||
);
|
||||
console.log('✓ Default academic year created');
|
||||
}
|
||||
|
||||
mainConn.release();
|
||||
console.log('✓ Database initialization complete');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Database initialization error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default pool;
|
||||
Reference in New Issue
Block a user