Initial commit apps directory with .gitignore
This commit is contained in:
129
sarpras-sma-negeri-1-abiansemal/.gitignore
vendored
Normal file
129
sarpras-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
|
||||
5
sarpras-sma-negeri-1-abiansemal/metadata.json
Executable file
5
sarpras-sma-negeri-1-abiansemal/metadata.json
Executable 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": []
|
||||
}
|
||||
4357
sarpras-sma-negeri-1-abiansemal/package-lock.json
generated
Normal file
4357
sarpras-sma-negeri-1-abiansemal/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
sarpras-sma-negeri-1-abiansemal/package.json
Normal file
42
sarpras-sma-negeri-1-abiansemal/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
sarpras-sma-negeri-1-abiansemal/public/._images
Executable file
BIN
sarpras-sma-negeri-1-abiansemal/public/._images
Executable file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/public/images/._desktop.ini
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/public/images/._desktop.ini
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/public/images/._kop_surat.png
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/public/images/._kop_surat.png
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/public/images/._logo.png
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/public/images/._logo.png
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/public/images/._logo_pemprov.png
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/public/images/._logo_pemprov.png
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/public/images/desktop.ini
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/public/images/desktop.ini
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/public/images/kop_surat.png
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/public/images/kop_surat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
sarpras-sma-negeri-1-abiansemal/public/images/logo.png
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
BIN
sarpras-sma-negeri-1-abiansemal/public/images/logo_pemprov.png
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/public/images/logo_pemprov.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 447 KiB |
126
sarpras-sma-negeri-1-abiansemal/server.js
Normal file
126
sarpras-sma-negeri-1-abiansemal/server.js
Normal 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();
|
||||
BIN
sarpras-sma-negeri-1-abiansemal/server/._db.js
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/server/._db.js
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/server/._routes
Executable file
BIN
sarpras-sma-negeri-1-abiansemal/server/._routes
Executable file
Binary file not shown.
42
sarpras-sma-negeri-1-abiansemal/server/db.js
Normal file
42
sarpras-sma-negeri-1-abiansemal/server/db.js
Normal 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;
|
||||
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._auth.js
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._auth.js
Normal file
Binary file not shown.
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._inspections.js
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._inspections.js
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._inventory.js
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._inventory.js
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._master-data.js
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._master-data.js
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._persediaan.js
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._persediaan.js
Normal file
Binary file not shown.
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._settings.js
Normal file
BIN
sarpras-sma-negeri-1-abiansemal/server/routes/._settings.js
Normal file
Binary file not shown.
110
sarpras-sma-negeri-1-abiansemal/server/routes/auth.js
Normal file
110
sarpras-sma-negeri-1-abiansemal/server/routes/auth.js
Normal 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;
|
||||
194
sarpras-sma-negeri-1-abiansemal/server/routes/damage-reports.js
Normal file
194
sarpras-sma-negeri-1-abiansemal/server/routes/damage-reports.js
Normal 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;
|
||||
71
sarpras-sma-negeri-1-abiansemal/server/routes/inspections.js
Normal file
71
sarpras-sma-negeri-1-abiansemal/server/routes/inspections.js
Normal 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;
|
||||
334
sarpras-sma-negeri-1-abiansemal/server/routes/inventory.js
Normal file
334
sarpras-sma-negeri-1-abiansemal/server/routes/inventory.js
Normal 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;
|
||||
107
sarpras-sma-negeri-1-abiansemal/server/routes/master-data.js
Normal file
107
sarpras-sma-negeri-1-abiansemal/server/routes/master-data.js
Normal 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;
|
||||
499
sarpras-sma-negeri-1-abiansemal/server/routes/persediaan.js
Normal file
499
sarpras-sma-negeri-1-abiansemal/server/routes/persediaan.js
Normal 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;
|
||||
90
sarpras-sma-negeri-1-abiansemal/server/routes/settings.js
Normal file
90
sarpras-sma-negeri-1-abiansemal/server/routes/settings.js
Normal 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;
|
||||
Reference in New Issue
Block a user