first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
backend/.env
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# WebGIS SPBU Pontianak — Backend + Frontend
|
||||||
|
|
||||||
|
## Struktur Project
|
||||||
|
|
||||||
|
```
|
||||||
|
spbu-webgis/
|
||||||
|
├── backend/
|
||||||
|
│ ├── server.js ← Express server + semua API route
|
||||||
|
│ ├── db.js ← Koneksi MySQL (pool)
|
||||||
|
│ ├── .env ← Konfigurasi database (edit ini!)
|
||||||
|
│ └── package.json
|
||||||
|
├── frontend/
|
||||||
|
│ ├── index.html ← Halaman peta utama
|
||||||
|
│ └── admin.html ← Halaman CRUD data SPBU
|
||||||
|
├── schema.sql ← SQL untuk buat tabel manual (opsional)
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cara Menjalankan
|
||||||
|
|
||||||
|
### 1. Pastikan MySQL berjalan dan buat database
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE spbu_pontianak CHARACTER SET utf8mb4;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Edit konfigurasi database
|
||||||
|
|
||||||
|
Buka `backend/.env` dan sesuaikan:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=password_anda ← ganti ini
|
||||||
|
DB_NAME=spbu_pontianak
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Jalankan server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mode biasa
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Mode development (auto-restart saat ada perubahan)
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Buka di browser
|
||||||
|
|
||||||
|
| URL | Keterangan |
|
||||||
|
|-----|-----------|
|
||||||
|
| http://localhost:3000/ | Peta WebGIS |
|
||||||
|
| http://localhost:3000/admin.html | CRUD Data SPBU |
|
||||||
|
| http://localhost:3000/api/spbu | JSON API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Endpoint
|
||||||
|
|
||||||
|
| Method | URL | Fungsi |
|
||||||
|
|--------|-----|--------|
|
||||||
|
| `GET` | `/api/spbu` | Ambil semua SPBU aktif |
|
||||||
|
| `GET` | `/api/spbu/:id` | Ambil satu SPBU |
|
||||||
|
| `POST` | `/api/spbu` | Tambah SPBU baru |
|
||||||
|
| `PUT` | `/api/spbu/:id` | Update data SPBU |
|
||||||
|
| `PATCH` | `/api/spbu/:id/override` | Update override status saja |
|
||||||
|
| `DELETE` | `/api/spbu/:id` | Hapus SPBU (soft delete) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Catatan
|
||||||
|
|
||||||
|
- Tabel MySQL dibuat otomatis saat server pertama kali dijalankan.
|
||||||
|
- Data 9 SPBU di-seed otomatis jika tabel masih kosong.
|
||||||
|
- Hapus SPBU bersifat **soft delete** (kolom `aktif = 0`), data tidak hilang dari database.
|
||||||
|
- Override status tersimpan ke database dan bertahan setelah server restart.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const mysql = require("mysql2/promise");
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
const pool = mysql.createPool({
|
||||||
|
host: process.env.DB_HOST || "localhost",
|
||||||
|
port: process.env.DB_PORT || 3306,
|
||||||
|
user: process.env.DB_USER || "root",
|
||||||
|
password: process.env.DB_PASSWORD || "",
|
||||||
|
database: process.env.DB_NAME || "spbu_pontianak",
|
||||||
|
waitForConnections: true,
|
||||||
|
connectionLimit: 10,
|
||||||
|
queueLimit: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test koneksi saat pertama kali diload
|
||||||
|
pool.getConnection()
|
||||||
|
.then((conn) => {
|
||||||
|
console.log("✅ Terkoneksi ke MySQL:", process.env.DB_NAME);
|
||||||
|
conn.release();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("❌ Gagal koneksi ke MySQL:", err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = pool;
|
||||||
Generated
+1381
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "spbu-webgis-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Backend API untuk WebGIS SPBU Pontianak",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"mysql2": "^3.9.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,679 @@
|
|||||||
|
// server.js
|
||||||
|
const express = require("express");
|
||||||
|
const cors = require("cors");
|
||||||
|
const path = require("path");
|
||||||
|
const db = require("./db");
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// ─── Middleware ───────────────────────────────────────────────────────────────
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// 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 index.html ─────────────────────────────
|
||||||
|
app.get("*", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "../frontend/index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Start ────────────────────────────────────────────────────────────────────
|
||||||
|
initDatabase().then(() => {
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`\n🚀 Server berjalan di http://localhost:${PORT}`);
|
||||||
|
console.log(` Peta → http://localhost:${PORT}/`);
|
||||||
|
console.log(` Admin → http://localhost:${PORT}/admin.html`);
|
||||||
|
console.log(` API → http://localhost:${PORT}/api/spbu`);
|
||||||
|
console.log(` API → http://localhost:${PORT}/api/jalan`);
|
||||||
|
console.log(` API → http://localhost:${PORT}/api/bangunan\n`);
|
||||||
|
});
|
||||||
|
});
|
||||||
+1682
File diff suppressed because it is too large
Load Diff
+1407
File diff suppressed because it is too large
Load Diff
+53
@@ -0,0 +1,53 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Schema Database WebGIS SPBU Pontianak
|
||||||
|
-- Jalankan sekali untuk membuat database & tabel secara manual
|
||||||
|
-- (Opsional — server.js sudah auto-create tabel saat start)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS spbu_pontianak
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE spbu_pontianak;
|
||||||
|
|
||||||
|
-- ── Tabel SPBU ──────────────────────────────────────────────
|
||||||
|
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 COMMENT 'Latitude (desimal)',
|
||||||
|
lng DOUBLE NOT NULL COMMENT 'Longitude (desimal)',
|
||||||
|
jam_buka TIME NOT NULL DEFAULT '06:00' COMMENT 'Format HH:MM',
|
||||||
|
jam_tutup TIME NOT NULL DEFAULT '22:00' COMMENT 'Format HH:MM',
|
||||||
|
jenis_bbm JSON NOT NULL COMMENT 'Array jenis BBM, contoh: ["Pertalite","Pertamax"]',
|
||||||
|
manual_override ENUM('auto','open','closed') NOT NULL DEFAULT 'auto',
|
||||||
|
aktif TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=aktif, 0=dihapus (soft delete)',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ── Tabel Jalan ─────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS jalan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_jalan VARCHAR(150) NOT NULL COMMENT 'Nama jalan, mis: Jl. Ahmad Yani',
|
||||||
|
jenis_jalan ENUM('nasional','provinsi','kota_kabupaten') NOT NULL DEFAULT 'kota_kabupaten'
|
||||||
|
COMMENT 'Klasifikasi jalan',
|
||||||
|
koordinat LONGTEXT NOT NULL COMMENT 'GeoJSON LineString koordinat polyline',
|
||||||
|
aktif TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=aktif, 0=dihapus (soft delete)',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- ── Tabel Bangunan ──────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS bangunan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_bangunan VARCHAR(150) NOT NULL COMMENT 'Nama bangunan atau kavling',
|
||||||
|
jenis_bangunan ENUM('SHM','HGB','HGU','HP','HPL') NOT NULL DEFAULT 'SHM'
|
||||||
|
COMMENT 'SHM=Sertifikat Hak Milik, HGB=Hak Guna Bangunan, HGU=Hak Guna Usaha, HP=Hak Pakai, HPL=Hak Pengelolaan',
|
||||||
|
nama_jalan VARCHAR(150) COMMENT 'Nama jalan lokasi bangunan',
|
||||||
|
koordinat LONGTEXT NOT NULL COMMENT 'GeoJSON Polygon koordinat area bangunan',
|
||||||
|
aktif TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=aktif, 0=dihapus (soft delete)',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
Reference in New Issue
Block a user