Initial commit
This commit is contained in:
138
jalan_tanah/api/jalan.php
Normal file
138
jalan_tanah/api/jalan.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
// api/jalan.php — CRUD Data Jalan (GeoJSON LineString)
|
||||
|
||||
require_once '../config/database.php';
|
||||
setCorsHeaders();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$conn = getConnection();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$id = intval($_GET['id']);
|
||||
$stmt = $conn->prepare("SELECT id, nama_jalan, status_jalan, panjang_meter, keterangan, geom, created_at, updated_at FROM data_jalan WHERE id = ?");
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
if (!$row) { http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']); break; }
|
||||
$row['koordinat'] = parseGeoJsonToLatLng($row['geom']);
|
||||
unset($row['geom']);
|
||||
echo json_encode(['status'=>'success','data'=>$row]);
|
||||
} else {
|
||||
// Filter support: ?status=Nasional&min_panjang=100&max_panjang=5000&q=nama
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
$types = '';
|
||||
|
||||
if (!empty($_GET['status'])) {
|
||||
$where[] = 'status_jalan = ?';
|
||||
$params[] = $_GET['status'];
|
||||
$types .= 's';
|
||||
}
|
||||
if (!empty($_GET['min_panjang'])) {
|
||||
$where[] = 'panjang_meter >= ?';
|
||||
$params[] = (float)$_GET['min_panjang'];
|
||||
$types .= 'd';
|
||||
}
|
||||
if (!empty($_GET['max_panjang'])) {
|
||||
$where[] = 'panjang_meter <= ?';
|
||||
$params[] = (float)$_GET['max_panjang'];
|
||||
$types .= 'd';
|
||||
}
|
||||
if (!empty($_GET['q'])) {
|
||||
$where[] = 'nama_jalan LIKE ?';
|
||||
$params[] = '%' . $_GET['q'] . '%';
|
||||
$types .= 's';
|
||||
}
|
||||
|
||||
$sql = 'SELECT id, nama_jalan, status_jalan, panjang_meter, keterangan, geom, created_at FROM data_jalan WHERE ' . implode(' AND ', $where) . ' ORDER BY created_at DESC';
|
||||
$stmt = $conn->prepare($sql);
|
||||
if ($params) $stmt->bind_param($types, ...$params);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$data = array_map(function($row) {
|
||||
$row['koordinat'] = parseGeoJsonToLatLng($row['geom']);
|
||||
unset($row['geom']);
|
||||
return $row;
|
||||
}, $rows);
|
||||
|
||||
echo json_encode(['status'=>'success','data'=>$data,'total'=>count($data)]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$input = getJsonInput();
|
||||
if (!$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'Input tidak valid']); break; }
|
||||
|
||||
$nama = trim($input['nama_jalan'] ?? '');
|
||||
$status = trim($input['status_jalan'] ?? '');
|
||||
$panjang = (float)($input['panjang_meter'] ?? 0);
|
||||
$ket = trim($input['keterangan'] ?? '');
|
||||
$coords = $input['koordinat'] ?? [];
|
||||
|
||||
if (!$nama || !$status || count($coords) < 2) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status'=>'error','message'=>'Nama, status, dan minimal 2 koordinat wajib diisi']);
|
||||
break;
|
||||
}
|
||||
if (!in_array($status, ['Nasional','Provinsi','Kabupaten'])) {
|
||||
http_response_code(400); echo json_encode(['status'=>'error','message'=>'Status tidak valid']); break;
|
||||
}
|
||||
|
||||
$geom = buildGeoJsonLine($coords);
|
||||
$stmt = $conn->prepare("INSERT INTO data_jalan (nama_jalan,status_jalan,panjang_meter,keterangan,geom) VALUES (?,?,?,?,?)");
|
||||
$stmt->bind_param("ssdss", $nama, $status, $panjang, $ket, $geom);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status'=>'success','message'=>'Data jalan berhasil ditambahkan','id'=>$conn->insert_id]);
|
||||
} else {
|
||||
http_response_code(500); echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$conn->error]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
$input = getJsonInput();
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
if (!$id || !$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID dan input wajib ada']); break; }
|
||||
|
||||
$nama = trim($input['nama_jalan'] ?? '');
|
||||
$status = trim($input['status_jalan'] ?? '');
|
||||
$panjang = (float)($input['panjang_meter'] ?? 0);
|
||||
$ket = trim($input['keterangan'] ?? '');
|
||||
$coords = $input['koordinat'] ?? [];
|
||||
|
||||
if (!$nama || !$status) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'Nama dan status wajib diisi']); break; }
|
||||
|
||||
$geom = buildGeoJsonLine($coords);
|
||||
$stmt = $conn->prepare("UPDATE data_jalan SET nama_jalan=?,status_jalan=?,panjang_meter=?,keterangan=?,geom=? WHERE id=?");
|
||||
$stmt->bind_param("ssdssi", $nama, $status, $panjang, $ket, $geom, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status'=>'success','message'=>'Data jalan berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500); echo json_encode(['status'=>'error','message'=>'Gagal memperbarui: '.$conn->error]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
if (!$id) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID tidak valid']); break; }
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM data_jalan WHERE id=?");
|
||||
$stmt->bind_param("i", $id);
|
||||
if ($stmt->execute() && $stmt->affected_rows > 0) {
|
||||
echo json_encode(['status'=>'success','message'=>'Data jalan berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
echo json_encode(['status'=>'error','message'=>'Method tidak diizinkan']);
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
363
jalan_tanah/api/laporan.php
Normal file
363
jalan_tanah/api/laporan.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
// api/laporan.php — CRUD + Analitik Laporan Jalan Rusak
|
||||
|
||||
require_once '../config/database.php';
|
||||
setCorsHeaders();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$conn = getConnection();
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
// ---- Endpoint analitik terpisah ----
|
||||
if ($method === 'GET' && $action === 'analytics') {
|
||||
handleAnalytics($conn);
|
||||
$conn->close();
|
||||
exit;
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ==========================================
|
||||
// GET — list dengan filter
|
||||
// ==========================================
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$id = intval($_GET['id']);
|
||||
$stmt = $conn->prepare("SELECT * FROM laporan_jalan_rusak WHERE id = ?");
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
if (!$row) { http_response_code(404); echo json_encode(['status'=>'error','message'=>'Tidak ditemukan']); break; }
|
||||
$row = formatLaporanRow($row);
|
||||
echo json_encode(['status'=>'success','data'=>$row]);
|
||||
} else {
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
$types = '';
|
||||
|
||||
// Filter: bulan terakhir (default), atau rentang tahun
|
||||
if (!empty($_GET['bulan_terakhir'])) {
|
||||
$bulan = intval($_GET['bulan_terakhir']);
|
||||
$where[] = 'tanggal_input >= DATE_SUB(NOW(), INTERVAL ? MONTH)';
|
||||
$params[] = $bulan;
|
||||
$types .= 'i';
|
||||
} elseif (!empty($_GET['tahun_terakhir'])) {
|
||||
$tahun = intval($_GET['tahun_terakhir']);
|
||||
$where[] = 'tanggal_input >= DATE_SUB(NOW(), INTERVAL ? YEAR)';
|
||||
$params[] = $tahun;
|
||||
$types .= 'i';
|
||||
}
|
||||
|
||||
if (!empty($_GET['status'])) {
|
||||
$where[] = 'status = ?';
|
||||
$params[] = $_GET['status'];
|
||||
$types .= 's';
|
||||
}
|
||||
if (!empty($_GET['nama_jalan'])) {
|
||||
$where[] = 'nama_jalan LIKE ?';
|
||||
$params[] = '%'.$_GET['nama_jalan'].'%';
|
||||
$types .= 's';
|
||||
}
|
||||
if (!empty($_GET['q'])) {
|
||||
$where[] = '(nama_jalan LIKE ? OR deskripsi LIKE ? OR nama_pelapor LIKE ?)';
|
||||
$params[] = '%'.$_GET['q'].'%';
|
||||
$params[] = '%'.$_GET['q'].'%';
|
||||
$params[] = '%'.$_GET['q'].'%';
|
||||
$types .= 'sss';
|
||||
}
|
||||
|
||||
$sql = 'SELECT * FROM laporan_jalan_rusak WHERE ' . implode(' AND ', $where) . ' ORDER BY tanggal_input DESC';
|
||||
$stmt = $conn->prepare($sql);
|
||||
if ($params) $stmt->bind_param($types, ...$params);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$data = array_map('formatLaporanRow', $rows);
|
||||
|
||||
// Hitung cluster (radius 50m, min 3 laporan → urgent)
|
||||
$clusters = buildClusters($data, 50, 3);
|
||||
|
||||
echo json_encode(['status'=>'success','data'=>$data,'total'=>count($data),'clusters'=>$clusters]);
|
||||
}
|
||||
break;
|
||||
|
||||
// ==========================================
|
||||
// POST — Tambah laporan (multipart/form-data)
|
||||
// ==========================================
|
||||
case 'POST':
|
||||
$nama_jalan = trim($_POST['nama_jalan'] ?? '');
|
||||
$deskripsi = trim($_POST['deskripsi'] ?? '');
|
||||
$nama_pelapor= trim($_POST['nama_pelapor']?? '');
|
||||
$lat = (float)($_POST['lat'] ?? 0);
|
||||
$lng = (float)($_POST['lng'] ?? 0);
|
||||
|
||||
if (!$nama_jalan || !$lat || !$lng) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status'=>'error','message'=>'Nama jalan dan koordinat wajib diisi']);
|
||||
break;
|
||||
}
|
||||
|
||||
$foto_path = null;
|
||||
$foto_lat = null;
|
||||
$foto_lng = null;
|
||||
$foto_datetime = null;
|
||||
|
||||
// Handle upload foto
|
||||
if (!empty($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $_FILES['foto']['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mime, ALLOWED_TYPES)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status'=>'error','message'=>'Format foto tidak didukung (JPG/PNG/WebP)']);
|
||||
break;
|
||||
}
|
||||
if ($_FILES['foto']['size'] > MAX_FILE_SIZE) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status'=>'error','message'=>'Ukuran foto melebihi batas 8MB']);
|
||||
break;
|
||||
}
|
||||
|
||||
// Baca EXIF sebelum pindah file
|
||||
$exifData = @exif_read_data($_FILES['foto']['tmp_name']);
|
||||
if ($exifData) {
|
||||
// Ekstrak koordinat GPS dari EXIF
|
||||
$gpsCoords = extractExifGps($exifData);
|
||||
if ($gpsCoords) {
|
||||
$foto_lat = $gpsCoords['lat'];
|
||||
$foto_lng = $gpsCoords['lng'];
|
||||
}
|
||||
// Ekstrak datetime
|
||||
$dt = $exifData['DateTimeOriginal'] ?? $exifData['DateTime'] ?? null;
|
||||
if ($dt) {
|
||||
$foto_datetime = date('Y-m-d H:i:s', strtotime(str_replace(':', '-', substr($dt, 0, 10)) . substr($dt, 10)));
|
||||
}
|
||||
}
|
||||
|
||||
// Jika GPS tidak ada di EXIF, pakai koordinat yang dikirim user
|
||||
if (!$foto_lat) $foto_lat = $lat;
|
||||
if (!$foto_lng) $foto_lng = $lng;
|
||||
|
||||
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
|
||||
$ext = match($mime) { 'image/png' => 'png', 'image/webp' => 'webp', default => 'jpg' };
|
||||
$filename = 'laporan_' . date('Ymd_His') . '_' . uniqid() . '.' . $ext;
|
||||
$destPath = UPLOAD_DIR . $filename;
|
||||
|
||||
if (!move_uploaded_file($_FILES['foto']['tmp_name'], $destPath)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status'=>'error','message'=>'Gagal menyimpan foto']);
|
||||
break;
|
||||
}
|
||||
$foto_path = UPLOAD_URL . $filename;
|
||||
}
|
||||
|
||||
// Koordinat point: prioritaskan GPS foto, fallback ke input user
|
||||
$pointLat = $foto_lat ?? $lat;
|
||||
$pointLng = $foto_lng ?? $lng;
|
||||
$geom = buildGeoJsonPoint($pointLat, $pointLng);
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"INSERT INTO laporan_jalan_rusak (nama_pelapor,nama_jalan,deskripsi,foto_path,foto_lat,foto_lng,foto_datetime,geom)
|
||||
VALUES (?,?,?,?,?,?,?,?)"
|
||||
);
|
||||
$npVal = $nama_pelapor ?: null;
|
||||
$stmt->bind_param("ssssddss", $npVal, $nama_jalan, $deskripsi, $foto_path, $foto_lat, $foto_lng, $foto_datetime, $geom);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status'=>'success','message'=>'Laporan berhasil dikirim','id'=>$conn->insert_id]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$conn->error]);
|
||||
}
|
||||
break;
|
||||
|
||||
// ==========================================
|
||||
// PUT — Update status laporan
|
||||
// ==========================================
|
||||
case 'PUT':
|
||||
$input = getJsonInput();
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
if (!$id || !$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID dan input wajib ada']); break; }
|
||||
|
||||
$status = trim($input['status'] ?? '');
|
||||
if (!in_array($status, ['pending','verified','resolved'])) {
|
||||
http_response_code(400); echo json_encode(['status'=>'error','message'=>'Status tidak valid']); break;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("UPDATE laporan_jalan_rusak SET status=? WHERE id=?");
|
||||
$stmt->bind_param("si", $status, $id);
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status'=>'success','message'=>'Status laporan diperbarui']);
|
||||
} else {
|
||||
http_response_code(500); echo json_encode(['status'=>'error','message'=>$conn->error]);
|
||||
}
|
||||
break;
|
||||
|
||||
// ==========================================
|
||||
// DELETE
|
||||
// ==========================================
|
||||
case 'DELETE':
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
if (!$id) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID tidak valid']); break; }
|
||||
|
||||
// Hapus file foto juga
|
||||
$stmt = $conn->prepare("SELECT foto_path FROM laporan_jalan_rusak WHERE id=?");
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
if ($row && $row['foto_path']) {
|
||||
$fullPath = __DIR__ . '/../' . $row['foto_path'];
|
||||
if (file_exists($fullPath)) @unlink($fullPath);
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM laporan_jalan_rusak WHERE id=?");
|
||||
$stmt->bind_param("i", $id);
|
||||
if ($stmt->execute() && $stmt->affected_rows > 0) {
|
||||
echo json_encode(['status'=>'success','message'=>'Laporan berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
echo json_encode(['status'=>'error','message'=>'Method tidak diizinkan']);
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
|
||||
// ============================================
|
||||
// HELPERS
|
||||
// ============================================
|
||||
|
||||
function formatLaporanRow(array $row): array {
|
||||
$geom = json_decode($row['geom'] ?? '{}', true);
|
||||
$row['lat'] = $geom['coordinates'][1] ?? null;
|
||||
$row['lng'] = $geom['coordinates'][0] ?? null;
|
||||
unset($row['geom']);
|
||||
return $row;
|
||||
}
|
||||
|
||||
function extractExifGps(array $exif): ?array {
|
||||
if (empty($exif['GPSLatitude']) || empty($exif['GPSLongitude'])) return null;
|
||||
$lat = gpsToDecimal($exif['GPSLatitude'], $exif['GPSLatitudeRef'] ?? 'N');
|
||||
$lng = gpsToDecimal($exif['GPSLongitude'], $exif['GPSLongitudeRef'] ?? 'E');
|
||||
return ['lat' => $lat, 'lng' => $lng];
|
||||
}
|
||||
|
||||
function gpsToDecimal(array $parts, string $ref): float {
|
||||
$deg = evalFraction($parts[0]);
|
||||
$min = evalFraction($parts[1]);
|
||||
$sec = evalFraction($parts[2]);
|
||||
$dec = $deg + ($min / 60) + ($sec / 3600);
|
||||
return in_array($ref, ['S','W']) ? -$dec : $dec;
|
||||
}
|
||||
|
||||
function evalFraction(string $frac): float {
|
||||
if (strpos($frac, '/') === false) return (float)$frac;
|
||||
[$n, $d] = explode('/', $frac);
|
||||
return $d ? (float)$n / (float)$d : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung jarak Haversine antara dua titik (meter)
|
||||
*/
|
||||
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||
$R = 6371000;
|
||||
$p1 = deg2rad($lat1); $p2 = deg2rad($lat2);
|
||||
$dp = deg2rad($lat2 - $lat1);
|
||||
$dl = deg2rad($lng2 - $lng1);
|
||||
$a = sin($dp/2)**2 + cos($p1)*cos($p2)*sin($dl/2)**2;
|
||||
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bangun cluster sederhana: greedy radius clustering
|
||||
* Laporan dalam radius $radiusM meter dianggap satu cluster.
|
||||
* Cluster dengan >= $minCount laporan ditandai urgent.
|
||||
*/
|
||||
function buildClusters(array $laporan, float $radiusM, int $minCount): array {
|
||||
$clusters = [];
|
||||
$assigned = [];
|
||||
|
||||
foreach ($laporan as $i => $l) {
|
||||
if (isset($assigned[$i])) continue;
|
||||
if ($l['lat'] === null) continue;
|
||||
|
||||
$cluster = [$i];
|
||||
$assigned[$i] = true;
|
||||
|
||||
foreach ($laporan as $j => $m) {
|
||||
if ($j === $i || isset($assigned[$j])) continue;
|
||||
if ($m['lat'] === null) continue;
|
||||
if (haversine($l['lat'], $l['lng'], $m['lat'], $m['lng']) <= $radiusM) {
|
||||
$cluster[] = $j;
|
||||
$assigned[$j] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($cluster) >= $minCount) {
|
||||
// Centroid
|
||||
$cLat = array_sum(array_map(fn($k) => $laporan[$k]['lat'], $cluster)) / count($cluster);
|
||||
$cLng = array_sum(array_map(fn($k) => $laporan[$k]['lng'], $cluster)) / count($cluster);
|
||||
$ids = array_map(fn($k) => $laporan[$k]['id'], $cluster);
|
||||
$jalanNames = array_unique(array_map(fn($k) => $laporan[$k]['nama_jalan'], $cluster));
|
||||
|
||||
$clusters[] = [
|
||||
'lat' => $cLat,
|
||||
'lng' => $cLng,
|
||||
'count' => count($cluster),
|
||||
'ids' => $ids,
|
||||
'nama_jalan' => implode(', ', $jalanNames),
|
||||
'urgent' => count($cluster) >= $minCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $clusters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analitik: frekuensi kerusakan per jalan, per periode
|
||||
*/
|
||||
function handleAnalytics(mysqli $conn): void {
|
||||
$tahun = intval($_GET['tahun'] ?? 1);
|
||||
if (!in_array($tahun, [1,2,3,5,10])) $tahun = 1;
|
||||
|
||||
// Frekuensi per jalan
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT nama_jalan,
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) AS resolved,
|
||||
SUM(CASE WHEN status='verified' THEN 1 ELSE 0 END) AS verified,
|
||||
SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) AS pending,
|
||||
MIN(tanggal_input) AS pertama,
|
||||
MAX(tanggal_input) AS terakhir
|
||||
FROM laporan_jalan_rusak
|
||||
WHERE tanggal_input >= DATE_SUB(NOW(), INTERVAL ? YEAR)
|
||||
GROUP BY nama_jalan
|
||||
ORDER BY total DESC"
|
||||
);
|
||||
$stmt->bind_param("i", $tahun);
|
||||
$stmt->execute();
|
||||
$perJalan = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Tren per bulan (12 bulan terakhir)
|
||||
$tren = $conn->query(
|
||||
"SELECT DATE_FORMAT(tanggal_input,'%Y-%m') AS bulan, COUNT(*) AS total
|
||||
FROM laporan_jalan_rusak
|
||||
WHERE tanggal_input >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan ASC"
|
||||
)->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
// Sering rusak: jalan dengan total >= 3 dalam rentang waktu
|
||||
$seringRusak = array_filter($perJalan, fn($r) => (int)$r['total'] >= 3);
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'per_jalan' => $perJalan,
|
||||
'tren_bulanan' => $tren,
|
||||
'sering_rusak' => array_values($seringRusak),
|
||||
'rentang_tahun'=> $tahun,
|
||||
]);
|
||||
}
|
||||
144
jalan_tanah/api/parsil.php
Normal file
144
jalan_tanah/api/parsil.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
// api/parsil.php — CRUD Data Parsil Tanah (GeoJSON Polygon)
|
||||
|
||||
require_once '../config/database.php';
|
||||
setCorsHeaders();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$conn = getConnection();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$id = intval($_GET['id']);
|
||||
$stmt = $conn->prepare("SELECT id, nama_parsil, pemilik, status_sertifikat, nomor_sertifikat, luas_meter2, keterangan, geom, created_at FROM data_parsil WHERE id = ?");
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
if (!$row) { http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']); break; }
|
||||
$row['koordinat'] = parseGeoJsonToLatLng($row['geom']);
|
||||
unset($row['geom']);
|
||||
echo json_encode(['status'=>'success','data'=>$row]);
|
||||
} else {
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
$types = '';
|
||||
|
||||
if (!empty($_GET['status'])) {
|
||||
$where[] = 'status_sertifikat = ?';
|
||||
$params[] = $_GET['status'];
|
||||
$types .= 's';
|
||||
}
|
||||
if (!empty($_GET['min_luas'])) {
|
||||
$where[] = 'luas_meter2 >= ?';
|
||||
$params[] = (float)$_GET['min_luas'];
|
||||
$types .= 'd';
|
||||
}
|
||||
if (!empty($_GET['max_luas'])) {
|
||||
$where[] = 'luas_meter2 <= ?';
|
||||
$params[] = (float)$_GET['max_luas'];
|
||||
$types .= 'd';
|
||||
}
|
||||
if (!empty($_GET['q'])) {
|
||||
$where[] = '(nama_parsil LIKE ? OR pemilik LIKE ?)';
|
||||
$params[] = '%'.$_GET['q'].'%';
|
||||
$params[] = '%'.$_GET['q'].'%';
|
||||
$types .= 'ss';
|
||||
}
|
||||
|
||||
$sql = 'SELECT id, nama_parsil, pemilik, status_sertifikat, nomor_sertifikat, luas_meter2, keterangan, geom, created_at FROM data_parsil WHERE ' . implode(' AND ', $where) . ' ORDER BY created_at DESC';
|
||||
$stmt = $conn->prepare($sql);
|
||||
if ($params) $stmt->bind_param($types, ...$params);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
$data = array_map(function($row) {
|
||||
$row['koordinat'] = parseGeoJsonToLatLng($row['geom']);
|
||||
unset($row['geom']);
|
||||
return $row;
|
||||
}, $rows);
|
||||
|
||||
echo json_encode(['status'=>'success','data'=>$data,'total'=>count($data)]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$input = getJsonInput();
|
||||
if (!$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'Input tidak valid']); break; }
|
||||
|
||||
$nama = trim($input['nama_parsil'] ?? '');
|
||||
$pemilik = trim($input['pemilik'] ?? '');
|
||||
$status = trim($input['status_sertifikat'] ?? '');
|
||||
$nosert = trim($input['nomor_sertifikat'] ?? '');
|
||||
$luas = (float)($input['luas_meter2'] ?? 0);
|
||||
$ket = trim($input['keterangan'] ?? '');
|
||||
$coords = $input['koordinat'] ?? [];
|
||||
|
||||
if (!$nama || !$pemilik || !$status || count($coords) < 3) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status'=>'error','message'=>'Nama, pemilik, status, dan minimal 3 koordinat wajib diisi']);
|
||||
break;
|
||||
}
|
||||
if (!in_array($status, ['SHM','HGB','HGU','HP'])) {
|
||||
http_response_code(400); echo json_encode(['status'=>'error','message'=>'Status tidak valid']); break;
|
||||
}
|
||||
|
||||
$geom = buildGeoJsonPolygon($coords);
|
||||
$stmt = $conn->prepare("INSERT INTO data_parsil (nama_parsil,pemilik,status_sertifikat,nomor_sertifikat,luas_meter2,keterangan,geom) VALUES (?,?,?,?,?,?,?)");
|
||||
$stmt->bind_param("ssssdss", $nama, $pemilik, $status, $nosert, $luas, $ket, $geom);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status'=>'success','message'=>'Data parsil berhasil ditambahkan','id'=>$conn->insert_id]);
|
||||
} else {
|
||||
http_response_code(500); echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$conn->error]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
$input = getJsonInput();
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
if (!$id || !$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID dan input wajib ada']); break; }
|
||||
|
||||
$nama = trim($input['nama_parsil'] ?? '');
|
||||
$pemilik = trim($input['pemilik'] ?? '');
|
||||
$status = trim($input['status_sertifikat'] ?? '');
|
||||
$nosert = trim($input['nomor_sertifikat'] ?? '');
|
||||
$luas = (float)($input['luas_meter2'] ?? 0);
|
||||
$ket = trim($input['keterangan'] ?? '');
|
||||
$coords = $input['koordinat'] ?? [];
|
||||
|
||||
if (!$nama || !$pemilik || !$status) {
|
||||
http_response_code(400); echo json_encode(['status'=>'error','message'=>'Nama, pemilik, dan status wajib diisi']); break;
|
||||
}
|
||||
|
||||
$geom = buildGeoJsonPolygon($coords);
|
||||
$stmt = $conn->prepare("UPDATE data_parsil SET nama_parsil=?,pemilik=?,status_sertifikat=?,nomor_sertifikat=?,luas_meter2=?,keterangan=?,geom=? WHERE id=?");
|
||||
$stmt->bind_param("ssssdssi", $nama, $pemilik, $status, $nosert, $luas, $ket, $geom, $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status'=>'success','message'=>'Data parsil berhasil diperbarui']);
|
||||
} else {
|
||||
http_response_code(500); echo json_encode(['status'=>'error','message'=>'Gagal memperbarui: '.$conn->error]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
if (!$id) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID tidak valid']); break; }
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM data_parsil WHERE id=?");
|
||||
$stmt->bind_param("i", $id);
|
||||
if ($stmt->execute() && $stmt->affected_rows > 0) {
|
||||
echo json_encode(['status'=>'success','message'=>'Data parsil berhasil dihapus']);
|
||||
} else {
|
||||
http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(405);
|
||||
echo json_encode(['status'=>'error','message'=>'Method tidak diizinkan']);
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
108
jalan_tanah/config/database.php
Normal file
108
jalan_tanah/config/database.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// config/database.php
|
||||
// ============================================
|
||||
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', '');
|
||||
define('DB_NAME', 'db_jalan');
|
||||
|
||||
// Path untuk upload foto laporan (relatif dari root project)
|
||||
define('UPLOAD_DIR', __DIR__ . '/../uploads/laporan/');
|
||||
define('UPLOAD_URL', 'uploads/laporan/');
|
||||
define('MAX_FILE_SIZE', 8 * 1024 * 1024); // 8MB
|
||||
define('ALLOWED_TYPES', ['image/jpeg', 'image/png', 'image/webp']);
|
||||
|
||||
function getConnection(): mysqli {
|
||||
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS);
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
die(json_encode(['status' => 'error', 'message' => 'Koneksi database gagal: ' . $conn->connect_error]));
|
||||
}
|
||||
|
||||
// Check if database exists
|
||||
$dbCheck = $conn->query("SHOW DATABASES LIKE '" . DB_NAME . "'");
|
||||
$dbExists = ($dbCheck && $dbCheck->num_rows > 0);
|
||||
|
||||
if (!$dbExists) {
|
||||
$conn->query("CREATE DATABASE IF NOT EXISTS " . DB_NAME . " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
$conn->select_db(DB_NAME);
|
||||
|
||||
// Load db_jalan.sql to seed tables
|
||||
$sqlPath = __DIR__ . '/../database/db_jalan.sql';
|
||||
if (file_exists($sqlPath)) {
|
||||
$sqlContent = file_get_contents($sqlPath);
|
||||
// Remove USE command from raw SQL to avoid conflicts
|
||||
$sqlContent = preg_replace('/USE\s+[a-zA-Z0-9_]+;/i', '', $sqlContent);
|
||||
$conn->multi_query($sqlContent);
|
||||
// Clear multi-query results to prevent synch error
|
||||
do {
|
||||
if ($res = $conn->store_result()) {
|
||||
$res->free();
|
||||
}
|
||||
} while ($conn->next_result());
|
||||
}
|
||||
} else {
|
||||
$conn->select_db(DB_NAME);
|
||||
}
|
||||
|
||||
$conn->set_charset('utf8mb4');
|
||||
return $conn;
|
||||
}
|
||||
|
||||
// CORS & JSON headers — panggil sekali dari tiap API entry point
|
||||
function setCorsHeaders(): void {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: baca JSON body
|
||||
function getJsonInput(): ?array {
|
||||
$raw = file_get_contents('php://input');
|
||||
if (!$raw) return null;
|
||||
$data = json_decode($raw, true);
|
||||
return (json_last_error() === JSON_ERROR_NONE) ? $data : null;
|
||||
}
|
||||
|
||||
// Helper: validasi & encode GeoJSON dari array koordinat [lat,lng]
|
||||
// Frontend tetap kirim [lat,lng] (Leaflet-style), kita konversi ke GeoJSON [lng,lat]
|
||||
function buildGeoJsonLine(array $coords): string {
|
||||
$gjCoords = array_map(fn($p) => [(float)$p[1], (float)$p[0]], $coords);
|
||||
return json_encode(['type' => 'LineString', 'coordinates' => $gjCoords]);
|
||||
}
|
||||
|
||||
function buildGeoJsonPolygon(array $coords): string {
|
||||
$gjCoords = array_map(fn($p) => [(float)$p[1], (float)$p[0]], $coords);
|
||||
// tutup ring jika belum
|
||||
if ($gjCoords[0] !== end($gjCoords)) $gjCoords[] = $gjCoords[0];
|
||||
return json_encode(['type' => 'Polygon', 'coordinates' => [$gjCoords]]);
|
||||
}
|
||||
|
||||
function buildGeoJsonPoint(float $lat, float $lng): string {
|
||||
return json_encode(['type' => 'Point', 'coordinates' => [$lng, $lat]]);
|
||||
}
|
||||
|
||||
// Helper: parse GeoJSON → array [lat,lng] (untuk dikirim ke frontend/Leaflet)
|
||||
function parseGeoJsonToLatLng(?string $geomStr): array {
|
||||
if (!$geomStr) return [];
|
||||
$geom = json_decode($geomStr, true);
|
||||
if (!$geom) return [];
|
||||
|
||||
switch ($geom['type'] ?? '') {
|
||||
case 'LineString':
|
||||
return array_map(fn($c) => [$c[1], $c[0]], $geom['coordinates']);
|
||||
case 'Polygon':
|
||||
return array_map(fn($c) => [$c[1], $c[0]], $geom['coordinates'][0]);
|
||||
case 'Point':
|
||||
return [$geom['coordinates'][1], $geom['coordinates'][0]];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
118
jalan_tanah/database/db_jalan.sql
Normal file
118
jalan_tanah/database/db_jalan.sql
Normal file
@@ -0,0 +1,118 @@
|
||||
-- ============================================
|
||||
-- WebGIS - Manajemen Jalan, Parsil & Laporan Jalan Rusak
|
||||
-- Database Schema (GeoJSON-based)
|
||||
-- ============================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS db_jalan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE db_jalan;
|
||||
|
||||
-- ============================================
|
||||
-- Tabel Data Jalan (GeoJSON LineString)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS data_jalan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_jalan VARCHAR(255) NOT NULL,
|
||||
status_jalan ENUM('Nasional','Provinsi','Kabupaten') NOT NULL,
|
||||
panjang_meter DOUBLE NOT NULL DEFAULT 0,
|
||||
keterangan TEXT,
|
||||
-- GeoJSON LineString: {"type":"LineString","coordinates":[[lng,lat],...]}
|
||||
geom JSON NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- Tabel Data Parsil Tanah (GeoJSON Polygon)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS data_parsil (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_parsil VARCHAR(255) NOT NULL,
|
||||
pemilik VARCHAR(255) NOT NULL,
|
||||
status_sertifikat ENUM('SHM','HGB','HGU','HP') NOT NULL,
|
||||
nomor_sertifikat VARCHAR(100),
|
||||
luas_meter2 DOUBLE NOT NULL DEFAULT 0,
|
||||
keterangan TEXT,
|
||||
-- GeoJSON Polygon: {"type":"Polygon","coordinates":[[[lng,lat],...,[lng,lat]]]}
|
||||
geom JSON NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- Tabel Laporan Jalan Rusak (GeoJSON Point)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS laporan_jalan_rusak (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_pelapor VARCHAR(255), -- opsional
|
||||
nama_jalan VARCHAR(255) NOT NULL, -- untuk analisis frekuensi per jalan
|
||||
deskripsi TEXT,
|
||||
foto_path VARCHAR(500), -- path relatif file foto
|
||||
foto_lat DOUBLE, -- dari metadata exif atau manual
|
||||
foto_lng DOUBLE, -- dari metadata exif atau manual
|
||||
foto_datetime DATETIME, -- dari metadata exif
|
||||
-- GeoJSON Point: {"type":"Point","coordinates":[lng,lat]}
|
||||
geom JSON NOT NULL,
|
||||
status ENUM('pending','verified','resolved') NOT NULL DEFAULT 'pending',
|
||||
tanggal_input TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Index untuk query geospasial & analitik
|
||||
CREATE INDEX idx_laporan_tanggal ON laporan_jalan_rusak(tanggal_input);
|
||||
CREATE INDEX idx_laporan_nama_jalan ON laporan_jalan_rusak(nama_jalan);
|
||||
CREATE INDEX idx_laporan_status ON laporan_jalan_rusak(status);
|
||||
CREATE INDEX idx_jalan_status ON data_jalan(status_jalan);
|
||||
CREATE INDEX idx_parsil_status ON data_parsil(status_sertifikat);
|
||||
|
||||
-- ============================================
|
||||
-- Sample Data Jalan (GeoJSON)
|
||||
-- ============================================
|
||||
INSERT INTO data_jalan (nama_jalan, status_jalan, panjang_meter, keterangan, geom) VALUES
|
||||
(
|
||||
'Jalan Trans Kalimantan', 'Nasional', 2500.50,
|
||||
'Jalan utama lintas Kalimantan',
|
||||
'{"type":"LineString","coordinates":[[109.3425,0.0263],[109.3500,0.0270],[109.3580,0.0280],[109.3650,0.0290]]}'
|
||||
),
|
||||
(
|
||||
'Jalan Soekarno-Hatta', 'Provinsi', 1800.75,
|
||||
'Jalan provinsi kawasan kota',
|
||||
'{"type":"LineString","coordinates":[[109.3300,0.0200],[109.3380,0.0210],[109.3460,0.0225]]}'
|
||||
),
|
||||
(
|
||||
'Jalan Parit Baru', 'Kabupaten', 950.25,
|
||||
'Jalan kabupaten menuju kawasan industri',
|
||||
'{"type":"LineString","coordinates":[[109.3200,0.0150],[109.3270,0.0160],[109.3320,0.0175]]}'
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- Sample Data Parsil (GeoJSON)
|
||||
-- ============================================
|
||||
INSERT INTO data_parsil (nama_parsil, pemilik, status_sertifikat, nomor_sertifikat, luas_meter2, keterangan, geom) VALUES
|
||||
(
|
||||
'Kavling A1', 'Budi Santoso', 'SHM', 'SHM-001/2020', 450.50,
|
||||
'Kavling perumahan blok A',
|
||||
'{"type":"Polygon","coordinates":[[[109.3320,0.0230],[109.3330,0.0230],[109.3330,0.0235],[109.3320,0.0235],[109.3320,0.0230]]]}'
|
||||
),
|
||||
(
|
||||
'Kavling B3', 'PT Maju Bersama', 'HGB', 'HGB-023/2019', 1250.00,
|
||||
'Kavling komersial blok B',
|
||||
'{"type":"Polygon","coordinates":[[[109.3350,0.0240],[109.3365,0.0240],[109.3365,0.0250],[109.3350,0.0250],[109.3350,0.0240]]]}'
|
||||
),
|
||||
(
|
||||
'Lahan Usaha C1', 'CV Subur Makmur', 'HGU', 'HGU-007/2018', 5800.00,
|
||||
'Lahan perkebunan',
|
||||
'{"type":"Polygon","coordinates":[[[109.3270,0.0180],[109.3295,0.0180],[109.3295,0.0195],[109.3270,0.0195],[109.3270,0.0180]]]}'
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- Sample Laporan Jalan Rusak
|
||||
-- ============================================
|
||||
INSERT INTO laporan_jalan_rusak (nama_pelapor, nama_jalan, deskripsi, foto_lat, foto_lng, geom, status, tanggal_input) VALUES
|
||||
('Ahmad Fauzi', 'Jalan Trans Kalimantan', 'Aspal berlubang besar, berbahaya saat hujan', 0.0268, 109.3460, '{"type":"Point","coordinates":[109.3460,0.0268]}', 'pending', NOW() - INTERVAL 5 DAY),
|
||||
('Siti Rahayu', 'Jalan Trans Kalimantan', 'Jalan retak dan bergelombang', 0.0271, 109.3465, '{"type":"Point","coordinates":[109.3465,0.0271]}', 'pending', NOW() - INTERVAL 8 DAY),
|
||||
('Budi Hartono', 'Jalan Trans Kalimantan', 'Lubang besar di tengah jalan', 0.0269, 109.3462, '{"type":"Point","coordinates":[109.3462,0.0269]}', 'verified', NOW() - INTERVAL 3 DAY),
|
||||
('Dewi Lestari', 'Jalan Soekarno-Hatta', 'Marka jalan hilang, genangan air', 0.0215, 109.3385, '{"type":"Point","coordinates":[109.3385,0.0215]}', 'pending', NOW() - INTERVAL 15 DAY),
|
||||
('Eko Prasetyo', 'Jalan Soekarno-Hatta', 'Bahu jalan rusak', 0.0218, 109.3388, '{"type":"Point","coordinates":[109.3388,0.0218]}', 'pending', NOW() - INTERVAL 20 DAY),
|
||||
('Fitri Handayani','Jalan Parit Baru', 'Aspal terkelupas', 0.0165, 109.3275, '{"type":"Point","coordinates":[109.3275,0.0165]}', 'resolved', NOW() - INTERVAL 45 DAY),
|
||||
(NULL, 'Jalan Trans Kalimantan', 'Permukaan jalan sangat rusak parah', 0.0272, 109.3468, '{"type":"Point","coordinates":[109.3468,0.0272]}', 'pending', NOW() - INTERVAL 2 DAY),
|
||||
('Hendra Wijaya', 'Jalan Trans Kalimantan', 'Lubang dalam di tepi jalan', 0.0266, 109.3458, '{"type":"Point","coordinates":[109.3458,0.0266]}', 'pending', NOW() - INTERVAL 1 DAY);
|
||||
1455
jalan_tanah/index.html
Normal file
1455
jalan_tanah/index.html
Normal file
File diff suppressed because it is too large
Load Diff
1
jalan_tanah/uploads/laporan/.gitkeep
Normal file
1
jalan_tanah/uploads/laporan/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep this directory in version control, but ignore contents via .gitignore
|
||||
Reference in New Issue
Block a user