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

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,5 @@
{
"name": "Sarpras SMA Negeri 1 Abiansemal",
"description": "Aplikasi Manajemen Sarana dan Prasarana SMA Negeri 1 Abiansemal. Fitur pelaporan kerusakan, inspeksi rutin, dan SOP digital.",
"requestFramePermissions": []
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
{
"name": "sarpras-smanab",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"start": "node server.js"
},
"dependencies": {
"clsx": "^2.1.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^4.19.2",
"fflate": "^0.8.2",
"html2pdf.js": "^0.10.1",
"html5-qrcode": "^2.3.8",
"lucide-react": "^0.344.0",
"mysql2": "^3.16.0",
"node-fetch": "^3.3.2",
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"recharts": "^2.12.2",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/node": "^20.11.24",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.18",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.1.6"
}
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

View File

@@ -0,0 +1,126 @@
import express from 'express';
import path from 'path';
import cors from 'cors';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Import database and routes
import pool, { testConnection } from './server/db.js';
import authRoutes from './server/routes/auth.js';
import masterDataRoutes from './server/routes/master-data.js';
import settingsRoutes from './server/routes/settings.js';
import damageReportsRoutes from './server/routes/damage-reports.js';
import inspectionsRoutes from './server/routes/inspections.js';
import inventoryRoutes from './server/routes/inventory.js';
import persediaanRoutes from './server/routes/persediaan.js';
// Konfigurasi __dirname untuk ES Modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3005;
// Middleware
app.use(cors());
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
// Request Logger for Debugging
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// API Routes - Moved before static to prioritize matching
app.use('/api', authRoutes);
app.use('/api', masterDataRoutes);
app.use('/api', settingsRoutes);
app.use('/api', damageReportsRoutes);
app.use('/api', inspectionsRoutes);
app.use('/api', inventoryRoutes);
app.use('/api', persediaanRoutes);
// Menyajikan file statis dari folder 'public' (gambar lokal, dll)
app.use(express.static(path.join(__dirname, 'public')));
// Menyajikan file statis dari folder 'dist' (hasil build Vite)
app.use(express.static(path.join(__dirname, 'dist')));
// Image Proxy Endpoint - Bypass CORS for Google Drive images
app.get('/api/image-proxy', async (req, res) => {
const imageUrl = req.query.url;
if (!imageUrl) {
return res.status(400).send('Missing url parameter');
}
console.log('[Image Proxy] Fetching:', imageUrl);
try {
// Dynamic import for node-fetch (ESM compatible)
const fetch = (await import('node-fetch')).default;
const response = await fetch(imageUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9'
},
redirect: 'follow'
});
if (!response.ok) {
console.log('[Image Proxy] Failed with status:', response.status);
return res.status(response.status).send('Failed to fetch image');
}
const contentType = response.headers.get('content-type') || 'image/png';
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
console.log('[Image Proxy] Success, size:', buffer.length, 'bytes');
// Set CORS headers
res.set({
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=86400' // Cache for 1 day
});
res.send(buffer);
} catch (error) {
console.error('[Image Proxy] Error:', error.message);
res.status(500).send('Failed to proxy image: ' + error.message);
}
});
// Wildcard route untuk Single Page Application (SPA)
// Mengembalikan index.html untuk semua route yang tidak dikenali server
// Ini penting agar React Router menangani routing di sisi klien
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
// Start server with database connection test
const startServer = async () => {
// Test database connection
const dbConnected = await testConnection();
if (!dbConnected) {
console.warn('⚠️ Database connection failed. Server will start but API calls may fail.');
}
app.listen(PORT, () => {
console.log(`\n🚀 Server is running on port ${PORT}`);
console.log(`📍 Access locally at http://localhost:${PORT}`);
console.log(`🗄️ Database: ${process.env.DB_NAME || 'Not configured'}`);
});
};
startServer();

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,42 @@
// Database Connection Module - MySQL
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
// Create connection pool
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'sarpras',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0
});
// Test connection
export const testConnection = async () => {
try {
const connection = await pool.getConnection();
console.log('✅ Database connected successfully');
connection.release();
return true;
} catch (error) {
console.error('❌ Database connection failed:', error.message);
return false;
}
};
// Helper: Generate UUID
export const generateId = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
export default pool;

View File

@@ -0,0 +1,110 @@
// Auth Routes - Login & User Management
import express from 'express';
import pool, { generateId } from '../db.js';
const router = express.Router();
// POST /api/login
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.json({ status: 'error', message: 'Username dan password harus diisi' });
}
const [rows] = await pool.query(
'SELECT id, username, fullname, role, allowed_menus FROM users WHERE username = ? AND password = ?',
[username, password]
);
if (rows.length === 0) {
return res.json({ status: 'error', message: 'Username atau password salah' });
}
const user = rows[0];
// Parse allowed_menus if it's a string
if (typeof user.allowed_menus === 'string') {
try {
user.allowed_menus = JSON.parse(user.allowed_menus);
} catch (e) {
user.allowed_menus = ['/*'];
}
}
res.json({ status: 'success', data: user });
} catch (error) {
console.error('Login error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/getUsers
router.post('/getUsers', async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT id, username, fullname, role, allowed_menus FROM users ORDER BY id'
);
// Parse allowed_menus for each user
const users = rows.map(user => ({
...user,
allowed_menus: typeof user.allowed_menus === 'string'
? JSON.parse(user.allowed_menus)
: user.allowed_menus
}));
res.json({ status: 'success', data: users });
} catch (error) {
console.error('Get users error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/saveUser
router.post('/saveUser', async (req, res) => {
try {
const { id, username, password, fullname, role, allowed_menus } = req.body;
const menusJson = JSON.stringify(allowed_menus || ['/*']);
if (id) {
// Update existing user
if (password) {
await pool.query(
'UPDATE users SET username = ?, password = ?, fullname = ?, role = ?, allowed_menus = ? WHERE id = ?',
[username, password, fullname, role, menusJson, id]
);
} else {
await pool.query(
'UPDATE users SET username = ?, fullname = ?, role = ?, allowed_menus = ? WHERE id = ?',
[username, fullname, role, menusJson, id]
);
}
res.json({ status: 'success', message: 'User berhasil diperbarui', id });
} else {
// Create new user
const [result] = await pool.query(
'INSERT INTO users (username, password, fullname, role, allowed_menus) VALUES (?, ?, ?, ?, ?)',
[username, password || 'password123', fullname, role || 'Staf Sarpras', menusJson]
);
res.json({ status: 'success', message: 'User berhasil ditambahkan', id: result.insertId });
}
} catch (error) {
console.error('Save user error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/deleteUser
router.post('/deleteUser', async (req, res) => {
try {
const { id } = req.body;
await pool.query('DELETE FROM users WHERE id = ?', [id]);
res.json({ status: 'success', message: 'User berhasil dihapus' });
} catch (error) {
console.error('Delete user error:', error);
res.json({ status: 'error', message: error.message });
}
});
export default router;

View File

@@ -0,0 +1,194 @@
// Damage Reports & Maintenance Routes
import express from 'express';
import pool, { generateId } from '../db.js';
const router = express.Router();
// ==========================================
// DAMAGE REPORTS
// ==========================================
// POST /api/saveDamageReport
router.post('/saveDamageReport', async (req, res) => {
try {
const data = req.body;
const id = data.id || generateId();
if (data.id) {
// Update existing
await pool.query(
`UPDATE damage_reports SET
report_date = ?, reporter_name = ?, unit = ?, location = ?, item_name = ?,
inventory_code = ?, quantity = ?, description = ?, priority = ?,
has_photo = ?, photo_base64 = ?, photo_mime_type = ?, status = ?
WHERE id = ?`,
[
data.reportDate, data.reporterName, data.unit, data.location, data.itemName,
data.inventoryCode || null, data.quantity || 1, data.description, data.priority,
data.hasPhoto ? 1 : 0, data.photoBase64 || null, data.photoMimeType || null, data.status,
data.id
]
);
res.json({ status: 'success', message: 'Laporan berhasil diperbarui', id: data.id });
} else {
// Create new
await pool.query(
`INSERT INTO damage_reports
(id, report_date, reporter_name, unit, location, item_name, inventory_code,
quantity, description, priority, has_photo, photo_base64, photo_mime_type, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
id, data.reportDate, data.reporterName, data.unit, data.location, data.itemName,
data.inventoryCode || null, data.quantity || 1, data.description, data.priority,
data.hasPhoto ? 1 : 0, data.photoBase64 || null, data.photoMimeType || null,
data.status || 'Menunggu Verifikasi'
]
);
res.json({ status: 'success', message: 'Laporan berhasil disimpan', id });
}
} catch (error) {
console.error('Save damage report error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/updateReportStatus
router.post('/updateReportStatus', async (req, res) => {
try {
const { id, status } = req.body;
await pool.query('UPDATE damage_reports SET status = ? WHERE id = ?', [status, id]);
res.json({ status: 'success', message: 'Status berhasil diperbarui' });
} catch (error) {
console.error('Update report status error:', error);
res.json({ status: 'error', message: error.message });
}
});
// ==========================================
// MAINTENANCE RECORDS
// ==========================================
// POST /api/saveMaintenanceRecord
router.post('/saveMaintenanceRecord', async (req, res) => {
try {
const data = req.body;
await pool.query(
`INSERT INTO maintenance_records
(date, location, item_name, specification, work_type, materials_used, cost, executor, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
data.date, data.location, data.itemName, data.specification || '',
data.workType, data.materialsUsed || '', data.cost || 0,
data.executor || '', data.notes || ''
]
);
res.json({ status: 'success', message: 'Catatan pemeliharaan berhasil disimpan' });
} catch (error) {
console.error('Save maintenance record error:', error);
res.json({ status: 'error', message: error.message });
}
});
// ==========================================
// GET REPORTS (Unified)
// ==========================================
// POST /api/getReports
router.post('/getReports', async (req, res) => {
try {
const { type, startDate, endDate } = req.body;
let data = [];
if (type === 'damage') {
let query = 'SELECT * FROM damage_reports';
const params = [];
if (startDate && endDate) {
query += ' WHERE report_date BETWEEN ? AND ?';
params.push(startDate, endDate);
}
query += ' ORDER BY report_date DESC';
const [rows] = await pool.query(query, params);
data = rows.map(row => ({
id: row.id,
reportDate: row.report_date,
reporterName: row.reporter_name,
unit: row.unit,
location: row.location,
itemName: row.item_name,
inventoryCode: row.inventory_code,
quantity: row.quantity,
description: row.description,
priority: row.priority,
hasPhoto: row.has_photo === 1,
photoBase64: row.photo_base64,
photoMimeType: row.photo_mime_type,
status: row.status
}));
} else if (type === 'maintenance') {
let query = 'SELECT * FROM maintenance_records';
const params = [];
if (startDate && endDate) {
query += ' WHERE date BETWEEN ? AND ?';
params.push(startDate, endDate);
}
query += ' ORDER BY date DESC';
const [rows] = await pool.query(query, params);
data = rows.map(row => ({
date: row.date,
location: row.location,
itemName: row.item_name,
specification: row.specification,
workType: row.work_type,
materialsUsed: row.materials_used,
cost: parseFloat(row.cost),
executor: row.executor,
notes: row.notes
}));
} else if (type === 'inspection') {
let query = `
SELECT i.*, GROUP_CONCAT(
CONCAT(ii.item_name, ':', ii.condition, ':', IFNULL(ii.notes, ''))
SEPARATOR '||'
) as items
FROM inspections i
LEFT JOIN inspection_items ii ON i.id = ii.inspection_id
`;
const params = [];
if (startDate && endDate) {
query += ' WHERE i.inspection_date BETWEEN ? AND ?';
params.push(startDate, endDate);
}
query += ' GROUP BY i.id ORDER BY i.inspection_date DESC';
const [rows] = await pool.query(query, params);
data = rows.map(row => ({
id: row.id,
areaId: row.area_id,
areaName: row.area_name,
locationName: row.location_name,
inspectorName: row.inspector_name,
inspectionDate: row.inspection_date,
summary: row.summary,
status: row.status,
checks: row.items ? row.items.split('||').map(item => {
const [name, condition, notes] = item.split(':');
return { name, condition, notes };
}) : []
}));
}
res.json({ status: 'success', data });
} catch (error) {
console.error('Get reports error:', error);
res.json({ status: 'error', message: error.message });
}
});
export default router;

View File

@@ -0,0 +1,71 @@
// Inspections Routes
import express from 'express';
import pool, { generateId } from '../db.js';
const router = express.Router();
// POST /api/saveInspection
router.post('/saveInspection', async (req, res) => {
try {
const data = req.body;
const id = data.id || generateId();
// Insert/Update inspection header
await pool.query(
`INSERT INTO inspections (id, area_id, area_name, location_name, inspector_name, inspection_date, summary, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
area_name = VALUES(area_name),
location_name = VALUES(location_name),
inspector_name = VALUES(inspector_name),
summary = VALUES(summary),
status = VALUES(status)`,
[
id,
data.areaId,
data.areaName || '',
data.locationName || '',
data.inspectorName || '',
data.date || data.inspectionDate || new Date().toISOString().split('T')[0],
data.summary || '',
data.status || 'Pending'
]
);
// Delete existing items and re-insert
await pool.query('DELETE FROM inspection_items WHERE inspection_id = ?', [id]);
if (data.checks && data.checks.length > 0) {
const values = data.checks.map(check => [
id,
check.name || check.itemName,
check.condition || 'Baik',
check.notes || ''
]);
await pool.query(
`INSERT INTO inspection_items (inspection_id, item_name, \`condition\`, notes) VALUES ?`,
[values]
);
}
res.json({ status: 'success', message: 'Inspeksi berhasil disimpan', id });
} catch (error) {
console.error('Save inspection error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/updateInspectionStatus
router.post('/updateInspectionStatus', async (req, res) => {
try {
const { id, status } = req.body;
await pool.query('UPDATE inspections SET status = ? WHERE id = ?', [status, id]);
res.json({ status: 'success', message: 'Status inspeksi berhasil diperbarui' });
} catch (error) {
console.error('Update inspection status error:', error);
res.json({ status: 'error', message: error.message });
}
});
export default router;

View File

@@ -0,0 +1,334 @@
// Room Inventory (KIR) Routes
import express from 'express';
import pool, { generateId } from '../db.js';
const router = express.Router();
// POST /api/getRoomInventory
router.post('/getRoomInventory', async (req, res) => {
try {
const { roomName } = req.body;
// Get or create room
let [rooms] = await pool.query(
'SELECT id, room_name, location_code FROM room_inventory WHERE room_name = ?',
[roomName]
);
if (rooms.length === 0) {
// Create new room inventory
const [result] = await pool.query(
'INSERT INTO room_inventory (room_name) VALUES (?)',
[roomName]
);
rooms = [{ id: result.insertId, room_name: roomName, location_code: '' }];
}
const room = rooms[0];
// Get items
const [items] = await pool.query(
'SELECT * FROM inventory_items WHERE room_inventory_id = ? ORDER BY name',
[room.id]
);
const formattedItems = items.map(item => ({
id: item.id,
name: item.name,
brand: item.brand,
year: item.year,
code: item.code,
unit: item.unit,
price: parseFloat(item.price),
conditionGood: item.condition_good,
conditionLess: item.condition_less,
conditionBad: item.condition_bad,
notes: item.notes,
serialNumber: item.serial_number,
size: item.size,
material: item.material
}));
res.json({
status: 'success',
data: {
items: formattedItems,
locationCode: room.location_code || ''
}
});
} catch (error) {
console.error('Get room inventory error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/saveRoomInventory
router.post('/saveRoomInventory', async (req, res) => {
try {
const { roomName, locationCode, items } = req.body;
// Get or create room
let [rooms] = await pool.query(
'SELECT id FROM room_inventory WHERE room_name = ?',
[roomName]
);
let roomId;
if (rooms.length === 0) {
const [result] = await pool.query(
'INSERT INTO room_inventory (room_name, location_code) VALUES (?, ?)',
[roomName, locationCode || '']
);
roomId = result.insertId;
} else {
roomId = rooms[0].id;
await pool.query(
'UPDATE room_inventory SET location_code = ? WHERE id = ?',
[locationCode || '', roomId]
);
}
// Delete existing items
await pool.query('DELETE FROM inventory_items WHERE room_inventory_id = ?', [roomId]);
// Insert new items
if (items && items.length > 0) {
for (const item of items) {
const itemId = item.id || generateId();
await pool.query(
`INSERT INTO inventory_items
(id, room_inventory_id, name, brand, year, code, unit, price,
condition_good, condition_less, condition_bad, notes, serial_number, size, material)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
itemId, roomId, item.name, item.brand || '', item.year || '',
item.code || '', item.unit || 'Buah', item.price || 0,
item.conditionGood || 0, item.conditionLess || 0, item.conditionBad || 0,
item.notes || '', item.serialNumber || '', item.size || '', item.material || ''
]
);
}
}
res.json({ status: 'success', message: 'Inventaris ruangan berhasil disimpan' });
} catch (error) {
console.error('Save room inventory error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/lookupItemByQR
// Lookup an inventory item by code + year (from QR data format ITEM-{code}-{year}-{index})
router.post('/lookupItemByQR', async (req, res) => {
try {
const { code, year } = req.body;
if (!code || !year) {
return res.json({ status: 'error', message: 'Parameter code dan year wajib diisi.' });
}
const [items] = await pool.query(
`SELECT ii.*, ri.room_name
FROM inventory_items ii
JOIN room_inventory ri ON ii.room_inventory_id = ri.id
WHERE ii.code = ? AND ii.year = ?
LIMIT 10`,
[code, year]
);
if (items.length === 0) {
return res.json({ status: 'not_found', message: 'Barang tidak ditemukan.' });
}
const formattedItems = items.map(item => ({
id: item.id,
name: item.name,
brand: item.brand,
year: item.year,
code: item.code,
unit: item.unit,
price: parseFloat(item.price),
conditionGood: item.condition_good,
conditionLess: item.condition_less,
conditionBad: item.condition_bad,
notes: item.notes,
serialNumber: item.serial_number,
size: item.size,
material: item.material,
roomName: item.room_name
}));
res.json({ status: 'success', data: formattedItems });
} catch (error) {
console.error('Lookup item by QR error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/updateItemCondition
// Update condition counts for a single inventory item
router.post('/updateItemCondition', async (req, res) => {
try {
const { itemId, conditionGood, conditionLess, conditionBad } = req.body;
if (!itemId) {
return res.json({ status: 'error', message: 'Parameter itemId wajib diisi.' });
}
// Verify item exists
const [existing] = await pool.query('SELECT id FROM inventory_items WHERE id = ?', [itemId]);
if (existing.length === 0) {
return res.json({ status: 'error', message: 'Barang tidak ditemukan.' });
}
await pool.query(
`UPDATE inventory_items
SET condition_good = ?, condition_less = ?, condition_bad = ?
WHERE id = ?`,
[conditionGood || 0, conditionLess || 0, conditionBad || 0, itemId]
);
res.json({ status: 'success', message: 'Kondisi barang berhasil diperbarui.' });
} catch (error) {
console.error('Update item condition error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/getDamagedItems
// Get all items with condition_less > 0 or condition_bad > 0 in a specific room
router.post('/getDamagedItems', async (req, res) => {
try {
const { roomName } = req.body;
if (!roomName) {
return res.json({ status: 'error', message: 'Parameter roomName wajib diisi.' });
}
const [rooms] = await pool.query(
'SELECT id FROM room_inventory WHERE room_name = ?',
[roomName]
);
if (rooms.length === 0) {
return res.json({ status: 'success', data: [] });
}
const roomId = rooms[0].id;
const [items] = await pool.query(
`SELECT * FROM inventory_items
WHERE room_inventory_id = ? AND (condition_less > 0 OR condition_bad > 0)
ORDER BY name`,
[roomId]
);
const formattedItems = items.map(item => ({
id: item.id,
name: item.name,
brand: item.brand,
year: item.year,
code: item.code,
unit: item.unit,
price: parseFloat(item.price),
conditionGood: item.condition_good,
conditionLess: item.condition_less,
conditionBad: item.condition_bad,
notes: item.notes,
serialNumber: item.serial_number,
size: item.size,
material: item.material
}));
res.json({ status: 'success', data: formattedItems });
} catch (error) {
console.error('Get damaged items error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/getInventoryConditionStats
// Get summary of inventory condition across ALL rooms for dashboard
router.post('/getInventoryConditionStats', async (req, res) => {
try {
const [rows] = await pool.query(
`SELECT
COUNT(*) as totalItems,
SUM(condition_good) as totalGood,
SUM(condition_less) as totalLess,
SUM(condition_bad) as totalBad,
SUM(CASE WHEN condition_less > 0 OR condition_bad > 0 THEN 1 ELSE 0 END) as itemsNeedRepair
FROM inventory_items`
);
const data = rows[0] || {};
res.json({
status: 'success',
data: {
totalItems: parseInt(data.totalItems) || 0,
totalGood: parseInt(data.totalGood) || 0,
totalLess: parseInt(data.totalLess) || 0,
totalBad: parseInt(data.totalBad) || 0,
itemsNeedRepair: parseInt(data.itemsNeedRepair) || 0
}
});
} catch (error) {
console.error('Get inventory condition stats error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/getConditionByRoom
// Get condition breakdown grouped by room for dashboard charts
router.post('/getConditionByRoom', async (req, res) => {
try {
const [rows] = await pool.query(
`SELECT
ri.room_name,
SUM(ii.condition_good) as total_good,
SUM(ii.condition_less) as total_less,
SUM(ii.condition_bad) as total_bad,
COUNT(*) as item_count,
SUM(CASE WHEN ii.condition_less > 0 OR ii.condition_bad > 0 THEN 1 ELSE 0 END) as damaged_items
FROM inventory_items ii
JOIN room_inventory ri ON ii.room_inventory_id = ri.id
GROUP BY ri.room_name
ORDER BY (SUM(ii.condition_less) + SUM(ii.condition_bad)) DESC`
);
// Also get recent damaged items with room info
const [recentDamaged] = await pool.query(
`SELECT ii.name, ii.brand, ii.code, ii.condition_less, ii.condition_bad, ri.room_name
FROM inventory_items ii
JOIN room_inventory ri ON ii.room_inventory_id = ri.id
WHERE ii.condition_less > 0 OR ii.condition_bad > 0
ORDER BY (ii.condition_less + ii.condition_bad) DESC
LIMIT 10`
);
const roomData = rows.map(r => ({
roomName: r.room_name,
totalGood: parseInt(r.total_good) || 0,
totalLess: parseInt(r.total_less) || 0,
totalBad: parseInt(r.total_bad) || 0,
itemCount: parseInt(r.item_count) || 0,
damagedItems: parseInt(r.damaged_items) || 0
}));
const damagedList = recentDamaged.map(d => ({
name: d.name,
brand: d.brand || '',
code: d.code || '',
conditionLess: d.condition_less,
conditionBad: d.condition_bad,
roomName: d.room_name
}));
res.json({ status: 'success', data: { rooms: roomData, recentDamaged: damagedList } });
} catch (error) {
console.error('Get condition by room error:', error);
res.json({ status: 'error', message: error.message });
}
});
export default router;

View File

@@ -0,0 +1,107 @@
// Master Data Routes - Rooms, Classes, Labs, etc.
import express from 'express';
import pool from '../db.js';
const router = express.Router();
// POST /api/getMasterData
router.post('/getMasterData', async (req, res) => {
try {
const [rows] = await pool.query('SELECT category, value FROM master_data ORDER BY category, id');
const result = {
classes: [],
offices: [],
labs: [],
others: [],
items: []
};
rows.forEach(row => {
let cat = row.category;
// Map singular categories from DB to plural keys in response
if (cat === 'item') cat = 'items';
if (cat === 'class') cat = 'classes';
if (cat === 'office') cat = 'offices';
if (cat === 'lab') cat = 'labs';
if (cat === 'other') cat = 'others';
if (result[cat]) {
result[cat].push(row.value);
}
});
// Get signatories from settings
const [settings] = await pool.query(
"SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'headmaster%' OR setting_key LIKE 'wakasek%' OR setting_key LIKE 'assetAssistant%'"
);
const signatories = {
headmaster: { name: '', nip: '' },
wakasek: { name: '', nip: '' },
assetAssistant: { name: '', nip: '' }
};
settings.forEach(s => {
if (s.setting_key === 'headmaster_name') signatories.headmaster.name = s.setting_value;
if (s.setting_key === 'headmaster_nip') signatories.headmaster.nip = s.setting_value;
if (s.setting_key === 'wakasek_name') signatories.wakasek.name = s.setting_value;
if (s.setting_key === 'wakasek_nip') signatories.wakasek.nip = s.setting_value;
if (s.setting_key === 'assetAssistant_name') signatories.assetAssistant.name = s.setting_value;
if (s.setting_key === 'assetAssistant_nip') signatories.assetAssistant.nip = s.setting_value;
});
result.signatories = signatories;
res.json({ status: 'success', data: result });
} catch (error) {
console.error('Get master data error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/saveMasterData
router.post('/saveMasterData', async (req, res) => {
try {
const { category, value } = req.body;
// Check if exists
const [existing] = await pool.query(
'SELECT id FROM master_data WHERE category = ? AND value = ?',
[category, value]
);
if (existing.length > 0) {
return res.json({ status: 'error', message: 'Data sudah ada' });
}
await pool.query(
'INSERT INTO master_data (category, value) VALUES (?, ?)',
[category, value]
);
res.json({ status: 'success', message: 'Data berhasil ditambahkan' });
} catch (error) {
console.error('Save master data error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/deleteMasterData
router.post('/deleteMasterData', async (req, res) => {
try {
const { category, value } = req.body;
await pool.query(
'DELETE FROM master_data WHERE category = ? AND value = ?',
[category, value]
);
res.json({ status: 'success', message: 'Data berhasil dihapus' });
} catch (error) {
console.error('Delete master data error:', error);
res.json({ status: 'error', message: error.message });
}
});
export default router;

View File

@@ -0,0 +1,499 @@
// Persediaan (Stock/Supply) Routes
import express from 'express';
import pool, { generateId } from '../db.js';
const router = express.Router();
// ==========================================
// MASTER BARANG
// ==========================================
// POST /api/getMasterBarang
router.post('/getMasterBarang', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM master_barang ORDER BY nama_barang');
const data = rows.map(row => ({
id: row.id,
namaBarang: row.nama_barang,
jenisKategori: row.jenis_kategori,
rincianSpesifikasi: row.rincian_spesifikasi,
satuan: row.satuan,
kodeBarcode: row.kode_barcode,
createdAt: row.created_at,
updatedAt: row.updated_at
}));
res.json({ status: 'success', data });
} catch (error) {
console.error('Get master barang error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/saveMasterBarang
router.post('/saveMasterBarang', async (req, res) => {
try {
const data = req.body;
if (data.id) {
// Update
await pool.query(
`UPDATE master_barang SET
nama_barang = ?, jenis_kategori = ?, rincian_spesifikasi = ?,
satuan = ?, kode_barcode = ?
WHERE id = ?`,
[data.namaBarang, data.jenisKategori, data.rincianSpesifikasi || '',
data.satuan, data.kodeBarcode || '', data.id]
);
res.json({ status: 'success', message: 'Data berhasil diperbarui', id: data.id });
} else {
// Create
const id = generateId();
await pool.query(
`INSERT INTO master_barang (id, nama_barang, jenis_kategori, rincian_spesifikasi, satuan, kode_barcode)
VALUES (?, ?, ?, ?, ?, ?)`,
[id, data.namaBarang, data.jenisKategori, data.rincianSpesifikasi || '',
data.satuan, data.kodeBarcode || '']
);
res.json({ status: 'success', message: 'Data berhasil ditambahkan', id });
}
} catch (error) {
console.error('Save master barang error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/deleteMasterBarang
router.post('/deleteMasterBarang', async (req, res) => {
try {
const { id } = req.body;
await pool.query('DELETE FROM master_barang WHERE id = ?', [id]);
res.json({ status: 'success', message: 'Data berhasil dihapus' });
} catch (error) {
console.error('Delete master barang error:', error);
res.json({ status: 'error', message: error.message });
}
});
// ==========================================
// SALDO AWAL
// ==========================================
// POST /api/getSaldoAwal
router.post('/getSaldoAwal', async (req, res) => {
try {
const { tahunAnggaran } = req.body;
const [rows] = await pool.query(
'SELECT * FROM saldo_awal WHERE tahun_anggaran = ?',
[tahunAnggaran]
);
const data = rows.map(row => ({
id: row.id,
barangId: row.barang_id,
tahunAnggaran: row.tahun_anggaran,
jumlahAwal: row.jumlah_awal,
hargaSatuan: parseFloat(row.harga_satuan),
totalHarga: parseFloat(row.total_harga),
tanggalInput: row.tanggal_input
}));
res.json({ status: 'success', data });
} catch (error) {
console.error('Get saldo awal error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/saveSaldoAwal
router.post('/saveSaldoAwal', async (req, res) => {
try {
const { items } = req.body;
for (const item of items) {
const totalHarga = (item.jumlahAwal || 0) * (item.hargaSatuan || 0);
await pool.query(
`INSERT INTO saldo_awal (id, barang_id, tahun_anggaran, jumlah_awal, harga_satuan, total_harga)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
jumlah_awal = VALUES(jumlah_awal),
harga_satuan = VALUES(harga_satuan),
total_harga = VALUES(total_harga)`,
[item.id || generateId(), item.barangId, item.tahunAnggaran,
item.jumlahAwal || 0, item.hargaSatuan || 0, totalHarga]
);
}
res.json({ status: 'success', message: 'Saldo awal berhasil disimpan' });
} catch (error) {
console.error('Save saldo awal error:', error);
res.json({ status: 'error', message: error.message });
}
});
// ==========================================
// TRANSAKSI MASUK
// ==========================================
// POST /api/getTransaksiMasuk
router.post('/getTransaksiMasuk', async (req, res) => {
try {
const { tahunAnggaran } = req.body;
let query = 'SELECT * FROM transaksi_masuk';
const params = [];
if (tahunAnggaran) {
query += ' WHERE YEAR(tanggal) = ?';
params.push(tahunAnggaran);
}
query += ' ORDER BY tanggal DESC';
const [rows] = await pool.query(query, params);
const data = rows.map(row => ({
id: row.id,
nomorBAST: row.nomor_bast,
tanggal: row.tanggal,
sumberDana: row.sumber_dana,
keterangan: row.keterangan,
totalNilai: parseFloat(row.total_nilai),
createdBy: row.created_by,
createdAt: row.created_at
}));
res.json({ status: 'success', data });
} catch (error) {
console.error('Get transaksi masuk error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/getTransaksiMasukDetail
router.post('/getTransaksiMasukDetail', async (req, res) => {
try {
const { transaksiMasukId } = req.body;
const [rows] = await pool.query(
`SELECT d.*, m.nama_barang, m.satuan
FROM transaksi_masuk_detail d
LEFT JOIN master_barang m ON d.barang_id = m.id
WHERE d.transaksi_masuk_id = ?`,
[transaksiMasukId]
);
const data = rows.map(row => ({
id: row.id,
transaksiMasukId: row.transaksi_masuk_id,
barangId: row.barang_id,
jumlah: row.jumlah,
hargaSatuan: parseFloat(row.harga_satuan),
subTotal: parseFloat(row.sub_total),
namaBarang: row.nama_barang,
satuan: row.satuan
}));
res.json({ status: 'success', data });
} catch (error) {
console.error('Get transaksi masuk detail error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/saveTransaksiMasuk
router.post('/saveTransaksiMasuk', async (req, res) => {
try {
const { header, details } = req.body;
const transaksiId = generateId();
// Calculate total
let totalNilai = 0;
for (const detail of details) {
totalNilai += (detail.jumlah || 0) * (detail.hargaSatuan || 0);
}
// Insert header
await pool.query(
`INSERT INTO transaksi_masuk (id, nomor_bast, tanggal, sumber_dana, keterangan, total_nilai, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[transaksiId, header.nomorBAST || '', header.tanggal, header.sumberDana,
header.keterangan || '', totalNilai, header.createdBy || '']
);
// Insert details
for (const detail of details) {
await pool.query(
`INSERT INTO transaksi_masuk_detail (id, transaksi_masuk_id, barang_id, jumlah, harga_satuan, sub_total)
VALUES (?, ?, ?, ?, ?, ?)`,
[generateId(), transaksiId, detail.barangId, detail.jumlah || 0,
detail.hargaSatuan || 0, (detail.jumlah || 0) * (detail.hargaSatuan || 0)]
);
}
res.json({ status: 'success', message: 'Transaksi masuk berhasil disimpan', id: transaksiId });
} catch (error) {
console.error('Save transaksi masuk error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/deleteTransaksiMasuk
router.post('/deleteTransaksiMasuk', async (req, res) => {
try {
const { id } = req.body;
await pool.query('DELETE FROM transaksi_masuk WHERE id = ?', [id]);
res.json({ status: 'success', message: 'Transaksi berhasil dihapus' });
} catch (error) {
console.error('Delete transaksi masuk error:', error);
res.json({ status: 'error', message: error.message });
}
});
// ==========================================
// TRANSAKSI KELUAR
// ==========================================
// POST /api/getTransaksiKeluar
router.post('/getTransaksiKeluar', async (req, res) => {
try {
const { tahunAnggaran } = req.body;
let query = `
SELECT k.*, m.nama_barang, m.satuan
FROM transaksi_keluar k
LEFT JOIN master_barang m ON k.barang_id = m.id
`;
const params = [];
if (tahunAnggaran) {
query += ' WHERE YEAR(k.tanggal) = ?';
params.push(tahunAnggaran);
}
query += ' ORDER BY k.tanggal DESC';
const [rows] = await pool.query(query, params);
const data = rows.map(row => ({
id: row.id,
tanggal: row.tanggal,
barangId: row.barang_id,
jumlah: row.jumlah,
penerima: row.penerima,
keterangan: row.keterangan,
createdBy: row.created_by,
createdAt: row.created_at,
namaBarang: row.nama_barang,
satuan: row.satuan
}));
res.json({ status: 'success', data });
} catch (error) {
console.error('Get transaksi keluar error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/saveTransaksiKeluar
router.post('/saveTransaksiKeluar', async (req, res) => {
try {
const { items } = req.body;
for (const item of items) {
await pool.query(
`INSERT INTO transaksi_keluar (id, tanggal, barang_id, jumlah, penerima, keterangan, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[generateId(), item.tanggal, item.barangId, item.jumlah || 0,
item.penerima || '', item.keterangan || '', item.createdBy || '']
);
}
res.json({ status: 'success', message: 'Transaksi keluar berhasil disimpan' });
} catch (error) {
console.error('Save transaksi keluar error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/deleteTransaksiKeluar
router.post('/deleteTransaksiKeluar', async (req, res) => {
try {
const { id } = req.body;
await pool.query('DELETE FROM transaksi_keluar WHERE id = ?', [id]);
res.json({ status: 'success', message: 'Transaksi berhasil dihapus' });
} catch (error) {
console.error('Delete transaksi keluar error:', error);
res.json({ status: 'error', message: error.message });
}
});
// ==========================================
// LAPORAN STOK
// ==========================================
// POST /api/getLaporanStok
router.post('/getLaporanStok', async (req, res) => {
try {
const { tahunAnggaran } = req.body;
const [masterBarang] = await pool.query('SELECT * FROM master_barang');
const [saldoAwal] = await pool.query(
'SELECT * FROM saldo_awal WHERE tahun_anggaran = ?', [tahunAnggaran]
);
const [transaksiMasukDetail] = await pool.query(`
SELECT d.barang_id, SUM(d.jumlah) as total_masuk
FROM transaksi_masuk_detail d
JOIN transaksi_masuk t ON d.transaksi_masuk_id = t.id
WHERE YEAR(t.tanggal) = ?
GROUP BY d.barang_id
`, [tahunAnggaran]);
const [transaksiKeluar] = await pool.query(`
SELECT barang_id, SUM(jumlah) as total_keluar
FROM transaksi_keluar
WHERE YEAR(tanggal) = ?
GROUP BY barang_id
`, [tahunAnggaran]);
const data = masterBarang.map(barang => {
const saldo = saldoAwal.find(s => s.barang_id === barang.id);
const masuk = transaksiMasukDetail.find(m => m.barang_id === barang.id);
const keluar = transaksiKeluar.find(k => k.barang_id === barang.id);
const saldoAwalValue = saldo ? saldo.jumlah_awal : 0;
const totalMasuk = masuk ? parseInt(masuk.total_masuk) : 0;
const totalKeluar = keluar ? parseInt(keluar.total_keluar) : 0;
const stokAkhir = saldoAwalValue + totalMasuk - totalKeluar;
return {
barangId: barang.id,
namaBarang: barang.nama_barang,
jenisKategori: barang.jenis_kategori,
satuan: barang.satuan,
kodeBarcode: barang.kode_barcode,
saldoAwal: saldoAwalValue,
totalMasuk,
totalKeluar,
stokAkhir,
status: stokAkhir <= 0 ? 'HABIS' : stokAkhir < 10 ? 'RENDAH' : 'NORMAL'
};
});
res.json({ status: 'success', data });
} catch (error) {
console.error('Get laporan stok error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/getMutasiBarang
router.post('/getMutasiBarang', async (req, res) => {
try {
const { tahunAnggaran, startDate, endDate } = req.body;
const [masterBarang] = await pool.query('SELECT * FROM master_barang');
const [saldoAwal] = await pool.query(
'SELECT * FROM saldo_awal WHERE tahun_anggaran = ?', [tahunAnggaran]
);
// Get masuk grouped by sumber dana
const [transaksiMasuk] = await pool.query(`
SELECT d.barang_id, t.sumber_dana, SUM(d.jumlah) as jumlah
FROM transaksi_masuk_detail d
JOIN transaksi_masuk t ON d.transaksi_masuk_id = t.id
WHERE t.tanggal BETWEEN ? AND ?
GROUP BY d.barang_id, t.sumber_dana
`, [startDate, endDate]);
const [transaksiKeluar] = await pool.query(`
SELECT barang_id, SUM(jumlah) as total_keluar
FROM transaksi_keluar
WHERE tanggal BETWEEN ? AND ?
GROUP BY barang_id
`, [startDate, endDate]);
const data = masterBarang.map(barang => {
const saldo = saldoAwal.find(s => s.barang_id === barang.id);
const masukItems = transaksiMasuk.filter(m => m.barang_id === barang.id);
const keluar = transaksiKeluar.find(k => k.barang_id === barang.id);
const saldoAwalValue = saldo ? saldo.jumlah_awal : 0;
const masukBOS = masukItems.find(m => m.sumber_dana === 'BOS')?.jumlah || 0;
const masukAPBD = masukItems.find(m => m.sumber_dana === 'APBD')?.jumlah || 0;
const masukKomite = masukItems.find(m => m.sumber_dana === 'Komite')?.jumlah || 0;
const masukHibah = masukItems.find(m => m.sumber_dana === 'Hibah')?.jumlah || 0;
const totalMasuk = parseInt(masukBOS) + parseInt(masukAPBD) + parseInt(masukKomite) + parseInt(masukHibah);
const totalKeluar = keluar ? parseInt(keluar.total_keluar) : 0;
const saldoAkhir = saldoAwalValue + totalMasuk - totalKeluar;
return {
barangId: barang.id,
namaBarang: barang.nama_barang,
satuan: barang.satuan,
saldoAwalPeriode: saldoAwalValue,
masukBOS: parseInt(masukBOS),
masukAPBD: parseInt(masukAPBD),
masukKomite: parseInt(masukKomite),
masukHibah: parseInt(masukHibah),
totalMasuk,
totalKeluar,
saldoAkhir
};
});
res.json({ status: 'success', data });
} catch (error) {
console.error('Get mutasi barang error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/getRiwayatMutasi
router.post('/getRiwayatMutasi', async (req, res) => {
try {
const { barangId, tahunAnggaran } = req.body;
// Get all transactions for this item
const [masuk] = await pool.query(`
SELECT t.tanggal, CONCAT('Masuk - ', t.sumber_dana, ': ', t.keterangan) as keterangan,
d.jumlah as masuk, 0 as keluar
FROM transaksi_masuk_detail d
JOIN transaksi_masuk t ON d.transaksi_masuk_id = t.id
WHERE d.barang_id = ? AND YEAR(t.tanggal) = ?
`, [barangId, tahunAnggaran]);
const [keluar] = await pool.query(`
SELECT tanggal, CONCAT('Keluar - ', penerima, ': ', keterangan) as keterangan,
0 as masuk, jumlah as keluar
FROM transaksi_keluar
WHERE barang_id = ? AND YEAR(tanggal) = ?
`, [barangId, tahunAnggaran]);
// Combine and sort by date
const transactions = [...masuk, ...keluar].sort((a, b) =>
new Date(a.tanggal) - new Date(b.tanggal)
);
// Get initial saldo
const [saldo] = await pool.query(
'SELECT jumlah_awal FROM saldo_awal WHERE barang_id = ? AND tahun_anggaran = ?',
[barangId, tahunAnggaran]
);
let runningBalance = saldo.length > 0 ? saldo[0].jumlah_awal : 0;
const data = transactions.map(t => {
runningBalance = runningBalance + t.masuk - t.keluar;
return {
tanggal: t.tanggal,
keterangan: t.keterangan,
masuk: t.masuk,
keluar: t.keluar,
saldo: runningBalance
};
});
res.json({ status: 'success', data });
} catch (error) {
console.error('Get riwayat mutasi error:', error);
res.json({ status: 'error', message: error.message });
}
});
export default router;

View File

@@ -0,0 +1,90 @@
// Settings Routes
import express from 'express';
import pool from '../db.js';
const router = express.Router();
// POST /api/getSettings
router.post('/getSettings', async (req, res) => {
try {
const [rows] = await pool.query('SELECT setting_key, setting_value FROM settings');
const settings = {
appLogo: '',
logoPemprov: '/images/logo_pemprov.png',
kopSurat: '/images/kop_surat.png',
tahunAnggaran: new Date().getFullYear().toString(),
activeUserRole: 'Staf Sarpras',
activeUserName: 'Administrator',
headmaster: { name: '', nip: '' },
wakasek: { name: '', nip: '' },
assetAssistant: { name: '', nip: '' }
};
rows.forEach(row => {
const key = row.setting_key;
const value = row.setting_value;
if (key === 'appLogo') settings.appLogo = value;
else if (key === 'logoPemprov') settings.logoPemprov = value;
else if (key === 'kopSurat') settings.kopSurat = value;
else if (key === 'tahunAnggaran') settings.tahunAnggaran = value;
else if (key === 'activeUserRole') settings.activeUserRole = value;
else if (key === 'activeUserName') settings.activeUserName = value;
else if (key === 'headmaster_name') settings.headmaster.name = value;
else if (key === 'headmaster_nip') settings.headmaster.nip = value;
else if (key === 'wakasek_name') settings.wakasek.name = value;
else if (key === 'wakasek_nip') settings.wakasek.nip = value;
else if (key === 'assetAssistant_name') settings.assetAssistant.name = value;
else if (key === 'assetAssistant_nip') settings.assetAssistant.nip = value;
});
res.json({ status: 'success', data: settings });
} catch (error) {
console.error('Get settings error:', error);
res.json({ status: 'error', message: error.message });
}
});
// POST /api/saveSettings
router.post('/saveSettings', async (req, res) => {
try {
const settings = req.body;
const upsert = async (key, value) => {
await pool.query(
`INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)`,
[key, value || '']
);
};
// Save all settings
if (settings.appLogo !== undefined) await upsert('appLogo', settings.appLogo);
if (settings.logoPemprov !== undefined) await upsert('logoPemprov', settings.logoPemprov);
if (settings.kopSurat !== undefined) await upsert('kopSurat', settings.kopSurat);
if (settings.tahunAnggaran !== undefined) await upsert('tahunAnggaran', settings.tahunAnggaran);
if (settings.activeUserRole !== undefined) await upsert('activeUserRole', settings.activeUserRole);
if (settings.activeUserName !== undefined) await upsert('activeUserName', settings.activeUserName);
if (settings.headmaster) {
await upsert('headmaster_name', settings.headmaster.name);
await upsert('headmaster_nip', settings.headmaster.nip);
}
if (settings.wakasek) {
await upsert('wakasek_name', settings.wakasek.name);
await upsert('wakasek_nip', settings.wakasek.nip);
}
if (settings.assetAssistant) {
await upsert('assetAssistant_name', settings.assetAssistant.name);
await upsert('assetAssistant_nip', settings.assetAssistant.nip);
}
res.json({ status: 'success', message: 'Pengaturan berhasil disimpan' });
} catch (error) {
console.error('Save settings error:', error);
res.json({ status: 'error', message: error.message });
}
});
export default router;