tugasKASIR/belajar_express/index.js

647 lines
23 KiB
JavaScript

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...");
});