initial commit
This commit is contained in:
+517
@@ -0,0 +1,517 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Portal WebGIS & Aplikasi Spasial</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #2563EB;
|
||||||
|
--primary-dark: #1D4ED8;
|
||||||
|
--primary-light: #60A5FA;
|
||||||
|
--primary-pale: #EFF6FF;
|
||||||
|
--bg: #F8FAFC;
|
||||||
|
--surface: #FFFFFF;
|
||||||
|
--text-primary: #0F172A;
|
||||||
|
--text-secondary: #475569;
|
||||||
|
--text-muted: #94A3B8;
|
||||||
|
--border: #E2E8F0;
|
||||||
|
--border-light: #F1F5F9;
|
||||||
|
--radius: 18px;
|
||||||
|
--radius-sm: 10px;
|
||||||
|
--shadow: 0 10px 30px -10px rgba(37, 99, 235, 0.08), 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
--shadow-hover: 0 20px 40px -15px rgba(37, 99, 235, 0.15), 0 1px 10px rgba(37, 99, 235, 0.05);
|
||||||
|
--tr: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Abstract Background Graphics */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -200px;
|
||||||
|
right: -200px;
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(96, 165, 250, 0.12) 0%, rgba(255, 255, 255, 0) 70%);
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -150px;
|
||||||
|
left: -150px;
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(37, 99, 235, 0.06) 0%, rgba(255, 255, 255, 0) 70%);
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HEADER */
|
||||||
|
header {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 18px 24px;
|
||||||
|
transition: var(--tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-subtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HERO SECTION */
|
||||||
|
.hero {
|
||||||
|
padding: 60px 24px 40px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: var(--primary-pale);
|
||||||
|
color: var(--primary);
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text-primary);
|
||||||
|
letter-spacing: -1px;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 span {
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MAIN CONTENT & GRID */
|
||||||
|
main {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px 80px;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PROJECT CARDS */
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: var(--tr);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--accent, var(--primary));
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: var(--tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
border-color: rgba(37, 99, 235, 0.15);
|
||||||
|
box-shadow: var(--shadow-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover::before {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon-wrap {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--accent-bg, var(--primary-pale));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: var(--accent, var(--primary));
|
||||||
|
transition: var(--tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .card-icon-wrap {
|
||||||
|
transform: scale(1.08) rotate(3deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
letter-spacing: -0.4px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BADGES */
|
||||||
|
.badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: var(--border-light);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: 30px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ACTION BUTTON */
|
||||||
|
.card-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 18px;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--accent, var(--primary));
|
||||||
|
transition: var(--tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
transition: var(--tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .card-action svg {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
footer {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Profile Badge */
|
||||||
|
.user-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: var(--primary-pale);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.12);
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 30px;
|
||||||
|
transition: var(--tr);
|
||||||
|
}
|
||||||
|
.user-badge:hover {
|
||||||
|
background: #E0EEFF;
|
||||||
|
border-color: rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
.user-avatar {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.user-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.user-nim {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESPONSIVE */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero {
|
||||||
|
padding: 40px 16px 20px;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.hero p {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 0 16px 60px;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<a href="#" class="brand">
|
||||||
|
<div class="brand-icon">🌐</div>
|
||||||
|
<div class="brand-text">
|
||||||
|
<span class="brand-title">Portal WebGIS</span>
|
||||||
|
<span class="brand-subtitle">Tugas & UAS Sistem Informasi Geografis</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="user-badge">
|
||||||
|
<div class="user-avatar">CY</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<span class="user-name">Cecilia Yvon Gunawan</span>
|
||||||
|
<span class="user-nim">NIM D1041231056</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- HERO SECTION -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-tag">Kumpulan Proyek SIG</div>
|
||||||
|
<h1>Kumpulan Project <span>WebGIS Interaktif</span></h1>
|
||||||
|
<p>Hub aplikasi spasial interaktif hasil pengerjaan tugas praktikum dan Ujian Akhir Semester (UAS) untuk mata kuliah Sistem Informasi Geografis.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MAIN CONTAINER -->
|
||||||
|
<main>
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<!-- PROJECT 1: Poverty Map -->
|
||||||
|
<a href="http://localhost/webgis/povertymap/" class="card" style="--accent: #4F46E5; --accent-bg: #EEF2FF;">
|
||||||
|
<div class="card-icon-wrap">
|
||||||
|
<!-- Hand holding heart / charity icon -->
|
||||||
|
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>WebGIS Poverty Mapping</h3>
|
||||||
|
<p>Sistem informasi geografis untuk memetakan sebaran kemiskinan warga di sekitar wilayah tempat ibadah guna penargetan program bantuan sosial.</p>
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge">Leaflet.js</span>
|
||||||
|
<span class="badge">MySQL</span>
|
||||||
|
<span class="badge">PHP Backend</span>
|
||||||
|
<span class="badge">Geotagging</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<span>Buka Aplikasi</span>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- PROJECT 2: SPBU Map -->
|
||||||
|
<a href="http://localhost/webgis/spbu/" class="card" style="--accent: #0D9488; --accent-bg: #F0FDF4;">
|
||||||
|
<div class="card-icon-wrap">
|
||||||
|
<!-- Gas pump icon -->
|
||||||
|
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 22V2h10v20M13 10h5M18 10l3 3v6M18 20v2M9 6h1"/>
|
||||||
|
<circle cx="8.5" cy="11.5" r="1.5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>WebGIS SPBU</h3>
|
||||||
|
<p>Aplikasi pemetaan lokasi Stasiun Pengisian Bahan Bakar Umum (SPBU) di wilayah Pontianak, lengkap dengan informasi status operasional 24 jam.</p>
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge">Leaflet.js</span>
|
||||||
|
<span class="badge">MySQL</span>
|
||||||
|
<span class="badge">PHP WebApp</span>
|
||||||
|
<span class="badge">Interactive Map</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<span>Buka Aplikasi</span>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- PROJECT 3: Parsil Jalan Laporan -->
|
||||||
|
<a href="http://localhost/webgis/parsil_jalan_jalanrusak/" class="card" style="--accent: #2563EB; --accent-bg: #EFF6FF;">
|
||||||
|
<div class="card-icon-wrap">
|
||||||
|
<!-- Road/infrastructure icon -->
|
||||||
|
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 20h18M6 20v-4a6 6 0 0112 0v4M12 3v10M12 17h.01M9 7l3-3 3 3"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>WebGIS Jalan, Parsil Tanah, & Jalan Rusak</h3>
|
||||||
|
<p>Portal pemetaan spasial terintegrasi untuk jaringan rute jalan raya, batas legalitas parsil tanah, serta pelaporan kerusakan jalan berbasis partisipasi warga.</p>
|
||||||
|
<div class="badges">
|
||||||
|
<span class="badge">Turf.js</span>
|
||||||
|
<span class="badge">Leaflet.js</span>
|
||||||
|
<span class="badge">MySQL</span>
|
||||||
|
<span class="badge">Geotagging</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<span>Buka Aplikasi</span>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<p style="font-weight: 700; color: var(--text-primary); margin-bottom: 2px;">Cecilia Yvon Gunawan — NIM D1041231056</p>
|
||||||
|
<p>© 2026 Portal WebGIS Spasial. All rights reserved.</p>
|
||||||
|
<p style="font-size: 11px; color: var(--text-muted);">Dikembangkan menggunakan HTML5, Vanilla CSS, Javascript & LeafletJS.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# 🗺️ WebGIS Manajemen Jalan & Parsil Tanah
|
||||||
|
|
||||||
|
Sistem Informasi Geografis berbasis web untuk manajemen data spasial jalan dan parsil tanah.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Struktur File
|
||||||
|
|
||||||
|
```
|
||||||
|
webgis/
|
||||||
|
├── index.html ← Halaman utama WebGIS
|
||||||
|
├── database.sql ← Schema & sample data MySQL
|
||||||
|
├── README.md ← Dokumentasi ini
|
||||||
|
└── api/
|
||||||
|
├── config.php ← Konfigurasi koneksi database
|
||||||
|
├── jalan.php ← API CRUD Data Jalan
|
||||||
|
└── parsil.php ← API CRUD Data Parsil Tanah
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Cara Install
|
||||||
|
|
||||||
|
### 1. Persyaratan
|
||||||
|
- **Web Server**: Apache / Nginx dengan PHP 7.4+
|
||||||
|
- **Database**: MySQL 5.7+ atau MariaDB 10.3+
|
||||||
|
- **Browser**: Chrome, Firefox, Edge (modern)
|
||||||
|
|
||||||
|
### 2. Setup Database
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Jalankan di MySQL / phpMyAdmin
|
||||||
|
SOURCE database.sql;
|
||||||
|
```
|
||||||
|
|
||||||
|
Atau import file `database.sql` melalui phpMyAdmin.
|
||||||
|
|
||||||
|
### 3. Konfigurasi Database
|
||||||
|
|
||||||
|
Edit file `api/config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
define('DB_HOST', 'localhost'); // Host database
|
||||||
|
define('DB_USER', 'root'); // Username MySQL
|
||||||
|
define('DB_PASS', ''); // Password MySQL
|
||||||
|
define('DB_NAME', 'webgis_db'); // Nama database
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Deploy ke Web Server
|
||||||
|
|
||||||
|
**Menggunakan XAMPP/WAMP/LAMP:**
|
||||||
|
```bash
|
||||||
|
# Copy folder webgis/ ke:
|
||||||
|
# Windows XAMPP: C:/xampp/htdocs/webgis/
|
||||||
|
# Linux LAMP: /var/www/html/webgis/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Akses aplikasi:**
|
||||||
|
```
|
||||||
|
http://localhost/polyline_polygon/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 Fitur Utama
|
||||||
|
|
||||||
|
### 🛣️ Manajemen Data Jalan (Polyline)
|
||||||
|
| Fitur | Deskripsi |
|
||||||
|
|-------|-----------|
|
||||||
|
| **Tambah Jalan** | Gambar polyline di peta atau input koordinat manual |
|
||||||
|
| **Status Jalan** | Nasional (merah), Provinsi (oranye), Kabupaten (hijau) |
|
||||||
|
| **Panjang Otomatis** | Dihitung menggunakan `L.latLng.distanceTo()` dari LeafletJS |
|
||||||
|
| **Edit & Hapus** | CRUD lengkap dengan konfirmasi hapus |
|
||||||
|
| **Drag Titik** | Titik koordinat bisa diubah langsung di tabel |
|
||||||
|
|
||||||
|
### 🏘️ Manajemen Parsil Tanah (Polygon)
|
||||||
|
| Fitur | Deskripsi |
|
||||||
|
|-------|-----------|
|
||||||
|
| **Tambah Parsil** | Gambar polygon di peta atau input koordinat manual |
|
||||||
|
| **Status Kepemilikan** | SHM, HGB, HGU, HP — masing-masing warna berbeda |
|
||||||
|
| **Luas Otomatis** | Dihitung dengan formula Spherical Excess dari LeafletJS |
|
||||||
|
| **Data Lengkap** | Nama parsil, nomor sertifikat, nama pemilik, keterangan |
|
||||||
|
|
||||||
|
### 🗺️ Fitur Peta
|
||||||
|
- **Basemap**: OpenStreetMap + Esri Satellite (switchable)
|
||||||
|
- **Draw Mode**: Klik peta untuk menggambar fitur spasial
|
||||||
|
- **Popup Interaktif**: Klik fitur di peta untuk melihat detail
|
||||||
|
- **Zoom to Feature**: Langsung zoom ke lokasi dari sidebar
|
||||||
|
- **Koordinat Real-time**: Tampil di pojok kiri bawah
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Kode Warna
|
||||||
|
|
||||||
|
### Jalan
|
||||||
|
| Status | Warna | Hex |
|
||||||
|
|--------|-------|-----|
|
||||||
|
| Jalan Nasional | 🔴 Merah | `#E63946` |
|
||||||
|
| Jalan Provinsi | 🟠 Oranye | `#F4A261` |
|
||||||
|
| Jalan Kabupaten | 🟢 Hijau | `#2A9D8F` |
|
||||||
|
|
||||||
|
### Parsil Tanah
|
||||||
|
| Status | Warna | Hex |
|
||||||
|
|--------|-------|-----|
|
||||||
|
| SHM | 🔵 Biru | `#4361EE` |
|
||||||
|
| HGB | 🟣 Ungu | `#7209B7` |
|
||||||
|
| HGU | 🌸 Pink | `#F72585` |
|
||||||
|
| HP | 🩵 Cyan | `#4CC9F0` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 API Endpoints
|
||||||
|
|
||||||
|
### Jalan
|
||||||
|
```
|
||||||
|
GET api/jalan.php → Ambil semua data jalan
|
||||||
|
GET api/jalan.php?id={id} → Ambil data jalan by ID
|
||||||
|
POST api/jalan.php → Tambah data jalan baru
|
||||||
|
PUT api/jalan.php?id={id} → Update data jalan
|
||||||
|
DELETE api/jalan.php?id={id} → Hapus data jalan
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parsil Tanah
|
||||||
|
```
|
||||||
|
GET api/parsil.php → Ambil semua data parsil
|
||||||
|
GET api/parsil.php?id={id} → Ambil data parsil by ID
|
||||||
|
POST api/parsil.php → Tambah data parsil baru
|
||||||
|
PUT api/parsil.php?id={id} → Update data parsil
|
||||||
|
DELETE api/parsil.php?id={id} → Hapus data parsil
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contoh Request Body (POST/PUT Jalan)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nama_jalan": "Jalan Ahmad Yani",
|
||||||
|
"status_jalan": "Jalan Nasional",
|
||||||
|
"panjang_meter": 1250.5,
|
||||||
|
"koordinat": [
|
||||||
|
[0.0032, -109.3245],
|
||||||
|
[0.0051, -109.3180],
|
||||||
|
[0.0068, -109.3110]
|
||||||
|
],
|
||||||
|
"keterangan": "Jalan utama kota"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contoh Request Body (POST/PUT Parsil)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nama_parsil": "Kavling A-001",
|
||||||
|
"nomor_sertifikat": "SHM-2024-001",
|
||||||
|
"status_kepemilikan": "SHM",
|
||||||
|
"luas_meter2": 450.0,
|
||||||
|
"koordinat": [
|
||||||
|
[0.0010, -109.3280],
|
||||||
|
[0.0010, -109.3260],
|
||||||
|
[0.0020, -109.3260],
|
||||||
|
[0.0020, -109.3280]
|
||||||
|
],
|
||||||
|
"nama_pemilik": "Budi Santoso",
|
||||||
|
"keterangan": "Kavling perumahan"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Cara Penggunaan
|
||||||
|
|
||||||
|
### Menambah Data Jalan Baru
|
||||||
|
1. Klik tab **"Data Jalan"** di topbar
|
||||||
|
2. Klik **"✚ Tambah Data Baru"** di sidebar
|
||||||
|
3. **Opsi A**: Klik **"✏️ Gambar di Peta"** → klik titik-titik di peta → klik **"⏹ Selesai Gambar"**
|
||||||
|
4. **Opsi B**: Isi koordinat manual di tabel (lat/lng double)
|
||||||
|
5. Isi nama jalan, pilih status, tambah keterangan
|
||||||
|
6. Panjang jalan otomatis terhitung oleh LeafletJS
|
||||||
|
7. Klik **"💾 Simpan Data"**
|
||||||
|
|
||||||
|
### Menambah Data Parsil Baru
|
||||||
|
1. Klik tab **"Parsil Tanah"** di topbar
|
||||||
|
2. Klik **"✚ Tambah Data Baru"**
|
||||||
|
3. Gambar polygon di peta (min. 3 titik) atau isi koordinat manual
|
||||||
|
4. Luas tanah otomatis terhitung oleh LeafletJS
|
||||||
|
5. Isi nama parsil, nomor sertifikat, status kepemilikan, nama pemilik
|
||||||
|
6. Klik **"💾 Simpan Data"**
|
||||||
|
|
||||||
|
### Edit Data
|
||||||
|
1. Klik **"✏️ Edit"** pada kartu data di sidebar, atau klik fitur di peta → klik Edit di popup
|
||||||
|
2. Ubah data yang diperlukan, termasuk koordinat
|
||||||
|
3. Klik **"💾 Simpan Data"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Mode Demo
|
||||||
|
|
||||||
|
Jika server PHP belum terkonfigurasi, aplikasi otomatis berjalan dalam **Mode Demo** dengan data sample. Semua operasi CRUD tetap berfungsi secara lokal di browser (tidak tersimpan ke database).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Teknologi
|
||||||
|
|
||||||
|
- **Frontend**: HTML5, CSS3 (Custom Properties), Vanilla JavaScript
|
||||||
|
- **Peta**: LeafletJS 1.9.4 + Leaflet.draw 1.0.4
|
||||||
|
- **Backend**: PHP 7.4+ (REST API)
|
||||||
|
- **Database**: MySQL / MariaDB
|
||||||
|
- **Basemap**: OpenStreetMap, Esri World Imagery
|
||||||
|
- **Font**: Plus Jakarta Sans, DM Mono (Google Fonts)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
// config.php - Konfigurasi Database
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_USER', 'root');
|
||||||
|
define('DB_PASS', '');
|
||||||
|
define('DB_NAME', 'polyline_polygon');
|
||||||
|
|
||||||
|
function getDB() {
|
||||||
|
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
die(json_encode(['success' => false, 'message' => 'Koneksi database gagal: ' . $conn->connect_error]));
|
||||||
|
}
|
||||||
|
$conn->set_charset('utf8mb4');
|
||||||
|
return $conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set header untuk API
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
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') {
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
// api/jalan.php - API CRUD untuk Data Jalan (Format GeoJSON)
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET': getJalan($id); break;
|
||||||
|
case 'POST': createJalan(); break;
|
||||||
|
case 'PUT': updateJalan($id); break;
|
||||||
|
case 'DELETE': deleteJalan($id); break;
|
||||||
|
default:
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Helper: baris DB → array siap kirim ke client
|
||||||
|
// Mengembalikan struktur flat yang dipakai frontend,
|
||||||
|
// dengan field `koordinat` berisi [[lat,lng],...] agar
|
||||||
|
// Leaflet langsung bisa dipakai, PLUS field `geojson`
|
||||||
|
// berisi GeoJSON Feature asli.
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function rowToResponse($row) {
|
||||||
|
$feature = json_decode($row['geojson'], true);
|
||||||
|
|
||||||
|
// GeoJSON coords = [lng, lat] → ubah ke [lat, lng] untuk Leaflet
|
||||||
|
$coords_geojson = $feature['geometry']['coordinates'] ?? [];
|
||||||
|
$koordinat_leaflet = array_map(fn($c) => [$c[1], $c[0]], $coords_geojson);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int)$row['id'],
|
||||||
|
'nama_jalan' => $row['nama_jalan'],
|
||||||
|
'status_jalan' => $row['status_jalan'],
|
||||||
|
'panjang_meter' => (float)$row['panjang_meter'],
|
||||||
|
'koordinat' => $koordinat_leaflet, // [lat,lng] untuk Leaflet
|
||||||
|
'geojson' => $feature, // GeoJSON Feature asli
|
||||||
|
'created_at' => $row['created_at'],
|
||||||
|
'updated_at' => $row['updated_at'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Helper: koordinat [[lat,lng],...] → GeoJSON Feature string
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function buildGeoJSON($nama, $status, $panjang, $koordinat_leaflet) {
|
||||||
|
// Leaflet: [lat, lng] → GeoJSON: [lng, lat]
|
||||||
|
$coords = array_map(fn($c) => [(float)$c[1], (float)$c[0]], $koordinat_leaflet);
|
||||||
|
|
||||||
|
$feature = [
|
||||||
|
'type' => 'Feature',
|
||||||
|
'properties' => [
|
||||||
|
'nama_jalan' => $nama,
|
||||||
|
'status_jalan' => $status,
|
||||||
|
'panjang_meter' => (float)$panjang,
|
||||||
|
],
|
||||||
|
'geometry' => [
|
||||||
|
'type' => 'LineString',
|
||||||
|
'coordinates' => $coords,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
return json_encode($feature, JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJalan($id = null) {
|
||||||
|
$db = getDB();
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $db->prepare("SELECT * FROM jalan WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
if ($row) {
|
||||||
|
echo json_encode(['success' => true, 'data' => rowToResponse($row)]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$result = $db->query("SELECT * FROM jalan ORDER BY created_at DESC");
|
||||||
|
$rows = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$rows[] = rowToResponse($row);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true, 'data' => $rows, 'total' => count($rows)]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createJalan() {
|
||||||
|
$db = getDB();
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($input['nama_jalan']) || empty($input['status_jalan']) || empty($input['koordinat'])) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nama = $db->real_escape_string($input['nama_jalan']);
|
||||||
|
$status = $db->real_escape_string($input['status_jalan']);
|
||||||
|
$panjang = (float)($input['panjang_meter'] ?? 0);
|
||||||
|
$geojson = buildGeoJSON($nama, $status, $panjang, $input['koordinat']);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, geojson) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->bind_param('ssds', $nama, $status, $panjang, $geojson);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Data jalan berhasil disimpan', 'id' => $db->insert_id]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $db->error]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJalan($id) {
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
|
||||||
|
$db = getDB();
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
$nama = $db->real_escape_string($input['nama_jalan']);
|
||||||
|
$status = $db->real_escape_string($input['status_jalan']);
|
||||||
|
$panjang = (float)($input['panjang_meter'] ?? 0);
|
||||||
|
$geojson = buildGeoJSON($nama, $status, $panjang, $input['koordinat']);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE jalan SET nama_jalan=?, status_jalan=?, panjang_meter=?, geojson=?, updated_at=NOW() WHERE id=?");
|
||||||
|
$stmt->bind_param('ssdsi', $nama, $status, $panjang, $geojson, $id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Data jalan berhasil diperbarui']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal memperbarui data: ' . $db->error]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteJalan($id) {
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("DELETE FROM jalan WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Data jalan berhasil dihapus']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal menghapus data']);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
<?php
|
||||||
|
// api/laporan.php - API CRUD untuk Laporan Kerusakan Jalan (Format GeoJSON Point)
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET': getLaporan($id); break;
|
||||||
|
case 'POST': createLaporan(); break;
|
||||||
|
case 'PUT': updateLaporan($id); break;
|
||||||
|
case 'DELETE': deleteLaporan($id); break;
|
||||||
|
default:
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Info anggaran berdasarkan status jalan
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function getInfoAnggaran($status_jalan) {
|
||||||
|
switch ($status_jalan) {
|
||||||
|
case 'Jalan Nasional':
|
||||||
|
return ['pengelola' => 'Kementerian PUPR', 'sumber' => 'APBN'];
|
||||||
|
case 'Jalan Provinsi':
|
||||||
|
return ['pengelola' => 'Dinas PUPR Provinsi', 'sumber' => 'APBD Provinsi'];
|
||||||
|
case 'Jalan Kabupaten':
|
||||||
|
return ['pengelola' => 'Dinas PUPR Kabupaten/Kota', 'sumber' => 'APBD Kabupaten'];
|
||||||
|
default:
|
||||||
|
return ['pengelola' => 'Tidak Diketahui', 'sumber' => '-'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Helper: baris DB → array siap kirim ke client
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function rowToResponse($row) {
|
||||||
|
$feature = json_decode($row['geojson'], true);
|
||||||
|
$coords_geojson = $feature['geometry']['coordinates'] ?? [0, 0];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int)$row['id'],
|
||||||
|
'jalan_id' => (int)$row['jalan_id'],
|
||||||
|
'nama_jalan' => $row['nama_jalan'],
|
||||||
|
'status_jalan' => $row['status_jalan'],
|
||||||
|
'info_anggaran' => getInfoAnggaran($row['status_jalan']),
|
||||||
|
'nama_pelapor' => $row['nama_pelapor'],
|
||||||
|
'kategori_rusak' => $row['kategori_rusak'],
|
||||||
|
'tanggal_laporan' => $row['tanggal_laporan'],
|
||||||
|
'keterangan' => $row['keterangan'],
|
||||||
|
'foto_url' => $row['foto_url'],
|
||||||
|
'lat' => $coords_geojson[1],
|
||||||
|
'lng' => $coords_geojson[0],
|
||||||
|
'geojson' => $feature,
|
||||||
|
'created_at' => $row['created_at'],
|
||||||
|
'updated_at' => $row['updated_at'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLaporan($id = null) {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Query parameter opsional: filter bulan, tahun, jalan_id
|
||||||
|
$where = ['1=1'];
|
||||||
|
$params = [];
|
||||||
|
$types = '';
|
||||||
|
|
||||||
|
if (isset($_GET['bulan_terakhir'])) {
|
||||||
|
$n = (int)$_GET['bulan_terakhir'];
|
||||||
|
$where[] = "tanggal_laporan >= DATE_SUB(CURDATE(), INTERVAL ? MONTH)";
|
||||||
|
$params[] = $n;
|
||||||
|
$types .= 'i';
|
||||||
|
}
|
||||||
|
if (isset($_GET['tahun_terakhir'])) {
|
||||||
|
$n = (int)$_GET['tahun_terakhir'];
|
||||||
|
$where[] = "tanggal_laporan >= DATE_SUB(CURDATE(), INTERVAL ? YEAR)";
|
||||||
|
$params[] = $n;
|
||||||
|
$types .= 'i';
|
||||||
|
}
|
||||||
|
if (isset($_GET['jalan_id'])) {
|
||||||
|
$where[] = "jalan_id = ?";
|
||||||
|
$params[] = (int)$_GET['jalan_id'];
|
||||||
|
$types .= 'i';
|
||||||
|
}
|
||||||
|
if (isset($_GET['kategori'])) {
|
||||||
|
$where[] = "kategori_rusak = ?";
|
||||||
|
$params[] = $_GET['kategori'];
|
||||||
|
$types .= 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $db->prepare("SELECT * FROM laporan_kerusakan WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
if ($row) {
|
||||||
|
echo json_encode(['success' => true, 'data' => rowToResponse($row)]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$sql = "SELECT * FROM laporan_kerusakan WHERE " . implode(' AND ', $where) . " ORDER BY tanggal_laporan DESC";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
if ($params) {
|
||||||
|
$stmt->bind_param($types, ...$params);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$rows = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$rows[] = rowToResponse($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung jalan yang sering rusak (muncul >= 3 laporan dalam 3 tahun)
|
||||||
|
$res_sering = $db->query("
|
||||||
|
SELECT jalan_id, nama_jalan, COUNT(*) as jumlah
|
||||||
|
FROM laporan_kerusakan
|
||||||
|
WHERE tanggal_laporan >= DATE_SUB(CURDATE(), INTERVAL 3 YEAR)
|
||||||
|
GROUP BY jalan_id, nama_jalan
|
||||||
|
HAVING jumlah >= 3
|
||||||
|
ORDER BY jumlah DESC
|
||||||
|
");
|
||||||
|
$sering_rusak = [];
|
||||||
|
while ($sr = $res_sering->fetch_assoc()) {
|
||||||
|
$sering_rusak[] = ['jalan_id' => (int)$sr['jalan_id'], 'nama_jalan' => $sr['nama_jalan'], 'jumlah_laporan' => (int)$sr['jumlah']];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $rows,
|
||||||
|
'total' => count($rows),
|
||||||
|
'sering_rusak' => $sering_rusak
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLaporan() {
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Cek apakah ada upload foto
|
||||||
|
$foto_url = null;
|
||||||
|
if (!empty($_FILES['foto']['tmp_name'])) {
|
||||||
|
$ext = pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION);
|
||||||
|
$filename = 'laporan_' . time() . '_' . rand(1000,9999) . '.' . strtolower($ext);
|
||||||
|
$upload_dir = '../uploads/laporan/';
|
||||||
|
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
|
||||||
|
if (move_uploaded_file($_FILES['foto']['tmp_name'], $upload_dir . $filename)) {
|
||||||
|
$foto_url = 'uploads/laporan/' . $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baca JSON dari body (jika bukan multipart)
|
||||||
|
$input = [];
|
||||||
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||||
|
if (strpos($contentType, 'application/json') !== false) {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
// Foto bisa base64 embedded
|
||||||
|
if (!empty($input['foto_base64'])) {
|
||||||
|
$imgData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $input['foto_base64']));
|
||||||
|
$filename = 'laporan_' . time() . '_' . rand(1000,9999) . '.jpg';
|
||||||
|
$upload_dir = '../uploads/laporan/';
|
||||||
|
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
|
||||||
|
file_put_contents($upload_dir . $filename, $imgData);
|
||||||
|
$foto_url = 'uploads/laporan/' . $filename;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($input['lat']) || empty($input['lng']) || empty($input['kategori_rusak'])) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap (lat, lng, kategori_rusak wajib)']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lat = (float)$input['lat'];
|
||||||
|
$lng = (float)$input['lng'];
|
||||||
|
$jalan_id = (int)($input['jalan_id'] ?? 0);
|
||||||
|
$nama_jalan = $db->real_escape_string($input['nama_jalan'] ?? 'Tidak Diketahui');
|
||||||
|
$status_jalan = $db->real_escape_string($input['status_jalan'] ?? '');
|
||||||
|
$nama_pelapor = $db->real_escape_string($input['nama_pelapor'] ?? 'Anonim');
|
||||||
|
$kategori = $db->real_escape_string($input['kategori_rusak']);
|
||||||
|
$tanggal = $db->real_escape_string($input['tanggal_laporan'] ?? date('Y-m-d'));
|
||||||
|
$keterangan = $db->real_escape_string($input['keterangan'] ?? '');
|
||||||
|
|
||||||
|
// Build GeoJSON Point
|
||||||
|
$geojson = json_encode([
|
||||||
|
'type' => 'Feature',
|
||||||
|
'properties' => [
|
||||||
|
'nama_jalan' => $nama_jalan,
|
||||||
|
'status_jalan' => $status_jalan,
|
||||||
|
'kategori_rusak' => $kategori,
|
||||||
|
'tanggal_laporan'=> $tanggal,
|
||||||
|
'nama_pelapor' => $nama_pelapor,
|
||||||
|
],
|
||||||
|
'geometry' => [
|
||||||
|
'type' => 'Point',
|
||||||
|
'coordinates' => [$lng, $lat], // GeoJSON: [lng, lat]
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("INSERT INTO laporan_kerusakan (jalan_id, nama_jalan, status_jalan, nama_pelapor, kategori_rusak, tanggal_laporan, keterangan, foto_url, geojson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->bind_param('issssssss', $jalan_id, $nama_jalan, $status_jalan, $nama_pelapor, $kategori, $tanggal, $keterangan, $foto_url, $geojson);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Laporan berhasil disimpan', 'id' => $db->insert_id]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan: ' . $db->error]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLaporan($id) {
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
|
||||||
|
$db = getDB();
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
$kategori = $db->real_escape_string($input['kategori_rusak'] ?? '');
|
||||||
|
$tanggal = $db->real_escape_string($input['tanggal_laporan'] ?? date('Y-m-d'));
|
||||||
|
$keterangan = $db->real_escape_string($input['keterangan'] ?? '');
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE laporan_kerusakan SET kategori_rusak=?, tanggal_laporan=?, keterangan=?, updated_at=NOW() WHERE id=?");
|
||||||
|
$stmt->bind_param('sssi', $kategori, $tanggal, $keterangan, $id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Laporan berhasil diperbarui']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal memperbarui: ' . $db->error]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteLaporan($id) {
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("DELETE FROM laporan_kerusakan WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Laporan berhasil dihapus']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal menghapus: ' . $db->error]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
// api/parsil.php - API CRUD untuk Data Parsil Tanah (Format GeoJSON)
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET': getParsil($id); break;
|
||||||
|
case 'POST': createParsil(); break;
|
||||||
|
case 'PUT': updateParsil($id); break;
|
||||||
|
case 'DELETE': deleteParsil($id); break;
|
||||||
|
default:
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Method tidak diizinkan']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Helper: baris DB → array siap kirim ke client
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function rowToResponse($row) {
|
||||||
|
$feature = json_decode($row['geojson'], true);
|
||||||
|
|
||||||
|
// GeoJSON Polygon coords = array of rings, ring[0] = outer ring
|
||||||
|
// Setiap titik [lng, lat] → ubah ke [lat, lng] untuk Leaflet
|
||||||
|
// Hilangkan titik penutup (sama dengan titik pertama) yang GeoJSON haruskan
|
||||||
|
$ring_geojson = $feature['geometry']['coordinates'][0] ?? [];
|
||||||
|
$koordinat_leaflet = array_map(fn($c) => [$c[1], $c[0]], $ring_geojson);
|
||||||
|
// Hapus titik penutup jika sama dengan titik pertama
|
||||||
|
$n = count($koordinat_leaflet);
|
||||||
|
if ($n > 1 && $koordinat_leaflet[0] === $koordinat_leaflet[$n - 1]) {
|
||||||
|
array_pop($koordinat_leaflet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int)$row['id'],
|
||||||
|
'nama_parsil' => $row['nomor_sertifikat'] ?? $row['nama_pemilik'] ?? 'Parsil',
|
||||||
|
'nomor_sertifikat' => $row['nomor_sertifikat'],
|
||||||
|
'status_kepemilikan' => $row['status_kepemilikan'],
|
||||||
|
'luas_meter2' => (float)$row['luas_meter2'],
|
||||||
|
'koordinat' => $koordinat_leaflet, // [lat,lng] untuk Leaflet
|
||||||
|
'geojson' => $feature, // GeoJSON Feature asli
|
||||||
|
'nama_pemilik' => $row['nama_pemilik'],
|
||||||
|
'created_at' => $row['created_at'],
|
||||||
|
'updated_at' => $row['updated_at'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Helper: koordinat [[lat,lng],...] → GeoJSON Feature string
|
||||||
|
// GeoJSON Polygon ring harus ditutup (titik pertama = terakhir)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
function buildGeoJSON($nosert, $status, $luas, $pemilik, $koordinat_leaflet) {
|
||||||
|
// Leaflet: [lat, lng] → GeoJSON: [lng, lat]
|
||||||
|
$coords = array_map(fn($c) => [(float)$c[1], (float)$c[0]], $koordinat_leaflet);
|
||||||
|
|
||||||
|
// Tutup ring: tambahkan titik pertama di akhir jika belum
|
||||||
|
if (!empty($coords) && $coords[0] !== $coords[count($coords) - 1]) {
|
||||||
|
$coords[] = $coords[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$feature = [
|
||||||
|
'type' => 'Feature',
|
||||||
|
'properties' => [
|
||||||
|
'nomor_sertifikat' => $nosert,
|
||||||
|
'status_kepemilikan' => $status,
|
||||||
|
'luas_meter2' => (float)$luas,
|
||||||
|
'nama_pemilik' => $pemilik,
|
||||||
|
],
|
||||||
|
'geometry' => [
|
||||||
|
'type' => 'Polygon',
|
||||||
|
'coordinates' => [$coords], // array of rings
|
||||||
|
],
|
||||||
|
];
|
||||||
|
return json_encode($feature, JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParsil($id = null) {
|
||||||
|
$db = getDB();
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $db->prepare("SELECT * FROM parsil_tanah WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$row = $result->fetch_assoc();
|
||||||
|
if ($row) {
|
||||||
|
echo json_encode(['success' => true, 'data' => rowToResponse($row)]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data tidak ditemukan']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$result = $db->query("SELECT * FROM parsil_tanah ORDER BY created_at DESC");
|
||||||
|
$rows = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$rows[] = rowToResponse($row);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true, 'data' => $rows, 'total' => count($rows)]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParsil() {
|
||||||
|
$db = getDB();
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($input['status_kepemilikan']) || empty($input['koordinat'])) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nosert = $db->real_escape_string($input['nomor_sertifikat'] ?? '');
|
||||||
|
$status = $db->real_escape_string($input['status_kepemilikan']);
|
||||||
|
$luas = (float)($input['luas_meter2'] ?? 0);
|
||||||
|
$pemilik = $db->real_escape_string($input['nama_pemilik'] ?? '');
|
||||||
|
$geojson = buildGeoJSON($nosert, $status, $luas, $pemilik, $input['koordinat']);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("INSERT INTO parsil_tanah (nomor_sertifikat, status_kepemilikan, luas_meter2, geojson, nama_pemilik) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$stmt->bind_param('ssdss', $nosert, $status, $luas, $geojson, $pemilik);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Data parsil berhasil disimpan', 'id' => $db->insert_id]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $db->error]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateParsil($id) {
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
|
||||||
|
$db = getDB();
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
$nosert = $db->real_escape_string($input['nomor_sertifikat'] ?? '');
|
||||||
|
$status = $db->real_escape_string($input['status_kepemilikan']);
|
||||||
|
$luas = (float)($input['luas_meter2'] ?? 0);
|
||||||
|
$pemilik = $db->real_escape_string($input['nama_pemilik'] ?? '');
|
||||||
|
$geojson = buildGeoJSON($nosert, $status, $luas, $pemilik, $input['koordinat']);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE parsil_tanah SET nomor_sertifikat=?, status_kepemilikan=?, luas_meter2=?, geojson=?, nama_pemilik=?, updated_at=NOW() WHERE id=?");
|
||||||
|
$stmt->bind_param('ssdssi', $nosert, $status, $luas, $geojson, $pemilik, $id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Data parsil berhasil diperbarui']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal memperbarui data: ' . $db->error]);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteParsil($id) {
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID diperlukan']); return; }
|
||||||
|
$db = getDB();
|
||||||
|
$stmt = $db->prepare("DELETE FROM parsil_tanah WHERE id = ?");
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Data parsil berhasil dihapus']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Gagal menghapus data']);
|
||||||
|
}
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- WebGIS Manajemen Jalan & Parsil Tanah
|
||||||
|
-- Database Schema — Format GeoJSON
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS polyline_polygon CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE polyline_polygon;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Tabel Data Jalan (LineString GeoJSON)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS jalan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_jalan VARCHAR(255) NOT NULL,
|
||||||
|
status_jalan ENUM('Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten') NOT NULL,
|
||||||
|
panjang_meter DOUBLE DEFAULT 0,
|
||||||
|
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON Feature object with geometry type LineString',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Tabel Data Parsil Tanah (Polygon GeoJSON)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS parsil_tanah (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nomor_sertifikat VARCHAR(100),
|
||||||
|
status_kepemilikan ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
|
||||||
|
luas_meter2 DOUBLE DEFAULT 0,
|
||||||
|
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON Feature object with geometry type Polygon',
|
||||||
|
nama_pemilik VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Tabel Laporan Kerusakan Jalan (Point GeoJSON)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS laporan_kerusakan (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
jalan_id INT DEFAULT 0 COMMENT 'FK ke tabel jalan (0 jika tidak terdeteksi)',
|
||||||
|
nama_jalan VARCHAR(255) DEFAULT 'Tidak Diketahui',
|
||||||
|
status_jalan VARCHAR(50) DEFAULT '',
|
||||||
|
nama_pelapor VARCHAR(255) DEFAULT 'Anonim',
|
||||||
|
kategori_rusak ENUM('Ringan', 'Sedang', 'Berat') NOT NULL,
|
||||||
|
tanggal_laporan DATE NOT NULL,
|
||||||
|
keterangan TEXT,
|
||||||
|
foto_url VARCHAR(500) DEFAULT NULL COMMENT 'Path foto kerusakan yang diupload',
|
||||||
|
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON Feature Point [lng, lat]',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_jalan_id (jalan_id),
|
||||||
|
INDEX idx_tanggal (tanggal_laporan),
|
||||||
|
INDEX idx_kategori (kategori_rusak)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Sample Data Jalan
|
||||||
|
-- Catatan: GeoJSON menggunakan [lng, lat] (bukan [lat, lng])
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, geojson) VALUES
|
||||||
|
(
|
||||||
|
'Jalan Ahmad Yani',
|
||||||
|
'Jalan Nasional',
|
||||||
|
1250.5,
|
||||||
|
'{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"nama_jalan": "Jalan Ahmad Yani",
|
||||||
|
"status_jalan": "Jalan Nasional",
|
||||||
|
"panjang_meter": 1250.5
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "LineString",
|
||||||
|
"coordinates": [
|
||||||
|
[109.3300, -0.0220],
|
||||||
|
[109.3340, -0.0235],
|
||||||
|
[109.3375, -0.0250],
|
||||||
|
[109.3410, -0.0263]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Jalan Gajah Mada',
|
||||||
|
'Jalan Provinsi',
|
||||||
|
870.3,
|
||||||
|
'{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"nama_jalan": "Jalan Gajah Mada",
|
||||||
|
"status_jalan": "Jalan Provinsi",
|
||||||
|
"panjang_meter": 870.3
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "LineString",
|
||||||
|
"coordinates": [
|
||||||
|
[109.3280, -0.0280],
|
||||||
|
[109.3330, -0.0270],
|
||||||
|
[109.3380, -0.0260]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'Jalan Pahlawan',
|
||||||
|
'Jalan Kabupaten',
|
||||||
|
540.7,
|
||||||
|
'{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"nama_jalan": "Jalan Pahlawan",
|
||||||
|
"status_jalan": "Jalan Kabupaten",
|
||||||
|
"panjang_meter": 540.7
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "LineString",
|
||||||
|
"coordinates": [
|
||||||
|
[109.3420, -0.0300],
|
||||||
|
[109.3450, -0.0290],
|
||||||
|
[109.3480, -0.0280]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Sample Data Parsil Tanah
|
||||||
|
-- Polygon: koordinat pertama dan terakhir harus sama (closed ring)
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO parsil_tanah (nomor_sertifikat, status_kepemilikan, luas_meter2, geojson, nama_pemilik) VALUES
|
||||||
|
(
|
||||||
|
'SHM-2024-001',
|
||||||
|
'SHM',
|
||||||
|
450.0,
|
||||||
|
'{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"nomor_sertifikat": "SHM-2024-001",
|
||||||
|
"status_kepemilikan": "SHM",
|
||||||
|
"luas_meter2": 450.0,
|
||||||
|
"nama_pemilik": "Budi Santoso"
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[109.3355, -0.0255],
|
||||||
|
[109.3370, -0.0255],
|
||||||
|
[109.3370, -0.0245],
|
||||||
|
[109.3355, -0.0245],
|
||||||
|
[109.3355, -0.0255]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'Budi Santoso'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'HGB-2024-002',
|
||||||
|
'HGB',
|
||||||
|
620.5,
|
||||||
|
'{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"nomor_sertifikat": "HGB-2024-002",
|
||||||
|
"status_kepemilikan": "HGB",
|
||||||
|
"luas_meter2": 620.5,
|
||||||
|
"nama_pemilik": "PT. Maju Jaya"
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[109.3395, -0.0270],
|
||||||
|
[109.3415, -0.0270],
|
||||||
|
[109.3415, -0.0258],
|
||||||
|
[109.3395, -0.0258],
|
||||||
|
[109.3395, -0.0270]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'PT. Maju Jaya'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'HGU-2024-003',
|
||||||
|
'HGU',
|
||||||
|
1200.0,
|
||||||
|
'{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"nomor_sertifikat": "HGU-2024-003",
|
||||||
|
"status_kepemilikan": "HGU",
|
||||||
|
"luas_meter2": 1200.0,
|
||||||
|
"nama_pemilik": "CV. Agro Lestari"
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [[
|
||||||
|
[109.3310, -0.0285],
|
||||||
|
[109.3335, -0.0285],
|
||||||
|
[109.3335, -0.0268],
|
||||||
|
[109.3310, -0.0268],
|
||||||
|
[109.3310, -0.0285]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'CV. Agro Lestari'
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||||
|
jsonResponse(['error' => 'Method tidak didukung'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$admin = requireRole('admin');
|
||||||
|
$db = getDB();
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
$limit = min(100, max(10, (int)($_GET['limit'] ?? 50)));
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
$filterAksi = $_GET['aksi'] ?? null;
|
||||||
|
$filterUser = $_GET['user_id'] ?? null;
|
||||||
|
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($filterAksi) {
|
||||||
|
$where[] = "al.aksi LIKE ?";
|
||||||
|
$params[] = "%$filterAksi%";
|
||||||
|
}
|
||||||
|
if ($filterUser) {
|
||||||
|
$where[] = "al.user_id = ?";
|
||||||
|
$params[] = (int)$filterUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = count($where) > 0 ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
|
// Total count
|
||||||
|
$countSql = "SELECT COUNT(*) FROM audit_logs al $whereClause";
|
||||||
|
$countStmt = $db->prepare($countSql);
|
||||||
|
$countStmt->execute($params);
|
||||||
|
$total = (int)$countStmt->fetchColumn();
|
||||||
|
|
||||||
|
// Data
|
||||||
|
$sql = "
|
||||||
|
SELECT al.*, u.username, u.nama_lengkap
|
||||||
|
FROM audit_logs al
|
||||||
|
LEFT JOIN users u ON u.id = al.user_id
|
||||||
|
$whereClause
|
||||||
|
ORDER BY al.created_at DESC
|
||||||
|
LIMIT $limit OFFSET $offset
|
||||||
|
";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'data' => $rows,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => ceil($total / $limit),
|
||||||
|
]);
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
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');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// LOGIN
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
if ($action === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
$username = trim($body['username'] ?? '');
|
||||||
|
$password = $body['password'] ?? '';
|
||||||
|
|
||||||
|
if (!$username || !$password) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Username dan password wajib diisi'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND is_active = 1");
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$user || !password_verify($password, $user['password'])) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Username atau password salah'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set session
|
||||||
|
$_SESSION['user'] = [
|
||||||
|
'id' => (int)$user['id'],
|
||||||
|
'username' => $user['username'],
|
||||||
|
'nama_lengkap' => $user['nama_lengkap'],
|
||||||
|
'role' => $user['role'],
|
||||||
|
];
|
||||||
|
|
||||||
|
auditLog($db, (int)$user['id'], 'LOGIN', 'users', (int)$user['id']);
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'user' => $_SESSION['user']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// LOGOUT
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
if ($action === 'logout' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$user = getCurrentUser();
|
||||||
|
if ($user) {
|
||||||
|
auditLog($db, $user['id'], 'LOGOUT', 'users', $user['id']);
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// ME — cek session aktif
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
if ($action === 'me' && $_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
$user = getCurrentUser();
|
||||||
|
if (!$user) {
|
||||||
|
jsonResponse(['success' => false, 'logged_in' => false], 200);
|
||||||
|
}
|
||||||
|
jsonResponse(['success' => true, 'logged_in' => true, 'user' => $user]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// REGISTER — admin only
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
if ($action === 'register' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$admin = requireRole('admin');
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
$username = trim($body['username'] ?? '');
|
||||||
|
$password = $body['password'] ?? '';
|
||||||
|
$nama_lengkap = trim($body['nama_lengkap'] ?? '');
|
||||||
|
$role = $body['role'] ?? 'surveyor';
|
||||||
|
|
||||||
|
if (!$username || !$password || !$nama_lengkap) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Semua field wajib diisi'], 422);
|
||||||
|
}
|
||||||
|
if (strlen($password) < 6) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Password minimal 6 karakter'], 422);
|
||||||
|
}
|
||||||
|
if (!in_array($role, ['admin', 'surveyor', 'pemangku'])) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Role tidak valid'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek username unik
|
||||||
|
$check = $db->prepare("SELECT id FROM users WHERE username = ?");
|
||||||
|
$check->execute([$username]);
|
||||||
|
if ($check->fetch()) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Username sudah digunakan'], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashed = password_hash($password, PASSWORD_BCRYPT);
|
||||||
|
$stmt = $db->prepare("INSERT INTO users (username, password, nama_lengkap, role) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$username, $hashed, $nama_lengkap, $role]);
|
||||||
|
$newId = (int)$db->lastInsertId();
|
||||||
|
|
||||||
|
auditLog($db, $admin['id'], 'CREATE_USER', 'users', $newId, null, [
|
||||||
|
'username' => $username, 'nama_lengkap' => $nama_lengkap, 'role' => $role
|
||||||
|
]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true, 'id' => $newId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// LIST USERS — admin only
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
if ($action === 'users' && $_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
requireRole('admin');
|
||||||
|
$rows = $db->query("SELECT id, username, nama_lengkap, role, is_active, created_at FROM users ORDER BY created_at DESC")->fetchAll();
|
||||||
|
jsonResponse(['data' => $rows]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// UPDATE USER — admin only
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
if ($action === 'update' && $_SERVER['REQUEST_METHOD'] === 'PUT') {
|
||||||
|
$admin = requireRole('admin');
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||||
|
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
// Ambil data lama
|
||||||
|
$old = $db->prepare("SELECT id, username, nama_lengkap, role, is_active FROM users WHERE id = ?");
|
||||||
|
$old->execute([$id]);
|
||||||
|
$oldData = $old->fetch();
|
||||||
|
if (!$oldData) jsonResponse(['success' => false, 'error' => 'User tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
$fields = []; $vals = [];
|
||||||
|
|
||||||
|
if (!empty($body['nama_lengkap'])) {
|
||||||
|
$fields[] = "nama_lengkap = ?"; $vals[] = trim($body['nama_lengkap']);
|
||||||
|
}
|
||||||
|
if (!empty($body['role']) && in_array($body['role'], ['admin','surveyor','pemangku'])) {
|
||||||
|
$fields[] = "role = ?"; $vals[] = $body['role'];
|
||||||
|
}
|
||||||
|
if (isset($body['is_active'])) {
|
||||||
|
$fields[] = "is_active = ?"; $vals[] = $body['is_active'] ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (!empty($body['password']) && strlen($body['password']) >= 6) {
|
||||||
|
$fields[] = "password = ?"; $vals[] = password_hash($body['password'], PASSWORD_BCRYPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data diubah'], 422);
|
||||||
|
|
||||||
|
$vals[] = $id;
|
||||||
|
$db->prepare("UPDATE users SET " . implode(',', $fields) . " WHERE id = ?")->execute($vals);
|
||||||
|
|
||||||
|
auditLog($db, $admin['id'], 'UPDATE_USER', 'users', $id, $oldData, $body);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// DELETE (deactivate) USER — admin only
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||||
|
$admin = requireRole('admin');
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||||
|
|
||||||
|
if ($id === $admin['id']) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Tidak bisa menghapus akun sendiri'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->prepare("UPDATE users SET is_active = 0 WHERE id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
auditLog($db, $admin['id'], 'DEACTIVATE_USER', 'users', $id);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(['error' => 'Action tidak valid'], 400);
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
// ── Konfigurasi Database ──────────────────────
|
||||||
|
// Sesuaikan dengan pengaturan XAMPP Anda
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_USER', 'root'); // default XAMPP
|
||||||
|
define('DB_PASS', ''); // default XAMPP (kosong)
|
||||||
|
define('DB_NAME', 'webgis_poverty_mapping');
|
||||||
|
define('DB_CHARSET', 'utf8mb4');
|
||||||
|
|
||||||
|
// ── Upload Config ─────────────────────────────
|
||||||
|
define('UPLOAD_DIR', __DIR__ . '/../uploads/');
|
||||||
|
define('MAX_UPLOAD_SIZE', 5 * 1024 * 1024); // 5MB
|
||||||
|
|
||||||
|
// ── Session ───────────────────────────────────
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDB(): PDO {
|
||||||
|
static $pdo = null;
|
||||||
|
if ($pdo === null) {
|
||||||
|
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=%s', DB_HOST, DB_NAME, DB_CHARSET);
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
die(json_encode(['success' => false, 'error' => 'Koneksi DB gagal: ' . $e->getMessage()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON response helper ──────────────────────
|
||||||
|
function jsonResponse(array $data, int $code = 200): void {
|
||||||
|
http_response_code($code);
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
|
if ($origin) {
|
||||||
|
header('Access-Control-Allow-Origin: ' . $origin);
|
||||||
|
} else {
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
}
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auth Helpers ──────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cek apakah user sudah login.
|
||||||
|
* Return data user atau null.
|
||||||
|
*/
|
||||||
|
function getCurrentUser(): ?array {
|
||||||
|
return $_SESSION['user'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paksa login. Jika belum login, return 401.
|
||||||
|
*/
|
||||||
|
function requireAuth(): array {
|
||||||
|
$user = getCurrentUser();
|
||||||
|
if (!$user) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Unauthorized — silakan login'], 401);
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paksa role tertentu. Jika role tidak sesuai, return 403.
|
||||||
|
* @param array|string $roles Role yang diizinkan
|
||||||
|
*/
|
||||||
|
function requireRole($roles): array {
|
||||||
|
$user = requireAuth();
|
||||||
|
if (is_string($roles)) $roles = [$roles];
|
||||||
|
if (!in_array($user['role'], $roles)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Forbidden — Anda tidak memiliki akses'], 403);
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Audit Log Helper ──────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catat aktivitas ke audit_logs
|
||||||
|
*/
|
||||||
|
function auditLog(PDO $db, int $userId, string $aksi, ?string $targetType = null, ?int $targetId = null, $before = null, $after = null): void {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO audit_logs (user_id, aksi, target_type, target_id, data_before, data_after, ip_address)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$userId,
|
||||||
|
$aksi,
|
||||||
|
$targetType,
|
||||||
|
$targetId,
|
||||||
|
$before ? json_encode($before, JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
$after ? json_encode($after, JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
$_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
// ── Haversine distance (meter) ──────────────
|
||||||
|
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||||
|
$R = 6371000;
|
||||||
|
$dLat = deg2rad($lat2 - $lat1);
|
||||||
|
$dLng = deg2rad($lng2 - $lng1);
|
||||||
|
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
|
||||||
|
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cari rumah ibadah terdekat yang menjangkau koordinat ──
|
||||||
|
// Hanya rumah ibadah yang sudah verified
|
||||||
|
function findNearestIbadah(PDO $db, float $lat, float $lng): ?int {
|
||||||
|
$ibadahs = $db->query("SELECT id, lat, lng, radius FROM rumah_ibadah WHERE status = 'verified'")->fetchAll();
|
||||||
|
$nearest = null;
|
||||||
|
$minDist = PHP_FLOAT_MAX;
|
||||||
|
foreach ($ibadahs as $ib) {
|
||||||
|
$d = haversine($lat, $lng, (float)$ib['lat'], (float)$ib['lng']);
|
||||||
|
if ($d <= $ib['radius'] && $d < $minDist) {
|
||||||
|
$nearest = (int)$ib['id'];
|
||||||
|
$minDist = $d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Re-assign SEMUA warga verified (dipanggil saat radius/ibadah berubah) ──
|
||||||
|
function reassignAllWarga(PDO $db): void {
|
||||||
|
$wargas = $db->query("SELECT id, lat, lng FROM warga_miskin WHERE status = 'verified'")->fetchAll();
|
||||||
|
$stmt = $db->prepare("UPDATE warga_miskin SET ibadah_id = ? WHERE id = ?");
|
||||||
|
foreach ($wargas as $w) {
|
||||||
|
$ibadahId = findNearestIbadah($db, (float)$w['lat'], (float)$w['lng']);
|
||||||
|
$stmt->execute([$ibadahId, $w['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
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');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
|
||||||
|
// ── GET: list semua / satu ───────────────────
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$user = getCurrentUser();
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
jsonResponse($row ?: ['error' => 'Tidak ditemukan'], $row ? 200 : 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin, Surveyor, & Pemangku melihat semua, lainnya hanya verified
|
||||||
|
if ($user && ($user['role'] === 'admin' || $user['role'] === 'surveyor' || $user['role'] === 'pemangku')) {
|
||||||
|
$rows = $db->query("
|
||||||
|
SELECT ri.*,
|
||||||
|
COUNT(wm.id) AS jumlah_kk,
|
||||||
|
COALESCE(SUM(wm.jumlah_anggota),0) AS jumlah_jiwa,
|
||||||
|
u.nama_lengkap AS surveyor_nama
|
||||||
|
FROM rumah_ibadah ri
|
||||||
|
LEFT JOIN warga_miskin wm ON wm.ibadah_id = ri.id AND wm.status = 'verified'
|
||||||
|
LEFT JOIN users u ON u.id = ri.surveyor_id
|
||||||
|
GROUP BY ri.id
|
||||||
|
ORDER BY ri.created_at DESC
|
||||||
|
")->fetchAll();
|
||||||
|
} else {
|
||||||
|
// Pemangku / publik: hanya verified
|
||||||
|
$rows = $db->query("
|
||||||
|
SELECT ri.*,
|
||||||
|
COUNT(wm.id) AS jumlah_kk,
|
||||||
|
COALESCE(SUM(wm.jumlah_anggota),0) AS jumlah_jiwa
|
||||||
|
FROM rumah_ibadah ri
|
||||||
|
LEFT JOIN warga_miskin wm ON wm.ibadah_id = ri.id AND wm.status = 'verified'
|
||||||
|
WHERE ri.status = 'verified'
|
||||||
|
GROUP BY ri.id
|
||||||
|
ORDER BY ri.created_at DESC
|
||||||
|
")->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(['data' => $rows]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POST: tambah ─────────────────────────────
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$user = requireRole(['admin', 'surveyor']);
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
$required = ['nama','jenis','pic','lat','lng'];
|
||||||
|
foreach ($required as $f) {
|
||||||
|
if (empty($body[$f])) jsonResponse(['success'=>false,'error'=>"Field '$f' wajib diisi"], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Surveyor → pending, Admin → verified
|
||||||
|
$status = ($user['role'] === 'admin') ? 'verified' : 'pending';
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, radius, lat, lng, foto_path, surveyor_id, status, kecamatan)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$body['nama'],
|
||||||
|
$body['jenis'],
|
||||||
|
$body['alamat'] ?? '',
|
||||||
|
$body['pic'],
|
||||||
|
$body['no_wa'] ?? '',
|
||||||
|
(int)($body['radius'] ?? 500),
|
||||||
|
(float)$body['lat'],
|
||||||
|
(float)$body['lng'],
|
||||||
|
$body['foto_path'] ?? null,
|
||||||
|
$user['id'],
|
||||||
|
$status,
|
||||||
|
$body['kecamatan'] ?? null,
|
||||||
|
]);
|
||||||
|
$newId = (int)$db->lastInsertId();
|
||||||
|
|
||||||
|
// re-assign warga hanya jika langsung verified
|
||||||
|
if ($status === 'verified') {
|
||||||
|
reassignAllWarga($db);
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLog($db, $user['id'], 'CREATE_IBADAH', 'rumah_ibadah', $newId, null, $body);
|
||||||
|
|
||||||
|
jsonResponse(['success'=>true, 'id'=>$newId, 'status'=>$status]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT: update ──────────────────────────────
|
||||||
|
if ($method === 'PUT') {
|
||||||
|
$user = requireAuth();
|
||||||
|
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
|
||||||
|
|
||||||
|
// Cek permission
|
||||||
|
$old = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
|
||||||
|
$old->execute([$id]);
|
||||||
|
$oldData = $old->fetch();
|
||||||
|
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
// Surveyor hanya bisa edit milik sendiri yang masih pending
|
||||||
|
if ($user['role'] === 'surveyor') {
|
||||||
|
if ((int)$oldData['surveyor_id'] !== $user['id']) {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa edit data milik Anda'], 403);
|
||||||
|
}
|
||||||
|
if ($oldData['status'] !== 'pending') {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Data yang sudah diverifikasi tidak bisa diedit'], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pemangku tidak boleh edit
|
||||||
|
if ($user['role'] === 'pemangku') {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses edit'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
$fields = []; $vals = [];
|
||||||
|
foreach (['nama','jenis','alamat','pic','no_wa','radius','lat','lng','foto_path','kecamatan'] as $f) {
|
||||||
|
if (array_key_exists($f, $body)) {
|
||||||
|
$fields[] = "$f = ?";
|
||||||
|
$vals[] = in_array($f,['lat','lng']) ? (float)$body[$f] : ($f==='radius' ? (int)$body[$f] : $body[$f]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($fields)) jsonResponse(['success'=>false,'error'=>'Tidak ada data diubah'], 422);
|
||||||
|
$vals[] = $id;
|
||||||
|
$db->prepare("UPDATE rumah_ibadah SET ".implode(',',$fields)." WHERE id = ?")->execute($vals);
|
||||||
|
|
||||||
|
// re-assign ulang jika data verified
|
||||||
|
if ($oldData['status'] === 'verified') {
|
||||||
|
reassignAllWarga($db);
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLog($db, $user['id'], 'UPDATE_IBADAH', 'rumah_ibadah', $id, $oldData, $body);
|
||||||
|
|
||||||
|
jsonResponse(['success'=>true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DELETE ───────────────────────────────────
|
||||||
|
if ($method === 'DELETE') {
|
||||||
|
$user = requireAuth();
|
||||||
|
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
|
||||||
|
|
||||||
|
$old = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
|
||||||
|
$old->execute([$id]);
|
||||||
|
$oldData = $old->fetch();
|
||||||
|
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
// Surveyor bisa hapus milik sendiri
|
||||||
|
if ($user['role'] === 'surveyor') {
|
||||||
|
if ((int)$oldData['surveyor_id'] !== $user['id']) {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa hapus data milik Anda'], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($user['role'] === 'pemangku') {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses hapus'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// warga_miskin.ibadah_id akan SET NULL (FK ON DELETE SET NULL)
|
||||||
|
$db->prepare("DELETE FROM rumah_ibadah WHERE id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
// Jika rumah ibadah terverifikasi dihapus, reassign warga sekitar
|
||||||
|
if ($oldData['status'] === 'verified') {
|
||||||
|
reassignAllWarga($db);
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLog($db, $user['id'], 'DELETE_IBADAH', 'rumah_ibadah', $id, $oldData);
|
||||||
|
|
||||||
|
jsonResponse(['success'=>true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(['error'=>'Method tidak didukung'], 405);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$user = getCurrentUser();
|
||||||
|
|
||||||
|
// Statistik utama — hanya data verified (digabung dalam 1 query)
|
||||||
|
$stats = $db->query("
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM rumah_ibadah WHERE status = 'verified') AS ibadah,
|
||||||
|
COUNT(*) AS kk_total,
|
||||||
|
COALESCE(SUM(CASE WHEN ibadah_id IS NOT NULL THEN 1 ELSE 0 END), 0) AS kk_terlayani,
|
||||||
|
COALESCE(SUM(jumlah_anggota), 0) AS jiwa_total,
|
||||||
|
COALESCE(SUM(CASE WHEN ibadah_id IS NOT NULL THEN jumlah_anggota ELSE 0 END), 0) AS jiwa_terlayani,
|
||||||
|
COALESCE(SUM(CASE WHEN bantuan_diterima = 'sembako' THEN 1 ELSE 0 END), 0) AS bantuan_sembako,
|
||||||
|
COALESCE(SUM(CASE WHEN bantuan_diterima = 'beasiswa' THEN 1 ELSE 0 END), 0) AS bantuan_beasiswa,
|
||||||
|
COALESCE(SUM(CASE WHEN bantuan_diterima = 'modal' THEN 1 ELSE 0 END), 0) AS bantuan_modal,
|
||||||
|
COALESCE(SUM(CASE WHEN bantuan_diterima = 'tunai' THEN 1 ELSE 0 END), 0) AS bantuan_tunai,
|
||||||
|
COALESCE(SUM(CASE WHEN bantuan_diterima = 'tidak_ada' THEN 1 ELSE 0 END), 0) AS bantuan_tidak_ada
|
||||||
|
FROM warga_miskin
|
||||||
|
WHERE status = 'verified'
|
||||||
|
")->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$totalIbadah = (int)($stats['ibadah'] ?? 0);
|
||||||
|
$totalWarga = (int)($stats['kk_total'] ?? 0);
|
||||||
|
$totalCovered = (int)($stats['kk_terlayani'] ?? 0);
|
||||||
|
$totalJiwa = (int)($stats['jiwa_total'] ?? 0);
|
||||||
|
$jiwaLayan = (int)($stats['jiwa_terlayani'] ?? 0);
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'ibadah' => $totalIbadah,
|
||||||
|
'kk_total' => $totalWarga,
|
||||||
|
'kk_terlayani' => $totalCovered,
|
||||||
|
'kk_belum' => $totalWarga - $totalCovered,
|
||||||
|
'jiwa_total' => $totalJiwa,
|
||||||
|
'jiwa_terlayani' => $jiwaLayan,
|
||||||
|
'bantuan_sembako' => (int)($stats['bantuan_sembako'] ?? 0),
|
||||||
|
'bantuan_beasiswa' => (int)($stats['bantuan_beasiswa'] ?? 0),
|
||||||
|
'bantuan_modal' => (int)($stats['bantuan_modal'] ?? 0),
|
||||||
|
'bantuan_tunai' => (int)($stats['bantuan_tunai'] ?? 0),
|
||||||
|
'bantuan_tidak_ada' => (int)($stats['bantuan_tidak_ada'] ?? 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Data pending — hanya untuk admin (digabung dalam 1 query)
|
||||||
|
if ($user && $user['role'] === 'admin') {
|
||||||
|
$adminStats = $db->query("
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM rumah_ibadah WHERE status = 'pending') AS pending_ibadah,
|
||||||
|
(SELECT COUNT(*) FROM warga_miskin WHERE status = 'pending') AS pending_warga,
|
||||||
|
(SELECT COUNT(*) FROM users WHERE is_active = 1) AS total_users
|
||||||
|
")->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$response['pending_ibadah'] = (int)($adminStats['pending_ibadah'] ?? 0);
|
||||||
|
$response['pending_warga'] = (int)($adminStats['pending_warga'] ?? 0);
|
||||||
|
$response['total_users'] = (int)($adminStats['total_users'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data pending milik surveyor (digabung dalam 1 query)
|
||||||
|
if ($user && $user['role'] === 'surveyor') {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM warga_miskin WHERE surveyor_id = ? AND status = 'pending') AS my_pending_warga,
|
||||||
|
(SELECT COUNT(*) FROM rumah_ibadah WHERE surveyor_id = ? AND status = 'pending') AS my_pending_ibadah
|
||||||
|
");
|
||||||
|
$stmt->execute([$user['id'], $user['id']]);
|
||||||
|
$surveyorStats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$response['my_pending_warga'] = (int)($surveyorStats['my_pending_warga'] ?? 0);
|
||||||
|
$response['my_pending_ibadah'] = (int)($surveyorStats['my_pending_ibadah'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse($response);
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
jsonResponse(['error' => 'Method tidak didukung'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = requireRole(['admin', 'surveyor']);
|
||||||
|
|
||||||
|
// ── Validasi file ────────────────────────────
|
||||||
|
if (!isset($_FILES['foto']) || $_FILES['foto']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'File foto wajib diunggah'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['foto'];
|
||||||
|
|
||||||
|
// Cek ukuran
|
||||||
|
if ($file['size'] > MAX_UPLOAD_SIZE) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Ukuran file melebihi batas 5MB'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek tipe file
|
||||||
|
$allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
|
||||||
|
if (!in_array($mimeType, $allowedTypes)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Hanya file JPEG dan PNG yang diizinkan'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Buat folder uploads jika belum ada ───────
|
||||||
|
if (!is_dir(UPLOAD_DIR)) {
|
||||||
|
mkdir(UPLOAD_DIR, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generate nama file unik ──────────────────
|
||||||
|
$ext = ($mimeType === 'image/png') ? 'png' : 'jpg';
|
||||||
|
$filename = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||||
|
$destPath = UPLOAD_DIR . $filename;
|
||||||
|
$webPath = 'uploads/' . $filename;
|
||||||
|
|
||||||
|
// ── Pindahkan file ───────────────────────────
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Gagal menyimpan file'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Extract EXIF GPS ─────────────────────────
|
||||||
|
$lat = null;
|
||||||
|
$lng = null;
|
||||||
|
$hasGps = false;
|
||||||
|
|
||||||
|
if ($mimeType === 'image/jpeg' || $mimeType === 'image/jpg') {
|
||||||
|
$exif = @exif_read_data($destPath, 'GPS', true);
|
||||||
|
if ($exif && isset($exif['GPS']['GPSLatitude']) && isset($exif['GPS']['GPSLongitude'])) {
|
||||||
|
$lat = exifGpsToDecimal(
|
||||||
|
$exif['GPS']['GPSLatitude'],
|
||||||
|
$exif['GPS']['GPSLatitudeRef'] ?? 'N'
|
||||||
|
);
|
||||||
|
$lng = exifGpsToDecimal(
|
||||||
|
$exif['GPS']['GPSLongitude'],
|
||||||
|
$exif['GPS']['GPSLongitudeRef'] ?? 'E'
|
||||||
|
);
|
||||||
|
$hasGps = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLog(getDB(), $user['id'], 'UPLOAD_FOTO', null, null, null, [
|
||||||
|
'filename' => $filename, 'has_gps' => $hasGps
|
||||||
|
]);
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'foto_path' => $webPath,
|
||||||
|
'lat' => $lat,
|
||||||
|
'lng' => $lng,
|
||||||
|
'has_gps' => $hasGps,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
// EXIF GPS Helper
|
||||||
|
// ══════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konversi EXIF GPS coordinate (DMS) ke desimal
|
||||||
|
* @param array $coord Array of 3 fractions [degrees, minutes, seconds]
|
||||||
|
* @param string $ref Hemisphere reference (N/S/E/W)
|
||||||
|
*/
|
||||||
|
function exifGpsToDecimal(array $coord, string $ref): float {
|
||||||
|
$degrees = exifFractionToFloat($coord[0]);
|
||||||
|
$minutes = exifFractionToFloat($coord[1]);
|
||||||
|
$seconds = exifFractionToFloat($coord[2]);
|
||||||
|
|
||||||
|
$decimal = $degrees + ($minutes / 60) + ($seconds / 3600);
|
||||||
|
|
||||||
|
if ($ref === 'S' || $ref === 'W') {
|
||||||
|
$decimal *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($decimal, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konversi fraction string "num/den" ke float
|
||||||
|
*/
|
||||||
|
function exifFractionToFloat(string $fraction): float {
|
||||||
|
$parts = explode('/', $fraction);
|
||||||
|
if (count($parts) === 2 && (float)$parts[1] !== 0.0) {
|
||||||
|
return (float)$parts[0] / (float)$parts[1];
|
||||||
|
}
|
||||||
|
return (float)$parts[0];
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, PUT, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$type = $_GET['type'] ?? ''; // 'ibadah' atau 'warga'
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
|
||||||
|
if (!in_array($type, ['ibadah', 'warga'])) {
|
||||||
|
jsonResponse(['error' => 'Parameter type harus "ibadah" atau "warga"'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $type === 'ibadah' ? 'rumah_ibadah' : 'warga_miskin';
|
||||||
|
|
||||||
|
// ── GET: list data pending ───────────────────
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$admin = requireRole('admin');
|
||||||
|
|
||||||
|
$status = $_GET['status'] ?? 'pending';
|
||||||
|
if (!in_array($status, ['pending', 'verified', 'rejected'])) $status = 'pending';
|
||||||
|
|
||||||
|
if ($type === 'ibadah') {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT ri.*, u.nama_lengkap AS surveyor_nama
|
||||||
|
FROM rumah_ibadah ri
|
||||||
|
LEFT JOIN users u ON u.id = ri.surveyor_id
|
||||||
|
WHERE ri.status = ?
|
||||||
|
ORDER BY ri.created_at DESC
|
||||||
|
");
|
||||||
|
} else {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT wm.*,
|
||||||
|
ri.nama AS ibadah_nama,
|
||||||
|
u.nama_lengkap AS surveyor_nama
|
||||||
|
FROM warga_miskin wm
|
||||||
|
LEFT JOIN rumah_ibadah ri ON ri.id = wm.ibadah_id
|
||||||
|
LEFT JOIN users u ON u.id = wm.surveyor_id
|
||||||
|
WHERE wm.status = ?
|
||||||
|
ORDER BY wm.created_at DESC
|
||||||
|
");
|
||||||
|
}
|
||||||
|
$stmt->execute([$status]);
|
||||||
|
jsonResponse(['data' => $stmt->fetchAll()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT: approve / reject ────────────────────
|
||||||
|
if ($method === 'PUT') {
|
||||||
|
$admin = requireRole('admin');
|
||||||
|
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
|
||||||
|
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
$newStatus = $body['status'] ?? '';
|
||||||
|
if (!in_array($newStatus, ['verified', 'rejected'])) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Status harus "verified" atau "rejected"'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambil data lama
|
||||||
|
$old = $db->prepare("SELECT * FROM $table WHERE id = ?");
|
||||||
|
$old->execute([$id]);
|
||||||
|
$oldData = $old->fetch();
|
||||||
|
if (!$oldData) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
$alasan = ($newStatus === 'rejected') ? ($body['alasan_penolakan'] ?? '') : null;
|
||||||
|
$stmt = $db->prepare("UPDATE $table SET status = ?, alasan_penolakan = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$newStatus, $alasan, $id]);
|
||||||
|
|
||||||
|
// Jika diverifikasi, proses spasial
|
||||||
|
if ($newStatus === 'verified') {
|
||||||
|
if ($type === 'warga') {
|
||||||
|
// Cari ibadah terdekat untuk warga yang baru verified
|
||||||
|
$ibadahId = findNearestIbadah($db, (float)$oldData['lat'], (float)$oldData['lng']);
|
||||||
|
$db->prepare("UPDATE warga_miskin SET ibadah_id = ? WHERE id = ?")->execute([$ibadahId, $id]);
|
||||||
|
} elseif ($type === 'ibadah') {
|
||||||
|
// Ibadah baru verified → re-assign semua warga
|
||||||
|
reassignAllWarga($db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$aksi = ($newStatus === 'verified') ? 'VERIFY_' : 'REJECT_';
|
||||||
|
$aksi .= strtoupper($type);
|
||||||
|
auditLog($db, $admin['id'], $aksi, $table, $id, $oldData, [
|
||||||
|
'status' => $newStatus, 'alasan_penolakan' => $alasan
|
||||||
|
]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true, 'status' => $newStatus]);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(['error' => 'Method tidak didukung'], 405);
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
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');
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
|
||||||
|
|
||||||
|
$db = getDB();
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||||
|
|
||||||
|
// ── GET ───────────────────────────────────────
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$user = getCurrentUser();
|
||||||
|
|
||||||
|
// Filter tambahan: kecamatan
|
||||||
|
$filterKec = $_GET['kecamatan'] ?? null;
|
||||||
|
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($filterKec) { $where[] = "wm.kecamatan = ?"; $params[] = $filterKec; }
|
||||||
|
|
||||||
|
if ($user && ($user['role'] === 'admin' || $user['role'] === 'surveyor' || $user['role'] === 'pemangku')) {
|
||||||
|
// Admin, Surveyor, & Pemangku lihat semua data (untuk mencegah input ganda & analisis lengkap)
|
||||||
|
$statusFilter = "";
|
||||||
|
} else {
|
||||||
|
// Publik: hanya verified
|
||||||
|
$statusFilter = "AND wm.status = 'verified'";
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = count($where) > 0 ? 'WHERE ' . implode(' AND ', $where) . ' ' . $statusFilter : ($statusFilter ? 'WHERE 1=1 ' . $statusFilter : '');
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT wm.*,
|
||||||
|
ri.nama AS ibadah_nama,
|
||||||
|
ri.jenis AS ibadah_jenis,
|
||||||
|
u.nama_lengkap AS surveyor_nama
|
||||||
|
FROM warga_miskin wm
|
||||||
|
LEFT JOIN rumah_ibadah ri ON ri.id = wm.ibadah_id
|
||||||
|
LEFT JOIN users u ON u.id = wm.surveyor_id
|
||||||
|
$whereClause
|
||||||
|
ORDER BY wm.created_at DESC
|
||||||
|
";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
|
||||||
|
jsonResponse(['data' => $rows]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POST: tambah warga ───────────────────────
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$user = requireRole(['admin', 'surveyor']);
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
foreach (['nama_kk','lat','lng'] as $f) {
|
||||||
|
if (empty($body[$f])) jsonResponse(['success'=>false,'error'=>"Field '$f' wajib diisi"], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validasi NIK: harus 16 digit angka jika diisi
|
||||||
|
$nik = $body['nik'] ?? null;
|
||||||
|
if ($nik !== null && $nik !== '') {
|
||||||
|
$nik = trim($nik);
|
||||||
|
if (!preg_match('/^\d{16}$/', $nik)) {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'NIK harus tepat 16 digit angka'], 422);
|
||||||
|
}
|
||||||
|
// Cek duplikasi NIK
|
||||||
|
$chk = $db->prepare("SELECT id FROM warga_miskin WHERE nik = ?");
|
||||||
|
$chk->execute([$nik]);
|
||||||
|
if ($chk->fetch()) {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'NIK Kepala Keluarga sudah terdaftar di sistem'], 422);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$nik = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lat = (float)$body['lat'];
|
||||||
|
$lng = (float)$body['lng'];
|
||||||
|
|
||||||
|
// Surveyor → pending, Admin → verified
|
||||||
|
$status = ($user['role'] === 'admin') ? 'verified' : 'pending';
|
||||||
|
|
||||||
|
// otomatis cari rumah ibadah terdekat (hanya jika verified)
|
||||||
|
$ibadahId = ($status === 'verified') ? findNearestIbadah($db, $lat, $lng) : null;
|
||||||
|
|
||||||
|
$bantuan = $body['bantuan_diterima'] ?? 'tidak_ada';
|
||||||
|
if (!in_array($bantuan, ['tidak_ada', 'sembako', 'beasiswa', 'modal', 'tunai'])) {
|
||||||
|
$bantuan = 'tidak_ada';
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO warga_miskin (nama_kk, jumlah_anggota, keterangan, lat, lng, ibadah_id, foto_path, surveyor_id, status, alamat, nik, kecamatan, bantuan_diterima)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$body['nama_kk'],
|
||||||
|
(int)($body['jumlah_anggota'] ?? 1),
|
||||||
|
$body['keterangan'] ?? '',
|
||||||
|
$lat, $lng, $ibadahId,
|
||||||
|
$body['foto_path'] ?? null,
|
||||||
|
$user['id'],
|
||||||
|
$status,
|
||||||
|
$body['alamat'] ?? null,
|
||||||
|
$nik,
|
||||||
|
$body['kecamatan'] ?? null,
|
||||||
|
$bantuan,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newId = (int)$db->lastInsertId();
|
||||||
|
|
||||||
|
// ambil nama ibadah untuk respon
|
||||||
|
$ibadahNama = null;
|
||||||
|
if ($ibadahId) {
|
||||||
|
$r = $db->prepare("SELECT nama FROM rumah_ibadah WHERE id = ?");
|
||||||
|
$r->execute([$ibadahId]);
|
||||||
|
$ibadahNama = $r->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLog($db, $user['id'], 'CREATE_WARGA', 'warga_miskin', $newId, null, $body);
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'id' => $newId,
|
||||||
|
'status' => $status,
|
||||||
|
'ibadah_id' => $ibadahId,
|
||||||
|
'ibadah_nama' => $ibadahNama,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT: update warga ────────────────────────
|
||||||
|
if ($method === 'PUT') {
|
||||||
|
$user = requireAuth();
|
||||||
|
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
|
||||||
|
|
||||||
|
$old = $db->prepare("SELECT * FROM warga_miskin WHERE id = ?");
|
||||||
|
$old->execute([$id]);
|
||||||
|
$oldData = $old->fetch();
|
||||||
|
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
// Surveyor hanya bisa edit milik sendiri yang masih pending
|
||||||
|
if ($user['role'] === 'surveyor') {
|
||||||
|
if ((int)$oldData['surveyor_id'] !== $user['id']) {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa edit data milik Anda'], 403);
|
||||||
|
}
|
||||||
|
if ($oldData['status'] !== 'pending') {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Data yang sudah diverifikasi tidak bisa diedit'], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($user['role'] === 'pemangku') {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses edit'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
|
||||||
|
// Validasi NIK jika diubah
|
||||||
|
if (array_key_exists('nik', $body) && $body['nik'] !== null && $body['nik'] !== '') {
|
||||||
|
$nikVal = trim($body['nik']);
|
||||||
|
if (!preg_match('/^\d{16}$/', $nikVal)) {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'NIK harus tepat 16 digit angka'], 422);
|
||||||
|
}
|
||||||
|
// Cek duplikasi NIK (kecuali data sendiri)
|
||||||
|
$chk = $db->prepare("SELECT id FROM warga_miskin WHERE nik = ? AND id != ?");
|
||||||
|
$chk->execute([$nikVal, $id]);
|
||||||
|
if ($chk->fetch()) {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'NIK Kepala Keluarga sudah terdaftar di sistem'], 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = []; $vals = [];
|
||||||
|
foreach (['nama_kk','jumlah_anggota','keterangan','lat','lng','foto_path','alamat','nik','kecamatan','bantuan_diterima'] as $f) {
|
||||||
|
if (array_key_exists($f, $body)) {
|
||||||
|
$fields[] = "$f = ?";
|
||||||
|
if ($f === 'nik') {
|
||||||
|
$nikVal = $body['nik'] !== null ? trim($body['nik']) : '';
|
||||||
|
$vals[] = ($nikVal === '') ? null : $nikVal;
|
||||||
|
} else {
|
||||||
|
$vals[] = in_array($f,['lat','lng']) ? (float)$body[$f]
|
||||||
|
: ($f==='jumlah_anggota' ? (int)$body[$f] : $body[$f]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($fields)) jsonResponse(['success'=>false,'error'=>'Tidak ada data diubah'], 422);
|
||||||
|
|
||||||
|
// jika koordinat berubah dan data verified, cari ulang ibadah terdekat
|
||||||
|
if ($oldData['status'] === 'verified' && (array_key_exists('lat', $body) || array_key_exists('lng', $body))) {
|
||||||
|
$lat = array_key_exists('lat',$body) ? (float)$body['lat'] : (float)$oldData['lat'];
|
||||||
|
$lng = array_key_exists('lng',$body) ? (float)$body['lng'] : (float)$oldData['lng'];
|
||||||
|
$ibadahId = findNearestIbadah($db, $lat, $lng);
|
||||||
|
$fields[] = "ibadah_id = ?";
|
||||||
|
$vals[] = $ibadahId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$vals[] = $id;
|
||||||
|
$db->prepare("UPDATE warga_miskin SET ".implode(',',$fields)." WHERE id = ?")->execute($vals);
|
||||||
|
|
||||||
|
auditLog($db, $user['id'], 'UPDATE_WARGA', 'warga_miskin', $id, $oldData, $body);
|
||||||
|
|
||||||
|
jsonResponse(['success'=>true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DELETE ───────────────────────────────────
|
||||||
|
if ($method === 'DELETE') {
|
||||||
|
$user = requireAuth();
|
||||||
|
if (!$id) jsonResponse(['success'=>false,'error'=>'ID diperlukan'], 400);
|
||||||
|
|
||||||
|
$old = $db->prepare("SELECT * FROM warga_miskin WHERE id = ?");
|
||||||
|
$old->execute([$id]);
|
||||||
|
$oldData = $old->fetch();
|
||||||
|
if (!$oldData) jsonResponse(['success'=>false,'error'=>'Tidak ditemukan'], 404);
|
||||||
|
|
||||||
|
// Surveyor bisa hapus milik sendiri
|
||||||
|
if ($user['role'] === 'surveyor') {
|
||||||
|
if ((int)$oldData['surveyor_id'] !== $user['id']) {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Anda hanya bisa hapus data milik Anda'], 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($user['role'] === 'pemangku') {
|
||||||
|
jsonResponse(['success'=>false,'error'=>'Anda tidak memiliki akses hapus'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->prepare("DELETE FROM warga_miskin WHERE id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
auditLog($db, $user['id'], 'DELETE_WARGA', 'warga_miskin', $id, $oldData);
|
||||||
|
|
||||||
|
jsonResponse(['success'=>true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(['error'=>'Method tidak didukung'], 405);
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
CREATE DATABASE IF NOT EXISTS webgis_poverty_mapping
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE webgis_poverty_mapping;
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
nama_lengkap VARCHAR(150) NOT NULL,
|
||||||
|
role ENUM('admin','surveyor','pemangku') NOT NULL DEFAULT 'surveyor',
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Admin default — password: admin123
|
||||||
|
-- Hash di-generate dengan password_hash('admin123', PASSWORD_BCRYPT)
|
||||||
|
INSERT INTO users (username, password, nama_lengkap, role) VALUES
|
||||||
|
('admin', '$2y$10$Xb5FU6uo67rVETXAlSYJ..wn8IudyYwGzUbyMe8yQ1TLFlvt.S9F6', 'Administrator', 'admin');
|
||||||
|
|
||||||
|
CREATE TABLE rumah_ibadah (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(200) NOT NULL,
|
||||||
|
jenis ENUM('masjid','gereja','pura','klenteng','vihara','lain') NOT NULL DEFAULT 'masjid',
|
||||||
|
alamat TEXT,
|
||||||
|
pic VARCHAR(150) NOT NULL,
|
||||||
|
no_wa VARCHAR(30) DEFAULT NULL,
|
||||||
|
radius INT NOT NULL DEFAULT 500, -- meter
|
||||||
|
lat DOUBLE NOT NULL,
|
||||||
|
lng DOUBLE NOT NULL,
|
||||||
|
foto_path VARCHAR(255) DEFAULT NULL,
|
||||||
|
surveyor_id INT DEFAULT NULL,
|
||||||
|
status ENUM('pending','verified','rejected') NOT NULL DEFAULT 'verified',
|
||||||
|
alasan_penolakan TEXT DEFAULT NULL,
|
||||||
|
kecamatan VARCHAR(100) DEFAULT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT fk_ri_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE warga_miskin (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_kk VARCHAR(200) NOT NULL,
|
||||||
|
nik VARCHAR(16) DEFAULT NULL,
|
||||||
|
jumlah_anggota INT NOT NULL DEFAULT 1,
|
||||||
|
keterangan TEXT,
|
||||||
|
alamat TEXT DEFAULT NULL,
|
||||||
|
kecamatan VARCHAR(100) DEFAULT NULL,
|
||||||
|
lat DOUBLE NOT NULL,
|
||||||
|
lng DOUBLE NOT NULL,
|
||||||
|
ibadah_id INT DEFAULT NULL, -- NULL = belum terjangkau
|
||||||
|
foto_path VARCHAR(255) DEFAULT NULL,
|
||||||
|
surveyor_id INT DEFAULT NULL,
|
||||||
|
status ENUM('pending','verified','rejected') NOT NULL DEFAULT 'verified',
|
||||||
|
alasan_penolakan TEXT DEFAULT NULL,
|
||||||
|
bantuan_diterima ENUM('sembako','beasiswa','modal','tunai','tidak_ada') NOT NULL DEFAULT 'tidak_ada',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT uq_warga_nik UNIQUE (nik),
|
||||||
|
CONSTRAINT fk_wm_ibadah FOREIGN KEY (ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT fk_wm_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE audit_logs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
aksi VARCHAR(100) NOT NULL,
|
||||||
|
target_type VARCHAR(50) DEFAULT NULL,
|
||||||
|
target_id INT DEFAULT NULL,
|
||||||
|
data_before JSON DEFAULT NULL,
|
||||||
|
data_after JSON DEFAULT NULL,
|
||||||
|
ip_address VARCHAR(45) DEFAULT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT fk_audit_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE INDEX idx_ri_status ON rumah_ibadah(status);
|
||||||
|
CREATE INDEX idx_ri_kecamatan ON rumah_ibadah(kecamatan);
|
||||||
|
CREATE INDEX idx_wm_status ON warga_miskin(status);
|
||||||
|
CREATE INDEX idx_wm_kecamatan ON warga_miskin(kecamatan);
|
||||||
|
CREATE INDEX idx_wm_ibadah ON warga_miskin(ibadah_id);
|
||||||
|
CREATE INDEX idx_audit_user ON audit_logs(user_id);
|
||||||
|
CREATE INDEX idx_audit_aksi ON audit_logs(aksi);
|
||||||
|
CREATE INDEX idx_audit_created ON audit_logs(created_at);
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
# Hanya izinkan akses file gambar
|
||||||
|
<FilesMatch "\.(jpg|jpeg|png|gif|webp)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Blok semua file lain
|
||||||
|
<FilesMatch "\.php$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Disable directory listing
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Disable PHP execution
|
||||||
|
php_flag engine off
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================
|
||||||
|
// api.php — API Backend WebGIS SPBU
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(0);
|
||||||
|
|
||||||
|
register_shutdown_function(function() {
|
||||||
|
$err = error_get_last();
|
||||||
|
if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||||
|
if (!headers_sent()) header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'PHP Error: ' . $err['message']]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
// ── GET ALL ───────────────────────────────────────────
|
||||||
|
if ($action === 'all') {
|
||||||
|
$conn = getConnection();
|
||||||
|
|
||||||
|
$check = $conn->query("SHOW TABLES LIKE 'spbu'");
|
||||||
|
if ($check->num_rows === 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => "Tabel 'spbu' tidak ditemukan. Pastikan Anda sudah import file database.sql"]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $conn->query(
|
||||||
|
"SELECT id, nama, nomor_spbu, status, latitude, longitude,
|
||||||
|
DATE_FORMAT(created_at, '%d %b %Y %H:%i') AS created_at
|
||||||
|
FROM spbu ORDER BY created_at DESC"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Query error: ' . $conn->error]);
|
||||||
|
$conn->close(); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
while ($row = $result->fetch_assoc()) $rows[] = $row;
|
||||||
|
echo json_encode(['success' => true, 'data' => $rows]);
|
||||||
|
$conn->close();
|
||||||
|
|
||||||
|
// ── SAVE ──────────────────────────────────────────────
|
||||||
|
} elseif ($action === 'save') {
|
||||||
|
$nama = trim($_POST['nama'] ?? '');
|
||||||
|
$nomor_spbu = trim($_POST['nomor_spbu'] ?? '');
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
|
$latitude = $_POST['latitude'] ?? '';
|
||||||
|
$longitude = $_POST['longitude'] ?? '';
|
||||||
|
|
||||||
|
if (!$nama || !$nomor_spbu || !$status || $latitude === '' || $longitude === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Semua field wajib diisi']); exit;
|
||||||
|
}
|
||||||
|
if (!in_array($status, ['buka 24 jam', 'tidak'])) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Status tidak valid']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lat = (double) $latitude;
|
||||||
|
$lng = (double) $longitude;
|
||||||
|
|
||||||
|
$conn = getConnection();
|
||||||
|
$stmt = $conn->prepare("INSERT INTO spbu (nama, nomor_spbu, status, latitude, longitude) VALUES (?,?,?,?,?)");
|
||||||
|
if (!$stmt) { echo json_encode(['success' => false, 'message' => 'Prepare error: ' . $conn->error]); exit; }
|
||||||
|
$stmt->bind_param('sssdd', $nama, $nomor_spbu, $status, $lat, $lng);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'SPBU berhasil disimpan', 'id' => $stmt->insert_id]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Execute error: ' . $conn->error]);
|
||||||
|
}
|
||||||
|
$stmt->close(); $conn->close();
|
||||||
|
|
||||||
|
// ── UPDATE ────────────────────────────────────────────
|
||||||
|
} elseif ($action === 'update') {
|
||||||
|
$id = (int) ($_POST['id'] ?? 0);
|
||||||
|
$nama = trim($_POST['nama'] ?? '');
|
||||||
|
$nomor_spbu = trim($_POST['nomor_spbu'] ?? '');
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
|
$latitude = $_POST['latitude'] ?? '';
|
||||||
|
$longitude = $_POST['longitude'] ?? '';
|
||||||
|
|
||||||
|
if (!$id || !$nama || !$nomor_spbu || !$status || $latitude === '' || $longitude === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Semua field wajib diisi']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lat = (double) $latitude;
|
||||||
|
$lng = (double) $longitude;
|
||||||
|
|
||||||
|
$conn = getConnection();
|
||||||
|
$stmt = $conn->prepare("UPDATE spbu SET nama=?, nomor_spbu=?, status=?, latitude=?, longitude=? WHERE id=?");
|
||||||
|
if (!$stmt) { echo json_encode(['success' => false, 'message' => 'Prepare error: ' . $conn->error]); exit; }
|
||||||
|
$stmt->bind_param('sssddi', $nama, $nomor_spbu, $status, $lat, $lng, $id);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Data berhasil diupdate']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Execute error: ' . $conn->error]);
|
||||||
|
}
|
||||||
|
$stmt->close(); $conn->close();
|
||||||
|
|
||||||
|
// ── UPDATE KOORDINAT SAJA ─────────────────────────────
|
||||||
|
} elseif ($action === 'update_coords') {
|
||||||
|
$id = (int) ($_POST['id'] ?? 0);
|
||||||
|
$latitude = $_POST['latitude'] ?? '';
|
||||||
|
$longitude = $_POST['longitude'] ?? '';
|
||||||
|
|
||||||
|
if (!$id || $latitude === '' || $longitude === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lat = (double) $latitude;
|
||||||
|
$lng = (double) $longitude;
|
||||||
|
|
||||||
|
$conn = getConnection();
|
||||||
|
$stmt = $conn->prepare("UPDATE spbu SET latitude=?, longitude=? WHERE id=?");
|
||||||
|
if (!$stmt) { echo json_encode(['success' => false, 'message' => 'Prepare error: ' . $conn->error]); exit; }
|
||||||
|
$stmt->bind_param('ddi', $lat, $lng, $id);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Koordinat berhasil diupdate']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Execute error: ' . $conn->error]);
|
||||||
|
}
|
||||||
|
$stmt->close(); $conn->close();
|
||||||
|
|
||||||
|
// ── DELETE ────────────────────────────────────────────
|
||||||
|
} elseif ($action === 'delete') {
|
||||||
|
$id = (int) ($_POST['id'] ?? 0);
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'message' => 'ID tidak valid']); exit; }
|
||||||
|
|
||||||
|
$conn = getConnection();
|
||||||
|
$stmt = $conn->prepare("DELETE FROM spbu WHERE id=?");
|
||||||
|
if (!$stmt) { echo json_encode(['success' => false, 'message' => 'Prepare error: ' . $conn->error]); exit; }
|
||||||
|
$stmt->bind_param('i', $id);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Data berhasil dihapus']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Execute error: ' . $conn->error]);
|
||||||
|
}
|
||||||
|
$stmt->close(); $conn->close();
|
||||||
|
|
||||||
|
// ── CEK KONEKSI (debug) ───────────────────────────────
|
||||||
|
} elseif ($action === 'ping') {
|
||||||
|
$conn = getConnection();
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Koneksi OK', 'db' => DB_NAME]);
|
||||||
|
$conn->close();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Action tidak dikenali: ' . $action]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================
|
||||||
|
// config.php — Konfigurasi Database SPBU
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_USER', 'root'); // Ganti jika perlu
|
||||||
|
define('DB_PASS', ''); // Ganti jika perlu
|
||||||
|
define('DB_NAME', 'webgis_spbu');
|
||||||
|
|
||||||
|
function getConnection() {
|
||||||
|
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
|
||||||
|
$conn->set_charset('utf8mb4');
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
http_response_code(500);
|
||||||
|
die(json_encode(['success' => false, 'message' => 'Koneksi gagal: ' . $conn->connect_error]));
|
||||||
|
}
|
||||||
|
return $conn;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- WebGIS SPBU — Database Setup
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS webgis_spbu CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE webgis_spbu;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS spbu (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama VARCHAR(255) NOT NULL,
|
||||||
|
nomor_spbu VARCHAR(100) NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'tidak',
|
||||||
|
latitude DOUBLE NOT NULL,
|
||||||
|
longitude DOUBLE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Contoh data awal
|
||||||
|
INSERT INTO spbu (nama, nomor_spbu, status, latitude, longitude) VALUES
|
||||||
|
('SPBU Pontianak Kota', '64.761.01', 'buka 24 jam', -0.0263, 109.3425),
|
||||||
|
('SPBU Sungai Raya', '64.761.02', 'tidak', -0.0512, 109.3610),
|
||||||
|
('SPBU Ahmad Yani', '64.761.03', 'buka 24 jam', -0.0198, 109.3580);
|
||||||
+1393
File diff suppressed because it is too large
Load Diff
+1177
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user