// server.js const express = require("express"); const cors = require("cors"); const path = require("path"); const db = require("./db"); const povertyRoutes = require("./routes/poverty"); require("dotenv").config(); const app = express(); const PORT = process.env.PORT || 3000; // ─── Middleware ─────────────────────────────────────────────────────────────── app.use(cors()); app.use(express.json()); app.use("/api/poverty", povertyRoutes); // ─── Root: landing page (harus sebelum express.static) ─────────────────────── app.get("/", (req, res) => { res.sendFile(path.join(__dirname, "../frontend/landing.html")); }); // Sajikan folder frontend secara statis app.use(express.static(path.join(__dirname, "../frontend"))); // ─── Inisialisasi tabel saat server start ──────────────────────────────────── async function initDatabase() { const createSpbu = ` CREATE TABLE IF NOT EXISTS spbu ( id INT AUTO_INCREMENT PRIMARY KEY, kode VARCHAR(20) NOT NULL UNIQUE COMMENT 'Kode unik SPBU, mis: 64.781.01', nama VARCHAR(100) NOT NULL, alamat TEXT NOT NULL, lat DOUBLE NOT NULL, lng DOUBLE NOT NULL, jam_buka TIME NOT NULL DEFAULT '06:00:00', jam_tutup TIME NOT NULL DEFAULT '22:00:00', jenis_bbm JSON NOT NULL COMMENT 'Array jenis BBM tersedia', manual_override ENUM('auto','open','closed') NOT NULL DEFAULT 'auto', aktif TINYINT(1) NOT NULL DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `; const createJalan = ` CREATE TABLE IF NOT EXISTS jalan ( id INT AUTO_INCREMENT PRIMARY KEY, nama_jalan VARCHAR(150) NOT NULL, jenis_jalan ENUM('nasional','provinsi','kota_kabupaten') NOT NULL DEFAULT 'kota_kabupaten', koordinat LONGTEXT NOT NULL COMMENT 'GeoJSON LineString koordinat polyline', aktif TINYINT(1) NOT NULL DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `; const createBangunan = ` CREATE TABLE IF NOT EXISTS bangunan ( id INT AUTO_INCREMENT PRIMARY KEY, nama_bangunan VARCHAR(150) NOT NULL, jenis_bangunan ENUM('SHM','HGB','HGU','HP','HPL') NOT NULL DEFAULT 'SHM', nama_jalan VARCHAR(150), koordinat LONGTEXT NOT NULL COMMENT 'GeoJSON Polygon koordinat area bangunan', aktif TINYINT(1) NOT NULL DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `; try { await db.execute(createSpbu); console.log("✅ Tabel 'spbu' siap."); await db.execute(createJalan); console.log("✅ Tabel 'jalan' siap."); await db.execute(createBangunan); console.log("✅ Tabel 'bangunan' siap."); await seedDataIfEmpty(); } catch (err) { console.error("❌ Gagal membuat tabel:", err.message); } } // Isi data awal jika tabel kosong async function seedDataIfEmpty() { const [[{ count }]] = await db.execute("SELECT COUNT(*) AS count FROM spbu"); if (Number(count) > 0) return; const seedData = [ [ "64.781.01", "SPBU 64.781.01 Ahmad Yani", "Jl. Jend. Ahmad Yani, Bansir Laut, Pontianak Tenggara", -0.0529, 109.3484, "06:00", "22:00", ["Pertalite", "Pertamax", "Solar", "Dexlite"], ], [ "64.781.02", "SPBU 64.781.02 Tanjung Raya", "Jl. Tanjung Raya II, Saigon, Pontianak Timur", -0.0334, 109.3702, "00:00", "23:59", ["Pertalite", "Pertamax", "Pertamina Dex"], ], [ "64.781.03", "SPBU 64.781.03 Gajah Mada", "Jl. Gajah Mada, Benua Melayu Darat, Pontianak Selatan", -0.0348, 109.3368, "05:30", "23:00", ["Pertalite", "Pertamax", "Pertamax Turbo", "Solar"], ], [ "64.781.04", "SPBU 64.781.04 Adi Sucipto", "Jl. Adi Sucipto, Sungai Raya, area Pontianak Selatan", -0.0907, 109.3605, "06:00", "21:30", ["Pertalite", "Pertamax", "Dexlite", "Solar"], ], [ "64.781.05", "SPBU 64.781.05 Sultan Hamid II", "Jl. Sultan Hamid II, Dalam Bugis, Pontianak Timur", -0.0125, 109.3567, "06:00", "22:30", ["Pertalite", "Pertamax", "Solar"], ], [ "64.781.06", "SPBU 64.781.06 Imam Bonjol", "Jl. Imam Bonjol, Benua Melayu Laut, Pontianak Selatan", -0.0261, 109.3449, "00:00", "23:59", ["Pertalite", "Pertamax", "Pertamax Turbo", "Pertamina Dex"], ], [ "64.781.07", "SPBU 64.781.07 Khatulistiwa", "Jl. Khatulistiwa, Siantan Hilir, Pontianak Utara", 0.0019, 109.3231, "05:00", "22:00", ["Pertalite", "Pertamax", "Solar", "Dexlite"], ], [ "64.781.08", "SPBU 64.781.08 Husein Hamzah", "Jl. Husein Hamzah, Pal Lima, Pontianak Barat", -0.0238, 109.3039, "06:00", "23:00", ["Pertalite", "Pertamax", "Solar", "Pertamina Dex"], ], [ "64.781.09", "SPBU 64.781.09 Dr. Wahidin", "Jl. Dr. Wahidin Sudirohusodo, Sungai Jawi, Pontianak Kota", -0.0359, 109.3148, "06:00", "21:00", ["Pertalite", "Pertamax", "Dexlite"], ], ]; for (const [ kode, nama, alamat, lat, lng, jam_buka, jam_tutup, jenis_bbm, ] of seedData) { await db.execute( `INSERT INTO spbu (kode, nama, alamat, lat, lng, jam_buka, jam_tutup, jenis_bbm) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ kode, nama, alamat, lat, lng, jam_buka, jam_tutup, JSON.stringify(jenis_bbm), ], ); } console.log("✅ Data awal 9 SPBU berhasil di-seed."); } // ─── Helper ─────────────────────────────────────────────────────────────────── function parseSpbu(row) { return { ...row, jenis_bbm: typeof row.jenis_bbm === "string" ? JSON.parse(row.jenis_bbm) : row.jenis_bbm, aktif: Boolean(row.aktif), jam_buka: row.jam_buka ? row.jam_buka.slice(0, 5) : null, jam_tutup: row.jam_tutup ? row.jam_tutup.slice(0, 5) : null, }; } function parseGeo(row) { return { ...row, aktif: Boolean(row.aktif), koordinat: typeof row.koordinat === "string" ? JSON.parse(row.koordinat) : row.koordinat, }; } // ═══════════════════════════════════════════════════════════════════════════════ // ─── API: SPBU ──────────────────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════════════════════ // GET /api/spbu app.get("/api/spbu", async (req, res) => { try { const [rows] = await db.execute( "SELECT * FROM spbu WHERE aktif = 1 ORDER BY kode ASC", ); res.json({ success: true, data: rows.map(parseSpbu) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // GET /api/spbu/:id app.get("/api/spbu/:id", async (req, res) => { try { const [rows] = await db.execute("SELECT * FROM spbu WHERE id = ?", [ req.params.id, ]); if (!rows.length) return res .status(404) .json({ success: false, message: "SPBU tidak ditemukan." }); res.json({ success: true, data: parseSpbu(rows[0]) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // POST /api/spbu app.post("/api/spbu", async (req, res) => { const { kode, nama, alamat, lat, lng, jam_buka, jam_tutup, jenis_bbm, manual_override, } = req.body; if ( !kode || !nama || !alamat || lat == null || lng == null || !jam_buka || !jam_tutup || !jenis_bbm ) { return res .status(400) .json({ success: false, message: "Semua field wajib diisi." }); } if (!Array.isArray(jenis_bbm) || jenis_bbm.length === 0) { return res.status(400).json({ success: false, message: "Minimal satu jenis BBM harus dipilih.", }); } try { const [result] = await db.execute( `INSERT INTO spbu (kode, nama, alamat, lat, lng, jam_buka, jam_tutup, jenis_bbm, manual_override) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ kode, nama, alamat, lat, lng, jam_buka, jam_tutup, JSON.stringify(jenis_bbm), manual_override || "auto", ], ); res.status(201).json({ success: true, message: "SPBU berhasil ditambahkan.", id: result.insertId, }); } catch (err) { if (err.code === "ER_DUP_ENTRY") { return res.status(409).json({ success: false, message: `Kode SPBU '${kode}' sudah digunakan.`, }); } res.status(500).json({ success: false, message: err.message }); } }); // PUT /api/spbu/:id app.put("/api/spbu/:id", async (req, res) => { const { kode, nama, alamat, lat, lng, jam_buka, jam_tutup, jenis_bbm, manual_override, aktif, } = req.body; if ( !kode || !nama || !alamat || lat == null || lng == null || !jam_buka || !jam_tutup || !jenis_bbm ) { return res .status(400) .json({ success: false, message: "Semua field wajib diisi." }); } try { const [result] = await db.execute( `UPDATE spbu SET kode=?, nama=?, alamat=?, lat=?, lng=?, jam_buka=?, jam_tutup=?, jenis_bbm=?, manual_override=?, aktif=? WHERE id=?`, [ kode, nama, alamat, lat, lng, jam_buka, jam_tutup, JSON.stringify(jenis_bbm), manual_override || "auto", aktif !== undefined ? aktif : 1, req.params.id, ], ); if (result.affectedRows === 0) return res .status(404) .json({ success: false, message: "SPBU tidak ditemukan." }); res.json({ success: true, message: "SPBU berhasil diperbarui." }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // PATCH /api/spbu/:id/override app.patch("/api/spbu/:id/override", async (req, res) => { const { manual_override } = req.body; const allowed = ["auto", "open", "closed"]; if (!allowed.includes(manual_override)) { return res .status(400) .json({ success: false, message: "Nilai override tidak valid." }); } try { const [result] = await db.execute( "UPDATE spbu SET manual_override = ? WHERE id = ?", [manual_override, req.params.id], ); if (result.affectedRows === 0) return res .status(404) .json({ success: false, message: "SPBU tidak ditemukan." }); res.json({ success: true, message: "Override berhasil diperbarui." }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // DELETE /api/spbu/:id app.delete("/api/spbu/:id", async (req, res) => { try { const [result] = await db.execute( "UPDATE spbu SET aktif = 0 WHERE id = ?", [req.params.id], ); if (result.affectedRows === 0) return res .status(404) .json({ success: false, message: "SPBU tidak ditemukan." }); res.json({ success: true, message: "SPBU berhasil dihapus." }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ═══════════════════════════════════════════════════════════════════════════════ // ─── API: JALAN ─────────────────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════════════════════ // GET /api/jalan — ambil semua jalan aktif app.get("/api/jalan", async (req, res) => { try { const [rows] = await db.execute( "SELECT * FROM jalan WHERE aktif = 1 ORDER BY nama_jalan ASC", ); res.json({ success: true, data: rows.map(parseGeo) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // GET /api/jalan/:id app.get("/api/jalan/:id", async (req, res) => { try { const [rows] = await db.execute("SELECT * FROM jalan WHERE id = ?", [ req.params.id, ]); if (!rows.length) return res .status(404) .json({ success: false, message: "Jalan tidak ditemukan." }); res.json({ success: true, data: parseGeo(rows[0]) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // POST /api/jalan — tambah jalan baru app.post("/api/jalan", async (req, res) => { const { nama_jalan, jenis_jalan, koordinat } = req.body; if (!nama_jalan || !jenis_jalan || !koordinat) { return res.status(400).json({ success: false, message: "nama_jalan, jenis_jalan, dan koordinat wajib diisi.", }); } const jenisValid = ["nasional", "provinsi", "kota_kabupaten"]; if (!jenisValid.includes(jenis_jalan)) { return res .status(400) .json({ success: false, message: "jenis_jalan tidak valid." }); } try { const koordinatStr = typeof koordinat === "string" ? koordinat : JSON.stringify(koordinat); const [result] = await db.execute( "INSERT INTO jalan (nama_jalan, jenis_jalan, koordinat) VALUES (?, ?, ?)", [nama_jalan.trim(), jenis_jalan, koordinatStr], ); res.status(201).json({ success: true, message: "Jalan berhasil ditambahkan.", id: result.insertId, }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // PUT /api/jalan/:id — update jalan app.put("/api/jalan/:id", async (req, res) => { const { nama_jalan, jenis_jalan, koordinat } = req.body; if (!nama_jalan || !jenis_jalan || !koordinat) { return res.status(400).json({ success: false, message: "nama_jalan, jenis_jalan, dan koordinat wajib diisi.", }); } try { const koordinatStr = typeof koordinat === "string" ? koordinat : JSON.stringify(koordinat); const [result] = await db.execute( "UPDATE jalan SET nama_jalan=?, jenis_jalan=?, koordinat=? WHERE id=?", [nama_jalan.trim(), jenis_jalan, koordinatStr, req.params.id], ); if (result.affectedRows === 0) return res .status(404) .json({ success: false, message: "Jalan tidak ditemukan." }); res.json({ success: true, message: "Jalan berhasil diperbarui." }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // DELETE /api/jalan/:id — soft delete app.delete("/api/jalan/:id", async (req, res) => { try { const [result] = await db.execute( "UPDATE jalan SET aktif = 0 WHERE id = ?", [req.params.id], ); if (result.affectedRows === 0) return res .status(404) .json({ success: false, message: "Jalan tidak ditemukan." }); res.json({ success: true, message: "Jalan berhasil dihapus." }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ═══════════════════════════════════════════════════════════════════════════════ // ─── API: BANGUNAN ──────────────────────────────────────────────────────────── // ═══════════════════════════════════════════════════════════════════════════════ // GET /api/bangunan — ambil semua bangunan aktif app.get("/api/bangunan", async (req, res) => { try { const [rows] = await db.execute( "SELECT * FROM bangunan WHERE aktif = 1 ORDER BY nama_bangunan ASC", ); res.json({ success: true, data: rows.map(parseGeo) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // GET /api/bangunan/:id app.get("/api/bangunan/:id", async (req, res) => { try { const [rows] = await db.execute("SELECT * FROM bangunan WHERE id = ?", [ req.params.id, ]); if (!rows.length) return res .status(404) .json({ success: false, message: "Bangunan tidak ditemukan." }); res.json({ success: true, data: parseGeo(rows[0]) }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // POST /api/bangunan — tambah bangunan baru app.post("/api/bangunan", async (req, res) => { const { nama_bangunan, jenis_bangunan, nama_jalan, koordinat } = req.body; if (!nama_bangunan || !jenis_bangunan || !koordinat) { return res.status(400).json({ success: false, message: "nama_bangunan, jenis_bangunan, dan koordinat wajib diisi.", }); } const jenisValid = ["SHM", "HGB", "HGU", "HP", "HPL"]; if (!jenisValid.includes(jenis_bangunan)) { return res .status(400) .json({ success: false, message: "jenis_bangunan tidak valid." }); } try { const koordinatStr = typeof koordinat === "string" ? koordinat : JSON.stringify(koordinat); const [result] = await db.execute( "INSERT INTO bangunan (nama_bangunan, jenis_bangunan, nama_jalan, koordinat) VALUES (?, ?, ?, ?)", [ nama_bangunan.trim(), jenis_bangunan, nama_jalan ? nama_jalan.trim() : null, koordinatStr, ], ); res.status(201).json({ success: true, message: "Bangunan berhasil ditambahkan.", id: result.insertId, }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // PUT /api/bangunan/:id — update bangunan app.put("/api/bangunan/:id", async (req, res) => { const { nama_bangunan, jenis_bangunan, nama_jalan, koordinat } = req.body; if (!nama_bangunan || !jenis_bangunan || !koordinat) { return res.status(400).json({ success: false, message: "nama_bangunan, jenis_bangunan, dan koordinat wajib diisi.", }); } try { const koordinatStr = typeof koordinat === "string" ? koordinat : JSON.stringify(koordinat); const [result] = await db.execute( "UPDATE bangunan SET nama_bangunan=?, jenis_bangunan=?, nama_jalan=?, koordinat=? WHERE id=?", [ nama_bangunan.trim(), jenis_bangunan, nama_jalan ? nama_jalan.trim() : null, koordinatStr, req.params.id, ], ); if (result.affectedRows === 0) return res .status(404) .json({ success: false, message: "Bangunan tidak ditemukan." }); res.json({ success: true, message: "Bangunan berhasil diperbarui." }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // DELETE /api/bangunan/:id — soft delete app.delete("/api/bangunan/:id", async (req, res) => { try { const [result] = await db.execute( "UPDATE bangunan SET aktif = 0 WHERE id = ?", [req.params.id], ); if (result.affectedRows === 0) return res .status(404) .json({ success: false, message: "Bangunan tidak ditemukan." }); res.json({ success: true, message: "Bangunan berhasil dihapus." }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ─── Fallback: semua route non-API ke landing.html ─────────────────────────── app.get("*", (req, res) => { res.sendFile(path.join(__dirname, "../frontend/landing.html")); }); // ─── Start ──────────────────────────────────────────────────────────────────── initDatabase().then(() => { app.listen(PORT, () => { console.log(`\n🚀 Server berjalan di http://localhost:${PORT}`); console.log(` Portal → http://localhost:${PORT}/`); console.log(` SPBU → http://localhost:${PORT}/index.html`); console.log(` Poverty → http://localhost:${PORT}/poverty-map.html`); console.log(` Pengajuan → http://localhost:${PORT}/pengajuan.html`); console.log(` Admin → http://localhost:${PORT}/admin.html`); console.log(` Admin POV → http://localhost:${PORT}/admin-poverty.html`); console.log(` API SPBU → http://localhost:${PORT}/api/spbu`); console.log(` API POV → http://localhost:${PORT}/api/poverty\n`); }); });