Files
WEBGIS_Project/backend/server.js
T
tistopandita b350e4e2c8 landing_page
2026-06-11 15:01:02 +07:00

671 lines
22 KiB
JavaScript

// 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`);
});
});