import express from 'express'; import mysql from 'mysql2/promise'; import bcrypt from 'bcrypt'; import cors from 'cors'; import QRCode from 'qrcode'; // --- IMPORT UNTUK FILE UPLOAD --- import multer from 'multer'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); app.use(express.json()); app.use(cors()); // CORS aktif, React aman! // --- BUAT FOLDER UPLOADS SECARA OTOMATIS JIKALAU BELUM ADA --- const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir); } // --- AKSI STATISKAN FOLDER UPLOADS AGAR GAMBAR BISA DIAKSES FRONTEND --- app.use('/uploads', express.static(uploadDir)); // --- SETTING MULTER UNTUK MENANGKAP FOTO DARI FRONTEND --- const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, uploadDir); }, filename: (req, file, cb) => { const namaUnik = `foto-${Date.now()}${path.extname(file.originalname)}`; cb(null, namaUnik); } }); const upload = multer({ storage: storage }); // --- KONFIGURASI DATABASE CIFO --- const db = mysql.createPool({ host: "sql.cifo.co.id", port: 3307, user: "pkl_bast", password: "PklCifo2026", database: "pkl_bast", waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); // Cek Koneksi Database db.getConnection() .then(conn => { console.log("Koneksi CIFO Berhasil. Siap Tempur!"); conn.release(); }) .catch(err => { console.error("GAGAL KONEK:", err.message); }); // ======================================================= // 1. MODUL AUTHENTICATION (REGISTER & LOGIN) // ======================================================= // API REGISTER app.post("/api/register", async (req, res) => { const { nama_lengkap, username, password, role, email, lembaga } = req.body; if (!username || !password || !nama_lengkap) { return res.status(400).json({ success: false, message: "Nama, Username, dan Password wajib diisi Bang!" }); } try { const [cekUser] = await db.query("SELECT * FROM User WHERE username = ?", [username]); if (cekUser.length > 0) { return res.status(400).json({ success: false, message: "Username sudah ada, cari yang lain!" }); } const hashedPassword = await bcrypt.hash(password, 10); const sql = `INSERT INTO User (nama_lengkap, username, password, role, email, lembaga) VALUES (?, ?, ?, ?, ?, ?)`; await db.query(sql, [ nama_lengkap, username, hashedPassword, role || 'User', email || null, lembaga || null ]); res.json({ success: true, message: "User Berhasil Terdaftar! Silakan Login." }); } catch (error) { console.error(error); res.status(500).json({ success: false, error: error.message }); } }); // API LOGIN RESMI app.post('/api/login', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ success: false, message: "Username dan Password wajib diisi, Bang!" }); } try { const [rows] = await db.query("SELECT * FROM User WHERE username = ?", [username]); if (rows.length === 0) { return res.status(401).json({ success: false, message: "Username tidak ditemukan, Bang!" }); } const user = rows[0]; const passwordCocok = await bcrypt.compare(password, user.password); if (!passwordCocok) { return res.status(401).json({ success: false, message: "Password salah, Bang!" }); } const tokenKontrak = `SECRET_TOKEN_UKK_${user.user_id}_${Date.now()}`; res.json({ success: true, message: "Login Berhasil! Selamat Datang Kembali.", token: tokenKontrak, data: { user_id: user.user_id, username: user.username, role: user.role, nama_lengkap: user.nama_lengkap, email: user.email } }); } catch (error) { console.error(error); res.status(500).json({ success: false, error: error.message }); } }); // API GET PROFILE USER LOGIN app.get('/api/user/:id', async (req, res) => { const { id } = req.params; try { const [rows] = await db.query( ` SELECT user_id, nama_lengkap, username, role, email FROM User WHERE user_id = ? `, [id] ); if (rows.length === 0) { return res.status(404).json({ success: false, message: "User tidak ditemukan!" }); } res.json({ success: true, data: rows[0] }); } catch (error) { console.error(error); res.status(500).json({ success: false, error: error.message }); } }); // ======================================================= // 2. MODUL MANAJEMEN USER (CRUD USER) // ======================================================= app.get("/api/users", async (req, res) => { try { const [rows] = await db.query("SELECT user_id, nama_lengkap, username, role, email, lembaga FROM User"); res.json({ success: true, data: rows }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); app.put("/api/users/:id", async (req, res) => { const { id } = req.params; const { nama_lengkap, username, role, email, lembaga } = req.body; try { await db.query( "UPDATE User SET nama_lengkap = ?, username = ?, role = ?, email = ?, lembaga = ? WHERE user_id = ?", [nama_lengkap, username, role, email, lembaga, id] ); res.json({ success: true, message: "User Berhasil Diupdate!" }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); app.delete("/api/users/:id", async (req, res) => { const { id } = req.params; try { await db.query("DELETE FROM User WHERE user_id = ?", [id]); res.json({ success: true, message: "User Berhasil Dihapus!" }); } catch (error) { res.status(500).json({ success: false, message: "Gagal hapus! User mungkin terikat dengan data transaksi lain." }); } }); // ======================================================= // 3. MODUL MASTER DATA BARANG (CRUD BARANG) // ======================================================= app.get("/api/barang", async (req, res) => { try { const sql = ` SELECT b.barang_id, b.kode_barang, b.nama_barang, b.stok, b.status_barang, b.qr_image, b.foto_barang, k.nama_kategori, l.nama_lokasi FROM Barang b LEFT JOIN Kategori k ON b.kategori_id = k.kategori_id LEFT JOIN Lokasi l ON b.lokasi_id = l.lokasi_id ORDER BY b.barang_id DESC`; const [rows] = await db.query(sql); res.json({ success: true, data: rows }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // GET DETAIL BARANG KHUSUS UNTUK SCAN QR CODE app.get("/api/barang/detail/:kode", async (req, res) => { const { kode } = req.params; try { const sqlBarang = ` SELECT b.barang_id, b.kode_barang, b.nama_barang, b.stok, b.status_barang, b.qr_image, b.foto_barang, b.dibuat_pada, k.nama_kategori, l.nama_lokasi FROM Barang b LEFT JOIN Kategori k ON b.kategori_id = k.kategori_id LEFT JOIN Lokasi l ON b.lokasi_id = l.lokasi_id WHERE b.kode_barang = ? LIMIT 1`; const [rowsBarang] = await db.query(sqlBarang, [kode]); if (rowsBarang.length === 0) { return res.status(404).json({ success: false, message: "Barang tidak ditemukan!" }); } const barang = rowsBarang[0]; const sqlBast = ` SELECT b.bast_id, b.status_serah, b.status_terima, b.dibuat_pada, u1.nama_lengkap AS nama_penyerah, u2.nama_lengkap AS nama_penerima FROM BAST b LEFT JOIN User u1 ON b.user_serah_id = u1.user_id LEFT JOIN User u2 ON b.user_terima_id = u2.user_id WHERE b.barang_id = ? ORDER BY b.dibuat_pada DESC`; const [riwayatBast] = await db.query(sqlBast, [barang.barang_id]); res.json({ success: true, data: { ...barang, riwayat_bast: riwayatBast } }); } catch (error) { console.error("Error Detail Barang:", error); res.status(500).json({ success: false, error: error.message }); } }); // API POST: Tambah Barang Baru + Auto Create Kategori & Lokasi app.post("/api/barang", upload.single('foto_barang'), async (req, res) => { const { kode_barang, nama_barang, nama_kategori, nama_lokasi, status_barang, stok } = req.body; try { if (!kode_barang || !nama_barang || !nama_kategori || !nama_lokasi) { return res.status(400).json({ success: false, message: "Data nama, kategori, and lokasi wajib diisi lengkap, Bang!" }); } let urlFotoBaru = ""; if (req.file) { urlFotoBaru = `http://localhost:5000/uploads/${req.file.filename}`; } else if (req.body.foto_barang && req.body.foto_barang.startsWith('http')) { urlFotoBaru = req.body.foto_barang; } else { return res.status(400).json({ success: false, message: "File gambar foto_barang belum diupload, Bang!" }); } // --- AUTO-CREATE KATEGORI --- let finalKategoriId; const [cekKategori] = await db.query("SELECT kategori_id FROM Kategori WHERE LOWER(nama_kategori) = LOWER(?)", [nama_kategori.trim()]); if (cekKategori.length > 0) { finalKategoriId = cekKategori[0].kategori_id; } else { const namaKategoriBersih = nama_kategori.trim().replace(/[^a-zA-Z0-9 ]/g, "").toUpperCase(); const kataKategori = namaKategoriBersih.split(" ").filter(k => k.length > 0); let kodeKategoriOtomatis = ""; if (kataKategori.length >= 2) { kodeKategoriOtomatis = (kataKategori[0].slice(0, 2) + kataKategori[1].slice(0, 1)).slice(0, 3); } else { kodeKategoriOtomatis = namaKategoriBersih.slice(0, 3); } if (kodeKategoriOtomatis.length < 3) { kodeKategoriOtomatis = (kodeKategoriOtomatis + "XXX").slice(0, 3); } const [buatKategori] = await db.query( "INSERT INTO Kategori (kode_kategori, nama_kategori) VALUES (?, ?)", [kodeKategoriOtomatis, nama_kategori.trim()] ); finalKategoriId = buatKategori.insertId; } // --- AUTO-CREATE LOKASI --- let finalLokasiId; const [cekLokasi] = await db.query("SELECT lokasi_id FROM Lokasi WHERE LOWER(nama_lokasi) = LOWER(?)", [nama_lokasi.trim()]); if (cekLokasi.length > 0) { finalLokasiId = cekLokasi[0].lokasi_id; } else { const namaLokasiBersih = nama_lokasi.trim().replace(/[^a-zA-Z0-9 ]/g, "").toUpperCase(); const kataLokasi = namaLokasiBersih.split(" ").filter(l => l.length > 0); let kodeLokasiOtomatis = ""; if (kataLokasi.length >= 2) { kodeLokasiOtomatis = (kataLokasi[0].slice(0, 2) + kataLokasi[1].slice(0, 1)).slice(0, 3); } else { kodeLokasiOtomatis = namaLokasiBersih.slice(0, 3); } if (kodeLokasiOtomatis.length < 3) { kodeLokasiOtomatis = (kodeLokasiOtomatis + "XXX").slice(0, 3); } const [buatLokasi] = await db.query( "INSERT INTO Lokasi (kode_lokasi, nama_lokasi) VALUES (?, ?)", [kodeLokasiOtomatis, nama_lokasi.trim()] ); finalLokasiId = buatLokasi.insertId; } // 5. Generate QR Code Otomatis menggunakan IP Lokal agar bisa discan HP const qrUrl = `http://1.9.77.179:5173/detail/${encodeURIComponent(kode_barang)}`; const qrImage = await QRCode.toDataURL(qrUrl); // --- INSERT KE TABEL BARANG --- const sql = `INSERT INTO Barang (kode_barang, nama_barang, kategori_id, lokasi_id, status_barang, stok, qr_image, foto_barang) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`; await db.query(sql, [ kode_barang, nama_barang.trim(), Number(finalKategoriId), Number(finalLokasiId), status_barang || 'Baik', Number(stok) || 0, qrImage, urlFotoBaru ]); res.json({ success: true, message: "Mantap Bang! Data masuk beserta Kategori dan Lokasi otomatis, Foto Online, dan QR Code ke database CIFO.", kode_barang: kode_barang }); } catch (error) { console.error("Eror Backend Simpan Barang:", error); res.status(500).json({ success: false, message: "Gagal memproses data ke database!", error: error.message }); } }); // API PUT: Edit Data Barang app.put("/api/barang/:id", upload.single('foto_barang'), async (req, res) => { const barangId = req.params.id; const { nama_barang, stok, status_barang } = req.body; try { let sql = "UPDATE Barang SET nama_barang = ?, stok = ?, status_barang = ? WHERE barang_id = ?"; let params = [nama_barang, Number(stok), status_barang, barangId]; if (req.file) { const urlFotoBaru = `http://localhost:5000/uploads/${req.file.filename}`; sql = "UPDATE Barang SET nama_barang = ?, stok = ?, status_barang = ?, foto_barang = ? WHERE barang_id = ?"; params = [nama_barang, Number(stok), status_barang, urlFotoBaru, barangId]; } else if (req.body.foto_barang && req.body.foto_barang.startsWith('http')) { sql = "UPDATE Barang SET nama_barang = ?, stok = ?, status_barang = ?, foto_barang = ? WHERE barang_id = ?"; params = [nama_barang, Number(stok), status_barang, req.body.foto_barang, barangId]; } await db.query(sql, params); const [rows] = await db.query("SELECT * FROM Barang WHERE barang_id = ?", [barangId]); res.json({ success: true, message: "Perubahan data inventaris berhasil disimpan dengan sukses!", data: rows[0] }); } catch (error) { console.error("Error backend edit barang:", error); res.status(500).json({ success: false, message: "Gagal memproses data ke database!", error: error.message }); } }); // API DELETE: Hapus Barang app.delete("/api/barang/:id", async (req, res) => { const { id } = req.params; try { await db.query("DELETE FROM Barang WHERE barang_id = ?", [id]); res.json({ success: true, message: "Barang berhasil dihapus dari database!" }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // GET SEMUA KATEGORI app.get("/api/kategori", async (req, res) => { try { const [rows] = await db.query("SELECT * FROM Kategori"); res.json(rows); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // CREATE Kategori app.post("/api/kategori", async (req, res) => { const { nama_kategori } = req.body; if (!nama_kategori) return res.status(400).json({ success: false, message: "Nama kategori wajib" }); try { const [result] = await db.query("INSERT INTO Kategori (nama_kategori) VALUES (?)", [nama_kategori]); res.json({ success: true, message: "Kategori berhasil ditambah", id: result.insertId }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // UPDATE Kategori app.put("/api/kategori/:id", async (req, res) => { const { id } = req.params; const { nama_kategori } = req.body; if (!nama_kategori) return res.status(400).json({ success: false, message: "Nama kategori wajib" }); try { await db.query("UPDATE Kategori SET nama_kategori = ? WHERE kategori_id = ?", [nama_kategori, id]); res.json({ success: true, message: "Kategori berhasil diupdate" }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // DELETE Kategori app.delete("/api/kategori/:id", async (req, res) => { const { id } = req.params; try { await db.query("DELETE FROM Kategori WHERE kategori_id = ?", [id]); res.json({ success: true, message: "Kategori berhasil dihapus" }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // GET SEMUA LOKASI app.get("/api/lokasi", async (req, res) => { try { const [rows] = await db.query("SELECT * FROM Lokasi"); res.json({ success: true, data: rows }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // CREATE Lokasi app.post("/api/lokasi", async (req, res) => { const { nama_lokasi } = req.body; if (!nama_lokasi) return res.status(400).json({ success: false, message: "Nama lokasi wajib" }); try { const [result] = await db.query("INSERT INTO Lokasi (nama_lokasi) VALUES (?)", [nama_lokasi]); res.json({ success: true, message: "Lokasi berhasil ditambah", id: result.insertId }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // UPDATE Lokasi app.put("/api/lokasi/:id", async (req, res) => { const { id } = req.params; const { nama_lokasi } = req.body; if (!nama_lokasi) return res.status(400).json({ success: false, message: "Nama lokasi wajib" }); try { await db.query("UPDATE Lokasi SET nama_lokasi = ? WHERE lokasi_id = ?", [nama_lokasi, id]); res.json({ success: true, message: "Lokasi berhasil diupdate" }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // DELETE Lokasi app.delete("/api/lokasi/:id", async (req, res) => { const { id } = req.params; try { await db.query("DELETE FROM Lokasi WHERE lokasi_id = ?", [id]); res.json({ success: true, message: "Lokasi berhasil dihapus" }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // ======================================================= // 4. MODUL TRANSAKSI BAST RESMI (SOAL UKK PPLG) // ======================================================= // --- SINKRONISASI 1: MENYEDIAKAN USER_SERAH_ID DAN USER_TERIMA_ID KE FRONTEND --- app.get("/api/bast", async (req, res) => { try { const sql = ` SELECT b.bast_id, b.barang_id, b.user_serah_id, b.user_terima_id, b.status_serah, b.status_terima, b.dibuat_pada, brg.nama_barang, brg.kode_barang, brg.foto_barang, k.nama_kategori, l.nama_lokasi, u1.nama_lengkap AS nama_penyerah, u2.nama_lengkap AS nama_penerima FROM BAST b LEFT JOIN Barang brg ON b.barang_id = brg.barang_id LEFT JOIN Kategori k ON brg.kategori_id = k.kategori_id LEFT JOIN Lokasi l ON brg.lokasi_id = l.lokasi_id LEFT JOIN User u1 ON b.user_serah_id = u1.user_id LEFT JOIN User u2 ON b.user_terima_id = u2.user_id ORDER BY b.bast_id DESC`; const [rows] = await db.query(sql); res.json({ success: true, data: rows }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // API POST: Membuat Transaksi BAST baru oleh Admin app.post("/api/bast", async (req, res) => { const { barang_id, user_serah_id, user_terima_id } = req.body; if (!barang_id || !user_serah_id || !user_terima_id) { return res.status(400).json({ success: false, message: "Barang, User Serah, dan User Terima wajib diisi sesuai standar UKK!" }); } try { const sql = `INSERT INTO BAST (barang_id, user_serah_id, user_terima_id, status_serah, status_terima, dibuat_pada) VALUES (?, ?, ?, 'Menunggu', 'Menunggu', NOW())`; await db.query(sql, [barang_id, user_serah_id, user_terima_id]); res.json({ success: true, message: "Dokumen BAST berhasil dibuat oleh Admin! Menunggu Approval." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // --- SINKRONISASI 2: DYNAMIC ROUTE APPROVAL (DAPAT UPDATE REJECTED MAUPUN APPROVED SISI PENYERAH/PENERIMA) --- app.put("/api/bast/:id/status", async (req, res) => { const { id } = req.params; const { peran, status } = req.body; // peran = 'serah' / 'terima', status = 'Approved' / 'Rejected' if (!peran || !status) { return res.status(400).json({ success: false, message: "Parameter keputusan data tidak lengkap." }); } try { let sql = ""; if (peran === "serah") { sql = "UPDATE BAST SET status_serah = ? WHERE bast_id = ?"; } else if (peran === "terima") { sql = "UPDATE BAST SET status_terima = ? WHERE bast_id = ?"; } else { return res.status(400).json({ success: false, message: "Peran user tidak terdefinisi secara sah!" }); } await db.query(sql, [status, id]); res.json({ success: true, message: `Berhasil melakukan update persetujuan sebagai pihak ${peran}!` }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); app.get("/api/bast/cetak/:id", async (req, res) => { const { id } = req.params; try { const sql = ` SELECT b.bast_id, b.status_serah, b.status_terima, b.dibuat_pada, brg.nama_barang, brg.kode_barang, u1.nama_lengkap AS nama_penyerah, u2.nama_lengkap AS nama_penerima FROM BAST b LEFT JOIN Barang brg ON b.barang_id = brg.barang_id LEFT JOIN User u1 ON b.user_serah_id = u1.user_id LEFT JOIN User u2 ON b.user_terima_id = u2.user_id WHERE b.bast_id = ?`; const [rows] = await db.query(sql, [id]); if (rows.length === 0) { return res.status(404).json({ success: false, message: "Data BAST tidak ditemukan, Bang!" }); } res.json({ success: true, message: "Data cetak dokumen berhasil disiapkan!", data: rows[0] }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); app.listen(5000, () => { console.log("Server backend berjalan di port 5000..."); });